/** * KGDebugger - Global debugging utility for KGSP * Provides console access to internal methods for testing and debugging */ import { KGCore } from './KGCore'; import { KGMidiRegion } from './region/KGMidiRegion'; import { convertRegionToABCNotation } from '../util/abcNotationUtil'; import { extractXMLFromString } from '../util/xmlUtil'; import { XMLToolExecutor } from '../agent/core/XMLToolExecutor'; import { AgentCore } from '../agent/core/AgentCore'; import { AttemptCompletionTool } from '../agent/tools/AttemptCompletionTool'; import type { TimeSignature } from '../types/projectTypes'; import { useProjectStore } from '../stores/projectStore'; /** * Global debugger singleton for console-based testing */ export class KGDebugger { // Private static instance for singleton pattern private static _instance: KGDebugger | null = null; // Private constructor to prevent direct instantiation private constructor() { console.log("šŸ”§ KGDebugger initialized - Available methods:", [ 'convertSelectedRegionToABCNotation(startFromBeat?)', 'testQuantizeDuration(durationBeats, timeSignature?)', 'debugSelectedItems()', 'createTestRegion()', 'testExtractXMLFromString(input)', 'testXMLToolExecution(input)', 'testAttemptCompletion(comment)', 'inputChatBox(content, interval?)' ]); } /** * Get the singleton instance of KGDebugger */ public static instance(): KGDebugger { if (!KGDebugger._instance) { KGDebugger._instance = new KGDebugger(); } return KGDebugger._instance; } /** * Convert the currently selected region to ABC notation * @param startFromBeat - Optional absolute beat position to start from (defaults to region start) */ public convertSelectedRegionToABCNotation(startFromBeat?: number): void { const core = KGCore.instance(); const selectedItems = core.getSelectedItems(); let midiRegion: KGMidiRegion | null = null; // First try to find MIDI region in selected items if (selectedItems && selectedItems.length > 0) { console.log(`šŸ“ Converting selected region to ABC notation...`); console.log(`šŸ“Š Selected items count: ${selectedItems.length}`); midiRegion = selectedItems.find(item => item.getCurrentType() === 'KGMidiRegion' ) as KGMidiRegion; } // If no MIDI region selected, try to use active region from piano roll if (!midiRegion) { console.log("šŸ“ No MIDI region selected, checking for active region..."); const storeState = useProjectStore.getState(); const activeRegionId = storeState.activeRegionId; const tracks = storeState.tracks; if (activeRegionId) { // Find the active region in tracks for (const track of tracks) { const regions = track.getRegions(); const region = regions.find(r => r.getId() === activeRegionId); if (region && region instanceof KGMidiRegion) { midiRegion = region; console.log(`āœ… Found active region: "${region.getName()}"`); break; } } } } if (!midiRegion) { console.error("āŒ No MIDI region found."); console.log("šŸ’” Try one of these:"); console.log(" • Select a region in the track grid"); console.log(" • Open a region in the piano roll editor"); return; } // Use provided startFromBeat or default to region start const effectiveStartBeat = startFromBeat ?? midiRegion.getStartFromBeat(); console.log(`šŸŽµ Converting region: "${midiRegion.getName()}"`); console.log(`šŸ“ Region starts at beat: ${midiRegion.getStartFromBeat()}`); console.log(`šŸ“ Conversion starts at beat: ${effectiveStartBeat}`); console.log(`šŸŽ¼ Notes in region: ${midiRegion.getNotes().length}`); try { const abcNotation = convertRegionToABCNotation(midiRegion, effectiveStartBeat); console.log("āœ… ABC Notation conversion successful!"); console.log("šŸ“„ Result:"); console.log("─".repeat(50)); console.log(abcNotation); console.log("─".repeat(50)); // Also copy to clipboard if possible if (navigator.clipboard) { navigator.clipboard.writeText(abcNotation).then(() => { console.log("šŸ“‹ ABC notation copied to clipboard!"); }).catch(() => { console.log("šŸ“‹ Could not copy to clipboard (requires HTTPS)"); }); } } catch (error) { console.error("āŒ Error converting to ABC notation:", error); } } /** * Test the quantization duration method with a specific duration * @param durationBeats - Duration in beats to test * @param timeSignature - Optional time signature (defaults to project time signature) */ public testQuantizeDuration(durationBeats: number, timeSignature?: TimeSignature): void { const core = KGCore.instance(); const project = core.getCurrentProject(); const effectiveTimeSignature = timeSignature ?? project.getTimeSignature(); console.log(`🧮 Testing quantization for ${durationBeats} beats...`); console.log(`ā±ļø Time signature: ${effectiveTimeSignature.numerator}/${effectiveTimeSignature.denominator}`); // Import quantization testing (we'll need to expose some internal methods) // For now, let's create a simple test const ticksPerBeat = 480 * (4 / effectiveTimeSignature.denominator); const durationTicks = Math.round(durationBeats * ticksPerBeat); console.log(`šŸŽµ Input: ${durationBeats} beats = ${durationTicks} ticks`); // Test different quantization values manually for demonstration const testValues = [ { name: '1/1', ticks: 1920 }, { name: '1/2', ticks: 960 }, { name: '1/3', ticks: 640 }, { name: '1/4', ticks: 480 }, { name: '1/6', ticks: 320 }, { name: '1/8', ticks: 240 }, { name: '1/12', ticks: 160 }, { name: '1/16', ticks: 120 } ]; console.log("šŸ“Š Quantization analysis:"); let bestMatch = { name: '1/4', error: Infinity, ticks: 480 }; testValues.forEach(val => { const remainder = durationTicks % val.ticks; const error = Math.min(remainder, val.ticks - remainder); const errorPercent = ((error / val.ticks) * 100).toFixed(1); if (error < bestMatch.error) { bestMatch = { name: val.name, error, ticks: val.ticks }; } console.log(` ${val.name}: ${error} ticks error (${errorPercent}%)`); }); const quantizedTicks = Math.round(durationTicks / bestMatch.ticks) * bestMatch.ticks; const quantizedBeats = quantizedTicks / ticksPerBeat; console.log(`āœ… Best match: ${bestMatch.name} grid`); console.log(`šŸŽÆ Quantized: ${quantizedBeats} beats = ${quantizedTicks} ticks`); console.log(`šŸ“ Difference: ${Math.abs(durationBeats - quantizedBeats).toFixed(4)} beats`); } /** * Debug currently selected items */ public debugSelectedItems(): void { const core = KGCore.instance(); const selectedItems = core.getSelectedItems(); console.log(`šŸ” Currently selected items: ${selectedItems.length}`); if (selectedItems.length === 0) { console.log("šŸ“ No items selected. Try selecting regions or notes first."); return; } selectedItems.forEach((item, index) => { const type = item.getCurrentType(); const id = item.getId(); console.log(` ${index + 1}. ${type} (ID: ${id})`); if (type === 'KGMidiRegion') { const region = item as KGMidiRegion; console.log(` šŸ“ Position: ${region.getStartFromBeat()} beats`); console.log(` šŸ“ Length: ${region.getLength()} beats`); console.log(` šŸŽµ Notes: ${region.getNotes().length}`); console.log(` šŸ“› Name: "${region.getName()}"`); } }); } /** * Create a test region with sample notes for testing (future implementation) */ public createTestRegion(): void { console.log("🚧 createTestRegion() - Not implemented yet"); console.log("šŸ’” This method would create a region with sample notes for testing"); console.log("šŸ’” For now, please create regions manually in the DAW interface"); } /** * Test the extractXMLFromString utility function with a given input * @param input - String that may contain XML blocks to extract */ public testExtractXMLFromString(input: string): void { console.log("šŸ” Testing extractXMLFromString utility..."); console.log("šŸ“ Input string:"); console.log("─".repeat(50)); console.log(input); console.log("─".repeat(50)); try { const xmlBlocks = extractXMLFromString(input); console.log(`āœ… Extraction successful! Found ${xmlBlocks.length} XML block(s):`); if (xmlBlocks.length === 0) { console.log("šŸ“­ No XML blocks found in the input string."); console.log("šŸ’” Try input with XML tags like: ..."); } else { xmlBlocks.forEach((block, index) => { console.log("\nšŸ“„ XML Block " + (index + 1) + ":"); console.log("─".repeat(30)); console.log(block); console.log("─".repeat(30)); }); // Copy all blocks to clipboard if possible if (navigator.clipboard && xmlBlocks.length > 0) { const allBlocks = xmlBlocks.join('\n\n'); navigator.clipboard.writeText(allBlocks).then(() => { console.log("šŸ“‹ XML blocks copied to clipboard!"); }).catch(() => { console.log("šŸ“‹ Could not copy to clipboard (requires HTTPS)"); }); } } } catch (error) { console.error("āŒ Error extracting XML:", error); } } /** * Test the complete XML tool execution pipeline * @param input - String containing XML tool invocations to execute */ public async testXMLToolExecution(input: string): Promise { console.log('------------ ASSISTANT ------------'); console.log(input); console.log('-----------------------------------'); try { // Extract XML blocks first to get tool names (same logic as ChatBox) const xmlBlocks = extractXMLFromString(input); if (xmlBlocks.length === 0) { console.log('------------ USER ------------'); console.log('No XML tool invocations found in the input string.'); console.log('------------------------------'); return; } const executor = XMLToolExecutor.instance(); let accumulatedResults = ''; // Execute tools sequentially and format like ChatBox for (let i = 0; i < xmlBlocks.length; i++) { // Determine tool name from XML block (same as ChatBox lines 148-149) const toolNameMatch = xmlBlocks[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(xmlBlocks[i]); const result = results[0]; // Single block should give single result if (result) { // Format exactly like ChatBox lines 161-162 const formattedResult = `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`; accumulatedResults += formattedResult; } } catch (error) { // Handle individual tool error (same format) const formattedResult = `tool: ${toolName}\nsuccess: false\nresult:\nTool execution failed: ${error}\n------------\n`; accumulatedResults += formattedResult; } } // Log accumulated results as USER (what gets sent back to LLM) console.log('------------ USER ------------'); console.log(accumulatedResults); console.log('------------------------------'); // Copy results to clipboard if possible if (navigator.clipboard) { navigator.clipboard.writeText(accumulatedResults).then(() => { console.log("Tool execution results copied to clipboard!"); }).catch(() => { console.log("Could not copy to clipboard (requires HTTPS)"); }); } } catch (error) { console.log('------------ USER ------------'); console.log(`Error testing XML tool execution: ${error}`); console.log('------------------------------'); } } /** * Test the AttemptCompletionTool with agent state integration * @param comment - Completion comment to test with */ public async testAttemptCompletion(comment: string): Promise { console.log("šŸŽÆ Testing AttemptCompletionTool..."); console.log(`šŸ“ Comment: "${comment}"`); try { // Get current agent state before test const agentCore = AgentCore.instance(); const agentState = agentCore.getAgentState(); const initialTaskState = agentState.getIsWorkingOnTask(); console.log(`šŸ“Š Initial agent state:`); console.log(` • isWorkingOnTask: ${initialTaskState}`); // Set to working state to test the completion properly if (!initialTaskState) { console.log("šŸ”„ Setting isWorkingOnTask to true for testing..."); agentState.setIsWorkingOnTask(true); } // Create and execute the tool const completionTool = new AttemptCompletionTool(); const result = await completionTool.execute({ comment }); console.log(`āœ… Tool execution result:`); console.log(` • Success: ${result.success}`); console.log(` • Result: ${result.result}`); // Check final agent state const finalTaskState = agentState.getIsWorkingOnTask(); console.log(`šŸ“Š Final agent state:`); console.log(` • isWorkingOnTask: ${finalTaskState}`); // Verify state change if (result.success && finalTaskState === false) { console.log("šŸŽ‰ Success! Agent state correctly updated to not working on task."); } else if (!result.success) { console.log("āš ļø Tool execution failed - state may not have changed."); } else { console.log("āš ļø Warning: State did not change as expected."); } // Copy result to clipboard if possible if (navigator.clipboard) { const clipboardContent = JSON.stringify(result, null, 2); navigator.clipboard.writeText(clipboardContent).then(() => { console.log("šŸ“‹ Test results copied to clipboard!"); }).catch(() => { console.log("šŸ“‹ Could not copy to clipboard (requires HTTPS)"); }); } } catch (error) { console.error("āŒ Error testing AttemptCompletionTool:", error); } } /** * Show help information */ public help(): void { console.log("šŸ”§ KGDebugger Help"); console.log("Available methods:"); console.log(" convertSelectedRegionToABCNotation(startFromBeat?) - Convert selected region to ABC"); console.log(" testQuantizeDuration(beats, timeSignature?) - Test quantization logic"); console.log(" debugSelectedItems() - Show info about selected items"); console.log(" createTestRegion() - Create test region (not implemented)"); console.log(" testExtractXMLFromString(input) - Test XML extraction from string"); console.log(" testXMLToolExecution(input) - Test complete XML tool execution pipeline"); console.log(" testAttemptCompletion(comment) - Test AttemptCompletionTool with agent state"); console.log(" inputChatBox(content, interval?) - Type into ChatBox textarea and submit with Enter"); console.log(" help() - Show this help"); console.log(""); console.log("šŸ’” Usage tips:"); console.log(" - Select regions in the DAW first, then run debug methods"); console.log(" - Results are logged to console and copied to clipboard when possible"); console.log(" - Use browser developer tools for best experience"); console.log(" - For XML testing, try: testExtractXMLFromString('I will ... create notes');"); console.log(" - For full tool execution, try: await testXMLToolExecution('Create notes: C401');"); console.log(" - For completion testing, try: await testAttemptCompletion('Successfully created a C major chord');"); } /** * Type content into ChatBox textarea character-by-character and submit with Enter. * Honors auto-resize (by dispatching 'input' events) and ChatBox's Enter-to-send behavior. * @param content - The text to type into the chat input * @param interval - Delay in ms between characters (default 30ms) */ public async inputChatBox(content: string, interval: number = 30): Promise { try { const textarea = document.querySelector('textarea.chatbox-input') as HTMLTextAreaElement | null; if (!textarea) { console.error('āŒ ChatBox textarea not found. Ensure ChatBox is mounted and visible.'); return; } // Focus to trigger ChatBox focus handlers and ensure caret/attribute setup textarea.focus(); // Use native value setter to keep React's value tracker in sync const valueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set; const setValue = (val: string) => { if (valueSetter) { valueSetter.call(textarea, val); } else { textarea.value = val; } }; // Helper to dispatch an InputEvent so React's onChange fires const dispatchInput = (data?: string) => { const ev = typeof InputEvent !== 'undefined' ? new InputEvent('input', { bubbles: true, data, inputType: 'insertText' }) : new Event('input', { bubbles: true }); textarea.dispatchEvent(ev); }; // Start from empty content to simulate a fresh user input setValue(''); dispatchInput(''); // Helper: sleep const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let typed = ''; for (let i = 0; i < content.length; i++) { typed += content[i]; // Update the controlled textarea and dispatch input so React onChange fires setValue(typed); dispatchInput(content[i]); // Place caret at end for realism try { textarea.selectionStart = textarea.selectionEnd = typed.length; } catch { // noop } if (interval > 0) { await sleep(interval); } } // Give React a brief moment to commit the last setState and run auto-resize effect await sleep(Math.max(30, interval)); // Simulate pressing Enter to submit (ChatBox listens on keydown) const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true } as KeyboardEventInit & { keyCode: number; which: number }); textarea.dispatchEvent(enterEvent); // Optional: follow-up keyup to mirror real typing (some UIs inspect it) const keyupEvent = new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, } as KeyboardEventInit & { keyCode: number; which: number }); textarea.dispatchEvent(keyupEvent); } catch (error) { console.error('āŒ Error in inputChatBox:', error); } } }