From 204f65ef35571a79aaef12327aabc5695f64dbbe Mon Sep 17 00:00:00 2001 From: Utkarsh-Patel-13 Date: Mon, 21 Jul 2025 22:43:28 -0700 Subject: [PATCH 01/19] Added basic llama index chat ui and corresponding pages --- .../[search_space_id]/v2/[chat_id]/page.tsx | 193 + .../dashboard/[search_space_id]/v2/page.tsx | 94 + surfsense_web/app/globals.css | 4 +- surfsense_web/components/chat_v2/ChatMain.tsx | 75 + surfsense_web/components/ui/textarea.tsx | 18 + surfsense_web/package.json | 1 + surfsense_web/pnpm-lock.yaml | 3265 +++++++++++++++++ 7 files changed, 3649 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/app/dashboard/[search_space_id]/v2/[chat_id]/page.tsx create mode 100644 surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx create mode 100644 surfsense_web/components/chat_v2/ChatMain.tsx create mode 100644 surfsense_web/components/ui/textarea.tsx 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 new file mode 100644 index 0000000..ecb9e4c --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/v2/[chat_id]/page.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { Message, useChat } from "@ai-sdk/react"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import ChatMain from "@/components/chat_v2/ChatMain"; +import { ResearchMode } from "@/components/chat"; + +export default function ResearcherChatPageV2() { + const { search_space_id, chat_id } = useParams(); + + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // const [initialMessages, setInitialMessages] = useState([]); + + const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">( + "DOCUMENTS" + ); + const [researchMode, setResearchMode] = useState("QNA"); + const [selectedConnectors, setSelectedConnectors] = useState([]); + + 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: selectedConnectors, + research_mode: researchMode, + search_mode: searchMode, + document_ids_to_add_in_context: [], + }, + }, + onError: (error) => { + console.error("Chat error:", error); + // You can add additional error handling here if needed + }, + }); + + useEffect(() => { + setIsLoading(true); + let token = localStorage.getItem("surfsense_bearer_token"); + if (token) { + setToken(token); + fetchChatDetails(token); + setIsLoading(false); + } + }, [chat_id]); + + const fetchChatDetails = async (token: string) => { + try { + if (!token) return; + + // console.log('Fetching chat details for chat ID:', chat_id); + + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chat_id)}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch chat details: ${response.statusText}` + ); + } + + const chatData = await response.json(); + // console.log('Chat details fetched:', chatData); + + // Set research mode from chat data + if (chatData.type) { + setResearchMode(chatData.type as ResearchMode); + } + + // Set connectors from chat data + if ( + chatData.initial_connectors && + Array.isArray(chatData.initial_connectors) + ) { + setSelectedConnectors(chatData.initial_connectors); + } + + if (chatData.messages && Array.isArray(chatData.messages)) { + console.log("chatData.messages", chatData.messages); + + if ( + chatData.messages.length === 1 && + chatData.messages[0].role === "user" + ) { + console.log("appending"); + handler.append({ + role: "user", + content: chatData.messages[0].content, + }); + } else { + console.log("setting"); + handler.setMessages(chatData.messages); + } + } + } catch (err) { + console.error("Error fetching chat details:", err); + } + }; + + const updateChat = async (messages: Message[]) => { + try { + const token = localStorage.getItem("surfsense_bearer_token"); + console.log("updating chat", messages, token); + if (!token) return; + + // Find the first user message to use as title + const userMessages = handler.messages.filter( + (msg: any) => msg.role === "user" + ); + + console.log("userMessages", userMessages); + console.log("handler.messages", handler.messages); + + if (userMessages.length === 0) return; + + // Use the first user message as the title + const title = userMessages[0].content; + + // console.log('Updating chat with title:', title); + + // Update the chat + console.log("messages", messages); + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chat_id)}`, + { + 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}` + ); + } + + // console.log('Chat updated successfully'); + } catch (err) { + console.error("Error updating chat:", err); + } + }; + + useEffect(() => { + console.log("handler.messages", handler.messages, handler.status); + if ( + handler.status === "ready" && + handler.messages.length > 0 && + handler.messages[handler.messages.length - 1]?.role === "assistant" + ) { + updateChat(handler.messages); + } + }, [handler.messages, handler.status]); + + const handleQuerySubmit = (input: string, handleSubmit: () => void) => { + handleSubmit(); + }; + + if (isLoading) { + return
Loading...
; + } + + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx new file mode 100644 index 0000000..5dd26e2 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; + +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import ChatMain from "@/components/chat_v2/ChatMain"; + +export default function ResearcherPageV2() { + const { search_space_id, chat_id } = useParams(); + const router = useRouter(); + + const [token, setToken] = useState(null); + + useEffect(() => { + setToken(localStorage.getItem("surfsense_bearer_token")); + }, []); + + const handleQuerySubmit = (input: string, handleSubmit: () => void) => { + const createChat = async () => { + try { + if (!token) { + console.error("Authentication token not found"); + return; + } + + // Create a new chat + 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: "QNA", + title: "Untitled Chat", // Empty title initially + initial_connectors: [], // No default connectors + messages: [ + { + role: "user", + content: input, + }, + ], + search_space_id: Number(search_space_id), + }), + } + ); + + if (!response.ok) { + throw new Error( + `Failed to create chat: ${response.statusText}` + ); + } + + const data = await response.json(); + + router.replace(`/dashboard/${search_space_id}/v2/${data.id}`); + } catch (err) { + console.error("Error creating chat:", err); + } + }; + + if (!chat_id) { + createChat(); + return; + } + }; + + const handler = useChat({ + api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`, + streamProtocol: "data", + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: { + data: { + search_space_id: search_space_id, + selected_connectors: [], + research_mode: "QNA", + search_mode: "DOCUMENTS", + document_ids_to_add_in_context: [], + }, + }, + onError: (error) => { + console.error("Chat error:", error); + // You can add additional error handling here if needed + }, + }); + + return ; +} diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css index a744656..88bd7ce 100644 --- a/surfsense_web/app/globals.css +++ b/surfsense_web/app/globals.css @@ -155,4 +155,6 @@ button { cursor: pointer; -} \ No newline at end of file +} + +@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}' \ No newline at end of file diff --git a/surfsense_web/components/chat_v2/ChatMain.tsx b/surfsense_web/components/chat_v2/ChatMain.tsx new file mode 100644 index 0000000..430c26f --- /dev/null +++ b/surfsense_web/components/chat_v2/ChatMain.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { + ChatCanvas, + ChatMessages, + ChatSection, + useChatUI, + ChatHandler, +} from "@llamaindex/chat-ui"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; +import { useEffect } from "react"; + +interface ChatMainProps { + handler: ChatHandler; + handleQuerySubmit: (input: string, handleSubmit: () => void) => void; +} + +const ChatInput = (props: { + handleQuerySubmit: (input: string, handleSubmit: () => void) => void; +}) => { + const { input, setInput, handleSubmit } = useChatUI(); + const { handleQuerySubmit } = props; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + + handleQuerySubmit(input, handleSubmit); + }; + + return ( +
+