mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-02 10:39:13 +00:00
Added basic llama index chat ui and corresponding pages
This commit is contained in:
parent
7d2c6e383f
commit
204f65ef35
7 changed files with 3649 additions and 1 deletions
|
@ -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<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// const [initialMessages, setInitialMessages] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">(
|
||||||
|
"DOCUMENTS"
|
||||||
|
);
|
||||||
|
const [researchMode, setResearchMode] = useState<ResearchMode>("QNA");
|
||||||
|
const [selectedConnectors, setSelectedConnectors] = useState<string[]>([]);
|
||||||
|
|
||||||
|
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 <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ChatMain handler={handler} handleQuerySubmit={handleQuerySubmit} />;
|
||||||
|
}
|
94
surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx
Normal file
94
surfsense_web/app/dashboard/[search_space_id]/v2/page.tsx
Normal file
|
@ -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<string | null>(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 <ChatMain handler={handler} handleQuerySubmit={handleQuerySubmit} />;
|
||||||
|
}
|
|
@ -156,3 +156,5 @@
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'
|
75
surfsense_web/components/chat_v2/ChatMain.tsx
Normal file
75
surfsense_web/components/chat_v2/ChatMain.tsx
Normal file
|
@ -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<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!input.trim()) return;
|
||||||
|
|
||||||
|
handleQuerySubmit(input, handleSubmit);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="flex flex-row items-center justify-between gap-2"
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Type your message here..."
|
||||||
|
rows={4}
|
||||||
|
className="max-h-[150px] overflow-y-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">Send</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChatMain({
|
||||||
|
handler,
|
||||||
|
handleQuerySubmit,
|
||||||
|
}: ChatMainProps) {
|
||||||
|
return (
|
||||||
|
<ChatSection handler={handler} className="flex h-full">
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<ChatMessages className="flex-1">
|
||||||
|
<ChatMessages.List className="p-4">
|
||||||
|
{/* Custom message rendering */}
|
||||||
|
</ChatMessages.List>
|
||||||
|
<ChatMessages.Loading>
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
</ChatMessages.Loading>
|
||||||
|
</ChatMessages>
|
||||||
|
|
||||||
|
<div className="border-t p-4">
|
||||||
|
<ChatInput handleQuerySubmit={handleQuerySubmit} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChatCanvas className="w-1/2 border-l" />
|
||||||
|
</ChatSection>
|
||||||
|
);
|
||||||
|
}
|
18
surfsense_web/components/ui/textarea.tsx
Normal file
18
surfsense_web/components/ui/textarea.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
|
@ -17,6 +17,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/react": "^1.1.21",
|
"@ai-sdk/react": "^1.1.21",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
|
"@llamaindex/chat-ui": "^0.5.17",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
|
|
3265
surfsense_web/pnpm-lock.yaml
generated
3265
surfsense_web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue