comparison infinite_canvas/main.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 c57149ad216e
comparison
equal deleted inserted replaced
279:b3b547563ec7 280:49e9e591c9bb
1 #include "infinite_canvas/agent_service.h"
1 #include "infinite_canvas/canvas.h" 2 #include "infinite_canvas/canvas.h"
2 #include "infinite_canvas/web_surface.h" 3 #include "infinite_canvas/web_surface.h"
3 4
4 #if defined(INFINITE_CANVAS_DEV_UI) 5 #if defined(INFINITE_CANVAS_DEV_UI)
5 #include "infinite_canvas/dev_ui.h" 6 #include "infinite_canvas/dev_ui.h"
7 8
8 #if defined(PLATFORM_WEB) 9 #if defined(PLATFORM_WEB)
9 #include <emscripten/emscripten.h> 10 #include <emscripten/emscripten.h>
10 #endif 11 #endif
11 12
13 #include <math.h>
12 #include <stdio.h> 14 #include <stdio.h>
13 #include <stdlib.h> 15 #include <stdlib.h>
14 #include <string.h> 16 #include <string.h>
15 17
16 typedef struct { 18 typedef struct {
17 Dowa_Arena *p_arena; 19 Dowa_Arena *p_arena;
18 Canvas_Camera camera; 20 Canvas_Camera camera;
19 Canvas_Scene scene; 21 Canvas_Scene scene;
20 Canvas_Web_Surface web_surface; 22 Canvas_Web_Surface web_surface;
23 Canvas_Agent_Service agent_service;
21 Font font; 24 Font font;
22 Canvas_Theme theme; 25 Canvas_Theme theme;
23 boolean owns_font; 26 boolean owns_font;
24 const char *p_screenshot_path; 27 const char *p_screenshot_path;
25 uint32 frame_count; 28 uint32 frame_count;
29 uint32 screenshot_frame;
30 uint32 pending_conversation_id;
31 uint32 dictation_entity_id;
32 boolean agent_initialized;
33 boolean dictation_overlay_visible;
34 boolean dictation_listening;
35 boolean dictation_requested_active;
36 boolean dictation_send_pending;
37 char dictation_status[128];
38 char dictation_partial[CANVAS_ENTITY_TEXT_CAPACITY];
39 char dictation_transcript[CANVAS_ENTITY_TEXT_CAPACITY];
40 char pending_prompt[CANVAS_ENTITY_TEXT_CAPACITY];
41 char agent_context[CANVAS_CONTEXT_MAX_LENGTH];
26 #if defined(INFINITE_CANVAS_DEV_UI) 42 #if defined(INFINITE_CANVAS_DEV_UI)
27 Canvas_Dev_UI dev_ui; 43 Canvas_Dev_UI dev_ui;
28 #endif 44 #endif
29 } Canvas_App; 45 } Canvas_App;
30 46
31 static Canvas_App *g_app = NULL; 47 static Canvas_App *g_app = NULL;
48
49 static const char *Canvas_App_JSON_String(
50 const char *p_json,
51 const char *p_key,
52 char *p_output,
53 size_t output_capacity)
54 {
55 char needle[128];
56 snprintf(needle, sizeof(needle), "\"%s\"", p_key);
57 const char *p_cursor = strstr(p_json, needle);
58 if (!p_cursor) return NULL;
59 p_cursor = strchr(p_cursor + strlen(needle), ':');
60 if (!p_cursor) return NULL;
61 while (*++p_cursor == ' ') {}
62 if (*p_cursor != '"') return NULL;
63 p_cursor++;
64 size_t output = 0;
65 while (*p_cursor && *p_cursor != '"' && output + 1 < output_capacity) {
66 if (*p_cursor == '\\' && p_cursor[1]) {
67 p_cursor++;
68 if (*p_cursor == 'n') p_output[output++] = '\n';
69 else if (*p_cursor == 't') p_output[output++] = '\t';
70 else if (*p_cursor != 'r') p_output[output++] = *p_cursor;
71 } else {
72 p_output[output++] = *p_cursor;
73 }
74 p_cursor++;
75 }
76 p_output[output] = '\0';
77 return p_output;
78 }
79
80 static uint32 Canvas_App_JSON_Uint(const char *p_json, const char *p_key)
81 {
82 char needle[128];
83 snprintf(needle, sizeof(needle), "\"%s\"", p_key);
84 const char *p_cursor = strstr(p_json, needle);
85 if (!p_cursor) return 0;
86 p_cursor = strchr(p_cursor + strlen(needle), ':');
87 if (!p_cursor) return 0;
88 return (uint32)strtoul(p_cursor + 1, NULL, 10);
89 }
90
91 static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id)
92 {
93 for (size_t index = 0;
94 index < Dowa_Array_Length(g_app->scene.p_entities);
95 index++) {
96 if (g_app->scene.p_entities[index].id == entity_id) {
97 return &g_app->scene.p_entities[index];
98 }
99 }
100 return NULL;
101 }
102
103 static void Canvas_App_Set_Conversation_Text(
104 Canvas_Entity *p_entity,
105 const char *p_prompt,
106 const char *p_speaker,
107 const char *p_response)
108 {
109 size_t length = 0;
110 int32 written = snprintf(
111 p_entity->text,
112 sizeof(p_entity->text),
113 "You\n");
114 if (written > 0) length = (size_t)written;
115 written = snprintf(
116 p_entity->text + length,
117 sizeof(p_entity->text) - length,
118 "%.*s\n\n%s\n",
119 900,
120 p_prompt,
121 p_speaker);
122 if (written > 0) length += (size_t)written;
123 if (length >= sizeof(p_entity->text)) {
124 length = sizeof(p_entity->text) - 1;
125 }
126 snprintf(
127 p_entity->text + length,
128 sizeof(p_entity->text) - length,
129 "%.*s",
130 900,
131 p_response);
132 Canvas_Conversation_Scroll_To_End(p_entity, g_app->font);
133 }
134
135 static boolean Canvas_App_Submit_Prompt(const char *p_prompt)
136 {
137 if (!p_prompt || !p_prompt[0]) return FALSE;
138 Canvas_Scene_Build_Visible_Context(
139 &g_app->scene,
140 &g_app->camera,
141 g_app->agent_context,
142 sizeof(g_app->agent_context));
143 if (!Canvas_Agent_Service_Submit(
144 &g_app->agent_service,
145 p_prompt,
146 g_app->agent_context)) {
147 return FALSE;
148 }
149 snprintf(
150 g_app->pending_prompt,
151 sizeof(g_app->pending_prompt),
152 "%s",
153 p_prompt);
154 Vector2 position = Canvas_Camera_Screen_To_World(
155 &g_app->camera,
156 (Vector2){
157 (float)g_app->camera.viewport_width * 0.5f - 210.0f,
158 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
159 });
160 g_app->pending_conversation_id = Canvas_Scene_Add_Conversation(
161 &g_app->scene,
162 "Routing thought...",
163 p_prompt,
164 "Copilot is deciding whether to create or extend a session.",
165 position);
166 Canvas_Entity *p_pending =
167 Canvas_App_Find_Entity(g_app->pending_conversation_id);
168 if (p_pending) p_pending->agent_working = TRUE;
169 return TRUE;
170 }
171
172 static void Canvas_App_Apply_Agent_Response(const char *p_json)
173 {
174 char action[32] = {0};
175 char title[128] = {0};
176 char response[CANVAS_ENTITY_TEXT_CAPACITY] = {0};
177 Canvas_App_JSON_String(p_json, "action", action, sizeof(action));
178 Canvas_App_JSON_String(p_json, "title", title, sizeof(title));
179 if (!Canvas_App_JSON_String(
180 p_json,
181 "response",
182 response,
183 sizeof(response))) {
184 snprintf(response, sizeof(response), "%s", p_json);
185 }
186 uint32 conversation_id =
187 Canvas_App_JSON_Uint(p_json, "conversation_id");
188 Canvas_Entity *p_pending =
189 Canvas_App_Find_Entity(g_app->pending_conversation_id);
190 if (strcmp(action, "append") == 0 &&
191 conversation_id != 0 &&
192 Canvas_Scene_Append_Conversation(
193 &g_app->scene,
194 conversation_id,
195 g_app->pending_prompt,
196 response)) {
197 Canvas_Entity *p_conversation =
198 Canvas_App_Find_Entity(conversation_id);
199 Canvas_Conversation_Scroll_To_End(
200 p_conversation,
201 g_app->font);
202 if (p_pending) p_pending->removing = TRUE;
203 } else if (p_pending) {
204 snprintf(
205 p_pending->label,
206 sizeof(p_pending->label),
207 "%s",
208 title[0] ? title : "Copilot session");
209 Canvas_App_Set_Conversation_Text(
210 p_pending,
211 g_app->pending_prompt,
212 "Copilot",
213 response);
214 p_pending->agent_working = FALSE;
215 } else {
216 Vector2 position = Canvas_Camera_Screen_To_World(
217 &g_app->camera,
218 (Vector2){
219 (float)g_app->camera.viewport_width * 0.5f - 210.0f,
220 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
221 });
222 uint32 created_id = Canvas_Scene_Add_Conversation(
223 &g_app->scene,
224 title[0] ? title : "Copilot session",
225 g_app->pending_prompt,
226 response,
227 position);
228 Canvas_Conversation_Scroll_To_End(
229 Canvas_App_Find_Entity(created_id),
230 g_app->font);
231 }
232 g_app->pending_conversation_id = 0;
233 g_app->pending_prompt[0] = '\0';
234 }
235
236 static Canvas_Entity *Canvas_App_Ensure_Dictation_Entity(void)
237 {
238 Canvas_Entity *p_entity =
239 Canvas_App_Find_Entity(g_app->dictation_entity_id);
240 if (p_entity) return p_entity;
241
242 Vector2 center = Canvas_Camera_Screen_To_World(
243 &g_app->camera,
244 (Vector2){
245 (float)g_app->camera.viewport_width * 0.5f,
246 (float)g_app->camera.viewport_height * 0.5f,
247 });
248 Vector2 size = {520.0f, 240.0f};
249 if (!Canvas_Scene_Add(
250 &g_app->scene,
251 CANVAS_ENTITY_TEXT_AREA,
252 (Vector2){
253 center.x - size.x * 0.5f,
254 center.y - size.y * 0.5f,
255 })) {
256 return NULL;
257 }
258 size_t index = Dowa_Array_Length(g_app->scene.p_entities) - 1;
259 p_entity = &g_app->scene.p_entities[index];
260 p_entity->size = size;
261 snprintf(p_entity->label, sizeof(p_entity->label), "Dictation");
262 g_app->dictation_entity_id = p_entity->id;
263 g_app->scene.selected_index = (int32)index;
264 return p_entity;
265 }
266
267 static void Canvas_App_Sync_Dictation_Entity(void)
268 {
269 Canvas_Entity *p_entity =
270 Canvas_App_Find_Entity(g_app->dictation_entity_id);
271 if (!p_entity) return;
272
273 char text[CANVAS_ENTITY_TEXT_CAPACITY];
274 snprintf(text, sizeof(text), "%s", g_app->dictation_transcript);
275 size_t length = strlen(text);
276 if (length > 0 && g_app->dictation_partial[0] &&
277 length + 1 < sizeof(text)) {
278 text[length++] = ' ';
279 text[length] = '\0';
280 }
281 snprintf(
282 text + length,
283 sizeof(text) - length,
284 "%s",
285 g_app->dictation_partial);
286 Canvas_Text_Area_Set_Content(
287 p_entity,
288 g_app->font,
289 text,
290 g_app->dictation_partial[0] ? TRUE : FALSE);
291 }
292
293 static void Canvas_App_Set_Dictation_Active(boolean active)
294 {
295 g_app->dictation_overlay_visible = TRUE;
296 g_app->dictation_requested_active = active;
297 g_app->dictation_listening = FALSE;
298 g_app->dictation_send_pending = FALSE;
299 if (active) {
300 Canvas_App_Ensure_Dictation_Entity();
301 Canvas_App_Sync_Dictation_Entity();
302 }
303 snprintf(
304 g_app->dictation_status,
305 sizeof(g_app->dictation_status),
306 "%s",
307 active ? "Starting microphone..." : "Stopping microphone...");
308 Canvas_Web_Surface_Set_Dictation_Active(
309 &g_app->web_surface,
310 active);
311 }
312
313 static boolean Canvas_App_Submit_Dictation(void)
314 {
315 if (!g_app->dictation_transcript[0]) return FALSE;
316 if (!Canvas_App_Submit_Prompt(g_app->dictation_transcript)) {
317 g_app->dictation_send_pending = FALSE;
318 snprintf(
319 g_app->dictation_status,
320 sizeof(g_app->dictation_status),
321 "Copilot is busy; press Enter to retry");
322 return FALSE;
323 }
324 Canvas_Entity *p_entity =
325 Canvas_App_Find_Entity(g_app->dictation_entity_id);
326 if (p_entity) p_entity->text_muted = FALSE;
327 g_app->dictation_transcript[0] = '\0';
328 g_app->dictation_partial[0] = '\0';
329 Canvas_App_Sync_Dictation_Entity();
330 g_app->dictation_send_pending = FALSE;
331 g_app->dictation_overlay_visible =
332 g_app->dictation_listening;
333 return TRUE;
334 }
335
336 static void Canvas_App_Append_Dictation(const char *p_text)
337 {
338 if (!p_text || !p_text[0]) return;
339 size_t length = strlen(g_app->dictation_transcript);
340 if (length > 0 && length + 1 < sizeof(g_app->dictation_transcript)) {
341 g_app->dictation_transcript[length++] = ' ';
342 g_app->dictation_transcript[length] = '\0';
343 }
344 size_t available = sizeof(g_app->dictation_transcript) - length - 1;
345 size_t text_length = strlen(p_text);
346 size_t copy_length = text_length < available ? text_length : available;
347 memcpy(g_app->dictation_transcript + length, p_text, copy_length);
348 g_app->dictation_transcript[length + copy_length] = '\0';
349 Canvas_App_Sync_Dictation_Entity();
350 }
351
352 static void Canvas_App_Update_Dictation_Overlay_Input(void)
353 {
354 if (!g_app->dictation_overlay_visible) return;
355 if (g_app->dictation_listening && IsKeyPressed(KEY_BACKSPACE)) {
356 g_app->dictation_send_pending = FALSE;
357 g_app->dictation_partial[0] = '\0';
358 g_app->dictation_transcript[0] = '\0';
359 Canvas_App_Sync_Dictation_Entity();
360 snprintf(
361 g_app->dictation_status,
362 sizeof(g_app->dictation_status),
363 "Listening");
364 return;
365 }
366 if (IsKeyPressed(KEY_ESCAPE)) {
367 if (g_app->dictation_listening) {
368 Canvas_Web_Surface_Set_Dictation_Active(
369 &g_app->web_surface,
370 FALSE);
371 g_app->dictation_requested_active = FALSE;
372 g_app->dictation_listening = FALSE;
373 }
374 g_app->dictation_send_pending = FALSE;
375 g_app->dictation_partial[0] = '\0';
376 g_app->dictation_transcript[0] = '\0';
377 Canvas_App_Sync_Dictation_Entity();
378 g_app->dictation_overlay_visible = FALSE;
379 return;
380 }
381 if (!IsKeyPressed(KEY_ENTER)) return;
382
383 if (g_app->dictation_partial[0]) {
384 g_app->dictation_send_pending = TRUE;
385 snprintf(
386 g_app->dictation_status,
387 sizeof(g_app->dictation_status),
388 "Finishing dictation...");
389 Canvas_Web_Surface_Commit_Dictation(
390 &g_app->web_surface);
391 } else if (g_app->dictation_transcript[0]) {
392 Canvas_App_Submit_Dictation();
393 }
394 }
395
396 static boolean Canvas_App_Dictation_Status_Is_Error(const char *p_status)
397 {
398 return strstr(p_status, "failed") != NULL ||
399 strstr(p_status, "Unable") != NULL ||
400 strstr(p_status, "closed") != NULL ||
401 strstr(p_status, "invalid") != NULL ||
402 strstr(p_status, "error") != NULL;
403 }
404
405 static void Canvas_App_Draw_Listening_Indicator(void)
406 {
407 if (!g_app->dictation_listening) return;
408
409 float pulse = 0.72f + 0.28f * sinf((float)GetTime() * 5.0f);
410 Vector2 dot = {28.0f, (float)GetScreenHeight() - 28.0f};
411 DrawCircleV(
412 dot,
413 5.0f + pulse * 2.0f,
414 Fade(g_app->theme.danger, 0.82f));
415 DrawTextEx(
416 g_app->font,
417 "Listening",
418 (Vector2){44.0f, dot.y - 8.0f},
419 14.0f,
420 0.0f,
421 g_app->theme.text_muted);
422 }
32 423
33 static void Canvas_App_Draw_Frame(void) 424 static void Canvas_App_Draw_Frame(void)
34 { 425 {
35 int32 width = GetScreenWidth(); 426 int32 width = GetScreenWidth();
36 int32 height = GetScreenHeight(); 427 int32 height = GetScreenHeight();
41 block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui); 432 block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui);
42 #endif 433 #endif
43 Canvas_Web_Surface_Set_Dark_Mode( 434 Canvas_Web_Surface_Set_Dark_Mode(
44 &g_app->web_surface, 435 &g_app->web_surface,
45 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE); 436 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE);
437 boolean dictation_hotkey =
438 !Canvas_Scene_Is_Text_Editing(&g_app->scene) &&
439 !Canvas_Web_Chrome_Is_Editing(&g_app->scene) &&
440 IsKeyPressed(KEY_M);
441 if (dictation_hotkey) {
442 Canvas_App_Set_Dictation_Active(
443 g_app->dictation_requested_active ? FALSE : TRUE);
444 }
445 Canvas_App_Update_Dictation_Overlay_Input();
46 Canvas_Web_Chrome_Action chrome_action = CANVAS_WEB_CHROME_NONE; 446 Canvas_Web_Chrome_Action chrome_action = CANVAS_WEB_CHROME_NONE;
47 if (!block_pointer_input) { 447 if (!block_pointer_input) {
48 const char *p_current_url = NULL; 448 const char *p_current_url = NULL;
49 if (g_app->scene.selected_index >= 0 && 449 if (g_app->scene.selected_index >= 0 &&
50 g_app->scene.selected_index < 450 g_app->scene.selected_index <
74 &g_app->web_surface, 474 &g_app->web_surface,
75 p_selected->id); 475 p_selected->id);
76 } 476 }
77 } 477 }
78 boolean web_input = FALSE; 478 boolean web_input = FALSE;
79 if (!block_pointer_input && chrome_action == CANVAS_WEB_CHROME_NONE) { 479 if (!block_pointer_input &&
480 !dictation_hotkey &&
481 chrome_action == CANVAS_WEB_CHROME_NONE) {
80 web_input = Canvas_Web_Surface_Update_Input( 482 web_input = Canvas_Web_Surface_Update_Input(
81 &g_app->web_surface, 483 &g_app->web_surface,
82 &g_app->camera, 484 &g_app->camera,
83 &g_app->scene); 485 &g_app->scene);
84 } 486 }
97 Canvas_Web_Surface_Is_Focused(&g_app->web_surface); 499 Canvas_Web_Surface_Is_Focused(&g_app->web_surface);
98 if (!keyboard_focus && IsKeyPressed(KEY_P)) { 500 if (!keyboard_focus && IsKeyPressed(KEY_P)) {
99 Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera); 501 Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera);
100 } 502 }
101 if (!keyboard_focus && 503 if (!keyboard_focus &&
504 !g_app->dictation_listening &&
102 (IsKeyPressed(KEY_DELETE) || IsKeyPressed(KEY_BACKSPACE))) { 505 (IsKeyPressed(KEY_DELETE) || IsKeyPressed(KEY_BACKSPACE))) {
103 Canvas_Scene_Fade_Out_Selected(&g_app->scene); 506 Canvas_Scene_Fade_Out_Selected(&g_app->scene);
104 } 507 }
105 Canvas_Update_Camera_Input( 508 Canvas_Update_Camera_Input(
106 &g_app->camera, 509 &g_app->camera,
107 block_pointer_input || entity_input, 510 block_pointer_input || entity_input,
108 keyboard_focus); 511 keyboard_focus);
109 Canvas_Scene_Sync_Pinned(&g_app->scene, &g_app->camera); 512 Canvas_Scene_Sync_Pinned(&g_app->scene, &g_app->camera);
110 Canvas_Scene_Update_Notification_Stack(&g_app->scene, &g_app->camera); 513 Canvas_Scene_Update_Notification_Stack(&g_app->scene, &g_app->camera);
514
515 char dictation_text[CANVAS_ENTITY_TEXT_CAPACITY];
516 Canvas_Dictation_Event dictation_event =
517 Canvas_Web_Surface_Consume_Dictation(
518 &g_app->web_surface,
519 dictation_text,
520 sizeof(dictation_text));
521 if (dictation_event == CANVAS_DICTATION_EVENT_STATUS) {
522 snprintf(
523 g_app->dictation_status,
524 sizeof(g_app->dictation_status),
525 "%.127s",
526 dictation_text);
527 if (strcmp(dictation_text, "Idle") == 0) {
528 g_app->dictation_listening = FALSE;
529 if (g_app->dictation_send_pending) {
530 if (g_app->dictation_partial[0]) {
531 Canvas_App_Append_Dictation(
532 g_app->dictation_partial);
533 g_app->dictation_partial[0] = '\0';
534 }
535 Canvas_App_Submit_Dictation();
536 } else if (!g_app->dictation_transcript[0] &&
537 !g_app->dictation_partial[0]) {
538 g_app->dictation_overlay_visible = FALSE;
539 }
540 } else if (strcmp(dictation_text, "Listening") == 0 ||
541 strcmp(dictation_text, "Speech detected") == 0) {
542 g_app->dictation_listening =
543 g_app->dictation_requested_active;
544 if (strcmp(dictation_text, "Listening") == 0 &&
545 g_app->dictation_send_pending) {
546 if (g_app->dictation_partial[0]) {
547 Canvas_App_Append_Dictation(
548 g_app->dictation_partial);
549 g_app->dictation_partial[0] = '\0';
550 }
551 Canvas_App_Submit_Dictation();
552 }
553 } else if (Canvas_App_Dictation_Status_Is_Error(dictation_text)) {
554 g_app->dictation_listening = FALSE;
555 g_app->dictation_overlay_visible = TRUE;
556 }
557 } else if (dictation_event == CANVAS_DICTATION_EVENT_PARTIAL) {
558 g_app->dictation_overlay_visible = TRUE;
559 snprintf(
560 g_app->dictation_partial,
561 sizeof(g_app->dictation_partial),
562 "%s",
563 dictation_text);
564 Canvas_App_Sync_Dictation_Entity();
565 } else if (dictation_event == CANVAS_DICTATION_EVENT_FINAL) {
566 g_app->dictation_overlay_visible = TRUE;
567 Canvas_App_Append_Dictation(dictation_text);
568 g_app->dictation_partial[0] = '\0';
569 Canvas_App_Sync_Dictation_Entity();
570 if (g_app->dictation_send_pending) {
571 Canvas_App_Submit_Dictation();
572 } else {
573 snprintf(
574 g_app->dictation_status,
575 sizeof(g_app->dictation_status),
576 "Ready to send");
577 }
578 }
579 if (!g_app->dictation_overlay_visible &&
580 g_app->scene.selected_index >= 0 &&
581 g_app->scene.selected_index <
582 (int32)Dowa_Array_Length(g_app->scene.p_entities) &&
583 (IsKeyDown(KEY_LEFT_CONTROL) ||
584 IsKeyDown(KEY_RIGHT_CONTROL) ||
585 IsKeyDown(KEY_LEFT_SUPER) ||
586 IsKeyDown(KEY_RIGHT_SUPER)) &&
587 IsKeyPressed(KEY_ENTER)) {
588 Canvas_Entity *p_selected =
589 &g_app->scene.p_entities[g_app->scene.selected_index];
590 if (p_selected->type == CANVAS_ENTITY_TEXT_AREA &&
591 p_selected->text[0]) {
592 size_t length = strlen(p_selected->text);
593 if (length > 0 && p_selected->text[length - 1] == '\n') {
594 p_selected->text[length - 1] = '\0';
595 }
596 if (Canvas_App_Submit_Prompt(p_selected->text)) {
597 p_selected->text[0] = '\0';
598 p_selected->text_cursor = 0;
599 p_selected->text_selection_anchor = 0;
600 }
601 }
602 }
603
604 char agent_response[CANVAS_AGENT_RESPONSE_CAPACITY];
605 char agent_error[512];
606 Canvas_Agent_Status agent_status = g_app->agent_initialized ?
607 Canvas_Agent_Service_Poll(
608 &g_app->agent_service,
609 agent_response,
610 sizeof(agent_response),
611 agent_error,
612 sizeof(agent_error)) :
613 CANVAS_AGENT_IDLE;
614 if (agent_status == CANVAS_AGENT_READY) {
615 Canvas_App_Apply_Agent_Response(agent_response);
616 } else if (agent_status == CANVAS_AGENT_ERROR) {
617 Canvas_Entity *p_pending =
618 Canvas_App_Find_Entity(g_app->pending_conversation_id);
619 if (p_pending) {
620 snprintf(p_pending->label, sizeof(p_pending->label), "Copilot unavailable");
621 Canvas_App_Set_Conversation_Text(
622 p_pending,
623 g_app->pending_prompt,
624 "System",
625 agent_error);
626 p_pending->agent_working = FALSE;
627 } else if (g_app->pending_prompt[0]) {
628 Vector2 position = Canvas_Camera_Screen_To_World(
629 &g_app->camera,
630 (Vector2){
631 (float)g_app->camera.viewport_width * 0.5f - 210.0f,
632 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
633 });
634 Canvas_Scene_Add_Conversation(
635 &g_app->scene,
636 "Copilot unavailable",
637 g_app->pending_prompt,
638 agent_error,
639 position);
640 }
641 g_app->pending_conversation_id = 0;
642 g_app->pending_prompt[0] = '\0';
643 }
111 644
112 Canvas_Web_Surface_Sync(&g_app->web_surface, &g_app->camera, &g_app->scene); 645 Canvas_Web_Surface_Sync(&g_app->web_surface, &g_app->camera, &g_app->scene);
113 646
114 BeginDrawing(); 647 BeginDrawing();
115 ClearBackground(g_app->theme.background); 648 ClearBackground(g_app->theme.background);
160 &g_app->camera, 693 &g_app->camera,
161 &g_app->scene, 694 &g_app->scene,
162 g_app->font, 695 g_app->font,
163 &g_app->theme); 696 &g_app->theme);
164 #endif 697 #endif
698 Canvas_App_Draw_Listening_Indicator();
165 EndDrawing(); 699 EndDrawing();
166 g_app->frame_count++; 700 g_app->frame_count++;
167 #if !defined(PLATFORM_WEB) 701 #if !defined(PLATFORM_WEB)
168 if (g_app->frame_count == 30) { 702 if (g_app->frame_count == 30) {
169 RestoreWindow(); 703 RestoreWindow();
170 SetWindowFocused(); 704 SetWindowFocused();
171 } 705 }
172 #endif 706 #endif
173 if (g_app->p_screenshot_path && 707 if (g_app->p_screenshot_path &&
174 g_app->frame_count == 180) { 708 g_app->frame_count == g_app->screenshot_frame) {
175 TakeScreenshot(g_app->p_screenshot_path); 709 TakeScreenshot(g_app->p_screenshot_path);
176 } 710 }
177 } 711 }
178 712
179 int main(int argc, char **p_argv) 713 int main(int argc, char **p_argv)
194 Dowa_Arena_Free(p_arena); 728 Dowa_Arena_Free(p_arena);
195 return 1; 729 return 1;
196 } 730 }
197 memset(g_app, 0, sizeof(*g_app)); 731 memset(g_app, 0, sizeof(*g_app));
198 g_app->p_arena = p_arena; 732 g_app->p_arena = p_arena;
733 snprintf(
734 g_app->dictation_status,
735 sizeof(g_app->dictation_status),
736 "Idle");
199 g_app->p_screenshot_path = getenv("INFINITE_CANVAS_SCREENSHOT"); 737 g_app->p_screenshot_path = getenv("INFINITE_CANVAS_SCREENSHOT");
738 g_app->screenshot_frame = 180;
739 const char *p_screenshot_frame =
740 getenv("INFINITE_CANVAS_SCREENSHOT_FRAME");
741 if (p_screenshot_frame) {
742 uint32 screenshot_frame =
743 (uint32)strtoul(p_screenshot_frame, NULL, 10);
744 if (screenshot_frame > 0) {
745 g_app->screenshot_frame = screenshot_frame;
746 }
747 }
200 Canvas_Theme_Resolve( 748 Canvas_Theme_Resolve(
201 &g_app->theme, 749 &g_app->theme,
202 Canvas_Theme_Mode_From_String(getenv("INFINITE_CANVAS_THEME"))); 750 Canvas_Theme_Mode_From_String(getenv("INFINITE_CANVAS_THEME")));
203 751
204 SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI | FLAG_MSAA_4X_HINT); 752 SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI | FLAG_MSAA_4X_HINT);
216 if (!Canvas_Web_Surface_Init( 764 if (!Canvas_Web_Surface_Init(
217 &g_app->web_surface, 765 &g_app->web_surface,
218 p_arena, 766 p_arena,
219 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE)) { 767 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE)) {
220 TraceLog(LOG_WARNING, "Web surface initialization failed"); 768 TraceLog(LOG_WARNING, "Web surface initialization failed");
769 }
770 g_app->agent_initialized =
771 Canvas_Agent_Service_Init(&g_app->agent_service, p_arena);
772 if (!g_app->agent_initialized) {
773 TraceLog(LOG_WARNING, "Agent service initialization failed");
221 } 774 }
222 775
223 g_app->font = LoadFontEx("infinite_canvas/assets/Inter-Variable.ttf", 48, NULL, 0); 776 g_app->font = LoadFontEx("infinite_canvas/assets/Inter-Variable.ttf", 48, NULL, 0);
224 g_app->owns_font = g_app->font.texture.id != 0; 777 g_app->owns_font = g_app->font.texture.id != 0;
225 if (!g_app->owns_font) g_app->font = GetFontDefault(); 778 if (!g_app->owns_font) g_app->font = GetFontDefault();
229 if (getenv("INFINITE_CANVAS_BROWSER_STRESS")) { 782 if (getenv("INFINITE_CANVAS_BROWSER_STRESS")) {
230 Canvas_Scene_Add_Browser_Grid(&g_app->scene); 783 Canvas_Scene_Add_Browser_Grid(&g_app->scene);
231 } else { 784 } else {
232 Canvas_Scene_Add_Demo(&g_app->scene); 785 Canvas_Scene_Add_Demo(&g_app->scene);
233 } 786 }
787 if (getenv("INFINITE_CANVAS_DICTATION_AUTOSTART")) {
788 Canvas_App_Set_Dictation_Active(TRUE);
789 }
790 const char *p_initial_prompt = getenv("INFINITE_CANVAS_AGENT_PROMPT");
791 if (p_initial_prompt && p_initial_prompt[0]) {
792 Canvas_App_Submit_Prompt(p_initial_prompt);
793 }
234 const char *p_image_source = getenv("INFINITE_CANVAS_IMAGE_SOURCE"); 794 const char *p_image_source = getenv("INFINITE_CANVAS_IMAGE_SOURCE");
235 if (p_image_source) { 795 if (p_image_source) {
236 for (size_t index = 0; index < Dowa_Array_Length(g_app->scene.p_entities); index++) { 796 for (size_t index = 0; index < Dowa_Array_Length(g_app->scene.p_entities); index++) {
237 Canvas_Entity *p_entity = &g_app->scene.p_entities[index]; 797 Canvas_Entity *p_entity = &g_app->scene.p_entities[index];
238 if (p_entity->type == CANVAS_ENTITY_IMAGE) { 798 if (p_entity->type == CANVAS_ENTITY_IMAGE) {
256 816
257 #if defined(PLATFORM_WEB) 817 #if defined(PLATFORM_WEB)
258 emscripten_set_main_loop(Canvas_App_Draw_Frame, 0, 1); 818 emscripten_set_main_loop(Canvas_App_Draw_Frame, 0, 1);
259 #else 819 #else
260 while (!WindowShouldClose()) Canvas_App_Draw_Frame(); 820 while (!WindowShouldClose()) Canvas_App_Draw_Frame();
821 if (g_app->agent_initialized) {
822 Canvas_Agent_Service_Shutdown(&g_app->agent_service);
823 }
261 Canvas_Web_Surface_Shutdown(&g_app->web_surface); 824 Canvas_Web_Surface_Shutdown(&g_app->web_surface);
262 if (g_app->owns_font) UnloadFont(g_app->font); 825 if (g_app->owns_font) UnloadFont(g_app->font);
263 Dowa_Arena_Free(p_arena); 826 Dowa_Arena_Free(p_arena);
264 g_app = NULL; 827 g_app = NULL;
265 CloseWindow(); 828 CloseWindow();