// UI Components for G3 Console const components = { // Render status badge statusBadge(status) { const colors = { running: 'badge-success', completed: 'badge-success', failed: 'badge-error', idle: 'badge-warning', terminated: 'badge-neutral' }; return `${status}`; }, // Render progress bar progressBar(instance, stats) { const duration = stats.duration_secs; // Handle zero duration to avoid NaN if (duration === 0) { return this.singleProgressBar(0); } const estimated = duration * 1.5; // Simple estimation const progress = Math.min((duration / estimated) * 100, 100); // Check if this is ensemble mode with turn data if (instance.instance_type === 'ensemble' && stats.turns && stats.turns.length > 0) { return this.ensembleProgressBar(stats.turns, duration); } return `
${Math.round(duration / 60)}m elapsed
`; }, // Render multi-segment progress bar for ensemble mode ensembleProgressBar(turns, totalDuration) { const colors = { coach: '#3b82f6', player: '#6b7280', completed: '#10b981', error: '#ef4444' }; if (turns.length === 0) { // Fallback to single progress bar if no turn data return this.singleProgressBar(totalDuration); } let segments = ''; for (const turn of turns) { // Handle zero total duration to avoid NaN if (totalDuration === 0) { continue; } // Ensure percentage never exceeds 100% const rawPercentage = (turn.duration_secs / totalDuration) * 100; const percentage = Math.min(rawPercentage, 100); const color = colors[turn.agent] || colors.player; const statusColor = turn.status === 'error' ? colors.error : color; const agentLabel = turn.agent.charAt(0).toUpperCase() + turn.agent.slice(1); const durationMin = Math.round(turn.duration_secs / 60); const tooltip = `${agentLabel}: ${durationMin}m ${Math.round(turn.duration_secs % 60)}s - ${turn.status}`; segments += `
`; } return `
${segments} ${Math.round(totalDuration / 60)}m elapsed
`; }, // Single progress bar (fallback) singleProgressBar(duration) { // Handle zero duration if (duration === 0) { return `
Starting...
`; } const estimated = duration * 1.5; const progress = Math.min((duration / estimated) * 100, 100); return `
${Math.round(duration / 60)}m elapsed
`; }, // Render instance panel instancePanel(instance, stats, latestMessage) { return `

${instance.workspace}

${this.statusBadge(instance.status)}
${instance.instance_type} PID: ${instance.pid} ${new Date(instance.start_time).toLocaleString()}
${this.progressBar(instance, stats)}
Tokens ${stats.total_tokens.toLocaleString()}
Tool Calls ${stats.tool_calls}
Errors ${stats.errors}
Duration ${Math.round(stats.duration_secs / 60)}m
${latestMessage ? `
Latest: ${this.truncate(latestMessage, 100)}
` : ''}
${instance.status === 'running' ? ` ` : ''} ${instance.status === 'terminated' ? ` ` : ''}
`; }, // Render loading spinner spinner(message = 'Loading...') { return `

${message}

`; }, // Render error message error(message) { return `
Error: ${message}
`; }, // Render empty state emptyState(message) { return `

${message}

`; }, // Truncate text truncate(text, length) { if (text.length <= length) return text; return text.substring(0, length) + '...'; }, // Render chat message chatMessage(message, agent = null) { // Handle agent as string or object let agentStr = null; if (typeof agent === 'string') { agentStr = agent.toLowerCase(); } else if (agent && typeof agent === 'object') { agentStr = String(agent).toLowerCase(); } const agentClass = agentStr === 'coach' ? 'message-coach' : agentStr === 'player' ? 'message-player' : ''; return `
${agentStr ? `
${agentStr}
` : ''}
${marked.parse(message)}
`; }, // Render tool call toolCall(toolCall) { const statusIcon = toolCall.success ? '✓' : '✗'; const statusClass = toolCall.success ? 'success' : 'error'; return `
🔧 ${toolCall.tool_name}
${toolCall.execution_time_ms ? `${toolCall.execution_time_ms}ms` : ''} ${statusIcon}
Parameters:
${JSON.stringify(toolCall.parameters, null, 2)}
${toolCall.result ? `
Result:
${JSON.stringify(toolCall.result, null, 2)}
` : ''} ${toolCall.error ? `
Error:
${this.escapeHtml(toolCall.error)}
` : ''}
Timestamp: ${new Date(toolCall.timestamp).toLocaleString()} ${toolCall.execution_time_ms ? ` • Duration: ${toolCall.execution_time_ms}ms` : ''} • Status: ${toolCall.success ? 'Success' : 'Failed'}
`; }, // Render git status section gitStatus(gitStatus) { if (!gitStatus) { return '

No git repository detected

'; } return `
📍 ${gitStatus.branch} ${gitStatus.uncommitted_changes} uncommitted changes
${gitStatus.uncommitted_changes > 0 ? `
${gitStatus.modified_files.length > 0 ? `
Modified:
    ${gitStatus.modified_files.map(f => `
  • ${f}
  • `).join('')}
` : ''} ${gitStatus.added_files.length > 0 ? `
Added:
    ${gitStatus.added_files.map(f => `
  • ${f}
  • `).join('')}
` : ''} ${gitStatus.deleted_files.length > 0 ? `
Deleted:
    ${gitStatus.deleted_files.map(f => `
  • ${f}
  • `).join('')}
` : ''}
` : ''}
`; }, // Render project files section projectFiles(projectFiles) { if (!projectFiles || (!projectFiles.requirements && !projectFiles.readme && !projectFiles.agents)) { return '

No project files found

'; } let html = '
'; if (projectFiles.requirements) { html += `
📄 requirements.md
${this.escapeHtml(projectFiles.requirements)}

Showing first 10 lines...

`; } if (projectFiles.readme) { html += `
📄 README.md
${this.escapeHtml(projectFiles.readme)}

Showing first 10 lines...

`; } if (projectFiles.agents) { html += `
📄 AGENTS.md
${this.escapeHtml(projectFiles.agents)}

Showing first 10 lines...

`; } html += '
'; return html; }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // Expose to window for global access window.components = components;