Spaces:
Paused
Paused
| <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 — evidence → reasoning → 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> | |
| · 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 — 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 — 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 + " · " + chars + " chars · " + 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 + " · " + 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 — 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 — 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 & 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 & 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> |