csc-engine / frontend /index_old.html
josephrw's picture
Upload folder using huggingface_hub
fd4fad1 verified
Raw
History Blame Contribute Delete
72.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>CSC Engine — Continuity Sensory Code Engine</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
font-family: 'SF Pro Display', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", 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', 'Fira Code', 'Cascadia Code', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: var(--bg);
color: var(--text);
overflow-x: hidden;
}
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: 1800px;
margin: 0 auto;
padding: 16px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0 16px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-mark {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 800;
color: #000;
}
header h1 {
font-size: 20px;
font-weight: 700;
letter-spacing: -.3px;
}
header h1 span {
color: var(--accent);
}
.sub {
color: var(--text2);
font-size: 12px;
margin-top: 1px;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: var(--panel2);
border: 1px solid var(--border2);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
}
.dot-idle {
background: var(--gray);
}
.dot-active {
background: var(--green);
box-shadow: 0 0 6px var(--green);
}
.dot-error {
background: var(--red);
box-shadow: 0 0 6px var(--red);
}
.dot-thinking {
background: var(--amber);
box-shadow: 0 0 6px var(--amber);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: .4;
}
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
}
.btn {
padding: 8px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: .15s;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: linear-gradient(135deg, #1f6feb, #388bfd);
color: #fff;
}
.btn-primary:hover {
filter: brightness(1.15);
}
.btn-ghost {
background: var(--panel2);
color: var(--text2);
border: 1px solid var(--border2);
}
.btn-ghost:hover {
background: #161b2d;
color: var(--text);
}
.btn-danger {
background: rgba(248, 113, 113, .15);
color: var(--red);
border: 1px solid rgba(248, 113, 113, .3);
}
.btn-danger:hover {
background: rgba(248, 113, 113, .25);
}
.btn svg {
width: 14px;
height: 14px;
}
.ctrl-group {
display: flex;
align-items: center;
gap: 6px;
}
.ctrl-group label {
font-size: 11px;
color: var(--text2);
text-transform: uppercase;
letter-spacing: .5px;
}
.ctrl-group select,
.ctrl-group input {
background: var(--panel2);
color: var(--text);
border: 1px solid var(--border2);
border-radius: 6px;
padding: 6px 10px;
font-size: 12px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
@media (max-width: 1100px) {
.grid {
grid-template-columns: 1fr;
}
}
.pane {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pane-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, var(--panel2), transparent);
}
.pane-title {
font-size: 11px;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 1.2px;
display: flex;
align-items: center;
gap: 6px;
}
.pane-title .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
.pane-body {
padding: 14px;
overflow-y: auto;
max-height: 580px;
flex: 1;
}
.pane-body::-webkit-scrollbar {
width: 6px;
}
.pane-body::-webkit-scrollbar-track {
background: transparent;
}
.pane-body::-webkit-scrollbar-thumb {
background: var(--border2);
border-radius: 3px;
}
video,
canvas {
width: 100%;
border-radius: 8px;
border: 1px solid var(--border);
background: #000;
}
canvas {
display: none;
}
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
margin-top: 10px;
}
.metric {
background: var(--panel2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 8px;
text-align: center;
}
.metric strong {
display: block;
font-size: 16px;
color: #fff;
font-family: var(--mono);
}
.metric small {
color: var(--text2);
font-size: 9px;
text-transform: uppercase;
letter-spacing: .5px;
}
.qvd-bar {
height: 4px;
background: var(--panel2);
border-radius: 2px;
margin-top: 10px;
overflow: hidden;
position: relative;
}
.qvd-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent2), var(--accent));
transition: width .4s;
border-radius: 2px;
}
.qvd-label {
font-size: 10px;
color: var(--text2);
margin-top: 4px;
font-family: var(--mono);
}
.speaker-entry {
padding: 8px 12px;
margin: 4px 0;
border-radius: 8px;
font-size: 12px;
border-left: 3px solid;
animation: slideIn .3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.speaker-user {
background: rgba(31, 111, 235, .08);
border-color: #1f6feb;
}
.speaker-background {
background: rgba(248, 113, 113, .08);
border-color: var(--red);
}
.speaker-unknown {
background: rgba(107, 114, 128, .08);
border-color: var(--gray);
}
.speaker-entry .label {
font-size: 9px;
color: var(--text2);
text-transform: uppercase;
letter-spacing: .5px;
margin-bottom: 2px;
}
.speaker-entry .text {
color: var(--text);
font-family: var(--mono);
font-size: 11px;
}
.observer-trace {
font-family: var(--mono);
font-size: 11px;
line-height: 1.7;
color: var(--text2);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.observer-trace .section-header {
color: var(--accent);
font-weight: 700;
margin-top: 8px;
}
.observer-trace .reasoning {
color: var(--amber);
font-style: italic;
}
.observer-trace .intent {
color: var(--green);
}
.ide-patch {
background: #080a12;
border: 1px solid var(--border2);
border-radius: 10px;
margin: 8px 0;
overflow: hidden;
animation: slideIn .4s ease;
}
.ide-titlebar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #0c0f1a;
border-bottom: 1px solid var(--border);
}
.ide-dots {
display: flex;
gap: 5px;
}
.ide-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.ide-dot.r {
background: #ff5f57;
}
.ide-dot.y {
background: #febc2e;
}
.ide-dot.g {
background: #28c840;
}
.ide-filename {
font-size: 11px;
color: var(--text2);
font-family: var(--mono);
margin-left: 4px;
}
.ide-meta {
margin-left: auto;
display: flex;
gap: 10px;
font-size: 10px;
color: var(--text2);
font-family: var(--mono);
}
.ide-meta span {
color: var(--accent);
}
.ide-body {
padding: 0;
}
.ide-section {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
}
.ide-section:last-child {
border-bottom: none;
}
.ide-section-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 4px;
}
.label-evidence {
color: var(--accent);
}
.label-reasoning {
color: var(--amber);
}
.label-code {
color: var(--green);
}
.label-run {
color: #a78bfa;
}
.label-test {
color: #f472b6;
}
.ide-evidence {
font-family: var(--mono);
font-size: 11px;
color: var(--text2);
line-height: 1.6;
}
.ide-evidence li {
margin: 2px 0;
list-style: none;
padding-left: 16px;
position: relative;
}
.ide-evidence li::before {
content: '\25B8';
position: absolute;
left: 0;
color: var(--accent);
}
.ide-reasoning {
font-family: var(--mono);
font-size: 11px;
color: var(--text2);
line-height: 1.7;
white-space: pre-wrap;
}
.ide-code {
font-family: var(--mono);
font-size: 12px;
line-height: 1.6;
color: #c8d3f5;
background: #060810;
padding: 12px 14px;
overflow-x: auto;
}
.ide-code .kw {
color: #c792ea;
}
.ide-code .str {
color: #c3e88d;
}
.ide-code .num {
color: #f78c6c;
}
.ide-code .com {
color: #546e7a;
font-style: italic;
}
.ide-code .fn {
color: #82aaff;
}
.ide-run,
.ide-test {
font-family: var(--mono);
font-size: 11px;
color: var(--text);
padding: 8px 14px;
background: #0a0d18;
display: flex;
align-items: center;
gap: 8px;
}
.ide-run .prompt {
color: var(--green);
}
.ide-test .prompt {
color: #f472b6;
}
.empty-state {
color: var(--text2);
font-size: 12px;
padding: 24px;
text-align: center;
font-family: var(--mono);
}
/* Terminal output */
.ide-terminal {
background: #04060c;
border-top: 1px solid var(--border);
padding: 10px 14px;
font-family: var(--mono);
font-size: 11px;
line-height: 1.6;
max-height: 200px;
overflow-y: auto;
}
.ide-terminal .term-line {
color: var(--text2);
}
.ide-terminal .term-stdout {
color: var(--green);
}
.ide-terminal .term-stderr {
color: var(--red);
}
.ide-terminal .term-meta {
color: var(--accent);
font-size: 10px;
margin-bottom: 4px;
}
.ide-terminal .term-prompt {
color: var(--green);
}
/* Run button in titlebar */
.ide-run-btn {
padding: 3px 10px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 10px;
font-weight: 700;
background: rgba(52, 211, 153, .15);
color: var(--green);
border: 1px solid rgba(52, 211, 153, .3);
transition: .15s;
margin-left: auto;
}
.ide-run-btn:hover {
background: rgba(52, 211, 153, .25);
}
.ide-run-btn:disabled {
opacity: .4;
cursor: wait;
}
.ide-artifact-link {
padding: 3px 10px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 10px;
font-weight: 600;
background: rgba(0, 212, 255, .1);
color: var(--accent);
border: 1px solid rgba(0, 212, 255, .2);
text-decoration: none;
margin-left: 6px;
}
.ide-artifact-link:hover {
background: rgba(0, 212, 255, .2);
}
/* ─── Terminal Dock ─── */
.terminal-dock {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 280px;
background: var(--panel);
border-top: 1px solid var(--border2);
z-index: 100;
display: flex;
flex-direction: column;
transition: height .2s ease;
}
.terminal-dock.collapsed {
height: 32px;
}
.terminal-dock-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 12px;
height: 32px;
flex-shrink: 0;
background: var(--panel2);
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.terminal-dock-tabs {
display: flex;
gap: 2px;
}
.term-tab {
padding: 3px 10px;
border-radius: 4px 4px 0 0;
font-size: 11px;
font-family: var(--mono);
cursor: pointer;
border: none;
background: none;
color: var(--text2);
transition: .15s;
}
.term-tab.active {
background: var(--panel);
color: var(--accent);
}
.term-tab:hover {
color: var(--text);
}
.term-tab-close {
margin-left: 4px;
opacity: .5;
}
.term-tab-close:hover {
opacity: 1;
}
.terminal-dock-body {
flex: 1;
overflow: hidden;
display: flex;
}
.terminal-output {
flex: 1;
overflow-y: auto;
padding: 8px 12px;
font-family: var(--mono);
font-size: 12px;
line-height: 1.5;
color: #c8d3f5;
white-space: pre-wrap;
word-break: break-all;
}
.terminal-output .term-line {
min-height: 18px;
}
.terminal-output .term-prompt {
color: var(--accent);
}
.terminal-output .term-err {
color: var(--red);
}
.terminal-output .term-out {
color: var(--green);
}
.terminal-input-row {
display: flex;
align-items: center;
padding: 4px 12px;
gap: 8px;
border-top: 1px solid var(--border);
background: var(--panel2);
}
.terminal-input-row .prompt {
color: var(--accent);
font-family: var(--mono);
font-size: 12px;
}
.terminal-input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
}
.terminal-side {
width: 200px;
border-left: 1px solid var(--border);
padding: 8px;
overflow-y: auto;
font-size: 11px;
flex-shrink: 0;
}
.terminal-side h4 {
font-size: 10px;
color: var(--text2);
text-transform: uppercase;
margin-bottom: 6px;
}
.terminal-side .ts-row {
padding: 3px 0;
color: var(--text2);
display: flex;
justify-content: space-between;
}
.terminal-side .ts-row .ts-val {
color: var(--text);
font-family: var(--mono);
}
.terminal-side .ts-btn {
width: 100%;
padding: 4px;
margin-top: 4px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--text2);
font-size: 10px;
cursor: pointer;
text-align: center;
}
.terminal-side .ts-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* ─── Command Palette ─── */
.cmd-palette {
position: fixed;
top: 20%;
left: 50%;
transform: translateX(-50%);
width: 520px;
max-width: 90vw;
z-index: 200;
background: var(--panel);
border: 1px solid var(--border2);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
display: none;
}
.cmd-palette.open {
display: block;
}
.cmd-palette input {
width: 100%;
padding: 14px 16px;
background: none;
border: none;
outline: none;
color: var(--text);
font-size: 15px;
border-bottom: 1px solid var(--border);
}
.cmd-palette-results {
max-height: 320px;
overflow-y: auto;
}
.cmd-item {
padding: 10px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid var(--border);
transition: .1s;
}
.cmd-item:hover,
.cmd-item.selected {
background: var(--panel2);
}
.cmd-item .cmd-icon {
width: 20px;
color: var(--accent);
font-size: 14px;
}
.cmd-item .cmd-label {
font-size: 13px;
color: var(--text);
}
.cmd-item .cmd-desc {
font-size: 11px;
color: var(--text2);
margin-left: auto;
}
.cmd-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .4);
z-index: 199;
display: none;
}
.cmd-overlay.open {
display: block;
}
/* ─── API Explorer Panel ─── */
.api-panel {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 420px;
background: var(--panel);
border-left: 1px solid var(--border2);
z-index: 150;
transform: translateX(100%);
transition: transform .25s ease;
display: flex;
flex-direction: column;
}
.api-panel.open {
transform: translateX(0);
}
.api-panel-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.api-panel-header h3 {
font-size: 13px;
color: var(--accent);
}
.api-panel-body {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.api-endpoint {
padding: 10px;
margin-bottom: 8px;
border-radius: 8px;
background: var(--panel2);
border: 1px solid var(--border);
cursor: pointer;
transition: .15s;
}
.api-endpoint:hover {
border-color: var(--accent);
}
.api-method {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 3px;
}
.api-method.GET {
background: rgba(52, 211, 153, .15);
color: var(--green);
}
.api-method.POST {
background: rgba(0, 212, 255, .15);
color: var(--accent);
}
.api-path {
font-family: var(--mono);
font-size: 12px;
color: var(--text);
margin-left: 8px;
}
.api-desc {
font-size: 11px;
color: var(--text2);
margin-top: 4px;
}
.api-try-btn {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--text2);
cursor: pointer;
margin-top: 6px;
}
.api-try-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.api-response {
margin-top: 8px;
padding: 8px;
border-radius: 6px;
background: var(--bg);
border: 1px solid var(--border);
font-family: var(--mono);
font-size: 11px;
color: var(--green);
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
display: none;
}
.api-response.open {
display: block;
}
/* ─── Toolbar buttons ─── */
.toolbar-btn {
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--text2);
font-size: 11px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: .15s;
}
.toolbar-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.kbd {
padding: 1px 5px;
border-radius: 3px;
background: var(--panel2);
border: 1px solid var(--border);
font-size: 10px;
font-family: var(--mono);
color: var(--text2);
}
</style>
</head>
<body>
<main>
<header>
<div class="logo">
<div class="logo-mark">C</div>
<div>
<h1>CSC <span>Engine</span></h1>
<div class="sub">Continuity Sensory Code Engine &mdash; evidence &rarr; reasoning &rarr; code</div>
</div>
</div>
<div class="status-pill" id="status"><span class="status-dot dot-idle"></span>Idle</div>
<div style="display:flex;gap:6px;margin-left:8px">
<button class="toolbar-btn" onclick="openCmdPalette()" title="Command Palette">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 21l-4.35-4.35" />
<circle cx="11" cy="11" r="8" />
</svg>
<span class="kbd">⌘K</span>
</button>
<button class="toolbar-btn" onclick="toggleApiPanel()" title="API Explorer">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
API
</button>
<button class="toolbar-btn" onclick="toggleTerminalDock()" title="Terminal">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
Terminal
</button>
</div>
</header>
<div class="controls">
<button id="startBtn" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
Start Engine
</button>
<button id="stopBtn" class="btn btn-danger">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="6" width="12" height="12" />
</svg>
Stop
</button>
<button id="speechBtn" class="btn btn-ghost">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" y1="19" x2="12" y2="23" />
</svg>
Start Speech
</button>
<button id="forceBtn" class="btn btn-ghost">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
Force Generate
</button>
<button id="recBtn" class="btn btn-ghost"
style="background:rgba(248,113,113,.1);color:var(--red);border-color:rgba(248,113,113,.2)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5" />
</svg>
Record Audio
</button>
<a href="/diary" class="btn btn-ghost" style="text-decoration:none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
Lab Diary
</a>
<div class="ctrl-group">
<label>Mode</label>
<select id="mode">
<option value="continuous_code">continuous_code</option>
<option value="debug_visible_error">debug_visible_error</option>
<option value="derive_pipeline">derive_pipeline</option>
<option value="research_prism">research_prism</option>
</select>
</div>
<div class="ctrl-group">
<label>Camera</label>
<select id="camSelect" style="width:140px">
<option value="">Loading...</option>
</select>
</div>
<div class="ctrl-group">
<label>Video</label>
<input type="file" id="videoFile" accept="video/*" style="width:120px;font-size:10px" />
</div>
<div class="ctrl-group">
<label>Frame ms</label>
<input id="frameMs" value="800" style="width:55px" />
</div>
<div class="ctrl-group">
<label>Auto-Run</label>
<input type="checkbox" id="autoRun" checked />
</div>
</div>
<div class="grid">
<div class="pane">
<div class="pane-header">
<div class="pane-title"><span class="dot"></span>Camera Stream</div>
<div style="font-size:10px;color:var(--text2);font-family:var(--mono)" id="camStatus">off</div>
</div>
<div class="pane-body">
<video id="video" autoplay playsinline muted></video>
<canvas id="canvas"></canvas>
<div class="metrics">
<div class="metric"><strong id="entropy">0.0</strong><small>entropy</small></div>
<div class="metric"><strong id="motion">0.0</strong><small>motion</small></div>
<div class="metric"><strong id="fps">1.0</strong><small>fps</small></div>
<div class="metric"><strong id="buffered">0</strong><small>frames</small></div>
</div>
<div class="qvd-bar">
<div id="qvdFill" class="qvd-fill" style="width:0%"></div>
</div>
<div class="qvd-label">QVD: <span id="qvdVal">0.000</span> / threshold <span id="qvdThreshold">0.30</span>
&middot; novelty <span id="noveltyVal">0.000</span></div>
</div>
</div>
<div class="pane">
<div class="pane-header">
<div class="pane-title"><span class="dot"></span>Audio / Speaker Ledger</div>
<div style="font-size:10px;color:var(--text2);font-family:var(--mono)" id="audioStatus">silent</div>
</div>
<div class="pane-body" id="speakerLedger">
<div class="empty-state">No audio yet. Start speech to feed evidence.</div>
</div>
</div>
<div class="pane">
<div class="pane-header">
<div class="pane-title"><span class="dot"></span>Observer &mdash; Live Reasoning</div>
<div style="font-size:10px;color:var(--text2);font-family:var(--mono)" id="observerStatus">waiting</div>
</div>
<div class="pane-body" id="observerPane">
<div class="empty-state">Observer will reason about what the camera sees and audio hears.</div>
</div>
</div>
<div class="pane">
<div class="pane-header">
<div class="pane-title"><span class="dot"></span>Code Patches &mdash; Micro IDE</div>
<div style="font-size:10px;color:var(--text2);font-family:var(--mono)" id="patchCount">0 patches</div>
</div>
<div class="pane-body" id="patchStream">
<div class="empty-state">No patches yet. Camera movement or sound will trigger code generation.</div>
</div>
</div>
</div>
</main>
<!-- Command Palette Overlay -->
<div class="cmd-overlay" id="cmdOverlay" onclick="closeCmdPalette()"></div>
<div class="cmd-palette" id="cmdPalette">
<input type="text" id="cmdInput" placeholder="Search commands..." oninput="filterCmds()"
onkeydown="handleCmdKey(event)" />
<div class="cmd-palette-results" id="cmdResults"></div>
</div>
<!-- API Explorer Panel -->
<div class="api-panel" id="apiPanel">
<div class="api-panel-header">
<h3>API Explorer</h3>
<button class="toolbar-btn" onclick="toggleApiPanel()"></button>
</div>
<div class="api-panel-body" id="apiPanelBody">
<div style="font-size:11px;color:var(--text2);margin-bottom:12px">
ChatGPT Integration: <a href="/gpt-actions.json" target="_blank"
style="color:var(--accent)">/gpt-actions.json</a><br>
Full Help: <a href="/api/help" target="_blank" style="color:var(--accent)">/api/help</a>
</div>
<div id="apiEndpoints"></div>
</div>
</div>
<!-- Terminal Dock -->
<div class="terminal-dock collapsed" id="termDock">
<div class="terminal-dock-header" onclick="toggleTerminalDock()">
<div class="terminal-dock-tabs" id="termTabs">
<div class="term-tab active" onclick="event.stopPropagation()">Terminal</div>
</div>
<div style="display:flex;gap:6px;align-items:center">
<span style="font-size:10px;color:var(--text2)" id="termStatus">no session</span>
<button class="toolbar-btn" onclick="event.stopPropagation();createTerminalSession()"
style="font-size:10px;padding:2px 8px">+ New</button>
<span style="font-size:10px;color:var(--text2)"></span>
</div>
</div>
<div class="terminal-dock-body">
<div style="flex:1;display:flex;flex-direction:column">
<div class="terminal-output" id="termOutput">
<div class="term-line" style="color:var(--text2)">Click "+ New" to create a terminal session. Supports
long-running backtests.</div>
</div>
<div class="terminal-input-row">
<span class="prompt">csc$</span>
<input class="terminal-input" id="termInput" placeholder="Type a command..."
onkeydown="handleTermInput(event)" disabled />
</div>
</div>
<div class="terminal-side" id="termSide">
<h4>Session Info</h4>
<div class="ts-row"><span>Status</span><span class="ts-val" id="tsStatus"></span></div>
<div class="ts-row"><span>Uptime</span><span class="ts-val" id="tsUptime"></span></div>
<div class="ts-row"><span>Lines</span><span class="ts-val" id="tsLines"></span></div>
<div class="ts-row"><span>Last cmd</span><span class="ts-val" id="tsLastCmd"></span></div>
<button class="ts-btn" onclick="pollTerminalOutput()">Refresh Output</button>
<button class="ts-btn" onclick="killTerminalSession()"
style="color:var(--red);border-color:rgba(248,113,113,.2)">Kill Session</button>
</div>
</div>
</div>
<script>
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const statusEl = document.getElementById("status");
const speakerLedger = document.getElementById("speakerLedger");
const observerPane = document.getElementById("observerPane");
const patchStream = document.getElementById("patchStream");
const camSelect = document.getElementById("camSelect");
const videoFileEl = document.getElementById("videoFile");
let stream = null, ws = null, frameTimer = null, recognition = null, sessionId = null;
let patchCount = 0;
let mediaRecorder = null, audioChunks = [], currentAudioHash = null, audioStream = null;
let lastPatchHash = null, lastPatchCode = null;
// Populate camera selector (including iPhone Continuity Camera)
async function populateCameras() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoInputs = devices.filter(d => d.kind === "videoinput");
camSelect.innerHTML = "";
videoInputs.forEach((d, i) => {
const label = d.label || ("Camera " + (i + 1));
const opt = document.createElement("option");
opt.value = d.deviceId;
opt.textContent = label.length > 30 ? label.slice(0, 30) + "..." : label;
camSelect.appendChild(opt);
});
if (videoInputs.length === 0) {
camSelect.innerHTML = '<option value="">No cameras found</option>';
}
} catch (e) {
camSelect.innerHTML = '<option value="">Permission needed</option>';
}
}
populateCameras();
camSelect.onchange = () => { if (stream) { startCamera(); } };
// Video file upload — play as camera source
videoFileEl.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
video.src = url;
video.loop = true;
video.muted = true;
video.play();
document.getElementById("camStatus").textContent = "video file";
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
setStatus("Playing video file as camera source", "dot-active");
};
function setStatus(msg, dotClass) {
statusEl.innerHTML = '<span class="status-dot ' + (dotClass || 'dot-idle') + '"></span>' + msg;
}
function wsUrl() {
const scheme = location.protocol === "https:" ? "wss" : "ws";
return scheme + "://" + location.host + "/ws/stream";
}
async function startCamera() {
const selectedId = camSelect.value;
const constraints = {
video: { width: { ideal: 1280 }, height: { ideal: 720 } },
audio: true
};
if (selectedId) {
constraints.video.deviceId = { exact: selectedId };
} else {
constraints.video.facingMode = "environment";
}
stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
document.getElementById("camStatus").textContent = "live";
// Re-populate camera list now that we have permissions (labels available)
populateCameras();
}
function connectWs() {
ws = new WebSocket(wsUrl());
ws.onopen = () => setStatus("Connected", "dot-active");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "session":
sessionId = msg.session_id;
setStatus("Session " + sessionId.slice(0, 8), "dot-active");
break;
case "frame_ack":
document.getElementById("entropy").textContent = msg.entropy.toFixed(2);
document.getElementById("motion").textContent = msg.motion.toFixed(2);
document.getElementById("fps").textContent = msg.fps_adaptive.toFixed(1);
document.getElementById("buffered").textContent = msg.buffered;
break;
case "qvd":
document.getElementById("qvdVal").textContent = msg.value.toFixed(3);
document.getElementById("qvdThreshold").textContent = msg.threshold.toFixed(2);
document.getElementById("noveltyVal").textContent = msg.novelty.toFixed(3);
document.getElementById("qvdFill").style.width = Math.min(100, (msg.value / msg.threshold) * 50) + "%";
break;
case "transcript_ack":
addSpeakerEntry(msg.speaker, msg.chars, msg.chunk_id);
document.getElementById("audioStatus").textContent = "active";
break;
case "observer":
renderObserverTrace(msg.output, msg.novelty);
setStatus("Observer reasoning complete", "dot-active");
document.getElementById("observerStatus").textContent = "updated";
break;
case "patch":
addPatchEntry(msg.output, msg.patch_hash, msg.receipt, msg.qvd);
patchCount++;
document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
setStatus("Code patch generated", "dot-active");
// Store code + reasoning + evidence on server for diary
const sections = parseSections(msg.output);
const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
if (codeText && msg.patch_hash) {
const reasoningText = sections.REASONING || "";
const evidenceText = sections.EVIDENCE || "";
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(() => { });
// If we have a pending audio recording, attach it
if (currentAudioHash === null && audioChunks.length > 0) {
currentAudioHash = msg.patch_hash;
finishAudioUpload(msg.patch_hash);
}
// Auto-run if checkbox is checked
if (document.getElementById("autoRun") && document.getElementById("autoRun").checked) {
setTimeout(() => runCode(msg.patch_hash, null), 800);
}
}
break;
case "error":
setStatus("Error: " + msg.message, "dot-error");
if (msg.stage === "observer") {
observerPane.innerHTML = '<div class="empty-state" style="color:var(--red)">' + escapeHtml(msg.message) + '</div>';
} else if (msg.stage === "builder") {
patchStream.innerHTML = '<div class="empty-state" style="color:var(--red)">' + escapeHtml(msg.message) + '</div>';
}
break;
case "gate_block":
const gateDiv = document.createElement("div");
gateDiv.className = "patch-entry";
gateDiv.style.borderColor = "var(--amber)";
gateDiv.innerHTML = '<div class="patch-header" style="color:var(--amber)">\u26d4 Evidence Gate Blocked</div>' +
'<div style="padding:12px 16px;font-size:13px;color:var(--text2);line-height:1.6">' +
'<strong>Reason:</strong> ' + escapeHtml(msg.reason) + '<br>' +
'<strong>Evidence type:</strong> ' + escapeHtml(msg.evidence_type) + '<br>' +
'<em style="color:var(--gray);margin-top:8px;display:block">Speak to the camera or show a screen with code to trigger code generation.</em>' +
'</div>';
patchStream.prepend(gateDiv);
setStatus("Evidence Gate: " + msg.evidence_type, "dot-idle");
break;
case "state":
updateSpeakerLedger(msg.state);
break;
case "pong":
break;
}
};
ws.onclose = () => setStatus("Disconnected", "dot-idle");
ws.onerror = () => setStatus("WebSocket error", "dot-error");
}
function captureFrameDataUrl() {
const w = video.videoWidth || 1280;
const h = video.videoHeight || 720;
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, w, h);
return canvas.toDataURL("image/jpeg", 0.74);
}
function sendFrame(force) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({
type: "frame",
frame: captureFrameDataUrl(),
mode: document.getElementById("mode").value,
auto: true,
force: !!force,
}));
}
async function startLive() {
await startCamera();
connectWs();
const ms = Math.max(300, Number(document.getElementById("frameMs").value || 800));
frameTimer = setInterval(() => sendFrame(false), ms);
setStatus("Live stream active", "dot-active");
}
function stopLive() {
if (frameTimer) clearInterval(frameTimer);
frameTimer = null;
if (recognition) { recognition.stop(); recognition = null; }
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
if (videoFileEl) { videoFileEl.pause(); }
if (ws) ws.close();
document.getElementById("camStatus").textContent = "off";
document.getElementById("audioStatus").textContent = "silent";
setStatus("Stopped", "dot-idle");
}
function startSpeech() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) { setStatus("SpeechRecognition not available", "dot-error"); return; }
recognition = new SR();
recognition.lang = "en-US";
recognition.continuous = true;
recognition.interimResults = true;
recognition.onresult = (event) => {
let text = "";
for (let i = 0; i < event.results.length; i++) {
text += event.results[i][0].transcript + " ";
}
text = text.trim();
if (text && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "transcript", text: text }));
}
};
recognition.onerror = (e) => setStatus("Speech error: " + e.error, "dot-error");
recognition.start();
setStatus("Speech recognition active", "dot-active");
document.getElementById("audioStatus").textContent = "listening";
}
function addSpeakerEntry(speaker, chars, chunkId) {
const ph = speakerLedger.querySelector(".empty-state");
if (ph) ph.remove();
const div = document.createElement("div");
div.className = "speaker-entry speaker-" + speaker;
div.innerHTML = '<div class="label">' + speaker + " &middot; " + chars + " chars &middot; " + chunkId + "</div>";
speakerLedger.prepend(div);
}
function updateSpeakerLedger(state) {
const speakers = (state.audio && state.audio.speakers) || {};
speakerLedger.innerHTML = "";
for (const [spk, info] of Object.entries(speakers)) {
const div = document.createElement("div");
div.className = "speaker-entry speaker-" + spk;
div.innerHTML = '<div class="label">' + spk + " &middot; " + info.chunk_count + " chunks</div>" +
'<div class="text">' + escapeHtml((info.transcript || "").slice(-300)) + "</div>";
speakerLedger.appendChild(div);
}
if (!speakerLedger.children.length) {
speakerLedger.innerHTML = '<div class="empty-state">No audio yet.</div>';
}
}
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) {
// Match both **PAT**: and PAT: (with optional markdown bold)
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();
}
// Extract code from fenced block if present
if (sections.CODE) {
const fenceMatch = sections.CODE.match(/```(?:python)?\s*([\s\S]*?)```/);
if (fenceMatch) sections.CODE = fenceMatch[1].trim();
}
return sections;
}
let observerHistory = [];
function renderObserverTrace(output, novelty) {
const ph = observerPane.querySelector(".empty-state");
if (ph) ph.remove();
const sections = parseSections(output);
const ts = new Date().toLocaleTimeString();
observerHistory.unshift({ time: ts, output: output, novelty: novelty });
if (observerHistory.length > 20) observerHistory.pop();
let html = '<div class="observer-trace">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">';
html += '<span style="color:var(--accent);font-size:11px;font-weight:600">\u258C ' + ts + '</span>';
html += '<span style="color:var(--text2);font-size:10px">novelty: ' + novelty.toFixed(3) + ' \u00b7 ' + observerHistory.length + ' observations</span>';
html += '</div>';
if (sections.REASONING) {
html += '<div class="section-header">\u258C REASONING</div>';
html += '<div class="reasoning">' + escapeHtml(sections.REASONING) + "</div>";
}
if (sections.SCENE) {
html += '<div class="section-header">\u258C SCENE</div>';
html += escapeHtml(sections.SCENE) + "\n";
}
if (sections.INFERRED_INTENT || sections.INTENT) {
html += '<div class="section-header">\u258C INFERRED INTENT</div>';
html += '<div class="intent">' + escapeHtml(sections.INFERRED_INTENT || sections.INTENT) + "</div>\n";
}
if (sections.SIGNALS) {
html += '<div class="section-header">\u258C SIGNALS</div>';
html += escapeHtml(sections.SIGNALS) + "\n";
}
if (sections.CANDIDATE_TASK) {
html += '<div class="section-header">\u258C CANDIDATE TASK</div>';
html += escapeHtml(sections.CANDIDATE_TASK) + "\n";
}
if (sections.UNCERTAINTY) {
html += '<div class="section-header">\u258C UNCERTAINTY</div>';
html += escapeHtml(sections.UNCERTAINTY) + "\n";
}
if (!sections.REASONING && !sections.SCENE) html += escapeHtml(output);
// History dropdown
if (observerHistory.length > 1) {
html += '<details style="margin-top:10px;border-top:1px solid var(--border);padding-top:8px">';
html += '<summary style="font-size:10px;color:var(--text2);cursor:pointer">History (' + observerHistory.length + ')</summary>';
for (let i = 1; i < observerHistory.length; i++) {
const h = observerHistory[i];
html += '<div style="font-size:10px;color:var(--text2);margin:4px 0;padding:4px;border-left:2px solid var(--border)">';
html += '<span style="color:var(--accent)">' + h.time + '</span> novelty:' + h.novelty.toFixed(3) + '<br>';
const hSec = parseSections(h.output);
if (hSec.REASONING) html += escapeHtml(hSec.REASONING.slice(0, 120)) + (hSec.REASONING.length > 120 ? '...' : '');
html += '</div>';
}
html += '</details>';
}
html += "</div>";
observerPane.innerHTML = html;
}
function highlightCode(code) {
let html = escapeHtml(code);
html = html.replace(/(#[^\n]*)/g, '<span class="com">$1</span>');
html = html.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g, '<span class="str">$1</span>');
const kws = ['def', 'class', 'import', 'from', 'return', 'if', 'else', 'elif', 'for', 'while', 'try', 'except', 'finally', 'with', 'as', 'async', 'await', 'yield', 'lambda', 'pass', 'break', 'continue', 'raise', 'in', 'not', 'and', 'or', 'is', 'None', 'True', 'False'];
for (const kw of kws) {
html = html.replace(new RegExp("\\b(" + kw + ")\\b", "g"), '<span class="kw">$1</span>');
}
html = html.replace(/\b(\d+\.?\d*)\b/g, '<span class="num">$1</span>');
html = html.replace(/(\w+)(\()/g, '<span class="fn">$1</span>$2');
return html;
}
function addPatchEntry(output, hash, receipt, qvd) {
const ph = patchStream.querySelector(".empty-state");
if (ph) ph.remove();
const sections = parseSections(output);
const ts = new Date().toLocaleTimeString();
const filename = "patch_" + hash.slice(0, 8) + ".py";
let html = '<div class="ide-patch">';
html += '<div class="ide-titlebar">';
html += '<div class="ide-dots"><div class="ide-dot r"></div><div class="ide-dot y"></div><div class="ide-dot g"></div></div>';
html += '<div class="ide-filename">' + filename + "</div>";
html += '<div class="ide-meta"><span>' + ts + "</span>";
if (qvd !== undefined) html += "<span>QVD " + qvd.toFixed(3) + "</span>";
html += "<span>hash " + hash.slice(0, 12) + "</span></div>";
// Run button + artifact link
const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
if (codeText) {
html += '<button class="ide-run-btn" onclick="runCode(\'' + hash + '\', this)">\u25B6 Run</button>';
html += '<a class="ide-artifact-link" href="/artifact/' + hash + '" target="_blank">Artifact</a>';
}
html += "</div>";
html += '<div class="ide-body">';
if (sections.EVIDENCE) {
html += '<div class="ide-section">';
html += '<div class="ide-section-label label-evidence">\u258C Evidence &mdash; raw material from sensors</div>';
html += '<ul class="ide-evidence">';
for (const line of sections.EVIDENCE.split("\n").filter(l => l.trim())) {
html += "<li>" + escapeHtml(line.replace(/^[-\u2022*]\s*/, "")) + "</li>";
}
html += "</ul></div>";
}
if (sections.REASONING) {
html += '<div class="ide-section">';
html += '<div class="ide-section-label label-reasoning">\u258C Reasoning &mdash; why this code exists</div>';
html += '<div class="ide-reasoning">' + escapeHtml(sections.REASONING) + "</div>";
html += "</div>";
}
if (sections.CODE) {
html += '<div class="ide-section" style="padding:0">';
html += '<div class="ide-section-label label-code" style="padding:10px 14px 0">\u258C Code</div>';
const codeText = sections.CODE.replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
html += '<pre class="ide-code">' + highlightCode(codeText) + "</pre>";
html += "</div>";
}
if (sections.RUN) {
html += '<div class="ide-section ide-run">';
html += '<div class="ide-section-label label-run" style="margin:0">\u258C Run</div>';
html += '<span class="prompt">$</span><span>' + escapeHtml(sections.RUN.trim()) + "</span>";
html += "</div>";
}
// Terminal output area (filled when Run is clicked)
if (codeText) {
html += '<div class="ide-terminal" id="term_' + hash.slice(0, 12) + '">';
html += '<div class="term-meta">Click \u25B6 Run to execute in terminal</div>';
html += '</div>';
}
if (sections.TEST) {
html += '<div class="ide-section ide-test">';
html += '<div class="ide-section-label label-test" style="margin:0">\u258C Test</div>';
html += '<span class="prompt">$</span><span>' + escapeHtml(sections.TEST.trim()) + "</span>";
html += "</div>";
}
if (sections.ATTRIBUTION) {
html += '<div class="ide-section">';
html += '<div class="ide-section-label" style="color:var(--text2)">\u258C Attribution</div>';
html += '<div class="ide-evidence">' + escapeHtml(sections.ATTRIBUTION) + "</div>";
html += "</div>";
}
if (!sections.EVIDENCE && !sections.REASONING && !sections.CODE) {
html += '<div class="ide-section"><pre class="ide-code">' + escapeHtml(output) + "</pre></div>";
}
html += "</div></div>";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
patchStream.prepend(wrapper.firstChild);
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
async function runCode(patchHash, btn) {
const termId = 'term_' + patchHash.slice(0, 12);
const term = document.getElementById(termId);
if (!term) return;
if (btn) { btn.disabled = true; btn.textContent = 'Running...'; }
term.innerHTML = '<div class="term-meta">Executing...</div>';
try {
const resp = await fetch('/run?patch_hash=' + patchHash, { method: 'POST' });
const data = await resp.json();
let html = '';
if (data.stdout) {
html += '<div class="term-meta">exit_code: ' + data.exit_code + '</div>';
html += '<div class="term-stdout">' + escapeHtml(data.stdout) + '</div>';
}
if (data.stderr) {
html += '<div class="term-stderr">' + escapeHtml(data.stderr) + '</div>';
}
if (!data.stdout && !data.stderr) {
html += '<div class="term-meta">exit_code: ' + data.exit_code + ' (no output)</div>';
}
// Add Debug button if execution failed
if (data.exit_code !== 0 && data.exit_code !== undefined) {
html += '<button class="ide-run-btn" style="background:rgba(251,191,36,.15);color:var(--amber);border-color:rgba(251,191,36,.3);margin-top:6px" onclick="debugCode(\'' + patchHash + '\', this)">\u2691 Debug &amp; Fix with LLM</button>';
}
term.innerHTML = html;
} catch (e) {
term.innerHTML = '<div class="term-stderr">Error: ' + escapeHtml(e.message) + '</div>';
}
if (btn) { btn.disabled = false; btn.textContent = '\u25B6 Run'; }
}
async function debugCode(patchHash, btn) {
if (btn) { btn.disabled = true; btn.textContent = 'Debugging...'; }
const termId = 'term_' + patchHash.slice(0, 12);
const term = document.getElementById(termId);
try {
const resp = await fetch('/debug?patch_hash=' + patchHash, { method: 'POST' });
const data = await resp.json();
if (data.error) {
if (term) term.innerHTML += '<div class="term-stderr">Debug error: ' + escapeHtml(data.error) + '</div>';
} else {
// Parse the fixed code and add as new patch entry
addPatchEntry(data.patch_output, data.patch_hash, {}, undefined);
patchCount++;
document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
// Store the fixed artifact
const sections = parseSections(data.patch_output);
const fixedCode = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
if (fixedCode && data.patch_hash) {
fetch("/store-artifact?patch_hash=" + data.patch_hash + "&code=" + encodeURIComponent(fixedCode) + "&observer_output=" + encodeURIComponent(data.patch_output), { method: "POST" }).catch(() => { });
}
if (term) term.innerHTML += '<div class="term-meta" style="color:var(--amber)">\u2713 Fixed! New artifact: ' + data.patch_hash.slice(0, 12) + ' \u2014 see above</div>';
// Auto-run the fixed code if auto-run is checked
if (document.getElementById("autoRun") && document.getElementById("autoRun").checked) {
setTimeout(() => runCode(data.patch_hash, null), 500);
}
}
} catch (e) {
if (term) term.innerHTML += '<div class="term-stderr">Debug error: ' + escapeHtml(e.message) + '</div>';
}
if (btn) { btn.disabled = false; btn.textContent = '\u2691 Debug &amp; Fix with LLM'; }
}
document.getElementById("startBtn").onclick = () => startLive().catch(e => setStatus(e.message, "dot-error"));
document.getElementById("stopBtn").onclick = stopLive;
document.getElementById("speechBtn").onclick = startSpeech;
document.getElementById("forceBtn").onclick = () => sendFrame(true);
// Audio Recording with MediaRecorder
document.getElementById("recBtn").onclick = toggleRecording;
function toggleRecording() {
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
document.getElementById("recBtn").style.background = "rgba(248,113,113,.1)";
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';
document.getElementById("audioStatus").textContent = "processing";
} else {
startAudioRecording();
}
}
async function startAudioRecording() {
try {
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioChunks = [];
currentAudioHash = null;
mediaRecorder = new MediaRecorder(audioStream);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = () => {
if (audioStream) { audioStream.getTracks().forEach(t => t.stop()); audioStream = null; }
// If we already have a patch hash, upload now
if (currentAudioHash) {
finishAudioUpload(currentAudioHash);
}
};
mediaRecorder.start();
const btn = document.getElementById("recBtn");
btn.style.background = "rgba(248,113,113,.25)";
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';
document.getElementById("audioStatus").textContent = "recording";
setStatus("Audio recording started", "dot-active");
} catch (e) {
setStatus("Audio recording error: " + e.message, "dot-error");
}
}
function finishAudioUpload(patchHash) {
if (audioChunks.length === 0) return;
const blob = new Blob(audioChunks, { type: "audio/webm" });
const reader = new FileReader();
reader.onloadend = () => {
const b64 = reader.result.split(",")[1];
if (b64) {
fetch("/audio/store?patch_hash=" + patchHash + "&audio_b64=" + encodeURIComponent(b64), { method: "POST" })
.then(() => {
audioChunks = [];
document.getElementById("audioStatus").textContent = "stored";
})
.catch(() => { });
}
};
reader.readAsDataURL(blob);
}
// ─── Terminal Dock ───
let currentTermSession = null;
let termPollTimer = null;
function toggleTerminalDock() {
const dock = document.getElementById("termDock");
dock.classList.toggle("collapsed");
if (!dock.classList.contains("collapsed") && !currentTermSession) {
createTerminalSession();
}
}
async function createTerminalSession() {
try {
const resp = await fetch("/terminal/create", { method: "POST" });
const data = await resp.json();
currentTermSession = data.session_id;
document.getElementById("termInput").disabled = false;
document.getElementById("termStatus").textContent = data.session_id.slice(0, 12);
document.getElementById("termOutput").innerHTML = '<div class="term-line term-prompt">Terminal session created: ' + data.session_id + '</div>';
document.getElementById("termInput").focus();
startTermPolling();
} catch (e) {
document.getElementById("termOutput").innerHTML = '<div class="term-line term-err">Error: ' + escapeHtml(e.message) + '</div>';
}
}
async function handleTermInput(event) {
if (event.key !== "Enter") return;
const input = document.getElementById("termInput");
const cmd = input.value.trim();
if (!cmd || !currentTermSession) return;
input.value = "";
const outEl = document.getElementById("termOutput");
outEl.innerHTML += '<div class="term-line"><span class="term-prompt">csc$</span> ' + escapeHtml(cmd) + '</div>';
outEl.scrollTop = outEl.scrollHeight;
try {
const resp = await fetch("/terminal/" + currentTermSession + "/exec?command=" + encodeURIComponent(cmd), { method: "POST" });
const data = await resp.json();
if (data.error) {
outEl.innerHTML += '<div class="term-line term-err">' + escapeHtml(data.error) + '</div>';
} else if (data.output) {
outEl.innerHTML += '<div class="term-line term-out">' + escapeHtml(data.output) + '</div>';
}
outEl.scrollTop = outEl.scrollHeight;
updateTermSide(data);
} catch (e) {
outEl.innerHTML += '<div class="term-line term-err">' + escapeHtml(e.message) + '</div>';
}
}
function startTermPolling() {
if (termPollTimer) clearInterval(termPollTimer);
termPollTimer = setInterval(pollTerminalOutput, 3000);
}
async function pollTerminalOutput() {
if (!currentTermSession) return;
try {
const resp = await fetch("/terminal/" + currentTermSession + "/output");
const data = await resp.json();
if (data.output) {
const outEl = document.getElementById("termOutput");
const existing = outEl.querySelector(".term-poll-out");
if (existing) existing.remove();
outEl.innerHTML += '<div class="term-line term-out term-poll-out">' + escapeHtml(data.output.slice(-2000)) + '</div>';
outEl.scrollTop = outEl.scrollHeight;
}
document.getElementById("tsStatus").textContent = data.is_alive ? "alive" : "dead";
document.getElementById("tsLines").textContent = data.line_count || 0;
} catch (e) { }
}
async function updateTermSide(data) {
try {
const resp = await fetch("/terminal/" + currentTermSession + "/status");
const s = await resp.json();
document.getElementById("tsStatus").textContent = s.alive ? "alive" : "dead";
document.getElementById("tsUptime").textContent = s.uptime_s + "s";
document.getElementById("tsLines").textContent = s.buffer_lines;
document.getElementById("tsLastCmd").textContent = (s.last_command || "—").slice(0, 20);
} catch (e) { }
}
async function killTerminalSession() {
if (!currentTermSession) return;
try {
await fetch("/terminal/" + currentTermSession + "/kill", { method: "POST" });
document.getElementById("termOutput").innerHTML += '<div class="term-line term-err">Session killed</div>';
currentTermSession = null;
document.getElementById("termInput").disabled = true;
document.getElementById("termStatus").textContent = "no session";
if (termPollTimer) { clearInterval(termPollTimer); termPollTimer = null; }
} catch (e) { }
}
// ─── Command Palette ───
const commands = [
{ icon: "▶", label: "Start Engine", desc: "Begin live capture", action: () => startLive().catch(e => setStatus(e.message, "dot-error")) },
{ icon: "■", label: "Stop Engine", desc: "Stop live capture", action: stopLive },
{ icon: "⚡", label: "Force Generate", desc: "Force code generation", action: () => sendFrame(true) },
{ icon: "🎤", label: "Start Speech", desc: "Begin speech recognition", action: startSpeech },
{ icon: "🔴", label: "Record Audio", desc: "Toggle audio recording", action: toggleRecording },
{ icon: "📖", label: "Open Lab Diary", desc: "View all artifacts", action: () => window.open("/diary", "_blank") },
{ icon: "📐", label: "Open API Help", desc: "List all endpoints", action: () => window.open("/api/help", "_blank") },
{ icon: "🔌", label: "GPT Actions Schema", desc: "ChatGPT integration schema", action: () => window.open("/gpt-actions.json", "_blank") },
{ icon: "🖥", label: "Toggle Terminal", desc: "Open/close terminal dock", action: toggleTerminalDock },
{ icon: "🔍", label: "Toggle API Explorer", desc: "Open/close API panel", action: toggleApiPanel },
{ icon: "📊", label: "List Artifacts", desc: "Fetch all artifacts as JSON", action: () => fetch("/api/artifacts").then(r => r.json()).then(d => console.log(d)) },
{ icon: "📋", label: "List Terminal Sessions", desc: "Show active terminals", action: () => fetch("/terminal/sessions").then(r => r.json()).then(d => console.log(d)) },
{ icon: "🏥", label: "Health Check", desc: "Check server status", action: () => fetch("/health").then(r => r.json()).then(d => setStatus(JSON.stringify(d).slice(0, 80), "dot-active")) },
{ icon: "🧪", label: "New Terminal Session", desc: "Create long-running terminal", action: () => { toggleTerminalDock(); if (!currentTermSession) createTerminalSession(); } },
];
let cmdSelectedIndex = 0;
function openCmdPalette() {
document.getElementById("cmdOverlay").classList.add("open");
document.getElementById("cmdPalette").classList.add("open");
const input = document.getElementById("cmdInput");
input.value = "";
input.focus();
cmdSelectedIndex = 0;
renderCmds(commands);
}
function closeCmdPalette() {
document.getElementById("cmdOverlay").classList.remove("open");
document.getElementById("cmdPalette").classList.remove("open");
}
function filterCmds() {
const q = document.getElementById("cmdInput").value.toLowerCase();
const filtered = commands.filter(c => c.label.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q));
cmdSelectedIndex = 0;
renderCmds(filtered);
}
function renderCmds(cmds) {
const el = document.getElementById("cmdResults");
el.innerHTML = cmds.map((c, i) =>
'<div class="cmd-item' + (i === cmdSelectedIndex ? ' selected' : '') + '" onclick="execCmd(' + commands.indexOf(c) + ')">' +
'<span class="cmd-icon">' + c.icon + '</span>' +
'<span class="cmd-label">' + c.label + '</span>' +
'<span class="cmd-desc">' + c.desc + '</span></div>'
).join("");
}
function execCmd(idx) {
closeCmdPalette();
if (commands[idx]) commands[idx].action();
}
function handleCmdKey(event) {
const q = document.getElementById("cmdInput").value.toLowerCase();
const filtered = commands.filter(c => c.label.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q));
if (event.key === "ArrowDown") { event.preventDefault(); cmdSelectedIndex = Math.min(cmdSelectedIndex + 1, filtered.length - 1); renderCmds(filtered); }
else if (event.key === "ArrowUp") { event.preventDefault(); cmdSelectedIndex = Math.max(cmdSelectedIndex - 1, 0); renderCmds(filtered); }
else if (event.key === "Enter") { event.preventDefault(); if (filtered[cmdSelectedIndex]) execCmd(commands.indexOf(filtered[cmdSelectedIndex])); }
else if (event.key === "Escape") { closeCmdPalette(); }
}
// ─── API Explorer ───
function toggleApiPanel() {
document.getElementById("apiPanel").classList.toggle("open");
if (document.getElementById("apiPanel").classList.contains("open")) loadApiEndpoints();
}
async function loadApiEndpoints() {
try {
const resp = await fetch("/api/help");
const data = await resp.json();
window._apiEndpoints = data.endpoints;
const el = document.getElementById("apiEndpoints");
el.innerHTML = data.endpoints.map((e, i) => {
let paramsHtml = '';
if (e.params && e.params.length > 0) {
paramsHtml = '<div class="api-params" style="margin-top:8px;display:flex;flex-direction:column;gap:6px">';
for (const p of e.params) {
const req = p.required ? ' <span style="color:var(--red)">*</span>' : '';
if (p.type === 'textarea') {
paramsHtml += '<label style="font-size:10px;color:var(--text2)">' + p.name + req + '</label>' +
'<textarea id="apiparam_' + i + '_' + p.name + '" placeholder="' + (p.placeholder || '') + '" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:6px 8px;color:var(--text);font-family:var(--mono);font-size:11px;min-height:60px;resize:vertical;outline:none" onfocus="this.style.borderColor=\'var(--accent)\'" onblur="this.style.borderColor=\'var(--border)\'"></textarea>';
} else {
paramsHtml += '<div style="display:flex;align-items:center;gap:6px">' +
'<label style="font-size:10px;color:var(--text2);min-width:80px">' + p.name + req + '</label>' +
'<input id="apiparam_' + i + '_' + p.name + '" type="text" placeholder="' + (p.placeholder || '') + '" style="flex:1;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:4px 8px;color:var(--text);font-family:var(--mono);font-size:11px;outline:none" onfocus="this.style.borderColor=\'var(--accent)\'" onblur="this.style.borderColor=\'var(--border)\'" />' +
'</div>';
}
}
paramsHtml += '</div>';
}
return '<div class="api-endpoint" id="apiep_' + i + '">' +
'<div style="display:flex;align-items:center;cursor:pointer" onclick="toggleEndpointExpand(' + i + ')">' +
'<span class="api-method ' + e.method + '">' + e.method + '</span>' +
'<span class="api-path">' + e.path + '</span>' +
'<span style="margin-left:auto;font-size:10px;color:var(--text2)" id="apitoggle_' + i + '">▸</span>' +
'</div>' +
'<div class="api-desc">' + e.description + '</div>' +
'<div class="api-expand" id="apiexpand_' + i + '" style="display:none">' +
paramsHtml +
'<button class="api-try-btn" onclick="event.stopPropagation();tryEndpoint(' + i + ')" style="margin-top:8px">▶ Send Request</button>' +
'<div class="api-response" id="apiResp' + i + '"></div>' +
'</div>' +
'</div>';
}).join("");
} catch (e) {
document.getElementById("apiEndpoints").innerHTML = '<div style="color:var(--red)">Error loading: ' + escapeHtml(e.message) + '</div>';
}
}
function toggleEndpointExpand(i) {
const expand = document.getElementById("apiexpand_" + i);
const toggle = document.getElementById("apitoggle_" + i);
if (expand.style.display === "none") {
expand.style.display = "block";
toggle.textContent = "▾";
} else {
expand.style.display = "none";
toggle.textContent = "▸";
}
}
async function tryEndpoint(i) {
const ep = window._apiEndpoints[i];
const respEl = document.getElementById("apiResp" + i);
respEl.classList.add("open");
respEl.textContent = "Sending...";
respEl.style.color = "var(--text2)";
// Build URL — replace path params and add query params
let url = ep.path;
const queryParams = [];
for (const p of (ep.params || [])) {
const input = document.getElementById("apiparam_" + i + "_" + p.name);
if (!input) continue;
const val = input.value.trim();
if (!val) continue;
// Check if it's a path param ({name} in path)
const pathPlaceholder = "{" + p.name + "}";
if (url.includes(pathPlaceholder)) {
url = url.replace(pathPlaceholder, encodeURIComponent(val));
} else {
// Query param
queryParams.push(encodeURIComponent(p.name) + "=" + encodeURIComponent(val));
}
}
if (queryParams.length > 0) {
url += (url.includes("?") ? "&" : "?") + queryParams.join("&");
}
try {
const opts = { method: ep.method };
const resp = await fetch(url, opts);
const text = await resp.text();
// Try to pretty-print JSON
try {
const json = JSON.parse(text);
respEl.textContent = JSON.stringify(json, null, 2).slice(0, 3000);
respEl.style.color = resp.ok ? "var(--green)" : "var(--amber)";
} catch {
respEl.textContent = text.slice(0, 3000);
respEl.style.color = resp.ok ? "var(--green)" : "var(--amber)";
}
} catch (e) {
respEl.textContent = "Error: " + e.message;
respEl.style.color = "var(--red)";
}
}
// ─── Keyboard Shortcuts ───
document.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
openCmdPalette();
}
if (e.key === "Escape") {
closeCmdPalette();
if (document.getElementById("apiPanel").classList.contains("open")) document.getElementById("apiPanel").classList.remove("open");
}
});
</script>
</body>
</html>