make knowledge search a component used for knowledge and chunk page
This commit is contained in:
parent
16b9784c2f
commit
2547af298f
4 changed files with 171 additions and 184 deletions
|
|
@ -321,7 +321,7 @@ export function KnowledgeFilterPanel() {
|
|||
className="font-mono placeholder:font-mono"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
rows={2}
|
||||
disabled={!!queryOverride}
|
||||
disabled={!!queryOverride && !createMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
100
frontend/components/knowledge-search-input.tsx
Normal file
100
frontend/components/knowledge-search-input.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||
import {
|
||||
ChangeEvent,
|
||||
FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { filterAccentClasses } from "./knowledge-filter-panel";
|
||||
import { ArrowRight, Search, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const KnowledgeSearchInput = () => {
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
parsedFilterData,
|
||||
queryOverride,
|
||||
setQueryOverride,
|
||||
} = useKnowledgeFilter();
|
||||
|
||||
const [searchQueryInput, setSearchQueryInput] = useState(queryOverride || "");
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e?: FormEvent<HTMLFormElement>) => {
|
||||
if (e) e.preventDefault();
|
||||
setQueryOverride(searchQueryInput.trim());
|
||||
},
|
||||
[searchQueryInput, setQueryOverride]
|
||||
);
|
||||
|
||||
// Reset the query text when the selected filter changes
|
||||
useEffect(() => {
|
||||
setSearchQueryInput(queryOverride);
|
||||
}, [queryOverride]);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-1 max-w-[min(640px,100%)] min-w-[100px]"
|
||||
onSubmit={handleSearch}
|
||||
>
|
||||
<div className="primary-input group/input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]">
|
||||
{selectedFilter?.name && (
|
||||
<div
|
||||
title={selectedFilter?.name}
|
||||
className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
|
||||
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<Search
|
||||
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<input
|
||||
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
|
||||
name="search-query"
|
||||
id="search-query"
|
||||
type="text"
|
||||
placeholder="Search your documents..."
|
||||
value={searchQueryInput}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchQueryInput(e.target.value)
|
||||
}
|
||||
/>
|
||||
{queryOverride && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-full !px-1.5 !py-0"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearchQueryInput("");
|
||||
setQueryOverride("");
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-full !px-1.5 !py-0 hidden group-focus-within/input:block",
|
||||
searchQueryInput && "block"
|
||||
)}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,15 +6,13 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import { ProtectedRoute } from "@/components/protected-route";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
import {
|
||||
type ChunkResult,
|
||||
type File,
|
||||
useGetSearchQuery,
|
||||
} from "../../api/queries/useGetSearchQuery";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { KnowledgeSearchInput } from "@/components/knowledge-search-input";
|
||||
|
||||
const getFileTypeLabel = (mimetype: string) => {
|
||||
if (mimetype === "application/pdf") return "PDF";
|
||||
|
|
@ -26,8 +24,7 @@ const getFileTypeLabel = (mimetype: string) => {
|
|||
function ChunksPageContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { isMenuOpen } = useTask();
|
||||
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter();
|
||||
const { parsedFilterData, queryOverride } = useKnowledgeFilter();
|
||||
|
||||
const filename = searchParams.get("filename");
|
||||
const [chunks, setChunks] = useState<ChunkResult[]>([]);
|
||||
|
|
@ -47,25 +44,25 @@ function ChunksPageContent() {
|
|||
[chunks]
|
||||
);
|
||||
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
const [queryInputText, setQueryInputText] = useState(
|
||||
parsedFilterData?.query ?? ""
|
||||
);
|
||||
// const [selectAll, setSelectAll] = useState(false);
|
||||
|
||||
// Use the same search query as the knowledge page, but we'll filter for the specific file
|
||||
const { data = [], isFetching } = useGetSearchQuery("*", parsedFilterData);
|
||||
const { data = [], isFetching } = useGetSearchQuery(
|
||||
queryOverride,
|
||||
parsedFilterData
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryInputText === "") {
|
||||
setChunksFilteredByQuery(chunks);
|
||||
} else {
|
||||
setChunksFilteredByQuery(
|
||||
chunks.filter((chunk) =>
|
||||
chunk.text.toLowerCase().includes(queryInputText.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [queryInputText, chunks]);
|
||||
// useEffect(() => {
|
||||
// if (queryInputText === "") {
|
||||
// setChunksFilteredByQuery(chunks);
|
||||
// } else {
|
||||
// setChunksFilteredByQuery(
|
||||
// chunks.filter((chunk) =>
|
||||
// chunk.text.toLowerCase().includes(queryInputText.toLowerCase())
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }, [queryInputText, chunks]);
|
||||
|
||||
const handleCopy = useCallback((text: string, index: number) => {
|
||||
// Trim whitespace and remove new lines/tabs for cleaner copy
|
||||
|
|
@ -89,13 +86,13 @@ function ChunksPageContent() {
|
|||
}, [data, filename]);
|
||||
|
||||
// Set selected state for all checkboxes when selectAll changes
|
||||
useEffect(() => {
|
||||
if (selectAll) {
|
||||
setSelectedChunks(new Set(chunks.map((_, index) => index)));
|
||||
} else {
|
||||
setSelectedChunks(new Set());
|
||||
}
|
||||
}, [selectAll, setSelectedChunks, chunks]);
|
||||
// useEffect(() => {
|
||||
// if (selectAll) {
|
||||
// setSelectedChunks(new Set(chunks.map((_, index) => index)));
|
||||
// } else {
|
||||
// setSelectedChunks(new Set());
|
||||
// }
|
||||
// }, [selectAll, setSelectedChunks, chunks]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
router.push("/knowledge");
|
||||
|
|
@ -131,25 +128,17 @@ function ChunksPageContent() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 md:left-72 top-[53px] flex flex-row transition-all duration-300 ${
|
||||
isMenuOpen && isPanelOpen
|
||||
? "md:right-[704px]"
|
||||
: // Both open: 384px (menu) + 320px (KF panel)
|
||||
isMenuOpen
|
||||
? "md:right-96"
|
||||
: // Only menu open: 384px
|
||||
isPanelOpen
|
||||
? "md:right-80"
|
||||
: // Only KF panel open: 320px
|
||||
"md:right-6" // Neither open: 24px
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 flex flex-col min-h-0 px-6 py-6">
|
||||
<div className="flex">
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col mb-6">
|
||||
<div className="flex flex-row items-center gap-3 mb-6">
|
||||
<Button variant="ghost" onClick={handleBack} size="sm">
|
||||
<div className="flex flex-col mb-6 gap-6">
|
||||
<div className="flex flex-row items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleBack}
|
||||
size="sm"
|
||||
className="max-h-8 max-w-8 -m-2 mr-1"
|
||||
>
|
||||
<ArrowLeft size={24} />
|
||||
</Button>
|
||||
<h1 className="text-lg font-semibold">
|
||||
|
|
@ -157,39 +146,12 @@ function ChunksPageContent() {
|
|||
{filename.replace(/\.[^/.]+$/, "")}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col items-start mt-2">
|
||||
<div className="flex-1 flex items-center gap-2 w-full max-w-[616px] mb-8">
|
||||
<Input
|
||||
name="search-query"
|
||||
icon={!queryInputText.length ? <Search size={18} /> : null}
|
||||
id="search-query"
|
||||
type="text"
|
||||
defaultValue={parsedFilterData?.query}
|
||||
value={queryInputText}
|
||||
onChange={(e) => setQueryInputText(e.target.value)}
|
||||
placeholder="Search chunks..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center pl-4 gap-2">
|
||||
<Checkbox
|
||||
id="selectAllChunks"
|
||||
checked={selectAll}
|
||||
onCheckedChange={(handleSelectAll) =>
|
||||
setSelectAll(!!handleSelectAll)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="selectAllChunks"
|
||||
className="font-medium text-muted-foreground whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
Select all
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
{/* Search input */}
|
||||
<KnowledgeSearchInput />
|
||||
</div>
|
||||
|
||||
{/* Content Area - matches knowledge page structure */}
|
||||
<div className="flex-1 overflow-scroll pr-6">
|
||||
<div className="flex-1 pr-6">
|
||||
{isFetching ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
|
|
@ -211,7 +173,23 @@ function ChunksPageContent() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 pb-6">
|
||||
{chunksFilteredByQuery.map((chunk, index) => (
|
||||
{/* TODO - add chunk selection when sync and delete are ready */}
|
||||
{/* <div className="flex items-center pl-4 gap-2">
|
||||
<Checkbox
|
||||
id="selectAllChunks"
|
||||
checked={selectAll}
|
||||
onCheckedChange={(handleSelectAll) =>
|
||||
setSelectAll(!!handleSelectAll)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="selectAllChunks"
|
||||
className="font-medium text-muted-foreground whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
Select all
|
||||
</Label>
|
||||
</div> */}
|
||||
{chunks.map((chunk, index) => (
|
||||
<div
|
||||
key={chunk.filename + index}
|
||||
className="bg-muted rounded-lg p-4 border border-border/50"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@
|
|||
|
||||
import { themeQuartz, type ColDef } from "ag-grid-community";
|
||||
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
|
||||
import { ArrowRight, Cloud, FileIcon, Search, X } from "lucide-react";
|
||||
import { Cloud, FileIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
|
@ -25,11 +22,10 @@ import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdow
|
|||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
||||
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
||||
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
||||
import GoogleDriveIcon from "../settings/icons/google-drive-icon";
|
||||
import OneDriveIcon from "../settings/icons/one-drive-icon";
|
||||
import SharePointIcon from "../settings/icons/share-point-icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { KnowledgeSearchInput } from "@/components/knowledge-search-input";
|
||||
|
||||
// Function to get the appropriate icon for a connector type
|
||||
function getSourceIcon(connectorType?: string) {
|
||||
|
|
@ -57,13 +53,9 @@ function SearchPage() {
|
|||
const router = useRouter();
|
||||
const { files: taskFiles } = useTask();
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
parsedFilterData,
|
||||
queryOverride,
|
||||
setQueryOverride,
|
||||
} = useKnowledgeFilter();
|
||||
const [searchQueryInput, setSearchQueryInput] = useState(queryOverride || "");
|
||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
||||
|
||||
|
|
@ -74,14 +66,6 @@ function SearchPage() {
|
|||
parsedFilterData
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e?: FormEvent<HTMLFormElement>) => {
|
||||
if (e) e.preventDefault();
|
||||
setQueryOverride(searchQueryInput.trim());
|
||||
},
|
||||
[searchQueryInput, setQueryOverride]
|
||||
);
|
||||
|
||||
// Convert TaskFiles to File format and merge with backend results
|
||||
const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => {
|
||||
return {
|
||||
|
|
@ -246,11 +230,6 @@ function SearchPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Reset the query text when the selected filter changes
|
||||
useEffect(() => {
|
||||
setSearchQueryInput(queryOverride);
|
||||
}, [queryOverride]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full">
|
||||
|
|
@ -260,78 +239,9 @@ function SearchPage() {
|
|||
|
||||
{/* Search Input Area */}
|
||||
<div className="flex-1 flex flex-shrink-0 flex-wrap-reverse gap-3 mb-6">
|
||||
<form
|
||||
className="flex flex-1 gap-3 max-w-full"
|
||||
onSubmit={handleSearch}
|
||||
>
|
||||
<div className="primary-input group/input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem] max-w-[min(640px,100%)] min-w-[100px]">
|
||||
{selectedFilter?.name && (
|
||||
<div
|
||||
title={selectedFilter?.name}
|
||||
className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
|
||||
filterAccentClasses[parsedFilterData?.color || "zinc"]
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<Search
|
||||
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<input
|
||||
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
|
||||
name="search-query"
|
||||
id="search-query"
|
||||
type="text"
|
||||
placeholder="Search your documents..."
|
||||
value={searchQueryInput}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchQueryInput(e.target.value)
|
||||
}
|
||||
/>
|
||||
{queryOverride && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-full !px-1.5 !py-0"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearchQueryInput("");
|
||||
setQueryOverride("");
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-full !px-1.5 !py-0 hidden group-focus-within/input:block",
|
||||
searchQueryInput && "block"
|
||||
)}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* <Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="rounded-lg p-0 flex-shrink-0"
|
||||
>
|
||||
{isFetching ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="h-4 w-4" />
|
||||
)}
|
||||
</Button> */}
|
||||
{/* //TODO: Implement sync button */}
|
||||
{/* <Button
|
||||
<KnowledgeSearchInput />
|
||||
{/* //TODO: Implement sync button */}
|
||||
{/* <Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-lg flex-shrink-0"
|
||||
|
|
@ -339,17 +249,16 @@ function SearchPage() {
|
|||
>
|
||||
Sync
|
||||
</Button> */}
|
||||
{selectedRows.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="rounded-lg flex-shrink-0"
|
||||
onClick={() => setShowBulkDeleteDialog(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
{selectedRows.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="rounded-lg flex-shrink-0"
|
||||
onClick={() => setShowBulkDeleteDialog(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<KnowledgeDropdown />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue