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