From 3816a9627e1f8fb362f0481bc21f65a68cc502e5 Mon Sep 17 00:00:00 2001 From: kacper Date: Thu, 5 Mar 2026 15:10:14 -0500 Subject: [PATCH] api channel and tools --- app.py | 21 +- static/index.html | 431 +++++++++++++++++++++++++++--- supertonic_gateway.py | 603 ++++++++++++++++-------------------------- voice_rtc.py | 211 ++++----------- 4 files changed, 684 insertions(+), 582 deletions(-) diff --git a/app.py b/app.py index 9d21706..56c3116 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Awaitable, Callable from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from supertonic_gateway import SuperTonicGateway @@ -28,8 +28,9 @@ async def health() -> JSONResponse: @app.get("/") -async def index() -> FileResponse: - return FileResponse(INDEX_PATH) +async def index() -> HTMLResponse: + html = INDEX_PATH.read_text(encoding="utf-8") + return HTMLResponse(content=html) @app.websocket("/ws/chat") @@ -65,18 +66,24 @@ async def websocket_chat(websocket: WebSocket) -> None: elif msg_type == "rtc-ice-candidate": await voice_session.handle_ice_candidate(message) elif msg_type == "voice-ptt": - voice_session.set_push_to_talk_pressed( - bool(message.get("pressed", False)) - ) + voice_session.set_push_to_talk_pressed(bool(message.get("pressed", False))) elif msg_type == "user-message": await gateway.send_user_message(str(message.get("text", ""))) + elif msg_type == "ui-response": + await gateway.send_ui_response( + str(message.get("request_id", "")), + str(message.get("value", "")), + ) + elif msg_type == "command": + await gateway.send_command(str(message.get("command", ""))) else: await safe_send_json( { "role": "system", "text": ( "Unknown message type. Use spawn, stop, rtc-offer, " - "rtc-ice-candidate, voice-ptt, or user-message." + "rtc-ice-candidate, voice-ptt, user-message, " + "ui-response, or command." ), "timestamp": "", } diff --git a/static/index.html b/static/index.html index 4139412..0cc11ff 100644 --- a/static/index.html +++ b/static/index.html @@ -16,7 +16,7 @@ width: 100%; height: 100%; overflow: hidden; - background: #1a1510; + background: #ffffff; touch-action: none; } #log { @@ -31,7 +31,7 @@ font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.6; - color: rgba(255, 245, 235, 0.35); + color: rgba(30, 20, 10, 0.35); white-space: pre-wrap; word-break: break-word; display: flex; @@ -45,8 +45,8 @@ mask-image: linear-gradient(to top, black 55%, transparent 100%); } #log:hover { - color: rgba(255, 245, 235, 0.92); - background: rgba(0, 0, 0, 0.18); + color: rgba(30, 20, 10, 0.85); + background: rgba(0, 0, 0, 0.06); -webkit-mask-image: none; mask-image: none; } @@ -62,17 +62,17 @@ margin-bottom: 4px; } .line.user { - color: rgba(255, 255, 255, 0.9); + color: rgba(20, 10, 0, 0.85); } .line.system { - color: rgba(255, 220, 180, 0.5); + color: rgba(120, 80, 40, 0.5); } .line.wisper { - color: rgba(255, 200, 160, 0.4); + color: rgba(120, 80, 40, 0.4); } - #log:hover .line.user { color: rgba(255, 255, 255, 1.0); } - #log:hover .line.system { color: rgba(255, 220, 180, 0.85); } - #log:hover .line.wisper { color: rgba(255, 200, 160, 0.75); } + #log:hover .line.user { color: rgba(20, 10, 0, 1.0); } + #log:hover .line.system { color: rgba(120, 80, 40, 0.85); } + #log:hover .line.wisper { color: rgba(120, 80, 40, 0.75); } #voiceStatus { position: fixed; bottom: 12px; @@ -119,11 +119,14 @@ border-radius: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 4px 4px 0px rgba(0,0,0,0.15); overflow: hidden; + pointer-events: auto; + cursor: pointer; } #agentViz canvas { width: 100% !important; height: 100% !important; display: block; + pointer-events: auto; } #agentIndicator .label { display: none; @@ -140,10 +143,6 @@ #agentIndicator.speaking { color: #8b4513; } - /* Deepen the background while PTT is active */ - body.ptt-active { - background: radial-gradient(ellipse at 50% 44%, #f2caa8 0%, #e8b898 100%); - } #controls { position: fixed; top: 12px; @@ -167,20 +166,236 @@ transform: translateY(1px); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); } + + /* Toast notifications */ + #toast-container { + position: fixed; + top: 16px; + left: 50%; + transform: translateX(-50%); + width: min(92vw, 480px); + max-height: calc(100vh - 32px); + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + gap: 10px; + z-index: 100; + pointer-events: auto; + /* Hide scrollbar until hovered */ + scrollbar-width: thin; + scrollbar-color: rgba(255,200,140,0.25) transparent; + padding-bottom: 4px; + } + #toast-container::-webkit-scrollbar { + width: 4px; + } + #toast-container::-webkit-scrollbar-track { + background: transparent; + } + #toast-container::-webkit-scrollbar-thumb { + background: rgba(255,200,140,0.25); + border-radius: 2px; + } + .toast { + pointer-events: auto; + background: rgba(28, 22, 16, 0.92); + border: 1px solid rgba(255, 200, 140, 0.18); + border-radius: 12px; + padding: 14px 16px 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.45); + animation: toast-in 0.22s cubic-bezier(0.34, 1.4, 0.64, 1) both; + position: relative; + overflow: hidden; + max-width: 100%; + } + .toast.dismissing { + animation: toast-out 0.18s ease-in both; + } + @keyframes toast-in { + from { opacity: 0; transform: translateY(-14px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } + } + @keyframes toast-out { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-10px) scale(0.96); } + } + .toast-progress { + position: absolute; + bottom: 0; + left: 0; + height: 2px; + background: rgba(255, 190, 120, 0.55); + width: 100%; + transform-origin: left; + animation: toast-progress-shrink linear both; + } + @keyframes toast-progress-shrink { + from { transform: scaleX(1); } + to { transform: scaleX(0); } + } + .toast-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 10px; + } + .toast-title { + font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.07em; + color: rgba(255, 200, 140, 0.85); + text-transform: uppercase; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .toast-close { + background: none; + border: none; + color: rgba(255, 245, 235, 0.35); + font-size: 16px; + line-height: 1; + cursor: pointer; + padding: 0 2px; + flex-shrink: 0; + transition: color 0.15s; + } + .toast-close:hover { + color: rgba(255, 245, 235, 0.85); + } + .toast-body { + font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; + font-size: 12px; + line-height: 1.65; + color: rgba(255, 245, 235, 0.82); + white-space: normal; + word-break: break-word; + user-select: text; + -webkit-user-select: text; + } + .toast-body p { margin: 0 0 6px; } + .toast-body p:last-child { margin-bottom: 0; } + .toast-body h1, .toast-body h2, .toast-body h3, + .toast-body h4, .toast-body h5, .toast-body h6 { + font-size: 13px; + font-weight: 700; + color: rgba(255, 200, 140, 0.95); + margin: 8px 0 4px; + } + .toast-body ul, .toast-body ol { + margin: 4px 0 6px; + padding-left: 18px; + } + .toast-body li { margin-bottom: 2px; } + .toast-body code { + background: rgba(255,255,255,0.07); + border-radius: 4px; + padding: 1px 5px; + font-size: 11px; + } + .toast-body pre { + background: rgba(0,0,0,0.35); + border-radius: 6px; + padding: 8px 10px; + overflow-x: auto; + margin: 6px 0; + } + .toast-body pre code { + background: none; + padding: 0; + font-size: 11px; + } + .toast-body table { + border-collapse: collapse; + width: 100%; + font-size: 11px; + margin: 6px 0; + } + .toast-body th, .toast-body td { + border: 1px solid rgba(255,200,140,0.2); + padding: 4px 8px; + text-align: left; + } + .toast-body th { + background: rgba(255,200,140,0.08); + color: rgba(255,200,140,0.9); + font-weight: 600; + } + .toast-body a { + color: rgba(255,200,140,0.85); + text-decoration: underline; + } + .toast-body blockquote { + border-left: 3px solid rgba(255,200,140,0.3); + margin: 6px 0; + padding-left: 10px; + color: rgba(255,245,235,0.55); + } + .toast-body hr { + border: none; + border-top: 1px solid rgba(255,200,140,0.15); + margin: 8px 0; + } + .toast-choices { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; + } + .toast-choice-btn { + background: rgba(255, 200, 140, 0.12); + border: 1px solid rgba(255, 200, 140, 0.35); + border-radius: 8px; + color: rgba(255, 245, 235, 0.90); + font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; + font-size: 12px; + padding: 6px 14px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + flex: 1 1 auto; + text-align: center; + } + .toast-choice-btn:hover { + background: rgba(255, 200, 140, 0.25); + border-color: rgba(255, 200, 140, 0.65); + } + .toast-choice-btn:active { + background: rgba(255, 200, 140, 0.38); + } + .toast-choice-btn:disabled { + opacity: 0.4; + cursor: default; + } + .toast-image { + width: 100%; + max-height: 320px; + object-fit: contain; + border-radius: 8px; + display: block; + } -
- +
+
-
-
+
+
+
+