diff --git a/surfsense_web/components/chat/AnimatedEmptyState.tsx b/surfsense_web/components/chat/AnimatedEmptyState.tsx new file mode 100644 index 000000000..2e1d7a884 --- /dev/null +++ b/surfsense_web/components/chat/AnimatedEmptyState.tsx @@ -0,0 +1,84 @@ +"use client"; +import { cn } from "@/lib/utils"; +import { Manrope } from "next/font/google"; +import React, { useRef, useEffect, useState } from "react"; +import { RoughNotation, RoughNotationGroup } from "react-rough-notation"; +import { useInView } from "framer-motion"; +import { useSidebar } from "@/components/ui/sidebar"; + +const manrope = Manrope({ subsets: ["latin"], weight: ["400", "700"] }); + +export function AnimatedEmptyState() { + const ref = useRef(null); + const isInView = useInView(ref); + const { state } = useSidebar(); + const [shouldShowHighlight, setShouldShowHighlight] = useState(false); + const [layoutStable, setLayoutStable] = useState(true); + + // Track sidebar state changes and manage highlight visibility + useEffect(() => { + // Set layout as unstable when sidebar state changes + setLayoutStable(false); + setShouldShowHighlight(false); + + // Wait for layout to stabilize after sidebar transition + const stabilizeTimer = setTimeout(() => { + setLayoutStable(true); + }, 300); // Wait for sidebar transition (200ms) + buffer + + return () => clearTimeout(stabilizeTimer); + }, [state]); + + // Re-enable highlights after layout stabilizes and component is in view + useEffect(() => { + if (layoutStable && isInView) { + const showTimer = setTimeout(() => { + setShouldShowHighlight(true); + }, 100); // Small delay to ensure layout is fully settled + + return () => clearTimeout(showTimer); + } else { + setShouldShowHighlight(false); + } + }, [layoutStable, isInView]); + + return ( +
+
+ +

+ + SurfSense + +

+ +

+ + Let's Start Surfing + {" "} + through your knowledge base. +

+
+
+
+ ); +} diff --git a/surfsense_web/components/chat/ChatFurtherQuestions.tsx b/surfsense_web/components/chat/ChatFurtherQuestions.tsx index 580052fc6..51285eda2 100644 --- a/surfsense_web/components/chat/ChatFurtherQuestions.tsx +++ b/surfsense_web/components/chat/ChatFurtherQuestions.tsx @@ -3,43 +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"; export const ChatFurtherQuestions: React.FC<{ message: Message }> = ({ - message, + message, }) => { - const annotations: string[][] = getAnnotationData( - message, - "FURTHER_QUESTIONS", - ); - const { append, requestData } = useChatUI(); + const annotations: string[][] = getAnnotationData( + message, + "FURTHER_QUESTIONS", + ); + const { append, requestData } = useChatUI(); - if (annotations.length !== 1 || annotations[0].length === 0) { - return <>; - } + if (annotations.length !== 1 || annotations[0].length === 0) { + return <>; + } - return ( - - - - Suggested Questions - - - - - - - ); + return ( + + + + Further Suggested Questions + + + + + + + ); }; diff --git a/surfsense_web/components/chat/ChatMessages.tsx b/surfsense_web/components/chat/ChatMessages.tsx index 5720cf48d..473564ddf 100644 --- a/surfsense_web/components/chat/ChatMessages.tsx +++ b/surfsense_web/components/chat/ChatMessages.tsx @@ -2,77 +2,80 @@ import React from "react"; import { - ChatMessage as LlamaIndexChatMessage, - ChatMessages as LlamaIndexChatMessages, - Message, - useChatUI, + ChatMessage as LlamaIndexChatMessage, + ChatMessages as LlamaIndexChatMessages, + Message, + useChatUI, } from "@llamaindex/chat-ui"; import TerminalDisplay from "@/components/chat/ChatTerminal"; import ChatSourcesDisplay from "@/components/chat/ChatSources"; import { CitationDisplay } from "@/components/chat/ChatCitation"; import { ChatFurtherQuestions } from "@/components/chat/ChatFurtherQuestions"; +import { AnimatedEmptyState } from "@/components/chat/AnimatedEmptyState"; 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/hooks/useChat.ts b/surfsense_web/hooks/useChat.ts index 545deeb53..fb2c5a007 100644 --- a/surfsense_web/hooks/useChat.ts +++ b/surfsense_web/hooks/useChat.ts @@ -4,190 +4,184 @@ import { ResearchMode } from "@/components/chat"; import { Document } from "@/hooks/use-documents"; interface UseChatStateProps { - search_space_id: string; - chat_id?: string; + search_space_id: string; + chat_id?: string; } export function useChatState({ search_space_id, chat_id }: UseChatStateProps) { - const [token, setToken] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [currentChatId, setCurrentChatId] = useState( - chat_id || null - ); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [currentChatId, setCurrentChatId] = useState( + chat_id || null, + ); - // Chat configuration state - const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">( - "DOCUMENTS" - ); - const [researchMode, setResearchMode] = useState("QNA"); - const [selectedConnectors, setSelectedConnectors] = useState([]); - const [selectedDocuments, setSelectedDocuments] = useState([]); + // Chat configuration state + const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">( + "CHUNKS", + ); + const [researchMode, setResearchMode] = useState("QNA"); + const [selectedConnectors, setSelectedConnectors] = useState([]); + const [selectedDocuments, setSelectedDocuments] = useState([]); - useEffect(() => { - const bearerToken = localStorage.getItem("surfsense_bearer_token"); - setToken(bearerToken); - }, []); + useEffect(() => { + const bearerToken = localStorage.getItem("surfsense_bearer_token"); + setToken(bearerToken); + }, []); - return { - token, - setToken, - isLoading, - setIsLoading, - currentChatId, - setCurrentChatId, - searchMode, - setSearchMode, - researchMode, - setResearchMode, - selectedConnectors, - setSelectedConnectors, - selectedDocuments, - setSelectedDocuments, - }; + return { + token, + setToken, + isLoading, + setIsLoading, + currentChatId, + setCurrentChatId, + searchMode, + setSearchMode, + researchMode, + setResearchMode, + selectedConnectors, + setSelectedConnectors, + selectedDocuments, + setSelectedDocuments, + }; } interface UseChatAPIProps { - token: string | null; - search_space_id: string; + token: string | null; + search_space_id: string; } export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { - const fetchChatDetails = useCallback( - async (chatId: string) => { - if (!token) return null; + const fetchChatDetails = useCallback( + async (chatId: string) => { + if (!token) return null; - try { - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/chats/${Number(chatId)}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - } - ); + try { + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chatId)}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }, + ); - if (!response.ok) { - throw new Error( - `Failed to fetch chat details: ${response.statusText}` - ); - } + if (!response.ok) { + throw new Error( + `Failed to fetch chat details: ${response.statusText}`, + ); + } - return await response.json(); - } catch (err) { - console.error("Error fetching chat details:", err); - return null; - } - }, - [token] - ); + return await response.json(); + } catch (err) { + console.error("Error fetching chat details:", err); + return null; + } + }, + [token], + ); - const createChat = useCallback( - async ( - initialMessage: string, - researchMode: ResearchMode, - selectedConnectors: string[] - ): Promise => { - if (!token) { - console.error("Authentication token not found"); - return null; - } + const createChat = useCallback( + async ( + initialMessage: string, + researchMode: ResearchMode, + selectedConnectors: string[], + ): Promise => { + if (!token) { + console.error("Authentication token not found"); + return null; + } - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - type: researchMode, - title: "Untitled Chat", - initial_connectors: selectedConnectors, - messages: [ - { - role: "user", - content: initialMessage, - }, - ], - search_space_id: Number(search_space_id), - }), - } - ); + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + type: researchMode, + title: "Untitled Chat", + initial_connectors: selectedConnectors, + messages: [ + { + role: "user", + content: initialMessage, + }, + ], + search_space_id: Number(search_space_id), + }), + }, + ); - if (!response.ok) { - throw new Error( - `Failed to create chat: ${response.statusText}` - ); - } + if (!response.ok) { + throw new Error(`Failed to create chat: ${response.statusText}`); + } - const data = await response.json(); - return data.id; - } catch (err) { - console.error("Error creating chat:", err); - return null; - } - }, - [token, search_space_id] - ); + const data = await response.json(); + return data.id; + } catch (err) { + console.error("Error creating chat:", err); + return null; + } + }, + [token, search_space_id], + ); - const updateChat = useCallback( - async ( - chatId: string, - messages: Message[], - researchMode: ResearchMode, - selectedConnectors: string[] - ) => { - if (!token) return; + const updateChat = useCallback( + async ( + chatId: string, + messages: Message[], + researchMode: ResearchMode, + selectedConnectors: string[], + ) => { + if (!token) return; - try { - const userMessages = messages.filter( - (msg) => msg.role === "user" - ); - if (userMessages.length === 0) return; + try { + const userMessages = messages.filter((msg) => msg.role === "user"); + if (userMessages.length === 0) return; - const title = userMessages[0].content; + const title = userMessages[0].content; - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/chats/${Number(chatId)}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - type: researchMode, - title: title, - initial_connectors: selectedConnectors, - messages: messages, - search_space_id: Number(search_space_id), - }), - } - ); + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chatId)}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + type: researchMode, + title: title, + initial_connectors: selectedConnectors, + messages: messages, + search_space_id: Number(search_space_id), + }), + }, + ); - if (!response.ok) { - throw new Error( - `Failed to update chat: ${response.statusText}` - ); - } - } catch (err) { - console.error("Error updating chat:", err); - } - }, - [token, search_space_id] - ); + if (!response.ok) { + throw new Error(`Failed to update chat: ${response.statusText}`); + } + } catch (err) { + console.error("Error updating chat:", err); + } + }, + [token, search_space_id], + ); - return { - fetchChatDetails, - createChat, - updateChat, - }; + return { + fetchChatDetails, + createChat, + updateChat, + }; } diff --git a/surfsense_web/package.json b/surfsense_web/package.json index a9a647969..12314768f 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -61,6 +61,7 @@ "react-json-view": "^1.21.3", "react-json-view-lite": "^2.4.0", "react-markdown": "^10.0.1", + "react-rough-notation": "^1.0.5", "react-syntax-highlighter": "^15.6.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index 6d218a871..7569cb106 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: react-markdown: specifier: ^10.0.1 version: 10.0.1(@types/react@19.0.10)(react@19.0.0) + react-rough-notation: + specifier: ^1.0.5 + version: 1.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-syntax-highlighter: specifier: ^15.6.1 version: 15.6.1(react@19.0.0) @@ -8099,6 +8102,15 @@ packages: "@types/react": optional: true + react-rough-notation@1.0.5: + resolution: + { + integrity: sha512-TTDnw1Qn96PwitGsBsjNh4911+o26Vivj/dhHbsHHhN6lDgkQsFhv1X/eOnCjHA2EOyDE5JBQ1HEZ1yW8sCmKA==, + } + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + react-style-singleton@2.2.3: resolution: { @@ -8382,6 +8394,12 @@ packages: engines: { node: ">=14.18.0", npm: ">=8.0.0" } hasBin: true + rough-notation@0.5.1: + resolution: + { + integrity: sha512-ITHofTzm13cWFVfoGsh/4c/k2Mg8geKgBCwex71UZLnNuw403tCRjYPQ68jSAd37DMbZIePXPjDgY0XdZi9HPw==, + } + run-parallel@1.2.0: resolution: { @@ -15622,6 +15640,12 @@ snapshots: optionalDependencies: "@types/react": 19.0.10 + react-rough-notation@1.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + rough-notation: 0.5.1 + react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0): dependencies: get-nonce: 1.0.1 @@ -15887,6 +15911,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rough-notation@0.5.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3