From 3a1bde1bb5a6260ab0ade4a5e2ffb713f0ac6d46 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Thu, 2 Oct 2025 17:17:09 -0500 Subject: [PATCH 01/11] refactor page layout styles to use grid --- frontend/components/knowledge-dropdown.tsx | 110 +++++------------- .../components/knowledge-filter-panel.tsx | 4 +- frontend/src/app/globals.css | 25 ++++ frontend/src/app/knowledge/page.tsx | 49 +++----- frontend/src/app/settings/page.tsx | 50 ++++---- frontend/src/components/layout-wrapper.tsx | 69 ++++++----- .../src/components/task-notification-menu.tsx | 4 +- 7 files changed, 145 insertions(+), 166 deletions(-) diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index ee49fc3a..c70fb234 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -6,7 +6,6 @@ import { FolderOpen, Loader2, PlugZap, - Plus, Upload, } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -25,15 +24,7 @@ import { Label } from "@/components/ui/label"; import { useTask } from "@/contexts/task-context"; import { cn } from "@/lib/utils"; -interface KnowledgeDropdownProps { - active?: boolean; - variant?: "navigation" | "button"; -} - -export function KnowledgeDropdown({ - active, - variant = "navigation", -}: KnowledgeDropdownProps) { +export function KnowledgeDropdown() { const { addTask } = useTask(); const router = useRouter(); const [isOpen, setIsOpen] = useState(false); @@ -437,77 +428,35 @@ export function KnowledgeDropdown({ return ( <>
- + /> + )} + + {isOpen && !isLoading && (
@@ -519,12 +468,13 @@ export function KnowledgeDropdown({ disabled={"disabled" in item ? item.disabled : false} title={"tooltip" in item ? item.tooltip : undefined} className={cn( - "w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground", + "w-full flex flex-nowrap items-center px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground", "disabled" in item && item.disabled && "opacity-50 cursor-not-allowed hover:bg-transparent hover:text-current" )} > + {item.icon && } {item.label} ))} @@ -561,7 +511,7 @@ export function KnowledgeDropdown({ type="text" placeholder="/path/to/documents" value={folderPath} - onChange={e => setFolderPath(e.target.value)} + onChange={(e) => setFolderPath(e.target.value)} />
@@ -603,7 +553,7 @@ export function KnowledgeDropdown({ type="text" placeholder="s3://bucket/path" value={bucketUrl} - onChange={e => setBucketUrl(e.target.value)} + onChange={(e) => setBucketUrl(e.target.value)} />
diff --git a/frontend/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index 6bf9285b..a45304e9 100644 --- a/frontend/components/knowledge-filter-panel.tsx +++ b/frontend/components/knowledge-filter-panel.tsx @@ -231,8 +231,8 @@ export function KnowledgeFilterPanel() { }; return ( -
- +
+
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 150d1c56..22601457 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -53,6 +53,8 @@ --flow-icon: #2f67d0; --radius: 0.5rem; + + --app-header-height: 53px; } .dark { @@ -108,6 +110,29 @@ } @layer components { + .app-grid-cols-arrangement { + --sidebar-width: 0px; + --notifications-width: 0px; + --filters-width: 0px; + + @media (width >= 48rem) { + --sidebar-width: 288px; + } + + &.notifications-open { + --notifications-width: 320px; + } + &.filters-open { + --filters-width: 320px; + } + display: grid; + height: calc(100% - var(--app-header-height)); + grid-template-columns: var(--sidebar-width) 1fr var(--notifications-width) var( + --filters-width + ); + transition: grid-template-columns 0.3s ease-in-out; + } + .header-arrangement { @apply flex w-full h-[53px] items-center justify-between border-b border-border; } diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 1b8b60ef..34595eb2 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -1,8 +1,8 @@ "use client"; -import type { ColDef } from "ag-grid-community"; +import { themeQuartz, type ColDef } from "ag-grid-community"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; -import { Building2, Cloud, HardDrive, Search, Trash2, X } from "lucide-react"; +import { Building2, Cloud, HardDrive, Search, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { type ChangeEvent, useCallback, useRef, useState } from "react"; import { SiGoogledrive } from "react-icons/si"; @@ -46,8 +46,8 @@ function getSourceIcon(connectorType?: string) { function SearchPage() { const router = useRouter(); - const { isMenuOpen, files: taskFiles } = useTask(); - const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } = + const { files: taskFiles } = useTask(); + const { selectedFilter, setSelectedFilter, parsedFilterData } = useKnowledgeFilter(); const [selectedRows, setSelectedRows] = useState([]); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); @@ -144,7 +144,6 @@ function SearchPage() { { field: "avgScore", headerName: "Avg score", - initialFlex: 0.5, cellRenderer: ({ value }: CustomCellRendererProps) => { return ( @@ -229,32 +228,19 @@ function SearchPage() { }; return ( -
-
+ <> +
-

Project Knowledge

- +

Knowledge

{/* Search Input Area */} -
-
-
+
+ +
{selectedFilter?.name && (
setShowBulkDeleteDialog(true)} > - Delete + Delete )} +
+ +
params.data.filename} domLayout="normal" + theme={themeQuartz.withParams({ browserColorScheme: "inherit" })} onSelectionChanged={onSelectionChanged} noRowsOverlayComponent={() => (
@@ -347,15 +337,12 @@ function SearchPage() { selectedRows.length } document${ selectedRows.length > 1 ? "s" : "" - }? This will remove all chunks and data associated with these documents. This action cannot be undone. - -Documents to be deleted: -${selectedRows.map((row) => `• ${row.filename}`).join("\n")}`} + }? This will remove all chunks and data associated with these documents. This action cannot be undone.`} confirmText="Delete All" onConfirm={handleBulkDelete} isLoading={deleteDocumentMutation.isPending} /> -
+ ); } diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 7530bcbb..587d3946 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -114,7 +114,8 @@ function KnowledgeSourcesPage() { const [chunkOverlap, setChunkOverlap] = useState(50); const [tableStructure, setTableStructure] = useState(false); const [ocr, setOcr] = useState(false); - const [pictureDescriptions, setPictureDescriptions] = useState(false); + const [pictureDescriptions, setPictureDescriptions] = + useState(false); // Fetch settings using React Query const { data: settings = {} } = useGetSettingsQuery({ @@ -164,7 +165,7 @@ function KnowledgeSourcesPage() { onSuccess: () => { console.log("Setting updated successfully"); }, - onError: error => { + onError: (error) => { console.error("Failed to update setting:", error.message); }, }); @@ -303,8 +304,8 @@ function KnowledgeSourcesPage() { // Initialize connectors list with metadata from backend const initialConnectors = connectorTypes - .filter(type => connectorsResult.connectors[type].available) // Only show available connectors - .map(type => ({ + .filter((type) => connectorsResult.connectors[type].available) // Only show available connectors + .map((type) => ({ id: type, name: connectorsResult.connectors[type].name, description: connectorsResult.connectors[type].description, @@ -327,8 +328,8 @@ function KnowledgeSourcesPage() { ); const isConnected = activeConnection !== undefined; - setConnectors(prev => - prev.map(c => + setConnectors((prev) => + prev.map((c) => c.type === connectorType ? { ...c, @@ -347,7 +348,7 @@ function KnowledgeSourcesPage() { const handleConnect = async (connector: Connector) => { setIsConnecting(connector.id); - setSyncResults(prev => ({ ...prev, [connector.id]: null })); + setSyncResults((prev) => ({ ...prev, [connector.id]: null })); try { // Use the shared auth callback URL, same as connectors page @@ -508,9 +509,9 @@ function KnowledgeSourcesPage() { // Watch for task completions and refresh stats useEffect(() => { // Find newly completed tasks by comparing with previous state - const newlyCompletedTasks = tasks.filter(task => { + const newlyCompletedTasks = tasks.filter((task) => { const wasCompleted = - prevTasks.find(prev => prev.task_id === task.task_id)?.status === + prevTasks.find((prev) => prev.task_id === task.task_id)?.status === "completed"; return task.status === "completed" && !wasCompleted; }); @@ -564,7 +565,7 @@ function KnowledgeSourcesPage() { fetch(`/api/reset-flow/retrieval`, { method: "POST", }) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } @@ -577,7 +578,7 @@ function KnowledgeSourcesPage() { handleModelChange(DEFAULT_AGENT_SETTINGS.llm_model); closeDialog(); // Close after successful completion }) - .catch(error => { + .catch((error) => { console.error("Error restoring retrieval flow:", error); closeDialog(); // Close even on error (could show error toast instead) }); @@ -587,7 +588,7 @@ function KnowledgeSourcesPage() { fetch(`/api/reset-flow/ingest`, { method: "POST", }) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } @@ -602,14 +603,14 @@ function KnowledgeSourcesPage() { setPictureDescriptions(false); closeDialog(); // Close after successful completion }) - .catch(error => { + .catch((error) => { console.error("Error restoring ingest flow:", error); closeDialog(); // Close even on error (could show error toast instead) }); }; return ( -
+
{/* Connectors Section */}
@@ -700,7 +701,7 @@ function KnowledgeSourcesPage() { {/* Connectors Grid */}
- {connectors.map(connector => ( + {connectors.map((connector) => (
@@ -834,7 +835,7 @@ function KnowledgeSourcesPage() { } confirmText="Proceed" confirmIcon={} - onConfirm={closeDialog => + onConfirm={(closeDialog) => handleEditInLangflow("chat", closeDialog) } variant="warning" @@ -854,7 +855,8 @@ function KnowledgeSourcesPage() { filter.name .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), + .includes(filterSearchTerm.toLowerCase()) ) .map((filter, index) => (
); diff --git a/frontend/src/contexts/knowledge-filter-context.tsx b/frontend/src/contexts/knowledge-filter-context.tsx index 043f6fae..1e23fdf0 100644 --- a/frontend/src/contexts/knowledge-filter-context.tsx +++ b/frontend/src/contexts/knowledge-filter-context.tsx @@ -44,6 +44,8 @@ interface KnowledgeFilterContextType { createMode: boolean; startCreateMode: () => void; endCreateMode: () => void; + queryOverride: string; + setQueryOverride: (query: string) => void; } const KnowledgeFilterContext = createContext< @@ -73,6 +75,7 @@ export function KnowledgeFilterProvider({ useState(null); const [isPanelOpen, setIsPanelOpen] = useState(false); const [createMode, setCreateMode] = useState(false); + const [queryOverride, setQueryOverride] = useState(''); const setSelectedFilter = (filter: KnowledgeFilter | null) => { setSelectedFilterState(filter); @@ -148,6 +151,8 @@ export function KnowledgeFilterProvider({ createMode, startCreateMode, endCreateMode, + queryOverride, + setQueryOverride, }; return ( From dd5db8acc1e7d5cec2e6ed2dddac58348734a660 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Fri, 3 Oct 2025 17:18:38 -0500 Subject: [PATCH 05/11] cleaning up search override functionality --- .../components/knowledge-filter-panel.tsx | 2 ++ frontend/src/app/knowledge/page.tsx | 34 +++++++++++++++++-- frontend/src/components/layout-wrapper.tsx | 2 +- .../src/contexts/knowledge-filter-context.tsx | 8 ++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/frontend/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index a45304e9..e8c26e37 100644 --- a/frontend/components/knowledge-filter-panel.tsx +++ b/frontend/components/knowledge-filter-panel.tsx @@ -50,6 +50,7 @@ export const filterAccentClasses: Record = { export function KnowledgeFilterPanel() { const { + queryOverride, selectedFilter, parsedFilterData, setSelectedFilter, @@ -320,6 +321,7 @@ export function KnowledgeFilterPanel() { className="font-mono placeholder:font-mono" onChange={(e) => setQuery(e.target.value)} rows={2} + disabled={!!queryOverride} />
diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 9e135ddd..1cbc7563 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -2,12 +2,13 @@ import { themeQuartz, type ColDef } from "ag-grid-community"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; -import { Cloud, FileIcon, Search, X } from "lucide-react"; +import { ArrowRight, Cloud, FileIcon, Search, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { type ChangeEvent, FormEvent, useCallback, + useEffect, useRef, useState, } from "react"; @@ -28,6 +29,7 @@ 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"; // Function to get the appropriate icon for a connector type function getSourceIcon(connectorType?: string) { @@ -244,6 +246,11 @@ function SearchPage() { } }; + // Reset the query text when the selected filter changes + useEffect(() => { + setSearchQueryInput(queryOverride); + }, [queryOverride]); + return ( <>
@@ -257,7 +264,7 @@ function SearchPage() { className="flex flex-1 gap-3 max-w-full" onSubmit={handleSearch} > -
+
{selectedFilter?.name && (
+ {queryOverride && ( + + )} +
{/* + )} + +
+ + ); +}; diff --git a/frontend/src/app/knowledge/chunks/page.tsx b/frontend/src/app/knowledge/chunks/page.tsx index cb96eddc..c6a3d5e8 100644 --- a/frontend/src/app/knowledge/chunks/page.tsx +++ b/frontend/src/app/knowledge/chunks/page.tsx @@ -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([]); @@ -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 ( -
-
+
+
{/* Header */} -
-
-

@@ -157,39 +146,12 @@ function ChunksPageContent() { {filename.replace(/\.[^/.]+$/, "")}

-
-
- : null} - id="search-query" - type="text" - defaultValue={parsedFilterData?.query} - value={queryInputText} - onChange={(e) => setQueryInputText(e.target.value)} - placeholder="Search chunks..." - /> -
-
- - setSelectAll(!!handleSelectAll) - } - /> - -
-
+ {/* Search input */} +
{/* Content Area - matches knowledge page structure */} -
+
{isFetching ? (
@@ -211,7 +173,23 @@ function ChunksPageContent() {
) : (
- {chunksFilteredByQuery.map((chunk, index) => ( + {/* TODO - add chunk selection when sync and delete are ready */} + {/*
+ + setSelectAll(!!handleSelectAll) + } + /> + +
*/} + {chunks.map((chunk, index) => (
([]); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); @@ -74,14 +66,6 @@ function SearchPage() { parsedFilterData ); - const handleSearch = useCallback( - (e?: FormEvent) => { - 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 ( <>
@@ -260,78 +239,9 @@ function SearchPage() { {/* Search Input Area */}
-
-
- {selectedFilter?.name && ( -
- {selectedFilter?.name} - setSelectedFilter(null)} - /> -
- )} - - ) => - setSearchQueryInput(e.target.value) - } - /> - {queryOverride && ( - - )} - -
- {/* */} - {/* //TODO: Implement sync button */} - {/* */} - {selectedRows.length > 0 && ( - - )} -
+ {selectedRows.length > 0 && ( + + )}
From 9d1aced1e748773310e4da9b02da2d98ab8d8873 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Mon, 6 Oct 2025 12:02:36 -0500 Subject: [PATCH 07/11] refactor the grid layout to inlcude banner and header rows --- frontend/src/app/globals.css | 31 ++++++-- frontend/src/app/knowledge/chunks/page.tsx | 2 - frontend/src/components/layout-wrapper.tsx | 89 ++++++++++------------ frontend/src/contexts/layout-context.tsx | 34 --------- 4 files changed, 64 insertions(+), 92 deletions(-) delete mode 100644 frontend/src/contexts/layout-context.tsx diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e3f4c154..630cdf53 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -110,31 +110,46 @@ } @layer components { - .app-grid-cols-arrangement { + .app-grid-arrangement { --sidebar-width: 0px; --notifications-width: 0px; --filters-width: 0px; + --app-header-height: 53px; + --top-banner-height: 0px; @media (width >= 48rem) { --sidebar-width: 288px; } - &.notifications-open { --notifications-width: 320px; } &.filters-open { --filters-width: 320px; } + &.banner-visible { + --top-banner-height: 52px; + } display: grid; - height: calc(100% - var(--app-header-height)); - grid-template-columns: var(--sidebar-width) 1fr var(--notifications-width) var( - --filters-width - ); - transition: grid-template-columns 0.3s ease-in-out; + height: 100%; + width: 100%; + grid-template-rows: + var(--top-banner-height) + var(--app-header-height) + 1fr; + grid-template-columns: + var(--sidebar-width) + 1fr + var(--notifications-width) + var(--filters-width); + grid-template-areas: + "banner banner banner banner" + "header header header header" + "nav main notifications filters"; + transition: grid-template-columns 0.25s ease-in-out; } .header-arrangement { - @apply flex w-full h-[53px] items-center justify-between border-b border-border; + @apply flex w-full items-center justify-between border-b border-border; } .header-start-display { diff --git a/frontend/src/app/knowledge/chunks/page.tsx b/frontend/src/app/knowledge/chunks/page.tsx index e1ea9ed9..3c8b4339 100644 --- a/frontend/src/app/knowledge/chunks/page.tsx +++ b/frontend/src/app/knowledge/chunks/page.tsx @@ -6,8 +6,6 @@ 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 { useLayout } from "@/contexts/layout-context"; -import { useTask } from "@/contexts/task-context"; import { type ChunkResult, type File, diff --git a/frontend/src/components/layout-wrapper.tsx b/frontend/src/components/layout-wrapper.tsx index b89ab5d5..dbd04fea 100644 --- a/frontend/src/components/layout-wrapper.tsx +++ b/frontend/src/components/layout-wrapper.tsx @@ -16,7 +16,6 @@ import { UserNav } from "@/components/user-nav"; import { useAuth } from "@/contexts/auth-context"; import { useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; -import { LayoutProvider } from "@/contexts/layout-context"; // import { GitHubStarButton } from "@/components/github-star-button" // import { DiscordLink } from "@/components/discord-link" import { useTask } from "@/contexts/task-context"; @@ -34,7 +33,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { refreshConversations, startNewConversation, } = useChat(); - const { isLoading: isSettingsLoading, data: settings } = useGetSettingsQuery({ + const { isLoading: isSettingsLoading } = useGetSettingsQuery({ enabled: isAuthenticated || isNoAuthMode, }); const { @@ -75,14 +74,6 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { const isUnhealthy = health?.status === "unhealthy" || isError; const isBannerVisible = !isHealthLoading && isUnhealthy; - // Dynamic height calculations based on banner visibility - const headerHeight = 53; - const bannerHeight = 52; // Approximate banner height - const totalTopOffset = isBannerVisible - ? headerHeight + bannerHeight - : headerHeight; - const mainContentHeight = `calc(100vh - ${totalTopOffset}px)`; - // Show loading state when backend isn't ready if (isLoading || isSettingsLoading) { return ( @@ -102,9 +93,18 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { // For all other pages, render with Langflow-styled navigation and task menu return ( -
- -
+
+
+ +
+
{/* Logo/Title */}
@@ -144,44 +144,37 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
-
- {/* Sidebar Navigation */} - - {/* Main Content */} -
-
- {children} -
-
+ {/* Sidebar Navigation */} + - {/* Task Notifications Panel */} - + {/* Main Content */} +
+
+ {children} +
+
- {/* Knowledge Filter Panel */} - -
+ {/* Task Notifications Panel */} + + + {/* Knowledge Filter Panel */} +
); } diff --git a/frontend/src/contexts/layout-context.tsx b/frontend/src/contexts/layout-context.tsx deleted file mode 100644 index f40ea28c..00000000 --- a/frontend/src/contexts/layout-context.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { createContext, useContext } from "react"; - -interface LayoutContextType { - headerHeight: number; - totalTopOffset: number; -} - -const LayoutContext = createContext(undefined); - -export function useLayout() { - const context = useContext(LayoutContext); - if (context === undefined) { - throw new Error("useLayout must be used within a LayoutProvider"); - } - return context; -} - -export function LayoutProvider({ - children, - headerHeight, - totalTopOffset -}: { - children: React.ReactNode; - headerHeight: number; - totalTopOffset: number; -}) { - return ( - - {children} - - ); -} \ No newline at end of file From 983eb82d4fe79011304ea541a243a0a66d0dc363 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Mon, 6 Oct 2025 13:15:21 -0500 Subject: [PATCH 08/11] styles --- frontend/src/app/globals.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 630cdf53..5a2ff074 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -145,7 +145,8 @@ "banner banner banner banner" "header header header header" "nav main notifications filters"; - transition: grid-template-columns 0.25s ease-in-out; + transition: grid-template-columns 0.25s ease-in-out, + grid-template-rows 0.25s ease-in-out; } .header-arrangement { From 99fd1d7215823e9ea862b2a8e10802035d27c9fa Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Mon, 6 Oct 2025 13:49:45 -0500 Subject: [PATCH 09/11] fix merge conflicts --- frontend/components/knowledge-dropdown.tsx | 60 +- frontend/src/app/knowledge/page.tsx | 656 ++++++++++----------- 2 files changed, 310 insertions(+), 406 deletions(-) diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index 7fe84259..d9f92355 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -7,7 +7,6 @@ import { FolderOpen, Loader2, PlugZap, - Plus, Upload, } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -29,15 +28,7 @@ import { useTask } from "@/contexts/task-context"; import { cn } from "@/lib/utils"; import type { File as SearchFile } from "@/src/app/api/queries/useGetSearchQuery"; -interface KnowledgeDropdownProps { - active?: boolean; - variant?: "navigation" | "button"; -} - -export function KnowledgeDropdown({ - active, - variant = "navigation", -}: KnowledgeDropdownProps) { +export function KnowledgeDropdown() { const { addTask } = useTask(); const { refetch: refetchTasks } = useGetTasksQuery(); const queryClient = useQueryClient(); @@ -498,28 +489,16 @@ export function KnowledgeDropdown({ return ( <>
- + {isOpen && !isLoading && (
diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 64eeb49c..e7dbac08 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -2,22 +2,19 @@ import type { ColDef, GetRowIdParams } from "ag-grid-community"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; -import { Building2, Cloud, HardDrive, Search, Trash2, X } from "lucide-react"; +import { Cloud, FileIcon, Search, Trash2, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { - type ChangeEvent, - useCallback, - useEffect, - useRef, - useState, + type ChangeEvent, + useCallback, + useEffect, + useRef, + useState, } from "react"; -import { SiGoogledrive } from "react-icons/si"; -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 { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; -import { useLayout } from "@/contexts/layout-context"; import { useTask } from "@/contexts/task-context"; import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery"; import "@/components/AgGrid/registerAgGridModules"; @@ -28,319 +25,272 @@ import { filterAccentClasses } from "@/components/knowledge-filter-panel"; import { StatusBadge } from "@/components/ui/status-badge"; import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog"; import { useDeleteDocument } from "../api/mutations/useDeleteDocument"; +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 { KnowledgeSearchInput } from "@/components/knowledge-search-input"; // Function to get the appropriate icon for a connector type function getSourceIcon(connectorType?: string) { - switch (connectorType) { - case "google_drive": - return ( - - ); - case "onedrive": - return ( - - ); - case "sharepoint": - return ; - case "s3": - return ; - default: - return ( - - ); - } + switch (connectorType) { + case "google_drive": + return ( + + ); + case "onedrive": + return ; + case "sharepoint": + return ( + + ); + case "s3": + return ; + default: + return ( + + ); + } } function SearchPage() { - const router = useRouter(); - const { isMenuOpen, files: taskFiles, refreshTasks } = useTask(); - const { totalTopOffset } = useLayout(); - const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } = - useKnowledgeFilter(); - const [selectedRows, setSelectedRows] = useState([]); - const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); + const router = useRouter(); + const { isMenuOpen, files: taskFiles, refreshTasks } = useTask(); + const { + selectedFilter, + setSelectedFilter, + parsedFilterData, + isPanelOpen, + queryOverride, + } = useKnowledgeFilter(); + const [selectedRows, setSelectedRows] = useState([]); + const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); - const deleteDocumentMutation = useDeleteDocument(); + const deleteDocumentMutation = useDeleteDocument(); - useEffect(() => { - refreshTasks(); - }, [refreshTasks]); + useEffect(() => { + refreshTasks(); + }, [refreshTasks]); - const { data: searchData = [], isFetching } = useGetSearchQuery( - parsedFilterData?.query || "*", - parsedFilterData, - ); - // Convert TaskFiles to File format and merge with backend results - const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { - return { - filename: taskFile.filename, - mimetype: taskFile.mimetype, - source_url: taskFile.source_url, - size: taskFile.size, - connector_type: taskFile.connector_type, - status: taskFile.status, - }; - }); + const { data: searchData = [], isFetching } = useGetSearchQuery( + queryOverride, + parsedFilterData + ); + // Convert TaskFiles to File format and merge with backend results + const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { + return { + filename: taskFile.filename, + mimetype: taskFile.mimetype, + source_url: taskFile.source_url, + size: taskFile.size, + connector_type: taskFile.connector_type, + status: taskFile.status, + }; + }); - // Create a map of task files by filename for quick lookup - const taskFileMap = new Map( - taskFilesAsFiles.map((file) => [file.filename, file]), - ); + // Create a map of task files by filename for quick lookup + const taskFileMap = new Map( + taskFilesAsFiles.map((file) => [file.filename, file]) + ); - // Override backend files with task file status if they exist - const backendFiles = (searchData as File[]) - .map((file) => { - const taskFile = taskFileMap.get(file.filename); - if (taskFile) { - // Override backend file with task file data (includes status) - return { ...file, ...taskFile }; - } - return file; - }) - .filter((file) => { - // Only filter out files that are currently processing AND in taskFiles - const taskFile = taskFileMap.get(file.filename); - return !taskFile || taskFile.status !== "processing"; - }); + // Override backend files with task file status if they exist + const backendFiles = (searchData as File[]) + .map((file) => { + const taskFile = taskFileMap.get(file.filename); + if (taskFile) { + // Override backend file with task file data (includes status) + return { ...file, ...taskFile }; + } + return file; + }) + .filter((file) => { + // Only filter out files that are currently processing AND in taskFiles + const taskFile = taskFileMap.get(file.filename); + return !taskFile || taskFile.status !== "processing"; + }); - const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => { - return ( - taskFile.status !== "active" && - !backendFiles.some( - (backendFile) => backendFile.filename === taskFile.filename, - ) - ); - }); + const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => { + return ( + taskFile.status !== "active" && + !backendFiles.some( + (backendFile) => backendFile.filename === taskFile.filename + ) + ); + }); - // Combine task files first, then backend files - const fileResults = [...backendFiles, ...filteredTaskFiles]; + // Combine task files first, then backend files + const fileResults = [...backendFiles, ...filteredTaskFiles]; - const handleTableSearch = (e: ChangeEvent) => { - gridRef.current?.api.setGridOption("quickFilterText", e.target.value); - }; + const gridRef = useRef(null); - const gridRef = useRef(null); + const columnDefs = [ + { + field: "filename", + headerName: "Source", + checkboxSelection: (params: CustomCellRendererProps) => + (params?.data?.status || "active") === "active", + headerCheckboxSelection: true, + initialFlex: 2, + minWidth: 220, + cellRenderer: ({ data, value }: CustomCellRendererProps) => { + // Read status directly from data on each render + const status = data?.status || "active"; + const isActive = status === "active"; + console.log(data?.filename, status, "a"); + return ( +
+
+ +
+ ); + }, + }, + { + field: "size", + headerName: "Size", + valueFormatter: (params: CustomCellRendererProps) => + params.value ? `${Math.round(params.value / 1024)} KB` : "-", + }, + { + field: "mimetype", + headerName: "Type", + }, + { + field: "owner", + headerName: "Owner", + valueFormatter: (params: CustomCellRendererProps) => + params.data?.owner_name || params.data?.owner_email || "—", + }, + { + field: "chunkCount", + headerName: "Chunks", + valueFormatter: (params: CustomCellRendererProps) => + params.data?.chunkCount?.toString() || "-", + }, + { + field: "avgScore", + headerName: "Avg score", + cellRenderer: ({ value }: CustomCellRendererProps) => { + return ( + + {value?.toFixed(2) ?? "-"} + + ); + }, + }, + { + field: "status", + headerName: "Status", + cellRenderer: ({ data }: CustomCellRendererProps) => { + console.log(data?.filename, data?.status, "b"); + // Default to 'active' status if no status is provided + const status = data?.status || "active"; + return ; + }, + }, + { + cellRenderer: ({ data }: CustomCellRendererProps) => { + const status = data?.status || "active"; + if (status !== "active") { + return null; + } + return ; + }, + cellStyle: { + alignItems: "center", + display: "flex", + justifyContent: "center", + padding: 0, + }, + colId: "actions", + filter: false, + minWidth: 0, + width: 40, + resizable: false, + sortable: false, + initialFlex: 0, + }, + ]; - const columnDefs = [ - { - field: "filename", - headerName: "Source", - checkboxSelection: (params: CustomCellRendererProps) => - (params?.data?.status || "active") === "active", - headerCheckboxSelection: true, - initialFlex: 2, - minWidth: 220, - cellRenderer: ({ data, value }: CustomCellRendererProps) => { - // Read status directly from data on each render - const status = data?.status || "active"; - const isActive = status === "active"; - console.log(data?.filename, status, "a"); - return ( -
-
- -
- ); - }, - }, - { - field: "size", - headerName: "Size", - valueFormatter: (params: CustomCellRendererProps) => - params.value ? `${Math.round(params.value / 1024)} KB` : "-", - }, - { - field: "mimetype", - headerName: "Type", - }, - { - field: "owner", - headerName: "Owner", - valueFormatter: (params: CustomCellRendererProps) => - params.data?.owner_name || params.data?.owner_email || "—", - }, - { - field: "chunkCount", - headerName: "Chunks", - valueFormatter: (params: CustomCellRendererProps) => params.data?.chunkCount?.toString() || "-", - }, - { - field: "avgScore", - headerName: "Avg score", - initialFlex: 0.5, - cellRenderer: ({ value }: CustomCellRendererProps) => { - return ( - - {value?.toFixed(2) ?? "-"} - - ); - }, - }, - { - field: "status", - headerName: "Status", - cellRenderer: ({ data }: CustomCellRendererProps) => { - console.log(data?.filename, data?.status, "b"); - // Default to 'active' status if no status is provided - const status = data?.status || "active"; - return ; - }, - }, - { - cellRenderer: ({ data }: CustomCellRendererProps) => { - const status = data?.status || "active"; - if (status !== "active") { - return null; - } - return ; - }, - cellStyle: { - alignItems: "center", - display: "flex", - justifyContent: "center", - padding: 0, - }, - colId: "actions", - filter: false, - minWidth: 0, - width: 40, - resizable: false, - sortable: false, - initialFlex: 0, - }, - ]; + const defaultColDef: ColDef = { + resizable: false, + suppressMovable: true, + initialFlex: 1, + minWidth: 100, + }; - const defaultColDef: ColDef = { - resizable: false, - suppressMovable: true, - initialFlex: 1, - minWidth: 100, - }; + const onSelectionChanged = useCallback(() => { + if (gridRef.current) { + const selectedNodes = gridRef.current.api.getSelectedRows(); + setSelectedRows(selectedNodes); + } + }, []); - const onSelectionChanged = useCallback(() => { - if (gridRef.current) { - const selectedNodes = gridRef.current.api.getSelectedRows(); - setSelectedRows(selectedNodes); - } - }, []); + const handleBulkDelete = async () => { + if (selectedRows.length === 0) return; - const handleBulkDelete = async () => { - if (selectedRows.length === 0) return; + try { + // Delete each file individually since the API expects one filename at a time + const deletePromises = selectedRows.map((row) => + deleteDocumentMutation.mutateAsync({ filename: row.filename }) + ); - try { - // Delete each file individually since the API expects one filename at a time - const deletePromises = selectedRows.map((row) => - deleteDocumentMutation.mutateAsync({ filename: row.filename }), - ); + await Promise.all(deletePromises); - await Promise.all(deletePromises); + toast.success( + `Successfully deleted ${selectedRows.length} document${ + selectedRows.length > 1 ? "s" : "" + }` + ); + setSelectedRows([]); + setShowBulkDeleteDialog(false); - toast.success( - `Successfully deleted ${selectedRows.length} document${ - selectedRows.length > 1 ? "s" : "" - }`, - ); - setSelectedRows([]); - setShowBulkDeleteDialog(false); - - // Clear selection in the grid - if (gridRef.current) { - gridRef.current.api.deselectAll(); - } - } catch (error) { - toast.error( - error instanceof Error - ? error.message - : "Failed to delete some documents", - ); - } - }; + // Clear selection in the grid + if (gridRef.current) { + gridRef.current.api.deselectAll(); + } + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to delete some documents" + ); + } + }; return ( -
-
+ <> +

Project Knowledge

-
- {/* Search Input Area */} -
-
-
- {selectedFilter?.name && ( -
- {selectedFilter?.name} - setSelectedFilter(null)} - /> -
- )} - - -
- {/* */} - {/* //TODO: Implement sync button */} - {/* */} - {selectedRows.length > 0 && ( - - )} -
-
- []} - defaultColDef={defaultColDef} - loading={isFetching} - ref={gridRef} - rowData={fileResults} - rowSelection="multiple" - rowMultiSelectWithClick={false} - suppressRowClickSelection={true} - getRowId={(params: GetRowIdParams) => params.data?.filename} - domLayout="normal" - onSelectionChanged={onSelectionChanged} - noRowsOverlayComponent={() => ( -
-
- No knowledge -
-
- Add files from local or your preferred cloud. -
-
- )} - /> -
+ {selectedRows.length > 0 && ( + + )} +
+ +
+
+ []} + defaultColDef={defaultColDef} + loading={isFetching} + ref={gridRef} + rowData={fileResults} + rowSelection="multiple" + rowMultiSelectWithClick={false} + suppressRowClickSelection={true} + getRowId={(params: GetRowIdParams) => params.data?.filename} + domLayout="normal" + onSelectionChanged={onSelectionChanged} + noRowsOverlayComponent={() => ( +
+
+ No knowledge +
+
+ Add files from local or your preferred cloud. +
+
+ )} + /> +
- {/* Bulk Delete Confirmation Dialog */} - 1 ? "s" : "" - }? This will remove all chunks and data associated with these documents. This action cannot be undone. + {/* Bulk Delete Confirmation Dialog */} + 1 ? "s" : "" + }? This will remove all chunks and data associated with these documents. This action cannot be undone. Documents to be deleted: ${selectedRows.map((row) => `• ${row.filename}`).join("\n")}`} - confirmText="Delete All" - onConfirm={handleBulkDelete} - isLoading={deleteDocumentMutation.isPending} - /> -
- ); + confirmText="Delete All" + onConfirm={handleBulkDelete} + isLoading={deleteDocumentMutation.isPending} + /> + + ); } export default function ProtectedSearchPage() { - return ( - - - - ); + return ( + + + + ); } From 984b9c419529bf871a6553ad574eeb58c3b57252 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Mon, 6 Oct 2025 14:00:09 -0500 Subject: [PATCH 10/11] import --- frontend/src/app/knowledge/page.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 2f9f42b2..d4aebec8 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -1,16 +1,14 @@ "use client"; -import type { ColDef, GetRowIdParams } from "ag-grid-community"; +import { + themeQuartz, + type ColDef, + type GetRowIdParams, +} from "ag-grid-community"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; import { Cloud, FileIcon, Globe } from "lucide-react"; import { useRouter } from "next/navigation"; -import { - type ChangeEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { KnowledgeDropdown } from "@/components/knowledge-dropdown"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; @@ -314,6 +312,7 @@ function SearchPage() { defaultColDef={defaultColDef} loading={isFetching} ref={gridRef} + theme={themeQuartz.withParams({ browserColorScheme: "inherit" })} rowData={fileResults} rowSelection="multiple" rowMultiSelectWithClick={false} From 80eac4824cdc0fc0ad37af749c5cc08b4f46b562 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Mon, 6 Oct 2025 14:05:10 -0500 Subject: [PATCH 11/11] remove css --- frontend/src/app/globals.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5a2ff074..56dc8dc8 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -53,8 +53,6 @@ --flow-icon: #2f67d0; --radius: 0.5rem; - - --app-header-height: 40px; } .dark {