Closes #875 - Added new ReasoningTrace component for real-time reasoning visualization - Shows agent's reasoning steps during complex task execution - Supports step types: THINK, DECIDE, RECALL, PLAN, EXECUTE, VERIFY, DOUBT, MEMORY - Includes confidence visualization, task tracking, and export functionality - Integrated into existing GOFAI HUD system
451 lines
14 KiB
JavaScript
451 lines
14 KiB
JavaScript
// ═══════════════════════════════════════════════════
|
|
// REASONING TRACE HUD COMPONENT
|
|
// ═══════════════════════════════════════════════════
|
|
//
|
|
// Displays a real-time trace of the agent's reasoning
|
|
// steps during complex task execution. Shows the chain
|
|
// of thought, decision points, and confidence levels.
|
|
//
|
|
// Usage:
|
|
// ReasoningTrace.init();
|
|
// ReasoningTrace.addStep(step);
|
|
// ReasoningTrace.clear();
|
|
// ReasoningTrace.toggle();
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
const ReasoningTrace = (() => {
|
|
// ── State ─────────────────────────────────────────
|
|
let _container = null;
|
|
let _content = null;
|
|
let _header = null;
|
|
let _steps = [];
|
|
let _maxSteps = 20;
|
|
let _isVisible = true;
|
|
let _currentTask = null;
|
|
let _stepCounter = 0;
|
|
|
|
// ── Config ────────────────────────────────────────
|
|
const STEP_TYPES = {
|
|
THINK: { icon: '💭', color: '#4af0c0', label: 'THINK' },
|
|
DECIDE: { icon: '⚖️', color: '#ffd700', label: 'DECIDE' },
|
|
RECALL: { icon: '🔍', color: '#7b5cff', label: 'RECALL' },
|
|
PLAN: { icon: '📋', color: '#ff8c42', label: 'PLAN' },
|
|
EXECUTE: { icon: '⚡', color: '#ff4466', label: 'EXECUTE' },
|
|
VERIFY: { icon: '✅', color: '#4af0c0', label: 'VERIFY' },
|
|
DOUBT: { icon: '❓', color: '#ff8c42', label: 'DOUBT' },
|
|
MEMORY: { icon: '💾', color: '#7b5cff', label: 'MEMORY' }
|
|
};
|
|
|
|
// ── Helpers ───────────────────────────────────────
|
|
|
|
function _escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function _formatTimestamp(timestamp) {
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
}
|
|
|
|
function _getConfidenceBar(confidence) {
|
|
if (confidence === undefined || confidence === null) return '';
|
|
const percent = Math.max(0, Math.min(100, Math.round(confidence * 100)));
|
|
const bars = Math.round(percent / 10);
|
|
const filled = '█'.repeat(bars);
|
|
const empty = '░'.repeat(10 - bars);
|
|
return `<span class="confidence-bar" title="${percent}% confidence">${filled}${empty}</span>`;
|
|
}
|
|
|
|
// ── DOM Setup ─────────────────────────────────────
|
|
|
|
function _createDOM() {
|
|
// Create container if it doesn't exist
|
|
if (_container) return;
|
|
|
|
_container = document.createElement('div');
|
|
_container.id = 'reasoning-trace';
|
|
_container.className = 'hud-panel reasoning-trace';
|
|
|
|
_header = document.createElement('div');
|
|
_header.className = 'panel-header';
|
|
_header.innerHTML = `<span class="trace-icon">🧠</span> REASONING TRACE`;
|
|
|
|
// Task indicator
|
|
const taskIndicator = document.createElement('div');
|
|
taskIndicator.className = 'trace-task';
|
|
taskIndicator.id = 'trace-task';
|
|
taskIndicator.textContent = 'No active task';
|
|
|
|
// Step counter
|
|
const stepCounter = document.createElement('div');
|
|
stepCounter.className = 'trace-counter';
|
|
stepCounter.id = 'trace-counter';
|
|
stepCounter.textContent = '0 steps';
|
|
|
|
// Controls
|
|
const controls = document.createElement('div');
|
|
controls.className = 'trace-controls';
|
|
controls.innerHTML = `
|
|
<button class="trace-btn" id="trace-clear" title="Clear trace">🗑️</button>
|
|
<button class="trace-btn" id="trace-toggle" title="Toggle visibility">👁️</button>
|
|
<button class="trace-btn" id="trace-export" title="Export trace">📤</button>
|
|
`;
|
|
|
|
// Header container
|
|
const headerContainer = document.createElement('div');
|
|
headerContainer.className = 'trace-header-container';
|
|
headerContainer.appendChild(_header);
|
|
headerContainer.appendChild(controls);
|
|
|
|
// Content area
|
|
_content = document.createElement('div');
|
|
_content.className = 'panel-content trace-content';
|
|
_content.id = 'reasoning-trace-content';
|
|
|
|
// Assemble
|
|
_container.appendChild(headerContainer);
|
|
_container.appendChild(taskIndicator);
|
|
_container.appendChild(stepCounter);
|
|
_container.appendChild(_content);
|
|
|
|
// Add to HUD
|
|
const hud = document.getElementById('hud');
|
|
if (hud) {
|
|
const gofaiHud = hud.querySelector('.gofai-hud');
|
|
if (gofaiHud) {
|
|
gofaiHud.appendChild(_container);
|
|
} else {
|
|
hud.appendChild(_container);
|
|
}
|
|
}
|
|
|
|
// Add event listeners
|
|
document.getElementById('trace-clear')?.addEventListener('click', clear);
|
|
document.getElementById('trace-toggle')?.addEventListener('click', toggle);
|
|
document.getElementById('trace-export')?.addEventListener('click', exportTrace);
|
|
}
|
|
|
|
// ── Rendering ─────────────────────────────────────
|
|
|
|
function _renderStep(step, index) {
|
|
const typeConfig = STEP_TYPES[step.type] || STEP_TYPES.THINK;
|
|
const timestamp = _formatTimestamp(step.timestamp);
|
|
const confidence = _getConfidenceBar(step.confidence);
|
|
|
|
const stepEl = document.createElement('div');
|
|
stepEl.className = `trace-step trace-step-${step.type.toLowerCase()}`;
|
|
stepEl.dataset.stepId = step.id;
|
|
|
|
// Step header
|
|
const header = document.createElement('div');
|
|
header.className = 'trace-step-header';
|
|
header.innerHTML = `
|
|
<span class="step-icon">${typeConfig.icon}</span>
|
|
<span class="step-type" style="color: ${typeConfig.color}">${typeConfig.label}</span>
|
|
<span class="step-time">${timestamp}</span>
|
|
${confidence}
|
|
`;
|
|
|
|
// Step content
|
|
const content = document.createElement('div');
|
|
content.className = 'trace-step-content';
|
|
|
|
if (step.thought) {
|
|
const thought = document.createElement('div');
|
|
thought.className = 'step-thought';
|
|
thought.textContent = step.thought;
|
|
content.appendChild(thought);
|
|
}
|
|
|
|
if (step.reasoning) {
|
|
const reasoning = document.createElement('div');
|
|
reasoning.className = 'step-reasoning';
|
|
reasoning.textContent = step.reasoning;
|
|
content.appendChild(reasoning);
|
|
}
|
|
|
|
if (step.decision) {
|
|
const decision = document.createElement('div');
|
|
decision.className = 'step-decision';
|
|
decision.innerHTML = `<strong>Decision:</strong> ${_escapeHtml(step.decision)}`;
|
|
content.appendChild(decision);
|
|
}
|
|
|
|
if (step.alternatives && step.alternatives.length > 0) {
|
|
const alternatives = document.createElement('div');
|
|
alternatives.className = 'step-alternatives';
|
|
alternatives.innerHTML = `<strong>Alternatives:</strong> ${step.alternatives.map(a => _escapeHtml(a)).join(', ')}`;
|
|
content.appendChild(alternatives);
|
|
}
|
|
|
|
if (step.source) {
|
|
const source = document.createElement('div');
|
|
source.className = 'step-source';
|
|
source.innerHTML = `<strong>Source:</strong> ${_escapeHtml(step.source)}`;
|
|
content.appendChild(source);
|
|
}
|
|
|
|
stepEl.appendChild(header);
|
|
stepEl.appendChild(content);
|
|
|
|
return stepEl;
|
|
}
|
|
|
|
function _render() {
|
|
if (!_content) return;
|
|
|
|
// Clear content
|
|
_content.innerHTML = '';
|
|
|
|
// Update task indicator
|
|
const taskEl = document.getElementById('trace-task');
|
|
if (taskEl) {
|
|
taskEl.textContent = _currentTask || 'No active task';
|
|
taskEl.className = _currentTask ? 'trace-task active' : 'trace-task';
|
|
}
|
|
|
|
// Update step counter
|
|
const counterEl = document.getElementById('trace-counter');
|
|
if (counterEl) {
|
|
counterEl.textContent = `${_steps.length} step${_steps.length !== 1 ? 's' : ''}`;
|
|
}
|
|
|
|
// Render steps (newest first)
|
|
const sortedSteps = [..._steps].sort((a, b) => b.timestamp - a.timestamp);
|
|
|
|
for (let i = 0; i < sortedSteps.length; i++) {
|
|
const stepEl = _renderStep(sortedSteps[i], i);
|
|
_content.appendChild(stepEl);
|
|
|
|
// Add separator between steps
|
|
if (i < sortedSteps.length - 1) {
|
|
const separator = document.createElement('div');
|
|
separator.className = 'trace-separator';
|
|
_content.appendChild(separator);
|
|
}
|
|
}
|
|
|
|
// Show empty state if no steps
|
|
if (_steps.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'trace-empty';
|
|
empty.innerHTML = `
|
|
<span class="empty-icon">💭</span>
|
|
<span class="empty-text">No reasoning steps yet</span>
|
|
<span class="empty-hint">Start a task to see the trace</span>
|
|
`;
|
|
_content.appendChild(empty);
|
|
}
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────
|
|
|
|
function init() {
|
|
_createDOM();
|
|
_render();
|
|
console.info('[ReasoningTrace] Initialized');
|
|
}
|
|
|
|
/**
|
|
* Add a reasoning step to the trace.
|
|
* @param {Object} step - The reasoning step
|
|
* @param {string} step.type - Step type (THINK, DECIDE, RECALL, PLAN, EXECUTE, VERIFY, DOUBT, MEMORY)
|
|
* @param {string} step.thought - The main thought/content
|
|
* @param {string} [step.reasoning] - Detailed reasoning
|
|
* @param {string} [step.decision] - Decision made
|
|
* @param {string[]} [step.alternatives] - Alternative options considered
|
|
* @param {string} [step.source] - Source of information
|
|
* @param {number} [step.confidence] - Confidence level (0-1)
|
|
* @param {string} [step.taskId] - Associated task ID
|
|
*/
|
|
function addStep(step) {
|
|
if (!step || !step.thought) return;
|
|
|
|
// Generate unique ID
|
|
const id = `step-${++_stepCounter}-${Date.now()}`;
|
|
|
|
// Create step object
|
|
const newStep = {
|
|
id,
|
|
timestamp: Date.now(),
|
|
type: step.type || 'THINK',
|
|
thought: step.thought,
|
|
reasoning: step.reasoning || null,
|
|
decision: step.decision || null,
|
|
alternatives: step.alternatives || null,
|
|
source: step.source || null,
|
|
confidence: step.confidence !== undefined ? Math.max(0, Math.min(1, step.confidence)) : null,
|
|
taskId: step.taskId || _currentTask
|
|
};
|
|
|
|
// Add to steps array
|
|
_steps.unshift(newStep);
|
|
|
|
// Limit number of steps
|
|
if (_steps.length > _maxSteps) {
|
|
_steps = _steps.slice(0, _maxSteps);
|
|
}
|
|
|
|
// Update task if provided
|
|
if (step.taskId && step.taskId !== _currentTask) {
|
|
setTask(step.taskId);
|
|
}
|
|
|
|
// Re-render
|
|
_render();
|
|
|
|
// Log to console for debugging
|
|
console.debug(`[ReasoningTrace] ${newStep.type}: ${newStep.thought}`);
|
|
|
|
return newStep.id;
|
|
}
|
|
|
|
/**
|
|
* Set the current task being traced.
|
|
* @param {string} taskId - Task identifier
|
|
*/
|
|
function setTask(taskId) {
|
|
_currentTask = taskId;
|
|
_render();
|
|
console.info(`[ReasoningTrace] Task set: ${taskId}`);
|
|
}
|
|
|
|
/**
|
|
* Clear all steps from the trace.
|
|
*/
|
|
function clear() {
|
|
_steps = [];
|
|
_stepCounter = 0;
|
|
_render();
|
|
console.info('[ReasoningTrace] Cleared');
|
|
}
|
|
|
|
/**
|
|
* Toggle the visibility of the trace panel.
|
|
*/
|
|
function toggle() {
|
|
_isVisible = !_isVisible;
|
|
if (_container) {
|
|
_container.style.display = _isVisible ? 'block' : 'none';
|
|
}
|
|
console.info(`[ReasoningTrace] Visibility: ${_isVisible ? 'shown' : 'hidden'}`);
|
|
}
|
|
|
|
/**
|
|
* Export the trace as JSON.
|
|
* @returns {string} JSON string of the trace
|
|
*/
|
|
function exportTrace() {
|
|
const exportData = {
|
|
task: _currentTask,
|
|
exportedAt: new Date().toISOString(),
|
|
steps: _steps.map(step => ({
|
|
type: step.type,
|
|
thought: step.thought,
|
|
reasoning: step.reasoning,
|
|
decision: step.decision,
|
|
alternatives: step.alternatives,
|
|
source: step.source,
|
|
confidence: step.confidence,
|
|
timestamp: new Date(step.timestamp).toISOString()
|
|
}))
|
|
};
|
|
|
|
const json = JSON.stringify(exportData, null, 2);
|
|
|
|
// Copy to clipboard
|
|
navigator.clipboard.writeText(json).then(() => {
|
|
console.info('[ReasoningTrace] Copied to clipboard');
|
|
// Show feedback
|
|
const btn = document.getElementById('trace-export');
|
|
if (btn) {
|
|
const original = btn.innerHTML;
|
|
btn.innerHTML = '✅';
|
|
setTimeout(() => { btn.innerHTML = original; }, 1000);
|
|
}
|
|
}).catch(err => {
|
|
console.error('[ReasoningTrace] Failed to copy:', err);
|
|
});
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Get the current trace data.
|
|
* @returns {Object} Current trace state
|
|
*/
|
|
function getTrace() {
|
|
return {
|
|
task: _currentTask,
|
|
steps: [..._steps],
|
|
stepCount: _steps.length,
|
|
isVisible: _isVisible
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get steps filtered by type.
|
|
* @param {string} type - Step type to filter by
|
|
* @returns {Array} Filtered steps
|
|
*/
|
|
function getStepsByType(type) {
|
|
return _steps.filter(step => step.type === type);
|
|
}
|
|
|
|
/**
|
|
* Get steps for a specific task.
|
|
* @param {string} taskId - Task ID to filter by
|
|
* @returns {Array} Filtered steps
|
|
*/
|
|
function getStepsByTask(taskId) {
|
|
return _steps.filter(step => step.taskId === taskId);
|
|
}
|
|
|
|
/**
|
|
* Mark the current task as complete.
|
|
* @param {string} [result] - Optional result description
|
|
*/
|
|
function completeTask(result) {
|
|
if (_currentTask) {
|
|
addStep({
|
|
type: 'VERIFY',
|
|
thought: `Task completed: ${result || 'Success'}`,
|
|
taskId: _currentTask
|
|
});
|
|
|
|
// Clear current task after a delay
|
|
setTimeout(() => {
|
|
_currentTask = null;
|
|
_render();
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
// ── Return Public API ─────────────────────────────
|
|
|
|
return {
|
|
init,
|
|
addStep,
|
|
setTask,
|
|
clear,
|
|
toggle,
|
|
exportTrace,
|
|
getTrace,
|
|
getStepsByType,
|
|
getStepsByTask,
|
|
completeTask,
|
|
STEP_TYPES
|
|
};
|
|
})();
|
|
|
|
export { ReasoningTrace }; |