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"
onChange={(e) => setQuery(e.target.value)}
rows={2}
disabled={!!queryOverride}
disabled={!!queryOverride && !createMode}
/>
</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 { 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"

View file

@ -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>