|
1 | | -"use client" |
2 | | - |
3 | | -import React, { useState, useEffect, useRef, useCallback } from "react" |
4 | | -import { useRouter, usePathname, useParams } from "next/navigation" |
5 | | -import { IconSend, IconPlus, IconLoader } from "@tabler/icons-react" |
6 | | -import ChatBubble from "@components/ChatBubble" |
7 | | -import toast from "react-hot-toast" |
8 | | - |
9 | | -export default function HomePage() { |
10 | | - const router = useRouter() |
11 | | - const pathname = usePathname() |
12 | | - const params = useParams() |
13 | | - |
14 | | - const [chatId, setChatId] = useState(null) |
15 | | - const [messages, setMessages] = useState([]) |
16 | | - const [input, setInput] = useState("") |
17 | | - const [isLoading, setIsLoading] = useState(false) |
18 | | - const [isStreaming, setIsStreaming] = useState(false) |
19 | | - const messagesEndRef = useRef(null) |
20 | | - |
21 | | - // Existing useEffect for params and pathname |
22 | | - useEffect(() => { |
23 | | - const urlChatId = |
24 | | - params.chatId || |
25 | | - (Array.isArray(params.slug) ? params.slug[0] : null) |
26 | | - if (!urlChatId && pathname === "/chat") { |
27 | | - setMessages([]) |
28 | | - setChatId(null) |
29 | | - return |
30 | | - } |
31 | | - setChatId(urlChatId) |
32 | | - }, [params, pathname]) |
33 | | - |
34 | | - // Existing useEffect for fetching history |
35 | | - useEffect(() => { |
36 | | - const fetchHistory = async () => { |
37 | | - if (!chatId) { |
38 | | - setMessages([]) |
39 | | - return |
40 | | - } |
41 | | - setIsLoading(true) |
42 | | - try { |
43 | | - const response = await fetch(`/api/chat/${chatId}/history`) |
44 | | - if (!response.ok) { |
45 | | - throw new Error(`HTTP error! status: ${response.status}`) |
46 | | - } |
47 | | - const data = await response.json() |
48 | | - setMessages(data.messages) |
49 | | - } catch (error) { |
50 | | - console.error("Failed to fetch chat history:", error) |
51 | | - toast.error("Failed to load chat history.") |
52 | | - setMessages([]) |
53 | | - } finally { |
54 | | - setIsLoading(false) |
55 | | - } |
56 | | - } |
57 | | - fetchHistory() |
58 | | - }, [chatId]) |
59 | | - |
60 | | - // New useEffect for route change |
61 | | - useEffect(() => { |
62 | | - const handleRouteChange = (url) => { |
63 | | - if (url === "/chat") { |
64 | | - setMessages([]) |
65 | | - setChatId(null) |
66 | | - } |
67 | | - } |
68 | | - |
69 | | - router.events.on("routeChangeComplete", handleRouteChange) |
70 | | - |
71 | | - return () => { |
72 | | - router.events.off("routeChangeComplete", handleRouteChange) |
73 | | - } |
74 | | - }, [router]) |
75 | | - |
76 | | - const scrollToBottom = useCallback(() => { |
77 | | - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) |
78 | | - }, []) |
79 | | - |
80 | | - useEffect(scrollToBottom, [messages]) |
81 | | - |
82 | | - const handleNewChat = () => { |
83 | | - setMessages([]) // always clear messages |
84 | | - setChatId(null) // clear current chatId so that new chat gets created |
85 | | - router.push("/chat") // navigate to base chat page |
86 | | - } |
87 | | - |
88 | | - const handleSend = async (e) => { |
89 | | - e.preventDefault() |
90 | | - if (!input.trim() || isStreaming) return |
91 | | - |
92 | | - const userMessage = { role: "user", content: input } |
93 | | - setMessages((prev) => [...prev, userMessage]) |
94 | | - setInput("") |
95 | | - setIsStreaming(true) |
96 | | - setIsLoading(true) |
97 | | - |
98 | | - try { |
99 | | - const response = await fetch("/api/chat", { |
100 | | - method: "POST", |
101 | | - headers: { |
102 | | - "Content-Type": "application/json" |
103 | | - }, |
104 | | - body: JSON.stringify({ |
105 | | - chat_id: chatId, |
106 | | - message: input |
107 | | - }) |
108 | | - }) |
109 | | - |
110 | | - if (!response.ok || !response.body) { |
111 | | - throw new Error(`HTTP error! status: ${response.status}`) |
112 | | - } |
113 | | - |
114 | | - const assistantMessage = { role: "assistant", content: "" } |
115 | | - setMessages((prev) => [...prev, assistantMessage]) |
116 | | - |
117 | | - const reader = response.body.getReader() |
118 | | - const decoder = new TextDecoder() |
119 | | - let currentChatId = chatId |
120 | | - |
121 | | - while (true) { |
122 | | - const { value, done } = await reader.read() |
123 | | - if (done) break |
124 | | - |
125 | | - const chunk = decoder.decode(value, { stream: true }) |
126 | | - const lines = chunk |
127 | | - .split("\n") |
128 | | - .filter((line) => line.trim() !== "") |
129 | | - |
130 | | - for (const line of lines) { |
131 | | - try { |
132 | | - const json = JSON.parse(line) |
133 | | - if (json.chat_id && !currentChatId) { |
134 | | - const newChatId = json.chat_id |
135 | | - // FIX: Update URL without a full page reload to preserve the stream |
136 | | - window.history.replaceState( |
137 | | - {}, |
138 | | - "", |
139 | | - `/chat/${newChatId}` |
140 | | - ) |
141 | | - setChatId(newChatId) |
142 | | - currentChatId = newChatId // Update for subsequent chunks in this stream |
143 | | - } |
144 | | - if (json.token) { |
145 | | - assistantMessage.content += json.token |
146 | | - setMessages((prev) => { |
147 | | - const newMessages = [...prev] |
148 | | - newMessages[newMessages.length - 1] = { |
149 | | - ...assistantMessage |
150 | | - } |
151 | | - return newMessages |
152 | | - }) |
153 | | - } |
154 | | - } catch (error) { |
155 | | - console.error( |
156 | | - "Failed to parse JSON from stream:", |
157 | | - error, |
158 | | - "Line:", |
159 | | - line |
160 | | - ) |
161 | | - } |
162 | | - } |
163 | | - } |
164 | | - } catch (error) { |
165 | | - console.error("Streaming error:", error) |
166 | | - toast.error("Failed to get response from AI.") |
167 | | - setMessages((prev) => prev.slice(0, prev.length - 1)) // Remove the empty assistant message |
168 | | - } finally { |
169 | | - setIsStreaming(false) |
170 | | - setIsLoading(false) |
171 | | - } |
172 | | - } |
173 | | - |
174 | | - return ( |
175 | | - <div className="flex-1 flex flex-col h-screen bg-neutral-900 text-white md:pl-20"> |
176 | | - <main className="flex-1 flex flex-col overflow-hidden"> |
177 | | - <header className="flex items-center justify-between p-4 border-b border-neutral-800"> |
178 | | - <h1 className="text-xl font-semibold">Chat</h1> |
179 | | - <button |
180 | | - onClick={handleNewChat} |
181 | | - className="flex items-center gap-2 px-3 py-2 text-sm rounded-lg bg-neutral-800 hover:bg-neutral-700" |
182 | | - > |
183 | | - <IconPlus size={16} /> New Chat |
184 | | - </button> |
185 | | - </header> |
186 | | - <div className="flex-1 overflow-y-auto p-4 space-y-4"> |
187 | | - {messages.map((msg, i) => ( |
188 | | - <ChatBubble |
189 | | - key={i} |
190 | | - message={msg.content} |
191 | | - isUser={msg.role === "user"} |
192 | | - /> |
193 | | - ))} |
194 | | - {isLoading && <IconLoader className="animate-spin" />} |
195 | | - <div ref={messagesEndRef} /> |
196 | | - </div> |
197 | | - <div className="p-4 border-t border-neutral-800"> |
198 | | - <form |
199 | | - onSubmit={handleSend} |
200 | | - className="flex items-center gap-3" |
201 | | - > |
202 | | - <input |
203 | | - type="text" |
204 | | - value={input} |
205 | | - onChange={(e) => setInput(e.target.value)} |
206 | | - placeholder="Ask Sentient anything..." |
207 | | - className="flex-1 p-3 bg-neutral-800 rounded-lg focus:ring-2 focus:ring-blue-500" |
208 | | - disabled={isStreaming} |
209 | | - /> |
210 | | - <button |
211 | | - type="submit" |
212 | | - className="p-3 bg-blue-600 rounded-lg" |
213 | | - disabled={isStreaming || !input.trim()} |
214 | | - > |
215 | | - <IconSend size={20} /> |
216 | | - </button> |
217 | | - </form> |
218 | | - </div> |
219 | | - </main> |
220 | | - </div> |
221 | | - ) |
222 | | -} |
0 commit comments