make knowledge search a component used for knowledge and chunk page

This commit is contained in:
Cole Goldsmith 2025-10-06 10:40:03 -05:00
parent 16b9784c2f
commit 2547af298f
4 changed files with 171 additions and 184 deletions

View file

@ -321,7 +321,7 @@ export function KnowledgeFilterPanel() {
className="font-mono placeholder:font-mono" className="font-mono placeholder:font-mono"
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
rows={2} rows={2}
disabled={!!queryOverride} disabled={!!queryOverride && !createMode}
/> />
</div> </div>

View 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>
);
};

View file

@ -6,15 +6,13 @@ import { useRouter, useSearchParams } from "next/navigation";
import { ProtectedRoute } from "@/components/protected-route"; import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
import { useTask } from "@/contexts/task-context";
import { import {
type ChunkResult, type ChunkResult,
type File, type File,
useGetSearchQuery, useGetSearchQuery,
} from "../../api/queries/useGetSearchQuery"; } from "../../api/queries/useGetSearchQuery";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { KnowledgeSearchInput } from "@/components/knowledge-search-input";
const getFileTypeLabel = (mimetype: string) => { const getFileTypeLabel = (mimetype: string) => {
if (mimetype === "application/pdf") return "PDF"; if (mimetype === "application/pdf") return "PDF";
@ -26,8 +24,7 @@ const getFileTypeLabel = (mimetype: string) => {
function ChunksPageContent() { function ChunksPageContent() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { isMenuOpen } = useTask(); const { parsedFilterData, queryOverride } = useKnowledgeFilter();
const { parsedFilterData, isPanelOpen } = useKnowledgeFilter();
const filename = searchParams.get("filename"); const filename = searchParams.get("filename");
const [chunks, setChunks] = useState<ChunkResult[]>([]); const [chunks, setChunks] = useState<ChunkResult[]>([]);
@ -47,25 +44,25 @@ function ChunksPageContent() {
[chunks] [chunks]
); );
const [selectAll, setSelectAll] = useState(false); // const [selectAll, setSelectAll] = useState(false);
const [queryInputText, setQueryInputText] = useState(
parsedFilterData?.query ?? ""
);
// Use the same search query as the knowledge page, but we'll filter for the specific file // 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(() => { // useEffect(() => {
if (queryInputText === "") { // if (queryInputText === "") {
setChunksFilteredByQuery(chunks); // setChunksFilteredByQuery(chunks);
} else { // } else {
setChunksFilteredByQuery( // setChunksFilteredByQuery(
chunks.filter((chunk) => // chunks.filter((chunk) =>
chunk.text.toLowerCase().includes(queryInputText.toLowerCase()) // chunk.text.toLowerCase().includes(queryInputText.toLowerCase())
) // )
); // );
} // }
}, [queryInputText, chunks]); // }, [queryInputText, chunks]);
const handleCopy = useCallback((text: string, index: number) => { const handleCopy = useCallback((text: string, index: number) => {
// Trim whitespace and remove new lines/tabs for cleaner copy // Trim whitespace and remove new lines/tabs for cleaner copy
@ -89,13 +86,13 @@ function ChunksPageContent() {
}, [data, filename]); }, [data, filename]);
// Set selected state for all checkboxes when selectAll changes // Set selected state for all checkboxes when selectAll changes
useEffect(() => { // useEffect(() => {
if (selectAll) { // if (selectAll) {
setSelectedChunks(new Set(chunks.map((_, index) => index))); // setSelectedChunks(new Set(chunks.map((_, index) => index)));
} else { // } else {
setSelectedChunks(new Set()); // setSelectedChunks(new Set());
} // }
}, [selectAll, setSelectedChunks, chunks]); // }, [selectAll, setSelectedChunks, chunks]);
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
router.push("/knowledge"); router.push("/knowledge");
@ -131,25 +128,17 @@ function ChunksPageContent() {
} }
return ( return (
<div <div className="flex">
className={`fixed inset-0 md:left-72 top-[53px] flex flex-row transition-all duration-300 ${ <div className="flex-1 flex flex-col min-h-0">
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">
{/* Header */} {/* Header */}
<div className="flex flex-col mb-6"> <div className="flex flex-col mb-6 gap-6">
<div className="flex flex-row items-center gap-3 mb-6"> <div className="flex flex-row items-center">
<Button variant="ghost" onClick={handleBack} size="sm"> <Button
variant="ghost"
onClick={handleBack}
size="sm"
className="max-h-8 max-w-8 -m-2 mr-1"
>
<ArrowLeft size={24} /> <ArrowLeft size={24} />
</Button> </Button>
<h1 className="text-lg font-semibold"> <h1 className="text-lg font-semibold">
@ -157,39 +146,12 @@ function ChunksPageContent() {
{filename.replace(/\.[^/.]+$/, "")} {filename.replace(/\.[^/.]+$/, "")}
</h1> </h1>
</div> </div>
<div className="flex flex-col items-start mt-2"> {/* Search input */}
<div className="flex-1 flex items-center gap-2 w-full max-w-[616px] mb-8"> <KnowledgeSearchInput />
<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>
</div> </div>
{/* Content Area - matches knowledge page structure */} {/* Content Area - matches knowledge page structure */}
<div className="flex-1 overflow-scroll pr-6"> <div className="flex-1 pr-6">
{isFetching ? ( {isFetching ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
@ -211,7 +173,23 @@ function ChunksPageContent() {
</div> </div>
) : ( ) : (
<div className="space-y-4 pb-6"> <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 <div
key={chunk.filename + index} key={chunk.filename + index}
className="bg-muted rounded-lg p-4 border border-border/50" className="bg-muted rounded-lg p-4 border border-border/50"

View file

@ -2,13 +2,10 @@
import { themeQuartz, type ColDef } from "ag-grid-community"; import { themeQuartz, type ColDef } from "ag-grid-community";
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; 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 { useRouter } from "next/navigation";
import { import {
type ChangeEvent,
FormEvent,
useCallback, useCallback,
useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
@ -25,11 +22,10 @@ import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdow
import { StatusBadge } from "@/components/ui/status-badge"; import { StatusBadge } from "@/components/ui/status-badge";
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog"; import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
import { useDeleteDocument } from "../api/mutations/useDeleteDocument"; import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
import GoogleDriveIcon from "../settings/icons/google-drive-icon"; import GoogleDriveIcon from "../settings/icons/google-drive-icon";
import OneDriveIcon from "../settings/icons/one-drive-icon"; import OneDriveIcon from "../settings/icons/one-drive-icon";
import SharePointIcon from "../settings/icons/share-point-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 to get the appropriate icon for a connector type
function getSourceIcon(connectorType?: string) { function getSourceIcon(connectorType?: string) {
@ -57,13 +53,9 @@ function SearchPage() {
const router = useRouter(); const router = useRouter();
const { files: taskFiles } = useTask(); const { files: taskFiles } = useTask();
const { const {
selectedFilter,
setSelectedFilter,
parsedFilterData, parsedFilterData,
queryOverride, queryOverride,
setQueryOverride,
} = useKnowledgeFilter(); } = useKnowledgeFilter();
const [searchQueryInput, setSearchQueryInput] = useState(queryOverride || "");
const [selectedRows, setSelectedRows] = useState<File[]>([]); const [selectedRows, setSelectedRows] = useState<File[]>([]);
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
@ -74,14 +66,6 @@ function SearchPage() {
parsedFilterData 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 // Convert TaskFiles to File format and merge with backend results
const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => {
return { return {
@ -246,11 +230,6 @@ function SearchPage() {
} }
}; };
// Reset the query text when the selected filter changes
useEffect(() => {
setSearchQueryInput(queryOverride);
}, [queryOverride]);
return ( return (
<> <>
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
@ -260,78 +239,9 @@ function SearchPage() {
{/* Search Input Area */} {/* Search Input Area */}
<div className="flex-1 flex flex-shrink-0 flex-wrap-reverse gap-3 mb-6"> <div className="flex-1 flex flex-shrink-0 flex-wrap-reverse gap-3 mb-6">
<form <KnowledgeSearchInput />
className="flex flex-1 gap-3 max-w-full" {/* //TODO: Implement sync button */}
onSubmit={handleSearch} {/* <Button
>
<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
type="button" type="button"
variant="outline" variant="outline"
className="rounded-lg flex-shrink-0" className="rounded-lg flex-shrink-0"
@ -339,17 +249,16 @@ function SearchPage() {
> >
Sync Sync
</Button> */} </Button> */}
{selectedRows.length > 0 && ( {selectedRows.length > 0 && (
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
className="rounded-lg flex-shrink-0" className="rounded-lg flex-shrink-0"
onClick={() => setShowBulkDeleteDialog(true)} onClick={() => setShowBulkDeleteDialog(true)}
> >
Delete Delete
</Button> </Button>
)} )}
</form>
<div className="ml-auto"> <div className="ml-auto">
<KnowledgeDropdown /> <KnowledgeDropdown />
</div> </div>