synthos 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell.v3.js — Parent-frame chrome logic for the SynthOS iframe architecture.
|
|
3
|
+
*
|
|
4
|
+
* Runs in the parent frame (shell document). Manages:
|
|
5
|
+
* - Chat form submission → POST to server → send result to iframe via postMessage
|
|
6
|
+
* - Toolbar button wiring (builder toggle, pages, save, settings)
|
|
7
|
+
* - Save modal
|
|
8
|
+
* - Brainstorm modal
|
|
9
|
+
* - Attachment handling (file picker, paste, drag-drop, screenshot)
|
|
10
|
+
* - Undo / try-again links
|
|
11
|
+
* - Unsaved-changes guard
|
|
12
|
+
* - Focus management between chat and iframe
|
|
13
|
+
* - postMessage bridge to iframe (shell:* ← page, page:* → page)
|
|
14
|
+
*/
|
|
15
|
+
(function() {
|
|
16
|
+
var ORIGIN = window.location.origin;
|
|
17
|
+
var frame = document.getElementById('viewerFrame');
|
|
18
|
+
|
|
19
|
+
// --- Shell Init Data (injected by server as JSON) ------------------------
|
|
20
|
+
var shellInit = window.__shellInit || {};
|
|
21
|
+
var pageName = shellInit.page || '';
|
|
22
|
+
var isRequestActive = false;
|
|
23
|
+
var capturedErrors = [];
|
|
24
|
+
var pageVersion = shellInit.version || 0;
|
|
25
|
+
// A page with `page.c*.html` on disk has pending unsaved changes from a
|
|
26
|
+
// prior session — Save should save, not fall through to Copy.
|
|
27
|
+
var pageDirty = pageVersion > 0;
|
|
28
|
+
|
|
29
|
+
function setPageDirty(v) {
|
|
30
|
+
pageDirty = !!v;
|
|
31
|
+
if (typeof updateSaveBtnState === 'function') updateSaveBtnState();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Gate all inline chat affordances (suggestion chips, send: links,
|
|
35
|
+
// undo/try-again/reset links) while a server request is in flight.
|
|
36
|
+
// The CSS class does the visual + pointer-events block; the boolean
|
|
37
|
+
// is checked at the top of each action handler as defense-in-depth.
|
|
38
|
+
function setChatBusy(busy) {
|
|
39
|
+
isRequestActive = !!busy;
|
|
40
|
+
var cm = document.getElementById('chatMessages');
|
|
41
|
+
if (cm) cm.classList.toggle('chat-busy', !!busy);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Product name for branding
|
|
45
|
+
var pn = shellInit.productName || 'SynthOS';
|
|
46
|
+
|
|
47
|
+
// --- Loading Overlay ----------------------------------------------------
|
|
48
|
+
// Corner-peek radial fade. Tune these to change how much of the page shows
|
|
49
|
+
// at the edges of the overlay.
|
|
50
|
+
var LOADING_FADE_SIZE = 95; // 40..95 — diameter of the opaque centre (% of viewport)
|
|
51
|
+
var LOADING_FADE_SOFTNESS = 47; // 20..80 — width of the fade edge
|
|
52
|
+
|
|
53
|
+
var LOADING_SAYINGS = [
|
|
54
|
+
'Waving my AI wand...',
|
|
55
|
+
'Brewing up something magical...',
|
|
56
|
+
'Consulting the digital oracle...',
|
|
57
|
+
'Sprinkling some pixel dust...',
|
|
58
|
+
'Teaching electrons to dance...',
|
|
59
|
+
'Summoning the code spirits...',
|
|
60
|
+
'Painting with algorithms...',
|
|
61
|
+
'Weaving digital tapestry...',
|
|
62
|
+
'Charging the flux capacitor...',
|
|
63
|
+
'Aligning the digital stars...',
|
|
64
|
+
'Mixing the perfect pixel potion...',
|
|
65
|
+
'Whispering to the servers...',
|
|
66
|
+
'Folding space-time (just a little)...',
|
|
67
|
+
'Assembling tiny digital elves...',
|
|
68
|
+
'Tuning the neural frequencies...',
|
|
69
|
+
'Polishing every last pixel...',
|
|
70
|
+
'Asking the cloud for a favor...',
|
|
71
|
+
'Spinning up the imagination engine...',
|
|
72
|
+
'Translating thoughts into code...',
|
|
73
|
+
'Doing some AI magic...'
|
|
74
|
+
];
|
|
75
|
+
var LOADING_TIPS = [
|
|
76
|
+
'You can use markdown in any text field for rich formatting.',
|
|
77
|
+
'Always remember to save your work \u2014 save intermediate versions to separate pages when exploring an idea.',
|
|
78
|
+
'Try asking for specific color schemes to customize your pages theme.',
|
|
79
|
+
'Use the brainstorm icon in the chat panel to have AI help you flush out an idea.',
|
|
80
|
+
'You can reference data from other pages using shared tables.',
|
|
81
|
+
'Pin your most-used pages to favorites in the pages gallery for quick access.',
|
|
82
|
+
'Use keyboard shortcuts to speed up your workflow.',
|
|
83
|
+
'You can upload files and images for reference directly to the page builder.',
|
|
84
|
+
'Ask the AI to add animations or interactive elements to your pages.',
|
|
85
|
+
'Connect external APIs through the Connectors settings for live data.',
|
|
86
|
+
'Ask the page builder to add an AI chat feature so users can interact with your page conversationally.',
|
|
87
|
+
'Brainstorm with the AI first to flesh out your idea before building \u2014 a clear spec makes for a better page.',
|
|
88
|
+
'Tell the AI to use shared storage for data you want reused across pages, like a list of employees or products.',
|
|
89
|
+
'Describe who will use the page and what they need to accomplish \u2014 context helps the AI build something useful.',
|
|
90
|
+
'Try asking for a specific layout like a dashboard, kanban board, or wizard to steer the structure.',
|
|
91
|
+
'Paste a screenshot of a design you like and ask the AI to build something inspired by it.',
|
|
92
|
+
'When a change misses the mark, use the undo link in the chat to roll back to the previous version.',
|
|
93
|
+
'Break complex builds into stages \u2014 start with the layout, then add data, then wire up interactions.',
|
|
94
|
+
'Ask the AI to include form validation or confirmation dialogs for a more polished feel.',
|
|
95
|
+
'Give the AI specific feedback when something is off \u2014 "make the header smaller" works better than "fix the header".'
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
var loadingSubstatusInterval = null;
|
|
99
|
+
var loadingTipInterval = null;
|
|
100
|
+
var lastSayingIdx = -1;
|
|
101
|
+
var lastTipIdx = -1;
|
|
102
|
+
var loadingMaskApplied = false;
|
|
103
|
+
|
|
104
|
+
function pickRandom(arr, lastIdxRef) {
|
|
105
|
+
var idx;
|
|
106
|
+
do {
|
|
107
|
+
idx = Math.floor(Math.random() * arr.length);
|
|
108
|
+
} while (idx === lastIdxRef.value && arr.length > 1);
|
|
109
|
+
lastIdxRef.value = idx;
|
|
110
|
+
return arr[idx];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyLoadingMask(el) {
|
|
114
|
+
if (!el || loadingMaskApplied) return;
|
|
115
|
+
var size = LOADING_FADE_SIZE;
|
|
116
|
+
var soft = LOADING_FADE_SOFTNESS;
|
|
117
|
+
var innerStop = Math.max(0, size - soft);
|
|
118
|
+
var main = 'radial-gradient(ellipse ' + size + '% ' + (size - 5) + '% at 50% 48%, black ' + innerStop + '%, transparent ' + size + '%)';
|
|
119
|
+
var blobSize = Math.round(size * 0.45);
|
|
120
|
+
var blobSoft = Math.round(soft * 0.7);
|
|
121
|
+
var blobInner = Math.max(0, blobSize - blobSoft);
|
|
122
|
+
var tl = 'radial-gradient(ellipse ' + (blobSize + 8) + '% ' + (blobSize - 3) + '% at 12% 15%, rgba(0,0,0,0.55) ' + blobInner + '%, transparent ' + blobSize + '%)';
|
|
123
|
+
var tr = 'radial-gradient(ellipse ' + (blobSize - 2) + '% ' + (blobSize + 5) + '% at 88% 18%, rgba(0,0,0,0.45) ' + blobInner + '%, transparent ' + blobSize + '%)';
|
|
124
|
+
var bl = 'radial-gradient(ellipse ' + (blobSize + 3) + '% ' + (blobSize - 5) + '% at 18% 82%, rgba(0,0,0,0.4) ' + blobInner + '%, transparent ' + blobSize + '%)';
|
|
125
|
+
var br = 'radial-gradient(ellipse ' + (blobSize + 10) + '% ' + blobSize + '% at 82% 80%, rgba(0,0,0,0.5) ' + blobInner + '%, transparent ' + blobSize + '%)';
|
|
126
|
+
var mask = [main, tl, tr, bl, br].join(', ');
|
|
127
|
+
el.style.webkitMaskImage = mask;
|
|
128
|
+
el.style.maskImage = mask;
|
|
129
|
+
el.style.webkitMaskComposite = 'source-over';
|
|
130
|
+
el.style.maskComposite = 'add';
|
|
131
|
+
loadingMaskApplied = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fadeSubstatusTo(text) {
|
|
135
|
+
var sub = document.getElementById('loadingSubstatus');
|
|
136
|
+
if (!sub) return;
|
|
137
|
+
sub.classList.add('fade-out');
|
|
138
|
+
setTimeout(function() {
|
|
139
|
+
sub.textContent = text;
|
|
140
|
+
sub.classList.remove('fade-out');
|
|
141
|
+
}, 300);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setLoadingStatus(text) {
|
|
145
|
+
var st = document.getElementById('loadingStatus');
|
|
146
|
+
if (st && text) st.textContent = text;
|
|
147
|
+
// Whenever the status changes, rotate the substatus saying too.
|
|
148
|
+
fadeSubstatusTo(pickRandom(LOADING_SAYINGS, lastSayingRef));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Shared refs so pickRandom sees the same "last index" state across calls.
|
|
152
|
+
var lastSayingRef = { value: -1 };
|
|
153
|
+
var lastTipRef = { value: -1 };
|
|
154
|
+
|
|
155
|
+
// "Building Page" only fits the first change on a starter or the builder page.
|
|
156
|
+
// Every subsequent change — and every change on a regular page — is an update.
|
|
157
|
+
function getTransformStatusLabel() {
|
|
158
|
+
var isStarterOrBuilder = shellInit.isStarter || pageName === 'builder';
|
|
159
|
+
return (pageVersion === 0 && isStarterOrBuilder) ? 'Building Page' : 'Updating Page';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function showLoadingOverlay(initialStatus) {
|
|
163
|
+
var overlay = document.getElementById('loadingOverlay');
|
|
164
|
+
if (!overlay) return;
|
|
165
|
+
var masked = overlay.querySelector('.loading-masked');
|
|
166
|
+
applyLoadingMask(masked);
|
|
167
|
+
|
|
168
|
+
var statusText = initialStatus || getTransformStatusLabel();
|
|
169
|
+
var statusEl = document.getElementById('loadingStatus');
|
|
170
|
+
if (statusEl) statusEl.textContent = statusText;
|
|
171
|
+
|
|
172
|
+
var sub = document.getElementById('loadingSubstatus');
|
|
173
|
+
if (sub) {
|
|
174
|
+
sub.classList.remove('fade-out');
|
|
175
|
+
sub.textContent = pickRandom(LOADING_SAYINGS, lastSayingRef);
|
|
176
|
+
}
|
|
177
|
+
var tipText = overlay.querySelector('#loadingTip .loading-tip-text');
|
|
178
|
+
if (tipText) tipText.textContent = pickRandom(LOADING_TIPS, lastTipRef);
|
|
179
|
+
|
|
180
|
+
overlay.style.display = 'flex';
|
|
181
|
+
|
|
182
|
+
if (!loadingSubstatusInterval) {
|
|
183
|
+
loadingSubstatusInterval = setInterval(function() {
|
|
184
|
+
fadeSubstatusTo(pickRandom(LOADING_SAYINGS, lastSayingRef));
|
|
185
|
+
}, 3500);
|
|
186
|
+
}
|
|
187
|
+
if (!loadingTipInterval) {
|
|
188
|
+
loadingTipInterval = setInterval(function() {
|
|
189
|
+
var t = document.querySelector('#loadingTip .loading-tip-text');
|
|
190
|
+
if (t) t.textContent = pickRandom(LOADING_TIPS, lastTipRef);
|
|
191
|
+
}, 15000);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function hideLoadingOverlay() {
|
|
196
|
+
var overlay = document.getElementById('loadingOverlay');
|
|
197
|
+
if (overlay) overlay.style.display = 'none';
|
|
198
|
+
if (loadingSubstatusInterval) { clearInterval(loadingSubstatusInterval); loadingSubstatusInterval = null; }
|
|
199
|
+
if (loadingTipInterval) { clearInterval(loadingTipInterval); loadingTipInterval = null; }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- postMessage Bridge --------------------------------------------------
|
|
203
|
+
|
|
204
|
+
function sendToPage(type, payload) {
|
|
205
|
+
if (frame && frame.contentWindow) {
|
|
206
|
+
frame.contentWindow.postMessage(
|
|
207
|
+
{ source: 'synthos-shell', type: type, payload: payload || {} },
|
|
208
|
+
ORIGIN
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
window.addEventListener('message', function(event) {
|
|
214
|
+
if (event.origin !== ORIGIN) return;
|
|
215
|
+
var msg = event.data;
|
|
216
|
+
if (!msg || msg.source !== 'synthos-page') return;
|
|
217
|
+
|
|
218
|
+
switch (msg.type) {
|
|
219
|
+
case 'shell:ready':
|
|
220
|
+
// iframe bridge initialized
|
|
221
|
+
break;
|
|
222
|
+
case 'shell:error':
|
|
223
|
+
if (Array.isArray(msg.payload.errors)) {
|
|
224
|
+
capturedErrors = capturedErrors.concat(msg.payload.errors);
|
|
225
|
+
showErrorInChat();
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case 'shell:navigate':
|
|
229
|
+
navigateTo(msg.payload.url);
|
|
230
|
+
break;
|
|
231
|
+
case 'shell:dirty':
|
|
232
|
+
// Only honor dirty=true from the iframe. Clean transitions
|
|
233
|
+
// (after save / reset / undo) are shell-initiated and bypass
|
|
234
|
+
// this channel — an incoming dirty=false is either spurious
|
|
235
|
+
// (leftover setter firing on iframe reload) or redundant.
|
|
236
|
+
if (msg.payload.dirty) setPageDirty(true);
|
|
237
|
+
break;
|
|
238
|
+
case 'shell:modal':
|
|
239
|
+
if (msg.payload.modal === 'save') {
|
|
240
|
+
if (window.__synthOSOpenSaveModal) window.__synthOSOpenSaveModal();
|
|
241
|
+
} else if (msg.payload.modal === 'copy' && msg.payload.page) {
|
|
242
|
+
if (window.__synthOSOpenCopyModal) window.__synthOSOpenCopyModal(msg.payload.page);
|
|
243
|
+
} else if (msg.payload.modal === 'edit' && msg.payload.page) {
|
|
244
|
+
if (window.__synthOSOpenEditModal) window.__synthOSOpenEditModal(msg.payload.page, function() {
|
|
245
|
+
sendToPage('page:pages-changed', {});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case 'shell:builder':
|
|
250
|
+
if (msg.payload.action === 'toggle') toggleBuilder();
|
|
251
|
+
else if (msg.payload.action === 'open') setCollapsed(false);
|
|
252
|
+
else if (msg.payload.action === 'close') setCollapsed(true);
|
|
253
|
+
break;
|
|
254
|
+
case 'shell:submit-chat':
|
|
255
|
+
if (msg.payload.message) submitChatMessage(msg.payload.message);
|
|
256
|
+
break;
|
|
257
|
+
case 'shell:loading':
|
|
258
|
+
if (msg.payload.show) showLoadingOverlay(msg.payload.status);
|
|
259
|
+
else hideLoadingOverlay();
|
|
260
|
+
break;
|
|
261
|
+
case 'shell:focus-chat':
|
|
262
|
+
var ci = document.getElementById('chatInput');
|
|
263
|
+
if (ci) ci.focus();
|
|
264
|
+
break;
|
|
265
|
+
case 'shell:reload':
|
|
266
|
+
window.location.reload();
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// --- Error Display in Chat -----------------------------------------------
|
|
272
|
+
|
|
273
|
+
function showErrorInChat() {
|
|
274
|
+
var cm = document.getElementById('chatMessages');
|
|
275
|
+
if (!cm) return;
|
|
276
|
+
if (showErrorInChat._pending) return;
|
|
277
|
+
showErrorInChat._pending = true;
|
|
278
|
+
setTimeout(function() {
|
|
279
|
+
showErrorInChat._pending = false;
|
|
280
|
+
if (capturedErrors.length === 0) return;
|
|
281
|
+
var fixPrompt = 'Fix the following JavaScript errors on this page:\n\nCONSOLE_ERRORS:\n' + capturedErrors.join('\n---\n');
|
|
282
|
+
capturedErrors = [];
|
|
283
|
+
var md = 'I noticed a JavaScript error on this page. [Let me try to fix it](send:' + encodeURIComponent(fixPrompt) + ')';
|
|
284
|
+
appendChatMessage('assistant', md);
|
|
285
|
+
}, 500);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Surface silently-skipped change ops (stale node IDs, locked elements, etc.)
|
|
289
|
+
// so partial/broken builds don't pass unnoticed. The "try again" link from
|
|
290
|
+
// appendUndoLinks below gives the user a recovery path.
|
|
291
|
+
function showSkippedOpsWarning(result) {
|
|
292
|
+
if (!result || !result.skippedOps || result.skippedOps <= 0) return;
|
|
293
|
+
var reasons = Array.isArray(result.skipReasons) ? result.skipReasons : [];
|
|
294
|
+
var lines = reasons.map(function(r) { return '- ' + r; });
|
|
295
|
+
var md = '**Warning:** ' + result.skippedOps + ' change op(s) were skipped — the page may be incomplete.';
|
|
296
|
+
if (lines.length > 0) md += '\n' + lines.join('\n');
|
|
297
|
+
appendChatMessage('assistant', md);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- Render Chat Messages from Server Init Data --------------------------
|
|
301
|
+
|
|
302
|
+
function renderInitialMessages(messages) {
|
|
303
|
+
var cm = document.getElementById('chatMessages');
|
|
304
|
+
if (!cm || !messages || !messages.length) return;
|
|
305
|
+
cm.innerHTML = '';
|
|
306
|
+
for (var i = 0; i < messages.length; i++) {
|
|
307
|
+
var msg = messages[i];
|
|
308
|
+
if (msg.role && msg.content) {
|
|
309
|
+
// New format: { role: 'user'|'assistant', content: string }
|
|
310
|
+
var roleName = msg.role === 'user' ? 'User' : pn;
|
|
311
|
+
appendChatMessage(roleName, msg.content);
|
|
312
|
+
} else if (msg.html) {
|
|
313
|
+
// Legacy format: { id?, html } — raw HTML from extractChatMessages
|
|
314
|
+
var div = document.createElement('div');
|
|
315
|
+
div.className = 'chat-message';
|
|
316
|
+
if (msg.id) div.id = msg.id;
|
|
317
|
+
div.innerHTML = msg.html;
|
|
318
|
+
cm.appendChild(div);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
scrollChatToBottom();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Render greeting from metadata (authoritative source)
|
|
325
|
+
var greetingText = shellInit.greeting || '';
|
|
326
|
+
var isGreetingEditable = greetingText && !shellInit.isLocked && pageName !== 'builder';
|
|
327
|
+
|
|
328
|
+
// Always render the full conversation (greeting + every per-change turn).
|
|
329
|
+
// The server concatenates page.c<n>.json files in order so the shell only
|
|
330
|
+
// has to display what it's handed.
|
|
331
|
+
if (shellInit.messages && shellInit.messages.length > 0) {
|
|
332
|
+
renderInitialMessages(shellInit.messages);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Chat Scroll ---------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
function scrollChatToBottom() {
|
|
338
|
+
var cm = document.getElementById('chatMessages');
|
|
339
|
+
if (cm) {
|
|
340
|
+
cm.scrollTo({ top: cm.scrollHeight, behavior: 'smooth' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 0. First-run greeting — sourced from page.json firstRunGreeting field
|
|
345
|
+
(function() {
|
|
346
|
+
var params = new URLSearchParams(window.location.search);
|
|
347
|
+
if (!params.get('firstRun')) return;
|
|
348
|
+
var firstRunHtml = shellInit.firstRunGreeting || '';
|
|
349
|
+
if (!firstRunHtml) return;
|
|
350
|
+
var cm = document.getElementById('chatMessages');
|
|
351
|
+
if (!cm) return;
|
|
352
|
+
// Replace content with first-run greeting (HTML content from metadata)
|
|
353
|
+
cm.innerHTML = '';
|
|
354
|
+
var div = document.createElement('div');
|
|
355
|
+
div.className = 'chat-message';
|
|
356
|
+
div.id = 'defaultGreeting';
|
|
357
|
+
// Replace product name placeholder
|
|
358
|
+
div.innerHTML = firstRunHtml.replace(/SynthOS/g, pn);
|
|
359
|
+
cm.appendChild(div);
|
|
360
|
+
})();
|
|
361
|
+
|
|
362
|
+
// --- Chat Form Submit (§2) -----------------------------------------------
|
|
363
|
+
|
|
364
|
+
var chatInput = document.getElementById('chatInput');
|
|
365
|
+
var chatForm = document.getElementById('chatForm');
|
|
366
|
+
|
|
367
|
+
if (chatForm) {
|
|
368
|
+
chatForm.addEventListener('submit', function(e) {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
|
|
371
|
+
// Don't allow a second submit while one is in flight.
|
|
372
|
+
if (isRequestActive) return;
|
|
373
|
+
|
|
374
|
+
var ci = document.getElementById('chatInput');
|
|
375
|
+
var messageText = ci ? ci.value : '';
|
|
376
|
+
|
|
377
|
+
if (!messageText.trim()) return;
|
|
378
|
+
|
|
379
|
+
// Capture any pending JS errors as a separate field (not appended to user message)
|
|
380
|
+
var pendingErrors = null;
|
|
381
|
+
if (capturedErrors.length > 0) {
|
|
382
|
+
pendingErrors = capturedErrors.slice();
|
|
383
|
+
capturedErrors = [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Show overlay and disable inputs
|
|
387
|
+
showLoadingOverlay();
|
|
388
|
+
setTimeout(function() {
|
|
389
|
+
if (ci) ci.disabled = true;
|
|
390
|
+
var sb = document.querySelector('.chat-send-btn');
|
|
391
|
+
if (sb) sb.disabled = true;
|
|
392
|
+
}, 50);
|
|
393
|
+
|
|
394
|
+
// Append user message to chat (clean, without error noise)
|
|
395
|
+
appendChatMessage('User', messageText);
|
|
396
|
+
|
|
397
|
+
// Extract conversation history from chat DOM
|
|
398
|
+
var history = extractHistory();
|
|
399
|
+
|
|
400
|
+
// Build JSON body — errors sent as separate field so chat history stays clean
|
|
401
|
+
var body = { message: messageText, history: history };
|
|
402
|
+
if (pendingErrors) body.errors = pendingErrors;
|
|
403
|
+
if (shellInit.builderStarter) body.starter = shellInit.builderStarter;
|
|
404
|
+
var attachments = shellAttachments;
|
|
405
|
+
if (attachments && attachments.length > 0) {
|
|
406
|
+
body.attachments = attachments.map(function(a) {
|
|
407
|
+
return { mediaType: a.mediaType, data: a.data, name: a.name };
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// POST to server — streaming NDJSON response
|
|
412
|
+
setChatBusy(true);
|
|
413
|
+
var postUrl = '/' + pageName;
|
|
414
|
+
|
|
415
|
+
fetch(postUrl, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
418
|
+
body: JSON.stringify(body)
|
|
419
|
+
})
|
|
420
|
+
.then(function(res) {
|
|
421
|
+
// Stream NDJSON: read line by line, handle progress + result events
|
|
422
|
+
var reader = res.body.getReader();
|
|
423
|
+
var decoder = new TextDecoder();
|
|
424
|
+
var buffer = '';
|
|
425
|
+
var finalResult = null;
|
|
426
|
+
|
|
427
|
+
function processLines() {
|
|
428
|
+
var nlIdx;
|
|
429
|
+
while ((nlIdx = buffer.indexOf('\n')) !== -1) {
|
|
430
|
+
var line = buffer.slice(0, nlIdx).trim();
|
|
431
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
432
|
+
if (!line) continue;
|
|
433
|
+
try {
|
|
434
|
+
var evt = JSON.parse(line);
|
|
435
|
+
if (evt.event === 'progress') {
|
|
436
|
+
var statusText = evt.status === 'classifying' ? 'Analyzing Request'
|
|
437
|
+
: evt.status === 'transforming' ? getTransformStatusLabel()
|
|
438
|
+
: (typeof evt.status === 'string' && evt.status.indexOf('tool_call:') === 0) ? 'Gathering Context'
|
|
439
|
+
: null;
|
|
440
|
+
if (statusText) setLoadingStatus(statusText);
|
|
441
|
+
} else if (evt.event === 'result') {
|
|
442
|
+
finalResult = evt;
|
|
443
|
+
} else if (evt.event === 'error') {
|
|
444
|
+
finalResult = { errorText: evt.error };
|
|
445
|
+
}
|
|
446
|
+
} catch (e) { /* skip unparseable lines */ }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function readChunk() {
|
|
451
|
+
return reader.read().then(function(chunk) {
|
|
452
|
+
if (chunk.done) {
|
|
453
|
+
processLines(); // flush remaining buffer
|
|
454
|
+
return finalResult;
|
|
455
|
+
}
|
|
456
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
457
|
+
processLines();
|
|
458
|
+
return readChunk();
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return readChunk();
|
|
463
|
+
})
|
|
464
|
+
.then(function(result) {
|
|
465
|
+
setChatBusy(false);
|
|
466
|
+
if (!result) {
|
|
467
|
+
appendChatMessage(pn, 'No response received.');
|
|
468
|
+
hideLoadingOverlay();
|
|
469
|
+
if (ci) ci.disabled = false;
|
|
470
|
+
var sb = document.querySelector('.chat-send-btn');
|
|
471
|
+
if (sb) sb.disabled = false;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
setPageDirty(true);
|
|
475
|
+
if (typeof result.version === 'number') pageVersion = result.version;
|
|
476
|
+
else pageVersion++;
|
|
477
|
+
|
|
478
|
+
sendToPage('page:load', { html: result.html });
|
|
479
|
+
|
|
480
|
+
// Hide overlay and re-enable inputs
|
|
481
|
+
hideLoadingOverlay();
|
|
482
|
+
if (ci) { ci.disabled = false; ci.value = ''; ci.focus(); }
|
|
483
|
+
var sb = document.querySelector('.chat-send-btn');
|
|
484
|
+
if (sb) sb.disabled = false;
|
|
485
|
+
|
|
486
|
+
// Append assistant response message based on result type
|
|
487
|
+
if (result.errorText) {
|
|
488
|
+
var errMsg = String(result.errorText);
|
|
489
|
+
if (errMsg.length > 500) errMsg = errMsg.slice(0, 500) + '\u2026';
|
|
490
|
+
appendChatMessage(pn, 'Error: ' + errMsg);
|
|
491
|
+
} else if (result.replyText) {
|
|
492
|
+
appendChatMessage(pn, result.replyText);
|
|
493
|
+
} else {
|
|
494
|
+
appendChatMessage(pn, 'Page updated.');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Surface skipped ops (partial build — some ops silently dropped)
|
|
498
|
+
showSkippedOpsWarning(result);
|
|
499
|
+
|
|
500
|
+
// Add undo/try-again links and clear action
|
|
501
|
+
appendUndoLinks(messageText);
|
|
502
|
+
appendClearAction();
|
|
503
|
+
|
|
504
|
+
scrollChatToBottom();
|
|
505
|
+
})
|
|
506
|
+
.catch(function(err) {
|
|
507
|
+
setChatBusy(false);
|
|
508
|
+
console.error('Submit failed:', err);
|
|
509
|
+
hideLoadingOverlay();
|
|
510
|
+
if (ci) ci.disabled = false;
|
|
511
|
+
var sb = document.querySelector('.chat-send-btn');
|
|
512
|
+
if (sb) sb.disabled = false;
|
|
513
|
+
|
|
514
|
+
appendChatMessage(pn, 'Something went wrong: ' + err.message);
|
|
515
|
+
scrollChatToBottom();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Clear attachments
|
|
519
|
+
shellAttachments = [];
|
|
520
|
+
var pillsContainer = document.querySelector('.attachment-pills');
|
|
521
|
+
if (pillsContainer) pillsContainer.innerHTML = '';
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 2b. Enter submits, Shift+Enter adds newline
|
|
526
|
+
(function() {
|
|
527
|
+
var ci = document.getElementById('chatInput');
|
|
528
|
+
if (!ci) return;
|
|
529
|
+
ci.addEventListener('keydown', function(e) {
|
|
530
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
var form = document.getElementById('chatForm');
|
|
533
|
+
if (form) form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit'));
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
})();
|
|
537
|
+
|
|
538
|
+
// --- Chat Helpers --------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
function escapeHtml(str) {
|
|
541
|
+
var div = document.createElement('div');
|
|
542
|
+
div.appendChild(document.createTextNode(str));
|
|
543
|
+
return div.innerHTML;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function renderMarkdown(text) {
|
|
547
|
+
var result = { html: '', suggestions: [] };
|
|
548
|
+
if (typeof marked === 'undefined') { result.html = escapeHtml(text); return result; }
|
|
549
|
+
var html = marked.parse(text);
|
|
550
|
+
// Strip dangerous URI schemes from rendered links
|
|
551
|
+
var tmp = document.createElement('div');
|
|
552
|
+
tmp.innerHTML = html;
|
|
553
|
+
var anchors = tmp.querySelectorAll('a[href]');
|
|
554
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
555
|
+
var href = (anchors[i].getAttribute('href') || '').trim().toLowerCase();
|
|
556
|
+
if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0 || href.indexOf('vbscript:') === 0) {
|
|
557
|
+
anchors[i].removeAttribute('href');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Extract suggest: links and collect as chips
|
|
561
|
+
var suggestAnchors = tmp.querySelectorAll('a[href^="suggest:"]');
|
|
562
|
+
for (var i = 0; i < suggestAnchors.length; i++) {
|
|
563
|
+
var a = suggestAnchors[i];
|
|
564
|
+
var payload = decodeURIComponent(a.getAttribute('href').substring(8));
|
|
565
|
+
var label = a.textContent;
|
|
566
|
+
result.suggestions.push({ label: label, payload: payload });
|
|
567
|
+
// Remove the anchor (and its parent li/p if it's the only child)
|
|
568
|
+
var parent = a.parentNode;
|
|
569
|
+
if (parent && parent.tagName === 'LI' && parent.children.length === 1) {
|
|
570
|
+
var ul = parent.parentNode;
|
|
571
|
+
ul.removeChild(parent);
|
|
572
|
+
if (ul.children.length === 0) ul.parentNode.removeChild(ul);
|
|
573
|
+
} else if (parent && parent.tagName === 'P' && parent.children.length === 1 && parent.textContent.trim() === label) {
|
|
574
|
+
parent.parentNode.removeChild(parent);
|
|
575
|
+
} else {
|
|
576
|
+
a.parentNode.removeChild(a);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
result.html = tmp.innerHTML;
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildSuggestionChips(suggestions) {
|
|
584
|
+
if (!suggestions || suggestions.length === 0) return null;
|
|
585
|
+
var container = document.createElement('div');
|
|
586
|
+
container.className = 'suggestion-chips';
|
|
587
|
+
for (var i = 0; i < suggestions.length; i++) {
|
|
588
|
+
var btn = document.createElement('button');
|
|
589
|
+
btn.type = 'button';
|
|
590
|
+
btn.className = 'suggestion-chip';
|
|
591
|
+
btn.textContent = suggestions[i].label;
|
|
592
|
+
btn.setAttribute('data-payload', suggestions[i].payload);
|
|
593
|
+
container.appendChild(btn);
|
|
594
|
+
}
|
|
595
|
+
return container;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function appendChatMessage(role, text) {
|
|
599
|
+
var cm = document.getElementById('chatMessages');
|
|
600
|
+
if (!cm) return;
|
|
601
|
+
var div = document.createElement('div');
|
|
602
|
+
div.className = 'chat-message';
|
|
603
|
+
if (role === 'User') {
|
|
604
|
+
div.classList.add('chat-message--user');
|
|
605
|
+
var header = document.createElement('p');
|
|
606
|
+
header.innerHTML = '<strong>User:</strong>';
|
|
607
|
+
div.appendChild(header);
|
|
608
|
+
var body = document.createElement('div');
|
|
609
|
+
body.className = 'chat-user-body';
|
|
610
|
+
body.textContent = text;
|
|
611
|
+
div.appendChild(body);
|
|
612
|
+
applyUserMessageClamp(body, div);
|
|
613
|
+
} else {
|
|
614
|
+
// Any non-User role renders under the configured product name so
|
|
615
|
+
// legacy callers that pass 'assistant' don't leak the raw role
|
|
616
|
+
// string as a bubble label.
|
|
617
|
+
var rendered = renderMarkdown(text);
|
|
618
|
+
div.innerHTML = '<p><strong>' + escapeHtml(pn) + ':</strong></p>' + rendered.html;
|
|
619
|
+
var chips = buildSuggestionChips(rendered.suggestions);
|
|
620
|
+
if (chips) div.appendChild(chips);
|
|
621
|
+
}
|
|
622
|
+
cm.appendChild(div);
|
|
623
|
+
scrollChatToBottom();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Add a "More" toggle to a user message body when its content overflows
|
|
627
|
+
// the collapsed max-height. Uses the body's own scrollHeight vs clientHeight
|
|
628
|
+
// after layout, so short messages never get the toggle.
|
|
629
|
+
function applyUserMessageClamp(body, container) {
|
|
630
|
+
requestAnimationFrame(function() {
|
|
631
|
+
if (body.scrollHeight <= body.clientHeight + 1) return;
|
|
632
|
+
body.classList.add('chat-user-body--clamped');
|
|
633
|
+
var toggle = document.createElement('button');
|
|
634
|
+
toggle.type = 'button';
|
|
635
|
+
toggle.className = 'chat-more-toggle';
|
|
636
|
+
toggle.textContent = 'More';
|
|
637
|
+
toggle.addEventListener('click', function() {
|
|
638
|
+
var expanded = body.classList.toggle('chat-user-body--expanded');
|
|
639
|
+
toggle.textContent = expanded ? 'Less' : 'More';
|
|
640
|
+
});
|
|
641
|
+
container.appendChild(toggle);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function extractHistory() {
|
|
646
|
+
var messages = [];
|
|
647
|
+
var chatMsgs = document.querySelectorAll('#chatMessages .chat-message');
|
|
648
|
+
for (var i = 0; i < chatMsgs.length; i++) {
|
|
649
|
+
var el = chatMsgs[i];
|
|
650
|
+
var strong = el.querySelector('strong');
|
|
651
|
+
if (!strong) continue;
|
|
652
|
+
var roleName = strong.textContent.trim().replace(/:$/, '');
|
|
653
|
+
var role = (roleName === 'User') ? 'user' : 'assistant';
|
|
654
|
+
// Clone the entire message div, remove the role label, chips, and
|
|
655
|
+
// any UI-only affordances (More toggle), then extract text
|
|
656
|
+
var clone = el.cloneNode(true);
|
|
657
|
+
var s = clone.querySelector('strong');
|
|
658
|
+
if (s) s.remove();
|
|
659
|
+
var chipsEl = clone.querySelector('.suggestion-chips');
|
|
660
|
+
if (chipsEl) chipsEl.remove();
|
|
661
|
+
var moreBtn = clone.querySelector('.chat-more-toggle');
|
|
662
|
+
if (moreBtn) moreBtn.remove();
|
|
663
|
+
messages.push({ role: role, content: clone.textContent.trim() });
|
|
664
|
+
}
|
|
665
|
+
return messages;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// --- Submit Chat Message (programmatic) ----------------------------------
|
|
669
|
+
|
|
670
|
+
function submitChatMessage(text) {
|
|
671
|
+
if (isRequestActive) return;
|
|
672
|
+
var ci = document.getElementById('chatInput');
|
|
673
|
+
if (ci) ci.value = text;
|
|
674
|
+
var form = document.getElementById('chatForm');
|
|
675
|
+
if (form) form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit'));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// --- Chat Link Click Handler ---------------------------------------------
|
|
679
|
+
|
|
680
|
+
(function() {
|
|
681
|
+
var cm = document.getElementById('chatMessages');
|
|
682
|
+
if (!cm) return;
|
|
683
|
+
cm.addEventListener('click', function(e) {
|
|
684
|
+
var anchor = e.target.closest('a[href]');
|
|
685
|
+
if (!anchor) return;
|
|
686
|
+
var href = anchor.getAttribute('href');
|
|
687
|
+
if (!href) return;
|
|
688
|
+
|
|
689
|
+
// Send link: send: scheme — submit message on behalf of user
|
|
690
|
+
if (href.indexOf('send:') === 0) {
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
var message = decodeURIComponent(href.substring(5));
|
|
693
|
+
submitChatMessage(message);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Navigation link: root-relative path
|
|
698
|
+
if (href.charAt(0) === '/' && href.charAt(1) !== '/') {
|
|
699
|
+
e.preventDefault();
|
|
700
|
+
navigateTo(href);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// External links: open in new tab
|
|
705
|
+
anchor.target = '_blank';
|
|
706
|
+
anchor.rel = 'noopener';
|
|
707
|
+
});
|
|
708
|
+
// Suggestion chip click handler
|
|
709
|
+
cm.addEventListener('click', function(e) {
|
|
710
|
+
var btn = e.target.closest ? e.target.closest('.suggestion-chip') : null;
|
|
711
|
+
if (!btn) return;
|
|
712
|
+
var payload = btn.getAttribute('data-payload') || '';
|
|
713
|
+
// Disable all chips in this message
|
|
714
|
+
var chips = btn.parentNode.querySelectorAll('.suggestion-chip');
|
|
715
|
+
for (var i = 0; i < chips.length; i++) chips[i].disabled = true;
|
|
716
|
+
submitChatMessage(payload);
|
|
717
|
+
});
|
|
718
|
+
})();
|
|
719
|
+
|
|
720
|
+
// --- Undo / Try Again Links (§9) -----------------------------------------
|
|
721
|
+
|
|
722
|
+
function appendUndoLinks(lastUserMsg) {
|
|
723
|
+
var cm = document.getElementById('chatMessages');
|
|
724
|
+
if (!cm || pageVersion <= 0) return;
|
|
725
|
+
|
|
726
|
+
var undoDiv = document.createElement('div');
|
|
727
|
+
undoDiv.className = 'synthos-undo-link';
|
|
728
|
+
undoDiv.style.cssText = 'text-align:right;padding:4px 12px 2px;';
|
|
729
|
+
|
|
730
|
+
var undoLink = document.createElement('a');
|
|
731
|
+
undoLink.href = '#';
|
|
732
|
+
undoLink.textContent = 'undo';
|
|
733
|
+
undoLink.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
|
|
734
|
+
undoLink.addEventListener('mouseenter', function() { undoLink.style.opacity = '1'; undoLink.style.textDecoration = 'underline'; });
|
|
735
|
+
undoLink.addEventListener('mouseleave', function() { undoLink.style.opacity = '0.7'; undoLink.style.textDecoration = 'none'; });
|
|
736
|
+
|
|
737
|
+
undoLink.addEventListener('click', function(e) {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
doUndo();
|
|
740
|
+
});
|
|
741
|
+
undoDiv.appendChild(undoLink);
|
|
742
|
+
|
|
743
|
+
if (lastUserMsg) {
|
|
744
|
+
var sep = document.createTextNode(' \u00b7 ');
|
|
745
|
+
undoDiv.appendChild(sep);
|
|
746
|
+
|
|
747
|
+
var tryLink = document.createElement('a');
|
|
748
|
+
tryLink.href = '#';
|
|
749
|
+
tryLink.textContent = 'try again';
|
|
750
|
+
tryLink.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
|
|
751
|
+
tryLink.addEventListener('mouseenter', function() { tryLink.style.opacity = '1'; tryLink.style.textDecoration = 'underline'; });
|
|
752
|
+
tryLink.addEventListener('mouseleave', function() { tryLink.style.opacity = '0.7'; tryLink.style.textDecoration = 'none'; });
|
|
753
|
+
|
|
754
|
+
tryLink.addEventListener('click', function(e) {
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
doTryAgain(lastUserMsg);
|
|
757
|
+
});
|
|
758
|
+
undoDiv.appendChild(tryLink);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
cm.appendChild(undoDiv);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// --- Clear Conversation Action -------------------------------------------
|
|
765
|
+
|
|
766
|
+
function appendClearAction() {
|
|
767
|
+
var cm = document.getElementById('chatMessages');
|
|
768
|
+
if (!cm) return;
|
|
769
|
+
// Remove any existing clear action first
|
|
770
|
+
var existing = cm.querySelector('.synthos-clear-action');
|
|
771
|
+
if (existing) existing.remove();
|
|
772
|
+
|
|
773
|
+
// Regular pages now rely on undo to roll back changes — no chat-level reset
|
|
774
|
+
// affordance. Only ephemeral pages (Starters/System) still surface the link
|
|
775
|
+
// as "Clear conversation", since their chat history isn't journaled to disk.
|
|
776
|
+
if (!shellInit.noPersistHistory) return;
|
|
777
|
+
|
|
778
|
+
// Only show if there's conversation beyond the initial greeting
|
|
779
|
+
var msgs = cm.querySelectorAll('.chat-message');
|
|
780
|
+
if (msgs.length <= 1) return;
|
|
781
|
+
|
|
782
|
+
var clearDiv = document.createElement('div');
|
|
783
|
+
clearDiv.className = 'synthos-clear-action';
|
|
784
|
+
clearDiv.style.cssText = 'text-align:center;padding:8px 12px 4px;';
|
|
785
|
+
|
|
786
|
+
var clearLink = document.createElement('a');
|
|
787
|
+
clearLink.href = '#';
|
|
788
|
+
clearLink.textContent = 'Clear conversation';
|
|
789
|
+
clearLink.style.cssText = 'color:var(--bodySubtext,#888);font-size:12px;text-decoration:none;opacity:0.7;';
|
|
790
|
+
clearLink.addEventListener('mouseenter', function() { clearLink.style.opacity = '1'; clearLink.style.textDecoration = 'underline'; });
|
|
791
|
+
clearLink.addEventListener('mouseleave', function() { clearLink.style.opacity = '0.7'; clearLink.style.textDecoration = 'none'; });
|
|
792
|
+
clearLink.addEventListener('click', function(e) {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
doReset();
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
clearDiv.appendChild(clearLink);
|
|
798
|
+
cm.appendChild(clearDiv);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
function doReset() {
|
|
803
|
+
if (isRequestActive) return;
|
|
804
|
+
setChatBusy(true);
|
|
805
|
+
showLoadingOverlay('Updating Page');
|
|
806
|
+
|
|
807
|
+
fetch('/' + pageName + '/reset', {
|
|
808
|
+
method: 'POST',
|
|
809
|
+
headers: { 'Content-Type': 'application/json' },
|
|
810
|
+
body: '{}'
|
|
811
|
+
})
|
|
812
|
+
.then(function(res) { return res.json(); })
|
|
813
|
+
.then(function(result) {
|
|
814
|
+
setChatBusy(false);
|
|
815
|
+
setPageDirty(false);
|
|
816
|
+
pageVersion = 0;
|
|
817
|
+
|
|
818
|
+
// Reload the iframe with the fresh page
|
|
819
|
+
sendToPage('page:load', { html: result.html });
|
|
820
|
+
|
|
821
|
+
// Re-render chat with just the greeting
|
|
822
|
+
var cm = document.getElementById('chatMessages');
|
|
823
|
+
if (cm) cm.innerHTML = '';
|
|
824
|
+
if (result.greeting && result.greeting.length > 0) {
|
|
825
|
+
renderInitialMessages(result.greeting);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
hideLoadingOverlay();
|
|
829
|
+
var ci = document.getElementById('chatInput');
|
|
830
|
+
if (ci) ci.focus();
|
|
831
|
+
scrollChatToBottom();
|
|
832
|
+
})
|
|
833
|
+
.catch(function(err) {
|
|
834
|
+
setChatBusy(false);
|
|
835
|
+
console.error('Reset failed:', err);
|
|
836
|
+
hideLoadingOverlay();
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function doUndo() {
|
|
841
|
+
if (isRequestActive) return;
|
|
842
|
+
setChatBusy(true);
|
|
843
|
+
showLoadingOverlay('Updating Page');
|
|
844
|
+
|
|
845
|
+
fetch('/' + pageName + '/undo', {
|
|
846
|
+
method: 'POST',
|
|
847
|
+
headers: { 'Content-Type': 'application/json' },
|
|
848
|
+
body: '{}'
|
|
849
|
+
})
|
|
850
|
+
.then(function(res) { return res.text(); })
|
|
851
|
+
.then(function(html) {
|
|
852
|
+
setChatBusy(false);
|
|
853
|
+
if (pageVersion > 0) pageVersion--;
|
|
854
|
+
// Pending c-files may still remain after undo — mirror that.
|
|
855
|
+
setPageDirty(pageVersion > 0);
|
|
856
|
+
sendToPage('page:load', { html: html });
|
|
857
|
+
hideLoadingOverlay();
|
|
858
|
+
|
|
859
|
+
// Remove the last undo link + the entire last turn from the chat.
|
|
860
|
+
// A turn is: one user message followed by one-or-more assistant
|
|
861
|
+
// bubbles (primary reply, plus any validation/skipped-ops warnings
|
|
862
|
+
// appended after it). Counting "last N messages" misses the user
|
|
863
|
+
// message whenever warnings pushed it further up the list.
|
|
864
|
+
var cm = document.getElementById('chatMessages');
|
|
865
|
+
if (cm) {
|
|
866
|
+
var undoLinks = cm.querySelectorAll('.synthos-undo-link');
|
|
867
|
+
if (undoLinks.length > 0) undoLinks[undoLinks.length - 1].remove();
|
|
868
|
+
var userMsgs = cm.querySelectorAll('.chat-message--user');
|
|
869
|
+
if (userMsgs.length > 0) {
|
|
870
|
+
var lastUser = userMsgs[userMsgs.length - 1];
|
|
871
|
+
var cursor = lastUser.nextElementSibling;
|
|
872
|
+
while (cursor) {
|
|
873
|
+
var next = cursor.nextElementSibling;
|
|
874
|
+
if (cursor.classList.contains('chat-message')) cursor.remove();
|
|
875
|
+
cursor = next;
|
|
876
|
+
}
|
|
877
|
+
lastUser.remove();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
appendClearAction();
|
|
881
|
+
scrollChatToBottom();
|
|
882
|
+
})
|
|
883
|
+
.catch(function(err) {
|
|
884
|
+
setChatBusy(false);
|
|
885
|
+
console.error('Undo failed:', err);
|
|
886
|
+
hideLoadingOverlay();
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function doTryAgain(message) {
|
|
891
|
+
if (isRequestActive) return;
|
|
892
|
+
showLoadingOverlay();
|
|
893
|
+
var ci = document.getElementById('chatInput');
|
|
894
|
+
if (ci) ci.disabled = true;
|
|
895
|
+
var sb = document.querySelector('.chat-send-btn');
|
|
896
|
+
if (sb) sb.disabled = true;
|
|
897
|
+
|
|
898
|
+
setChatBusy(true);
|
|
899
|
+
var tryAgainBody = { message: message, tryAgain: true, history: extractHistory() };
|
|
900
|
+
if (shellInit.builderStarter) tryAgainBody.starter = shellInit.builderStarter;
|
|
901
|
+
fetch('/' + pageName, {
|
|
902
|
+
method: 'POST',
|
|
903
|
+
headers: { 'Content-Type': 'application/json' },
|
|
904
|
+
body: JSON.stringify(tryAgainBody)
|
|
905
|
+
})
|
|
906
|
+
.then(function(res) {
|
|
907
|
+
// Stream NDJSON
|
|
908
|
+
var reader = res.body.getReader();
|
|
909
|
+
var decoder = new TextDecoder();
|
|
910
|
+
var buffer = '';
|
|
911
|
+
var finalResult = null;
|
|
912
|
+
function processLines() {
|
|
913
|
+
var nlIdx;
|
|
914
|
+
while ((nlIdx = buffer.indexOf('\n')) !== -1) {
|
|
915
|
+
var line = buffer.slice(0, nlIdx).trim();
|
|
916
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
917
|
+
if (!line) continue;
|
|
918
|
+
try {
|
|
919
|
+
var evt = JSON.parse(line);
|
|
920
|
+
if (evt.event === 'result') finalResult = evt;
|
|
921
|
+
else if (evt.event === 'error') finalResult = { errorText: evt.error };
|
|
922
|
+
} catch (e) { /* skip */ }
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function readChunk() {
|
|
926
|
+
return reader.read().then(function(chunk) {
|
|
927
|
+
if (chunk.done) { processLines(); return finalResult; }
|
|
928
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
929
|
+
processLines();
|
|
930
|
+
return readChunk();
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return readChunk();
|
|
934
|
+
})
|
|
935
|
+
.then(function(result) {
|
|
936
|
+
setChatBusy(false);
|
|
937
|
+
if (!result) { hideLoadingOverlay(); return; }
|
|
938
|
+
setPageDirty(true);
|
|
939
|
+
if (typeof result.version === 'number') pageVersion = result.version;
|
|
940
|
+
sendToPage('page:load', { html: result.html });
|
|
941
|
+
hideLoadingOverlay();
|
|
942
|
+
if (ci) { ci.disabled = false; ci.focus(); }
|
|
943
|
+
if (sb) sb.disabled = false;
|
|
944
|
+
|
|
945
|
+
// Remove the last undo link + every assistant/warning bubble that
|
|
946
|
+
// followed the most recent user message (primary reply plus any
|
|
947
|
+
// validation/skipped-ops warnings). Keep the user message itself —
|
|
948
|
+
// try-again re-submits it verbatim.
|
|
949
|
+
var cm = document.getElementById('chatMessages');
|
|
950
|
+
if (cm) {
|
|
951
|
+
var undoLinks = cm.querySelectorAll('.synthos-undo-link');
|
|
952
|
+
if (undoLinks.length > 0) undoLinks[undoLinks.length - 1].remove();
|
|
953
|
+
var userMsgs = cm.querySelectorAll('.chat-message--user');
|
|
954
|
+
if (userMsgs.length > 0) {
|
|
955
|
+
var lastUser = userMsgs[userMsgs.length - 1];
|
|
956
|
+
var cursor = lastUser.nextElementSibling;
|
|
957
|
+
while (cursor) {
|
|
958
|
+
var next = cursor.nextElementSibling;
|
|
959
|
+
if (cursor.classList.contains('chat-message')) cursor.remove();
|
|
960
|
+
cursor = next;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (result.replyText) {
|
|
965
|
+
appendChatMessage(pn, result.replyText);
|
|
966
|
+
} else {
|
|
967
|
+
appendChatMessage(pn, 'Page updated (retry).');
|
|
968
|
+
}
|
|
969
|
+
showSkippedOpsWarning(result);
|
|
970
|
+
appendUndoLinks(message);
|
|
971
|
+
appendClearAction();
|
|
972
|
+
scrollChatToBottom();
|
|
973
|
+
})
|
|
974
|
+
.catch(function(err) {
|
|
975
|
+
setChatBusy(false);
|
|
976
|
+
console.error('Try again failed:', err);
|
|
977
|
+
hideLoadingOverlay();
|
|
978
|
+
if (ci) ci.disabled = false;
|
|
979
|
+
if (sb) sb.disabled = false;
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// --- Save / Copy modal (§3) — implementation lives in shell-modals.v3.js -
|
|
984
|
+
// shell-modals.v3.js loads first and exposes:
|
|
985
|
+
// window.__synthOSOpenSaveModal() — save dialog (create new page from current)
|
|
986
|
+
// window.__synthOSOpenCopyModal(pageRecord) — copy dialog (unified with /pages gallery)
|
|
987
|
+
// window.__synthOSOpenEditModal(page, cb) — edit dialog (unified with /pages gallery)
|
|
988
|
+
// window.__synthOSShowError(msg) — shared error dialog
|
|
989
|
+
|
|
990
|
+
// --- Shell Toolbar (§5) --------------------------------------------------
|
|
991
|
+
|
|
992
|
+
var DB_NAME = 'synthos-ui';
|
|
993
|
+
var STORE_NAME = 'panel-state';
|
|
994
|
+
var isBuilderPage = pageName === 'builder';
|
|
995
|
+
|
|
996
|
+
function openDB(cb) {
|
|
997
|
+
var req = indexedDB.open(DB_NAME, 1);
|
|
998
|
+
req.onupgradeneeded = function() {
|
|
999
|
+
var db = req.result;
|
|
1000
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1001
|
+
db.createObjectStore(STORE_NAME);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
req.onsuccess = function() { cb(req.result); };
|
|
1005
|
+
req.onerror = function() { cb(null); };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function saveCollapsed(collapsed) {
|
|
1009
|
+
openDB(function(db) {
|
|
1010
|
+
if (!db) return;
|
|
1011
|
+
var tx = db.transaction(STORE_NAME, 'readwrite');
|
|
1012
|
+
tx.objectStore(STORE_NAME).put(collapsed, pageName);
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function loadCollapsed(cb) {
|
|
1017
|
+
openDB(function(db) {
|
|
1018
|
+
if (!db) { cb(null); return; }
|
|
1019
|
+
var tx = db.transaction(STORE_NAME, 'readonly');
|
|
1020
|
+
var req = tx.objectStore(STORE_NAME).get(pageName);
|
|
1021
|
+
req.onsuccess = function() { cb(req.result); };
|
|
1022
|
+
req.onerror = function() { cb(null); };
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function setCollapsed(collapsed) {
|
|
1027
|
+
if (collapsed) {
|
|
1028
|
+
document.body.classList.add('chat-collapsed');
|
|
1029
|
+
} else {
|
|
1030
|
+
document.body.classList.remove('chat-collapsed');
|
|
1031
|
+
}
|
|
1032
|
+
saveCollapsed(collapsed);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function toggleBuilder() {
|
|
1036
|
+
var collapsed = !document.body.classList.contains('chat-collapsed');
|
|
1037
|
+
setCollapsed(collapsed);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Restore chat panel state
|
|
1041
|
+
if (isBuilderPage) {
|
|
1042
|
+
document.body.classList.remove('chat-collapsed');
|
|
1043
|
+
} else {
|
|
1044
|
+
loadCollapsed(function(val) {
|
|
1045
|
+
if (val === true) {
|
|
1046
|
+
document.body.classList.add('chat-collapsed');
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Builder toggle
|
|
1052
|
+
var builderToggle = document.getElementById('builderToggle');
|
|
1053
|
+
if (builderToggle) {
|
|
1054
|
+
builderToggle.addEventListener('click', toggleBuilder);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Builder close button
|
|
1058
|
+
var builderClose = document.getElementById('builderClose');
|
|
1059
|
+
if (builderClose) {
|
|
1060
|
+
builderClose.addEventListener('click', function() { setCollapsed(true); });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Pages button — propagate firstRun flag until we reach the Pages gallery.
|
|
1064
|
+
// Disabled (greyed out, no clicks) when already on /pages.
|
|
1065
|
+
var pagesBtn = document.getElementById('pagesBtn');
|
|
1066
|
+
if (pagesBtn) {
|
|
1067
|
+
if (pageName === 'pages') {
|
|
1068
|
+
pagesBtn.disabled = true;
|
|
1069
|
+
pagesBtn.style.opacity = '0.4';
|
|
1070
|
+
pagesBtn.style.pointerEvents = 'none';
|
|
1071
|
+
pagesBtn.style.cursor = 'not-allowed';
|
|
1072
|
+
} else {
|
|
1073
|
+
pagesBtn.addEventListener('click', function() {
|
|
1074
|
+
var params = new URLSearchParams(window.location.search);
|
|
1075
|
+
var isFirstRun = params.get('firstRun') && pageName !== 'pages';
|
|
1076
|
+
navigateTo(isFirstRun ? '/pages?firstRun=1' : '/pages');
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Save button — dual-mode:
|
|
1082
|
+
// Builder page: "Save Page" — disabled until there are unsaved changes.
|
|
1083
|
+
// Other pages: "Save Page" while dirty, "Copy Page" when clean.
|
|
1084
|
+
var saveBtn = document.getElementById('saveBtn');
|
|
1085
|
+
function updateSaveBtnState() {
|
|
1086
|
+
if (!saveBtn) return;
|
|
1087
|
+
var mode;
|
|
1088
|
+
if (isBuilderPage) {
|
|
1089
|
+
mode = pageDirty ? 'save' : 'disabled';
|
|
1090
|
+
} else {
|
|
1091
|
+
mode = pageDirty ? 'save' : 'copy';
|
|
1092
|
+
}
|
|
1093
|
+
saveBtn.dataset.mode = mode;
|
|
1094
|
+
if (mode === 'disabled') {
|
|
1095
|
+
saveBtn.disabled = true;
|
|
1096
|
+
saveBtn.setAttribute('aria-label', 'Save Page');
|
|
1097
|
+
saveBtn.style.opacity = '0.4';
|
|
1098
|
+
saveBtn.style.pointerEvents = 'none';
|
|
1099
|
+
saveBtn.style.cursor = 'not-allowed';
|
|
1100
|
+
} else {
|
|
1101
|
+
saveBtn.disabled = false;
|
|
1102
|
+
saveBtn.setAttribute('aria-label', mode === 'save' ? 'Save Page' : 'Copy Page');
|
|
1103
|
+
saveBtn.style.opacity = '';
|
|
1104
|
+
saveBtn.style.pointerEvents = '';
|
|
1105
|
+
saveBtn.style.cursor = '';
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (saveBtn) {
|
|
1109
|
+
saveBtn.addEventListener('click', function() {
|
|
1110
|
+
if (saveBtn.disabled) return;
|
|
1111
|
+
var mode = saveBtn.dataset.mode || 'save';
|
|
1112
|
+
if (mode === 'copy' && window.__synthOSOpenCopyModal) {
|
|
1113
|
+
window.__synthOSOpenCopyModal({
|
|
1114
|
+
name: pageName,
|
|
1115
|
+
title: shellInit.pageTitle || '',
|
|
1116
|
+
categories: shellInit.pageCategories || [],
|
|
1117
|
+
greeting: shellInit.greeting || ''
|
|
1118
|
+
});
|
|
1119
|
+
} else if (window.__synthOSOpenSaveModal) {
|
|
1120
|
+
window.__synthOSOpenSaveModal();
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
updateSaveBtnState();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Settings button
|
|
1127
|
+
var settingsBtn = document.getElementById('settingsBtn');
|
|
1128
|
+
if (settingsBtn) {
|
|
1129
|
+
settingsBtn.addEventListener('click', function() { navigateTo('/settings'); });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// --- Navigation with Dirty Check (§10) -----------------------------------
|
|
1133
|
+
|
|
1134
|
+
// Create unsaved-changes dialog
|
|
1135
|
+
var unsavedOverlay = document.createElement('div');
|
|
1136
|
+
unsavedOverlay.className = 'flm-dialog-overlay';
|
|
1137
|
+
unsavedOverlay.id = 'synthos-unsavedDialog';
|
|
1138
|
+
unsavedOverlay.innerHTML =
|
|
1139
|
+
'<div class="flm-dialog">' +
|
|
1140
|
+
'<div class="flm-dialog-header">' +
|
|
1141
|
+
'<h2 class="flm-dialog-title">Unsaved Changes</h2>' +
|
|
1142
|
+
'<button class="flm-dialog-close" data-icon="Cancel" aria-label="Close" data-dialog-close></button>' +
|
|
1143
|
+
'</div>' +
|
|
1144
|
+
'<div class="flm-dialog-body">You have unsaved changes that will be lost if you leave this page.</div>' +
|
|
1145
|
+
'<div class="flm-dialog-footer">' +
|
|
1146
|
+
'<button class="flm-button" data-dialog-close id="synthos-unsavedStay">Stay</button>' +
|
|
1147
|
+
'<button class="flm-button flm-button--primary" id="synthos-unsavedLeave">Leave</button>' +
|
|
1148
|
+
'</div>' +
|
|
1149
|
+
'</div>';
|
|
1150
|
+
document.body.appendChild(unsavedOverlay);
|
|
1151
|
+
|
|
1152
|
+
var pendingUrl = null;
|
|
1153
|
+
|
|
1154
|
+
document.getElementById('synthos-unsavedLeave').addEventListener('click', function() {
|
|
1155
|
+
unsavedOverlay.classList.remove('flm-dialog-overlay--open');
|
|
1156
|
+
if (pendingUrl) {
|
|
1157
|
+
setPageDirty(false);
|
|
1158
|
+
window.location.href = pendingUrl;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
document.getElementById('synthos-unsavedStay').addEventListener('click', function() {
|
|
1163
|
+
unsavedOverlay.classList.remove('flm-dialog-overlay--open');
|
|
1164
|
+
pendingUrl = null;
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
function navigateTo(url) {
|
|
1168
|
+
if ((pageDirty || isRequestActive) && (shellInit.isRequiredPage || shellInit.isStarter)) {
|
|
1169
|
+
pendingUrl = url;
|
|
1170
|
+
unsavedOverlay.classList.add('flm-dialog-overlay--open');
|
|
1171
|
+
} else {
|
|
1172
|
+
window.location.href = url;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// --- Focus Management (§6) -----------------------------------------------
|
|
1177
|
+
|
|
1178
|
+
(function() {
|
|
1179
|
+
var ci = document.getElementById('chatInput');
|
|
1180
|
+
if (!ci) return;
|
|
1181
|
+
|
|
1182
|
+
ci.addEventListener('mousedown', function(e) {
|
|
1183
|
+
e.stopPropagation();
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
['keydown', 'keyup', 'keypress'].forEach(function(type) {
|
|
1187
|
+
document.addEventListener(type, function(e) {
|
|
1188
|
+
if (document.activeElement === ci) {
|
|
1189
|
+
if (e.key === 'Enter' && !e.shiftKey) return;
|
|
1190
|
+
e.stopImmediatePropagation();
|
|
1191
|
+
}
|
|
1192
|
+
}, true);
|
|
1193
|
+
});
|
|
1194
|
+
})();
|
|
1195
|
+
|
|
1196
|
+
// --- Brainstorm (§7) -----------------------------------------------------
|
|
1197
|
+
|
|
1198
|
+
(function() {
|
|
1199
|
+
var chatInputEl = document.getElementById('chatInput');
|
|
1200
|
+
if (!chatInputEl) return;
|
|
1201
|
+
|
|
1202
|
+
// --- Create icon row (.chat-input-wrapper) ---
|
|
1203
|
+
var form = document.getElementById('chatForm');
|
|
1204
|
+
var wrapper = document.querySelector('.chat-input-wrapper');
|
|
1205
|
+
if (!wrapper) {
|
|
1206
|
+
wrapper = document.createElement('div');
|
|
1207
|
+
wrapper.className = 'chat-input-wrapper';
|
|
1208
|
+
if (form) form.appendChild(wrapper);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// --- Brainstorm icon button ---
|
|
1212
|
+
var brainstormBtn = document.createElement('button');
|
|
1213
|
+
brainstormBtn.type = 'button';
|
|
1214
|
+
brainstormBtn.className = 'brainstorm-icon-btn';
|
|
1215
|
+
brainstormBtn.setAttribute('aria-label', 'Brainstorm ideas');
|
|
1216
|
+
brainstormBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
1217
|
+
'<circle cx="12" cy="12" r="3"></circle>' +
|
|
1218
|
+
'<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"></path>' +
|
|
1219
|
+
'</svg>';
|
|
1220
|
+
wrapper.appendChild(brainstormBtn);
|
|
1221
|
+
|
|
1222
|
+
// --- Send button ---
|
|
1223
|
+
var sendBtn = document.createElement('button');
|
|
1224
|
+
sendBtn.type = 'submit';
|
|
1225
|
+
sendBtn.className = 'chat-send-btn';
|
|
1226
|
+
sendBtn.setAttribute('aria-label', 'Send message');
|
|
1227
|
+
sendBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
1228
|
+
'<line x1="12" y1="19" x2="12" y2="5"></line>' +
|
|
1229
|
+
'<polyline points="5 12 12 5 19 12"></polyline>' +
|
|
1230
|
+
'</svg>';
|
|
1231
|
+
wrapper.appendChild(sendBtn);
|
|
1232
|
+
|
|
1233
|
+
// --- Auto-grow textarea ---
|
|
1234
|
+
chatInputEl.addEventListener('input', function() {
|
|
1235
|
+
this.style.height = 'auto';
|
|
1236
|
+
var maxH = parseFloat(getComputedStyle(this).maxHeight) || 120;
|
|
1237
|
+
this.style.height = Math.min(this.scrollHeight, maxH) + 'px';
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// --- Create brainstorm modal ---
|
|
1241
|
+
var bModal = document.createElement('div');
|
|
1242
|
+
bModal.id = 'synthos-brainstormModal';
|
|
1243
|
+
bModal.className = 'modal-overlay brainstorm-modal';
|
|
1244
|
+
bModal.innerHTML =
|
|
1245
|
+
'<div class="modal-content">' +
|
|
1246
|
+
'<div class="modal-header">' +
|
|
1247
|
+
'<span>Brainstorm</span>' +
|
|
1248
|
+
'<button type="button" class="brainstorm-close-btn" id="synthos-brainstormCloseBtn">×</button>' +
|
|
1249
|
+
'</div>' +
|
|
1250
|
+
'<div class="brainstorm-messages" id="synthos-brainstormMessages"></div>' +
|
|
1251
|
+
'<div class="brainstorm-input-row">' +
|
|
1252
|
+
'<input type="text" class="brainstorm-input" id="synthos-brainstormInput" placeholder="What\'s on your mind...">' +
|
|
1253
|
+
'<button type="button" class="brainstorm-send-btn" id="synthos-brainstormSendBtn">Send</button>' +
|
|
1254
|
+
'</div>' +
|
|
1255
|
+
'</div>';
|
|
1256
|
+
document.body.appendChild(bModal);
|
|
1257
|
+
|
|
1258
|
+
var brainstormHistory = [];
|
|
1259
|
+
|
|
1260
|
+
function openBrainstorm() {
|
|
1261
|
+
bModal.classList.add('show');
|
|
1262
|
+
var topic = chatInputEl.value.trim();
|
|
1263
|
+
if (topic) {
|
|
1264
|
+
chatInputEl.value = '';
|
|
1265
|
+
sendBrainstormText(topic, true);
|
|
1266
|
+
} else {
|
|
1267
|
+
sendBrainstormText('', true);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function closeBrainstorm() {
|
|
1272
|
+
bModal.classList.remove('show');
|
|
1273
|
+
brainstormHistory = [];
|
|
1274
|
+
document.getElementById('synthos-brainstormMessages').innerHTML = '';
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function scrollBrainstormToBottom() {
|
|
1278
|
+
var el = document.getElementById('synthos-brainstormMessages');
|
|
1279
|
+
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function appendBrainstormMessage(role, text, prompt, suggestions, isOpener) {
|
|
1283
|
+
var div = document.createElement('div');
|
|
1284
|
+
div.className = 'brainstorm-message ' + (role === 'user' ? 'brainstorm-user' : 'brainstorm-assistant');
|
|
1285
|
+
if (role === 'assistant') {
|
|
1286
|
+
var fullText = text;
|
|
1287
|
+
if (suggestions && suggestions.length > 0) {
|
|
1288
|
+
fullText += '\n\n';
|
|
1289
|
+
suggestions.forEach(function(s) {
|
|
1290
|
+
fullText += '- [' + s + '](suggest:' + encodeURIComponent(s) + ')\n';
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
var rendered;
|
|
1294
|
+
if (typeof marked !== 'undefined') {
|
|
1295
|
+
rendered = renderMarkdown(fullText);
|
|
1296
|
+
} else {
|
|
1297
|
+
rendered = { html: escapeHtml(fullText), suggestions: [] };
|
|
1298
|
+
}
|
|
1299
|
+
div.innerHTML = '<strong>' + pn + ':</strong> ' + rendered.html;
|
|
1300
|
+
var chips = buildSuggestionChips(rendered.suggestions);
|
|
1301
|
+
if (chips) div.appendChild(chips);
|
|
1302
|
+
if (prompt && !isOpener) {
|
|
1303
|
+
var btnRow = document.createElement('div');
|
|
1304
|
+
btnRow.className = 'brainstorm-build-row';
|
|
1305
|
+
var buildBtn = document.createElement('button');
|
|
1306
|
+
buildBtn.type = 'button';
|
|
1307
|
+
buildBtn.className = 'brainstorm-build-btn';
|
|
1308
|
+
buildBtn.textContent = 'Build It';
|
|
1309
|
+
buildBtn.setAttribute('data-prompt', prompt);
|
|
1310
|
+
buildBtn.addEventListener('click', function() {
|
|
1311
|
+
chatInputEl.value = this.getAttribute('data-prompt');
|
|
1312
|
+
closeBrainstorm();
|
|
1313
|
+
chatInputEl.focus();
|
|
1314
|
+
});
|
|
1315
|
+
btnRow.appendChild(buildBtn);
|
|
1316
|
+
div.appendChild(btnRow);
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
div.textContent = text;
|
|
1320
|
+
}
|
|
1321
|
+
document.getElementById('synthos-brainstormMessages').appendChild(div);
|
|
1322
|
+
scrollBrainstormToBottom();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Delegated click handler for suggestion chips inside brainstorm messages
|
|
1326
|
+
document.getElementById('synthos-brainstormMessages').addEventListener('click', function(e) {
|
|
1327
|
+
var btn = e.target.closest ? e.target.closest('.suggestion-chip') : null;
|
|
1328
|
+
if (!btn) return;
|
|
1329
|
+
var payload = btn.getAttribute('data-payload') || '';
|
|
1330
|
+
// Disable all suggestion chips in brainstorm to prevent re-clicks
|
|
1331
|
+
var chips = document.querySelectorAll('#synthos-brainstormMessages .suggestion-chip');
|
|
1332
|
+
for (var i = 0; i < chips.length; i++) {
|
|
1333
|
+
chips[i].disabled = true;
|
|
1334
|
+
}
|
|
1335
|
+
sendBrainstormText(payload, false);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
function getBrainstormContext() {
|
|
1339
|
+
// Read chat history from the shell's own chat panel
|
|
1340
|
+
var chatEl = document.getElementById('chatMessages');
|
|
1341
|
+
if (!chatEl) return '<CHAT_HISTORY>\n';
|
|
1342
|
+
var msgs = chatEl.querySelectorAll('.chat-message');
|
|
1343
|
+
var lines = [];
|
|
1344
|
+
var started = false;
|
|
1345
|
+
for (var i = 0; i < msgs.length; i++) {
|
|
1346
|
+
var text = msgs[i].innerText;
|
|
1347
|
+
if (!started && /^User:/i.test(text.trim())) started = true;
|
|
1348
|
+
if (started) lines.push(text);
|
|
1349
|
+
}
|
|
1350
|
+
return '<CHAT_HISTORY>\n' + lines.join('\n');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function sendBrainstormMessage() {
|
|
1354
|
+
var input = document.getElementById('synthos-brainstormInput');
|
|
1355
|
+
var text = input.value.trim();
|
|
1356
|
+
if (!text) return;
|
|
1357
|
+
input.value = '';
|
|
1358
|
+
sendBrainstormText(text, false);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function sendBrainstormText(text, isOpener) {
|
|
1362
|
+
var input = document.getElementById('synthos-brainstormInput');
|
|
1363
|
+
var userMsg = text || (isOpener ? 'Look at the conversation so far and suggest what we could build or improve.' : '');
|
|
1364
|
+
if (!userMsg) return;
|
|
1365
|
+
|
|
1366
|
+
if (text) appendBrainstormMessage('user', text);
|
|
1367
|
+
brainstormHistory.push({ role: 'user', content: userMsg });
|
|
1368
|
+
|
|
1369
|
+
var thinking = document.createElement('div');
|
|
1370
|
+
thinking.className = 'brainstorm-thinking';
|
|
1371
|
+
thinking.id = 'synthos-brainstormThinking';
|
|
1372
|
+
thinking.textContent = 'Thinking...';
|
|
1373
|
+
document.getElementById('synthos-brainstormMessages').appendChild(thinking);
|
|
1374
|
+
scrollBrainstormToBottom();
|
|
1375
|
+
|
|
1376
|
+
input.disabled = true;
|
|
1377
|
+
document.getElementById('synthos-brainstormSendBtn').disabled = true;
|
|
1378
|
+
|
|
1379
|
+
fetch('/api/brainstorm', {
|
|
1380
|
+
method: 'POST',
|
|
1381
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1382
|
+
body: JSON.stringify({
|
|
1383
|
+
context: getBrainstormContext(),
|
|
1384
|
+
messages: brainstormHistory
|
|
1385
|
+
})
|
|
1386
|
+
})
|
|
1387
|
+
.then(function(res) {
|
|
1388
|
+
if (!res.ok) throw new Error('Brainstorm request failed');
|
|
1389
|
+
return res.json();
|
|
1390
|
+
})
|
|
1391
|
+
.then(function(data) {
|
|
1392
|
+
var thinkingEl = document.getElementById('synthos-brainstormThinking');
|
|
1393
|
+
if (thinkingEl) thinkingEl.remove();
|
|
1394
|
+
|
|
1395
|
+
var response = data.response || 'Sorry, I didn\'t get a response.';
|
|
1396
|
+
var prompt = data.prompt || '';
|
|
1397
|
+
var suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
|
|
1398
|
+
appendBrainstormMessage('assistant', response, prompt, suggestions, isOpener);
|
|
1399
|
+
brainstormHistory.push({
|
|
1400
|
+
role: 'assistant',
|
|
1401
|
+
content: response + '\n\n[Suggested prompt: ' + prompt + ']'
|
|
1402
|
+
});
|
|
1403
|
+
})
|
|
1404
|
+
.catch(function(err) {
|
|
1405
|
+
var thinkingEl = document.getElementById('synthos-brainstormThinking');
|
|
1406
|
+
if (thinkingEl) thinkingEl.remove();
|
|
1407
|
+
appendBrainstormMessage('assistant', 'Something went wrong: ' + err.message);
|
|
1408
|
+
})
|
|
1409
|
+
.finally(function() {
|
|
1410
|
+
input.disabled = false;
|
|
1411
|
+
document.getElementById('synthos-brainstormSendBtn').disabled = false;
|
|
1412
|
+
input.focus();
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
brainstormBtn.addEventListener('click', openBrainstorm);
|
|
1417
|
+
document.getElementById('synthos-brainstormCloseBtn').addEventListener('click', closeBrainstorm);
|
|
1418
|
+
|
|
1419
|
+
var brainstormMouseDownTarget = null;
|
|
1420
|
+
bModal.addEventListener('mousedown', function(e) { brainstormMouseDownTarget = e.target; });
|
|
1421
|
+
bModal.addEventListener('click', function(e) {
|
|
1422
|
+
if (e.target === bModal && brainstormMouseDownTarget === bModal) closeBrainstorm();
|
|
1423
|
+
brainstormMouseDownTarget = null;
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
document.addEventListener('keydown', function(e) {
|
|
1427
|
+
if (e.key === 'Escape' && bModal.classList.contains('show')) closeBrainstorm();
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
document.getElementById('synthos-brainstormSendBtn').addEventListener('click', sendBrainstormMessage);
|
|
1431
|
+
document.getElementById('synthos-brainstormInput').addEventListener('keydown', function(e) {
|
|
1432
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1433
|
+
e.preventDefault();
|
|
1434
|
+
sendBrainstormMessage();
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
})();
|
|
1438
|
+
|
|
1439
|
+
// --- Attachments (§8) ----------------------------------------------------
|
|
1440
|
+
|
|
1441
|
+
var shellAttachments = [];
|
|
1442
|
+
|
|
1443
|
+
(function() {
|
|
1444
|
+
var chatInputEl = document.getElementById('chatInput');
|
|
1445
|
+
if (!chatInputEl) return;
|
|
1446
|
+
var chatFormEl = document.getElementById('chatForm');
|
|
1447
|
+
var wrapper = chatFormEl ? chatFormEl.querySelector('.chat-input-wrapper') : null;
|
|
1448
|
+
if (!wrapper) return;
|
|
1449
|
+
|
|
1450
|
+
// Pills container
|
|
1451
|
+
var pillsContainer = document.createElement('div');
|
|
1452
|
+
pillsContainer.className = 'attachment-pills';
|
|
1453
|
+
if (chatFormEl) chatFormEl.insertBefore(pillsContainer, chatFormEl.firstChild);
|
|
1454
|
+
|
|
1455
|
+
function renderPills() {
|
|
1456
|
+
pillsContainer.innerHTML = '';
|
|
1457
|
+
if (!shellAttachments || shellAttachments.length === 0) return;
|
|
1458
|
+
for (var i = 0; i < shellAttachments.length; i++) {
|
|
1459
|
+
(function(idx) {
|
|
1460
|
+
var att = shellAttachments[idx];
|
|
1461
|
+
var pill = document.createElement('div');
|
|
1462
|
+
pill.className = 'attachment-pill';
|
|
1463
|
+
|
|
1464
|
+
var thumb = document.createElement('img');
|
|
1465
|
+
thumb.src = 'data:' + att.mediaType + ';base64,' + att.data;
|
|
1466
|
+
thumb.style.cssText = 'width:24px;height:24px;object-fit:cover;border-radius:3px;';
|
|
1467
|
+
pill.appendChild(thumb);
|
|
1468
|
+
|
|
1469
|
+
var nameSpan = document.createElement('span');
|
|
1470
|
+
nameSpan.textContent = att.name || 'image';
|
|
1471
|
+
nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;';
|
|
1472
|
+
pill.appendChild(nameSpan);
|
|
1473
|
+
|
|
1474
|
+
var removeBtn = document.createElement('button');
|
|
1475
|
+
removeBtn.type = 'button';
|
|
1476
|
+
removeBtn.className = 'attachment-pill-remove';
|
|
1477
|
+
removeBtn.textContent = '\u00d7';
|
|
1478
|
+
removeBtn.addEventListener('click', function() {
|
|
1479
|
+
shellAttachments.splice(idx, 1);
|
|
1480
|
+
renderPills();
|
|
1481
|
+
});
|
|
1482
|
+
pill.appendChild(removeBtn);
|
|
1483
|
+
|
|
1484
|
+
pillsContainer.appendChild(pill);
|
|
1485
|
+
})(i);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function addImageFromDataUrl(dataUrl, name) {
|
|
1490
|
+
var commaIdx = dataUrl.indexOf(',');
|
|
1491
|
+
if (commaIdx === -1) return;
|
|
1492
|
+
var meta = dataUrl.substring(0, commaIdx);
|
|
1493
|
+
var base64 = dataUrl.substring(commaIdx + 1);
|
|
1494
|
+
var mediaType = meta.replace('data:', '').replace(';base64', '');
|
|
1495
|
+
shellAttachments.push({ mediaType: mediaType, data: base64, name: name || 'image' });
|
|
1496
|
+
renderPills();
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// File input
|
|
1500
|
+
var fileInput = document.createElement('input');
|
|
1501
|
+
fileInput.type = 'file';
|
|
1502
|
+
fileInput.style.display = 'none';
|
|
1503
|
+
document.body.appendChild(fileInput);
|
|
1504
|
+
|
|
1505
|
+
fileInput.addEventListener('change', function() {
|
|
1506
|
+
var file = fileInput.files && fileInput.files[0];
|
|
1507
|
+
if (!file) return;
|
|
1508
|
+
var reader = new FileReader();
|
|
1509
|
+
reader.onload = function() { addImageFromDataUrl(reader.result, file.name); };
|
|
1510
|
+
reader.readAsDataURL(file);
|
|
1511
|
+
fileInput.value = '';
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// Paste from clipboard
|
|
1515
|
+
document.addEventListener('paste', function(e) {
|
|
1516
|
+
var active = document.activeElement;
|
|
1517
|
+
var isEditable = active && (active.isContentEditable ||
|
|
1518
|
+
(active.tagName === 'TEXTAREA' && active !== chatInputEl) ||
|
|
1519
|
+
(active.tagName === 'INPUT' && active !== chatInputEl));
|
|
1520
|
+
if (isEditable) return;
|
|
1521
|
+
|
|
1522
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
1523
|
+
if (!items) return;
|
|
1524
|
+
for (var i = 0; i < items.length; i++) {
|
|
1525
|
+
if (items[i].type.indexOf('image/') === 0) {
|
|
1526
|
+
var blob = items[i].getAsFile();
|
|
1527
|
+
if (!blob) continue;
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
var reader = new FileReader();
|
|
1530
|
+
reader.onload = function() { addImageFromDataUrl(reader.result, 'pasted-image.png'); };
|
|
1531
|
+
reader.readAsDataURL(blob);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// Drag-and-drop
|
|
1538
|
+
if (chatFormEl) {
|
|
1539
|
+
chatFormEl.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); });
|
|
1540
|
+
chatFormEl.addEventListener('drop', function(e) {
|
|
1541
|
+
e.preventDefault();
|
|
1542
|
+
e.stopPropagation();
|
|
1543
|
+
var files = e.dataTransfer && e.dataTransfer.files;
|
|
1544
|
+
if (!files || !files.length) return;
|
|
1545
|
+
for (var i = 0; i < files.length; i++) {
|
|
1546
|
+
(function(file) {
|
|
1547
|
+
var reader = new FileReader();
|
|
1548
|
+
reader.onload = function() { addImageFromDataUrl(reader.result, file.name); };
|
|
1549
|
+
reader.readAsDataURL(file);
|
|
1550
|
+
})(files[i]);
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// + button
|
|
1556
|
+
var attachBtn = document.createElement('button');
|
|
1557
|
+
attachBtn.type = 'button';
|
|
1558
|
+
attachBtn.className = 'attach-btn';
|
|
1559
|
+
attachBtn.setAttribute('aria-label', 'Attach file or screenshot');
|
|
1560
|
+
attachBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
|
|
1561
|
+
wrapper.insertBefore(attachBtn, wrapper.firstChild);
|
|
1562
|
+
|
|
1563
|
+
// Popup menu
|
|
1564
|
+
var menu = document.createElement('div');
|
|
1565
|
+
menu.className = 'attach-menu';
|
|
1566
|
+
|
|
1567
|
+
var menuAttachFile = document.createElement('div');
|
|
1568
|
+
menuAttachFile.className = 'attach-menu-item';
|
|
1569
|
+
menuAttachFile.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"></path></svg> Attach File';
|
|
1570
|
+
menu.appendChild(menuAttachFile);
|
|
1571
|
+
|
|
1572
|
+
var menuScreenshot = document.createElement('div');
|
|
1573
|
+
menuScreenshot.className = 'attach-menu-item';
|
|
1574
|
+
menuScreenshot.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg> Screenshot';
|
|
1575
|
+
menu.appendChild(menuScreenshot);
|
|
1576
|
+
|
|
1577
|
+
wrapper.appendChild(menu);
|
|
1578
|
+
|
|
1579
|
+
var menuOpen = false;
|
|
1580
|
+
function toggleMenu() { menuOpen = !menuOpen; menu.style.display = menuOpen ? 'flex' : 'none'; }
|
|
1581
|
+
function closeMenu() { menuOpen = false; menu.style.display = 'none'; }
|
|
1582
|
+
|
|
1583
|
+
attachBtn.addEventListener('click', function(e) { e.stopPropagation(); toggleMenu(); });
|
|
1584
|
+
document.addEventListener('click', function() { if (menuOpen) closeMenu(); });
|
|
1585
|
+
menu.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
1586
|
+
|
|
1587
|
+
menuAttachFile.addEventListener('click', function() { closeMenu(); fileInput.click(); });
|
|
1588
|
+
|
|
1589
|
+
// Screenshot flow
|
|
1590
|
+
menuScreenshot.addEventListener('click', function() {
|
|
1591
|
+
closeMenu();
|
|
1592
|
+
startScreenshotAnnotation();
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
function startScreenshotAnnotation() {
|
|
1596
|
+
var viewerPanel = document.getElementById('viewerPanel');
|
|
1597
|
+
if (!viewerPanel) return;
|
|
1598
|
+
|
|
1599
|
+
var overlay = document.createElement('div');
|
|
1600
|
+
overlay.className = 'screenshot-overlay';
|
|
1601
|
+
|
|
1602
|
+
var instrBar = document.createElement('div');
|
|
1603
|
+
instrBar.style.cssText = 'position:absolute;top:10px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:white;padding:6px 14px;border-radius:6px;font-size:13px;z-index:10;pointer-events:none;';
|
|
1604
|
+
instrBar.textContent = 'Draw rectangles to highlight areas, then click Capture';
|
|
1605
|
+
overlay.appendChild(instrBar);
|
|
1606
|
+
|
|
1607
|
+
var actions = document.createElement('div');
|
|
1608
|
+
actions.className = 'screenshot-actions';
|
|
1609
|
+
actions.style.cssText = 'position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:10;';
|
|
1610
|
+
|
|
1611
|
+
var captureBtn = document.createElement('button');
|
|
1612
|
+
captureBtn.type = 'button';
|
|
1613
|
+
captureBtn.textContent = 'Capture';
|
|
1614
|
+
captureBtn.className = 'brainstorm-send-btn';
|
|
1615
|
+
captureBtn.style.cssText = 'padding:6px 16px;font-size:13px;';
|
|
1616
|
+
|
|
1617
|
+
var cancelBtn = document.createElement('button');
|
|
1618
|
+
cancelBtn.type = 'button';
|
|
1619
|
+
cancelBtn.textContent = 'Cancel';
|
|
1620
|
+
cancelBtn.className = 'brainstorm-send-btn';
|
|
1621
|
+
cancelBtn.style.cssText = 'padding:6px 16px;font-size:13px;background:transparent;border:1px solid rgba(255,255,255,0.3);color:white;';
|
|
1622
|
+
|
|
1623
|
+
actions.appendChild(captureBtn);
|
|
1624
|
+
actions.appendChild(cancelBtn);
|
|
1625
|
+
overlay.appendChild(actions);
|
|
1626
|
+
|
|
1627
|
+
var currentRect = null;
|
|
1628
|
+
var startX, startY, isDrawing = false;
|
|
1629
|
+
var allRects = [];
|
|
1630
|
+
|
|
1631
|
+
overlay.addEventListener('mousedown', function(e) {
|
|
1632
|
+
if (e.target.tagName === 'BUTTON') return;
|
|
1633
|
+
isDrawing = true;
|
|
1634
|
+
var overlayBounds = overlay.getBoundingClientRect();
|
|
1635
|
+
startX = e.clientX - overlayBounds.left;
|
|
1636
|
+
startY = e.clientY - overlayBounds.top;
|
|
1637
|
+
currentRect = document.createElement('div');
|
|
1638
|
+
currentRect.className = 'screenshot-rect';
|
|
1639
|
+
currentRect.style.display = 'block';
|
|
1640
|
+
currentRect.style.left = startX + 'px';
|
|
1641
|
+
currentRect.style.top = startY + 'px';
|
|
1642
|
+
currentRect.style.width = '0';
|
|
1643
|
+
currentRect.style.height = '0';
|
|
1644
|
+
overlay.appendChild(currentRect);
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
overlay.addEventListener('mousemove', function(e) {
|
|
1648
|
+
if (!isDrawing || !currentRect) return;
|
|
1649
|
+
var overlayBounds = overlay.getBoundingClientRect();
|
|
1650
|
+
var curX = e.clientX - overlayBounds.left;
|
|
1651
|
+
var curY = e.clientY - overlayBounds.top;
|
|
1652
|
+
var x = Math.min(startX, curX);
|
|
1653
|
+
var y = Math.min(startY, curY);
|
|
1654
|
+
currentRect.style.left = x + 'px';
|
|
1655
|
+
currentRect.style.top = y + 'px';
|
|
1656
|
+
currentRect.style.width = Math.abs(curX - startX) + 'px';
|
|
1657
|
+
currentRect.style.height = Math.abs(curY - startY) + 'px';
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
overlay.addEventListener('mouseup', function() {
|
|
1661
|
+
if (!isDrawing || !currentRect) return;
|
|
1662
|
+
isDrawing = false;
|
|
1663
|
+
var w = parseInt(currentRect.style.width);
|
|
1664
|
+
var h = parseInt(currentRect.style.height);
|
|
1665
|
+
if (w < 10 || h < 10) { currentRect.remove(); currentRect = null; return; }
|
|
1666
|
+
allRects.push({
|
|
1667
|
+
el: currentRect,
|
|
1668
|
+
x: parseInt(currentRect.style.left),
|
|
1669
|
+
y: parseInt(currentRect.style.top),
|
|
1670
|
+
w: w, h: h
|
|
1671
|
+
});
|
|
1672
|
+
currentRect = null;
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
captureBtn.addEventListener('click', doCapture);
|
|
1676
|
+
cancelBtn.addEventListener('click', cleanup);
|
|
1677
|
+
|
|
1678
|
+
function doCapture() {
|
|
1679
|
+
if (typeof html2canvas === 'undefined') {
|
|
1680
|
+
console.error('html2canvas not loaded');
|
|
1681
|
+
cleanup();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
overlay.style.visibility = 'hidden';
|
|
1685
|
+
html2canvas(viewerPanel, { useCORS: true, logging: false }).then(function(fullCanvas) {
|
|
1686
|
+
var vpRect = viewerPanel.getBoundingClientRect();
|
|
1687
|
+
var scaleX = fullCanvas.width / vpRect.width;
|
|
1688
|
+
var scaleY = fullCanvas.height / vpRect.height;
|
|
1689
|
+
var ctx = fullCanvas.getContext('2d');
|
|
1690
|
+
ctx.strokeStyle = 'red';
|
|
1691
|
+
ctx.lineWidth = 3 * Math.max(scaleX, scaleY);
|
|
1692
|
+
for (var i = 0; i < allRects.length; i++) {
|
|
1693
|
+
var r = allRects[i];
|
|
1694
|
+
ctx.strokeRect(r.x * scaleX, r.y * scaleY, r.w * scaleX, r.h * scaleY);
|
|
1695
|
+
}
|
|
1696
|
+
var dataUrl = fullCanvas.toDataURL('image/png');
|
|
1697
|
+
var b64 = dataUrl.split(',')[1];
|
|
1698
|
+
shellAttachments.push({ mediaType: 'image/png', data: b64, name: 'screenshot.png' });
|
|
1699
|
+
renderPills();
|
|
1700
|
+
cleanup();
|
|
1701
|
+
}).catch(function(err) {
|
|
1702
|
+
console.error('Screenshot capture failed:', err);
|
|
1703
|
+
cleanup();
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function cleanup() {
|
|
1708
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
1709
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function onKeyDown(e) { if (e.key === 'Escape') cleanup(); }
|
|
1713
|
+
document.addEventListener('keydown', onKeyDown);
|
|
1714
|
+
|
|
1715
|
+
viewerPanel.style.position = 'relative';
|
|
1716
|
+
viewerPanel.appendChild(overlay);
|
|
1717
|
+
}
|
|
1718
|
+
})();
|
|
1719
|
+
|
|
1720
|
+
// --- Initial Undo Links (§9) — show if version > 0 on load --------------
|
|
1721
|
+
|
|
1722
|
+
if (pageVersion > 0 && !shellInit.isLocked) {
|
|
1723
|
+
// Find the last user message from init messages — the new server
|
|
1724
|
+
// journal always sends { role, content } objects.
|
|
1725
|
+
var lastUserMsg = '';
|
|
1726
|
+
if (shellInit.messages) {
|
|
1727
|
+
for (var j = shellInit.messages.length - 1; j >= 0; j--) {
|
|
1728
|
+
var m = shellInit.messages[j];
|
|
1729
|
+
if (m && m.role === 'user' && typeof m.content === 'string') {
|
|
1730
|
+
lastUserMsg = m.content.trim();
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
appendUndoLinks(lastUserMsg);
|
|
1736
|
+
}
|
|
1737
|
+
// Append clear/reset action (no-op if there's nothing to reset)
|
|
1738
|
+
appendClearAction();
|
|
1739
|
+
|
|
1740
|
+
// --- Editable Greeting (non-locked, non-builder pages) -----------------
|
|
1741
|
+
|
|
1742
|
+
(function() {
|
|
1743
|
+
if (!isGreetingEditable) return;
|
|
1744
|
+
var cm = document.getElementById('chatMessages');
|
|
1745
|
+
if (!cm) return;
|
|
1746
|
+
var firstMsg = cm.querySelector('.chat-message');
|
|
1747
|
+
if (!firstMsg) return;
|
|
1748
|
+
|
|
1749
|
+
// Position the message relatively so we can place the edit icon
|
|
1750
|
+
firstMsg.style.position = 'relative';
|
|
1751
|
+
|
|
1752
|
+
var editBtn = document.createElement('button');
|
|
1753
|
+
editBtn.type = 'button';
|
|
1754
|
+
editBtn.className = 'synthos-greeting-edit-btn';
|
|
1755
|
+
editBtn.setAttribute('aria-label', 'Edit greeting');
|
|
1756
|
+
editBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
|
1757
|
+
editBtn.style.cssText = 'position:absolute;top:6px;right:6px;background:transparent;border:none;color:var(--bodySubtext,#888);cursor:pointer;padding:4px;border-radius:4px;opacity:0;transition:opacity 0.15s;';
|
|
1758
|
+
|
|
1759
|
+
firstMsg.addEventListener('mouseenter', function() { editBtn.style.opacity = '1'; });
|
|
1760
|
+
firstMsg.addEventListener('mouseleave', function() { editBtn.style.opacity = '0'; });
|
|
1761
|
+
|
|
1762
|
+
editBtn.addEventListener('click', function() {
|
|
1763
|
+
var p = firstMsg.querySelector('p');
|
|
1764
|
+
if (!p) return;
|
|
1765
|
+
|
|
1766
|
+
// Extract current greeting text (strip the "SynthOS:" prefix)
|
|
1767
|
+
var strong = p.querySelector('strong');
|
|
1768
|
+
var currentText = '';
|
|
1769
|
+
if (strong) {
|
|
1770
|
+
var clone = p.cloneNode(true);
|
|
1771
|
+
clone.querySelector('strong').remove();
|
|
1772
|
+
currentText = clone.textContent.trim();
|
|
1773
|
+
} else {
|
|
1774
|
+
currentText = p.textContent.trim();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Replace message content with editable textarea
|
|
1778
|
+
var original = firstMsg.innerHTML;
|
|
1779
|
+
firstMsg.innerHTML = '';
|
|
1780
|
+
firstMsg.style.padding = '8px';
|
|
1781
|
+
editBtn.style.display = 'none';
|
|
1782
|
+
|
|
1783
|
+
var textarea = document.createElement('textarea');
|
|
1784
|
+
textarea.className = 'brainstorm-input';
|
|
1785
|
+
textarea.value = currentText;
|
|
1786
|
+
textarea.style.cssText = 'width:100%;min-height:60px;box-sizing:border-box;resize:vertical;font-size:13px;';
|
|
1787
|
+
firstMsg.appendChild(textarea);
|
|
1788
|
+
|
|
1789
|
+
var btnRow = document.createElement('div');
|
|
1790
|
+
btnRow.style.cssText = 'display:flex;justify-content:flex-end;gap:6px;margin-top:6px;';
|
|
1791
|
+
|
|
1792
|
+
var cancelBtn = document.createElement('button');
|
|
1793
|
+
cancelBtn.type = 'button';
|
|
1794
|
+
cancelBtn.className = 'brainstorm-send-btn';
|
|
1795
|
+
cancelBtn.textContent = 'Cancel';
|
|
1796
|
+
cancelBtn.style.cssText = 'padding:4px 12px;font-size:12px;background:transparent;border:1px solid var(--bodyDivider,#444);color:var(--bodySubtext,#888);';
|
|
1797
|
+
|
|
1798
|
+
var saveBtn = document.createElement('button');
|
|
1799
|
+
saveBtn.type = 'button';
|
|
1800
|
+
saveBtn.className = 'brainstorm-send-btn';
|
|
1801
|
+
saveBtn.textContent = 'Save';
|
|
1802
|
+
saveBtn.style.cssText = 'padding:4px 12px;font-size:12px;';
|
|
1803
|
+
|
|
1804
|
+
btnRow.appendChild(cancelBtn);
|
|
1805
|
+
btnRow.appendChild(saveBtn);
|
|
1806
|
+
firstMsg.appendChild(btnRow);
|
|
1807
|
+
|
|
1808
|
+
textarea.focus();
|
|
1809
|
+
|
|
1810
|
+
cancelBtn.addEventListener('click', function() {
|
|
1811
|
+
firstMsg.innerHTML = original;
|
|
1812
|
+
firstMsg.style.padding = '';
|
|
1813
|
+
editBtn.style.display = '';
|
|
1814
|
+
firstMsg.appendChild(editBtn);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
saveBtn.addEventListener('click', function() {
|
|
1818
|
+
var newText = textarea.value.trim();
|
|
1819
|
+
if (!newText) { cancelBtn.click(); return; }
|
|
1820
|
+
|
|
1821
|
+
saveBtn.disabled = true;
|
|
1822
|
+
saveBtn.textContent = 'Saving...';
|
|
1823
|
+
|
|
1824
|
+
fetch('/' + pageName + '/greeting', {
|
|
1825
|
+
method: 'POST',
|
|
1826
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1827
|
+
body: JSON.stringify({ greeting: newText })
|
|
1828
|
+
})
|
|
1829
|
+
.then(function(res) { return res.json(); })
|
|
1830
|
+
.then(function(result) {
|
|
1831
|
+
if (result.ok) {
|
|
1832
|
+
// Update the displayed message
|
|
1833
|
+
firstMsg.innerHTML = '';
|
|
1834
|
+
firstMsg.style.padding = '';
|
|
1835
|
+
var newP = document.createElement('p');
|
|
1836
|
+
newP.innerHTML = '<strong>' + escapeHtml(pn) + ':</strong> ' + escapeHtml(newText);
|
|
1837
|
+
firstMsg.appendChild(newP);
|
|
1838
|
+
firstMsg.appendChild(editBtn);
|
|
1839
|
+
editBtn.style.display = '';
|
|
1840
|
+
// Update shellInit so save modal has current greeting
|
|
1841
|
+
shellInit.greeting = newText;
|
|
1842
|
+
} else {
|
|
1843
|
+
cancelBtn.click();
|
|
1844
|
+
}
|
|
1845
|
+
})
|
|
1846
|
+
.catch(function() { cancelBtn.click(); });
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
textarea.addEventListener('keydown', function(e) {
|
|
1850
|
+
if (e.key === 'Escape') { e.preventDefault(); cancelBtn.click(); }
|
|
1851
|
+
if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); saveBtn.click(); }
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
firstMsg.appendChild(editBtn);
|
|
1856
|
+
})();
|
|
1857
|
+
|
|
1858
|
+
// --- Initial Focus -------------------------------------------------------
|
|
1859
|
+
if (chatInput && !document.body.classList.contains('chat-collapsed')) {
|
|
1860
|
+
chatInput.focus();
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// --- Scroll chat to bottom on load ---
|
|
1864
|
+
scrollChatToBottom();
|
|
1865
|
+
})();
|