josephrw commited on
Commit
4f7624c
Β·
verified Β·
1 Parent(s): ab27fab

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +292 -0
  2. frontend/index.html +706 -0
app.py CHANGED
@@ -12,7 +12,11 @@ import hashlib
12
  import subprocess
13
  import tempfile
14
  import base64
 
 
 
15
  from datetime import datetime, timezone
 
16
 
17
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
18
  from fastapi.middleware.cors import CORSMiddleware
@@ -576,3 +580,291 @@ async def ws_stream(ws: WebSocket):
576
 
577
  except WebSocketDisconnect:
578
  sessions.pop(state.session_id, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  import subprocess
13
  import tempfile
14
  import base64
15
+ import uuid
16
+ import signal
17
+ import threading
18
  from datetime import datetime, timezone
19
+ from collections import deque
20
 
21
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
22
  from fastapi.middleware.cors import CORSMiddleware
 
580
 
581
  except WebSocketDisconnect:
582
  sessions.pop(state.session_id, None)
583
+
584
+
585
+ # ─── Long-Running Terminal Sessions ───
586
+
587
+ class TerminalSession:
588
+ """Persistent terminal session for long-running processes like backtests."""
589
+ def __init__(self, session_id: str, cwd: str = "/tmp", shell: str = "/bin/bash"):
590
+ self.session_id = session_id
591
+ self.cwd = cwd
592
+ self.shell = shell
593
+ self.proc = None
594
+ self.output_buffer = deque(maxlen=10000)
595
+ self.created_at = time.time()
596
+ self.last_command = None
597
+ self.lock = threading.Lock()
598
+ self._reader_thread = None
599
+
600
+ def start(self):
601
+ self.proc = subprocess.Popen(
602
+ [self.shell, "-i"],
603
+ stdin=subprocess.PIPE,
604
+ stdout=subprocess.PIPE,
605
+ stderr=subprocess.STDOUT,
606
+ text=True,
607
+ cwd=self.cwd,
608
+ bufsize=1,
609
+ env={**os.environ, "PS1": "csc$ ", "TERM": "dumb"},
610
+ )
611
+ self._reader_thread = threading.Thread(target=self._read_output, daemon=True)
612
+ self._reader_thread.start()
613
+
614
+ def _read_output(self):
615
+ try:
616
+ for line in self.proc.stdout:
617
+ self.output_buffer.append({"ts": time.time(), "line": line})
618
+ except Exception:
619
+ pass
620
+
621
+ def exec(self, command: str, timeout: float = 300) -> dict:
622
+ with self.lock:
623
+ if not self.proc or self.proc.poll() is not None:
624
+ return {"error": "Session terminated", "exit_code": self.proc.returncode if self.proc else -1}
625
+ self.last_command = command
626
+ buf_start = len(self.output_buffer)
627
+ try:
628
+ self.proc.stdin.write(command + "\n")
629
+ self.proc.stdin.flush()
630
+ except (BrokenPipeError, OSError):
631
+ return {"error": "Session pipe broken", "exit_code": -1}
632
+ # Wait for output to stabilize (no new lines for 500ms) or timeout
633
+ time.sleep(0.3)
634
+ deadline = time.time() + timeout
635
+ last_count = 0
636
+ while time.time() < deadline:
637
+ current = len(self.output_buffer)
638
+ if current == last_count and current > buf_start:
639
+ break
640
+ last_count = current
641
+ time.sleep(0.5)
642
+ new_lines = list(self.output_buffer)[buf_start:]
643
+ output = "".join(item["line"] for item in new_lines)
644
+ return {"output": output[-8000:], "lines": len(new_lines), "command": command}
645
+
646
+ def get_output(self, since: int = 0) -> dict:
647
+ lines = [item for item in self.output_buffer if item["ts"] >= since]
648
+ return {
649
+ "output": "".join(item["line"] for item in lines)[-16000:],
650
+ "line_count": len(self.output_buffer),
651
+ "is_alive": self.proc and self.proc.poll() is None,
652
+ }
653
+
654
+ def kill(self):
655
+ if self.proc and self.proc.poll() is None:
656
+ try:
657
+ os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM)
658
+ except (ProcessLookupError, PermissionError):
659
+ self.proc.kill()
660
+ return {"killed": True, "session_id": self.session_id}
661
+
662
+ def status(self) -> dict:
663
+ return {
664
+ "session_id": self.session_id,
665
+ "alive": self.proc and self.proc.poll() is None,
666
+ "created_at": self.created_at,
667
+ "uptime_s": round(time.time() - self.created_at, 1),
668
+ "last_command": self.last_command,
669
+ "buffer_lines": len(self.output_buffer),
670
+ "cwd": self.cwd,
671
+ }
672
+
673
+
674
+ terminal_sessions: dict[str, TerminalSession] = {}
675
+
676
+
677
+ @app.post("/terminal/create")
678
+ async def terminal_create(cwd: str = "/tmp"):
679
+ """Create a long-running terminal session for backtesting, training, etc."""
680
+ sid = f"term_{uuid.uuid4().hex[:12]}"
681
+ session = TerminalSession(sid, cwd=cwd)
682
+ session.start()
683
+ terminal_sessions[sid] = session
684
+ return {"session_id": sid, "created_at": session.created_at, "cwd": cwd}
685
+
686
+
687
+ @app.post("/terminal/{session_id}/exec")
688
+ async def terminal_exec(session_id: str, command: str = "", timeout: float = 300):
689
+ """Execute a command in a terminal session. Supports long-running commands."""
690
+ if session_id not in terminal_sessions:
691
+ return JSONResponse({"error": "Terminal session not found"}, 404)
692
+ session = terminal_sessions[session_id]
693
+ if not command.strip():
694
+ return JSONResponse({"error": "No command provided"}, 400)
695
+ return session.exec(command, timeout=timeout)
696
+
697
+
698
+ @app.get("/terminal/{session_id}/output")
699
+ async def terminal_output(session_id: str, since: float = 0):
700
+ """Get accumulated output from a terminal session."""
701
+ if session_id not in terminal_sessions:
702
+ return JSONResponse({"error": "Terminal session not found"}, 404)
703
+ return terminal_sessions[session_id].get_output(since=since)
704
+
705
+
706
+ @app.post("/terminal/{session_id}/kill")
707
+ async def terminal_kill(session_id: str):
708
+ """Kill a terminal session."""
709
+ if session_id not in terminal_sessions:
710
+ return JSONResponse({"error": "Terminal session not found"}, 404)
711
+ result = terminal_sessions[session_id].kill()
712
+ del terminal_sessions[session_id]
713
+ return result
714
+
715
+
716
+ @app.get("/terminal/{session_id}/status")
717
+ async def terminal_status(session_id: str):
718
+ """Get status of a terminal session."""
719
+ if session_id not in terminal_sessions:
720
+ return JSONResponse({"error": "Terminal session not found"}, 404)
721
+ return terminal_sessions[session_id].status()
722
+
723
+
724
+ @app.get("/terminal/sessions")
725
+ async def terminal_list():
726
+ """List all active terminal sessions."""
727
+ return {"sessions": [s.status() for s in terminal_sessions.values()]}
728
+
729
+
730
+ # ─── API Help & OpenAPI for ChatGPT ───
731
+
732
+ @app.get("/api/help")
733
+ async def api_help():
734
+ """List all available endpoints with descriptions β€” for ChatGPT custom GPT discovery."""
735
+ return {
736
+ "app": "CSC Engine",
737
+ "version": "1.0.0",
738
+ "description": "Live multimodal code compiler: camera + audio + speech β†’ observer LLM β†’ builder LLM β†’ code patches. Supports long-running terminal sessions for backtesting.",
739
+ "base_url": f"https://josephrw-csc-engine.hf.space",
740
+ "endpoints": [
741
+ {"method": "GET", "path": "/health", "description": "Health check β€” returns provider, version, active sessions"},
742
+ {"method": "POST", "path": "/session/start", "description": "Start a CSC Engine session"},
743
+ {"method": "POST", "path": "/session/stop", "description": "Stop a session by session_id"},
744
+ {"method": "GET", "path": "/state/current", "description": "Get current session state (requires session_id)"},
745
+ {"method": "POST", "path": "/generate/patch", "description": "Force a code generation cycle (requires session_id)"},
746
+ {"method": "POST", "path": "/run", "description": "Execute Python code β€” pass code or patch_hash. Returns stdout/stderr/exit_code"},
747
+ {"method": "POST", "path": "/debug", "description": "Send failed code + stderr to LLM for auto-fix. Pass patch_hash or code+stderr"},
748
+ {"method": "POST", "path": "/store-artifact", "description": "Store generated code + metadata for diary. Pass patch_hash, code, reasoning, evidence, observer_output"},
749
+ {"method": "GET", "path": "/artifact/{patch_hash}", "description": "View artifact as HTML page with Run and Debug buttons"},
750
+ {"method": "GET", "path": "/api/artifacts", "description": "List all artifacts as JSON (limit param, default 50)"},
751
+ {"method": "GET", "path": "/api/artifacts/{patch_hash}", "description": "Get single artifact as JSON"},
752
+ {"method": "GET", "path": "/receipts", "description": "List all patch receipts (limit param)"},
753
+ {"method": "GET", "path": "/receipts/{receipt_id}", "description": "Get a specific receipt"},
754
+ {"method": "POST", "path": "/audio/store", "description": "Store base64-encoded audio recording alongside an artifact"},
755
+ {"method": "GET", "path": "/audio/{filename}", "description": "Serve an audio file"},
756
+ {"method": "GET", "path": "/diary", "description": "Lab diary page β€” notebook-style view of all artifacts"},
757
+ {"method": "POST", "path": "/terminal/create", "description": "Create a long-running terminal session for backtesting. Pass cwd (default /tmp)"},
758
+ {"method": "POST", "path": "/terminal/{session_id}/exec", "description": "Execute a command in a terminal session. Pass command string. Supports 300s timeout"},
759
+ {"method": "GET", "path": "/terminal/{session_id}/output", "description": "Get accumulated output from a terminal session. Pass since=timestamp to get only new output"},
760
+ {"method": "POST", "path": "/terminal/{session_id}/kill", "description": "Kill a terminal session"},
761
+ {"method": "GET", "path": "/terminal/{session_id}/status", "description": "Get status of a terminal session"},
762
+ {"method": "GET", "path": "/terminal/sessions", "description": "List all active terminal sessions"},
763
+ {"method": "GET", "path": "/api/help", "description": "This endpoint β€” lists all available endpoints"},
764
+ {"method": "GET", "path": "/openapi.json", "description": "OpenAPI 3.1 schema for ChatGPT custom GPT integration"},
765
+ ],
766
+ "chatgpt_integration": {
767
+ "schema_url": "/openapi.json",
768
+ "auth": "none",
769
+ "description": "Add this URL as an Action in your ChatGPT custom GPT: https://josephrw-csc-engine.hf.space/openapi.json",
770
+ },
771
+ }
772
+
773
+
774
+ @app.get("/gpt-actions.json")
775
+ async def gpt_actions_schema():
776
+ """ChatGPT Custom GPT Actions schema β€” OpenAPI 3.1 with all endpoints."""
777
+ return {
778
+ "openapi": "3.1.0",
779
+ "info": {
780
+ "title": "CSC Engine",
781
+ "version": "1.0.0",
782
+ "description": "Live multimodal code compiler with long-running terminal sessions. Camera + audio β†’ observer LLM β†’ builder LLM β†’ code patches. Execute Python, debug with LLM, run backtests in persistent terminals.",
783
+ },
784
+ "servers": [{"url": "https://josephrw-csc-engine.hf.space"}],
785
+ "paths": {
786
+ "/health": {
787
+ "get": {"summary": "Health check", "operationId": "health", "responses": {"200": {"description": "OK"}}}
788
+ },
789
+ "/session/start": {
790
+ "post": {"summary": "Start session", "operationId": "startSession", "responses": {"200": {"description": "Session created"}}}
791
+ },
792
+ "/session/stop": {
793
+ "post": {"summary": "Stop session", "operationId": "stopSession",
794
+ "parameters": [{"name": "session_id", "in": "query", "required": True, "schema": {"type": "string"}}],
795
+ "responses": {"200": {"description": "Session stopped"}}}
796
+ },
797
+ "/generate/patch": {
798
+ "post": {"summary": "Generate code patch from current state", "operationId": "generatePatch",
799
+ "parameters": [{"name": "session_id", "in": "query", "required": True, "schema": {"type": "string"}}],
800
+ "responses": {"200": {"description": "Patch generated"}}}
801
+ },
802
+ "/run": {
803
+ "post": {"summary": "Execute Python code", "operationId": "runCode",
804
+ "parameters": [
805
+ {"name": "code", "in": "query", "required": False, "schema": {"type": "string"}, "description": "Python code to execute"},
806
+ {"name": "patch_hash", "in": "query", "required": False, "schema": {"type": "string"}, "description": "Hash of stored artifact to run"},
807
+ ],
808
+ "responses": {"200": {"description": "Execution result with stdout, stderr, exit_code"}}}
809
+ },
810
+ "/debug": {
811
+ "post": {"summary": "Debug and fix failed code with LLM", "operationId": "debugCode",
812
+ "parameters": [
813
+ {"name": "patch_hash", "in": "query", "required": False, "schema": {"type": "string"}},
814
+ {"name": "code", "in": "query", "required": False, "schema": {"type": "string"}},
815
+ {"name": "stderr", "in": "query", "required": False, "schema": {"type": "string"}},
816
+ ],
817
+ "responses": {"200": {"description": "Fixed code with new patch_hash"}}}
818
+ },
819
+ "/api/artifacts": {
820
+ "get": {"summary": "List all artifacts", "operationId": "listArtifacts",
821
+ "parameters": [{"name": "limit", "in": "query", "schema": {"type": "integer", "default": 50}}],
822
+ "responses": {"200": {"description": "List of artifacts"}}}
823
+ },
824
+ "/api/artifacts/{patch_hash}": {
825
+ "get": {"summary": "Get artifact by hash", "operationId": "getArtifact",
826
+ "parameters": [{"name": "patch_hash", "in": "path", "required": True, "schema": {"type": "string"}}],
827
+ "responses": {"200": {"description": "Artifact JSON"}}}
828
+ },
829
+ "/terminal/create": {
830
+ "post": {"summary": "Create long-running terminal session", "operationId": "createTerminal",
831
+ "parameters": [{"name": "cwd", "in": "query", "schema": {"type": "string", "default": "/tmp"}}],
832
+ "responses": {"200": {"description": "Terminal session created with session_id"}}}
833
+ },
834
+ "/terminal/{session_id}/exec": {
835
+ "post": {"summary": "Execute command in terminal session", "operationId": "execTerminal",
836
+ "parameters": [
837
+ {"name": "session_id", "in": "path", "required": True, "schema": {"type": "string"}},
838
+ {"name": "command", "in": "query", "required": True, "schema": {"type": "string"}, "description": "Shell command to execute"},
839
+ {"name": "timeout", "in": "query", "schema": {"type": "number", "default": 300}},
840
+ ],
841
+ "responses": {"200": {"description": "Command output"}}}
842
+ },
843
+ "/terminal/{session_id}/output": {
844
+ "get": {"summary": "Get terminal output", "operationId": "getTerminalOutput",
845
+ "parameters": [
846
+ {"name": "session_id", "in": "path", "required": True, "schema": {"type": "string"}},
847
+ {"name": "since", "in": "query", "schema": {"type": "number", "default": 0}},
848
+ ],
849
+ "responses": {"200": {"description": "Accumulated output"}}}
850
+ },
851
+ "/terminal/{session_id}/kill": {
852
+ "post": {"summary": "Kill terminal session", "operationId": "killTerminal",
853
+ "parameters": [{"name": "session_id", "in": "path", "required": True, "schema": {"type": "string"}}],
854
+ "responses": {"200": {"description": "Session killed"}}}
855
+ },
856
+ "/terminal/sessions": {
857
+ "get": {"summary": "List active terminal sessions", "operationId": "listTerminals",
858
+ "responses": {"200": {"description": "List of terminal sessions"}}}
859
+ },
860
+ "/receipts": {
861
+ "get": {"summary": "List all receipts", "operationId": "listReceipts",
862
+ "parameters": [{"name": "limit", "in": "query", "schema": {"type": "integer", "default": 50}}],
863
+ "responses": {"200": {"description": "List of receipts"}}}
864
+ },
865
+ "/api/help": {
866
+ "get": {"summary": "List all endpoints", "operationId": "apiHelp",
867
+ "responses": {"200": {"description": "Endpoint listing"}}}
868
+ },
869
+ },
870
+ }
frontend/index.html CHANGED
@@ -696,6 +696,407 @@
696
  .ide-artifact-link:hover {
697
  background: rgba(0, 212, 255, .2);
698
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  </style>
700
  </head>
701
 
@@ -710,6 +1111,29 @@
710
  </div>
711
  </div>
712
  <div class="status-pill" id="status"><span class="status-dot dot-idle"></span>Idle</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  </header>
714
 
715
  <div class="controls">
@@ -837,6 +1261,68 @@
837
  </div>
838
  </main>
839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  <script>
841
  const video = document.getElementById("video");
842
  const canvas = document.getElementById("canvas");
@@ -1384,6 +1870,226 @@
1384
  };
1385
  reader.readAsDataURL(blob);
1386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1387
  </script>
1388
  </body>
1389
 
 
696
  .ide-artifact-link:hover {
697
  background: rgba(0, 212, 255, .2);
698
  }
699
+
700
+ /* ─── Terminal Dock ─── */
701
+ .terminal-dock {
702
+ position: fixed;
703
+ bottom: 0;
704
+ left: 0;
705
+ right: 0;
706
+ height: 280px;
707
+ background: var(--panel);
708
+ border-top: 1px solid var(--border2);
709
+ z-index: 100;
710
+ display: flex;
711
+ flex-direction: column;
712
+ transition: height .2s ease;
713
+ }
714
+
715
+ .terminal-dock.collapsed {
716
+ height: 32px;
717
+ }
718
+
719
+ .terminal-dock-header {
720
+ display: flex;
721
+ align-items: center;
722
+ justify-content: space-between;
723
+ padding: 4px 12px;
724
+ height: 32px;
725
+ flex-shrink: 0;
726
+ background: var(--panel2);
727
+ border-bottom: 1px solid var(--border);
728
+ cursor: pointer;
729
+ user-select: none;
730
+ }
731
+
732
+ .terminal-dock-tabs {
733
+ display: flex;
734
+ gap: 2px;
735
+ }
736
+
737
+ .term-tab {
738
+ padding: 3px 10px;
739
+ border-radius: 4px 4px 0 0;
740
+ font-size: 11px;
741
+ font-family: var(--mono);
742
+ cursor: pointer;
743
+ border: none;
744
+ background: none;
745
+ color: var(--text2);
746
+ transition: .15s;
747
+ }
748
+
749
+ .term-tab.active {
750
+ background: var(--panel);
751
+ color: var(--accent);
752
+ }
753
+
754
+ .term-tab:hover {
755
+ color: var(--text);
756
+ }
757
+
758
+ .term-tab-close {
759
+ margin-left: 4px;
760
+ opacity: .5;
761
+ }
762
+
763
+ .term-tab-close:hover {
764
+ opacity: 1;
765
+ }
766
+
767
+ .terminal-dock-body {
768
+ flex: 1;
769
+ overflow: hidden;
770
+ display: flex;
771
+ }
772
+
773
+ .terminal-output {
774
+ flex: 1;
775
+ overflow-y: auto;
776
+ padding: 8px 12px;
777
+ font-family: var(--mono);
778
+ font-size: 12px;
779
+ line-height: 1.5;
780
+ color: #c8d3f5;
781
+ white-space: pre-wrap;
782
+ word-break: break-all;
783
+ }
784
+
785
+ .terminal-output .term-line {
786
+ min-height: 18px;
787
+ }
788
+
789
+ .terminal-output .term-prompt {
790
+ color: var(--accent);
791
+ }
792
+
793
+ .terminal-output .term-err {
794
+ color: var(--red);
795
+ }
796
+
797
+ .terminal-output .term-out {
798
+ color: var(--green);
799
+ }
800
+
801
+ .terminal-input-row {
802
+ display: flex;
803
+ align-items: center;
804
+ padding: 4px 12px;
805
+ gap: 8px;
806
+ border-top: 1px solid var(--border);
807
+ background: var(--panel2);
808
+ }
809
+
810
+ .terminal-input-row .prompt {
811
+ color: var(--accent);
812
+ font-family: var(--mono);
813
+ font-size: 12px;
814
+ }
815
+
816
+ .terminal-input {
817
+ flex: 1;
818
+ background: none;
819
+ border: none;
820
+ outline: none;
821
+ color: var(--text);
822
+ font-family: var(--mono);
823
+ font-size: 12px;
824
+ }
825
+
826
+ .terminal-side {
827
+ width: 200px;
828
+ border-left: 1px solid var(--border);
829
+ padding: 8px;
830
+ overflow-y: auto;
831
+ font-size: 11px;
832
+ flex-shrink: 0;
833
+ }
834
+
835
+ .terminal-side h4 {
836
+ font-size: 10px;
837
+ color: var(--text2);
838
+ text-transform: uppercase;
839
+ margin-bottom: 6px;
840
+ }
841
+
842
+ .terminal-side .ts-row {
843
+ padding: 3px 0;
844
+ color: var(--text2);
845
+ display: flex;
846
+ justify-content: space-between;
847
+ }
848
+
849
+ .terminal-side .ts-row .ts-val {
850
+ color: var(--text);
851
+ font-family: var(--mono);
852
+ }
853
+
854
+ .terminal-side .ts-btn {
855
+ width: 100%;
856
+ padding: 4px;
857
+ margin-top: 4px;
858
+ border-radius: 4px;
859
+ border: 1px solid var(--border);
860
+ background: var(--panel);
861
+ color: var(--text2);
862
+ font-size: 10px;
863
+ cursor: pointer;
864
+ text-align: center;
865
+ }
866
+
867
+ .terminal-side .ts-btn:hover {
868
+ border-color: var(--accent);
869
+ color: var(--accent);
870
+ }
871
+
872
+ /* ─── Command Palette ─── */
873
+ .cmd-palette {
874
+ position: fixed;
875
+ top: 20%;
876
+ left: 50%;
877
+ transform: translateX(-50%);
878
+ width: 520px;
879
+ max-width: 90vw;
880
+ z-index: 200;
881
+ background: var(--panel);
882
+ border: 1px solid var(--border2);
883
+ border-radius: 12px;
884
+ box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
885
+ display: none;
886
+ }
887
+
888
+ .cmd-palette.open {
889
+ display: block;
890
+ }
891
+
892
+ .cmd-palette input {
893
+ width: 100%;
894
+ padding: 14px 16px;
895
+ background: none;
896
+ border: none;
897
+ outline: none;
898
+ color: var(--text);
899
+ font-size: 15px;
900
+ border-bottom: 1px solid var(--border);
901
+ }
902
+
903
+ .cmd-palette-results {
904
+ max-height: 320px;
905
+ overflow-y: auto;
906
+ }
907
+
908
+ .cmd-item {
909
+ padding: 10px 16px;
910
+ cursor: pointer;
911
+ display: flex;
912
+ align-items: center;
913
+ gap: 10px;
914
+ border-bottom: 1px solid var(--border);
915
+ transition: .1s;
916
+ }
917
+
918
+ .cmd-item:hover,
919
+ .cmd-item.selected {
920
+ background: var(--panel2);
921
+ }
922
+
923
+ .cmd-item .cmd-icon {
924
+ width: 20px;
925
+ color: var(--accent);
926
+ font-size: 14px;
927
+ }
928
+
929
+ .cmd-item .cmd-label {
930
+ font-size: 13px;
931
+ color: var(--text);
932
+ }
933
+
934
+ .cmd-item .cmd-desc {
935
+ font-size: 11px;
936
+ color: var(--text2);
937
+ margin-left: auto;
938
+ }
939
+
940
+ .cmd-overlay {
941
+ position: fixed;
942
+ inset: 0;
943
+ background: rgba(0, 0, 0, .4);
944
+ z-index: 199;
945
+ display: none;
946
+ }
947
+
948
+ .cmd-overlay.open {
949
+ display: block;
950
+ }
951
+
952
+ /* ─── API Explorer Panel ─── */
953
+ .api-panel {
954
+ position: fixed;
955
+ right: 0;
956
+ top: 0;
957
+ bottom: 0;
958
+ width: 420px;
959
+ background: var(--panel);
960
+ border-left: 1px solid var(--border2);
961
+ z-index: 150;
962
+ transform: translateX(100%);
963
+ transition: transform .25s ease;
964
+ display: flex;
965
+ flex-direction: column;
966
+ }
967
+
968
+ .api-panel.open {
969
+ transform: translateX(0);
970
+ }
971
+
972
+ .api-panel-header {
973
+ padding: 12px 16px;
974
+ border-bottom: 1px solid var(--border);
975
+ display: flex;
976
+ justify-content: space-between;
977
+ align-items: center;
978
+ }
979
+
980
+ .api-panel-header h3 {
981
+ font-size: 13px;
982
+ color: var(--accent);
983
+ }
984
+
985
+ .api-panel-body {
986
+ flex: 1;
987
+ overflow-y: auto;
988
+ padding: 12px;
989
+ }
990
+
991
+ .api-endpoint {
992
+ padding: 10px;
993
+ margin-bottom: 8px;
994
+ border-radius: 8px;
995
+ background: var(--panel2);
996
+ border: 1px solid var(--border);
997
+ cursor: pointer;
998
+ transition: .15s;
999
+ }
1000
+
1001
+ .api-endpoint:hover {
1002
+ border-color: var(--accent);
1003
+ }
1004
+
1005
+ .api-method {
1006
+ font-size: 10px;
1007
+ font-weight: 700;
1008
+ padding: 2px 6px;
1009
+ border-radius: 3px;
1010
+ }
1011
+
1012
+ .api-method.GET {
1013
+ background: rgba(52, 211, 153, .15);
1014
+ color: var(--green);
1015
+ }
1016
+
1017
+ .api-method.POST {
1018
+ background: rgba(0, 212, 255, .15);
1019
+ color: var(--accent);
1020
+ }
1021
+
1022
+ .api-path {
1023
+ font-family: var(--mono);
1024
+ font-size: 12px;
1025
+ color: var(--text);
1026
+ margin-left: 8px;
1027
+ }
1028
+
1029
+ .api-desc {
1030
+ font-size: 11px;
1031
+ color: var(--text2);
1032
+ margin-top: 4px;
1033
+ }
1034
+
1035
+ .api-try-btn {
1036
+ padding: 3px 8px;
1037
+ border-radius: 4px;
1038
+ font-size: 10px;
1039
+ border: 1px solid var(--border);
1040
+ background: var(--panel);
1041
+ color: var(--text2);
1042
+ cursor: pointer;
1043
+ margin-top: 6px;
1044
+ }
1045
+
1046
+ .api-try-btn:hover {
1047
+ border-color: var(--accent);
1048
+ color: var(--accent);
1049
+ }
1050
+
1051
+ .api-response {
1052
+ margin-top: 8px;
1053
+ padding: 8px;
1054
+ border-radius: 6px;
1055
+ background: var(--bg);
1056
+ border: 1px solid var(--border);
1057
+ font-family: var(--mono);
1058
+ font-size: 11px;
1059
+ color: var(--green);
1060
+ max-height: 200px;
1061
+ overflow-y: auto;
1062
+ white-space: pre-wrap;
1063
+ word-break: break-all;
1064
+ display: none;
1065
+ }
1066
+
1067
+ .api-response.open {
1068
+ display: block;
1069
+ }
1070
+
1071
+ /* ─── Toolbar buttons ─── */
1072
+ .toolbar-btn {
1073
+ padding: 4px 10px;
1074
+ border-radius: 6px;
1075
+ border: 1px solid var(--border);
1076
+ background: var(--panel);
1077
+ color: var(--text2);
1078
+ font-size: 11px;
1079
+ cursor: pointer;
1080
+ display: inline-flex;
1081
+ align-items: center;
1082
+ gap: 4px;
1083
+ transition: .15s;
1084
+ }
1085
+
1086
+ .toolbar-btn:hover {
1087
+ border-color: var(--accent);
1088
+ color: var(--accent);
1089
+ }
1090
+
1091
+ .kbd {
1092
+ padding: 1px 5px;
1093
+ border-radius: 3px;
1094
+ background: var(--panel2);
1095
+ border: 1px solid var(--border);
1096
+ font-size: 10px;
1097
+ font-family: var(--mono);
1098
+ color: var(--text2);
1099
+ }
1100
  </style>
1101
  </head>
1102
 
 
1111
  </div>
1112
  </div>
1113
  <div class="status-pill" id="status"><span class="status-dot dot-idle"></span>Idle</div>
1114
+ <div style="display:flex;gap:6px;margin-left:8px">
1115
+ <button class="toolbar-btn" onclick="openCmdPalette()" title="Command Palette">
1116
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1117
+ <path d="M21 21l-4.35-4.35" />
1118
+ <circle cx="11" cy="11" r="8" />
1119
+ </svg>
1120
+ <span class="kbd">⌘K</span>
1121
+ </button>
1122
+ <button class="toolbar-btn" onclick="toggleApiPanel()" title="API Explorer">
1123
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1124
+ <polyline points="16 18 22 12 16 6" />
1125
+ <polyline points="8 6 2 12 8 18" />
1126
+ </svg>
1127
+ API
1128
+ </button>
1129
+ <button class="toolbar-btn" onclick="toggleTerminalDock()" title="Terminal">
1130
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1131
+ <polyline points="4 17 10 11 4 5" />
1132
+ <line x1="12" y1="19" x2="20" y2="19" />
1133
+ </svg>
1134
+ Terminal
1135
+ </button>
1136
+ </div>
1137
  </header>
1138
 
1139
  <div class="controls">
 
1261
  </div>
1262
  </main>
1263
 
1264
+ <!-- Command Palette Overlay -->
1265
+ <div class="cmd-overlay" id="cmdOverlay" onclick="closeCmdPalette()"></div>
1266
+ <div class="cmd-palette" id="cmdPalette">
1267
+ <input type="text" id="cmdInput" placeholder="Search commands..." oninput="filterCmds()"
1268
+ onkeydown="handleCmdKey(event)" />
1269
+ <div class="cmd-palette-results" id="cmdResults"></div>
1270
+ </div>
1271
+
1272
+ <!-- API Explorer Panel -->
1273
+ <div class="api-panel" id="apiPanel">
1274
+ <div class="api-panel-header">
1275
+ <h3>API Explorer</h3>
1276
+ <button class="toolbar-btn" onclick="toggleApiPanel()">βœ•</button>
1277
+ </div>
1278
+ <div class="api-panel-body" id="apiPanelBody">
1279
+ <div style="font-size:11px;color:var(--text2);margin-bottom:12px">
1280
+ ChatGPT Integration: <a href="/gpt-actions.json" target="_blank"
1281
+ style="color:var(--accent)">/gpt-actions.json</a><br>
1282
+ Full Help: <a href="/api/help" target="_blank" style="color:var(--accent)">/api/help</a>
1283
+ </div>
1284
+ <div id="apiEndpoints"></div>
1285
+ </div>
1286
+ </div>
1287
+
1288
+ <!-- Terminal Dock -->
1289
+ <div class="terminal-dock collapsed" id="termDock">
1290
+ <div class="terminal-dock-header" onclick="toggleTerminalDock()">
1291
+ <div class="terminal-dock-tabs" id="termTabs">
1292
+ <div class="term-tab active" onclick="event.stopPropagation()">Terminal</div>
1293
+ </div>
1294
+ <div style="display:flex;gap:6px;align-items:center">
1295
+ <span style="font-size:10px;color:var(--text2)" id="termStatus">no session</span>
1296
+ <button class="toolbar-btn" onclick="event.stopPropagation();createTerminalSession()"
1297
+ style="font-size:10px;padding:2px 8px">+ New</button>
1298
+ <span style="font-size:10px;color:var(--text2)">β–Ύ</span>
1299
+ </div>
1300
+ </div>
1301
+ <div class="terminal-dock-body">
1302
+ <div style="flex:1;display:flex;flex-direction:column">
1303
+ <div class="terminal-output" id="termOutput">
1304
+ <div class="term-line" style="color:var(--text2)">Click "+ New" to create a terminal session. Supports
1305
+ long-running backtests.</div>
1306
+ </div>
1307
+ <div class="terminal-input-row">
1308
+ <span class="prompt">csc$</span>
1309
+ <input class="terminal-input" id="termInput" placeholder="Type a command..."
1310
+ onkeydown="handleTermInput(event)" disabled />
1311
+ </div>
1312
+ </div>
1313
+ <div class="terminal-side" id="termSide">
1314
+ <h4>Session Info</h4>
1315
+ <div class="ts-row"><span>Status</span><span class="ts-val" id="tsStatus">β€”</span></div>
1316
+ <div class="ts-row"><span>Uptime</span><span class="ts-val" id="tsUptime">β€”</span></div>
1317
+ <div class="ts-row"><span>Lines</span><span class="ts-val" id="tsLines">β€”</span></div>
1318
+ <div class="ts-row"><span>Last cmd</span><span class="ts-val" id="tsLastCmd">β€”</span></div>
1319
+ <button class="ts-btn" onclick="pollTerminalOutput()">Refresh Output</button>
1320
+ <button class="ts-btn" onclick="killTerminalSession()"
1321
+ style="color:var(--red);border-color:rgba(248,113,113,.2)">Kill Session</button>
1322
+ </div>
1323
+ </div>
1324
+ </div>
1325
+
1326
  <script>
1327
  const video = document.getElementById("video");
1328
  const canvas = document.getElementById("canvas");
 
1870
  };
1871
  reader.readAsDataURL(blob);
1872
  }
1873
+
1874
+ // ─── Terminal Dock ───
1875
+ let currentTermSession = null;
1876
+ let termPollTimer = null;
1877
+
1878
+ function toggleTerminalDock() {
1879
+ const dock = document.getElementById("termDock");
1880
+ dock.classList.toggle("collapsed");
1881
+ if (!dock.classList.contains("collapsed") && !currentTermSession) {
1882
+ createTerminalSession();
1883
+ }
1884
+ }
1885
+
1886
+ async function createTerminalSession() {
1887
+ try {
1888
+ const resp = await fetch("/terminal/create", { method: "POST" });
1889
+ const data = await resp.json();
1890
+ currentTermSession = data.session_id;
1891
+ document.getElementById("termInput").disabled = false;
1892
+ document.getElementById("termStatus").textContent = data.session_id.slice(0, 12);
1893
+ document.getElementById("termOutput").innerHTML = '<div class="term-line term-prompt">Terminal session created: ' + data.session_id + '</div>';
1894
+ document.getElementById("termInput").focus();
1895
+ startTermPolling();
1896
+ } catch (e) {
1897
+ document.getElementById("termOutput").innerHTML = '<div class="term-line term-err">Error: ' + escapeHtml(e.message) + '</div>';
1898
+ }
1899
+ }
1900
+
1901
+ async function handleTermInput(event) {
1902
+ if (event.key !== "Enter") return;
1903
+ const input = document.getElementById("termInput");
1904
+ const cmd = input.value.trim();
1905
+ if (!cmd || !currentTermSession) return;
1906
+ input.value = "";
1907
+ const outEl = document.getElementById("termOutput");
1908
+ outEl.innerHTML += '<div class="term-line"><span class="term-prompt">csc$</span> ' + escapeHtml(cmd) + '</div>';
1909
+ outEl.scrollTop = outEl.scrollHeight;
1910
+ try {
1911
+ const resp = await fetch("/terminal/" + currentTermSession + "/exec?command=" + encodeURIComponent(cmd), { method: "POST" });
1912
+ const data = await resp.json();
1913
+ if (data.error) {
1914
+ outEl.innerHTML += '<div class="term-line term-err">' + escapeHtml(data.error) + '</div>';
1915
+ } else if (data.output) {
1916
+ outEl.innerHTML += '<div class="term-line term-out">' + escapeHtml(data.output) + '</div>';
1917
+ }
1918
+ outEl.scrollTop = outEl.scrollHeight;
1919
+ updateTermSide(data);
1920
+ } catch (e) {
1921
+ outEl.innerHTML += '<div class="term-line term-err">' + escapeHtml(e.message) + '</div>';
1922
+ }
1923
+ }
1924
+
1925
+ function startTermPolling() {
1926
+ if (termPollTimer) clearInterval(termPollTimer);
1927
+ termPollTimer = setInterval(pollTerminalOutput, 3000);
1928
+ }
1929
+
1930
+ async function pollTerminalOutput() {
1931
+ if (!currentTermSession) return;
1932
+ try {
1933
+ const resp = await fetch("/terminal/" + currentTermSession + "/output");
1934
+ const data = await resp.json();
1935
+ if (data.output) {
1936
+ const outEl = document.getElementById("termOutput");
1937
+ const existing = outEl.querySelector(".term-poll-out");
1938
+ if (existing) existing.remove();
1939
+ outEl.innerHTML += '<div class="term-line term-out term-poll-out">' + escapeHtml(data.output.slice(-2000)) + '</div>';
1940
+ outEl.scrollTop = outEl.scrollHeight;
1941
+ }
1942
+ document.getElementById("tsStatus").textContent = data.is_alive ? "alive" : "dead";
1943
+ document.getElementById("tsLines").textContent = data.line_count || 0;
1944
+ } catch (e) { }
1945
+ }
1946
+
1947
+ async function updateTermSide(data) {
1948
+ try {
1949
+ const resp = await fetch("/terminal/" + currentTermSession + "/status");
1950
+ const s = await resp.json();
1951
+ document.getElementById("tsStatus").textContent = s.alive ? "alive" : "dead";
1952
+ document.getElementById("tsUptime").textContent = s.uptime_s + "s";
1953
+ document.getElementById("tsLines").textContent = s.buffer_lines;
1954
+ document.getElementById("tsLastCmd").textContent = (s.last_command || "β€”").slice(0, 20);
1955
+ } catch (e) { }
1956
+ }
1957
+
1958
+ async function killTerminalSession() {
1959
+ if (!currentTermSession) return;
1960
+ try {
1961
+ await fetch("/terminal/" + currentTermSession + "/kill", { method: "POST" });
1962
+ document.getElementById("termOutput").innerHTML += '<div class="term-line term-err">Session killed</div>';
1963
+ currentTermSession = null;
1964
+ document.getElementById("termInput").disabled = true;
1965
+ document.getElementById("termStatus").textContent = "no session";
1966
+ if (termPollTimer) { clearInterval(termPollTimer); termPollTimer = null; }
1967
+ } catch (e) { }
1968
+ }
1969
+
1970
+ // ─── Command Palette ───
1971
+ const commands = [
1972
+ { icon: "β–Ά", label: "Start Engine", desc: "Begin live capture", action: () => startLive().catch(e => setStatus(e.message, "dot-error")) },
1973
+ { icon: "β– ", label: "Stop Engine", desc: "Stop live capture", action: stopLive },
1974
+ { icon: "⚑", label: "Force Generate", desc: "Force code generation", action: () => sendFrame(true) },
1975
+ { icon: "🎀", label: "Start Speech", desc: "Begin speech recognition", action: startSpeech },
1976
+ { icon: "πŸ”΄", label: "Record Audio", desc: "Toggle audio recording", action: toggleRecording },
1977
+ { icon: "πŸ“–", label: "Open Lab Diary", desc: "View all artifacts", action: () => window.open("/diary", "_blank") },
1978
+ { icon: "πŸ“", label: "Open API Help", desc: "List all endpoints", action: () => window.open("/api/help", "_blank") },
1979
+ { icon: "πŸ”Œ", label: "GPT Actions Schema", desc: "ChatGPT integration schema", action: () => window.open("/gpt-actions.json", "_blank") },
1980
+ { icon: "πŸ–₯", label: "Toggle Terminal", desc: "Open/close terminal dock", action: toggleTerminalDock },
1981
+ { icon: "πŸ”", label: "Toggle API Explorer", desc: "Open/close API panel", action: toggleApiPanel },
1982
+ { icon: "πŸ“Š", label: "List Artifacts", desc: "Fetch all artifacts as JSON", action: () => fetch("/api/artifacts").then(r => r.json()).then(d => console.log(d)) },
1983
+ { icon: "πŸ“‹", label: "List Terminal Sessions", desc: "Show active terminals", action: () => fetch("/terminal/sessions").then(r => r.json()).then(d => console.log(d)) },
1984
+ { 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")) },
1985
+ { icon: "πŸ§ͺ", label: "New Terminal Session", desc: "Create long-running terminal", action: () => { toggleTerminalDock(); if (!currentTermSession) createTerminalSession(); } },
1986
+ ];
1987
+
1988
+ let cmdSelectedIndex = 0;
1989
+
1990
+ function openCmdPalette() {
1991
+ document.getElementById("cmdOverlay").classList.add("open");
1992
+ document.getElementById("cmdPalette").classList.add("open");
1993
+ const input = document.getElementById("cmdInput");
1994
+ input.value = "";
1995
+ input.focus();
1996
+ cmdSelectedIndex = 0;
1997
+ renderCmds(commands);
1998
+ }
1999
+
2000
+ function closeCmdPalette() {
2001
+ document.getElementById("cmdOverlay").classList.remove("open");
2002
+ document.getElementById("cmdPalette").classList.remove("open");
2003
+ }
2004
+
2005
+ function filterCmds() {
2006
+ const q = document.getElementById("cmdInput").value.toLowerCase();
2007
+ const filtered = commands.filter(c => c.label.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q));
2008
+ cmdSelectedIndex = 0;
2009
+ renderCmds(filtered);
2010
+ }
2011
+
2012
+ function renderCmds(cmds) {
2013
+ const el = document.getElementById("cmdResults");
2014
+ el.innerHTML = cmds.map((c, i) =>
2015
+ '<div class="cmd-item' + (i === cmdSelectedIndex ? ' selected' : '') + '" onclick="execCmd(' + commands.indexOf(c) + ')">' +
2016
+ '<span class="cmd-icon">' + c.icon + '</span>' +
2017
+ '<span class="cmd-label">' + c.label + '</span>' +
2018
+ '<span class="cmd-desc">' + c.desc + '</span></div>'
2019
+ ).join("");
2020
+ }
2021
+
2022
+ function execCmd(idx) {
2023
+ closeCmdPalette();
2024
+ if (commands[idx]) commands[idx].action();
2025
+ }
2026
+
2027
+ function handleCmdKey(event) {
2028
+ const q = document.getElementById("cmdInput").value.toLowerCase();
2029
+ const filtered = commands.filter(c => c.label.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q));
2030
+ if (event.key === "ArrowDown") { event.preventDefault(); cmdSelectedIndex = Math.min(cmdSelectedIndex + 1, filtered.length - 1); renderCmds(filtered); }
2031
+ else if (event.key === "ArrowUp") { event.preventDefault(); cmdSelectedIndex = Math.max(cmdSelectedIndex - 1, 0); renderCmds(filtered); }
2032
+ else if (event.key === "Enter") { event.preventDefault(); if (filtered[cmdSelectedIndex]) execCmd(commands.indexOf(filtered[cmdSelectedIndex])); }
2033
+ else if (event.key === "Escape") { closeCmdPalette(); }
2034
+ }
2035
+
2036
+ // ─── API Explorer ───
2037
+ function toggleApiPanel() {
2038
+ document.getElementById("apiPanel").classList.toggle("open");
2039
+ if (document.getElementById("apiPanel").classList.contains("open")) loadApiEndpoints();
2040
+ }
2041
+
2042
+ async function loadApiEndpoints() {
2043
+ try {
2044
+ const resp = await fetch("/api/help");
2045
+ const data = await resp.json();
2046
+ const el = document.getElementById("apiEndpoints");
2047
+ el.innerHTML = data.endpoints.map((e, i) =>
2048
+ '<div class="api-endpoint" onclick="toggleApiResponse(' + i + ')">' +
2049
+ '<span class="api-method ' + e.method + '">' + e.method + '</span>' +
2050
+ '<span class="api-path">' + e.path + '</span>' +
2051
+ '<div class="api-desc">' + e.description + '</div>' +
2052
+ '<button class="api-try-btn" onclick="event.stopPropagation();tryEndpoint(' + i + ')">Try it</button>' +
2053
+ '<div class="api-response" id="apiResp' + i + '"></div>' +
2054
+ '</div>'
2055
+ ).join("");
2056
+ window._apiEndpoints = data.endpoints;
2057
+ } catch (e) {
2058
+ document.getElementById("apiEndpoints").innerHTML = '<div style="color:var(--red)">Error loading: ' + escapeHtml(e.message) + '</div>';
2059
+ }
2060
+ }
2061
+
2062
+ function toggleApiResponse(i) {
2063
+ const el = document.getElementById("apiResp" + i);
2064
+ el.classList.toggle("open");
2065
+ }
2066
+
2067
+ async function tryEndpoint(i) {
2068
+ const ep = window._apiEndpoints[i];
2069
+ const el = document.getElementById("apiResp" + i);
2070
+ el.classList.add("open");
2071
+ el.textContent = "Loading...";
2072
+ try {
2073
+ const opts = { method: ep.method };
2074
+ const resp = await fetch(ep.path, opts);
2075
+ const text = await resp.text();
2076
+ el.textContent = text.slice(0, 2000);
2077
+ } catch (e) {
2078
+ el.textContent = "Error: " + e.message;
2079
+ }
2080
+ }
2081
+
2082
+ // ─── Keyboard Shortcuts ───
2083
+ document.addEventListener("keydown", (e) => {
2084
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
2085
+ e.preventDefault();
2086
+ openCmdPalette();
2087
+ }
2088
+ if (e.key === "Escape") {
2089
+ closeCmdPalette();
2090
+ if (document.getElementById("apiPanel").classList.contains("open")) document.getElementById("apiPanel").classList.remove("open");
2091
+ }
2092
+ });
2093
  </script>
2094
  </body>
2095