Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse files- Dockerfile +1 -1
- app.py +100 -6
- frontend/diary.html +311 -0
- 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
|
| 217 |
if not patch_hash:
|
| 218 |
return JSONResponse({"error": "patch_hash required"}, 400)
|
| 219 |
-
|
|
|
|
|
|
|
| 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 — computed artifacts with evidence, reasoning & audio</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
<a href="/" class="back-link">← 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|