html_template.html

  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4    <meta charset="UTF-8">
  5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6    <title>{{.Title}}</title>
  7    <style>
  8        :root {
  9            /* Charmtone palette */
 10            --pepper: #201F26;
 11            --bbq: #2d2c35;
 12            --charcoal: #3A3943;
 13            --iron: #4D4C57;
 14            --oyster: #605F6B;
 15            --squid: #858392;
 16            --smoke: #BFBCC8;
 17            --ash: #DFDBDD;
 18            --salt: #F1EFEF;
 19            --butter: #FFFAF1;
 20            
 21            /* Accents */
 22            --charple: #6B50FF;
 23            --dolly: #FF60FF;
 24            --julep: #00FFB2;
 25            --tang: #FF985A;
 26            --malibu: #00A4FF;
 27            --cherry: #FF388B;
 28            --hazy: #8B75FF;
 29            --blush: #FF84FF;
 30            --bok: #68FFD6;
 31            
 32            /* Semantic */
 33            --bg: var(--pepper);
 34            --bg-secondary: var(--bbq);
 35            --bg-tertiary: var(--charcoal);
 36            --border: var(--iron);
 37            --text: var(--smoke);
 38            --text-bright: var(--butter);
 39            --text-muted: var(--squid);
 40            --user: var(--julep);
 41            --agent: var(--dolly);
 42            --system: var(--tang);
 43        }
 44        * { box-sizing: border-box; margin: 0; padding: 0; }
 45        body {
 46            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
 47            background: var(--bg);
 48            color: var(--text);
 49            line-height: 1.5;
 50            padding: 1.5rem;
 51            font-size: 14px;
 52        }
 53        .container { max-width: 1100px; margin: 0 auto; }
 54        header {
 55            border-bottom: 1px solid var(--border);
 56            padding-bottom: 1rem;
 57            margin-bottom: 1.5rem;
 58        }
 59        h1 { font-size: 1.25rem; font-weight: 600; color: var(--text-bright); margin-bottom: 0.25rem; }
 60        .meta { color: var(--text-muted); font-size: 0.75rem; display: flex; gap: 1rem; flex-wrap: wrap; }
 61        .metrics {
 62            display: flex;
 63            gap: 1.5rem;
 64            margin-top: 0.75rem;
 65            padding: 0.75rem 1rem;
 66            background: var(--bg-secondary);
 67            border-radius: 6px;
 68            font-size: 0.8rem;
 69        }
 70        .metric { text-align: center; }
 71        .metric-value { font-size: 1.1rem; font-weight: 600; color: var(--charple); }
 72        .metric-label { color: var(--text-muted); font-size: 0.7rem; }
 73        
 74        /* Timeline */
 75        .timeline { display: flex; flex-direction: column; gap: 2px; }
 76        .step {
 77            background: var(--bg-secondary);
 78            border-radius: 4px;
 79            overflow: hidden;
 80        }
 81        .step-header {
 82            display: flex;
 83            align-items: center;
 84            gap: 0.5rem;
 85            padding: 0.5rem 0.75rem;
 86            cursor: pointer;
 87            user-select: none;
 88            transition: background 0.15s;
 89        }
 90        .step-header:hover { background: var(--bg-tertiary); }
 91        .step-toggle {
 92            color: var(--text-muted);
 93            font-size: 0.7rem;
 94            transition: transform 0.15s;
 95            width: 1rem;
 96        }
 97        .step.expanded .step-toggle { transform: rotate(90deg); }
 98        .step-source {
 99            font-size: 0.65rem;
100            font-weight: 600;
101            text-transform: uppercase;
102            padding: 0.125rem 0.4rem;
103            border-radius: 3px;
104            letter-spacing: 0.03em;
105        }
106        .step.user .step-source { background: rgba(0, 255, 178, 0.15); color: var(--user); }
107        .step.agent .step-source { background: rgba(255, 96, 255, 0.15); color: var(--agent); }
108        .step.system .step-source { background: rgba(255, 152, 90, 0.15); color: var(--system); }
109        .step-id { font-size: 0.7rem; color: var(--text-muted); font-family: monospace; }
110        .step-preview {
111            flex: 1;
112            font-size: 0.8rem;
113            color: var(--text);
114            white-space: nowrap;
115            overflow: hidden;
116            text-overflow: ellipsis;
117            margin-left: 0.5rem;
118        }
119        .step-time { font-size: 0.65rem; color: var(--text-muted); margin-left: auto; }
120        .step-badges { display: flex; gap: 0.25rem; margin-left: 0.5rem; }
121        .badge {
122            font-size: 0.6rem;
123            padding: 0.1rem 0.35rem;
124            border-radius: 3px;
125            background: var(--bg-tertiary);
126            color: var(--text-muted);
127        }
128        .badge.tools { background: rgba(107, 80, 255, 0.2); color: var(--charple); }
129        .badge.thinking { background: rgba(255, 96, 255, 0.2); color: var(--blush); }
130        
131        /* Expanded content */
132        .step-body { display: none; padding: 0.75rem; border-top: 1px solid var(--border); }
133        .step.expanded .step-body { display: block; }
134        .message {
135            background: var(--bg);
136            padding: 0.75rem;
137            border-radius: 4px;
138            white-space: pre-wrap;
139            word-wrap: break-word;
140            font-size: 0.85rem;
141            max-height: 300px;
142            overflow-y: auto;
143        }
144        .message:empty { display: none; }
145        .reasoning {
146            margin-top: 0.5rem;
147            padding: 0.5rem 0.75rem;
148            background: rgba(255, 96, 255, 0.08);
149            border-left: 2px solid var(--agent);
150            border-radius: 0 4px 4px 0;
151            font-size: 0.8rem;
152            color: var(--text-muted);
153            max-height: 200px;
154            overflow-y: auto;
155        }
156        .section-label {
157            font-size: 0.65rem;
158            text-transform: uppercase;
159            color: var(--text-muted);
160            margin-bottom: 0.25rem;
161            letter-spacing: 0.03em;
162        }
163        .reasoning .section-label { color: var(--blush); }
164        
165        /* Tool calls */
166        .tool-calls { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 2px; }
167        .tool-call {
168            background: var(--bg);
169            border-radius: 4px;
170            overflow: hidden;
171        }
172        .tool-call-header {
173            display: flex;
174            align-items: center;
175            gap: 0.5rem;
176            padding: 0.4rem 0.6rem;
177            cursor: pointer;
178            user-select: none;
179            font-size: 0.8rem;
180        }
181        .tool-call-header:hover { background: var(--bg-tertiary); }
182        .tool-status { 
183            font-size: 0.7rem; 
184            width: 1rem;
185        }
186        .tool-call.completed .tool-status { color: var(--julep); }
187        .tool-call.pending .tool-status { color: var(--squid); }
188        .tool-call-name { font-family: monospace; font-weight: 600; color: var(--malibu); }
189        .tool-call-preview { 
190            flex: 1;
191            color: var(--squid); 
192            font-size: 0.75rem;
193            white-space: nowrap;
194            overflow: hidden;
195            text-overflow: ellipsis;
196        }
197        .tool-call-toggle { 
198            color: var(--text-muted); 
199            font-size: 0.6rem; 
200            transition: transform 0.15s;
201            margin-left: auto;
202        }
203        .tool-call.expanded .tool-call-toggle { transform: rotate(90deg); }
204        .tool-call-body { display: none; padding: 0.5rem 0.6rem; border-top: 1px solid var(--border); }
205        .tool-call.expanded .tool-call-body { display: block; }
206        .tool-args { margin-bottom: 0.5rem; }
207        .tool-result {
208            border-top: 1px solid var(--border);
209            padding-top: 0.5rem;
210            margin-top: 0.5rem;
211        }
212        .tool-result-content {
213            background: var(--bg-secondary);
214            padding: 0.5rem;
215            border-radius: 4px;
216            font-size: 0.75rem;
217            font-family: 'SF Mono', Monaco, 'Courier New', monospace;
218            white-space: pre-wrap;
219            word-wrap: break-word;
220            max-height: 200px;
221            overflow-y: auto;
222            color: var(--smoke);
223        }
224        .tool-pending {
225            color: var(--squid);
226            font-size: 0.75rem;
227            font-style: italic;
228            padding: 0.25rem 0;
229        }
230        pre {
231            background: var(--bg-tertiary);
232            padding: 0.5rem;
233            border-radius: 3px;
234            overflow-x: auto;
235            font-size: 0.75rem;
236            font-family: 'SF Mono', Monaco, 'Courier New', monospace;
237            max-height: 200px;
238            overflow-y: auto;
239        }
240        
241        footer {
242            margin-top: 1.5rem;
243            padding-top: 0.75rem;
244            border-top: 1px solid var(--border);
245            text-align: center;
246            color: var(--text-muted);
247            font-size: 0.7rem;
248        }
249        footer a { color: var(--charple); text-decoration: none; }
250        footer a:hover { text-decoration: underline; }
251        
252        /* Controls */
253        .controls {
254            display: flex;
255            gap: 0.5rem;
256            margin-bottom: 0.75rem;
257        }
258        .btn {
259            font-size: 0.7rem;
260            padding: 0.3rem 0.6rem;
261            border-radius: 4px;
262            border: 1px solid var(--border);
263            background: var(--bg-secondary);
264            color: var(--text);
265            cursor: pointer;
266            transition: all 0.15s;
267        }
268        .btn:hover { background: var(--bg-tertiary); border-color: var(--oyster); }
269        .filter-group { display: flex; gap: 0.25rem; }
270        .filter-btn { border-radius: 3px; }
271        .filter-btn.active { background: var(--charple); border-color: var(--charple); color: var(--butter); }
272    </style>
273</head>
274<body>
275    <div class="container">
276        <header>
277            <h1 id="title"></h1>
278            <div class="meta">
279                <span id="session-id"></span>
280                <span id="schema-version"></span>
281                <span id="model-name"></span>
282            </div>
283            <div class="metrics" id="metrics"></div>
284        </header>
285        <div class="controls">
286            <button class="btn" onclick="expandAll()">Expand All</button>
287            <button class="btn" onclick="collapseAll()">Collapse All</button>
288            <div class="filter-group">
289                <button class="btn filter-btn active" data-filter="all" onclick="filterSteps('all', this)">All</button>
290                <button class="btn filter-btn" data-filter="user" onclick="filterSteps('user', this)">User</button>
291                <button class="btn filter-btn" data-filter="agent" onclick="filterSteps('agent', this)">Agent</button>
292                <button class="btn filter-btn" data-filter="system" onclick="filterSteps('system', this)">System</button>
293            </div>
294        </div>
295        <div class="timeline" id="timeline"></div>
296        <footer>
297            Generated by <a href="https://github.com/charmbracelet/crush">Crush</a> · Harbor ATIF v1.4
298        </footer>
299    </div>
300
301    <script>
302        const trajectory = {{.TrajectoryJSON}};
303
304        function formatTimestamp(ts) {
305            if (!ts) return '';
306            const d = new Date(ts);
307            return d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
308        }
309
310        function escapeHtml(text) {
311            if (!text) return '';
312            const div = document.createElement('div');
313            div.textContent = text;
314            return div.innerHTML;
315        }
316
317        function truncate(text, max = 100) {
318            if (!text) return '';
319            const single = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
320            if (single.length <= max) return single;
321            return single.slice(0, max) + '…';
322        }
323
324        function renderHeader() {
325            document.getElementById('title').textContent = trajectory.agent.name + ' Trajectory';
326            document.getElementById('session-id').textContent = 'Session: ' + trajectory.session_id.slice(0, 20) + (trajectory.session_id.length > 20 ? '…' : '');
327            document.getElementById('schema-version').textContent = trajectory.schema_version;
328            if (trajectory.agent.model_name) {
329                document.getElementById('model-name').textContent = 'Model: ' + trajectory.agent.model_name;
330            }
331
332            const metrics = trajectory.final_metrics;
333            if (metrics) {
334                document.getElementById('metrics').innerHTML = `
335                    <div class="metric">
336                        <div class="metric-value">${metrics.total_steps || trajectory.steps.length}</div>
337                        <div class="metric-label">Steps</div>
338                    </div>
339                    <div class="metric">
340                        <div class="metric-value">${((metrics.total_prompt_tokens || 0) / 1000).toFixed(1)}k</div>
341                        <div class="metric-label">Prompt</div>
342                    </div>
343                    <div class="metric">
344                        <div class="metric-value">${((metrics.total_completion_tokens || 0) / 1000).toFixed(1)}k</div>
345                        <div class="metric-label">Completion</div>
346                    </div>
347                    <div class="metric">
348                        <div class="metric-value">$${(metrics.total_cost_usd || 0).toFixed(3)}</div>
349                        <div class="metric-label">Cost</div>
350                    </div>
351                `;
352            }
353        }
354
355        function renderToolCall(tc, idx, observationResults) {
356            const argsJson = typeof tc.arguments === 'string' 
357                ? tc.arguments 
358                : JSON.stringify(tc.arguments, null, 2);
359            
360            // Extract key params for header preview
361            const args = typeof tc.arguments === 'object' ? tc.arguments : {};
362            const preview = getToolPreview(tc.function_name, args);
363            
364            // Find the matching result for this tool call
365            const result = (observationResults || []).find(r => r.source_call_id === tc.tool_call_id);
366            
367            const resultHtml = result ? `
368                <div class="tool-result">
369                    <div class="tool-result-content">${escapeHtml(result.content)}</div>
370                </div>
371            ` : '<div class="tool-pending">Waiting for result...</div>';
372            
373            return `
374                <div class="tool-call ${result ? 'completed' : 'pending'}" id="tc-${idx}">
375                    <div class="tool-call-header" onclick="toggleToolCall('tc-${idx}')">
376                        <span class="tool-status">${result ? '✓' : '○'}</span>
377                        <span class="tool-call-name">${escapeHtml(tc.function_name)}</span>
378                        <span class="tool-call-preview">${escapeHtml(preview)}</span>
379                        <span class="tool-call-toggle">▶</span>
380                    </div>
381                    <div class="tool-call-body">
382                        <div class="tool-args">
383                            <div class="section-label">Arguments</div>
384                            <pre>${escapeHtml(argsJson)}</pre>
385                        </div>
386                        ${resultHtml}
387                    </div>
388                </div>
389            `;
390        }
391
392        function getToolPreview(name, args) {
393            // Extract key params like the TUI does
394            switch(name) {
395                case 'bash':
396                    return truncate(args.command || '', 60);
397                case 'view':
398                case 'edit':
399                case 'write':
400                case 'multiedit':
401                    return args.file_path || '';
402                case 'glob':
403                    return args.pattern || '';
404                case 'grep':
405                    return `${args.pattern || ''}${args.path ? ' in ' + args.path : ''}`;
406                case 'ls':
407                    return args.path || '.';
408                case 'fetch':
409                case 'agentic_fetch':
410                case 'download':
411                    return args.url || '';
412                case 'sourcegraph':
413                    return args.query || '';
414                case 'todos':
415                    const todos = args.todos || [];
416                    const completed = todos.filter(t => t.status === 'completed').length;
417                    return `${completed}/${todos.length}`;
418                case 'agent':
419                    return truncate(args.prompt || '', 50);
420                default:
421                    // Try to get first string value
422                    for (const key of Object.keys(args)) {
423                        if (typeof args[key] === 'string' && args[key].length < 80) {
424                            return truncate(args[key], 60);
425                        }
426                    }
427                    return '';
428            }
429        }
430
431        function renderStep(step, idx) {
432            const observationResults = step.observation?.results || [];
433            const toolCalls = (step.tool_calls || []).map((tc, i) => renderToolCall(tc, `${idx}-${i}`, observationResults)).join('');
434            const reasoning = step.reasoning_content ? `
435                <div class="reasoning">
436                    <div class="section-label">Thinking</div>
437                    ${escapeHtml(step.reasoning_content)}
438                </div>
439            ` : '';
440            
441            const badges = [];
442            if (step.tool_calls && step.tool_calls.length > 0) {
443                badges.push(`<span class="badge tools">${step.tool_calls.length} tool${step.tool_calls.length > 1 ? 's' : ''}</span>`);
444            }
445            if (step.reasoning_content) {
446                badges.push(`<span class="badge thinking">thinking</span>`);
447            }
448
449            const preview = step.message || (step.observation?.results?.[0]?.content) || '';
450
451            return `
452                <div class="step ${step.source}" data-source="${step.source}" id="step-${idx}">
453                    <div class="step-header" onclick="toggleStep('step-${idx}')">
454                        <span class="step-toggle">▶</span>
455                        <span class="step-source">${step.source}</span>
456                        <span class="step-id">#${step.step_id}</span>
457                        <span class="step-preview">${escapeHtml(truncate(preview, 80))}</span>
458                        <div class="step-badges">${badges.join('')}</div>
459                        <span class="step-time">${formatTimestamp(step.timestamp)}</span>
460                    </div>
461                    <div class="step-body">
462                        ${step.message ? `<div class="message">${escapeHtml(step.message)}</div>` : ''}
463                        ${reasoning}
464                        ${toolCalls ? '<div class="tool-calls">' + toolCalls + '</div>' : ''}
465                    </div>
466                </div>
467            `;
468        }
469
470        function renderTimeline() {
471            const timeline = document.getElementById('timeline');
472            timeline.innerHTML = trajectory.steps.map((s, i) => renderStep(s, i)).join('');
473        }
474
475        function toggleStep(id) {
476            document.getElementById(id).classList.toggle('expanded');
477        }
478
479        function toggleToolCall(id) {
480            document.getElementById(id).classList.toggle('expanded');
481        }
482
483        function expandAll() {
484            document.querySelectorAll('.step').forEach(el => el.classList.add('expanded'));
485        }
486
487        function collapseAll() {
488            document.querySelectorAll('.step').forEach(el => el.classList.remove('expanded'));
489            document.querySelectorAll('.tool-call').forEach(el => el.classList.remove('expanded'));
490        }
491
492        function filterSteps(filter, btn) {
493            document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
494            btn.classList.add('active');
495            document.querySelectorAll('.step').forEach(el => {
496                if (filter === 'all' || el.dataset.source === filter) {
497                    el.style.display = '';
498                } else {
499                    el.style.display = 'none';
500                }
501            });
502        }
503
504        renderHeader();
505        renderTimeline();
506    </script>
507</body>
508</html>