Create the HTML/CSS/JS for the trace viewer UI.
This is the frontend that users see when they click a trace link from Telegram. It opens in Telegram's in-app browser, so it must be mobile-friendly.
HTML page includes trace data directly via template substitution. Server replaces {{tool_name}}, {{input}}, {{output}}, etc.
HTML page fetches /api/trace/:id on load. Trace ID passed via URL or data attribute.
Recommend Option A for simplicity.
Use a lightweight solution:
Start simple, can enhance later.
If embedding in Haskell: define as Text constant in Omni/Ava/Web.hs If loading from file: Omni/Ava/Web/trace.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Trace: {{tool_name}}</title> <style> /* Mobile-first CSS */ :root { --bg: #1a1a2e; --fg: #eee; --accent: #4a9eff; } @media (prefers-color-scheme: light) { :root { --bg: #fff; --fg: #222; --accent: #0066cc; } } body { font-family: system-ui; background: var(--bg); color: var(--fg); padding: 1rem; } .section { margin: 1rem 0; border: 1px solid var(--accent); border-radius: 8px; } .section-header { padding: 0.75rem; cursor: pointer; } .section-content { padding: 1rem; display: none; overflow-x: auto; } .section.open .section-content { display: block; } pre { margin: 0; white-space: pre-wrap; word-break: break-word; } .copy-btn { float: right; } </style> </head> <body> <h1>{{tool_name}}</h1> <p>{{timestamp}} · {{duration_ms}}ms</p>
<div class="section open"> <div class="section-header">Input <button class="copy-btn">Copy</button></div> <div class="section-content"><pre>{{input_json}}</pre></div> </div>
<div class="section open"> <div class="section-header">Output <button class="copy-btn">Copy</button></div> <div class="section-content"><pre>{{output_json}}</pre></div> </div>
<script> document.querySelectorAll('.section-header').forEach(h => { h.addEventListener('click', e => { if (!e.target.classList.contains('copy-btn')) h.parentElement.classList.toggle('open'); }); }); document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => { const text = btn.closest('.section').querySelector('pre').textContent; navigator.clipboard.writeText(text); btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'Copy', 1500); }); }); </script> </body> </html>
Test in: