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>
    );
}