Mercurial
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(); |