josephrw commited on
Commit
f6eb2cb
·
verified ·
1 Parent(s): 05f68f1

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -1
  2. app.py +100 -6
  3. frontend/diary.html +311 -0
  4. frontend/index.html +83 -2
Dockerfile CHANGED
@@ -13,7 +13,7 @@ COPY app.py ./
13
  COPY src/ ./src/
14
  COPY frontend/ ./frontend/
15
 
16
- RUN mkdir -p /app/data/receipts
17
 
18
  ENV PORT=7860
19
  ENV PROVIDER=groq
 
13
  COPY src/ ./src/
14
  COPY frontend/ ./frontend/
15
 
16
+ RUN mkdir -p /app/data/receipts /app/data/artifacts /app/data/audio
17
 
18
  ENV PORT=7860
19
  ENV PROVIDER=groq
app.py CHANGED
@@ -11,9 +11,10 @@ import json
11
  import hashlib
12
  import subprocess
13
  import tempfile
 
14
  from datetime import datetime, timezone
15
 
16
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
17
  from fastapi.middleware.cors import CORSMiddleware
18
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
19
  from pathlib import Path
@@ -29,6 +30,11 @@ from src.code_receipts import save_receipt, list_receipts, get_receipt
29
 
30
  APP_ROOT = Path(__file__).resolve().parent
31
  FRONTEND_INDEX = APP_ROOT / "frontend" / "index.html"
 
 
 
 
 
32
 
33
  app = FastAPI(
34
  title="CSC Engine — Continuity Sensory Code Engine",
@@ -53,6 +59,35 @@ def index():
53
  return FileResponse(str(FRONTEND_INDEX))
54
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  @app.get("/health")
57
  def health():
58
  return {
@@ -128,9 +163,13 @@ artifact_store: dict[str, dict] = {}
128
 
129
  @app.post("/run")
130
  async def run_code(code: str = "", patch_hash: str = ""):
131
- """Execute Python code in a subprocess and return stdout/stderr."""
 
132
  if patch_hash and not code and patch_hash in artifact_store:
133
  code = artifact_store[patch_hash].get("code", "")
 
 
 
134
 
135
  if not code.strip():
136
  return JSONResponse({"error": "No code provided"}, 400)
@@ -168,10 +207,16 @@ async def run_code(code: str = "", patch_hash: str = ""):
168
  "timestamp": time.time(),
169
  }
170
 
 
171
  if patch_hash:
172
  if patch_hash not in artifact_store:
173
  artifact_store[patch_hash] = {"code": code}
174
  artifact_store[patch_hash].update(result)
 
 
 
 
 
175
 
176
  return result
177
 
@@ -179,7 +224,7 @@ async def run_code(code: str = "", patch_hash: str = ""):
179
  @app.get("/artifact/{patch_hash}", response_class=HTMLResponse)
180
  async def view_artifact(patch_hash: str):
181
  """Return an HTML page showing code + output, embeddable as iframe."""
182
- info = artifact_store.get(patch_hash, {})
183
  code = info.get("code", "")
184
  stdout = info.get("stdout", "")
185
  stderr = info.get("stderr", "")
@@ -212,14 +257,63 @@ async def view_artifact(patch_hash: str):
212
 
213
 
214
  @app.post("/store-artifact")
215
- async def store_artifact(patch_hash: str = "", code: str = ""):
216
- """Store generated code for later viewing/running via /artifact/{hash}."""
217
  if not patch_hash:
218
  return JSONResponse({"error": "patch_hash required"}, 400)
219
- artifact_store[patch_hash] = {"code": code, "timestamp": time.time()}
 
 
220
  return {"stored": True, "patch_hash": patch_hash}
221
 
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  @app.websocket("/ws/stream")
224
  async def ws_stream(ws: WebSocket):
225
  """Main WebSocket — receives frames + transcripts, runs the five loops."""
 
11
  import hashlib
12
  import subprocess
13
  import tempfile
14
+ import base64
15
  from datetime import datetime, timezone
16
 
17
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
18
  from fastapi.middleware.cors import CORSMiddleware
19
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
20
  from pathlib import Path
 
30
 
31
  APP_ROOT = Path(__file__).resolve().parent
32
  FRONTEND_INDEX = APP_ROOT / "frontend" / "index.html"
33
+ DATA_DIR = APP_ROOT / "data"
34
+ ARTIFACT_DIR = DATA_DIR / "artifacts"
35
+ AUDIO_DIR = DATA_DIR / "audio"
36
+ ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
37
+ AUDIO_DIR.mkdir(parents=True, exist_ok=True)
38
 
39
  app = FastAPI(
40
  title="CSC Engine — Continuity Sensory Code Engine",
 
59
  return FileResponse(str(FRONTEND_INDEX))
60
 
61
 
62
+ @app.get("/diary")
63
+ def diary():
64
+ """Lab diary page — notebook-style view of all artifacts."""
65
+ return FileResponse(str(APP_ROOT / "frontend" / "diary.html"))
66
+
67
+
68
+ def save_artifact_disk(patch_hash: str, data: dict):
69
+ """Persist artifact to disk."""
70
+ path = ARTIFACT_DIR / f"{patch_hash}.json"
71
+ path.write_text(json.dumps(data, indent=2, default=str))
72
+
73
+
74
+ def load_artifact_disk(patch_hash: str) -> dict:
75
+ """Load artifact from disk."""
76
+ path = ARTIFACT_DIR / f"{patch_hash}.json"
77
+ if not path.exists():
78
+ return {}
79
+ return json.loads(path.read_text())
80
+
81
+
82
+ def list_artifacts_disk(limit: int = 50) -> list:
83
+ """List recent artifacts from disk."""
84
+ files = sorted(ARTIFACT_DIR.glob("*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
85
+ artifacts = []
86
+ for f in files[:limit]:
87
+ artifacts.append(json.loads(f.read_text()))
88
+ return artifacts
89
+
90
+
91
  @app.get("/health")
92
  def health():
93
  return {
 
163
 
164
  @app.post("/run")
165
  async def run_code(code: str = "", patch_hash: str = ""):
166
+ """Execute Python code in a subprocess and return stdout/stderr.
167
+ Saves result to disk for the lab diary."""
168
  if patch_hash and not code and patch_hash in artifact_store:
169
  code = artifact_store[patch_hash].get("code", "")
170
+ elif patch_hash and not code:
171
+ disk = load_artifact_disk(patch_hash)
172
+ code = disk.get("code", "")
173
 
174
  if not code.strip():
175
  return JSONResponse({"error": "No code provided"}, 400)
 
207
  "timestamp": time.time(),
208
  }
209
 
210
+ # Store in memory + disk
211
  if patch_hash:
212
  if patch_hash not in artifact_store:
213
  artifact_store[patch_hash] = {"code": code}
214
  artifact_store[patch_hash].update(result)
215
+ # Persist to disk for diary
216
+ disk_data = load_artifact_disk(patch_hash)
217
+ disk_data.update({"code": code, "patch_hash": patch_hash})
218
+ disk_data.update(result)
219
+ save_artifact_disk(patch_hash, disk_data)
220
 
221
  return result
222
 
 
224
  @app.get("/artifact/{patch_hash}", response_class=HTMLResponse)
225
  async def view_artifact(patch_hash: str):
226
  """Return an HTML page showing code + output, embeddable as iframe."""
227
+ info = artifact_store.get(patch_hash) or load_artifact_disk(patch_hash)
228
  code = info.get("code", "")
229
  stdout = info.get("stdout", "")
230
  stderr = info.get("stderr", "")
 
257
 
258
 
259
  @app.post("/store-artifact")
260
+ async def store_artifact(patch_hash: str = "", code: str = "", reasoning: str = "", evidence: str = "", observer_output: str = ""):
261
+ """Store generated code + metadata for later viewing/running."""
262
  if not patch_hash:
263
  return JSONResponse({"error": "patch_hash required"}, 400)
264
+ data = {"code": code, "reasoning": reasoning, "evidence": evidence, "observer_output": observer_output, "timestamp": time.time(), "patch_hash": patch_hash}
265
+ artifact_store[patch_hash] = data
266
+ save_artifact_disk(patch_hash, data)
267
  return {"stored": True, "patch_hash": patch_hash}
268
 
269
 
270
+ @app.get("/api/artifacts")
271
+ async def list_artifacts(limit: int = 50):
272
+ """List all artifacts for the lab diary."""
273
+ return {"artifacts": list_artifacts_disk(limit=limit)}
274
+
275
+
276
+ @app.get("/api/artifacts/{patch_hash}")
277
+ async def get_artifact_api(patch_hash: str):
278
+ """Get a single artifact by hash."""
279
+ data = artifact_store.get(patch_hash) or load_artifact_disk(patch_hash)
280
+ if not data:
281
+ return JSONResponse({"error": "not found"}, 404)
282
+ return data
283
+
284
+
285
+ @app.post("/audio/upload")
286
+ async def upload_audio(patch_hash: str = ""):
287
+ """Receive audio recording and save to disk."""
288
+ from fastapi import Request
289
+ # This is called with raw body
290
+ pass
291
+
292
+
293
+ @app.post("/audio/store")
294
+ async def store_audio(patch_hash: str = "", audio_b64: str = ""):
295
+ """Store base64-encoded audio recording alongside an artifact."""
296
+ if not patch_hash or not audio_b64:
297
+ return JSONResponse({"error": "patch_hash and audio_b64 required"}, 400)
298
+ audio_path = AUDIO_DIR / f"{patch_hash}.webm"
299
+ audio_bytes = base64.b64decode(audio_b64)
300
+ audio_path.write_bytes(audio_bytes)
301
+ # Update artifact with audio reference
302
+ data = load_artifact_disk(patch_hash)
303
+ data["audio_path"] = str(audio_path.name)
304
+ save_artifact_disk(patch_hash, data)
305
+ return {"stored": True, "audio_file": audio_path.name}
306
+
307
+
308
+ @app.get("/audio/{filename}")
309
+ async def get_audio(filename: str):
310
+ """Serve an audio file."""
311
+ path = AUDIO_DIR / filename
312
+ if not path.exists():
313
+ return JSONResponse({"error": "not found"}, 404)
314
+ return FileResponse(str(path), media_type="audio/webm")
315
+
316
+
317
  @app.websocket("/ws/stream")
318
  async def ws_stream(ws: WebSocket):
319
  """Main WebSocket — receives frames + transcripts, runs the five loops."""
frontend/diary.html ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>CSC Engine — Lab Diary</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ :root {
9
+ font-family: 'SF Pro Display', system-ui, -apple-system, sans-serif;
10
+ --bg: #06080f; --panel: #0b0e18; --panel2: #0f1320;
11
+ --border: #1a1f2e; --border2: #232a3d;
12
+ --accent: #00d4ff; --accent2: #6c5ce7;
13
+ --green: #34d399; --amber: #fbbf24; --red: #f87171;
14
+ --gray: #6b7280; --text: #e2e8f0; --text2: #94a3b8;
15
+ --mono: 'SF Mono', 'JetBrains Mono', monospace;
16
+ }
17
+ * { box-sizing: border-box; margin: 0; padding: 0; }
18
+ body { background: var(--bg); color: var(--text); }
19
+ body::before {
20
+ content: ''; position: fixed; inset: 0; z-index: -1;
21
+ background: radial-gradient(ellipse at 20% 0%, rgba(0,212,255,.04), transparent 50%),
22
+ radial-gradient(ellipse at 80% 100%, rgba(108,92,231,.04), transparent 50%);
23
+ }
24
+ main { max-width: 1200px; margin: 0 auto; padding: 24px; }
25
+ header { display: flex; align-items: center; justify-content: space-between; padding: 8px 0 24px; }
26
+ .logo { display: flex; align-items: center; gap: 10px; }
27
+ .logo-mark {
28
+ width: 36px; height: 36px; border-radius: 8px;
29
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
30
+ display: flex; align-items: center; justify-content: center;
31
+ font-size: 18px; font-weight: 800; color: #000;
32
+ }
33
+ header h1 { font-size: 24px; font-weight: 700; }
34
+ header h1 span { color: var(--accent); }
35
+ .sub { color: var(--text2); font-size: 13px; margin-top: 2px; }
36
+ .back-link {
37
+ padding: 8px 16px; border-radius: 8px; font-size: 12px; font-weight: 600;
38
+ background: var(--panel2); color: var(--text2); border: 1px solid var(--border2);
39
+ text-decoration: none; transition: .15s;
40
+ }
41
+ .back-link:hover { background: #161b2d; color: var(--text); }
42
+
43
+ .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px; }
44
+ .stat {
45
+ background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
46
+ padding: 16px; text-align: center;
47
+ }
48
+ .stat strong { display: block; font-size: 28px; color: var(--accent); font-family: var(--mono); }
49
+ .stat small { color: var(--text2); font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
50
+
51
+ .entries { display: flex; flex-direction: column; gap: 16px; }
52
+ .entry {
53
+ background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
54
+ overflow: hidden; transition: .15s;
55
+ }
56
+ .entry:hover { border-color: var(--border2); }
57
+ .entry-header {
58
+ display: flex; align-items: center; gap: 12px;
59
+ padding: 14px 18px; border-bottom: 1px solid var(--border);
60
+ background: linear-gradient(180deg, var(--panel2), transparent);
61
+ }
62
+ .entry-num {
63
+ width: 28px; height: 28px; border-radius: 6px;
64
+ background: rgba(0,212,255,.1); color: var(--accent);
65
+ display: flex; align-items: center; justify-content: center;
66
+ font-size: 12px; font-weight: 700; font-family: var(--mono);
67
+ }
68
+ .entry-title { font-size: 14px; font-weight: 600; flex: 1; }
69
+ .entry-hash { font-size: 11px; color: var(--text2); font-family: var(--mono); }
70
+ .entry-time { font-size: 11px; color: var(--gray); font-family: var(--mono); }
71
+ .entry-body { padding: 18px; }
72
+
73
+ .entry-section { margin-bottom: 14px; }
74
+ .entry-section:last-child { margin-bottom: 0; }
75
+ .entry-label {
76
+ font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
77
+ margin-bottom: 6px;
78
+ }
79
+ .label-evidence { color: var(--accent); }
80
+ .label-reasoning { color: var(--amber); }
81
+ .label-code { color: var(--green); }
82
+ .label-output { color: #a78bfa; }
83
+ .label-audio { color: #f472b6; }
84
+
85
+ .entry-evidence { font-family: var(--mono); font-size: 12px; color: var(--text2); line-height: 1.6; }
86
+ .entry-evidence li { list-style: none; padding-left: 16px; position: relative; margin: 2px 0; }
87
+ .entry-evidence li::before { content: '\25B8'; position: absolute; left: 0; color: var(--accent); }
88
+
89
+ .entry-reasoning { font-family: var(--mono); font-size: 12px; color: var(--text2); line-height: 1.7; white-space: pre-wrap; }
90
+
91
+ .entry-code {
92
+ background: #060810; border: 1px solid var(--border); border-radius: 8px;
93
+ padding: 14px; font-family: var(--mono); font-size: 12px; line-height: 1.6;
94
+ color: #c8d3f5; overflow-x: auto;
95
+ }
96
+ .entry-output {
97
+ background: #04060c; border: 1px solid var(--border); border-radius: 8px;
98
+ padding: 14px; font-family: var(--mono); font-size: 12px; line-height: 1.6;
99
+ color: var(--green); overflow-x: auto;
100
+ }
101
+ .entry-output.err { color: var(--red); }
102
+
103
+ .entry-actions { display: flex; gap: 8px; margin-top: 12px; }
104
+ .entry-btn {
105
+ padding: 6px 14px; border-radius: 6px; border: none; cursor: pointer;
106
+ font-size: 11px; font-weight: 600; transition: .15s; text-decoration: none;
107
+ display: inline-flex; align-items: center; gap: 4px;
108
+ }
109
+ .btn-run { background: rgba(52,211,153,.15); color: var(--green); border: 1px solid rgba(52,211,153,.3); }
110
+ .btn-run:hover { background: rgba(52,211,153,.25); }
111
+ .btn-view { background: rgba(0,212,255,.1); color: var(--accent); border: 1px solid rgba(0,212,255,.2); }
112
+ .btn-view:hover { background: rgba(0,212,255,.2); }
113
+
114
+ .entry-audio { margin-top: 10px; }
115
+ .entry-audio audio { width: 100%; height: 36px; }
116
+
117
+ .empty { text-align: center; padding: 60px; color: var(--text2); font-family: var(--mono); }
118
+ .empty h2 { font-size: 18px; margin-bottom: 8px; color: var(--text); }
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <main>
123
+ <header>
124
+ <div class="logo">
125
+ <div class="logo-mark">C</div>
126
+ <div>
127
+ <h1>Lab <span>Diary</span></h1>
128
+ <div class="sub">AI Notebook &mdash; computed artifacts with evidence, reasoning &amp; audio</div>
129
+ </div>
130
+ </div>
131
+ <a href="/" class="back-link">&larr; Back to Engine</a>
132
+ </header>
133
+
134
+ <div class="stats">
135
+ <div class="stat"><strong id="totalCount">0</strong><small>Artifacts</small></div>
136
+ <div class="stat"><strong id="runCount">0</strong><small>Executed</small></div>
137
+ <div class="stat"><strong id="audioCount">0</strong><small>With Audio</small></div>
138
+ <div class="stat"><strong id="successCount">0</strong><small>Exit 0</small></div>
139
+ </div>
140
+
141
+ <div class="entries" id="entries">
142
+ <div class="empty">
143
+ <h2>Loading artifacts...</h2>
144
+ <p>Fetching from /api/artifacts</p>
145
+ </div>
146
+ </div>
147
+ </main>
148
+
149
+ <script>
150
+ function escapeHtml(text) {
151
+ const div = document.createElement("div");
152
+ div.textContent = text || "";
153
+ return div.innerHTML;
154
+ }
155
+
156
+ function formatTime(ts) {
157
+ if (!ts) return "unknown";
158
+ const d = new Date(ts * 1000);
159
+ return d.toLocaleString();
160
+ }
161
+
162
+ function parseSections(text) {
163
+ const sections = {};
164
+ const patterns = ['REASONING','SCENE','INFERRED_INTENT','INTENT','SIGNALS','CANDIDATE_TASK','UNCERTAINTY','EVIDENCE','CODE','RUN','TEST','ATTRIBUTION'];
165
+ const patStr = patterns.join('|');
166
+ for (const pat of patterns) {
167
+ const regex = new RegExp(pat + ":?\\s*([\\s\\S]*?)(?=\\n\\s*(?:" + patStr + "):|\\n\\s*(?:INSUFFICIENT)|$)", 'i');
168
+ const m = text.match(regex);
169
+ if (m && m[1].trim()) sections[pat] = m[1].trim();
170
+ }
171
+ return sections;
172
+ }
173
+
174
+ async function loadDiary() {
175
+ try {
176
+ const resp = await fetch('/api/artifacts?limit=50');
177
+ const data = await resp.json();
178
+ const artifacts = data.artifacts || [];
179
+ renderEntries(artifacts);
180
+ } catch(e) {
181
+ document.getElementById('entries').innerHTML = '<div class="empty"><h2>Error</h2><p>' + escapeHtml(e.message) + '</p></div>';
182
+ }
183
+ }
184
+
185
+ function renderEntries(artifacts) {
186
+ const container = document.getElementById('entries');
187
+ let runCount = 0, audioCount = 0, successCount = 0;
188
+
189
+ if (artifacts.length === 0) {
190
+ container.innerHTML = '<div class="empty"><h2>No artifacts yet</h2><p>Start the engine and generate code to populate the diary.</p></div>';
191
+ document.getElementById('totalCount').textContent = '0';
192
+ document.getElementById('runCount').textContent = '0';
193
+ document.getElementById('audioCount').textContent = '0';
194
+ document.getElementById('successCount').textContent = '0';
195
+ return;
196
+ }
197
+
198
+ let html = '';
199
+ artifacts.forEach((art, i) => {
200
+ const num = artifacts.length - i;
201
+ const hash = art.patch_hash || 'unknown';
202
+ const hasRun = art.exit_code !== undefined;
203
+ const hasAudio = !!art.audio_path;
204
+ const success = art.exit_code === 0;
205
+ if (hasRun) runCount++;
206
+ if (hasAudio) audioCount++;
207
+ if (success) successCount++;
208
+
209
+ // Parse sections from observer_output if available
210
+ const sections = parseSections(art.observer_output || '');
211
+ const evidence = art.evidence || sections.EVIDENCE || '';
212
+ const reasoning = art.reasoning || sections.REASONING || '';
213
+ const code = art.code || sections.CODE || '';
214
+ const stdout = art.stdout || '';
215
+ const stderr = art.stderr || '';
216
+
217
+ html += '<div class="entry">';
218
+ html += '<div class="entry-header">';
219
+ html += '<div class="entry-num">' + num + '</div>';
220
+ html += '<div class="entry-title">Artifact ' + hash.slice(0, 12) + '</div>';
221
+ html += '<div class="entry-hash">' + hash.slice(0, 16) + '</div>';
222
+ html += '<div class="entry-time">' + formatTime(art.timestamp) + '</div>';
223
+ html += '</div>';
224
+ html += '<div class="entry-body">';
225
+
226
+ if (evidence) {
227
+ html += '<div class="entry-section">';
228
+ html += '<div class="entry-label label-evidence">\u258C Evidence</div>';
229
+ html += '<ul class="entry-evidence">';
230
+ for (const line of evidence.split('\n').filter(l => l.trim())) {
231
+ html += '<li>' + escapeHtml(line.replace(/^[-\u2022*]\s*/, '')) + '</li>';
232
+ }
233
+ html += '</ul></div>';
234
+ }
235
+
236
+ if (reasoning) {
237
+ html += '<div class="entry-section">';
238
+ html += '<div class="entry-label label-reasoning">\u258C Reasoning</div>';
239
+ html += '<div class="entry-reasoning">' + escapeHtml(reasoning) + '</div>';
240
+ html += '</div>';
241
+ }
242
+
243
+ if (code) {
244
+ html += '<div class="entry-section">';
245
+ html += '<div class="entry-label label-code">\u258C Code</div>';
246
+ html += '<pre class="entry-code">' + escapeHtml(code) + '</pre>';
247
+ html += '</div>';
248
+ }
249
+
250
+ if (stdout) {
251
+ html += '<div class="entry-section">';
252
+ html += '<div class="entry-label label-output">\u258C Output (exit ' + art.exit_code + ')</div>';
253
+ html += '<pre class="entry-output">' + escapeHtml(stdout) + '</pre>';
254
+ html += '</div>';
255
+ }
256
+ if (stderr) {
257
+ html += '<div class="entry-section">';
258
+ html += '<div class="entry-label label-output" style="color:var(--red)">\u258C Errors</div>';
259
+ html += '<pre class="entry-output err">' + escapeHtml(stderr) + '</pre>';
260
+ html += '</div>';
261
+ }
262
+
263
+ if (hasAudio) {
264
+ html += '<div class="entry-section">';
265
+ html += '<div class="entry-label label-audio">\u258C Audio Recording</div>';
266
+ html += '<div class="entry-audio"><audio controls src="/audio/' + art.audio_path + '"></audio></div>';
267
+ html += '</div>';
268
+ }
269
+
270
+ html += '<div class="entry-actions">';
271
+ html += '<button class="entry-btn btn-run" onclick="runFromDiary(\'' + hash + '\', this)">\u25B6 Run</button>';
272
+ html += '<a class="entry-btn btn-view" href="/artifact/' + hash + '" target="_blank">View Artifact</a>';
273
+ html += '</div>';
274
+
275
+ html += '</div></div>';
276
+ });
277
+
278
+ container.innerHTML = html;
279
+ document.getElementById('totalCount').textContent = artifacts.length;
280
+ document.getElementById('runCount').textContent = runCount;
281
+ document.getElementById('audioCount').textContent = audioCount;
282
+ document.getElementById('successCount').textContent = successCount;
283
+ }
284
+
285
+ async function runFromDiary(patchHash, btn) {
286
+ if (btn) { btn.disabled = true; btn.textContent = 'Running...'; }
287
+ try {
288
+ const resp = await fetch('/run?patch_hash=' + patchHash, { method: 'POST' });
289
+ const data = await resp.json();
290
+ if (data.exit_code === 0) {
291
+ btn.style.background = 'rgba(52,211,153,.3)';
292
+ btn.textContent = '\u2713 Success';
293
+ } else {
294
+ btn.style.background = 'rgba(248,113,113,.2)';
295
+ btn.style.color = 'var(--red)';
296
+ btn.textContent = '\u2717 Exit ' + data.exit_code;
297
+ }
298
+ // Reload diary after a moment
299
+ setTimeout(() => loadDiary(), 500);
300
+ } catch(e) {
301
+ btn.textContent = 'Error';
302
+ }
303
+ setTimeout(() => {
304
+ if (btn) { btn.disabled = false; btn.textContent = '\u25B6 Run'; btn.style.background = ''; btn.style.color = ''; }
305
+ }, 3000);
306
+ }
307
+
308
+ loadDiary();
309
+ </script>
310
+ </body>
311
+ </html>
frontend/index.html CHANGED
@@ -739,6 +739,20 @@
739
  </svg>
740
  Force Generate
741
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  <div class="ctrl-group">
743
  <label>Mode</label>
744
  <select id="mode">
@@ -819,6 +833,7 @@
819
 
820
  let stream = null, ws = null, frameTimer = null, recognition = null, sessionId = null;
821
  let patchCount = 0;
 
822
 
823
  function setStatus(msg, dotClass) {
824
  statusEl.innerHTML = '<span class="status-dot ' + (dotClass || 'dot-idle') + '"></span>' + msg;
@@ -874,11 +889,18 @@
874
  patchCount++;
875
  document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
876
  setStatus("Code patch generated", "dot-active");
877
- // Store code on server for /artifact/{hash} and /run
878
  const sections = parseSections(msg.output);
879
  const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
880
  if (codeText && msg.patch_hash) {
881
- fetch("/store-artifact?patch_hash=" + msg.patch_hash + "&code=" + encodeURIComponent(codeText), { method: "POST" }).catch(() => { });
 
 
 
 
 
 
 
882
  }
883
  break;
884
  case "error":
@@ -1171,6 +1193,65 @@
1171
  document.getElementById("stopBtn").onclick = stopLive;
1172
  document.getElementById("speechBtn").onclick = startSpeech;
1173
  document.getElementById("forceBtn").onclick = () => sendFrame(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1174
  </script>
1175
  </body>
1176
 
 
739
  </svg>
740
  Force Generate
741
  </button>
742
+ <button id="recBtn" class="btn btn-ghost"
743
+ style="background:rgba(248,113,113,.1);color:var(--red);border-color:rgba(248,113,113,.2)">
744
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
745
+ <circle cx="12" cy="12" r="5" />
746
+ </svg>
747
+ Record Audio
748
+ </button>
749
+ <a href="/diary" class="btn btn-ghost" style="text-decoration:none">
750
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
751
+ <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
752
+ <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
753
+ </svg>
754
+ Lab Diary
755
+ </a>
756
  <div class="ctrl-group">
757
  <label>Mode</label>
758
  <select id="mode">
 
833
 
834
  let stream = null, ws = null, frameTimer = null, recognition = null, sessionId = null;
835
  let patchCount = 0;
836
+ let mediaRecorder = null, audioChunks = [], currentAudioHash = null, audioStream = null;
837
 
838
  function setStatus(msg, dotClass) {
839
  statusEl.innerHTML = '<span class="status-dot ' + (dotClass || 'dot-idle') + '"></span>' + msg;
 
889
  patchCount++;
890
  document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
891
  setStatus("Code patch generated", "dot-active");
892
+ // Store code + reasoning + evidence on server for diary
893
  const sections = parseSections(msg.output);
894
  const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
895
  if (codeText && msg.patch_hash) {
896
+ const reasoningText = sections.REASONING || "";
897
+ const evidenceText = sections.EVIDENCE || "";
898
+ fetch("/store-artifact?patch_hash=" + msg.patch_hash + "&code=" + encodeURIComponent(codeText) + "&reasoning=" + encodeURIComponent(reasoningText) + "&evidence=" + encodeURIComponent(evidenceText) + "&observer_output=" + encodeURIComponent(msg.output), { method: "POST" }).catch(() => { });
899
+ // If we have a pending audio recording, attach it
900
+ if (currentAudioHash === null && audioChunks.length > 0) {
901
+ currentAudioHash = msg.patch_hash;
902
+ finishAudioUpload(msg.patch_hash);
903
+ }
904
  }
905
  break;
906
  case "error":
 
1193
  document.getElementById("stopBtn").onclick = stopLive;
1194
  document.getElementById("speechBtn").onclick = startSpeech;
1195
  document.getElementById("forceBtn").onclick = () => sendFrame(true);
1196
+
1197
+ // Audio Recording with MediaRecorder
1198
+ document.getElementById("recBtn").onclick = toggleRecording;
1199
+
1200
+ function toggleRecording() {
1201
+ if (mediaRecorder && mediaRecorder.state === "recording") {
1202
+ mediaRecorder.stop();
1203
+ document.getElementById("recBtn").style.background = "rgba(248,113,113,.1)";
1204
+ document.getElementById("recBtn").innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>Record Audio';
1205
+ document.getElementById("audioStatus").textContent = "processing";
1206
+ } else {
1207
+ startAudioRecording();
1208
+ }
1209
+ }
1210
+
1211
+ async function startAudioRecording() {
1212
+ try {
1213
+ audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
1214
+ audioChunks = [];
1215
+ currentAudioHash = null;
1216
+ mediaRecorder = new MediaRecorder(audioStream);
1217
+ mediaRecorder.ondataavailable = (e) => {
1218
+ if (e.data.size > 0) audioChunks.push(e.data);
1219
+ };
1220
+ mediaRecorder.onstop = () => {
1221
+ if (audioStream) { audioStream.getTracks().forEach(t => t.stop()); audioStream = null; }
1222
+ // If we already have a patch hash, upload now
1223
+ if (currentAudioHash) {
1224
+ finishAudioUpload(currentAudioHash);
1225
+ }
1226
+ };
1227
+ mediaRecorder.start();
1228
+ const btn = document.getElementById("recBtn");
1229
+ btn.style.background = "rgba(248,113,113,.25)";
1230
+ btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12"/></svg>Stop Recording';
1231
+ document.getElementById("audioStatus").textContent = "recording";
1232
+ setStatus("Audio recording started", "dot-active");
1233
+ } catch (e) {
1234
+ setStatus("Audio recording error: " + e.message, "dot-error");
1235
+ }
1236
+ }
1237
+
1238
+ function finishAudioUpload(patchHash) {
1239
+ if (audioChunks.length === 0) return;
1240
+ const blob = new Blob(audioChunks, { type: "audio/webm" });
1241
+ const reader = new FileReader();
1242
+ reader.onloadend = () => {
1243
+ const b64 = reader.result.split(",")[1];
1244
+ if (b64) {
1245
+ fetch("/audio/store?patch_hash=" + patchHash + "&audio_b64=" + encodeURIComponent(b64), { method: "POST" })
1246
+ .then(() => {
1247
+ audioChunks = [];
1248
+ document.getElementById("audioStatus").textContent = "stored";
1249
+ })
1250
+ .catch(() => { });
1251
+ }
1252
+ };
1253
+ reader.readAsDataURL(blob);
1254
+ }
1255
  </script>
1256
  </body>
1257