import React, { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import { FaUser, FaRobot, FaSpinner } from 'react-icons/fa'; import axios from 'axios'; interface Message { role: 'user' | 'assistant'; content: string; } const ChatPanel: React.FC<{ projectPath: string; model: string }> = ({ projectPath, model }) => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [mentionQuery, setMentionQuery] = useState(''); const [mentionItems, setMentionItems] = useState([]); const [showMentions, setShowMentions] = useState(false); const [contextFiles, setContextFiles] = useState([]); const chatEnd = useRef(null); const inputRef = useRef(null); const send = async () => { if (!input.trim() || loading) return; const userMsg = input; setInput(''); setMessages(prev => [...prev, { role: 'user', content: userMsg }]); setLoading(true); const assistantMsgRef = { current: '' }; setMessages(prev => [...prev, { role: 'assistant', content: '' }]); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [...messages, { role: 'user', content: userMsg }], project_path: projectPath, model: model, context_files: contextFiles, }), }); if (!response.ok) throw new Error(`Server responded with ${response.status}`); const reader = response.body?.getReader(); if (!reader) throw new Error('No response body'); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') break; try { const { token } = JSON.parse(data); if (token.startsWith('ERROR:')) { assistantMsgRef.current = `❌ ${token.substring(7)}`; setMessages(prev => { const copy = [...prev]; copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current }; return copy; }); return; } assistantMsgRef.current += token; setMessages(prev => { const copy = [...prev]; copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current }; return copy; }); } catch (e) { assistantMsgRef.current = `Parse error: ${data}`; setMessages(prev => { const copy = [...prev]; copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current }; return copy; }); return; } } } } } catch (err: any) { setMessages(prev => { const copy = [...prev]; copy[copy.length - 1] = { role: 'assistant', content: `❌ ${err.message}` }; return copy; }); } finally { setLoading(false); } }; useEffect(() => { chatEnd.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setInput(value); const atIndex = value.lastIndexOf('@'); if (atIndex !== -1 && (atIndex === 0 || value[atIndex - 1] === ' ')) { const query = value.slice(atIndex + 1).toLowerCase(); setMentionQuery(query); if ('git'.startsWith(query)) { setMentionItems([{ name: 'git (current diff)', type: 'special', path: '__git__' }]); setShowMentions(true); return; } axios.get('/api/files', { params: { project_path: projectPath } }) .then(res => { const flatten = (nodes: any[]): any[] => { let result: any[] = []; for (const node of nodes) { if (node.name.toLowerCase().includes(query)) result.push(node); if (node.children) result = result.concat(flatten(node.children)); } return result; }; const matches = flatten(res.data).slice(0, 5); setMentionItems(matches); setShowMentions(matches.length > 0); }) .catch(() => setShowMentions(false)); } else { setShowMentions(false); } }; const insertMention = (item: any) => { const atIndex = input.lastIndexOf('@'); const newInput = input.slice(0, atIndex) + item.name + ' '; setInput(newInput); setShowMentions(false); if (item.path === '__git__') { if (!contextFiles.includes('__git__')) { setContextFiles(prev => [...prev, '__git__']); } } else if (!contextFiles.includes(item.path)) { setContextFiles(prev => [...prev, item.path]); } inputRef.current?.focus(); }; return (
{messages.map((m, i) => (
{m.role === 'user' ? : } {m.role === 'user' ? 'You' : 'Codeki'}
{m.content}
))} {loading && (
Codeki is thinking...
)}
{showMentions && (
{mentionItems.map(item => (
insertMention(item)} style={{ padding: '4px 8px', cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', gap: 6 }} > {item.type === 'folder' ? '📁' : item.type === 'special' ? '🔗' : '📄'} {item.name}
))}
)}
e.key === 'Enter' && send()} placeholder="Ask anything... Type @ to reference a file or @git" disabled={loading} />
{contextFiles.length > 0 && (
{contextFiles.map(f => ( 📎 {f === '__git__' ? 'Git Diff' : f.split('/').pop()} setContextFiles(prev => prev.filter(x => x !== f))}>× ))}
)}
); }; export default ChatPanel;