Files
g3/crates/g3-console/web/js/components.js
2025-11-07 09:27:13 +11:00

312 lines
12 KiB
JavaScript

// 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 `<span class="badge ${colors[status] || 'badge-neutral'}">${status}</span>`;
},
// Render progress bar
progressBar(instance, stats) {
const duration = stats.duration_secs;
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 `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
<span class="progress-text">${Math.round(duration / 60)}m elapsed</span>
</div>
`;
},
// Render multi-segment progress bar for ensemble mode
ensembleProgressBar(turns, totalDuration) {
const colors = {
coach: '#3b82f6',
player: '#6b7280',
completed: '#10b981',
error: '#ef4444'
};
let segments = '';
for (const turn of turns) {
const percentage = (turn.duration_secs / totalDuration) * 100;
const color = colors[turn.agent] || colors.player;
const statusColor = turn.status === 'error' ? colors.error : color;
segments += `
<div class="progress-segment"
style="width: ${percentage}%; background-color: ${statusColor};"
title="${turn.agent}: ${turn.duration_secs}s - ${turn.status}">
</div>
`;
}
return `
<div class="progress-bar ensemble">
${segments}
<span class="progress-text">${Math.round(totalDuration / 60)}m elapsed</span>
</div>
`;
},
// Render instance panel
instancePanel(instance, stats, latestMessage) {
return `
<div class="instance-panel" data-id="${instance.id}" onclick="router.navigate('/instance/${instance.id}')">
<div class="panel-header">
<div class="panel-title">
<h3>${instance.workspace}</h3>
${this.statusBadge(instance.status)}
</div>
<div class="panel-meta">
<span class="meta-item">${instance.instance_type}</span>
<span class="meta-item">PID: ${instance.pid}</span>
<span class="meta-item">${new Date(instance.start_time).toLocaleString()}</span>
</div>
</div>
${this.progressBar(instance, stats)}
<div class="panel-stats">
<div class="stat-item">
<span class="stat-label">Tokens</span>
<span class="stat-value">${stats.total_tokens.toLocaleString()}</span>
</div>
<div class="stat-item">
<span class="stat-label">Tool Calls</span>
<span class="stat-value">${stats.tool_calls}</span>
</div>
<div class="stat-item">
<span class="stat-label">Errors</span>
<span class="stat-value">${stats.errors}</span>
</div>
<div class="stat-item">
<span class="stat-label">Duration</span>
<span class="stat-value">${Math.round(stats.duration_secs / 60)}m</span>
</div>
</div>
${latestMessage ? `
<div class="panel-message">
<strong>Latest:</strong> ${this.truncate(latestMessage, 100)}
</div>
` : ''}
<div class="panel-actions">
${instance.status === 'running' ? `
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); handleKill('${instance.id}')">Kill</button>
` : ''}
${instance.status === 'terminated' ? `
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); handleRestart('${instance.id}')">Restart</button>
` : ''}
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); router.navigate('/instance/${instance.id}')">View Details</button>
</div>
</div>
`;
},
// Render loading spinner
spinner(message = 'Loading...') {
return `
<div class="spinner-container">
<div class="spinner"></div>
<p>${message}</p>
</div>
`;
},
// Render error message
error(message) {
return `
<div class="error-message">
<strong>Error:</strong> ${message}
</div>
`;
},
// Render empty state
emptyState(message) {
return `
<div class="empty-state">
<p>${message}</p>
</div>
`;
},
// Truncate text
truncate(text, length) {
if (text.length <= length) return text;
return text.substring(0, length) + '...';
},
// Render chat message
chatMessage(message, agent = null) {
const agentClass = agent === 'coach' ? 'message-coach' : agent === 'player' ? 'message-player' : '';
return `
<div class="chat-message ${agentClass}">
${agent ? `<div class="message-agent">${agent}</div>` : ''}
<div class="message-content">${marked.parse(message)}</div>
</div>
`;
},
// Render tool call
toolCall(toolCall) {
const statusIcon = toolCall.success ? '✓' : '✗';
const statusClass = toolCall.success ? 'success' : 'error';
return `
<div class="tool-call" data-tool-id="${toolCall.id}">
<div class="tool-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="tool-name">🔧 ${toolCall.tool_name}</span>
<div class="tool-header-right">
${toolCall.execution_time_ms ? `<span class="tool-time">${toolCall.execution_time_ms}ms</span>` : ''}
<span class="tool-status ${statusClass}">${statusIcon}</span>
</div>
</div>
<div class="tool-details">
<div class="tool-section">
<strong>Parameters:</strong>
<pre><code class="language-json">${JSON.stringify(toolCall.parameters, null, 2)}</code></pre>
</div>
${toolCall.result ? `
<div class="tool-section">
<strong>Result:</strong>
<pre><code class="language-json">${JSON.stringify(toolCall.result, null, 2)}</code></pre>
</div>
` : ''}
${toolCall.error ? `
<div class="tool-section">
<strong>Error:</strong>
<pre><code class="language-text">${this.escapeHtml(toolCall.error)}</code></pre>
</div>
` : ''}
<div class="tool-meta">
<span>Timestamp: ${new Date(toolCall.timestamp).toLocaleString()}</span>
${toolCall.execution_time_ms ? `<span> • Duration: ${toolCall.execution_time_ms}ms</span>` : ''}
<span> • Status: ${toolCall.success ? 'Success' : 'Failed'}</span>
</div>
</div>
</div>
`;
},
// Render git status section
gitStatus(gitStatus) {
if (!gitStatus) {
return '<p class="text-muted">No git repository detected</p>';
}
return `
<div class="git-status">
<div class="git-header">
<span class="git-branch">📍 ${gitStatus.branch}</span>
<span class="git-changes">${gitStatus.uncommitted_changes} uncommitted changes</span>
</div>
${gitStatus.uncommitted_changes > 0 ? `
<div class="git-files">
${gitStatus.modified_files.length > 0 ? `
<div class="git-file-group">
<strong class="file-status modified">Modified:</strong>
<ul>
${gitStatus.modified_files.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
` : ''}
${gitStatus.added_files.length > 0 ? `
<div class="git-file-group">
<strong class="file-status added">Added:</strong>
<ul>
${gitStatus.added_files.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
` : ''}
${gitStatus.deleted_files.length > 0 ? `
<div class="git-file-group">
<strong class="file-status deleted">Deleted:</strong>
<ul>
${gitStatus.deleted_files.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div>
`;
},
// Render project files section
projectFiles(projectFiles) {
if (!projectFiles || (!projectFiles.requirements && !projectFiles.readme && !projectFiles.agents)) {
return '<p class="text-muted">No project files found</p>';
}
let html = '<div class="project-files">';
if (projectFiles.requirements) {
html += `
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 requirements.md</span>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.requirements)}</code></pre>
</div>
</div>
`;
}
if (projectFiles.readme) {
html += `
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 README.md</span>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.readme)}</code></pre>
</div>
</div>
`;
}
if (projectFiles.agents) {
html += `
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 AGENTS.md</span>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.agents)}</code></pre>
</div>
</div>
`;
}
html += '</div>';
return html;
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};