diff --git a/src/react/components/ChatMessage.tsx b/src/react/components/ChatMessage.tsx new file mode 100644 index 0000000..d1e5f93 --- /dev/null +++ b/src/react/components/ChatMessage.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import {Box, Text, Loader, Badge, Stack} from "@mantine/core"; +import {IconCheck, IconX, IconRobot, IconUser} from "@tabler/icons-react"; +import {ChatMessage as ChatMessageType} from "../types/chatTypes"; + +interface ChatMessageProps { + message: ChatMessageType; +} + +export default function ChatMessage({message}: ChatMessageProps) { + const isUser = message.role === 'user'; + + if (message.isLoading) { + return ( + + + + + ); + } + + return ( + + + + {isUser ? : } + + + + {message.content} + + + + + {message.toolResults && message.toolResults.length > 0 && ( + + {message.toolResults.map((result, index) => ( + : } + color={result.success ? 'green' : 'red'} + variant="light" + size="sm" + > + {result.message.length > 50 + ? result.message.substring(0, 50) + '...' + : result.message} + + ))} + + )} + + ); +} diff --git a/src/react/components/ChatPanel.tsx b/src/react/components/ChatPanel.tsx new file mode 100644 index 0000000..bc2baf1 --- /dev/null +++ b/src/react/components/ChatPanel.tsx @@ -0,0 +1,178 @@ +import React, {useState, useRef, useEffect} from "react"; +import { + Paper, + ScrollArea, + Textarea, + ActionIcon, + Box, + Text, + Group, + CloseButton, + Loader, + Alert +} from "@mantine/core"; +import {IconSend, IconRobot, IconAlertCircle} from "@tabler/icons-react"; +import ChatMessage from "./ChatMessage"; +import {ChatMessage as ChatMessageType, ToolResult} from "../types/chatTypes"; +import {sendMessage, createUserMessage, createAssistantMessage, createLoadingMessage} from "../services/diagramAI"; +import log from "loglevel"; + +const logger = log.getLogger('ChatPanel'); + +interface ChatPanelProps { + width?: number; + onClose?: () => void; +} + +export default function ChatPanel({width = 400, onClose}: ChatPanelProps) { + const [messages, setMessages] = useState([ + createAssistantMessage("Hello! I can help you create and modify 3D diagrams. Try saying things like:\n\n• \"Add a blue box labeled 'Server'\"\n• \"Create a red sphere called 'Database'\"\n• \"Connect Server to Database\"\n• \"What's in my diagram?\"\n\nHow can I help you today?") + ]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const scrollAreaRef = useRef(null); + const textareaRef = useRef(null); + + useEffect(() => { + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTo({ + top: scrollAreaRef.current.scrollHeight, + behavior: 'smooth' + }); + } + }, [messages]); + + const handleSend = async () => { + const trimmedInput = inputValue.trim(); + if (!trimmedInput || isLoading) return; + + setError(null); + const userMessage = createUserMessage(trimmedInput); + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setIsLoading(true); + + const loadingMessage = createLoadingMessage(); + setMessages(prev => [...prev, loadingMessage]); + + try { + const allToolResults: ToolResult[] = []; + + const {response, toolResults} = await sendMessage( + trimmedInput, + messages, + (result) => { + allToolResults.push(result); + logger.debug('Tool executed:', result); + } + ); + + setMessages(prev => { + const filtered = prev.filter(m => m.id !== loadingMessage.id); + return [...filtered, createAssistantMessage(response, toolResults)]; + }); + } catch (err) { + logger.error('Chat error:', err); + setMessages(prev => prev.filter(m => m.id !== loadingMessage.id)); + setError(err instanceof Error ? err.message : 'Failed to send message'); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( + + {/* Header */} + + + + + Diagram Assistant + + {onClose && } + + + + {/* Messages */} + + {messages.map((message) => ( + + ))} + + + {/* Error Alert */} + {error && ( + } + withCloseButton + onClose={() => setError(null)} + style={{margin: '0 1rem'}} + > + {error} + + )} + + {/* Input */} + + +