add more mutation queries and fix backend to wait for updates
This commit is contained in:
parent
03116937ad
commit
8f3b149034
10 changed files with 501 additions and 392 deletions
|
|
@ -1,31 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
ChevronDown,
|
||||
Filter,
|
||||
Search,
|
||||
X,
|
||||
Loader2,
|
||||
Plus,
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import { Filter, X, Loader2, Plus, Save } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useGetFiltersSearchQuery,
|
||||
type KnowledgeFilter,
|
||||
} from "@/src/app/api/queries/useGetFiltersSearchQuery";
|
||||
import { useCreateFilter } from "@/src/app/api/mutations/useCreateFilter";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
|
@ -56,43 +47,22 @@ export function KnowledgeFilterList({
|
|||
const [createDescription, setCreateDescription] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useGetFiltersSearchQuery(searchQuery, 20, { enabled: true });
|
||||
const filters: KnowledgeFilter[] = (data ?? []) as KnowledgeFilter[];
|
||||
const { data, isFetching: loading } = useGetFiltersSearchQuery(
|
||||
searchQuery,
|
||||
20
|
||||
);
|
||||
|
||||
const deleteFilter = async (filterId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const filters = data || [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-filter/${filterId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// If this was the selected filter, clear selection
|
||||
if (selectedFilter?.id === filterId) {
|
||||
onFilterSelect(null);
|
||||
}
|
||||
// Refresh list
|
||||
refetch();
|
||||
} else {
|
||||
console.error("Failed to delete knowledge filter");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting knowledge filter:", error);
|
||||
}
|
||||
};
|
||||
const createFilterMutation = useCreateFilter();
|
||||
|
||||
const handleFilterSelect = (filter: KnowledgeFilter) => {
|
||||
onFilterSelect(filter);
|
||||
};
|
||||
|
||||
const handleClearFilter = () => {
|
||||
onFilterSelect(null);
|
||||
};
|
||||
// const handleClearFilter = () => {
|
||||
// onFilterSelect(null);
|
||||
// };
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setShowCreateModal(true);
|
||||
|
|
@ -115,43 +85,19 @@ export function KnowledgeFilterList({
|
|||
scoreThreshold: 0,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/knowledge-filter", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: createName.trim(),
|
||||
description: createDescription.trim(),
|
||||
queryData: JSON.stringify(defaultFilterData),
|
||||
}),
|
||||
const result = await createFilterMutation.mutateAsync({
|
||||
name: createName.trim(),
|
||||
description: createDescription.trim(),
|
||||
queryData: JSON.stringify(defaultFilterData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
// Create the new filter object
|
||||
const newFilter: KnowledgeFilter = {
|
||||
id: result.filter.id,
|
||||
name: createName.trim(),
|
||||
description: createDescription.trim(),
|
||||
query_data: JSON.stringify(defaultFilterData),
|
||||
owner: result.filter.owner,
|
||||
created_at: result.filter.created_at,
|
||||
updated_at: result.filter.updated_at,
|
||||
};
|
||||
// Select the new filter from API response
|
||||
onFilterSelect(result.filter);
|
||||
|
||||
// Select the new filter
|
||||
onFilterSelect(newFilter);
|
||||
|
||||
// Close modal and reset form
|
||||
setShowCreateModal(false);
|
||||
setCreateName("");
|
||||
setCreateDescription("");
|
||||
// Refresh list to include newly created filter
|
||||
refetch();
|
||||
} else {
|
||||
console.error("Failed to create knowledge filter:", result.error);
|
||||
}
|
||||
// Close modal and reset form
|
||||
setShowCreateModal(false);
|
||||
setCreateName("");
|
||||
setCreateDescription("");
|
||||
} catch (error) {
|
||||
console.error("Error creating knowledge filter:", error);
|
||||
} finally {
|
||||
|
|
@ -169,25 +115,6 @@ export function KnowledgeFilterList({
|
|||
return JSON.parse(queryData) as ParsedQueryData;
|
||||
};
|
||||
|
||||
const getFilterSummary = (filter: KnowledgeFilter): string => {
|
||||
try {
|
||||
const parsed = JSON.parse(filter.query_data) as ParsedQueryData;
|
||||
const parts = [];
|
||||
|
||||
if (parsed.query) parts.push(`"${parsed.query}"`);
|
||||
if (parsed.filters.data_sources.length > 0)
|
||||
parts.push(`${parsed.filters.data_sources.length} sources`);
|
||||
if (parsed.filters.document_types.length > 0)
|
||||
parts.push(`${parsed.filters.document_types.length} types`);
|
||||
if (parsed.filters.owners.length > 0)
|
||||
parts.push(`${parsed.filters.owners.length} owners`);
|
||||
|
||||
return parts.join(" • ") || "No filters";
|
||||
} catch {
|
||||
return "Invalid filter";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-1 px-3">
|
||||
|
|
@ -199,6 +126,7 @@ export function KnowledgeFilterList({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCreateNew}
|
||||
title="Create New Filter"
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
|
|
@ -255,14 +183,6 @@ export function KnowledgeFilterList({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => deleteFilter(filter.id, e)}
|
||||
className="opacity-0 group-hover:opacity-100 h-6 w-6 p-0 bg-transparent hover:bg-gray-700 hover:text-white transition-all duration-200 border border-transparent hover:border-gray-600"
|
||||
>
|
||||
<X className="h-3 w-3 text-gray-400 hover:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
|
@ -277,62 +197,60 @@ export function KnowledgeFilterList({
|
|||
knowledge base.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<div>
|
||||
<Label htmlFor="filter-name" className="font-medium">
|
||||
Name<span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="filter-name"
|
||||
type="text"
|
||||
placeholder="Enter filter name"
|
||||
value={createName}
|
||||
onChange={(e) => setCreateName(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="filter-description" className="font-medium">
|
||||
Description (optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="filter-description"
|
||||
placeholder="Brief description of this filter"
|
||||
value={createDescription}
|
||||
onChange={(e) => setCreateDescription(e.target.value)}
|
||||
className="mt-1"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<div>
|
||||
<Label htmlFor="filter-name" className="font-medium mb-2 gap-1">
|
||||
Name<span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="filter-name"
|
||||
type="text"
|
||||
placeholder="Enter filter name"
|
||||
value={createName}
|
||||
onChange={(e) => setCreateName(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelCreate}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateFilter}
|
||||
disabled={!createName.trim() || creating}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{creating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
Create Filter
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div>
|
||||
<Label htmlFor="filter-description" className="font-medium mb-2">
|
||||
Description (optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="filter-description"
|
||||
placeholder="Brief description of this filter"
|
||||
value={createDescription}
|
||||
onChange={(e) => setCreateDescription(e.target.value)}
|
||||
className="mt-1"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelCreate}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateFilter}
|
||||
disabled={!createName.trim() || creating}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{creating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
Create Filter
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,93 +1,106 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { X, Edit3, Save, Settings, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { MultiSelect } from '@/components/ui/multi-select'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { useKnowledgeFilter } from '@/contexts/knowledge-filter-context'
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { X, Edit3, Save, Settings, RefreshCw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { MultiSelect } from "@/components/ui/multi-select";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||
import { useDeleteFilter } from "@/app/api/mutations/useDeleteFilter";
|
||||
|
||||
interface FacetBucket {
|
||||
key: string
|
||||
count: number
|
||||
key: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface AvailableFacets {
|
||||
data_sources: FacetBucket[]
|
||||
document_types: FacetBucket[]
|
||||
owners: FacetBucket[]
|
||||
connector_types: FacetBucket[]
|
||||
data_sources: FacetBucket[];
|
||||
document_types: FacetBucket[];
|
||||
owners: FacetBucket[];
|
||||
connector_types: FacetBucket[];
|
||||
}
|
||||
|
||||
export function KnowledgeFilterPanel() {
|
||||
const { selectedFilter, parsedFilterData, setSelectedFilter, isPanelOpen, closePanelOnly } = useKnowledgeFilter()
|
||||
|
||||
const {
|
||||
selectedFilter,
|
||||
parsedFilterData,
|
||||
setSelectedFilter,
|
||||
isPanelOpen,
|
||||
closePanelOnly,
|
||||
} = useKnowledgeFilter();
|
||||
const deleteFilterMutation = useDeleteFilter();
|
||||
|
||||
// Edit mode states
|
||||
const [isEditingMeta, setIsEditingMeta] = useState(false)
|
||||
const [editingName, setEditingName] = useState('')
|
||||
const [editingDescription, setEditingDescription] = useState('')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isEditingMeta, setIsEditingMeta] = useState(false);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [editingDescription, setEditingDescription] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Filter configuration states (mirror search page exactly)
|
||||
const [query, setQuery] = useState('')
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedFilters, setSelectedFilters] = useState({
|
||||
data_sources: ["*"] as string[], // Default to wildcard
|
||||
document_types: ["*"] as string[], // Default to wildcard
|
||||
owners: ["*"] as string[], // Default to wildcard
|
||||
connector_types: ["*"] as string[] // Default to wildcard
|
||||
})
|
||||
const [resultLimit, setResultLimit] = useState(10)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(0)
|
||||
|
||||
connector_types: ["*"] as string[], // Default to wildcard
|
||||
});
|
||||
const [resultLimit, setResultLimit] = useState(10);
|
||||
const [scoreThreshold, setScoreThreshold] = useState(0);
|
||||
|
||||
// Available facets (loaded from API)
|
||||
const [availableFacets, setAvailableFacets] = useState<AvailableFacets>({
|
||||
data_sources: [],
|
||||
document_types: [],
|
||||
owners: [],
|
||||
connector_types: []
|
||||
})
|
||||
connector_types: [],
|
||||
});
|
||||
|
||||
// Load current filter data into controls
|
||||
useEffect(() => {
|
||||
if (selectedFilter && parsedFilterData) {
|
||||
setQuery(parsedFilterData.query || '')
|
||||
|
||||
setQuery(parsedFilterData.query || "");
|
||||
|
||||
// Set the actual filter selections from the saved knowledge filter
|
||||
const filters = parsedFilterData.filters
|
||||
|
||||
const filters = parsedFilterData.filters;
|
||||
|
||||
// Use the exact selections from the saved filter
|
||||
// Empty arrays mean "none selected" not "all selected"
|
||||
const processedFilters = {
|
||||
data_sources: filters.data_sources,
|
||||
document_types: filters.document_types,
|
||||
owners: filters.owners,
|
||||
connector_types: filters.connector_types || ["*"]
|
||||
}
|
||||
|
||||
console.log("[DEBUG] Loading filter selections:", processedFilters)
|
||||
|
||||
setSelectedFilters(processedFilters)
|
||||
setResultLimit(parsedFilterData.limit || 10)
|
||||
setScoreThreshold(parsedFilterData.scoreThreshold || 0)
|
||||
setEditingName(selectedFilter.name)
|
||||
setEditingDescription(selectedFilter.description || '')
|
||||
connector_types: filters.connector_types || ["*"],
|
||||
};
|
||||
|
||||
console.log("[DEBUG] Loading filter selections:", processedFilters);
|
||||
|
||||
setSelectedFilters(processedFilters);
|
||||
setResultLimit(parsedFilterData.limit || 10);
|
||||
setScoreThreshold(parsedFilterData.scoreThreshold || 0);
|
||||
setEditingName(selectedFilter.name);
|
||||
setEditingDescription(selectedFilter.description || "");
|
||||
}
|
||||
}, [selectedFilter, parsedFilterData])
|
||||
}, [selectedFilter, parsedFilterData]);
|
||||
|
||||
// Load available facets from API
|
||||
useEffect(() => {
|
||||
if (isPanelOpen) {
|
||||
loadAvailableFacets()
|
||||
loadAvailableFacets();
|
||||
}
|
||||
}, [isPanelOpen])
|
||||
}, [isPanelOpen]);
|
||||
|
||||
const loadAvailableFacets = async () => {
|
||||
console.log("[DEBUG] Loading available facets...")
|
||||
console.log("[DEBUG] Loading available facets...");
|
||||
try {
|
||||
// Do a search to get facets (similar to search page)
|
||||
const response = await fetch("/api/search", {
|
||||
|
|
@ -95,39 +108,36 @@ export function KnowledgeFilterPanel() {
|
|||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: JSON.stringify({
|
||||
query: "*", // Use wildcard like search page to get all documents/facets
|
||||
limit: 1,
|
||||
scoreThreshold: 0
|
||||
scoreThreshold: 0,
|
||||
// Omit filters entirely to get all available facets
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log("[DEBUG] Search API response:", result);
|
||||
|
||||
const result = await response.json()
|
||||
console.log("[DEBUG] Search API response:", result)
|
||||
|
||||
if (response.ok && result.aggregations) {
|
||||
const facets = {
|
||||
data_sources: result.aggregations.data_sources?.buckets || [],
|
||||
document_types: result.aggregations.document_types?.buckets || [],
|
||||
owners: result.aggregations.owners?.buckets || [],
|
||||
connector_types: result.aggregations.connector_types?.buckets || []
|
||||
}
|
||||
console.log("[DEBUG] Setting facets:", facets)
|
||||
setAvailableFacets(facets)
|
||||
connector_types: result.aggregations.connector_types?.buckets || [],
|
||||
};
|
||||
console.log("[DEBUG] Setting facets:", facets);
|
||||
setAvailableFacets(facets);
|
||||
} else {
|
||||
console.log("[DEBUG] No aggregations in response or response not ok")
|
||||
console.log("[DEBUG] No aggregations in response or response not ok");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load available facets:", error)
|
||||
console.error("Failed to load available facets:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render if panel is closed or no filter selected
|
||||
if (!isPanelOpen || !selectedFilter || !parsedFilterData) return null
|
||||
|
||||
|
||||
|
||||
if (!isPanelOpen || !selectedFilter || !parsedFilterData) return null;
|
||||
|
||||
const selectAllFilters = () => {
|
||||
// Use wildcards instead of listing all specific items
|
||||
|
|
@ -135,116 +145,135 @@ export function KnowledgeFilterPanel() {
|
|||
data_sources: ["*"],
|
||||
document_types: ["*"],
|
||||
owners: ["*"],
|
||||
connector_types: ["*"]
|
||||
})
|
||||
}
|
||||
connector_types: ["*"],
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setSelectedFilters({
|
||||
data_sources: [],
|
||||
document_types: [],
|
||||
owners: [],
|
||||
connector_types: []
|
||||
})
|
||||
}
|
||||
connector_types: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditMeta = () => {
|
||||
setIsEditingMeta(true)
|
||||
}
|
||||
setIsEditingMeta(true);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditingMeta(false)
|
||||
setEditingName(selectedFilter.name)
|
||||
setEditingDescription(selectedFilter.description || '')
|
||||
}
|
||||
setIsEditingMeta(false);
|
||||
setEditingName(selectedFilter.name);
|
||||
setEditingDescription(selectedFilter.description || "");
|
||||
};
|
||||
|
||||
const handleSaveMeta = async () => {
|
||||
if (!editingName.trim()) return
|
||||
if (!editingName.trim()) return;
|
||||
|
||||
setIsSaving(true)
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-filter/${selectedFilter.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: editingName.trim(),
|
||||
description: editingDescription.trim(),
|
||||
}),
|
||||
})
|
||||
const response = await fetch(
|
||||
`/api/knowledge-filter/${selectedFilter.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: editingName.trim(),
|
||||
description: editingDescription.trim(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json()
|
||||
const result = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
const updatedFilter = {
|
||||
...selectedFilter,
|
||||
name: editingName.trim(),
|
||||
description: editingDescription.trim(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
setSelectedFilter(updatedFilter)
|
||||
setIsEditingMeta(false)
|
||||
};
|
||||
setSelectedFilter(updatedFilter);
|
||||
setIsEditingMeta(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating filter:', error)
|
||||
console.error("Error updating filter:", error);
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfiguration = async () => {
|
||||
const filterData = {
|
||||
query,
|
||||
filters: selectedFilters,
|
||||
limit: resultLimit,
|
||||
scoreThreshold
|
||||
}
|
||||
scoreThreshold,
|
||||
};
|
||||
|
||||
setIsSaving(true)
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge-filter/${selectedFilter.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
queryData: JSON.stringify(filterData)
|
||||
}),
|
||||
})
|
||||
const response = await fetch(
|
||||
`/api/knowledge-filter/${selectedFilter.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
queryData: JSON.stringify(filterData),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json()
|
||||
const result = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
// Update the filter in context
|
||||
const updatedFilter = {
|
||||
...selectedFilter,
|
||||
query_data: JSON.stringify(filterData),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
setSelectedFilter(updatedFilter)
|
||||
};
|
||||
setSelectedFilter(updatedFilter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating filter configuration:', error)
|
||||
console.error("Error updating filter configuration:", error);
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const handleFilterChange = (facetType: keyof typeof selectedFilters, newValues: string[]) => {
|
||||
setSelectedFilters(prev => ({
|
||||
const handleFilterChange = (
|
||||
facetType: keyof typeof selectedFilters,
|
||||
newValues: string[]
|
||||
) => {
|
||||
setSelectedFilters((prev) => ({
|
||||
...prev,
|
||||
[facetType]: newValues
|
||||
}))
|
||||
}
|
||||
[facetType]: newValues,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDeleteFilter = async () => {
|
||||
const result = await deleteFilterMutation.mutateAsync({
|
||||
id: selectedFilter.id,
|
||||
});
|
||||
if (result.success) {
|
||||
setSelectedFilter(null);
|
||||
closePanelOnly();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-14 bottom-0 w-80 bg-background border-l border-border/40 z-40 overflow-y-auto">
|
||||
|
|
@ -301,7 +330,7 @@ export function KnowledgeFilterPanel() {
|
|||
className="flex-1"
|
||||
>
|
||||
<Save className="h-3 w-3 mr-1" />
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelEdit}
|
||||
|
|
@ -317,7 +346,9 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg">{selectedFilter.name}</h3>
|
||||
<h3 className="font-semibold text-lg">
|
||||
{selectedFilter.name}
|
||||
</h3>
|
||||
{selectedFilter.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{selectedFilter.description}
|
||||
|
|
@ -336,7 +367,10 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="text-xs text-muted-foreground">
|
||||
Created {formatDate(selectedFilter.created_at)}
|
||||
{selectedFilter.updated_at !== selectedFilter.created_at && (
|
||||
<span> • Updated {formatDate(selectedFilter.updated_at)}</span>
|
||||
<span>
|
||||
{" "}
|
||||
• Updated {formatDate(selectedFilter.updated_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -345,7 +379,9 @@ export function KnowledgeFilterPanel() {
|
|||
|
||||
{/* Search Query */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search-query" className="text-sm font-medium">Search Query</Label>
|
||||
<Label htmlFor="search-query" className="text-sm font-medium">
|
||||
Search Query
|
||||
</Label>
|
||||
<Input
|
||||
id="search-query"
|
||||
type="text"
|
||||
|
|
@ -361,13 +397,15 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Data Sources</Label>
|
||||
<MultiSelect
|
||||
options={(availableFacets.data_sources || []).map(bucket => ({
|
||||
options={(availableFacets.data_sources || []).map((bucket) => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count
|
||||
count: bucket.count,
|
||||
}))}
|
||||
value={selectedFilters.data_sources}
|
||||
onValueChange={(values) => handleFilterChange('data_sources', values)}
|
||||
onValueChange={(values) =>
|
||||
handleFilterChange("data_sources", values)
|
||||
}
|
||||
placeholder="Select data sources..."
|
||||
allOptionLabel="All Data Sources"
|
||||
/>
|
||||
|
|
@ -376,13 +414,17 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Document Types</Label>
|
||||
<MultiSelect
|
||||
options={(availableFacets.document_types || []).map(bucket => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count
|
||||
}))}
|
||||
options={(availableFacets.document_types || []).map(
|
||||
(bucket) => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count,
|
||||
})
|
||||
)}
|
||||
value={selectedFilters.document_types}
|
||||
onValueChange={(values) => handleFilterChange('document_types', values)}
|
||||
onValueChange={(values) =>
|
||||
handleFilterChange("document_types", values)
|
||||
}
|
||||
placeholder="Select document types..."
|
||||
allOptionLabel="All Document Types"
|
||||
/>
|
||||
|
|
@ -391,13 +433,13 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Owners</Label>
|
||||
<MultiSelect
|
||||
options={(availableFacets.owners || []).map(bucket => ({
|
||||
options={(availableFacets.owners || []).map((bucket) => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count
|
||||
count: bucket.count,
|
||||
}))}
|
||||
value={selectedFilters.owners}
|
||||
onValueChange={(values) => handleFilterChange('owners', values)}
|
||||
onValueChange={(values) => handleFilterChange("owners", values)}
|
||||
placeholder="Select owners..."
|
||||
allOptionLabel="All Owners"
|
||||
/>
|
||||
|
|
@ -406,13 +448,17 @@ export function KnowledgeFilterPanel() {
|
|||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Sources</Label>
|
||||
<MultiSelect
|
||||
options={(availableFacets.connector_types || []).map(bucket => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count
|
||||
}))}
|
||||
options={(availableFacets.connector_types || []).map(
|
||||
(bucket) => ({
|
||||
value: bucket.key,
|
||||
label: bucket.key,
|
||||
count: bucket.count,
|
||||
})
|
||||
)}
|
||||
value={selectedFilters.connector_types}
|
||||
onValueChange={(values) => handleFilterChange('connector_types', values)}
|
||||
onValueChange={(values) =>
|
||||
handleFilterChange("connector_types", values)
|
||||
}
|
||||
placeholder="Select sources..."
|
||||
allOptionLabel="All Sources"
|
||||
/>
|
||||
|
|
@ -420,18 +466,18 @@ export function KnowledgeFilterPanel() {
|
|||
|
||||
{/* All/None buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={selectAllFilters}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={selectAllFilters}
|
||||
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={clearAllFilters}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={clearAllFilters}
|
||||
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
|
||||
>
|
||||
None
|
||||
|
|
@ -449,11 +495,14 @@ export function KnowledgeFilterPanel() {
|
|||
max="1000"
|
||||
value={resultLimit}
|
||||
onChange={(e) => {
|
||||
const newLimit = Math.max(1, Math.min(1000, parseInt(e.target.value) || 1))
|
||||
setResultLimit(newLimit)
|
||||
const newLimit = Math.max(
|
||||
1,
|
||||
Math.min(1000, parseInt(e.target.value) || 1)
|
||||
);
|
||||
setResultLimit(newLimit);
|
||||
}}
|
||||
className="h-6 text-xs text-right px-2 bg-muted/30 !border-0 rounded ml-auto focus:ring-0 focus:outline-none"
|
||||
style={{ width: '70px' }}
|
||||
style={{ width: "70px" }}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
|
|
@ -476,9 +525,11 @@ export function KnowledgeFilterPanel() {
|
|||
max="5"
|
||||
step="0.1"
|
||||
value={scoreThreshold}
|
||||
onChange={(e) => setScoreThreshold(parseFloat(e.target.value) || 0)}
|
||||
onChange={(e) =>
|
||||
setScoreThreshold(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="h-6 text-xs text-right px-2 bg-muted/30 !border-0 rounded ml-auto focus:ring-0 focus:outline-none"
|
||||
style={{ width: '70px' }}
|
||||
style={{ width: "70px" }}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
|
|
@ -493,7 +544,7 @@ export function KnowledgeFilterPanel() {
|
|||
</div>
|
||||
|
||||
{/* Save Configuration Button */}
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border/50">
|
||||
<Button
|
||||
onClick={handleSaveConfiguration}
|
||||
disabled={isSaving}
|
||||
|
|
@ -512,10 +563,17 @@ export function KnowledgeFilterPanel() {
|
|||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={handleDeleteFilter}
|
||||
>
|
||||
Delete Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"primary-input !placeholder-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -50,35 +50,61 @@ async function proxyRequest(
|
|||
|
||||
try {
|
||||
let body: string | ArrayBuffer | undefined = undefined;
|
||||
|
||||
let willSendBody = false;
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
const contentType = request.headers.get('content-type') || '';
|
||||
|
||||
const contentLength = request.headers.get('content-length');
|
||||
|
||||
// For file uploads (multipart/form-data), preserve binary data
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
body = await request.arrayBuffer();
|
||||
const buf = await request.arrayBuffer();
|
||||
if (buf && buf.byteLength > 0) {
|
||||
body = buf;
|
||||
willSendBody = true;
|
||||
}
|
||||
} else {
|
||||
// For JSON and other text-based content, use text
|
||||
body = await request.text();
|
||||
const text = await request.text();
|
||||
if (text && text.length > 0) {
|
||||
body = text;
|
||||
willSendBody = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against incorrect non-zero content-length when there is no body
|
||||
if (!willSendBody && contentLength) {
|
||||
// We'll drop content-length/header below
|
||||
}
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
|
||||
// Copy relevant headers from the original request
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
if (!key.toLowerCase().startsWith('host') &&
|
||||
!key.toLowerCase().startsWith('x-forwarded') &&
|
||||
!key.toLowerCase().startsWith('x-real-ip')) {
|
||||
headers.set(key, value);
|
||||
const lower = key.toLowerCase();
|
||||
if (
|
||||
lower.startsWith('host') ||
|
||||
lower.startsWith('x-forwarded') ||
|
||||
lower.startsWith('x-real-ip') ||
|
||||
lower === 'content-length' ||
|
||||
(!willSendBody && lower === 'content-type')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
headers.set(key, value);
|
||||
}
|
||||
|
||||
const response = await fetch(backendUrl, {
|
||||
const init: RequestInit = {
|
||||
method: request.method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
};
|
||||
if (willSendBody) {
|
||||
// Convert ArrayBuffer to Uint8Array to satisfy BodyInit in all environments
|
||||
const bodyInit: BodyInit = typeof body === 'string' ? body : new Uint8Array(body as ArrayBuffer);
|
||||
init.body = bodyInit;
|
||||
}
|
||||
const response = await fetch(backendUrl, init);
|
||||
|
||||
const responseBody = await response.text();
|
||||
const responseHeaders = new Headers();
|
||||
|
|
|
|||
50
frontend/src/app/api/mutations/useCreateFilter.ts
Normal file
50
frontend/src/app/api/mutations/useCreateFilter.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { KnowledgeFilter } from "../queries/useGetFiltersSearchQuery";
|
||||
|
||||
export interface CreateFilterRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
queryData: string; // stringified ParsedQueryData
|
||||
}
|
||||
|
||||
export interface CreateFilterResponse {
|
||||
success: boolean;
|
||||
filter: KnowledgeFilter;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
async function createFilter(
|
||||
data: CreateFilterRequest,
|
||||
): Promise<CreateFilterResponse> {
|
||||
const response = await fetch("/api/knowledge-filter", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
description: data.description ?? "",
|
||||
queryData: data.queryData,
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = (json && (json.error as string)) || "Failed to create knowledge filter";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return json as CreateFilterResponse;
|
||||
}
|
||||
|
||||
export const useCreateFilter = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createFilter,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["knowledge-filters"], exact: false });
|
||||
},
|
||||
});
|
||||
};
|
||||
39
frontend/src/app/api/mutations/useDeleteFilter.ts
Normal file
39
frontend/src/app/api/mutations/useDeleteFilter.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export interface DeleteFilterRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DeleteFilterResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
async function deleteFilter(
|
||||
data: DeleteFilterRequest,
|
||||
): Promise<DeleteFilterResponse> {
|
||||
const response = await fetch(`/api/knowledge-filter/${data.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
const json = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = (json && (json.error as string)) || "Failed to delete knowledge filter";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return (json as DeleteFilterResponse) || { success: true };
|
||||
}
|
||||
|
||||
export const useDeleteFilter = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteFilter,
|
||||
onSuccess: () => {
|
||||
// Invalidate filters queries so UI refreshes automatically
|
||||
queryClient.invalidateQueries({ queryKey: ["knowledge-filters"], exact: false });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
type UseQueryOptions,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseQueryOptions,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
export interface KnowledgeFilter {
|
||||
|
|
@ -14,50 +14,34 @@ export interface KnowledgeFilter {
|
|||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface FiltersSearchResponse {
|
||||
success: boolean;
|
||||
filters: KnowledgeFilter[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const useGetFiltersSearchQuery = (
|
||||
search: string,
|
||||
limit = 20,
|
||||
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">
|
||||
options?: Omit<UseQueryOptions<KnowledgeFilter[]>, "queryKey" | "queryFn">
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function getFilters(): Promise<KnowledgeFilter[]> {
|
||||
try {
|
||||
const response = await fetch("/api/knowledge-filter/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ query: search, limit }),
|
||||
});
|
||||
const response = await fetch("/api/knowledge-filter/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: search, limit }),
|
||||
});
|
||||
|
||||
const result: FiltersSearchResponse = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
return result.filters || [];
|
||||
}
|
||||
console.error("Failed to load knowledge filters:", result.error);
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("Error loading knowledge filters:", error);
|
||||
const json = await response.json();
|
||||
if (!response.ok || !json.success) {
|
||||
// ensure we always return a KnowledgeFilter[] to satisfy the return type
|
||||
return [];
|
||||
}
|
||||
return (json.filters || []) as KnowledgeFilter[];
|
||||
}
|
||||
|
||||
const queryResult = useQuery(
|
||||
return useQuery<KnowledgeFilter[]>(
|
||||
{
|
||||
queryKey: ["knowledge-filters", search, limit],
|
||||
placeholderData: (prev) => prev,
|
||||
queryFn: getFilters,
|
||||
...options,
|
||||
},
|
||||
queryClient,
|
||||
queryClient
|
||||
);
|
||||
|
||||
return queryResult;
|
||||
};
|
||||
};
|
||||
|
|
@ -3,10 +3,12 @@
|
|||
import {
|
||||
Building2,
|
||||
Cloud,
|
||||
Filter,
|
||||
HardDrive,
|
||||
Loader2,
|
||||
Search,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { AgGridReact, CustomCellRendererProps } from "ag-grid-react";
|
||||
import {
|
||||
|
|
@ -22,7 +24,6 @@ import { TbBrandOnedrive } from "react-icons/tb";
|
|||
import { KnowledgeDropdown } from "@/components/knowledge-dropdown";
|
||||
import { ProtectedRoute } from "@/components/protected-route";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery";
|
||||
|
|
@ -59,7 +60,8 @@ function getSourceIcon(connectorType?: string) {
|
|||
function SearchPage() {
|
||||
const router = useRouter();
|
||||
const { isMenuOpen } = useTask();
|
||||
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter();
|
||||
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
|
||||
useKnowledgeFilter();
|
||||
const [query, setQuery] = useState("");
|
||||
const [queryInputText, setQueryInputText] = useState("");
|
||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||
|
|
@ -244,18 +246,31 @@ function SearchPage() {
|
|||
</div>
|
||||
|
||||
{/* Search Input Area */}
|
||||
<div className="flex-shrink-0 mb-6 lg:max-w-[75%] xl:max-w-[50%]">
|
||||
<div className="flex-shrink-0 mb-6 xl:max-w-[75%]">
|
||||
<form onSubmit={handleSearch} className="flex gap-3">
|
||||
<Input
|
||||
name="search-query"
|
||||
id="search-query"
|
||||
type="text"
|
||||
defaultValue={parsedFilterData?.query}
|
||||
value={queryInputText}
|
||||
onChange={(e) => setQueryInputText(e.target.value)}
|
||||
placeholder="Search your documents..."
|
||||
className="flex-1 bg-muted/20 rounded-lg border border-border/50 px-4 py-3 focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
<div className="primary-input !flex items-center flex-nowrap gap-2 focus-within:border-foreground transition-colors !py-0">
|
||||
{selectedFilter?.name && (
|
||||
<div className="flex items-center gap-2 bg-accent text-accent-foreground px-1 py-0.5 rounded max-w-[300px]">
|
||||
<Filter className="h-3 w-3 flex-shrink-0 ml-1" />
|
||||
<span className="truncate">{selectedFilter?.name}</span>
|
||||
<X
|
||||
aria-label="Remove filter"
|
||||
className="h-4 w-4 flex-shrink-0 cursor-pointer"
|
||||
onClick={() => setSelectedFilter(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className="bg-transparent w-full h-full focus:outline-none focus-visible:outline-none placeholder:font-mono"
|
||||
name="search-query"
|
||||
id="search-query"
|
||||
type="text"
|
||||
placeholder="Search your documents..."
|
||||
onChange={(e) => setQueryInputText(e.target.value)}
|
||||
value={queryInputText}
|
||||
defaultValue={parsedFilterData?.query}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
|
|
@ -298,8 +313,8 @@ function SearchPage() {
|
|||
rowMultiSelectWithClick={false}
|
||||
suppressRowClickSelection={true}
|
||||
getRowId={(params) => params.data.filename}
|
||||
domLayout="autoHeight"
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
suppressHorizontalScroll={false}
|
||||
noRowsOverlayComponent={() => (
|
||||
<div className="text-center">
|
||||
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button"
|
|||
import { Navigation } from "@/components/navigation"
|
||||
import { UserNav } from "@/components/user-nav"
|
||||
import { TaskNotificationMenu } from "@/components/task-notification-menu"
|
||||
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown"
|
||||
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"
|
||||
// import { GitHubStarButton } from "@/components/github-star-button"
|
||||
// import { DiscordLink } from "@/components/discord-link"
|
||||
|
|
@ -18,7 +17,7 @@ import { Loader2 } from "lucide-react"
|
|||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const { tasks, isMenuOpen, toggleMenu } = useTask()
|
||||
const { selectedFilter, setSelectedFilter, isPanelOpen } = useKnowledgeFilter()
|
||||
const { isPanelOpen } = useKnowledgeFilter()
|
||||
const { isLoading } = useAuth()
|
||||
|
||||
// List of paths that should not show navigation
|
||||
|
|
@ -68,12 +67,6 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
|||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="header-end-display">
|
||||
{/* Knowledge Filter Dropdown */}
|
||||
<KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/>
|
||||
|
||||
{/* GitHub Star Button */}
|
||||
{/* <GitHubStarButton repo="phact/openrag" /> */}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,18 @@ class KnowledgeFilterService:
|
|||
|
||||
# Index the knowledge filter document
|
||||
result = await opensearch_client.index(
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_doc["id"], body=filter_doc
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME,
|
||||
id=filter_doc["id"],
|
||||
body=filter_doc,
|
||||
refresh="wait_for",
|
||||
)
|
||||
|
||||
if result.get("result") == "created":
|
||||
# Extra safety: ensure visibility in subsequent searches
|
||||
try:
|
||||
await opensearch_client.indices.refresh(index=KNOWLEDGE_FILTERS_INDEX_NAME)
|
||||
except Exception:
|
||||
pass
|
||||
return {"success": True, "id": filter_doc["id"], "filter": filter_doc}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to create knowledge filter"}
|
||||
|
|
@ -138,11 +146,19 @@ class KnowledgeFilterService:
|
|||
|
||||
# Update the document
|
||||
result = await opensearch_client.update(
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id, body={"doc": updates}
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME,
|
||||
id=filter_id,
|
||||
body={"doc": updates},
|
||||
refresh="wait_for",
|
||||
)
|
||||
|
||||
if result.get("result") in ["updated", "noop"]:
|
||||
# Get the updated document
|
||||
# Ensure visibility before fetching/returning
|
||||
try:
|
||||
await opensearch_client.indices.refresh(index=KNOWLEDGE_FILTERS_INDEX_NAME)
|
||||
except Exception:
|
||||
pass
|
||||
updated_doc = await opensearch_client.get(
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id
|
||||
)
|
||||
|
|
@ -164,10 +180,17 @@ class KnowledgeFilterService:
|
|||
)
|
||||
|
||||
result = await opensearch_client.delete(
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME,
|
||||
id=filter_id,
|
||||
refresh="wait_for",
|
||||
)
|
||||
|
||||
if result.get("result") == "deleted":
|
||||
# Extra safety: ensure visibility in subsequent searches
|
||||
try:
|
||||
await opensearch_client.indices.refresh(index=KNOWLEDGE_FILTERS_INDEX_NAME)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Knowledge filter deleted successfully",
|
||||
|
|
@ -230,7 +253,10 @@ class KnowledgeFilterService:
|
|||
}
|
||||
|
||||
result = await opensearch_client.update(
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id, body=update_body
|
||||
index=KNOWLEDGE_FILTERS_INDEX_NAME,
|
||||
id=filter_id,
|
||||
body=update_body,
|
||||
refresh="wait_for",
|
||||
)
|
||||
|
||||
if result.get("result") in ["updated", "noop"]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue