import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Button } from '../ui/button'; import { Send, StopCircle, Copy, Check, ChevronDown, ChevronUp, X, CornerUpLeft, Loader2 } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import LoadingDots from '../ui/LoadingDots'; interface Message { role: 'user' | 'assistant'; content: string; context?: string; } export default function AIChat() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const chatContainerRef = useRef(null); const abortControllerRef = useRef(null); const [copiedIndex, setCopiedIndex] = useState(null); const [context, setContext] = useState(null); const [isContextExpanded, setIsContextExpanded] = useState(false); const [expandedMessageIndex, setExpandedMessageIndex] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { scrollToBottom(); }, [messages]); const scrollToBottom = () => { if (chatContainerRef.current) { setTimeout(() => { chatContainerRef.current?.scrollTo({ top: chatContainerRef.current.scrollHeight, behavior: 'smooth' }); }, 100); } }; const handleSend = useCallback(async () => { if (input.trim() === '' && !context) return; const newMessage: Message = { role: 'user', content: input, context: context || undefined }; setMessages(prev => [...prev, newMessage]); setInput(''); setIsContextExpanded(false); setIsGenerating(true); setIsLoading(true); abortControllerRef.current = new AbortController(); try { const queryParams = new URLSearchParams({ instructions: input, ...(context && { context }) }); const response = await fetch(`http://127.0.0.1:8787/api?${queryParams}`, { method: 'GET', signal: abortControllerRef.current.signal, }); if (!response.ok) { throw new Error('Failed to get AI response'); } const reader = response.body?.getReader(); const decoder = new TextDecoder(); const assistantMessage: Message = { role: 'assistant', content: '' }; setMessages(prev => [...prev, assistantMessage]); setIsLoading(false); let buffer = ''; const updateInterval = 100; // Update every 100ms let lastUpdateTime = Date.now(); if (reader) { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const currentTime = Date.now(); if (currentTime - lastUpdateTime > updateInterval) { setMessages(prev => { const updatedMessages = [...prev]; const lastMessage = updatedMessages[updatedMessages.length - 1]; lastMessage.content = buffer; return updatedMessages; }); lastUpdateTime = currentTime; } } // Final update to ensure all content is displayed setMessages(prev => { const updatedMessages = [...prev]; const lastMessage = updatedMessages[updatedMessages.length - 1]; lastMessage.content = buffer; return updatedMessages; }); } } catch (error: any) { if (error.name === 'AbortError') { console.log('Generation aborted'); } else { console.error('Error fetching AI response:', error); const errorMessage: Message = { role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }; setMessages(prev => [...prev, errorMessage]); } } finally { setIsGenerating(false); setIsLoading(false); abortControllerRef.current = null; } }, [input, context]); const handleStopGeneration = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; const copyToClipboard = (text: string, index: number) => { navigator.clipboard.writeText(text).then(() => { setCopiedIndex(index); setTimeout(() => setCopiedIndex(null), 1000); // Reset after 1 seconds }); }; const askAboutCode = (code: string) => { setContext(`Regarding this code:\n${code}`); setIsContextExpanded(false); }; const removeContext = () => { setContext(null); setIsContextExpanded(false); }; return (
CHAT
{messages.map((message, messageIndex) => (
{message.context && (
setExpandedMessageIndex(expandedMessageIndex === messageIndex ? null : messageIndex)} > Context {expandedMessageIndex === messageIndex ? ( ) : ( )}
{expandedMessageIndex === messageIndex && (
{(() => { // need to fix the language detection const code = message.context!.replace(/^Regarding this code:\n/, ''); const match = /language-(\w+)/.exec(code); const language = match ? match[1] : 'typescript'; return (
{code}
); })()}
)}
)} {message.role === 'assistant' ? (
{language}
{String(children).replace(/\n$/, '')}
) : ( {children} ); }, p({children}) { return

{children}

; }, ul({children}) { return
    {children}
; }, ol({children}) { return
    {children}
; }, li({children}) { return
  • {children}
  • ; }, }} > {message.content} ) : (
    {message.content}
    )}
    ))} {isLoading && ( )}
    {context && (
    setIsContextExpanded(!isContextExpanded)} > Context
    {isContextExpanded ? ( setIsContextExpanded(false)} /> ) : ( setIsContextExpanded(true)} /> )}
    {isContextExpanded && (