Spaces:
Sleeping
Sleeping
File size: 14,162 Bytes
c5b5cc8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
"use client";
import { useState, useRef, useEffect } from 'react';
import { Send, User, Bot, Loader2 } from 'lucide-react';
import { api } from '@/lib/api';
import { useRouter } from 'next/navigation';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export default function ChatInterface({ conversationId, initialMessages = [] }) {
const [messages, setMessages] = useState(initialMessages);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
const router = useRouter();
const [streamingContent, setStreamingContent] = useState('');
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages, streamingContent]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
setInput('');
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
setIsLoading(true);
setStreamingContent('');
let currentId = conversationId;
try {
// If no conversation ID, create one first
if (!currentId) {
const newConv = await api.createConversation();
if (!newConv) throw new Error("Failed to create conversation");
currentId = newConv.conversation_id;
// Update URL without reloading
window.history.pushState({}, '', `/c/${currentId}`);
// Or use router.replace if prefer Next.js way, but we want to stay mounted
// We might need to handle this carefully.
// For now, let's just proceed with the currentId for the request.
}
// Start streaming request
const response = await fetch(api.getChatEndpoint(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversation_id: currentId,
user_message: [{ type: 'text', text: userMessage }]
}),
});
if (!response.ok) throw new Error('Network response was not ok');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
let fullAssistantMessage = '';
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value, { stream: !done });
// Parse SSE format: "data: {...}\n\n"
const lines = chunkValue.split('\n\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6);
if (dataStr === '[DONE]') {
done = true;
break;
}
try {
const data = JSON.parse(dataStr);
if (data.content) {
fullAssistantMessage += data.content;
setStreamingContent(fullAssistantMessage);
}
if (data.error) {
console.error("Stream error:", data.error);
}
} catch (e) {
// ignore partial json
}
}
}
}
// Finalize message
setMessages(prev => [...prev, { role: 'assistant', content: fullAssistantMessage }]);
setStreamingContent('');
setIsLoading(false);
// If we created a new chat, update the sidebar without reloading the chat component
if (!conversationId && currentId) {
window.dispatchEvent(new Event('chat-update'));
// Ensure the router knows about the new path for future navigations,
// but do it silently if possible or just rely on pushState.
// We already did pushState.
} else {
// Triggers sidebar update for existing chats too (timestamp update)
window.dispatchEvent(new Event('chat-update'));
}
} catch (error) {
console.error("Error sending message:", error);
setMessages(prev => [...prev, { role: 'system', content: "Error sending message. Please try again." }]);
setIsLoading(false);
}
};
return (
<div className="flex flex-col h-full max-w-3xl mx-auto w-full">
{/* Messages Area */}
<div className="flex-1 overflow-y-auto w-full p-4 md:p-6 pb-32">
{messages.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-center opacity-50">
<h1 className="text-4xl font-semibold mb-8">Analytical Chat</h1>
<p className="max-w-md text-sm">Ask anything about your data. The AI will analyze and provide insights.</p>
</div>
)}
{messages.map((msg, idx) => (
<div key={idx} className={`group w-full text-gray-800 dark:text-gray-100 border-b border-black/5 dark:border-white/5 ${msg.role === 'assistant' ? 'bg-[var(--ai-msg-bg)]' : 'bg-[var(--user-msg-bg)]'
}`}>
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-[38rem] xl:max-w-3xl flex lg:px-0 m-auto p-4 md:py-6">
<div className="w-[30px] flex flex-col relative items-end">
<div className={`relative h-[30px] w-[30px] p-1 rounded-sm flex items-center justify-center ${msg.role === 'user' ? 'bg-black text-white dark:bg-white dark:text-black' : 'bg-green-500 text-white'}`}>
{msg.role === 'user' ? <User size={18} /> : <Bot size={18} />}
</div>
</div>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 md:gap-3 lg:w-[calc(100%-115px)]">
<div className="flex flex-grow flex-col gap-3">
<div className="min-h-[20px] flex flex-col items-start gap-4 whitespace-pre-wrap break-words prose prose-neutral dark:prose-invert max-w-none text-black dark:text-gray-100">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
table: ({ node, ...props }) => <div className="overflow-x-auto my-4 w-full"><table className="border-collapse table-auto w-full text-sm" {...props} /></div>,
th: ({ node, ...props }) => <th className="border-b dark:border-white/20 border-black/10 px-4 py-2 text-left font-semibold" {...props} />,
td: ({ node, ...props }) => <td className="border-b dark:border-white/10 border-black/5 px-4 py-2" {...props} />,
p: ({ node, ...props }) => <p className="mb-2 last:mb-0" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc pl-4 mb-4" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal pl-4 mb-4" {...props} />,
li: ({ node, ...props }) => <li className="mb-1" {...props} />,
code: ({ node, inline, className, children, ...props }) => {
return inline ?
<code className="bg-black/10 dark:bg-white/10 rounded-sm px-1 py-0.5 font-mono text-sm" {...props}>{children}</code> :
<code className="block bg-black/10 dark:bg-white/10 rounded-md p-4 font-mono text-sm overflow-x-auto my-2" {...props}>{children}</code>
}
}}
>
{msg.content}
</ReactMarkdown>
</div>
</div>
</div>
</div>
</div>
))}
{/* Streaming Message Bubble */}
{(isLoading && streamingContent) && (
<div className="group w-full text-gray-800 dark:text-gray-100 border-b border-black/5 dark:border-white/5 bg-[var(--ai-msg-bg)]">
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-[38rem] xl:max-w-3xl flex lg:px-0 m-auto p-4 md:py-6">
<div className="w-[30px] flex flex-col relative items-end">
<div className="relative h-[30px] w-[30px] p-1 rounded-sm flex items-center justify-center bg-green-500 text-white">
<Bot size={18} />
</div>
</div>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 md:gap-3 lg:w-[calc(100%-115px)]">
<div className="flex flex-grow flex-col gap-3">
<div className="min-h-[20px] flex flex-col items-start gap-4 whitespace-pre-wrap break-words prose prose-neutral dark:prose-invert max-w-none text-black dark:text-gray-100">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
table: ({ node, ...props }) => <div className="overflow-x-auto my-4 w-full"><table className="border-collapse table-auto w-full text-sm" {...props} /></div>,
th: ({ node, ...props }) => <th className="border-b dark:border-white/20 border-black/10 px-4 py-2 text-left font-semibold" {...props} />,
td: ({ node, ...props }) => <td className="border-b dark:border-white/10 border-black/5 px-4 py-2" {...props} />,
}}
>
{streamingContent}
</ReactMarkdown>
<span className="w-2 h-4 bg-gray-500 inline-block animate-pulse" />
</div>
</div>
</div>
</div>
</div>
)}
{(isLoading && !streamingContent) && (
<div className="flex justify-center p-4"><Loader2 className="animate-spin text-gray-400" /></div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="absolute bottom-0 left-0 w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent md:bg-gradient-to-t from-white dark:from-[var(--background-start-rgb)] to-transparent pt-0 md:pt-2">
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
<div className="relative flex h-full flex-1 items-stretch md:flex-col">
<div className="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 dark:border-gray-900/50 text-black dark:text-white bg-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
<form onSubmit={handleSubmit} className="flex flex-row w-full items-center">
<input
className="m-0 w-full resize-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 md:pl-0 outline-none overflow-y-hidden h-[24px]"
placeholder="Send a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
autoFocus
/>
<button
type="submit"
disabled={isLoading || input.length === 0}
className="absolute p-1 rounded-md text-gray-500 bottom-1.5 right-1 md:bottom-2.5 md:right-2 hover:bg-gray-100 dark:hover:bg-gray-900 disabled:hover:bg-transparent disabled:opacity-40 transition-colors"
>
<Send size={16} />
</button>
</form>
</div>
<div className="px-2 py-2 text-center text-xs text-gray-600 dark:text-gray-300 md:px-[60px]">
<span>AI can make mistakes. Consider checking important information.</span>
</div>
</div>
</div>
</div>
</div>
);
}
|