import React, {useEffect, useRef, useState} from "react"; import {ActionIcon, Alert, Box, CloseButton, Group, Paper, ScrollArea, Text, Textarea} from "@mantine/core"; import {IconAlertCircle, IconRobot, IconSend} from "@tabler/icons-react"; import ChatMessage from "./ChatMessage"; import {ChatMessage as ChatMessageType, SessionMessage, ToolResult} from "../types/chatTypes"; import { createAssistantMessage, createLoadingMessage, createOrResumeSession, createUserMessage, sendMessage, syncEntitiesToSession } from "../services/diagramAI"; import {getEntitiesForSync} from "../services/entityBridge"; import {getPath} from "../../util/functions/getPath"; import log from "loglevel"; import {v4 as uuidv4} from 'uuid'; const logger = log.getLogger('ChatPanel'); interface ChatPanelProps { width?: number; onClose?: () => void; } const WELCOME_MESSAGE = "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?"; /** * Convert session messages to ChatMessage format */ function sessionMessageToChatMessage(msg: SessionMessage): ChatMessageType | null { // Skip messages with non-string content (e.g., tool result objects) if (typeof msg.content !== 'string') { return null; } return { id: uuidv4(), role: msg.role, content: msg.content, timestamp: new Date(msg.timestamp), toolResults: msg.toolResults }; } export default function ChatPanel({width = 400, onClose}: ChatPanelProps) { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isInitializing, setIsInitializing] = useState(true); const [error, setError] = useState(null); const scrollAreaRef = useRef(null); const textareaRef = useRef(null); // Initialize or resume session on mount useEffect(() => { const initSession = async () => { const diagramId = getPath() || 'default'; logger.info('Initializing session for diagram:', diagramId); try { // Create or resume session const {session, isNew} = await createOrResumeSession(diagramId); logger.info(`Session ${isNew ? 'created' : 'resumed'}:`, session.id); // Sync current entities to server const entities = await getEntitiesForSync(); if (entities.length > 0) { await syncEntitiesToSession(entities); logger.info('Synced', entities.length, 'entities to session'); } // Restore conversation history or show welcome message if (!isNew && session.conversationHistory && session.conversationHistory.length > 0) { const restoredMessages = session.conversationHistory .map(sessionMessageToChatMessage) .filter((msg): msg is ChatMessageType => msg !== null); if (restoredMessages.length > 0) { setMessages(restoredMessages); logger.info('Restored', restoredMessages.length, 'messages from session'); } else { setMessages([createAssistantMessage(WELCOME_MESSAGE)]); } } else { setMessages([createAssistantMessage(WELCOME_MESSAGE)]); } } catch (err) { logger.error('Failed to initialize session:', err); // Fall back to welcome message setMessages([createAssistantMessage(WELCOME_MESSAGE)]); } finally { setIsInitializing(false); } }; initSession(); }, []); // Auto-scroll when messages change 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 { // Sync current entities before sending message so Claude has latest state const entities = await getEntitiesForSync(); if (entities.length > 0) { await syncEntitiesToSession(entities); logger.debug('Synced', entities.length, 'entities before message'); } 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 */}