2024-10-13 22:47:47 -04:00
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
2024-10-13 01:41:48 -04:00
|
|
|
import { Button } from '../ui/button';
|
2024-10-13 22:47:47 -04:00
|
|
|
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';
|
2024-10-13 01:41:48 -04:00
|
|
|
|
|
|
|
interface Message {
|
|
|
|
role: 'user' | 'assistant';
|
|
|
|
content: string;
|
2024-10-13 22:47:47 -04:00
|
|
|
context?: string;
|
2024-10-13 01:41:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export default function AIChat() {
|
|
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
|
|
const [input, setInput] = useState('');
|
2024-10-13 22:47:47 -04:00
|
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
2024-10-13 01:41:48 -04:00
|
|
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
2024-10-13 22:47:47 -04:00
|
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
|
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
|
|
const [context, setContext] = useState<string | null>(null);
|
|
|
|
const [isContextExpanded, setIsContextExpanded] = useState(false);
|
|
|
|
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2024-10-13 01:41:48 -04:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-10-13 22:47:47 -04:00
|
|
|
scrollToBottom();
|
|
|
|
}, [messages]);
|
|
|
|
|
|
|
|
const scrollToBottom = () => {
|
2024-10-13 01:41:48 -04:00
|
|
|
if (chatContainerRef.current) {
|
2024-10-13 22:47:47 -04:00
|
|
|
setTimeout(() => {
|
|
|
|
chatContainerRef.current?.scrollTo({
|
|
|
|
top: chatContainerRef.current.scrollHeight,
|
|
|
|
behavior: 'smooth'
|
|
|
|
});
|
|
|
|
}, 100);
|
2024-10-13 01:41:48 -04:00
|
|
|
}
|
2024-10-13 22:47:47 -04:00
|
|
|
};
|
2024-10-13 01:41:48 -04:00
|
|
|
|
|
|
|
const handleSend = async () => {
|
2024-10-13 22:47:47 -04:00
|
|
|
if (input.trim() === '' && !context) return;
|
2024-10-13 01:41:48 -04:00
|
|
|
|
2024-10-13 22:47:47 -04:00
|
|
|
const newMessage: Message = {
|
|
|
|
role: 'user',
|
|
|
|
content: input,
|
|
|
|
context: context || undefined
|
|
|
|
};
|
2024-10-13 01:41:48 -04:00
|
|
|
setMessages(prev => [...prev, newMessage]);
|
|
|
|
setInput('');
|
2024-10-13 22:47:47 -04:00
|
|
|
setIsContextExpanded(false);
|
|
|
|
setIsGenerating(true);
|
|
|
|
setIsLoading(true); // Set loading state to true
|
|
|
|
|
|
|
|
abortControllerRef.current = new AbortController();
|
2024-10-13 01:41:48 -04:00
|
|
|
|
2024-10-13 22:47:47 -04:00
|
|
|
try {
|
|
|
|
const queryParams = new URLSearchParams({
|
|
|
|
instructions: input,
|
|
|
|
...(context && { context }) // Include context only if it exists
|
|
|
|
});
|
|
|
|
const response = await fetch(`http://127.0.0.1:8787/api?${queryParams}`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
|
|
|
signal: abortControllerRef.current.signal,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error('Failed to get AI response');
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
const assistantMessage: Message = { role: 'assistant', content: '' };
|
2024-10-13 01:41:48 -04:00
|
|
|
setMessages(prev => [...prev, assistantMessage]);
|
2024-10-13 22:47:47 -04:00
|
|
|
setIsLoading(false); // Set loading state to false once we start receiving the response
|
|
|
|
|
|
|
|
// Simulate text generation
|
|
|
|
for (let i = 0; i <= data.response.length; i++) {
|
|
|
|
if (abortControllerRef.current.signal.aborted) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
setMessages(prev => {
|
|
|
|
const updatedMessages = [...prev];
|
|
|
|
updatedMessages[updatedMessages.length - 1].content = data.response.slice(0, i);
|
|
|
|
return updatedMessages;
|
|
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
|
|
}
|
|
|
|
} 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); // Ensure loading state is set to false
|
|
|
|
abortControllerRef.current = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
2024-10-13 01:41:48 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2024-10-13 22:47:47 -04:00
|
|
|
<div className="flex flex-col h-screen w-full">
|
|
|
|
<span className="text-muted-foreground/50 font-medium p-2">CHAT</span>
|
2024-10-13 01:41:48 -04:00
|
|
|
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
|
2024-10-13 22:47:47 -04:00
|
|
|
{messages.map((message, messageIndex) => (
|
|
|
|
<div key={messageIndex} className="text-left">
|
|
|
|
<div className={`inline-block p-2 rounded-lg ${
|
|
|
|
message.role === 'user'
|
|
|
|
? 'bg-[#262626] text-white'
|
|
|
|
: 'bg-transparent text-white'
|
|
|
|
} max-w-full`}>
|
|
|
|
{message.context && (
|
|
|
|
<div className="mb-2 bg-input rounded-lg">
|
|
|
|
<div
|
|
|
|
className="flex justify-between items-center cursor-pointer"
|
|
|
|
onClick={() => setExpandedMessageIndex(expandedMessageIndex === messageIndex ? null : messageIndex)}
|
|
|
|
>
|
|
|
|
<span className="text-sm text-gray-300">
|
|
|
|
Context
|
|
|
|
</span>
|
|
|
|
{expandedMessageIndex === messageIndex ? (
|
|
|
|
<ChevronUp size={16} />
|
|
|
|
) : (
|
|
|
|
<ChevronDown size={16} />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
{expandedMessageIndex === messageIndex && (
|
|
|
|
<div className="relative">
|
|
|
|
<div className="absolute top-0 right-0 flex p-1">
|
|
|
|
<Button
|
|
|
|
onClick={() => copyToClipboard(message.context!.replace(/^Regarding this code:\n/, ''), messageIndex)}
|
|
|
|
size="sm"
|
|
|
|
variant="ghost"
|
|
|
|
className="p-1 h-6"
|
|
|
|
>
|
|
|
|
{copiedIndex === messageIndex ? (
|
|
|
|
<Check className="w-4 h-4 text-green-500" />
|
|
|
|
) : (
|
|
|
|
<Copy className="w-4 h-4" />
|
|
|
|
)}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
{(() => {
|
|
|
|
// 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 (
|
|
|
|
<div className="pt-6">
|
|
|
|
<SyntaxHighlighter
|
|
|
|
style={vscDarkPlus as any}
|
|
|
|
language={language}
|
|
|
|
PreTag="div"
|
|
|
|
customStyle={{
|
|
|
|
margin: 0,
|
|
|
|
padding: '0.5rem',
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{code}
|
|
|
|
</SyntaxHighlighter>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})()}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{message.role === 'assistant' ? (
|
|
|
|
<ReactMarkdown
|
|
|
|
components={{
|
|
|
|
code({node, className, children, ...props}) {
|
|
|
|
const match = /language-(\w+)/.exec(className || '');
|
|
|
|
const language = match ? match[1] : '';
|
|
|
|
return match ? (
|
|
|
|
<div className="relative border border-input rounded-md my-4">
|
|
|
|
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
|
|
|
{language}
|
|
|
|
</div>
|
|
|
|
<div className="absolute top-0 right-0 flex">
|
|
|
|
<Button
|
|
|
|
onClick={() => copyToClipboard(String(children), messageIndex)}
|
|
|
|
size="sm"
|
|
|
|
variant="ghost"
|
|
|
|
className="p-1 h-6"
|
|
|
|
>
|
|
|
|
{copiedIndex === messageIndex ? (
|
|
|
|
<Check className="w-4 h-4 text-green-500" />
|
|
|
|
) : (
|
|
|
|
<Copy className="w-4 h-4" />
|
|
|
|
)}
|
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
onClick={() => askAboutCode(String(children))}
|
|
|
|
size="sm"
|
|
|
|
variant="ghost"
|
|
|
|
className="p-1 h-6"
|
|
|
|
>
|
|
|
|
<CornerUpLeft className="w-4 h-4" />
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
<div className="pt-6">
|
|
|
|
<SyntaxHighlighter
|
|
|
|
style={vscDarkPlus as any}
|
|
|
|
language={match[1]}
|
|
|
|
PreTag="div"
|
|
|
|
customStyle={{
|
|
|
|
margin: 0,
|
|
|
|
padding: '0.5rem',
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{String(children).replace(/\n$/, '')}
|
|
|
|
</SyntaxHighlighter>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<code className={className} {...props}>
|
|
|
|
{children}
|
|
|
|
</code>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
p({children}) {
|
|
|
|
return <p className="mb-4 whitespace-pre-line">{children}</p>;
|
|
|
|
},
|
|
|
|
ul({children}) {
|
|
|
|
return <ul className="list-disc pl-6 mb-4">{children}</ul>;
|
|
|
|
},
|
|
|
|
ol({children}) {
|
|
|
|
return <ol className="list-decimal pl-6 mb-4">{children}</ol>;
|
|
|
|
},
|
|
|
|
li({children}) {
|
|
|
|
return <li className="mb-2">{children}</li>;
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{message.content}
|
|
|
|
</ReactMarkdown>
|
|
|
|
) : (
|
|
|
|
<div className="whitespace-pre-wrap">{message.content}</div>
|
|
|
|
)}
|
2024-10-13 01:41:48 -04:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
2024-10-13 22:47:47 -04:00
|
|
|
{isLoading && (
|
|
|
|
<LoadingDots />
|
|
|
|
)}
|
2024-10-13 01:41:48 -04:00
|
|
|
</div>
|
2024-10-13 22:47:47 -04:00
|
|
|
<div className="p-4 border-t mb-14">
|
|
|
|
{context && (
|
|
|
|
<div className="mb-2 bg-input p-2 rounded-lg">
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
<div
|
|
|
|
className="flex-grow cursor-pointer"
|
|
|
|
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
|
|
|
>
|
|
|
|
<span className="text-sm text-gray-300">
|
|
|
|
Context
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<div className="flex items-center">
|
|
|
|
{isContextExpanded ? (
|
|
|
|
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
|
|
|
|
) : (
|
|
|
|
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
|
|
|
|
)}
|
|
|
|
<X
|
|
|
|
size={16}
|
|
|
|
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
|
|
|
onClick={removeContext}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{isContextExpanded && (
|
|
|
|
<textarea
|
|
|
|
value={context.replace(/^Regarding this code:\n/, '')}
|
|
|
|
onChange={(e) => setContext(e.target.value)}
|
|
|
|
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
|
|
|
rows={5}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<div className="flex space-x-2 min-w-0">
|
|
|
|
<input
|
2024-10-13 01:41:48 -04:00
|
|
|
type="text"
|
|
|
|
value={input}
|
|
|
|
onChange={(e) => setInput(e.target.value)}
|
2024-10-13 22:47:47 -04:00
|
|
|
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
|
|
|
|
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
|
|
|
placeholder={context ? "Add more context or ask a question..." : "Type your message..."}
|
|
|
|
disabled={isGenerating}
|
2024-10-13 01:41:48 -04:00
|
|
|
/>
|
2024-10-13 22:47:47 -04:00
|
|
|
{isGenerating ? (
|
|
|
|
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
|
|
|
|
<StopCircle className="w-4 h-4" />
|
|
|
|
</Button>
|
|
|
|
) : (
|
|
|
|
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
|
|
|
|
<Send className="w-4 h-4" />
|
|
|
|
</Button>
|
|
|
|
)}
|
2024-10-13 01:41:48 -04:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|