diff infinite_canvas/web_surface_web.c @ 280:49e9e591c9bb

Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 19:14:53 -0700
parents 8d560f50ed4c
children
line wrap: on
line diff
--- a/infinite_canvas/web_surface_web.c	Mon Aug 17 22:22:36 2026 -0700
+++ b/infinite_canvas/web_surface_web.c	Tue Aug 18 19:14:53 2026 -0700
@@ -9,6 +9,69 @@
     });
 });
 
+EM_JS(void, Canvas_Web_Dictation_Init, (), {
+    if (Module.canvasDictationInitialized) return;
+    Module.canvasDictationInitialized = true;
+    Module.canvasDictationQueue = [];
+    window.addEventListener("message", event => {
+        if (event.data?.type !== "zenbu.dictation.event") return;
+        const frame = document.getElementById("canvas-dictation-transport");
+        if (!frame || frame.contentWindow !== event.source) return;
+        Module.canvasDictationQueue.push({
+            kind: String(event.data.kind || ""),
+            text: String(event.data.text || ""),
+        });
+    });
+});
+
+EM_JS(void, Canvas_Web_Dictation_Set_Active, (int active), {
+    let frame = document.getElementById("canvas-dictation-transport");
+    if (!frame) {
+        frame = document.createElement("iframe");
+        frame.id = "canvas-dictation-transport";
+        frame.title = "Canvas dictation transport";
+        frame.allow = "microphone";
+        frame.sandbox =
+            "allow-same-origin allow-scripts";
+        frame.style.position = "fixed";
+        frame.style.left = "-2px";
+        frame.style.top = "-2px";
+        frame.style.width = "1px";
+        frame.style.height = "1px";
+        frame.style.opacity = "0";
+        frame.style.pointerEvents = "none";
+        frame.src = "http://127.0.0.1:8090/?canvas=1";
+        document.body.appendChild(frame);
+    }
+    const send = () => frame.contentWindow.postMessage(
+        {
+            type: "zenbu.dictation.set-active",
+            active: active !== 0,
+        },
+        "*");
+    frame.addEventListener("load", send, {once: true});
+    send();
+});
+
+EM_JS(void, Canvas_Web_Dictation_Commit, (), {
+    const frame = document.getElementById("canvas-dictation-transport");
+    frame?.contentWindow?.postMessage(
+        {type: "zenbu.dictation.commit"},
+        "*");
+});
+
+EM_JS(int, Canvas_Web_Dictation_Consume, (
+    char *p_text,
+    int text_capacity), {
+    const event = Module.canvasDictationQueue?.shift();
+    if (!event) return 0;
+    stringToUTF8(event.text, p_text, text_capacity);
+    if (event.kind === "status") return 1;
+    if (event.kind === "partial") return 2;
+    if (event.kind === "final") return 3;
+    return 0;
+});
+
 EM_JS(void, Canvas_Web_Create_Or_Update, (
     uint32 entity_id,
     const char *p_url,
@@ -170,6 +233,7 @@
         }
         surface.remove();
     });
+    document.getElementById("canvas-dictation-transport")?.remove();
 });
 
 static Rectangle Canvas_Web_Content_Bounds(
@@ -236,6 +300,7 @@
     p_surface->p_arena = p_arena;
     p_surface->dark_mode = dark_mode;
     p_surface->initialized = TRUE;
+    Canvas_Web_Dictation_Init();
     return TRUE;
 }
 
@@ -369,6 +434,32 @@
     Canvas_Web_History_Go(entity_id, 1);
 }
 
+void Canvas_Web_Surface_Set_Dictation_Active(
+    Canvas_Web_Surface *p_surface,
+    boolean active)
+{
+    (void)p_surface;
+    Canvas_Web_Dictation_Set_Active(active);
+}
+
+void Canvas_Web_Surface_Commit_Dictation(
+    Canvas_Web_Surface *p_surface)
+{
+    (void)p_surface;
+    Canvas_Web_Dictation_Commit();
+}
+
+Canvas_Dictation_Event Canvas_Web_Surface_Consume_Dictation(
+    Canvas_Web_Surface *p_surface,
+    char *p_text,
+    size_t text_capacity)
+{
+    (void)p_surface;
+    return (Canvas_Dictation_Event)Canvas_Web_Dictation_Consume(
+        p_text,
+        (int)text_capacity);
+}
+
 boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface)
 {
     (void)p_surface;