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/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index 6bf9285b..d0dd8196 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, @@ -231,8 +232,8 @@ export function KnowledgeFilterPanel() { }; return ( -
- +
+
@@ -320,6 +321,7 @@ export function KnowledgeFilterPanel() { className="font-mono placeholder:font-mono" onChange={(e) => setQuery(e.target.value)} rows={2} + disabled={!!queryOverride && !createMode} />
diff --git a/frontend/components/knowledge-search-input.tsx b/frontend/components/knowledge-search-input.tsx new file mode 100644 index 00000000..fd840628 --- /dev/null +++ b/frontend/components/knowledge-search-input.tsx @@ -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) => { + if (e) e.preventDefault(); + setQueryOverride(searchQueryInput.trim()); + }, + [searchQueryInput, setQueryOverride] + ); + + // Reset the query text when the selected filter changes + useEffect(() => { + setSearchQueryInput(queryOverride); + }, [queryOverride]); + + return ( +
+
+ {selectedFilter?.name && ( +
+ {selectedFilter?.name} + setSelectedFilter(null)} + /> +
+ )} + + ) => + setSearchQueryInput(e.target.value) + } + /> + {queryOverride && ( + + )} + +
+
+ ); +}; diff --git a/frontend/src/app/api/queries/useGetSearchQuery.ts b/frontend/src/app/api/queries/useGetSearchQuery.ts index d0c1a3a9..50905fcc 100644 --- a/frontend/src/app/api/queries/useGetSearchQuery.ts +++ b/frontend/src/app/api/queries/useGetSearchQuery.ts @@ -180,7 +180,7 @@ export const useGetSearchQuery = ( const queryResult = useQuery( { - queryKey: ["search", queryData], + queryKey: ["search", queryData, query], placeholderData: (prev) => prev, queryFn: getFiles, ...options, diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 2c3bf278..c543fc35 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -1,7 +1,6 @@ "use client"; import { - AtSign, Bot, Check, ChevronDown, @@ -11,7 +10,6 @@ import { Loader2, Plus, Settings, - Upload, User, X, Zap, @@ -31,7 +29,6 @@ import { import { useAuth } from "@/contexts/auth-context"; import { type EndpointType, useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; -import { useLayout } from "@/contexts/layout-context"; import { useTask } from "@/contexts/task-context"; import { useLoadingStore } from "@/stores/loadingStore"; import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery"; @@ -151,9 +148,8 @@ function ChatPage() { const streamAbortRef = useRef(null); const streamIdRef = useRef(0); const lastLoadedConversationRef = useRef(null); - const { addTask, isMenuOpen } = useTask(); - const { totalTopOffset } = useLayout(); - const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } = + const { addTask } = useTask(); + const { selectedFilter, parsedFilterData, setSelectedFilter } = useKnowledgeFilter(); const scrollToBottom = () => { @@ -260,7 +256,7 @@ function ChatPage() { "Upload failed with status:", response.status, "Response:", - errorText, + errorText ); throw new Error("Failed to process document"); } @@ -470,7 +466,7 @@ function ChatPage() { console.log( "Loading conversation with", conversationData.messages.length, - "messages", + "messages" ); // Convert backend message format to frontend Message interface const convertedMessages: Message[] = conversationData.messages.map( @@ -598,7 +594,7 @@ function ChatPage() { ) === "string" ? toolCall.function?.arguments || toolCall.arguments : JSON.stringify( - toolCall.function?.arguments || toolCall.arguments, + toolCall.function?.arguments || toolCall.arguments ), result: toolCall.result, status: "completed", @@ -617,7 +613,7 @@ function ChatPage() { } return message; - }, + } ); setMessages(convertedMessages); @@ -706,7 +702,7 @@ function ChatPage() { console.log( "Chat page received file upload error event:", filename, - error, + error ); // Replace the last message with error message @@ -720,43 +716,43 @@ function ChatPage() { window.addEventListener( "fileUploadStart", - handleFileUploadStart as EventListener, + handleFileUploadStart as EventListener ); window.addEventListener( "fileUploaded", - handleFileUploaded as EventListener, + handleFileUploaded as EventListener ); window.addEventListener( "fileUploadComplete", - handleFileUploadComplete as EventListener, + handleFileUploadComplete as EventListener ); window.addEventListener( "fileUploadError", - handleFileUploadError as EventListener, + handleFileUploadError as EventListener ); return () => { window.removeEventListener( "fileUploadStart", - handleFileUploadStart as EventListener, + handleFileUploadStart as EventListener ); window.removeEventListener( "fileUploaded", - handleFileUploaded as EventListener, + handleFileUploaded as EventListener ); window.removeEventListener( "fileUploadComplete", - handleFileUploadComplete as EventListener, + handleFileUploadComplete as EventListener ); window.removeEventListener( "fileUploadError", - handleFileUploadError as EventListener, + handleFileUploadError as EventListener ); }; }, [endpoint, setPreviousResponseIds]); const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery( - previousResponseIds[endpoint], + previousResponseIds[endpoint] ); const handleSSEStream = async (userMessage: Message) => { @@ -861,7 +857,7 @@ function ChatPage() { console.log( "Received chunk:", chunk.type || chunk.object, - chunk, + chunk ); // Extract response ID if present @@ -877,14 +873,14 @@ function ChatPage() { if (chunk.delta.function_call) { console.log( "Function call in delta:", - chunk.delta.function_call, + chunk.delta.function_call ); // Check if this is a new function call if (chunk.delta.function_call.name) { console.log( "New function call:", - chunk.delta.function_call.name, + chunk.delta.function_call.name ); const functionCall: FunctionCall = { name: chunk.delta.function_call.name, @@ -900,7 +896,7 @@ function ChatPage() { else if (chunk.delta.function_call.arguments) { console.log( "Function call arguments delta:", - chunk.delta.function_call.arguments, + chunk.delta.function_call.arguments ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; @@ -912,14 +908,14 @@ function ChatPage() { chunk.delta.function_call.arguments; console.log( "Accumulated arguments:", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); // Try to parse arguments if they look complete if (lastFunctionCall.argumentsString.includes("}")) { try { const parsed = JSON.parse( - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); lastFunctionCall.arguments = parsed; lastFunctionCall.status = "completed"; @@ -927,7 +923,7 @@ function ChatPage() { } catch (e) { console.log( "Arguments not yet complete or invalid JSON:", - e, + e ); } } @@ -960,7 +956,7 @@ function ChatPage() { else if (toolCall.function.arguments) { console.log( "Tool call arguments delta:", - toolCall.function.arguments, + toolCall.function.arguments ); const lastFunctionCall = currentFunctionCalls[ @@ -974,7 +970,7 @@ function ChatPage() { toolCall.function.arguments; console.log( "Accumulated tool arguments:", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); // Try to parse arguments if they look complete @@ -983,7 +979,7 @@ function ChatPage() { ) { try { const parsed = JSON.parse( - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); lastFunctionCall.arguments = parsed; lastFunctionCall.status = "completed"; @@ -991,7 +987,7 @@ function ChatPage() { } catch (e) { console.log( "Tool arguments not yet complete or invalid JSON:", - e, + e ); } } @@ -1023,7 +1019,7 @@ function ChatPage() { console.log( "Error parsing function call on finish:", fc, - e, + e ); } } @@ -1039,12 +1035,12 @@ function ChatPage() { console.log( "🟢 CREATING function call (added):", chunk.item.id, - chunk.item.tool_name || chunk.item.name, + chunk.item.tool_name || chunk.item.name ); // Try to find an existing pending call to update (created by earlier deltas) let existing = currentFunctionCalls.find( - (fc) => fc.id === chunk.item.id, + (fc) => fc.id === chunk.item.id ); if (!existing) { existing = [...currentFunctionCalls] @@ -1053,7 +1049,7 @@ function ChatPage() { (fc) => fc.status === "pending" && !fc.id && - fc.name === (chunk.item.tool_name || chunk.item.name), + fc.name === (chunk.item.tool_name || chunk.item.name) ); } @@ -1066,7 +1062,7 @@ function ChatPage() { chunk.item.inputs || existing.arguments; console.log( "🟢 UPDATED existing pending function call with id:", - existing.id, + existing.id ); } else { const functionCall: FunctionCall = { @@ -1084,7 +1080,7 @@ function ChatPage() { currentFunctionCalls.map((fc) => ({ id: fc.id, name: fc.name, - })), + })) ); } } @@ -1095,7 +1091,7 @@ function ChatPage() { ) { console.log( "Function args delta (Realtime API):", - chunk.delta, + chunk.delta ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; @@ -1106,7 +1102,7 @@ function ChatPage() { lastFunctionCall.argumentsString += chunk.delta || ""; console.log( "Accumulated arguments (Realtime API):", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); } } @@ -1117,26 +1113,26 @@ function ChatPage() { ) { console.log( "Function args done (Realtime API):", - chunk.arguments, + chunk.arguments ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; if (lastFunctionCall) { try { lastFunctionCall.arguments = JSON.parse( - chunk.arguments || "{}", + chunk.arguments || "{}" ); lastFunctionCall.status = "completed"; console.log( "Parsed function arguments (Realtime API):", - lastFunctionCall.arguments, + lastFunctionCall.arguments ); } catch (e) { lastFunctionCall.arguments = { raw: chunk.arguments }; lastFunctionCall.status = "error"; console.log( "Error parsing function arguments (Realtime API):", - e, + e ); } } @@ -1150,14 +1146,14 @@ function ChatPage() { console.log( "🔵 UPDATING function call (done):", chunk.item.id, - chunk.item.tool_name || chunk.item.name, + chunk.item.tool_name || chunk.item.name ); console.log( "🔵 Looking for existing function calls:", currentFunctionCalls.map((fc) => ({ id: fc.id, name: fc.name, - })), + })) ); // Find existing function call by ID or name @@ -1165,14 +1161,14 @@ function ChatPage() { (fc) => fc.id === chunk.item.id || fc.name === chunk.item.tool_name || - fc.name === chunk.item.name, + fc.name === chunk.item.name ); if (functionCall) { console.log( "🔵 FOUND existing function call, updating:", functionCall.id, - functionCall.name, + functionCall.name ); // Update existing function call with completion data functionCall.status = @@ -1195,7 +1191,7 @@ function ChatPage() { "🔴 WARNING: Could not find existing function call to update:", chunk.item.id, chunk.item.tool_name, - chunk.item.name, + chunk.item.name ); } } @@ -1216,7 +1212,7 @@ function ChatPage() { fc.name === chunk.item.name || fc.name === chunk.item.type || fc.name.includes(chunk.item.type.replace("_call", "")) || - chunk.item.type.includes(fc.name), + chunk.item.type.includes(fc.name) ); if (functionCall) { @@ -1260,12 +1256,12 @@ function ChatPage() { "🟡 CREATING tool call (added):", chunk.item.id, chunk.item.tool_name || chunk.item.name, - chunk.item.type, + chunk.item.type ); // Dedupe by id or pending with same name let existing = currentFunctionCalls.find( - (fc) => fc.id === chunk.item.id, + (fc) => fc.id === chunk.item.id ); if (!existing) { existing = [...currentFunctionCalls] @@ -1277,7 +1273,7 @@ function ChatPage() { fc.name === (chunk.item.tool_name || chunk.item.name || - chunk.item.type), + chunk.item.type) ); } @@ -1293,7 +1289,7 @@ function ChatPage() { chunk.item.inputs || existing.arguments; console.log( "🟡 UPDATED existing pending tool call with id:", - existing.id, + existing.id ); } else { const functionCall = { @@ -1314,7 +1310,7 @@ function ChatPage() { id: fc.id, name: fc.name, type: fc.type, - })), + })) ); } } @@ -1592,7 +1588,7 @@ function ChatPage() { const handleForkConversation = ( messageIndex: number, - event?: React.MouseEvent, + event?: React.MouseEvent ) => { // Prevent any default behavior and stop event propagation if (event) { @@ -1657,7 +1653,7 @@ function ChatPage() { const renderFunctionCalls = ( functionCalls: FunctionCall[], - messageIndex?: number, + messageIndex?: number ) => { if (!functionCalls || functionCalls.length === 0) return null; @@ -1908,7 +1904,7 @@ function ChatPage() { if (isFilterDropdownOpen) { const filteredFilters = availableFilters.filter((filter) => - filter.name.toLowerCase().includes(filterSearchTerm.toLowerCase()), + filter.name.toLowerCase().includes(filterSearchTerm.toLowerCase()) ); if (e.key === "Escape") { @@ -1926,7 +1922,7 @@ function ChatPage() { if (e.key === "ArrowDown") { e.preventDefault(); setSelectedFilterIndex((prev) => - prev < filteredFilters.length - 1 ? prev + 1 : 0, + prev < filteredFilters.length - 1 ? prev + 1 : 0 ); return; } @@ -1934,7 +1930,7 @@ function ChatPage() { if (e.key === "ArrowUp") { e.preventDefault(); setSelectedFilterIndex((prev) => - prev > 0 ? prev - 1 : filteredFilters.length - 1, + prev > 0 ? prev - 1 : filteredFilters.length - 1 ); return; } @@ -2031,7 +2027,7 @@ function ChatPage() { // Get button position for popover anchoring const button = document.querySelector( - "[data-filter-button]", + "[data-filter-button]" ) as HTMLElement; if (button) { const rect = button.getBoundingClientRect(); @@ -2047,21 +2043,10 @@ function ChatPage() { }; return ( -
+
{/* Debug header - only show in debug mode */} {isDebugMode && ( -
+
{/* Async Mode Toggle */} @@ -2167,7 +2152,7 @@ function ChatPage() {
{renderFunctionCalls( message.functionCalls || [], - index, + index )}
@@ -2196,7 +2181,7 @@ function ChatPage() {
{renderFunctionCalls( streamingMessage.functionCalls, - messages.length, + messages.length )}
)} -
- setTextareaHeight(height)} - maxRows={7} - minRows={2} - placeholder="Type to ask a question..." - disabled={loading} - className={`w-full bg-transparent px-4 ${ - selectedFilter ? "pt-2" : "pt-4" - } focus-visible:outline-none resize-none`} - rows={2} - /> +
+ setTextareaHeight(height)} + maxRows={7} + minRows={2} + placeholder="Type to ask a question..." + disabled={loading} + className={`w-full bg-transparent px-4 ${ + selectedFilter ? "pt-2" : "pt-4" + } focus-visible:outline-none resize-none`} + rows={2} + /> {/* Safe area at bottom for buttons */} -
-
filter.name .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), + .includes(filterSearchTerm.toLowerCase()) ) .map((filter, index) => ( -

- {/* Removes file extension from filename */} - {filename.replace(/\.[^/.]+$/, "")} -

-
-
-
-
- {selectedFilter?.name && ( -
- {selectedFilter?.name} - setSelectedFilter(null)} - /> -
- )} - - setQueryInputText(e.target.value)} - value={queryInputText} - /> -
-
- {/*
+ return ( +
+
+ {/* Header */} +
+
+ +

+ {/* Removes file extension from filename */} + {filename.replace(/\.[^/.]+$/, "")} +

+
+
+ + {/*
- {/* Content Area - matches knowledge page structure */} -
- {isFetching ? ( -
-
- -

- Loading chunks... -

-
-
- ) : chunks.length === 0 ? ( -
-
-

No knowledge

-

- Clear the knowledge filter or return to the knowledge page -

-
-
- ) : ( -
- {chunksFilteredByQuery.map((chunk, index) => ( -
-
-
- {/*
+ {/* Content Area - matches knowledge page structure */} +
+ {isFetching ? ( +
+
+ +

+ Loading chunks... +

+
+
+ ) : chunks.length === 0 ? ( +
+
+

No knowledge

+

+ Clear the knowledge filter or return to the knowledge page +

+
+
+ ) : ( +
+ {chunksFilteredByQuery.map((chunk, index) => ( +
+
+
+ {/*
diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index 2cd6f382..d4aebec8 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -1,72 +1,60 @@ "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 { - Building2, - Cloud, - Globe, - HardDrive, - Search, - Trash2, - X, -} from "lucide-react"; +import { Cloud, FileIcon, Globe } from "lucide-react"; import { useRouter } from "next/navigation"; -import { - type ChangeEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import { SiGoogledrive } from "react-icons/si"; -import { TbBrandOnedrive } from "react-icons/tb"; +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"; 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"; import "@/components/AgGrid/agGridStyles.css"; import { toast } from "sonner"; import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown"; -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 "url": - return ; case "google_drive": return ( - + ); case "onedrive": - return ( - - ); + return ; case "sharepoint": - return ; + return ( + + ); + case "url": + 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 { files: taskFiles, refreshTasks } = useTask(); + const { parsedFilterData, queryOverride } = useKnowledgeFilter(); const [selectedRows, setSelectedRows] = useState([]); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); @@ -77,8 +65,8 @@ function SearchPage() { }, [refreshTasks]); const { data: searchData = [], isFetching } = useGetSearchQuery( - parsedFilterData?.query || "*", - parsedFilterData, + queryOverride, + parsedFilterData ); // Convert TaskFiles to File format and merge with backend results const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { @@ -94,7 +82,7 @@ function SearchPage() { // Create a map of task files by filename for quick lookup const taskFileMap = new Map( - taskFilesAsFiles.map((file) => [file.filename, file]), + taskFilesAsFiles.map((file) => [file.filename, file]) ); // Override backend files with task file status if they exist @@ -117,7 +105,7 @@ function SearchPage() { return ( taskFile.status !== "active" && !backendFiles.some( - (backendFile) => backendFile.filename === taskFile.filename, + (backendFile) => backendFile.filename === taskFile.filename ) ); }); @@ -125,10 +113,6 @@ function SearchPage() { // 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 columnDefs = [ @@ -161,8 +145,8 @@ function SearchPage() { } router.push( `/knowledge/chunks?filename=${encodeURIComponent( - data?.filename ?? "", - )}`, + data?.filename ?? "" + )}` ); }} > @@ -263,7 +247,7 @@ function SearchPage() { try { // Delete each file individually since the API expects one filename at a time const deletePromises = selectedRows.map((row) => - deleteDocumentMutation.mutateAsync({ filename: row.filename }), + deleteDocumentMutation.mutateAsync({ filename: row.filename }) ); await Promise.all(deletePromises); @@ -271,7 +255,7 @@ function SearchPage() { toast.success( `Successfully deleted ${selectedRows.length} document${ selectedRows.length > 1 ? "s" : "" - }`, + }` ); setSelectedRows([]); setShowBulkDeleteDialog(false); @@ -284,74 +268,23 @@ function SearchPage() { toast.error( error instanceof Error ? error.message - : "Failed to delete some documents", + : "Failed to delete some documents" ); } }; return ( -
-
+ <> +

Project Knowledge

-
{/* Search Input Area */} -
-
-
- {selectedFilter?.name && ( -
- {selectedFilter?.name} - setSelectedFilter(null)} - /> -
- )} - - -
- {/* */} - {/* //TODO: Implement sync button */} - {/* */} - {selectedRows.length > 0 && ( - - )} -
+ {selectedRows.length > 0 && ( + + )} +
+ +
`• ${row.filename}`).join("\n")}`} onConfirm={handleBulkDelete} isLoading={deleteDocumentMutation.isPending} /> -
+ ); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index baf9f2d5..10ed826b 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -38,7 +38,7 @@ export default function RootLayout({ return ( ( +const GoogleDriveIcon = ({ className }: { className?: string }) => ( ( +const OneDriveIcon = ({ className }: { className?: string }) => ( ( +const SharePointIcon = ({ className }: { className?: string }) => ( +
{/* Connectors Section */}
diff --git a/frontend/src/components/AgGrid/agGridStyles.css b/frontend/src/components/AgGrid/agGridStyles.css index 590046c2..e307f617 100644 --- a/frontend/src/components/AgGrid/agGridStyles.css +++ b/frontend/src/components/AgGrid/agGridStyles.css @@ -10,6 +10,9 @@ body { --ag-row-hover-color: hsl(var(--muted)); --ag-wrapper-border: none; --ag-font-family: var(--font-sans); + --ag-selected-row-background-color: hsl(var(--accent)); + --ag-focus-shadow: none; + --ag-range-selection-border-color: hsl(var(--primary)); /* Checkbox styling */ --ag-checkbox-background-color: hsl(var(--background)); diff --git a/frontend/src/components/layout-wrapper.tsx b/frontend/src/components/layout-wrapper.tsx index 79417654..dbd04fea 100644 --- a/frontend/src/components/layout-wrapper.tsx +++ b/frontend/src/components/layout-wrapper.tsx @@ -12,12 +12,10 @@ import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"; import Logo from "@/components/logo/logo"; import { Navigation } from "@/components/navigation"; import { TaskNotificationMenu } from "@/components/task-notification-menu"; -import { Button } from "@/components/ui/button"; 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"; @@ -35,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 { @@ -59,6 +57,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { // List of paths that should not show navigation const authPaths = ["/login", "/auth/callback", "/onboarding"]; const isAuthPage = authPaths.includes(pathname); + const isOnKnowledgePage = pathname.startsWith("/knowledge"); // List of paths with smaller max-width const smallWidthPaths = ["/settings", "/settings/connector/new"]; @@ -66,7 +65,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { // Calculate active tasks for the bell icon const activeTasks = tasks.filter( - task => + (task) => task.status === "pending" || task.status === "running" || task.status === "processing" @@ -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,47 +144,37 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
-
+ + {/* Sidebar Navigation */} +
-
- + + {/* Main Content */} +
+
-
- {children} -
- + {children} +
- - + + {/* Task Notifications Panel */} + + + {/* Knowledge Filter Panel */} +
); } diff --git a/frontend/src/components/task-notification-menu.tsx b/frontend/src/components/task-notification-menu.tsx index fed7e6f1..6cb968e8 100644 --- a/frontend/src/components/task-notification-menu.tsx +++ b/frontend/src/components/task-notification-menu.tsx @@ -143,10 +143,10 @@ export function TaskNotificationMenu() { } return ( -
+
{/* Header */} -
+
diff --git a/frontend/src/components/ui/animated-processing-icon.tsx b/frontend/src/components/ui/animated-processing-icon.tsx index 51815414..431202c1 100644 --- a/frontend/src/components/ui/animated-processing-icon.tsx +++ b/frontend/src/components/ui/animated-processing-icon.tsx @@ -1,37 +1,70 @@ -import type { SVGProps } from "react"; +import { cn } from "@/lib/utils"; +import { motion, easeInOut } from "framer-motion"; -export const AnimatedProcessingIcon = (props: SVGProps) => { - return ( - - Processing - - - - - - - - ); +export const AnimatedProcessingIcon = ({ + className, +}: { + className?: string; +}) => { + const createAnimationFrames = (delay: number) => ({ + opacity: [1, 1, 0.5, 0], // Opacity Steps + transition: { + delay, + duration: 1, + ease: easeInOut, + repeat: Infinity, + times: [0, 0.33, 0.66, 1], // Duration Percentages that Correspond to opacity Array + }, + }); + + return ( + + + + + + + + + ); }; diff --git a/frontend/src/components/ui/status-badge.tsx b/frontend/src/components/ui/status-badge.tsx index e57ad3b5..19270284 100644 --- a/frontend/src/components/ui/status-badge.tsx +++ b/frontend/src/components/ui/status-badge.tsx @@ -50,7 +50,7 @@ export const StatusBadge = ({ status, className }: StatusBadgeProps) => { }`} > {status === "processing" && ( - + )} {config.label}
diff --git a/frontend/src/contexts/knowledge-filter-context.tsx b/frontend/src/contexts/knowledge-filter-context.tsx index 043f6fae..eebf355a 100644 --- a/frontend/src/contexts/knowledge-filter-context.tsx +++ b/frontend/src/contexts/knowledge-filter-context.tsx @@ -5,6 +5,7 @@ import React, { createContext, type ReactNode, useContext, + useEffect, useState, } from "react"; @@ -44,6 +45,8 @@ interface KnowledgeFilterContextType { createMode: boolean; startCreateMode: () => void; endCreateMode: () => void; + queryOverride: string; + setQueryOverride: (query: string) => void; } const KnowledgeFilterContext = createContext< @@ -73,6 +76,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); @@ -136,6 +140,11 @@ export function KnowledgeFilterProvider({ setCreateMode(false); }; + // Clear the search override when we change filters + useEffect(() => { + setQueryOverride(""); + }, [selectedFilter]); + const value: KnowledgeFilterContextType = { selectedFilter, parsedFilterData, @@ -148,6 +157,8 @@ export function KnowledgeFilterProvider({ createMode, startCreateMode, endCreateMode, + queryOverride, + setQueryOverride, }; return ( 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