Spaces:
Build error
Build error
| 'use client'; | |
| import { useState, useEffect } from 'react'; | |
| import { Card, CardContent } from '@/components/ui/card'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Button } from '@/components/ui/button'; | |
| import { MessageSquare, Copy, ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react'; | |
| import { cn } from '@/lib/utils'; | |
| interface ConversationPanelProps { | |
| completion: string; | |
| isLoading: boolean; | |
| className?: string; | |
| } | |
| interface Message { | |
| id: string; | |
| role: 'user' | 'assistant'; | |
| content: string; | |
| timestamp: Date; | |
| feedback?: 'positive' | 'negative'; | |
| } | |
| export function ConversationPanel({ completion, isLoading, className }: ConversationPanelProps) { | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [currentMessage, setCurrentMessage] = useState<string>(''); | |
| // Update messages when completion changes | |
| useEffect(() => { | |
| if (completion) { | |
| const newMessage: Message = { | |
| id: Date.now().toString(), | |
| role: 'assistant', | |
| content: completion, | |
| timestamp: new Date(), | |
| }; | |
| setMessages(prev => [...prev, newMessage]); | |
| } | |
| }, [completion]); | |
| // Update current message during streaming | |
| useEffect(() => { | |
| if (isLoading && completion) { | |
| setCurrentMessage(completion); | |
| } else if (!isLoading) { | |
| setCurrentMessage(''); | |
| } | |
| }, [completion, isLoading]); | |
| const handleCopy = (content: string) => { | |
| navigator.clipboard.writeText(content); | |
| }; | |
| const handleFeedback = (messageId: string, feedback: 'positive' | 'negative') => { | |
| setMessages(prev => prev.map(msg => | |
| msg.id === messageId ? { ...msg, feedback } : msg | |
| )); | |
| }; | |
| const formatCode = (content: string) => { | |
| // Simple code block formatting | |
| const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; | |
| let formatted = content; | |
| let match; | |
| const parts: Array<{ type: 'text' | 'code'; content: string; language?: string }> = []; | |
| let lastIndex = 0; | |
| while ((match = codeBlockRegex.exec(content)) !== null) { | |
| if (match.index > lastIndex) { | |
| parts.push({ type: 'text', content: content.slice(lastIndex, match.index) }); | |
| } | |
| parts.push({ | |
| type: 'code', | |
| content: match[2].trim(), | |
| language: match[1] || 'javascript' | |
| }); | |
| lastIndex = match.index + match[0].length; | |
| } | |
| if (lastIndex < content.length) { | |
| parts.push({ type: 'text', content: content.slice(lastIndex) }); | |
| } | |
| return parts.length > 0 ? parts : [{ type: 'text', content }]; | |
| }; | |
| return ( | |
| <div className={cn("flex flex-col h-full", className)}> | |
| {/* Messages */} | |
| <div className="flex-1 overflow-y-auto space-y-4 mb-4 max-h-[400px] code-area"> | |
| {messages.length === 0 && !isLoading && ( | |
| <div className="flex items-center justify-center h-full text-muted-foreground"> | |
| <div className="text-center"> | |
| <MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-50" /> | |
| <p className="text-lg font-medium mb-2">Start a conversation</p> | |
| <p className="text-sm"> | |
| Ask me to help with your code, generate functions, debug issues, or discuss programming concepts. | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| {messages.map((message) => ( | |
| <Card key={message.id} className="relative"> | |
| <CardContent className="p-4"> | |
| <div className="flex items-start justify-between mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <Badge | |
| variant={message.role === 'assistant' ? 'default' : 'secondary'} | |
| className="text-xs" | |
| > | |
| {message.role === 'assistant' ? 'AI Assistant' : 'You'} | |
| </Badge> | |
| <span className="text-xs text-muted-foreground"> | |
| {message.timestamp.toLocaleTimeString()} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => handleCopy(message.content)} | |
| className="h-6 w-6 p-0" | |
| > | |
| <Copy className="w-3 h-3" /> | |
| </Button> | |
| {message.role === 'assistant' && ( | |
| <> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => handleFeedback(message.id, 'positive')} | |
| className={cn( | |
| "h-6 w-6 p-0", | |
| message.feedback === 'positive' && "text-green-600" | |
| )} | |
| > | |
| <ThumbsUp className="w-3 h-3" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => handleFeedback(message.id, 'negative')} | |
| className={cn( | |
| "h-6 w-6 p-0", | |
| message.feedback === 'negative' && "text-red-600" | |
| )} | |
| > | |
| <ThumbsDown className="w-3 h-3" /> | |
| </Button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| <div className="prose prose-sm max-w-none"> | |
| {formatCode(message.content).map((part, index) => ( | |
| <div key={index}> | |
| {part.type === 'code' ? ( | |
| <div className="relative"> | |
| <Badge variant="outline" className="text-xs mb-2"> | |
| {part.language} | |
| </Badge> | |
| <pre className="bg-muted p-3 rounded-md overflow-x-auto text-sm"> | |
| <code>{part.content}</code> | |
| </pre> | |
| </div> | |
| ) : ( | |
| <div className="whitespace-pre-wrap text-sm leading-relaxed"> | |
| {part.content} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| {/* Current streaming message */} | |
| {isLoading && currentMessage && ( | |
| <Card className="relative border-dashed"> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <Badge variant="default" className="text-xs"> | |
| <RefreshCw className="w-3 h-3 mr-1 animate-spin" /> | |
| AI Assistant | |
| </Badge> | |
| <span className="text-xs text-muted-foreground">Thinking...</span> | |
| </div> | |
| <div className="prose prose-sm max-w-none"> | |
| {formatCode(currentMessage).map((part, index) => ( | |
| <div key={index}> | |
| {part.type === 'code' ? ( | |
| <div className="relative"> | |
| <Badge variant="outline" className="text-xs mb-2"> | |
| {part.language} | |
| </Badge> | |
| <pre className="bg-muted p-3 rounded-md overflow-x-auto text-sm opacity-75"> | |
| <code>{part.content}</code> | |
| </pre> | |
| </div> | |
| ) : ( | |
| <div className="whitespace-pre-wrap text-sm leading-relaxed opacity-75"> | |
| {part.content} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |