Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>CSC Engine — Lab Diary</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <style> | |
| :root { | |
| font-family: 'SF Pro Display', system-ui, -apple-system, sans-serif; | |
| --bg: #06080f; | |
| --panel: #0b0e18; | |
| --panel2: #0f1320; | |
| --border: #1a1f2e; | |
| --border2: #232a3d; | |
| --accent: #00d4ff; | |
| --accent2: #6c5ce7; | |
| --green: #34d399; | |
| --amber: #fbbf24; | |
| --red: #f87171; | |
| --gray: #6b7280; | |
| --text: #e2e8f0; | |
| --text2: #94a3b8; | |
| --mono: 'SF Mono', 'JetBrains Mono', monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| z-index: -1; | |
| background: radial-gradient(ellipse at 20% 0%, rgba(0, 212, 255, .04), transparent 50%), | |
| radial-gradient(ellipse at 80% 100%, rgba(108, 92, 231, .04), transparent 50%); | |
| } | |
| main { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 0 24px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo-mark { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 8px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| font-weight: 800; | |
| color: #000; | |
| } | |
| header h1 { | |
| font-size: 24px; | |
| font-weight: 700; | |
| } | |
| header h1 span { | |
| color: var(--accent); | |
| } | |
| .sub { | |
| color: var(--text2); | |
| font-size: 13px; | |
| margin-top: 2px; | |
| } | |
| .back-link { | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| background: var(--panel2); | |
| color: var(--text2); | |
| border: 1px solid var(--border2); | |
| text-decoration: none; | |
| transition: .15s; | |
| } | |
| .back-link:hover { | |
| background: #161b2d; | |
| color: var(--text); | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| margin-bottom: 24px; | |
| } | |
| .stat { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 16px; | |
| text-align: center; | |
| } | |
| .stat strong { | |
| display: block; | |
| font-size: 28px; | |
| color: var(--accent); | |
| font-family: var(--mono); | |
| } | |
| .stat small { | |
| color: var(--text2); | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .entries { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .entry { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| overflow: hidden; | |
| transition: .15s; | |
| } | |
| .entry:hover { | |
| border-color: var(--border2); | |
| } | |
| .entry-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 14px 18px; | |
| border-bottom: 1px solid var(--border); | |
| background: linear-gradient(180deg, var(--panel2), transparent); | |
| } | |
| .entry-num { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 6px; | |
| background: rgba(0, 212, 255, .1); | |
| color: var(--accent); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: 700; | |
| font-family: var(--mono); | |
| } | |
| .entry-title { | |
| font-size: 14px; | |
| font-weight: 600; | |
| flex: 1; | |
| } | |
| .entry-hash { | |
| font-size: 11px; | |
| color: var(--text2); | |
| font-family: var(--mono); | |
| } | |
| .entry-time { | |
| font-size: 11px; | |
| color: var(--gray); | |
| font-family: var(--mono); | |
| } | |
| .entry-body { | |
| padding: 18px; | |
| } | |
| .entry-section { | |
| margin-bottom: 14px; | |
| } | |
| .entry-section:last-child { | |
| margin-bottom: 0; | |
| } | |
| .entry-label { | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-bottom: 6px; | |
| } | |
| .label-evidence { | |
| color: var(--accent); | |
| } | |
| .label-reasoning { | |
| color: var(--amber); | |
| } | |
| .label-code { | |
| color: var(--green); | |
| } | |
| .label-output { | |
| color: #a78bfa; | |
| } | |
| .label-audio { | |
| color: #f472b6; | |
| } | |
| .entry-evidence { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--text2); | |
| line-height: 1.6; | |
| } | |
| .entry-evidence li { | |
| list-style: none; | |
| padding-left: 16px; | |
| position: relative; | |
| margin: 2px 0; | |
| } | |
| .entry-evidence li::before { | |
| content: '\25B8'; | |
| position: absolute; | |
| left: 0; | |
| color: var(--accent); | |
| } | |
| .entry-reasoning { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--text2); | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| } | |
| .entry-code { | |
| background: #060810; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 14px; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| line-height: 1.6; | |
| color: #c8d3f5; | |
| overflow-x: auto; | |
| } | |
| .entry-output { | |
| background: #04060c; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 14px; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| line-height: 1.6; | |
| color: var(--green); | |
| overflow-x: auto; | |
| } | |
| .entry-output.err { | |
| color: var(--red); | |
| } | |
| .entry-actions { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 12px; | |
| } | |
| .entry-btn { | |
| padding: 6px 14px; | |
| border-radius: 6px; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 11px; | |
| font-weight: 600; | |
| transition: .15s; | |
| text-decoration: none; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .btn-run { | |
| background: rgba(52, 211, 153, .15); | |
| color: var(--green); | |
| border: 1px solid rgba(52, 211, 153, .3); | |
| } | |
| .btn-run:hover { | |
| background: rgba(52, 211, 153, .25); | |
| } | |
| .btn-view { | |
| background: rgba(0, 212, 255, .1); | |
| color: var(--accent); | |
| border: 1px solid rgba(0, 212, 255, .2); | |
| } | |
| .btn-view:hover { | |
| background: rgba(0, 212, 255, .2); | |
| } | |
| .btn-debug { | |
| background: rgba(251, 191, 36, .15); | |
| color: var(--amber); | |
| border: 1px solid rgba(251, 191, 36, .3); | |
| } | |
| .btn-debug:hover { | |
| background: rgba(251, 191, 36, .25); | |
| } | |
| .entry-audio { | |
| margin-top: 10px; | |
| } | |
| .entry-audio audio { | |
| width: 100%; | |
| height: 36px; | |
| } | |
| .empty { | |
| text-align: center; | |
| padding: 60px; | |
| color: var(--text2); | |
| font-family: var(--mono); | |
| } | |
| .empty h2 { | |
| font-size: 18px; | |
| margin-bottom: 8px; | |
| color: var(--text); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-mark">C</div> | |
| <div> | |
| <h1>Lab <span>Diary</span></h1> | |
| <div class="sub">AI Notebook — computed artifacts with evidence, reasoning & audio</div> | |
| </div> | |
| </div> | |
| <a href="/" class="back-link">← Back to Engine</a> | |
| </header> | |
| <div class="stats"> | |
| <div class="stat"><strong id="totalCount">0</strong><small>Artifacts</small></div> | |
| <div class="stat"><strong id="runCount">0</strong><small>Executed</small></div> | |
| <div class="stat"><strong id="audioCount">0</strong><small>With Audio</small></div> | |
| <div class="stat"><strong id="successCount">0</strong><small>Exit 0</small></div> | |
| </div> | |
| <div class="entries" id="entries"> | |
| <div class="empty"> | |
| <h2>Loading artifacts...</h2> | |
| <p>Fetching from /api/artifacts</p> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| function escapeHtml(text) { | |
| const div = document.createElement("div"); | |
| div.textContent = text || ""; | |
| return div.innerHTML; | |
| } | |
| function formatTime(ts) { | |
| if (!ts) return "unknown"; | |
| const d = new Date(ts * 1000); | |
| return d.toLocaleString(); | |
| } | |
| function parseSections(text) { | |
| const sections = {}; | |
| const patterns = ['REASONING', 'SCENE', 'INFERRED_INTENT', 'INTENT', 'SIGNALS', 'CANDIDATE_TASK', 'UNCERTAINTY', 'EVIDENCE', 'CODE', 'RUN', 'TEST', 'ATTRIBUTION']; | |
| const patStr = patterns.join('|'); | |
| for (const pat of patterns) { | |
| const regex = new RegExp('\\*{0,2}' + pat + '\\*{0,2}:?\\s*([\\s\\S]*?)(?=\\n\\s*\\*{0,2}(?:' + patStr + ')\\*{0,2}:|\\n\\s*(?:INSUFFICIENT)|$)', 'i'); | |
| const m = text.match(regex); | |
| if (m && m[1].trim()) sections[pat] = m[1].trim(); | |
| } | |
| if (sections.CODE) { | |
| const fenceMatch = sections.CODE.match(/```(?:python)?\s*([\s\S]*?)```/); | |
| if (fenceMatch) sections.CODE = fenceMatch[1].trim(); | |
| } | |
| return sections; | |
| } | |
| async function loadDiary() { | |
| try { | |
| const resp = await fetch('/api/artifacts?limit=50'); | |
| const data = await resp.json(); | |
| const artifacts = data.artifacts || []; | |
| renderEntries(artifacts); | |
| } catch (e) { | |
| document.getElementById('entries').innerHTML = '<div class="empty"><h2>Error</h2><p>' + escapeHtml(e.message) + '</p></div>'; | |
| } | |
| } | |
| function renderEntries(artifacts) { | |
| const container = document.getElementById('entries'); | |
| let runCount = 0, audioCount = 0, successCount = 0; | |
| if (artifacts.length === 0) { | |
| container.innerHTML = '<div class="empty"><h2>No artifacts yet</h2><p>Start the engine and generate code to populate the diary.</p></div>'; | |
| document.getElementById('totalCount').textContent = '0'; | |
| document.getElementById('runCount').textContent = '0'; | |
| document.getElementById('audioCount').textContent = '0'; | |
| document.getElementById('successCount').textContent = '0'; | |
| return; | |
| } | |
| let html = ''; | |
| artifacts.forEach((art, i) => { | |
| const num = artifacts.length - i; | |
| const hash = art.patch_hash || 'unknown'; | |
| const hasRun = art.exit_code !== undefined; | |
| const hasAudio = !!art.audio_path; | |
| const success = art.exit_code === 0; | |
| if (hasRun) runCount++; | |
| if (hasAudio) audioCount++; | |
| if (success) successCount++; | |
| // Parse sections from observer_output if available | |
| const sections = parseSections(art.observer_output || ''); | |
| const evidence = art.evidence || sections.EVIDENCE || ''; | |
| const reasoning = art.reasoning || sections.REASONING || ''; | |
| const code = art.code || sections.CODE || ''; | |
| const stdout = art.stdout || ''; | |
| const stderr = art.stderr || ''; | |
| html += '<div class="entry">'; | |
| html += '<div class="entry-header">'; | |
| html += '<div class="entry-num">' + num + '</div>'; | |
| html += '<div class="entry-title">Artifact ' + hash.slice(0, 12) + '</div>'; | |
| html += '<div class="entry-hash">' + hash.slice(0, 16) + '</div>'; | |
| html += '<div class="entry-time">' + formatTime(art.timestamp) + '</div>'; | |
| html += '</div>'; | |
| html += '<div class="entry-body">'; | |
| if (evidence) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-evidence">\u258C Evidence</div>'; | |
| html += '<ul class="entry-evidence">'; | |
| for (const line of evidence.split('\n').filter(l => l.trim())) { | |
| html += '<li>' + escapeHtml(line.replace(/^[-\u2022*]\s*/, '')) + '</li>'; | |
| } | |
| html += '</ul></div>'; | |
| } | |
| if (reasoning) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-reasoning">\u258C Reasoning</div>'; | |
| html += '<div class="entry-reasoning">' + escapeHtml(reasoning) + '</div>'; | |
| html += '</div>'; | |
| } | |
| if (code) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-code">\u258C Code</div>'; | |
| html += '<pre class="entry-code">' + escapeHtml(code) + '</pre>'; | |
| html += '</div>'; | |
| } | |
| if (stdout) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-output">\u258C Output (exit ' + art.exit_code + ')</div>'; | |
| html += '<pre class="entry-output">' + escapeHtml(stdout) + '</pre>'; | |
| html += '</div>'; | |
| } | |
| if (stderr) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-output" style="color:var(--red)">\u258C Errors</div>'; | |
| html += '<pre class="entry-output err">' + escapeHtml(stderr) + '</pre>'; | |
| html += '</div>'; | |
| } | |
| if (hasAudio) { | |
| html += '<div class="entry-section">'; | |
| html += '<div class="entry-label label-audio">\u258C Audio Recording</div>'; | |
| html += '<div class="entry-audio"><audio controls src="/audio/' + art.audio_path + '"></audio></div>'; | |
| html += '</div>'; | |
| } | |
| html += '<div class="entry-actions">'; | |
| html += '<button class="entry-btn btn-run" onclick="runFromDiary(\'' + hash + '\', this)">\u25B6 Run</button>'; | |
| html += '<button class="entry-btn btn-debug" onclick="debugFromDiary(\'' + hash + '\', this)">\u2691 Debug & Fix</button>'; | |
| html += '<a class="entry-btn btn-view" href="/artifact/' + hash + '" target="_blank">View Artifact</a>'; | |
| html += '</div>'; | |
| html += '</div></div>'; | |
| }); | |
| container.innerHTML = html; | |
| document.getElementById('totalCount').textContent = artifacts.length; | |
| document.getElementById('runCount').textContent = runCount; | |
| document.getElementById('audioCount').textContent = audioCount; | |
| document.getElementById('successCount').textContent = successCount; | |
| } | |
| async function runFromDiary(patchHash, btn) { | |
| if (btn) { btn.disabled = true; btn.textContent = 'Running...'; } | |
| try { | |
| const resp = await fetch('/run?patch_hash=' + patchHash, { method: 'POST' }); | |
| const data = await resp.json(); | |
| if (data.exit_code === 0) { | |
| btn.style.background = 'rgba(52,211,153,.3)'; | |
| btn.textContent = '\u2713 Success'; | |
| } else { | |
| btn.style.background = 'rgba(248,113,113,.2)'; | |
| btn.style.color = 'var(--red)'; | |
| btn.textContent = '\u2717 Exit ' + data.exit_code; | |
| } | |
| // Reload diary after a moment | |
| setTimeout(() => loadDiary(), 500); | |
| } catch (e) { | |
| btn.textContent = 'Error'; | |
| } | |
| setTimeout(() => { | |
| if (btn) { btn.disabled = false; btn.textContent = '\u25B6 Run'; btn.style.background = ''; btn.style.color = ''; } | |
| }, 3000); | |
| } | |
| async function debugFromDiary(patchHash, btn) { | |
| if (btn) { btn.disabled = true; btn.textContent = 'Debugging...'; } | |
| try { | |
| const resp = await fetch('/debug?patch_hash=' + patchHash, { method: 'POST' }); | |
| const data = await resp.json(); | |
| if (data.error) { | |
| btn.textContent = 'Error'; | |
| } else { | |
| btn.style.background = 'rgba(251,191,36,.3)'; | |
| btn.textContent = '\u2713 Fixed'; | |
| setTimeout(() => loadDiary(), 1000); | |
| } | |
| } catch (e) { | |
| btn.textContent = 'Error'; | |
| } | |
| setTimeout(() => { | |
| if (btn) { btn.disabled = false; btn.textContent = '\u2691 Debug & Fix'; btn.style.background = ''; } | |
| }, 3000); | |
| } | |
| loadDiary(); | |
| </script> | |
| </body> | |
| </html> |