diff --git a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx index 8e831adcd..dc0d9ff94 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx @@ -1,241 +1,241 @@ "use client"; -import { useChat, Message, CreateMessage } from "@ai-sdk/react"; +import { type CreateMessage, type Message, useChat } from "@ai-sdk/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo } from "react"; +import type { ResearchMode } from "@/components/chat"; import ChatInterface from "@/components/chat_v2/ChatInterface"; -import { ResearchMode } from "@/components/chat"; -import { useChatState, useChatAPI } from "@/hooks/useChat"; -import { Document } from "@/hooks/use-documents"; +import type { Document } from "@/hooks/use-documents"; +import { useChatAPI, useChatState } from "@/hooks/useChat"; export default function ResearchChatPageV2() { - const { search_space_id, chat_id } = useParams(); - const router = useRouter(); + const { search_space_id, chat_id } = useParams(); + const router = useRouter(); - const chatIdParam = Array.isArray(chat_id) ? chat_id[0] : chat_id; - const isNewChat = !chatIdParam; + const chatIdParam = Array.isArray(chat_id) ? chat_id[0] : chat_id; + const isNewChat = !chatIdParam; - const { - token, - isLoading, - setIsLoading, - searchMode, - setSearchMode, - researchMode, - setResearchMode, - selectedConnectors, - setSelectedConnectors, - selectedDocuments, - setSelectedDocuments, - } = useChatState({ - search_space_id: search_space_id as string, - chat_id: chatIdParam, - }); + const { + token, + isLoading, + setIsLoading, + searchMode, + setSearchMode, + researchMode, + setResearchMode, + selectedConnectors, + setSelectedConnectors, + selectedDocuments, + setSelectedDocuments, + } = useChatState({ + search_space_id: search_space_id as string, + chat_id: chatIdParam, + }); - const { fetchChatDetails, updateChat, createChat } = useChatAPI({ - token, - search_space_id: search_space_id as string, - }); + const { fetchChatDetails, updateChat, createChat } = useChatAPI({ + token, + search_space_id: search_space_id as string, + }); - // Memoize document IDs to prevent infinite re-renders - const documentIds = useMemo(() => { - return selectedDocuments.map((doc) => doc.id); - }, [selectedDocuments]); + // Memoize document IDs to prevent infinite re-renders + const documentIds = useMemo(() => { + return selectedDocuments.map((doc) => doc.id); + }, [selectedDocuments]); - // Memoize connector types to prevent infinite re-renders - const connectorTypes = useMemo(() => { - return selectedConnectors; - }, [selectedConnectors]); + // Memoize connector types to prevent infinite re-renders + const connectorTypes = useMemo(() => { + return selectedConnectors; + }, [selectedConnectors]); - // Unified localStorage management for chat state - interface ChatState { - selectedDocuments: Document[]; - selectedConnectors: string[]; - searchMode: "DOCUMENTS" | "CHUNKS"; - researchMode: ResearchMode; - } + // Unified localStorage management for chat state + interface ChatState { + selectedDocuments: Document[]; + selectedConnectors: string[]; + searchMode: "DOCUMENTS" | "CHUNKS"; + researchMode: ResearchMode; + } - const getChatStateStorageKey = (searchSpaceId: string, chatId: string) => - `surfsense_chat_state_${searchSpaceId}_${chatId}`; + const getChatStateStorageKey = (searchSpaceId: string, chatId: string) => + `surfsense_chat_state_${searchSpaceId}_${chatId}`; - const storeChatState = ( - searchSpaceId: string, - chatId: string, - state: ChatState - ) => { - const key = getChatStateStorageKey(searchSpaceId, chatId); - localStorage.setItem(key, JSON.stringify(state)); - }; + const storeChatState = ( + searchSpaceId: string, + chatId: string, + state: ChatState, + ) => { + const key = getChatStateStorageKey(searchSpaceId, chatId); + localStorage.setItem(key, JSON.stringify(state)); + }; - const restoreChatState = ( - searchSpaceId: string, - chatId: string - ): ChatState | null => { - const key = getChatStateStorageKey(searchSpaceId, chatId); - const stored = localStorage.getItem(key); - if (stored) { - localStorage.removeItem(key); // Clean up after restoration - try { - return JSON.parse(stored); - } catch (error) { - console.error("Error parsing stored chat state:", error); - return null; - } - } - return null; - }; + const restoreChatState = ( + searchSpaceId: string, + chatId: string, + ): ChatState | null => { + const key = getChatStateStorageKey(searchSpaceId, chatId); + const stored = localStorage.getItem(key); + if (stored) { + localStorage.removeItem(key); // Clean up after restoration + try { + return JSON.parse(stored); + } catch (error) { + console.error("Error parsing stored chat state:", error); + return null; + } + } + return null; + }; - const handler = useChat({ - api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`, - streamProtocol: "data", - initialMessages: [], - headers: { - ...(token && { Authorization: `Bearer ${token}` }), - }, - body: { - data: { - search_space_id: search_space_id, - selected_connectors: connectorTypes, - research_mode: researchMode, - search_mode: searchMode, - document_ids_to_add_in_context: documentIds, - }, - }, - onError: (error) => { - console.error("Chat error:", error); - }, - }); + const handler = useChat({ + api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`, + streamProtocol: "data", + initialMessages: [], + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: { + data: { + search_space_id: search_space_id, + selected_connectors: connectorTypes, + research_mode: researchMode, + search_mode: searchMode, + document_ids_to_add_in_context: documentIds, + }, + }, + onError: (error) => { + console.error("Chat error:", error); + }, + }); - const customHandlerAppend = async ( - message: Message | CreateMessage, - chatRequestOptions?: { data?: any } - ) => { - const newChatId = await createChat( - message.content, - researchMode, - selectedConnectors - ); - if (newChatId) { - // Store chat state before navigation - storeChatState(search_space_id as string, newChatId, { - selectedDocuments, - selectedConnectors, - searchMode, - researchMode, - }); - router.replace(`/dashboard/${search_space_id}/v2/${newChatId}`); - } - return newChatId; - }; + const customHandlerAppend = async ( + message: Message | CreateMessage, + chatRequestOptions?: { data?: any }, + ) => { + const newChatId = await createChat( + message.content, + researchMode, + selectedConnectors, + ); + if (newChatId) { + // Store chat state before navigation + storeChatState(search_space_id as string, newChatId, { + selectedDocuments, + selectedConnectors, + searchMode, + researchMode, + }); + router.replace(`/dashboard/${search_space_id}/v2/${newChatId}`); + } + return newChatId; + }; - useEffect(() => { - if (token && !isNewChat && chatIdParam) { - setIsLoading(true); - loadChatData(chatIdParam); - } - }, [token, isNewChat, chatIdParam]); + useEffect(() => { + if (token && !isNewChat && chatIdParam) { + setIsLoading(true); + loadChatData(chatIdParam); + } + }, [token, isNewChat, chatIdParam]); - // Restore chat state from localStorage on page load - useEffect(() => { - if (chatIdParam && search_space_id) { - const restoredState = restoreChatState( - search_space_id as string, - chatIdParam - ); - if (restoredState) { - setSelectedDocuments(restoredState.selectedDocuments); - setSelectedConnectors(restoredState.selectedConnectors); - setSearchMode(restoredState.searchMode); - setResearchMode(restoredState.researchMode); - } - } - }, [ - chatIdParam, - search_space_id, - setSelectedDocuments, - setSelectedConnectors, - setSearchMode, - setResearchMode, - ]); + // Restore chat state from localStorage on page load + useEffect(() => { + if (chatIdParam && search_space_id) { + const restoredState = restoreChatState( + search_space_id as string, + chatIdParam, + ); + if (restoredState) { + setSelectedDocuments(restoredState.selectedDocuments); + setSelectedConnectors(restoredState.selectedConnectors); + setSearchMode(restoredState.searchMode); + setResearchMode(restoredState.researchMode); + } + } + }, [ + chatIdParam, + search_space_id, + setSelectedDocuments, + setSelectedConnectors, + setSearchMode, + setResearchMode, + ]); - const loadChatData = async (chatId: string) => { - try { - const chatData = await fetchChatDetails(chatId); - if (!chatData) return; + const loadChatData = async (chatId: string) => { + try { + const chatData = await fetchChatDetails(chatId); + if (!chatData) return; - // Update configuration from chat data - if (chatData.type) { - setResearchMode(chatData.type as ResearchMode); - } + // Update configuration from chat data + if (chatData.type) { + setResearchMode(chatData.type as ResearchMode); + } - if ( - chatData.initial_connectors && - Array.isArray(chatData.initial_connectors) - ) { - setSelectedConnectors(chatData.initial_connectors); - } + if ( + chatData.initial_connectors && + Array.isArray(chatData.initial_connectors) + ) { + setSelectedConnectors(chatData.initial_connectors); + } - // Load existing messages - if (chatData.messages && Array.isArray(chatData.messages)) { - if ( - chatData.messages.length === 1 && - chatData.messages[0].role === "user" - ) { - // Single user message - append to trigger LLM response - handler.append({ - role: "user", - content: chatData.messages[0].content, - }); - } else if (chatData.messages.length > 1) { - // Multiple messages - set them all - handler.setMessages(chatData.messages); - } - } - } finally { - setIsLoading(false); - } - }; + // Load existing messages + if (chatData.messages && Array.isArray(chatData.messages)) { + if ( + chatData.messages.length === 1 && + chatData.messages[0].role === "user" + ) { + // Single user message - append to trigger LLM response + handler.append({ + role: "user", + content: chatData.messages[0].content, + }); + } else if (chatData.messages.length > 1) { + // Multiple messages - set them all + handler.setMessages(chatData.messages); + } + } + } finally { + setIsLoading(false); + } + }; - // Auto-update chat when messages change (only for existing chats) - useEffect(() => { - if ( - !isNewChat && - chatIdParam && - handler.status === "ready" && - handler.messages.length > 0 && - handler.messages[handler.messages.length - 1]?.role === "assistant" - ) { - updateChat( - chatIdParam, - handler.messages, - researchMode, - selectedConnectors - ); - } - }, [handler.messages, handler.status, chatIdParam, isNewChat]); + // Auto-update chat when messages change (only for existing chats) + useEffect(() => { + if ( + !isNewChat && + chatIdParam && + handler.status === "ready" && + handler.messages.length > 0 && + handler.messages[handler.messages.length - 1]?.role === "assistant" + ) { + updateChat( + chatIdParam, + handler.messages, + researchMode, + selectedConnectors, + ); + } + }, [handler.messages, handler.status, chatIdParam, isNewChat]); - if (isLoading) { - return ( -
-
Loading...
-
- ); - } + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } - return ( - - ); + return ( + + ); } diff --git a/surfsense_web/components/chat_v2/ChatCitation.tsx b/surfsense_web/components/chat_v2/ChatCitation.tsx index c5570b171..14b61c675 100644 --- a/surfsense_web/components/chat_v2/ChatCitation.tsx +++ b/surfsense_web/components/chat_v2/ChatCitation.tsx @@ -1,56 +1,62 @@ -"use client" +"use client"; import React from "react"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ExternalLink } from "lucide-react"; -export const CitationDisplay: React.FC<{index: number, node: any}> = ({index, node}) => { +export const CitationDisplay: React.FC<{ index: number; node: any }> = ({ + index, + node, +}) => { + const truncateText = (text: string, maxLength: number = 200) => { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; + }; - const truncateText = (text: string, maxLength: number = 200) => { - if (text.length <= maxLength) return text; - return text.substring(0, maxLength) + '...'; - }; + const handleUrlClick = (e: React.MouseEvent, url: string) => { + e.preventDefault(); + e.stopPropagation(); + window.open(url, "_blank", "noopener,noreferrer"); + }; - const handleUrlClick = (e: React.MouseEvent, url: string) => { - e.preventDefault(); - e.stopPropagation(); - window.open(url, '_blank', 'noopener,noreferrer'); - }; + return ( + + + + {index + 1} + + + + {/* External Link Button - Top Right */} + {node?.url && ( + + )} - return ( - - - - {index + 1} - - - - {/* External Link Button - Top Right */} - {node?.url && ( - - )} - - {/* Heading */} -
- {node?.metadata?.group_name || 'Source'} -
- - {/* Source */} -
- {node?.metadata?.title || 'Untitled'} -
- - {/* Body */} -
- {truncateText(node?.text || 'No content available')} -
-
-
- ); -} \ No newline at end of file + {/* Heading */} +
+ {node?.metadata?.group_name || "Source"} +
+ + {/* Source */} +
+ {node?.metadata?.title || "Untitled"} +
+ + {/* Body */} +
+ {truncateText(node?.text || "No content available")} +
+
+
+ ); +}; diff --git a/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx b/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx index a3b1ea97e..580052fc6 100644 --- a/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx +++ b/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx @@ -3,34 +3,43 @@ import { SuggestedQuestions } from "@llamaindex/chat-ui/widgets"; import { getAnnotationData, Message, useChatUI } from "@llamaindex/chat-ui"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; -import { Card } from "../ui/card"; -export const ChatFurtherQuestions: React.FC<{message: Message}> = ({message}) => { - const annotations: string[][] = getAnnotationData(message, "FURTHER_QUESTIONS"); - const { append, requestData } = useChatUI(); +export const ChatFurtherQuestions: React.FC<{ message: Message }> = ({ + message, +}) => { + const annotations: string[][] = getAnnotationData( + message, + "FURTHER_QUESTIONS", + ); + const { append, requestData } = useChatUI(); - console.log('🔥 annotations', annotations); - + if (annotations.length !== 1 || annotations[0].length === 0) { + return <>; + } - if (annotations.length !== 1 || annotations[0].length === 0) { - return <>; - } - - return ( - - - - Suggested Questions - - - - - - - ); -}; \ No newline at end of file + return ( + + + + Suggested Questions + + + + + + + ); +}; diff --git a/surfsense_web/components/chat_v2/ChatInputGroup.tsx b/surfsense_web/components/chat_v2/ChatInputGroup.tsx index 4aea225e4..d4070a2b6 100644 --- a/surfsense_web/components/chat_v2/ChatInputGroup.tsx +++ b/surfsense_web/components/chat_v2/ChatInputGroup.tsx @@ -4,19 +4,19 @@ import { ChatInput } from "@llamaindex/chat-ui"; import { FolderOpen, Check, Zap, Brain } from "lucide-react"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, - DialogTrigger, - DialogFooter, + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, + DialogFooter, } from "@/components/ui/dialog"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Suspense, useState, useCallback } from "react"; @@ -25,621 +25,593 @@ import { useDocuments, Document } from "@/hooks/use-documents"; import { DocumentsDataTable } from "@/components/chat_v2/DocumentsDataTable"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - getConnectorIcon, - ConnectorButton as ConnectorButtonComponent, + getConnectorIcon, + ConnectorButton as ConnectorButtonComponent, } from "@/components/chat/ConnectorComponents"; import { ResearchMode } from "@/components/chat"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; import React from "react"; const DocumentSelector = React.memo( - ({ - onSelectionChange, - selectedDocuments = [], - }: { - onSelectionChange?: (documents: Document[]) => void; - selectedDocuments?: Document[]; - }) => { - const { search_space_id } = useParams(); - const [isOpen, setIsOpen] = useState(false); + ({ + onSelectionChange, + selectedDocuments = [], + }: { + onSelectionChange?: (documents: Document[]) => void; + selectedDocuments?: Document[]; + }) => { + const { search_space_id } = useParams(); + const [isOpen, setIsOpen] = useState(false); - const { documents, loading, isLoaded, fetchDocuments } = useDocuments( - Number(search_space_id), - true - ); + const { documents, loading, isLoaded, fetchDocuments } = useDocuments( + Number(search_space_id), + true, + ); - const handleOpenChange = useCallback( - (open: boolean) => { - setIsOpen(open); - if (open && !isLoaded) { - fetchDocuments(); - } - }, - [fetchDocuments, isLoaded] - ); + const handleOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open); + if (open && !isLoaded) { + fetchDocuments(); + } + }, + [fetchDocuments, isLoaded], + ); - const handleSelectionChange = useCallback( - (documents: Document[]) => { - onSelectionChange?.(documents); - }, - [onSelectionChange] - ); + const handleSelectionChange = useCallback( + (documents: Document[]) => { + onSelectionChange?.(documents); + }, + [onSelectionChange], + ); - const handleDone = useCallback(() => { - setIsOpen(false); - }, []); + const handleDone = useCallback(() => { + setIsOpen(false); + }, []); - const selectedCount = React.useMemo( - () => selectedDocuments.length, - [selectedDocuments.length] - ); + const selectedCount = React.useMemo( + () => selectedDocuments.length, + [selectedDocuments.length], + ); - return ( - - - - + return ( + + + + - -
-
- - Select Documents - - - Choose documents to include in your research - context - -
+ +
+
+ + Select Documents + + + Choose documents to include in your research context + +
-
- {loading ? ( -
-
-
-

- Loading documents... -

-
-
- ) : isLoaded ? ( - - ) : null} -
-
- -
- ); - } +
+ {loading ? ( +
+
+
+

+ Loading documents... +

+
+
+ ) : isLoaded ? ( + + ) : null} +
+
+ +
+ ); + }, ); DocumentSelector.displayName = "DocumentSelector"; const ConnectorSelector = React.memo( - ({ - onSelectionChange, - selectedConnectors = [], - }: { - onSelectionChange?: (connectorTypes: string[]) => void; - selectedConnectors?: string[]; - }) => { - const [isOpen, setIsOpen] = useState(false); + ({ + onSelectionChange, + selectedConnectors = [], + }: { + onSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + }) => { + const [isOpen, setIsOpen] = useState(false); - const { connectorSourceItems, isLoading, isLoaded, fetchConnectors } = - useSearchSourceConnectors(true); + const { connectorSourceItems, isLoading, isLoaded, fetchConnectors } = + useSearchSourceConnectors(true); - const handleOpenChange = useCallback( - (open: boolean) => { - setIsOpen(open); - if (open && !isLoaded) { - fetchConnectors(); - } - }, - [fetchConnectors, isLoaded] - ); + const handleOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open); + if (open && !isLoaded) { + fetchConnectors(); + } + }, + [fetchConnectors, isLoaded], + ); - const handleConnectorToggle = useCallback( - (connectorType: string) => { - const isSelected = selectedConnectors.includes(connectorType); - const newSelection = isSelected - ? selectedConnectors.filter( - (type) => type !== connectorType - ) - : [...selectedConnectors, connectorType]; - onSelectionChange?.(newSelection); - }, - [selectedConnectors, onSelectionChange] - ); + const handleConnectorToggle = useCallback( + (connectorType: string) => { + const isSelected = selectedConnectors.includes(connectorType); + const newSelection = isSelected + ? selectedConnectors.filter((type) => type !== connectorType) + : [...selectedConnectors, connectorType]; + onSelectionChange?.(newSelection); + }, + [selectedConnectors, onSelectionChange], + ); - const handleSelectAll = useCallback(() => { - onSelectionChange?.(connectorSourceItems.map((c) => c.type)); - }, [connectorSourceItems, onSelectionChange]); + const handleSelectAll = useCallback(() => { + onSelectionChange?.(connectorSourceItems.map((c) => c.type)); + }, [connectorSourceItems, onSelectionChange]); - const handleClearAll = useCallback(() => { - onSelectionChange?.([]); - }, [onSelectionChange]); + const handleClearAll = useCallback(() => { + onSelectionChange?.([]); + }, [onSelectionChange]); - return ( - - - setIsOpen(true)} - connectorSources={connectorSourceItems} - /> - + return ( + + + setIsOpen(true)} + connectorSources={connectorSourceItems} + /> + - - Select Connectors - - Choose which data sources to include in your research - + + Select Connectors + + Choose which data sources to include in your research + - {/* Connector selection grid */} -
- {isLoading ? ( -
-
-
- ) : ( - connectorSourceItems.map((connector) => { - const isSelected = selectedConnectors.includes( - connector.type - ); + {/* Connector selection grid */} +
+ {isLoading ? ( +
+
+
+ ) : ( + connectorSourceItems.map((connector) => { + const isSelected = selectedConnectors.includes(connector.type); - return ( -
- handleConnectorToggle( - connector.type - ) - } - role="checkbox" - aria-checked={isSelected} - tabIndex={0} - > -
- {getConnectorIcon(connector.type)} -
- - {connector.name} - - {isSelected && ( - - )} -
- ); - }) - )} -
+ return ( +
handleConnectorToggle(connector.type)} + role="checkbox" + aria-checked={isSelected} + tabIndex={0} + > +
+ {getConnectorIcon(connector.type)} +
+ + {connector.name} + + {isSelected && } +
+ ); + }) + )} +
- -
- - -
-
- -
- ); - } + +
+ + +
+
+ +
+ ); + }, ); ConnectorSelector.displayName = "ConnectorSelector"; const SearchModeSelector = React.memo( - ({ - searchMode, - onSearchModeChange, - }: { - searchMode?: "DOCUMENTS" | "CHUNKS"; - onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; - }) => { - const handleDocumentsClick = React.useCallback(() => { - onSearchModeChange?.("DOCUMENTS"); - }, [onSearchModeChange]); + ({ + searchMode, + onSearchModeChange, + }: { + searchMode?: "DOCUMENTS" | "CHUNKS"; + onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; + }) => { + const handleDocumentsClick = React.useCallback(() => { + onSearchModeChange?.("DOCUMENTS"); + }, [onSearchModeChange]); - const handleChunksClick = React.useCallback(() => { - onSearchModeChange?.("CHUNKS"); - }, [onSearchModeChange]); + const handleChunksClick = React.useCallback(() => { + onSearchModeChange?.("CHUNKS"); + }, [onSearchModeChange]); - return ( -
- - Scope: - -
- - -
-
- ); - } + return ( +
+ + Scope: + +
+ + +
+
+ ); + }, ); SearchModeSelector.displayName = "SearchModeSelector"; const ResearchModeSelector = React.memo( - ({ - researchMode, - onResearchModeChange, - }: { - researchMode?: ResearchMode; - onResearchModeChange?: (mode: ResearchMode) => void; - }) => { - const handleValueChange = React.useCallback( - (value: string) => { - onResearchModeChange?.(value as ResearchMode); - }, - [onResearchModeChange] - ); + ({ + researchMode, + onResearchModeChange, + }: { + researchMode?: ResearchMode; + onResearchModeChange?: (mode: ResearchMode) => void; + }) => { + const handleValueChange = React.useCallback( + (value: string) => { + onResearchModeChange?.(value as ResearchMode); + }, + [onResearchModeChange], + ); - // Memoize mode options to prevent recreation - const modeOptions = React.useMemo( - () => [ - { value: "QNA", label: "Q&A", shortLabel: "Q&A" }, - { - value: "REPORT_GENERAL", - label: "General Report", - shortLabel: "General", - }, - { - value: "REPORT_DEEP", - label: "Deep Report", - shortLabel: "Deep", - }, - { - value: "REPORT_DEEPER", - label: "Deeper Report", - shortLabel: "Deeper", - }, - ], - [] - ); + // Memoize mode options to prevent recreation + const modeOptions = React.useMemo( + () => [ + { value: "QNA", label: "Q&A", shortLabel: "Q&A" }, + { + value: "REPORT_GENERAL", + label: "General Report", + shortLabel: "General", + }, + { + value: "REPORT_DEEP", + label: "Deep Report", + shortLabel: "Deep", + }, + { + value: "REPORT_DEEPER", + label: "Deeper Report", + shortLabel: "Deeper", + }, + ], + [], + ); - return ( -
- - Mode: - - -
- ); - } + return ( +
+ + Mode: + + +
+ ); + }, ); ResearchModeSelector.displayName = "ResearchModeSelector"; const LLMSelector = React.memo(() => { - const { llmConfigs, loading: llmLoading, error } = useLLMConfigs(); - const { - preferences, - updatePreferences, - loading: preferencesLoading, - } = useLLMPreferences(); + const { llmConfigs, loading: llmLoading, error } = useLLMConfigs(); + const { + preferences, + updatePreferences, + loading: preferencesLoading, + } = useLLMPreferences(); - const isLoading = llmLoading || preferencesLoading; + const isLoading = llmLoading || preferencesLoading; - // Memoize the selected config to avoid repeated lookups - const selectedConfig = React.useMemo(() => { - if (!preferences.fast_llm_id || !llmConfigs.length) return null; - return ( - llmConfigs.find( - (config) => config.id === preferences.fast_llm_id - ) || null - ); - }, [preferences.fast_llm_id, llmConfigs]); + // Memoize the selected config to avoid repeated lookups + const selectedConfig = React.useMemo(() => { + if (!preferences.fast_llm_id || !llmConfigs.length) return null; + return ( + llmConfigs.find((config) => config.id === preferences.fast_llm_id) || null + ); + }, [preferences.fast_llm_id, llmConfigs]); - // Memoize the display value for the trigger - const displayValue = React.useMemo(() => { - if (!selectedConfig) return null; - return ( -
- - {selectedConfig.provider} - - • - - {selectedConfig.name} - -
- ); - }, [selectedConfig]); + // Memoize the display value for the trigger + const displayValue = React.useMemo(() => { + if (!selectedConfig) return null; + return ( +
+ {selectedConfig.provider} + • + + {selectedConfig.name} + +
+ ); + }, [selectedConfig]); - const handleValueChange = React.useCallback( - (value: string) => { - const llmId = value ? parseInt(value, 10) : undefined; - updatePreferences({ fast_llm_id: llmId }); - }, - [updatePreferences] - ); + const handleValueChange = React.useCallback( + (value: string) => { + const llmId = value ? parseInt(value, 10) : undefined; + updatePreferences({ fast_llm_id: llmId }); + }, + [updatePreferences], + ); - // Loading skeleton - if (isLoading) { - return ( -
-
-
-
-
-
- ); - } + // Loading skeleton + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } - // Error state - if (error) { - return ( -
- -
- ); - } + // Error state + if (error) { + return ( +
+ +
+ ); + } - return ( -
- + +
+ + + {displayValue || ( + Select LLM + )} + +
+
- -
-
- - Fast LLM Selection -
-
+ +
+
+ + Fast LLM Selection +
+
- {llmConfigs.length === 0 ? ( -
-
- -
-

- No LLM configurations -

-

- Configure AI models to get started -

- -
- ) : ( -
- {llmConfigs.map((config) => ( - -
-
-
- -
-
-
- - {config.name} - - - {config.provider} - -
-

- {config.model_name} -

-
-
- {preferences.fast_llm_id === - config.id && ( -
- -
- )} -
-
- ))} -
- )} -
- -
- ); + {llmConfigs.length === 0 ? ( +
+
+ +
+

+ No LLM configurations +

+

+ Configure AI models to get started +

+ +
+ ) : ( +
+ {llmConfigs.map((config) => ( + +
+
+
+ +
+
+
+ + {config.name} + + + {config.provider} + +
+

+ {config.model_name} +

+
+
+ {preferences.fast_llm_id === config.id && ( +
+ +
+ )} +
+
+ ))} +
+ )} + + +
+ ); }); LLMSelector.displayName = "LLMSelector"; const CustomChatInputOptions = React.memo( - ({ - onDocumentSelectionChange, - selectedDocuments, - onConnectorSelectionChange, - selectedConnectors, - searchMode, - onSearchModeChange, - researchMode, - onResearchModeChange, - }: { - onDocumentSelectionChange?: (documents: Document[]) => void; - selectedDocuments?: Document[]; - onConnectorSelectionChange?: (connectorTypes: string[]) => void; - selectedConnectors?: string[]; - searchMode?: "DOCUMENTS" | "CHUNKS"; - onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; - researchMode?: ResearchMode; - onResearchModeChange?: (mode: ResearchMode) => void; - }) => { - // Memoize the loading fallback to prevent recreation - const loadingFallback = React.useMemo( - () => ( -
- ), - [] - ); + ({ + onDocumentSelectionChange, + selectedDocuments, + onConnectorSelectionChange, + selectedConnectors, + searchMode, + onSearchModeChange, + researchMode, + onResearchModeChange, + }: { + onDocumentSelectionChange?: (documents: Document[]) => void; + selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + searchMode?: "DOCUMENTS" | "CHUNKS"; + onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; + researchMode?: ResearchMode; + onResearchModeChange?: (mode: ResearchMode) => void; + }) => { + // Memoize the loading fallback to prevent recreation + const loadingFallback = React.useMemo( + () => ( +
+ ), + [], + ); - return ( -
- - - - - - - - - -
- ); - } + return ( +
+ + + + + + + + + +
+ ); + }, ); CustomChatInputOptions.displayName = "CustomChatInputOptions"; export const ChatInputUI = React.memo( - ({ - onDocumentSelectionChange, - selectedDocuments, - onConnectorSelectionChange, - selectedConnectors, - searchMode, - onSearchModeChange, - researchMode, - onResearchModeChange, - }: { - onDocumentSelectionChange?: (documents: Document[]) => void; - selectedDocuments?: Document[]; - onConnectorSelectionChange?: (connectorTypes: string[]) => void; - selectedConnectors?: string[]; - searchMode?: "DOCUMENTS" | "CHUNKS"; - onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; - researchMode?: ResearchMode; - onResearchModeChange?: (mode: ResearchMode) => void; - }) => { - return ( - - - - - - - - ); - } + ({ + onDocumentSelectionChange, + selectedDocuments, + onConnectorSelectionChange, + selectedConnectors, + searchMode, + onSearchModeChange, + researchMode, + onResearchModeChange, + }: { + onDocumentSelectionChange?: (documents: Document[]) => void; + selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + searchMode?: "DOCUMENTS" | "CHUNKS"; + onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; + researchMode?: ResearchMode; + onResearchModeChange?: (mode: ResearchMode) => void; + }) => { + return ( + + + + + + + + ); + }, ); ChatInputUI.displayName = "ChatInputUI"; diff --git a/surfsense_web/components/chat_v2/ChatInterface.tsx b/surfsense_web/components/chat_v2/ChatInterface.tsx index 22c3c7bed..327c215e6 100644 --- a/surfsense_web/components/chat_v2/ChatInterface.tsx +++ b/surfsense_web/components/chat_v2/ChatInterface.tsx @@ -2,8 +2,8 @@ import React from "react"; import { - ChatSection as LlamaIndexChatSection, - ChatHandler, + ChatSection as LlamaIndexChatSection, + ChatHandler, } from "@llamaindex/chat-ui"; import { Document } from "@/hooks/use-documents"; import { ChatInputUI } from "@/components/chat_v2/ChatInputGroup"; @@ -11,46 +11,45 @@ import { ResearchMode } from "@/components/chat"; import { ChatMessagesUI } from "@/components/chat_v2/ChatMessages"; interface ChatInterfaceProps { - handler: ChatHandler; - onDocumentSelectionChange?: (documents: Document[]) => void; - selectedDocuments?: Document[]; - onConnectorSelectionChange?: (connectorTypes: string[]) => void; - selectedConnectors?: string[]; - searchMode?: "DOCUMENTS" | "CHUNKS"; - onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; - researchMode?: ResearchMode; - onResearchModeChange?: (mode: ResearchMode) => void; + handler: ChatHandler; + onDocumentSelectionChange?: (documents: Document[]) => void; + selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + searchMode?: "DOCUMENTS" | "CHUNKS"; + onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; + researchMode?: ResearchMode; + onResearchModeChange?: (mode: ResearchMode) => void; } - -export default function ChatInterface({ - handler, - onDocumentSelectionChange, - selectedDocuments = [], - onConnectorSelectionChange, - selectedConnectors = [], - searchMode, - onSearchModeChange, - researchMode, - onResearchModeChange, -}: ChatInterfaceProps) { - return ( - -
- -
- -
-
-
- ); +export default function ChatInterface({ + handler, + onDocumentSelectionChange, + selectedDocuments = [], + onConnectorSelectionChange, + selectedConnectors = [], + searchMode, + onSearchModeChange, + researchMode, + onResearchModeChange, +}: ChatInterfaceProps) { + return ( + +
+ +
+ +
+
+
+ ); } diff --git a/surfsense_web/components/chat_v2/ChatMessages.tsx b/surfsense_web/components/chat_v2/ChatMessages.tsx index 8434466ff..c5f2c9297 100644 --- a/surfsense_web/components/chat_v2/ChatMessages.tsx +++ b/surfsense_web/components/chat_v2/ChatMessages.tsx @@ -1,71 +1,77 @@ "use client"; import React from "react"; -import { ChatMessage as LlamaIndexChatMessage, ChatMessages as LlamaIndexChatMessages, Message, useChatUI } from "@llamaindex/chat-ui"; +import { + ChatMessage as LlamaIndexChatMessage, + ChatMessages as LlamaIndexChatMessages, + Message, + useChatUI, +} from "@llamaindex/chat-ui"; import TerminalDisplay from "@/components/chat_v2/ChatTerminal"; import ChatSourcesDisplay from "@/components/chat_v2/ChatSources"; import { CitationDisplay } from "@/components/chat_v2/ChatCitation"; import { ChatFurtherQuestions } from "@/components/chat_v2/ChatFurtherQuestions"; export function ChatMessagesUI() { - const { messages } = useChatUI(); + const { messages } = useChatUI(); - return ( - - - {messages.map((message, index) => ( - - ))} - - - - ); + return ( + + + {messages.map((message, index) => ( + + ))} + + + + ); } function ChatMessageUI({ - message, - isLast, + message, + isLast, }: { - message: Message; - isLast: boolean; + message: Message; + isLast: boolean; }) { + const bottomRef = React.useRef(null); - const bottomRef = React.useRef(null); + React.useEffect(() => { + if (isLast && bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [message]); - React.useEffect(() => { - if (isLast && bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [message]); - - return ( - - {message.role === "assistant" ? ( -
- - - - - -
-
- {isLast && } - -
-
- ) : ( - - - - )} - - ); + return ( + + {message.role === "assistant" ? ( +
+ + + + + +
+
+ {isLast && } + +
+
+ ) : ( + + + + )} + + ); } diff --git a/surfsense_web/components/chat_v2/ChatSources.tsx b/surfsense_web/components/chat_v2/ChatSources.tsx index 9706cdeeb..c334babdb 100644 --- a/surfsense_web/components/chat_v2/ChatSources.tsx +++ b/surfsense_web/components/chat_v2/ChatSources.tsx @@ -4,216 +4,221 @@ import { useState } from "react"; import { getAnnotationData, Message } from "@llamaindex/chat-ui"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ExternalLink, FileText, Globe } from "lucide-react"; import { IconBrandGithub } from "@tabler/icons-react"; interface Source { - id: string; - title: string; - description: string; - url: string; + id: string; + title: string; + description: string; + url: string; } interface SourceGroup { - id: number; - name: string; - type: string; - sources: Source[]; + id: number; + name: string; + type: string; + sources: Source[]; } // New interfaces for the updated data format interface NodeMetadata { - title: string; - source_type: string; - group_name: string; + title: string; + source_type: string; + group_name: string; } interface SourceNode { - id: string; - text: string; - url: string; - metadata: NodeMetadata; + id: string; + text: string; + url: string; + metadata: NodeMetadata; } interface NodesResponse { - nodes: SourceNode[]; + nodes: SourceNode[]; } function getSourceIcon(type: string) { - switch (type) { - case "USER_SELECTED_GITHUB_CONNECTOR": - case "GITHUB_CONNECTOR": - return ; - case "USER_SELECTED_NOTION_CONNECTOR": - case "NOTION_CONNECTOR": - return ; - case "USER_SELECTED_FILE": - case "FILE": - return ; - default: - return ; - } + switch (type) { + case "USER_SELECTED_GITHUB_CONNECTOR": + case "GITHUB_CONNECTOR": + return ; + case "USER_SELECTED_NOTION_CONNECTOR": + case "NOTION_CONNECTOR": + return ; + case "USER_SELECTED_FILE": + case "FILE": + return ; + default: + return ; + } } function SourceCard({ source }: { source: Source }) { - const hasUrl = source.url && source.url.trim() !== ""; + const hasUrl = source.url && source.url.trim() !== ""; - return ( - - -
- - {source.title} - - {hasUrl && ( - - )} -
-
- - - {source.description} - - -
- ); + return ( + + +
+ + {source.title} + + {hasUrl && ( + + )} +
+
+ + + {source.description} + + +
+ ); } export default function ChatSourcesDisplay({ message }: { message: Message }) { - const [open, setOpen] = useState(false); - const annotations = getAnnotationData(message, "sources"); + const [open, setOpen] = useState(false); + const annotations = getAnnotationData(message, "sources"); - // Transform the new data format to the expected SourceGroup format - const sourceGroups: SourceGroup[] = []; - - if (Array.isArray(annotations) && annotations.length > 0) { - // Extract all nodes from the response - const allNodes: SourceNode[] = []; - - annotations.forEach((item) => { - if (item && typeof item === "object" && "nodes" in item && Array.isArray(item.nodes)) { - allNodes.push(...item.nodes); - } - }); + // Transform the new data format to the expected SourceGroup format + const sourceGroups: SourceGroup[] = []; - // Group nodes by source_type - const groupedByType = allNodes.reduce((acc, node) => { - const sourceType = node.metadata.source_type; - if (!acc[sourceType]) { - acc[sourceType] = []; - } - acc[sourceType].push(node); - return acc; - }, {} as Record); + if (Array.isArray(annotations) && annotations.length > 0) { + // Extract all nodes from the response + const allNodes: SourceNode[] = []; - // Convert grouped nodes to SourceGroup format - Object.entries(groupedByType).forEach(([sourceType, nodes], index) => { - if (nodes.length > 0) { - const firstNode = nodes[0]; - sourceGroups.push({ - id: index + 100, // Generate unique ID - name: firstNode.metadata.group_name, - type: sourceType, - sources: nodes.map(node => ({ - id: node.id, - title: node.metadata.title, - description: node.text, - url: node.url || "" - })) - }); - } - }); - } + annotations.forEach((item) => { + if ( + item && + typeof item === "object" && + "nodes" in item && + Array.isArray(item.nodes) + ) { + allNodes.push(...item.nodes); + } + }); - if (sourceGroups.length === 0) { - return null; - } + // Group nodes by source_type + const groupedByType = allNodes.reduce( + (acc, node) => { + const sourceType = node.metadata.source_type; + if (!acc[sourceType]) { + acc[sourceType] = []; + } + acc[sourceType].push(node); + return acc; + }, + {} as Record, + ); - const totalSources = sourceGroups.reduce( - (acc, group) => acc + group.sources.length, - 0 - ); + // Convert grouped nodes to SourceGroup format + Object.entries(groupedByType).forEach(([sourceType, nodes], index) => { + if (nodes.length > 0) { + const firstNode = nodes[0]; + sourceGroups.push({ + id: index + 100, // Generate unique ID + name: firstNode.metadata.group_name, + type: sourceType, + sources: nodes.map((node) => ({ + id: node.id, + title: node.metadata.title, + description: node.text, + url: node.url || "", + })), + }); + } + }); + } - return ( - - - - - - - Sources - - -
- - {sourceGroups.map((group) => ( - - {getSourceIcon(group.type)} - - {group.name} - - - {group.sources.length} - - - ))} - -
- {sourceGroups.map((group) => ( - -
-
- {group.sources.map((source) => ( - - ))} -
-
-
- ))} -
-
-
- ); + if (sourceGroups.length === 0) { + return null; + } + + const totalSources = sourceGroups.reduce( + (acc, group) => acc + group.sources.length, + 0, + ); + + return ( + + + + + + + Sources + + +
+ + {sourceGroups.map((group) => ( + + {getSourceIcon(group.type)} + + {group.name} + + + {group.sources.length} + + + ))} + +
+ {sourceGroups.map((group) => ( + +
+
+ {group.sources.map((source) => ( + + ))} +
+
+
+ ))} +
+
+
+ ); } diff --git a/surfsense_web/components/chat_v2/ChatTerminal.tsx b/surfsense_web/components/chat_v2/ChatTerminal.tsx index 40890e42e..131d58a6d 100644 --- a/surfsense_web/components/chat_v2/ChatTerminal.tsx +++ b/surfsense_web/components/chat_v2/ChatTerminal.tsx @@ -3,109 +3,113 @@ import React from "react"; import { getAnnotationData, Message } from "@llamaindex/chat-ui"; -export default function TerminalDisplay({ message, open }: { message: Message, open: boolean }) { - const [isCollapsed, setIsCollapsed] = React.useState(!open); +export default function TerminalDisplay({ + message, + open, +}: { + message: Message; + open: boolean; +}) { + const [isCollapsed, setIsCollapsed] = React.useState(!open); - const bottomRef = React.useRef(null); + const bottomRef = React.useRef(null); - // Get the last assistant message that's not being typed - if (!message) { - return <>; - } + // Get the last assistant message that's not being typed + if (!message) { + return <>; + } - interface TerminalInfo { - id: number; - text: string; - type: string; - } + interface TerminalInfo { + id: number; + text: string; + type: string; + } - const events = getAnnotationData( - message, - "TERMINAL_INFO" - ) as TerminalInfo[]; + const events = getAnnotationData(message, "TERMINAL_INFO") as TerminalInfo[]; - if (events.length === 0) { - return <>; - } + if (events.length === 0) { + return <>; + } - React.useEffect(() => { - if (bottomRef.current) { - bottomRef.current.scrollTo({ top: bottomRef.current.scrollHeight, behavior: "smooth" }); - } - }, [events]); + React.useEffect(() => { + if (bottomRef.current) { + bottomRef.current.scrollTo({ + top: bottomRef.current.scrollHeight, + behavior: "smooth", + }); + } + }, [events]); - return ( -
- {/* Terminal Header */} -
setIsCollapsed(!isCollapsed)} - > -
-
-
-
-
-
- Agent Process Terminal ({events.length} events) -
-
- {isCollapsed ? ( - - - - ) : ( - - - - )} -
-
+ return ( +
+ {/* Terminal Header */} +
setIsCollapsed(!isCollapsed)} + > +
+
+
+
+
+
+ Agent Process Terminal ({events.length} events) +
+
+ {isCollapsed ? ( + + + + ) : ( + + + + )} +
+
- {/* Terminal Content */} - {!isCollapsed && ( -
- {events.map((event, index) => ( -
- $ - - [{event.type || ""}] - - - {event.text || ""}... - -
- ))} - {events.length === 0 && ( -
- No agent events to display... -
- )} -
- )} -
- ); + {/* Terminal Content */} + {!isCollapsed && ( +
+ {events.map((event, index) => ( +
+ $ + [{event.type || ""}] + + {event.text || ""}... + +
+ ))} + {events.length === 0 && ( +
+ No agent events to display... +
+ )} +
+ )} +
+ ); } diff --git a/surfsense_web/components/chat_v2/DocumentsDataTable.tsx b/surfsense_web/components/chat_v2/DocumentsDataTable.tsx index 8e6e16b1d..6041227a6 100644 --- a/surfsense_web/components/chat_v2/DocumentsDataTable.tsx +++ b/surfsense_web/components/chat_v2/DocumentsDataTable.tsx @@ -2,529 +2,501 @@ import * as React from "react"; import { - ColumnDef, - ColumnFiltersState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - SortingState, - useReactTable, - VisibilityState, + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, } from "@tanstack/react-table"; import { ArrowUpDown, Calendar, FileText, Search } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Document, DocumentType } from "@/hooks/use-documents"; interface DocumentsDataTableProps { - documents: Document[]; - onSelectionChange: (documents: Document[]) => void; - onDone: () => void; - initialSelectedDocuments?: Document[]; + documents: Document[]; + onSelectionChange: (documents: Document[]) => void; + onDone: () => void; + initialSelectedDocuments?: Document[]; } const DOCUMENT_TYPES: (DocumentType | "ALL")[] = [ - "ALL", - "FILE", - "EXTENSION", - "CRAWLED_URL", - "YOUTUBE_VIDEO", - "SLACK_CONNECTOR", - "NOTION_CONNECTOR", - "GITHUB_CONNECTOR", - "LINEAR_CONNECTOR", - "DISCORD_CONNECTOR", + "ALL", + "FILE", + "EXTENSION", + "CRAWLED_URL", + "YOUTUBE_VIDEO", + "SLACK_CONNECTOR", + "NOTION_CONNECTOR", + "GITHUB_CONNECTOR", + "LINEAR_CONNECTOR", + "DISCORD_CONNECTOR", ]; const getDocumentTypeColor = (type: DocumentType) => { - const colors = { - FILE: "bg-blue-50 text-blue-700 border-blue-200", - EXTENSION: "bg-green-50 text-green-700 border-green-200", - CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", - YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", - SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", - NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", - GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", - LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", - DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", - }; - return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; + const colors = { + FILE: "bg-blue-50 text-blue-700 border-blue-200", + EXTENSION: "bg-green-50 text-green-700 border-green-200", + CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", + YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", + SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", + NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", + GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", + LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", + DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", + }; + return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; }; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - - table.toggleAllPageRowsSelected(!!value) - } - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "title", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const title = row.getValue("title") as string; - return ( -
- {title} -
- ); - }, - }, - { - accessorKey: "document_type", - header: "Type", - cell: ({ row }) => { - const type = row.getValue("document_type") as DocumentType; - return ( - - - {type.replace(/_/g, " ")} - - {type.split("_")[0]} - - ); - }, - size: 80, - meta: { - className: "hidden sm:table-cell", - }, - }, - { - accessorKey: "content", - header: "Preview", - cell: ({ row }) => { - const content = row.getValue("content") as string; - return ( -
- - {content.substring(0, 30)}... - - - {content.substring(0, 100)}... - -
- ); - }, - enableSorting: false, - meta: { - className: "hidden md:table-cell", - }, - }, - { - accessorKey: "created_at", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return ( -
- - {date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} - - - {date.toLocaleDateString("en-US", { - month: "numeric", - day: "numeric", - })} - -
- ); - }, - size: 80, - }, + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const title = row.getValue("title") as string; + return ( +
+ {title} +
+ ); + }, + }, + { + accessorKey: "document_type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("document_type") as DocumentType; + return ( + + {type.replace(/_/g, " ")} + {type.split("_")[0]} + + ); + }, + size: 80, + meta: { + className: "hidden sm:table-cell", + }, + }, + { + accessorKey: "content", + header: "Preview", + cell: ({ row }) => { + const content = row.getValue("content") as string; + return ( +
+ {content.substring(0, 30)}... + + {content.substring(0, 100)}... + +
+ ); + }, + enableSorting: false, + meta: { + className: "hidden md:table-cell", + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
+ + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + + {date.toLocaleDateString("en-US", { + month: "numeric", + day: "numeric", + })} + +
+ ); + }, + size: 80, + }, ]; export function DocumentsDataTable({ - documents, - onSelectionChange, - onDone, - initialSelectedDocuments = [], + documents, + onSelectionChange, + onDone, + initialSelectedDocuments = [], }: DocumentsDataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = - React.useState([]); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [documentTypeFilter, setDocumentTypeFilter] = React.useState< - DocumentType | "ALL" - >("ALL"); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [documentTypeFilter, setDocumentTypeFilter] = React.useState< + DocumentType | "ALL" + >("ALL"); - // Memoize initial row selection to prevent infinite loops - const initialRowSelection = React.useMemo(() => { - if (!documents.length || !initialSelectedDocuments.length) return {}; + // Memoize initial row selection to prevent infinite loops + const initialRowSelection = React.useMemo(() => { + if (!documents.length || !initialSelectedDocuments.length) return {}; - const selection: Record = {}; - initialSelectedDocuments.forEach((selectedDoc) => { - const docIndex = documents.findIndex( - (doc) => doc.id === selectedDoc.id - ); - if (docIndex !== -1) { - selection[docIndex.toString()] = true; - } - }); - return selection; - }, [documents, initialSelectedDocuments]); + const selection: Record = {}; + initialSelectedDocuments.forEach((selectedDoc) => { + const docIndex = documents.findIndex((doc) => doc.id === selectedDoc.id); + if (docIndex !== -1) { + selection[docIndex.toString()] = true; + } + }); + return selection; + }, [documents, initialSelectedDocuments]); - const [rowSelection, setRowSelection] = React.useState< - Record - >({}); + const [rowSelection, setRowSelection] = React.useState< + Record + >({}); - // Only update row selection when initialRowSelection actually changes and is not empty - React.useEffect(() => { - const hasChanges = - JSON.stringify(rowSelection) !== - JSON.stringify(initialRowSelection); - if (hasChanges && Object.keys(initialRowSelection).length > 0) { - setRowSelection(initialRowSelection); - } - }, [initialRowSelection]); + // Only update row selection when initialRowSelection actually changes and is not empty + React.useEffect(() => { + const hasChanges = + JSON.stringify(rowSelection) !== JSON.stringify(initialRowSelection); + if (hasChanges && Object.keys(initialRowSelection).length > 0) { + setRowSelection(initialRowSelection); + } + }, [initialRowSelection]); - // Initialize row selection on mount - React.useEffect(() => { - if ( - Object.keys(rowSelection).length === 0 && - Object.keys(initialRowSelection).length > 0 - ) { - setRowSelection(initialRowSelection); - } - }, []); + // Initialize row selection on mount + React.useEffect(() => { + if ( + Object.keys(rowSelection).length === 0 && + Object.keys(initialRowSelection).length > 0 + ) { + setRowSelection(initialRowSelection); + } + }, []); - const filteredDocuments = React.useMemo(() => { - if (documentTypeFilter === "ALL") return documents; - return documents.filter( - (doc) => doc.document_type === documentTypeFilter - ); - }, [documents, documentTypeFilter]); + const filteredDocuments = React.useMemo(() => { + if (documentTypeFilter === "ALL") return documents; + return documents.filter((doc) => doc.document_type === documentTypeFilter); + }, [documents, documentTypeFilter]); - const table = useReactTable({ - data: filteredDocuments, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - initialState: { pagination: { pageSize: 10 } }, - state: { sorting, columnFilters, columnVisibility, rowSelection }, - }); + const table = useReactTable({ + data: filteredDocuments, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + initialState: { pagination: { pageSize: 10 } }, + state: { sorting, columnFilters, columnVisibility, rowSelection }, + }); - React.useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows; - const selectedDocuments = selectedRows.map((row) => row.original); - onSelectionChange(selectedDocuments); - }, [rowSelection, onSelectionChange, table]); + React.useEffect(() => { + const selectedRows = table.getFilteredSelectedRowModel().rows; + const selectedDocuments = selectedRows.map((row) => row.original); + onSelectionChange(selectedDocuments); + }, [rowSelection, onSelectionChange, table]); - const handleClearAll = () => setRowSelection({}); + const handleClearAll = () => setRowSelection({}); - const handleSelectPage = () => { - const currentPageRows = table.getRowModel().rows; - const newSelection = { ...rowSelection }; - currentPageRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectPage = () => { + const currentPageRows = table.getRowModel().rows; + const newSelection = { ...rowSelection }; + currentPageRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const handleSelectAllFiltered = () => { - const allFilteredRows = table.getFilteredRowModel().rows; - const newSelection: Record = {}; - allFilteredRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectAllFiltered = () => { + const allFilteredRows = table.getFilteredRowModel().rows; + const newSelection: Record = {}; + allFilteredRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const selectedCount = table.getFilteredSelectedRowModel().rows.length; - const totalFiltered = table.getFilteredRowModel().rows.length; + const selectedCount = table.getFilteredSelectedRowModel().rows.length; + const totalFiltered = table.getFilteredRowModel().rows.length; - return ( -
- {/* Header Controls */} -
- {/* Search and Filter Row */} -
-
- - - table - .getColumn("title") - ?.setFilterValue(event.target.value) - } - className="pl-10 text-sm" - /> -
- -
+ return ( +
+ {/* Header Controls */} +
+ {/* Search and Filter Row */} +
+
+ + + table.getColumn("title")?.setFilterValue(event.target.value) + } + className="pl-10 text-sm" + /> +
+ +
- {/* Action Controls Row */} -
-
- - {selectedCount} of {totalFiltered} selected - -
-
- - - - -
-
- -
-
+ {/* Action Controls Row */} +
+
+ + {selectedCount} of {totalFiltered} selected + +
+
+ + + + +
+
+ +
+
- {/* Table Container */} -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No documents found. - - - )} - -
-
-
+ {/* Table Container */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No documents found. + + + )} + +
+
+
- {/* Footer Pagination */} -
-
- Showing{" "} - {table.getState().pagination.pageIndex * - table.getState().pagination.pageSize + - 1}{" "} - to{" "} - {Math.min( - (table.getState().pagination.pageIndex + 1) * - table.getState().pagination.pageSize, - table.getFilteredRowModel().rows.length - )}{" "} - of {table.getFilteredRowModel().rows.length} documents -
-
- -
- Page - - {table.getState().pagination.pageIndex + 1} - - of - {table.getPageCount()} -
- -
-
-
- ); + {/* Footer Pagination */} +
+
+ Showing{" "} + {table.getState().pagination.pageIndex * + table.getState().pagination.pageSize + + 1}{" "} + to{" "} + {Math.min( + (table.getState().pagination.pageIndex + 1) * + table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length, + )}{" "} + of {table.getFilteredRowModel().rows.length} documents +
+
+ +
+ Page + {table.getState().pagination.pageIndex + 1} + of + {table.getPageCount()} +
+ +
+
+
+ ); }