/** * StudioChat - Context-aware AI chat for Studio * * Uses the existing useChat hook to communicate with Claude via WebSocket. * Injects model files and context documents into the conversation. */ import React, { useRef, useEffect, useState, useMemo } from 'react'; import { Send, Loader2, Sparkles, FileText, Wifi, WifiOff, Bot, User, File, AlertCircle } from 'lucide-react'; import { useChat } from '../../hooks/useChat'; import { useSpecStore, useSpec } from '../../hooks/useSpecStore'; import { MarkdownRenderer } from '../MarkdownRenderer'; import { ToolCallCard } from '../chat/ToolCallCard'; interface StudioChatProps { draftId: string; contextFiles: string[]; contextContent: string; modelFiles: string[]; onSpecUpdated: () => void; } export const StudioChat: React.FC = ({ draftId, contextFiles, contextContent, modelFiles, onSpecUpdated, }) => { const messagesEndRef = useRef(null); const inputRef = useRef(null); const [input, setInput] = useState(''); const [hasInjectedContext, setHasInjectedContext] = useState(false); // Get spec store for canvas updates const spec = useSpec(); const { reloadSpec, setSpecFromWebSocket } = useSpecStore(); // Build canvas state with full context for Claude const canvasState = useMemo(() => ({ nodes: [], edges: [], studyName: draftId, studyPath: `_inbox/${draftId}`, // Include file info for Claude context modelFiles, contextFiles, contextContent: contextContent.substring(0, 50000), // Limit context size }), [draftId, modelFiles, contextFiles, contextContent]); // Use the chat hook with WebSocket // Power mode gives Claude write permissions to modify the spec const { messages, isThinking, error, isConnected, sendMessage, updateCanvasState, } = useChat({ studyId: draftId, mode: 'power', // Power mode = --dangerously-skip-permissions = can write files useWebSocket: true, canvasState, onError: (err) => console.error('[StudioChat] Error:', err), onSpecUpdated: (newSpec) => { // Claude modified the spec - update the store directly console.log('[StudioChat] Spec updated by Claude'); setSpecFromWebSocket(newSpec, draftId); onSpecUpdated(); }, onCanvasModification: (modification) => { // Claude wants to modify canvas - reload the spec console.log('[StudioChat] Canvas modification:', modification); reloadSpec(); onSpecUpdated(); }, }); // Update canvas state when context changes useEffect(() => { updateCanvasState(canvasState); }, [canvasState, updateCanvasState]); // Scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Auto-focus input useEffect(() => { inputRef.current?.focus(); }, []); // Build context summary for display const contextSummary = useMemo(() => { const parts: string[] = []; if (modelFiles.length > 0) { parts.push(`${modelFiles.length} model file${modelFiles.length > 1 ? 's' : ''}`); } if (contextFiles.length > 0) { parts.push(`${contextFiles.length} context doc${contextFiles.length > 1 ? 's' : ''}`); } if (contextContent) { parts.push(`${contextContent.length.toLocaleString()} chars context`); } return parts.join(', '); }, [modelFiles, contextFiles, contextContent]); const handleSend = () => { if (!input.trim() || isThinking) return; let messageToSend = input.trim(); // On first message, inject full context so Claude has everything it needs if (!hasInjectedContext && (modelFiles.length > 0 || contextContent)) { const contextParts: string[] = []; // Add model files info if (modelFiles.length > 0) { contextParts.push(`**Model Files Uploaded:**\n${modelFiles.map(f => `- ${f}`).join('\n')}`); } // Add context document content (full text) if (contextContent) { contextParts.push(`**Context Documents Content:**\n\`\`\`\n${contextContent.substring(0, 30000)}\n\`\`\``); } // Add current spec state if (spec) { const dvCount = spec.design_variables?.length || 0; const objCount = spec.objectives?.length || 0; const extCount = spec.extractors?.length || 0; if (dvCount > 0 || objCount > 0 || extCount > 0) { contextParts.push(`**Current Configuration:** ${dvCount} design variables, ${objCount} objectives, ${extCount} extractors`); } } if (contextParts.length > 0) { messageToSend = `${contextParts.join('\n\n')}\n\n---\n\n**User Request:** ${messageToSend}`; } setHasInjectedContext(true); } sendMessage(messageToSend); setInput(''); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Welcome message for empty state const showWelcome = messages.length === 0; // Check if we have any context const hasContext = modelFiles.length > 0 || contextContent.length > 0; return (
{/* Header */}
Studio Assistant
{isConnected ? : } {isConnected ? 'Connected' : 'Disconnected'}
{/* Context indicator */} {contextSummary && (
{contextSummary}
{hasContext && !hasInjectedContext && ( Will be sent with first message )} {hasInjectedContext && ( Context sent )}
)}
{/* Messages */}
{/* Welcome message with context awareness */} {showWelcome && (
0 ? `**Model Files:** ${modelFiles.join(', ')}` : ''} ${contextContent ? `\n**Context Document:** ${contextContent.substring(0, 200)}...` : ''} Tell me what you want to optimize and I'll help you configure the study!` : `Welcome to Atomizer Studio! I'm here to help you configure your optimization study. **What I can do:** - Read your uploaded context documents - Help set up design variables, objectives, and constraints - Create extractors for physics outputs - Suggest optimization strategies Upload your model files and any requirements documents, then tell me what you want to optimize!`} />
)} {/* File context display (only if we have files but no messages yet) */} {showWelcome && modelFiles.length > 0 && (

Loaded Files:

{modelFiles.map((file, idx) => ( {file} ))} {contextFiles.map((file, idx) => ( {file} ))}
)} {/* Chat messages */} {messages.map((msg) => { const isAssistant = msg.role === 'assistant'; const isSystem = msg.role === 'system'; // System messages if (isSystem) { return (
{msg.content}
); } return (
{/* Avatar */}
{isAssistant ? : }
{/* Message content */}
{isAssistant ? ( <> {msg.content && } {msg.isStreaming && !msg.content && ( Thinking... )} {/* Tool calls */} {msg.toolCalls && msg.toolCalls.length > 0 && (
{msg.toolCalls.map((tool, idx) => ( ))}
)} ) : ( {msg.content} )}
); })} {/* Thinking indicator */} {isThinking && messages.length > 0 && !messages[messages.length - 1]?.isStreaming && (
Thinking...
)} {/* Error display */} {error && (
{error}
)}
{/* Input */}