import React, { useState, useRef, useEffect, memo, useCallback } from 'react'; import { FaPlus, FaBan } from 'react-icons/fa'; import { UserMessage, AssistantMessage } from './chat'; import { AgentCore } from '../agent/core/AgentCore'; import { OpenAIProvider } from '../agent/llm/OpenAIProvider'; import { ClaudeProvider } from '../agent/llm/ClaudeProvider'; import { GeminiProvider } from '../agent/llm/GeminiProvider'; import { LLMProvider } from '../agent/llm/LLMProvider'; import { ConfigManager } from '../core/config/ConfigManager'; import { useProjectStore } from '../stores/projectStore'; import { XMLToolExecutor } from '../agent/core/XMLToolExecutor'; import { extractXMLFromString } from '../util/xmlUtil'; import { SystemPrompts } from '../agent/core/SystemPrompts'; import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil'; import { processUserMessage } from '../util/messageFilter/UserMessageFilter'; // Module-level guard to avoid duplicate welcome in React StrictMode dev remounts let hasShownWelcomeOnceInRuntime = false; interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; isStreaming?: boolean; tokenCount?: number; } /** * Create the appropriate LLM provider based on configuration */ const createLLMProvider = (): LLMProvider => { const configManager = ConfigManager.instance(); const providerType = configManager.get('general.llm_provider') as string; switch (providerType) { case 'claude': return new ClaudeProvider(); case 'gemini': return new GeminiProvider(); case 'openai_compatible': case 'openai': default: return new OpenAIProvider(); } }; interface ChatBoxProps { isVisible: boolean; } const ChatBox: React.FC = ({ isVisible }) => { const [inputValue, setInputValue] = useState(''); const textareaRef = useRef(null); // Initialize with empty messages const [messages, setMessages] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [abortController, setAbortController] = useState(null); const [lastUserMessage, setLastUserMessage] = useState(''); // Tool execution state const [isExecutingTools, setIsExecutingTools] = useState(false); const [, setToolResults] = useState(''); // placeholder for future display/use const [, setCurrentToolIndex] = useState(0); // placeholder for future display/use // Track if this is the first message (for system prompt logging) const [isFirstMessage, setIsFirstMessage] = useState(true); const generateMessageId = (): string => { return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; }; const clearChatUI = useCallback(async () => { // Clear UI state setMessages([]); // Reset first message flag so system prompt will be logged again setIsFirstMessage(true); // Auto-show welcome message after clearing (like on app startup) try { const result = await processUserMessage('/welcome'); if (result.pseudoAssistantResponse) { const pseudoId = generateMessageId(); setMessages(prev => [...prev, { id: pseudoId, role: 'assistant', content: result.pseudoAssistantResponse!, }]); } } catch { // ignore errors, just don't show welcome if it fails } }, []); // Initialize AgentCore with configured provider and register clear UI callback useEffect(() => { const initializeProvider = async () => { const configManager = ConfigManager.instance(); // Ensure ConfigManager is initialized if (!configManager.getIsInitialized()) { await configManager.initialize(); } const provider = createLLMProvider(); const agentCore = AgentCore.instance(); agentCore.setLLMProvider(provider); console.log(`Switched to ${provider.name} provider`); }; // Register the UI clear callback for external components to use registerClearChatUICallback(clearChatUI); initializeProvider(); // Auto-trigger welcome on first launch (guard against React StrictMode double-invoke only) (async () => { try { if (hasShownWelcomeOnceInRuntime) return; hasShownWelcomeOnceInRuntime = true; const result = await processUserMessage('/welcome'); if (result.pseudoAssistantResponse) { const pseudoId = generateMessageId(); setMessages(prev => [...prev, { id: pseudoId, role: 'assistant', content: result.pseudoAssistantResponse!, }]); } } catch { // ignore } })(); }, [clearChatUI]); const handleAbort = () => { if (abortController) { abortController.abort(); setAbortController(null); // Use AgentCore to clean up the data model and get the user message content const agentCore = AgentCore.instance(); const userMessageContent = agentCore.abortCurrentRequest(); // Remove the last user message and assistant message from UI setMessages(prev => prev.slice(0, -2)); // Restore the user's input (use the content from AgentCore if available) setInputValue(userMessageContent || lastUserMessage); setIsProcessing(false); } }; const handleClearCommand = () => { const { setStatus } = useProjectStore.getState(); clearChatHistoryAndUI(setStatus); }; const addToolResultMessage = (toolName: string, success: boolean, result: string) => { const toolMsgId = generateMessageId(); const friendlyDisplay = `${success ? '✅' : '❌'} __**${toolName}**__ \n\n └── ${result}`; setMessages(prev => [...prev, { id: toolMsgId, role: 'user', content: friendlyDisplay }]); }; const executeToolsFromResponse = async (response: string): Promise => { try { // Check if response contains XML tool invocations const xmlBlocks = extractXMLFromString(response); // Consider only actionable tools (exclude think/thinking) const actionableBlocks = xmlBlocks.filter((block) => { const match = block.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/); const name = match ? match[1].toLowerCase() : ''; return name !== 'think' && name !== 'thinking'; }); if (actionableBlocks.length === 0) { // No actionable tools to execute, stop the loop return false; } // Start tool execution phase setIsExecutingTools(true); setToolResults(''); setCurrentToolIndex(0); const { setStatus } = useProjectStore.getState(); setStatus(`Executing ${actionableBlocks.length} tool(s)...`); const executor = XMLToolExecutor.instance(); let accumulatedResults = ''; // Execute tools sequentially with real-time updates for (let i = 0; i < actionableBlocks.length; i++) { setCurrentToolIndex(i + 1); setStatus(`Executing tool ${i + 1} of ${actionableBlocks.length}...`); // Determine tool name from XML block const toolNameMatch = actionableBlocks[i].match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/); const toolName = toolNameMatch ? toolNameMatch[1] : 'unknown_tool'; try { // Execute single XML block const results = await executor.executeXMLTools(actionableBlocks[i]); const result = results[0]; // Single block should give single result if (result) { // Add friendly display message addToolResultMessage(toolName, result.success, result.result); // Accumulate formatted result for LLM (skip thinking tools) if (toolName !== 'thinking' && toolName !== 'think') { const formattedResult = `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`; accumulatedResults += formattedResult; } } } catch (error) { // Handle individual tool error addToolResultMessage(toolName, false, `Tool execution failed: ${error}`); // Accumulate error result for LLM (skip thinking tools) if (toolName !== 'thinking' && toolName !== 'think') { const formattedResult = `tool: ${toolName}\nsuccess: false\nresult:\nTool execution failed: ${error}\n------------\n`; accumulatedResults += formattedResult; } } } // Store accumulated results setToolResults(accumulatedResults); setIsExecutingTools(false); // Check if agent is still working on task before sending results to LLM const agentCore = AgentCore.instance(); const isStillWorkingOnTask = agentCore.getAgentState().getIsWorkingOnTask(); if (isStillWorkingOnTask) { // Send tool results back to LLM setStatus('Processing tool results...'); await sendToolResultsToLLM(accumulatedResults); } else { // Agent is no longer working on task, ignore results and return control to user setStatus('Tool execution completed'); } return true; // Tools were found and executed } catch (error) { console.error('Error executing tools:', error); setIsExecutingTools(false); const { setStatus } = useProjectStore.getState(); setStatus(`Tool execution failed: ${error}`); return false; // Tool execution failed } }; const sendToolResultsToLLM = async (toolResultsString: string): Promise => { // Send tool results as hidden user input to LLM setIsProcessing(true); // Create abort controller for this request const controller = new AbortController(); setAbortController(controller); // Add streaming assistant message for the response const assistantMsgId = generateMessageId(); setMessages(prev => [...prev, { id: assistantMsgId, role: 'assistant', content: 'processing... 0 tokens received. click here to abort.', isStreaming: true, tokenCount: 0 }]); try { const agentCore = AgentCore.instance(); let assistantResponse = ''; let tokenCount = 0; // Log the tool results being sent to LLM console.log('------------ USER ------------'); console.log(toolResultsString); console.log('------------------------------'); for await (const chunk of agentCore.processUserInput(toolResultsString)) { // Check if request was aborted if (controller.signal.aborted) { return; } if (chunk.type === 'text') { assistantResponse += chunk.content; tokenCount++; // Update streaming message with token count and abort link setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: `processing... ${tokenCount} tokens received. click here to abort.`, tokenCount } : msg )); } else if (chunk.type === 'done') { // Replace with final response setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined } : msg )); // Log the complete assistant response console.log('------------ ASSISTANT ------------'); console.log(assistantResponse); console.log('-----------------------------------'); // Check if the new response contains more tools const hasMoreTools = await executeToolsFromResponse(assistantResponse); // If no more tools were found, set working flag to false if (!hasMoreTools) { const agentCore = AgentCore.instance(); agentCore.getAgentState().setIsWorkingOnTask(false); } break; } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // Request was aborted, don't show error return; } console.error('Error processing tool results:', error); // Update with error message setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: 'Error: Failed to process tool results', isStreaming: false, tokenCount: undefined } : msg )); } finally { setAbortController(null); setIsProcessing(false); } }; const handleSend = async () => { if (inputValue.trim() && !isProcessing) { const userMessage = inputValue.trim(); setLastUserMessage(userMessage); setInputValue(''); // Run message through the filter system const filterResult = await processUserMessage(userMessage); // Conditionally show the user message bubble if (filterResult.displayUserMessage) { const userMsgId = generateMessageId(); setMessages(prev => [...prev, { id: userMsgId, role: 'user', content: userMessage }]); } // If we have a pseudo assistant response, show it immediately if (filterResult.pseudoAssistantResponse) { const pseudoId = generateMessageId(); setMessages(prev => [...prev, { id: pseudoId, role: 'assistant', content: filterResult.pseudoAssistantResponse!, }]); } // If we shouldn't send anything to LLM, stop here if (!filterResult.sendToLLM || !filterResult.finalMessageForLLM) { return; } setIsProcessing(true); // Create abort controller for this request const controller = new AbortController(); setAbortController(controller); // Add streaming assistant message const assistantMsgId = generateMessageId(); setMessages(prev => [...prev, { id: assistantMsgId, role: 'assistant', content: 'processing... 0 tokens received. click here to abort.', isStreaming: true, tokenCount: 0 }]); try { const agentCore = AgentCore.instance(); let assistantResponse = ''; let tokenCount = 0; // Set working on task flag when user sends a message agentCore.getAgentState().setIsWorkingOnTask(true); // Log system prompt only for first message or first message after clear if (isFirstMessage) { try { const systemPrompt = await SystemPrompts.getSystemPromptWithContext(); console.log('------------ SYSTEM ------------'); console.log(systemPrompt); console.log('--------------------------------'); } catch (error) { console.error('Failed to log system prompt:', error); } // Mark that we've logged the system prompt for this conversation setIsFirstMessage(false); } // Log the final user message being sent to LLM console.log('------------ USER ------------'); console.log(filterResult.finalMessageForLLM); console.log('------------------------------'); for await (const chunk of agentCore.processUserInput(filterResult.finalMessageForLLM)) { // Check if request was aborted if (controller.signal.aborted) { return; } if (chunk.type === 'text') { assistantResponse += chunk.content; tokenCount++; // Update streaming message with token count and abort link setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: `processing... ${tokenCount} tokens received. click here to abort.`, tokenCount } : msg )); } else if (chunk.type === 'done') { // Replace with final response setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined } : msg )); // Log the complete assistant response console.log('------------ ASSISTANT ------------'); console.log(assistantResponse); console.log('-----------------------------------'); // Check if response contains tools to execute const hasTools = await executeToolsFromResponse(assistantResponse); // If no tools were found, set working flag to false and return control to user if (!hasTools) { agentCore.getAgentState().setIsWorkingOnTask(false); } break; } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // Request was aborted, don't show error return; } console.error('Error processing message:', error); // Update with error message setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: 'Error: Failed to process message', isStreaming: false, tokenCount: undefined } : msg )); } finally { setAbortController(null); setIsProcessing(false); } } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } // Allow Shift+Enter for new lines (default textarea behavior) }; const handleInputFocus = () => { // Set a data attribute on the textarea to help global keyboard handler identify it if (textareaRef.current) { textareaRef.current.setAttribute('data-chatbox-input', 'true'); } }; const handleInputBlur = () => { // Remove the data attribute when losing focus if (textareaRef.current) { textareaRef.current.removeAttribute('data-chatbox-input'); } }; // Auto-resize textarea based on content useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, [inputValue]); // Auto-scroll to bottom when new messages arrive const messagesEndRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Auto-focus input when it becomes visible (when processing and tool execution complete) useEffect(() => { if (!isProcessing && !isExecutingTools && textareaRef.current) { // Use a small delay to ensure the DOM has updated setTimeout(() => { textareaRef.current?.focus(); // Also scroll to bottom when input becomes visible after tool execution // This ensures proper scroll position after layout changes from showing input box messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 0); } }, [isProcessing, isExecutingTools]); return (

K.G.Studio Musician Assistant

{isProcessing && ( )}
{messages.map((message) => ( message.role === 'user' ? ( ) : ( ) ))}
{!isProcessing && !isExecutingTools && (