diff --git a/ui/app/c/[chatId]/page.tsx b/ui/app/c/[chatId]/page.tsx new file mode 100644 index 0000000..dc3c92a --- /dev/null +++ b/ui/app/c/[chatId]/page.tsx @@ -0,0 +1,7 @@ +import ChatWindow from '@/components/ChatWindow'; + +const Page = ({ params }: { params: { chatId: string } }) => { + return ; +}; + +export default Page; diff --git a/ui/app/library/layout.tsx b/ui/app/library/layout.tsx new file mode 100644 index 0000000..00d4a3b --- /dev/null +++ b/ui/app/library/layout.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import React from 'react'; + +export const metadata: Metadata = { + title: 'Library - Perplexica', +}; + +const Layout = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +export default Layout; diff --git a/ui/app/library/page.tsx b/ui/app/library/page.tsx new file mode 100644 index 0000000..6ba2fe4 --- /dev/null +++ b/ui/app/library/page.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { formatTimeDifference } from '@/lib/utils'; +import { BookOpenText, ClockIcon, ScanEye } from 'lucide-react'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; + +interface Chat { + id: string; + title: string; + createdAt: string; + focusMode: string; +} + +const Page = () => { + const [chats, setChats] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchChats = async () => { + setLoading(true); + + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await res.json(); + + setChats(data.chats); + setLoading(false); + }; + + fetchChats(); + }, []); + + return loading ? ( +
+ +
+ ) : ( +
+
+
+ +

+ Library +

+
+
+ {chats.length === 0 && ( +
+

+ No chats found. +

+
+ )} + {chats.length > 0 && ( +
+ {chats.map((chat, i) => ( +
+ + {chat.title} + +
+
+ +

+ {formatTimeDifference(new Date(), chat.createdAt)} Ago +

+
+
+
+ ))} +
+ )} +
+ ); +}; + +export default Page; diff --git a/ui/components/Chat.tsx b/ui/components/Chat.tsx index c0dbc92..8c0fb80 100644 --- a/ui/components/Chat.tsx +++ b/ui/components/Chat.tsx @@ -53,7 +53,7 @@ const Chat = ({ const isLast = i === messages.length - 1; return ( - + void, + setIsWSReady: (ready: boolean) => void, setError: (error: boolean) => void, ) => { const [ws, setWs] = useState(null); @@ -120,7 +122,7 @@ const useSocket = ( console.log('[DEBUG] open'); clearTimeout(timeoutId); setError(false); - setIsReady(true); + setIsWSReady(true); }; ws.onerror = () => { @@ -145,34 +147,114 @@ const useSocket = ( ws?.close(); console.log('[DEBUG] closed'); }; - }, [ws, url, setIsReady, setError]); + }, [ws, url, setIsWSReady, setError]); return ws; }; -const ChatWindow = () => { +const loadMessages = async ( + chatId: string, + setMessages: (messages: Message[]) => void, + setIsMessagesLoaded: (loaded: boolean) => void, + setChatHistory: (history: [string, string][]) => void, + setFocusMode: (mode: string) => void, +) => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const data = await res.json(); + + const messages = data.messages.map((msg: any) => { + return { + ...msg, + ...JSON.parse(msg.metadata), + }; + }) as Message[]; + + setMessages(messages); + + const history = messages.map((msg) => { + return [msg.role, msg.content]; + }) as [string, string][]; + + console.log('[DEBUG] messages loaded'); + + document.title = messages[0].content; + + setChatHistory(history); + setFocusMode(data.chat.focusMode); + console.log(data); + setIsMessagesLoaded(true); +}; + +const ChatWindow = ({ id }: { id?: string }) => { const searchParams = useSearchParams(); const initialMessage = searchParams.get('q'); - const [isReady, setIsReady] = useState(false); + const [chatId, setChatId] = useState(id); + const [newChatCreated, setNewChatCreated] = useState(false); + const [hasError, setHasError] = useState(false); + const [isReady, setIsReady] = useState(false); + + const [isWSReady, setIsWSReady] = useState(false); const ws = useSocket( process.env.NEXT_PUBLIC_WS_URL!, - setIsReady, + setIsWSReady, setHasError, ); - const [chatHistory, setChatHistory] = useState<[string, string][]>([]); - const [messages, setMessages] = useState([]); - const messagesRef = useRef([]); const [loading, setLoading] = useState(false); const [messageAppeared, setMessageAppeared] = useState(false); + + const [chatHistory, setChatHistory] = useState<[string, string][]>([]); + const [messages, setMessages] = useState([]); + const [focusMode, setFocusMode] = useState('webSearch'); + const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); + + useEffect(() => { + if ( + chatId && + !newChatCreated && + !isMessagesLoaded && + messages.length === 0 + ) { + loadMessages( + chatId, + setMessages, + setIsMessagesLoaded, + setChatHistory, + setFocusMode, + ); + } else if (!chatId) { + setNewChatCreated(true); + setIsMessagesLoaded(true); + setChatId(crypto.randomBytes(20).toString('hex')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const messagesRef = useRef([]); + useEffect(() => { messagesRef.current = messages; }, [messages]); + useEffect(() => { + if (isMessagesLoaded && isWSReady) { + setIsReady(true); + } + }, [isMessagesLoaded, isWSReady]); + const sendMessage = async (message: string) => { if (loading) return; setLoading(true); @@ -182,10 +264,15 @@ const ChatWindow = () => { let recievedMessage = ''; let added = false; + const messageId = crypto.randomBytes(7).toString('hex'); + ws?.send( JSON.stringify({ type: 'message', - content: message, + message: { + chatId: chatId!, + content: message, + }, focusMode: focusMode, history: [...chatHistory, ['human', message]], }), @@ -195,7 +282,8 @@ const ChatWindow = () => { ...prevMessages, { content: message, - id: Math.random().toString(36).substring(7), + messageId: messageId, + chatId: chatId!, role: 'user', createdAt: new Date(), }, @@ -217,7 +305,8 @@ const ChatWindow = () => { ...prevMessages, { content: '', - id: data.messageId, + messageId: data.messageId, + chatId: chatId!, role: 'assistant', sources: sources, createdAt: new Date(), @@ -234,7 +323,8 @@ const ChatWindow = () => { ...prevMessages, { content: data.data, - id: data.messageId, + messageId: data.messageId, + chatId: chatId!, role: 'assistant', sources: sources, createdAt: new Date(), @@ -245,7 +335,7 @@ const ChatWindow = () => { setMessages((prev) => prev.map((message) => { - if (message.id === data.messageId) { + if (message.messageId === data.messageId) { return { ...message, content: message.content + data.data }; } @@ -278,7 +368,7 @@ const ChatWindow = () => { const suggestions = await getSuggestions(messagesRef.current); setMessages((prev) => prev.map((msg) => { - if (msg.id === lastMsg.id) { + if (msg.messageId === lastMsg.messageId) { return { ...msg, suggestions: suggestions }; } return msg; @@ -292,7 +382,7 @@ const ChatWindow = () => { }; const rewrite = (messageId: string) => { - const index = messages.findIndex((msg) => msg.id === messageId); + const index = messages.findIndex((msg) => msg.messageId === messageId); if (index === -1) return; diff --git a/ui/components/MessageBox.tsx b/ui/components/MessageBox.tsx index 1dce2d0..b111088 100644 --- a/ui/components/MessageBox.tsx +++ b/ui/components/MessageBox.tsx @@ -119,7 +119,7 @@ const MessageBox = ({ {/* */} - +
diff --git a/ui/components/MessageBoxLoading.tsx b/ui/components/MessageBoxLoading.tsx index caa6f18..3c53d9e 100644 --- a/ui/components/MessageBoxLoading.tsx +++ b/ui/components/MessageBoxLoading.tsx @@ -1,6 +1,6 @@ const MessageBoxLoading = () => { return ( -
+
diff --git a/ui/components/Sidebar.tsx b/ui/components/Sidebar.tsx index ed8953e..cc2097d 100644 --- a/ui/components/Sidebar.tsx +++ b/ui/components/Sidebar.tsx @@ -23,7 +23,7 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => { { icon: Home, href: '/', - active: segments.length === 0, + active: segments.length === 0 || segments.includes('c'), label: 'Home', }, {