synthos 0.10.0 → 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.
Files changed (312) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +155 -30
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +12 -3
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +72 -4
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/providers.spec.ts +1 -1
  305. package/tests/sharedTableSchema.spec.ts +242 -0
  306. package/tests/transformPage.spec.ts +62 -81
  307. package/default-pages/application/page.json +0 -10
  308. package/default-pages/retro_game_starter/page.json +0 -12
  309. package/default-pages/sidebar_page/page.html +0 -51
  310. package/default-pages/sidebar_page/page.json +0 -10
  311. package/default-pages/two-panel_page/page.html +0 -68
  312. package/default-pages/two-panel_page/page.json +0 -10
@@ -1,1290 +1,1292 @@
1
- (function() {
2
- // Fallback navigation — overridden in section 10 with unsaved-changes guard
3
- if (!window.__synthOSNavigateTo) {
4
- window.__synthOSNavigateTo = function(url) { window.location.href = url; };
5
- }
6
-
7
- if (window.__synthOSChatPanel) return;
8
- window.__synthOSChatPanel = true;
9
-
10
- // Product name — used for branding throughout the page script
11
- var pn = (window.pageInfo && window.pageInfo.productName) ? window.pageInfo.productName : 'SynthOS';
12
-
13
- // 0. First-run greeting replace default greeting when ?firstRun=true
14
- (function() {
15
- var params = new URLSearchParams(window.location.search);
16
- if (params.get('firstRun') !== 'true') return;
17
- var greeting = document.getElementById('defaultGreeting');
18
- if (!greeting) return;
19
- greeting.innerHTML =
20
- '<p><strong>Welcome to ' + pn + '!</strong></p>' +
21
- '<p>You\'re all set up and ready to start creating. This is the <strong>Builder</strong> — your main workspace. Just type what you want to build into the chat and ' + pn + ' will generate it for you as a live, interactive page.</p>' +
22
- '<p>You can create just about anything: dashboards, tools, games, visualizations, forms, calculators — if it can be expressed as a web page, you can build it here through conversation.</p>' +
23
- '<p><strong>How pages work:</strong></p>' +
24
- '<ul>' +
25
- '<li>Each creation lives on its own <strong>page</strong>. When you save, it gets a name and becomes part of your collection.</li>' +
26
- '<li>The <strong>Pages</strong> button (in the link bar above) takes you to the <strong>Pages Gallery</strong> where you can browse, open, and manage all your saved creations.</li>' +
27
- '</ul>' +
28
- '<p><strong>Key actions:</strong></p>' +
29
- '<ul>' +
30
- '<li><strong>Save</strong> — saves your current page. You\'ll be prompted for a name the first time. <em>Save often!</em> Your work only persists when you save it.</li>' +
31
- '<li><strong>Reset</strong> clears the current page back to a blank slate. Useful when you want to start fresh on something new.</li>' +
32
- '</ul>' +
33
- '<p>When you\'re ready, we recommend trying the <a href="/solar_tutorial">Solar Tutorial</a> — it\'s a guided walkthrough that will show you the basics of creating with ' + pn + ' step by step.</p>' +
34
- '<p>Have fun building!</p>';
35
- })();
36
-
37
- // 1. Themed tooltips for chat panel controls
38
- (function() {
39
- var style = document.createElement('style');
40
- style.textContent =
41
- '.synthos-tooltip {' +
42
- 'position: fixed;' +
43
- 'padding: 6px 10px;' +
44
- 'background: var(--bg-tertiary, #0f0f23);' +
45
- 'color: var(--text-secondary, #b794f6);' +
46
- 'border: 1px solid var(--border-color, rgba(138,43,226,0.3));' +
47
- 'border-radius: 6px;' +
48
- 'font-size: 12px;' +
49
- 'max-width: 150px;' +
50
- 'text-align: center;' +
51
- 'pointer-events: none;' +
52
- 'z-index: 10000;' +
53
- 'box-shadow: 0 2px 8px rgba(0,0,0,0.3);' +
54
- 'opacity: 0;' +
55
- 'transition: opacity 0.15s;' +
56
- '}' +
57
- '.synthos-tooltip.visible { opacity: 1; }';
58
- document.head.appendChild(style);
59
-
60
- var tip = document.createElement('div');
61
- tip.className = 'synthos-tooltip';
62
- document.body.appendChild(tip);
63
-
64
- function show(el) {
65
- tip.textContent = el.getAttribute('data-tooltip');
66
- tip.style.display = 'block';
67
- tip.classList.remove('visible');
68
- var r = el.getBoundingClientRect();
69
- var tw = tip.offsetWidth;
70
- var left = r.left + (r.width / 2) - (tw / 2);
71
- if (left < 4) left = 4;
72
- if (left + tw > window.innerWidth - 4) left = window.innerWidth - tw - 4;
73
- tip.style.left = left + 'px';
74
- tip.style.top = (r.top - tip.offsetHeight - 6) + 'px';
75
- void tip.offsetWidth;
76
- tip.classList.add('visible');
77
- }
78
-
79
- function hide() {
80
- tip.classList.remove('visible');
81
- tip.style.display = 'none';
82
- }
83
- hide();
84
-
85
- function attach(el, text) {
86
- el.setAttribute('data-tooltip', text);
87
- el.addEventListener('mouseenter', function() { show(el); });
88
- el.addEventListener('mouseleave', hide);
89
- }
90
-
91
- window.__synthOSTooltip = attach;
92
- })();
93
-
94
- var chatInput = document.getElementById('chatInput');
95
-
96
- // 2. Form submit handler — fetch+JSON with attachment support
97
- var chatForm = document.getElementById('chatForm');
98
- if (chatForm) {
99
- chatForm.addEventListener('submit', function(e) {
100
- e.preventDefault();
101
-
102
- var ci = document.getElementById('chatInput');
103
- var messageText = ci ? ci.value : '';
104
-
105
- // Append any captured console errors to the outgoing message
106
- var errors = window.__synthOSErrors;
107
- if (errors && errors.length > 0 && messageText.trim()) {
108
- messageText = messageText + '\n\nCONSOLE_ERRORS:\n' + errors.join('\n---\n');
109
- window.__synthOSErrors = [];
110
- }
111
-
112
- if (!messageText.trim()) return;
113
-
114
- // Show overlay and disable inputs
115
- var overlay = document.getElementById('loadingOverlay');
116
- if (overlay) overlay.style.display = 'flex';
117
- setTimeout(function() {
118
- if (ci) ci.disabled = true;
119
- var sb = document.querySelector('.chat-send-btn');
120
- if (sb) sb.disabled = true;
121
- }, 50);
122
-
123
- // Build JSON body with optional attachments
124
- var body = { message: messageText };
125
- var attachments = window.__synthOSAttachments;
126
- if (attachments && attachments.length > 0) {
127
- body.attachments = attachments.map(function(a) {
128
- return { mediaType: a.mediaType, data: a.data, name: a.name };
129
- });
130
- }
131
-
132
- fetch(window.location.pathname, {
133
- method: 'POST',
134
- headers: { 'Content-Type': 'application/json' },
135
- body: JSON.stringify(body)
136
- })
137
- .then(function(res) { return res.text(); })
138
- .then(function(html) {
139
- // Mark page as dirty (unsaved changes) for required-page prompt
140
- window.__synthOSPageDirty = true;
141
- // Reset guards so page-v3.js re-initializes on the new DOM
142
- window.__synthOSChatPanel = false;
143
- window.__synthOSNavigateTo = null;
144
- document.open();
145
- document.write(html);
146
- document.close();
147
- })
148
- .catch(function(err) {
149
- console.error('Submit failed:', err);
150
- if (overlay) overlay.style.display = 'none';
151
- if (ci) ci.disabled = false;
152
- var sb = document.querySelector('.chat-send-btn');
153
- if (sb) sb.disabled = false;
154
- });
155
-
156
- // Clear attachments after submit
157
- window.__synthOSAttachments = [];
158
- var pillsContainer = document.querySelector('.attachment-pills');
159
- if (pillsContainer) pillsContainer.innerHTML = '';
160
- });
161
- }
162
-
163
- // 2b. Enter submits, Shift+Enter adds newline
164
- (function() {
165
- var ci = document.getElementById('chatInput');
166
- if (!ci) return;
167
- ci.addEventListener('keydown', function(e) {
168
- if (e.key === 'Enter' && !e.shiftKey) {
169
- e.preventDefault();
170
- var form = document.getElementById('chatForm');
171
- if (form) form.requestSubmit();
172
- }
173
- });
174
- })();
175
-
176
- // 3. Save modal themed modal with title, categories, greeting
177
- (function() {
178
-
179
- // Detect if current page is a Builders or System page (start with blank fields)
180
- var isBuilder = window.pageInfo && Array.isArray(window.pageInfo.categories) &&
181
- (window.pageInfo.categories.indexOf('Builders') !== -1 ||
182
- window.pageInfo.categories.indexOf('System') !== -1);
183
-
184
- // Original title for change detection
185
- var originalTitle = (window.pageInfo && window.pageInfo.title) ? window.pageInfo.title : '';
186
-
187
- // --- Create save modal ---
188
- var modal = document.createElement('div');
189
- modal.id = 'synthos-saveModal';
190
- modal.className = 'modal-overlay';
191
- modal.innerHTML =
192
- '<div class="modal-content" style="max-width:480px;">' +
193
- '<div class="modal-header">' +
194
- '<span>Save Page</span>' +
195
- '<button type="button" class="brainstorm-close-btn" id="synthos-saveCloseBtn">&times;</button>' +
196
- '</div>' +
197
- '<div class="modal-body" style="display:flex;flex-direction:column;gap:12px;padding:16px 20px;">' +
198
- '<div>' +
199
- '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Display Title <span style="color:var(--accent-primary);">*</span></label>' +
200
- '<input type="text" id="synthos-saveTitleInput" class="brainstorm-input" placeholder="Enter a display title..." style="width:100%;box-sizing:border-box;">' +
201
- '<div id="synthos-saveTitleError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">Title is required</div>' +
202
- '</div>' +
203
- '<div>' +
204
- '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Categories <span style="color:var(--accent-primary);">*</span></label>' +
205
- '<input type="text" id="synthos-saveCategoriesInput" class="brainstorm-input" placeholder="e.g. Tool, Game, Utility" style="width:100%;box-sizing:border-box;">' +
206
- '<div id="synthos-saveCategoriesError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">At least one category is required</div>' +
207
- '</div>' +
208
- '<div>' +
209
- '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Greeting <span style="font-size:11px;opacity:0.7;">(optional)</span></label>' +
210
- '<input type="text" id="synthos-saveGreetingInput" class="brainstorm-input" placeholder="Available when title changes" style="width:100%;box-sizing:border-box;" disabled>' +
211
- '<div style="font-size:11px;color:var(--text-secondary);margin-top:4px;opacity:0.7;" id="synthos-saveGreetingHint">Change the title to enable a custom greeting.</div>' +
212
- '</div>' +
213
- '</div>' +
214
- '<div class="modal-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;">' +
215
- '<button type="button" class="brainstorm-send-btn" id="synthos-saveCancelBtn" style="background:transparent;border:1px solid var(--border-color);color:var(--text-secondary);">Cancel</button>' +
216
- '<button type="button" class="brainstorm-send-btn" id="synthos-saveConfirmBtn">Save</button>' +
217
- '</div>' +
218
- '</div>';
219
- document.body.appendChild(modal);
220
-
221
- // --- Create error modal ---
222
- var errorModal = document.createElement('div');
223
- errorModal.id = 'synthos-errorModal';
224
- errorModal.className = 'modal-overlay';
225
- errorModal.innerHTML =
226
- '<div class="modal-content" style="max-width:400px;">' +
227
- '<div class="modal-header">' +
228
- '<span>Error</span>' +
229
- '<button type="button" class="brainstorm-close-btn" id="synthos-errorCloseBtn">&times;</button>' +
230
- '</div>' +
231
- '<div class="modal-body" style="padding:16px 20px;">' +
232
- '<p id="synthos-errorMessage" style="margin:0;color:var(--text-primary);"></p>' +
233
- '</div>' +
234
- '<div class="modal-footer" style="display:flex;justify-content:flex-end;padding:12px 20px;">' +
235
- '<button type="button" class="brainstorm-send-btn" id="synthos-errorOkBtn">OK</button>' +
236
- '</div>' +
237
- '</div>';
238
- document.body.appendChild(errorModal);
239
-
240
- // --- Element references ---
241
- var titleInput = document.getElementById('synthos-saveTitleInput');
242
- var categoriesInput = document.getElementById('synthos-saveCategoriesInput');
243
- var greetingInput = document.getElementById('synthos-saveGreetingInput');
244
- var greetingHint = document.getElementById('synthos-saveGreetingHint');
245
- var titleError = document.getElementById('synthos-saveTitleError');
246
- var categoriesError = document.getElementById('synthos-saveCategoriesError');
247
-
248
- // --- Greeting enable/disable based on title change ---
249
- titleInput.addEventListener('input', function() {
250
- var changed = titleInput.value.trim() !== originalTitle;
251
- greetingInput.disabled = !changed;
252
- if (changed) {
253
- greetingInput.placeholder = 'Enter a custom greeting...';
254
- greetingHint.textContent = 'Replaces the initial Synthos greeting and removes chat history.';
255
- } else {
256
- greetingInput.placeholder = 'Available when title changes';
257
- greetingInput.value = '';
258
- greetingHint.textContent = 'Change the title to enable a custom greeting.';
259
- }
260
- });
261
-
262
- // --- Open modal ---
263
- function openSaveModal() {
264
- // Pre-fill fields (blank for Builder pages)
265
- titleInput.value = isBuilder ? '' : originalTitle;
266
- categoriesInput.value = isBuilder ? '' : (
267
- (window.pageInfo && Array.isArray(window.pageInfo.categories))
268
- ? window.pageInfo.categories.join(', ')
269
- : ''
270
- );
271
- greetingInput.value = '';
272
- greetingInput.disabled = true;
273
- greetingInput.placeholder = 'Available when title changes';
274
- greetingHint.textContent = 'Change the title to enable a custom greeting.';
275
- titleError.style.display = 'none';
276
- categoriesError.style.display = 'none';
277
- modal.classList.add('show');
278
- titleInput.focus();
279
- }
280
-
281
- function closeSaveModal() {
282
- modal.classList.remove('show');
283
- }
284
-
285
- function showError(msg) {
286
- document.getElementById('synthos-errorMessage').textContent = msg;
287
- errorModal.classList.add('show');
288
- }
289
-
290
- function closeError() {
291
- errorModal.classList.remove('show');
292
- }
293
-
294
- // --- Submit ---
295
- function submitSave() {
296
- var title = titleInput.value.trim();
297
- var cats = categoriesInput.value.trim();
298
- var greeting = greetingInput.value.trim();
299
- var valid = true;
300
-
301
- // Validate
302
- if (!title) {
303
- titleError.style.display = 'block';
304
- valid = false;
305
- } else {
306
- titleError.style.display = 'none';
307
- }
308
- if (!cats) {
309
- categoriesError.style.display = 'block';
310
- valid = false;
311
- } else {
312
- categoriesError.style.display = 'none';
313
- }
314
- if (!valid) return;
315
-
316
- // Parse categories
317
- var categories = cats.split(',').map(function(c) { return c.trim(); }).filter(Boolean);
318
-
319
- // Disable button during save
320
- var confirmBtn = document.getElementById('synthos-saveConfirmBtn');
321
- confirmBtn.disabled = true;
322
- confirmBtn.textContent = 'Saving...';
323
-
324
- var body = { title: title, categories: categories };
325
- if (greeting && !greetingInput.disabled) {
326
- body.greeting = greeting;
327
- }
328
-
329
- fetch(window.location.pathname + '/save', {
330
- method: 'POST',
331
- headers: { 'Content-Type': 'application/json' },
332
- body: JSON.stringify(body)
333
- })
334
- .then(function(res) {
335
- return res.json().then(function(data) {
336
- return { ok: res.ok, data: data };
337
- });
338
- })
339
- .then(function(result) {
340
- if (result.ok && result.data.redirect) {
341
- window.__synthOSPageDirty = false;
342
- window.location.href = result.data.redirect;
343
- } else {
344
- closeSaveModal();
345
- showError(result.data.error || 'An unknown error occurred');
346
- confirmBtn.disabled = false;
347
- confirmBtn.textContent = 'Save';
348
- }
349
- })
350
- .catch(function(err) {
351
- closeSaveModal();
352
- showError('Network error: ' + err.message);
353
- confirmBtn.disabled = false;
354
- confirmBtn.textContent = 'Save';
355
- });
356
- }
357
-
358
- // Expose openSaveModal globally for toolbar button
359
- window.__synthOSOpenSaveModal = openSaveModal;
360
-
361
- // --- Event listeners ---
362
- document.getElementById('synthos-saveCloseBtn').addEventListener('click', closeSaveModal);
363
- document.getElementById('synthos-saveCancelBtn').addEventListener('click', closeSaveModal);
364
- document.getElementById('synthos-saveConfirmBtn').addEventListener('click', submitSave);
365
- document.getElementById('synthos-errorCloseBtn').addEventListener('click', closeError);
366
- document.getElementById('synthos-errorOkBtn').addEventListener('click', closeError);
367
-
368
- var saveModalMouseDownTarget = null;
369
- modal.addEventListener('mousedown', function(e) { saveModalMouseDownTarget = e.target; });
370
- modal.addEventListener('click', function(e) {
371
- if (e.target === modal && saveModalMouseDownTarget === modal) closeSaveModal();
372
- saveModalMouseDownTarget = null;
373
- });
374
- var errorModalMouseDownTarget = null;
375
- errorModal.addEventListener('mousedown', function(e) { errorModalMouseDownTarget = e.target; });
376
- errorModal.addEventListener('click', function(e) {
377
- if (e.target === errorModal && errorModalMouseDownTarget === errorModal) closeError();
378
- errorModalMouseDownTarget = null;
379
- });
380
-
381
- document.addEventListener('keydown', function(e) {
382
- if (e.key === 'Escape') {
383
- if (modal.classList.contains('show')) closeSaveModal();
384
- if (errorModal.classList.contains('show')) closeError();
385
- }
386
- });
387
-
388
- // Enter key in title/categories inputs triggers save
389
- titleInput.addEventListener('keydown', function(e) {
390
- if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
391
- });
392
- categoriesInput.addEventListener('keydown', function(e) {
393
- if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
394
- });
395
- })();
396
-
397
- // 4. Chat scroll to bottom
398
- var chatMessages = document.getElementById('chatMessages');
399
- if (chatMessages) {
400
- chatMessages.scrollTo({
401
- top: chatMessages.scrollHeight,
402
- behavior: 'smooth'
403
- });
404
- }
405
-
406
- // 5. Shell toolbar button wiring
407
- (function() {
408
- var DB_NAME = 'synthos-ui';
409
- var STORE_NAME = 'panel-state';
410
- var pageName = window.location.pathname.replace(/^\//, '') || 'builder';
411
- var isBuilderPage = pageName === 'builder';
412
-
413
- // IndexedDB helpers
414
- function openDB(cb) {
415
- var req = indexedDB.open(DB_NAME, 1);
416
- req.onupgradeneeded = function() {
417
- var db = req.result;
418
- if (!db.objectStoreNames.contains(STORE_NAME)) {
419
- db.createObjectStore(STORE_NAME);
420
- }
421
- };
422
- req.onsuccess = function() { cb(req.result); };
423
- req.onerror = function() { cb(null); };
424
- }
425
-
426
- function saveCollapsed(collapsed) {
427
- openDB(function(db) {
428
- if (!db) return;
429
- var tx = db.transaction(STORE_NAME, 'readwrite');
430
- tx.objectStore(STORE_NAME).put(collapsed, pageName);
431
- });
432
- }
433
-
434
- function loadCollapsed(cb) {
435
- openDB(function(db) {
436
- if (!db) { cb(null); return; }
437
- var tx = db.transaction(STORE_NAME, 'readonly');
438
- var req = tx.objectStore(STORE_NAME).get(pageName);
439
- req.onsuccess = function() { cb(req.result); };
440
- req.onerror = function() { cb(null); };
441
- });
442
- }
443
-
444
- function setCollapsed(collapsed) {
445
- if (collapsed) {
446
- document.body.classList.add('chat-collapsed');
447
- } else {
448
- document.body.classList.remove('chat-collapsed');
449
- }
450
- saveCollapsed(collapsed);
451
- }
452
-
453
- // Restore state — builder page always starts open
454
- if (isBuilderPage) {
455
- document.body.classList.remove('chat-collapsed');
456
- } else {
457
- loadCollapsed(function(val) {
458
- if (val === true) {
459
- document.body.classList.add('chat-collapsed');
460
- }
461
- });
462
- }
463
-
464
- // Builder toggle — show/hide chat panel
465
- var builderToggle = document.getElementById('builderToggle');
466
- if (builderToggle) {
467
- builderToggle.addEventListener('click', function() {
468
- var collapsed = !document.body.classList.contains('chat-collapsed');
469
- setCollapsed(collapsed);
470
- });
471
- }
472
-
473
- // Builder close button — same as collapse
474
- var builderClose = document.getElementById('builderClose');
475
- if (builderClose) {
476
- builderClose.addEventListener('click', function() {
477
- setCollapsed(true);
478
- });
479
- }
480
-
481
- // Pages button — navigate to pages gallery
482
- var pagesBtn = document.getElementById('pagesBtn');
483
- if (pagesBtn) {
484
- pagesBtn.addEventListener('click', function() {
485
- window.__synthOSNavigateTo('/pages');
486
- });
487
- }
488
-
489
- // Save button — open save modal
490
- var saveBtn = document.getElementById('saveBtn');
491
- if (saveBtn) {
492
- saveBtn.addEventListener('click', function() {
493
- if (window.__synthOSOpenSaveModal) window.__synthOSOpenSaveModal();
494
- });
495
- }
496
-
497
- // Settings button navigate to settings page
498
- var settingsBtn = document.getElementById('settingsBtn');
499
- if (settingsBtn) {
500
- settingsBtn.addEventListener('click', function() {
501
- window.__synthOSNavigateTo('/settings');
502
- });
503
- }
504
- })();
505
-
506
- // 6. Focus management — prevent viewer content from stealing keystrokes
507
- (function() {
508
- var ci = document.getElementById('chatInput');
509
- var vp = document.getElementById('viewerPanel');
510
- if (!ci || !vp) return;
511
-
512
- ci.addEventListener('mousedown', function(e) {
513
- e.stopPropagation();
514
- });
515
-
516
- ['keydown', 'keyup', 'keypress'].forEach(function(type) {
517
- document.addEventListener(type, function(e) {
518
- if (document.activeElement === ci) {
519
- // Allow Enter (without Shift) to reach the submit handler
520
- if (e.key === 'Enter' && !e.shiftKey) return;
521
- e.stopImmediatePropagation();
522
- }
523
- }, true);
524
- });
525
-
526
- vp.setAttribute('tabindex', '-1');
527
- ci.addEventListener('blur', function() {
528
- vp.focus();
529
- });
530
- })();
531
-
532
- // 7. Brainstorm — dynamic brainstorming UI (available on every v2 page)
533
- (function() {
534
- var chatInput = document.getElementById('chatInput');
535
- if (!chatInput) return;
536
-
537
- // --- Create icon row (.chat-input-wrapper) appended to #chatForm ---
538
- var form = document.getElementById('chatForm');
539
- var wrapper = document.querySelector('.chat-input-wrapper');
540
- if (!wrapper) {
541
- wrapper = document.createElement('div');
542
- wrapper.className = 'chat-input-wrapper';
543
- if (form) form.appendChild(wrapper);
544
- }
545
-
546
- // --- Create brainstorm icon button ---
547
- var brainstormBtn = document.createElement('button');
548
- brainstormBtn.type = 'button';
549
- brainstormBtn.className = 'brainstorm-icon-btn';
550
- if (window.__synthOSTooltip) window.__synthOSTooltip(brainstormBtn, 'Brainstorm ideas');
551
- 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">' +
552
- '<circle cx="12" cy="12" r="3"></circle>' +
553
- '<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>' +
554
- '</svg>';
555
- wrapper.appendChild(brainstormBtn);
556
-
557
- // --- Create send button ---
558
- var sendBtn = document.createElement('button');
559
- sendBtn.type = 'submit';
560
- sendBtn.className = 'chat-send-btn';
561
- if (window.__synthOSTooltip) window.__synthOSTooltip(sendBtn, 'Send message');
562
- 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">' +
563
- '<line x1="12" y1="19" x2="12" y2="5"></line>' +
564
- '<polyline points="5 12 12 5 19 12"></polyline>' +
565
- '</svg>';
566
- wrapper.appendChild(sendBtn);
567
-
568
- // --- Auto-grow textarea ---
569
- chatInput.addEventListener('input', function() {
570
- this.style.height = 'auto';
571
- this.style.height = this.scrollHeight + 'px';
572
- });
573
-
574
- // --- Create brainstorm modal ---
575
- var modal = document.createElement('div');
576
- modal.id = 'synthos-brainstormModal';
577
- modal.className = 'modal-overlay brainstorm-modal';
578
- modal.innerHTML =
579
- '<div class="modal-content">' +
580
- '<div class="modal-header">' +
581
- '<span>Brainstorm</span>' +
582
- '<button type="button" class="brainstorm-close-btn" id="synthos-brainstormCloseBtn">&times;</button>' +
583
- '</div>' +
584
- '<div class="brainstorm-messages" id="synthos-brainstormMessages"></div>' +
585
- '<div class="brainstorm-input-row">' +
586
- '<input type="text" class="brainstorm-input" id="synthos-brainstormInput" placeholder="What\'s on your mind...">' +
587
- '<button type="button" class="brainstorm-send-btn" id="synthos-brainstormSendBtn">Send</button>' +
588
- '</div>' +
589
- '</div>';
590
- document.body.appendChild(modal);
591
-
592
- // --- State ---
593
- var brainstormHistory = [];
594
-
595
- // --- Helpers ---
596
- function openBrainstorm() {
597
- modal.classList.add('show');
598
- // Grab text from chat input as initial topic
599
- var topic = chatInput.value.trim();
600
- if (topic) {
601
- chatInput.value = '';
602
- sendBrainstormText(topic, true);
603
- } else {
604
- // No topic send context-only opener so LLM starts the brainstorm
605
- sendBrainstormText('', true);
606
- }
607
- }
608
-
609
- function closeBrainstorm() {
610
- modal.classList.remove('show');
611
- brainstormHistory = [];
612
- document.getElementById('synthos-brainstormMessages').innerHTML = '';
613
- }
614
-
615
- function scrollBrainstormToBottom() {
616
- var el = document.getElementById('synthos-brainstormMessages');
617
- el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
618
- }
619
-
620
- function escapeHtml(str) {
621
- var div = document.createElement('div');
622
- div.appendChild(document.createTextNode(str));
623
- return div.innerHTML;
624
- }
625
-
626
- function appendBrainstormMessage(role, text, prompt, suggestions, isOpener) {
627
- var div = document.createElement('div');
628
- div.className = 'brainstorm-message ' + (role === 'user' ? 'brainstorm-user' : 'brainstorm-assistant');
629
- if (role === 'assistant') {
630
- var html;
631
- if (typeof marked !== 'undefined') {
632
- html = marked.parse(text);
633
- } else {
634
- html = escapeHtml(text);
635
- }
636
- div.innerHTML = '<strong>' + pn + ':</strong> ' + html;
637
- // Clickable suggestion chips
638
- if (suggestions && suggestions.length > 0) {
639
- var chips = document.createElement('div');
640
- chips.className = 'brainstorm-suggestions';
641
- suggestions.forEach(function(s) {
642
- var chip = document.createElement('button');
643
- chip.type = 'button';
644
- chip.className = 'brainstorm-suggestion-chip';
645
- chip.textContent = s;
646
- chip.addEventListener('click', function() {
647
- submitSuggestion(s);
648
- });
649
- chips.appendChild(chip);
650
- });
651
- div.appendChild(chips);
652
- }
653
- // "Build It" button — skip on the opener response
654
- if (prompt && !isOpener) {
655
- var btnRow = document.createElement('div');
656
- btnRow.className = 'brainstorm-build-row';
657
- var buildBtn = document.createElement('button');
658
- buildBtn.type = 'button';
659
- buildBtn.className = 'brainstorm-build-btn';
660
- buildBtn.textContent = 'Build It';
661
- buildBtn.setAttribute('data-prompt', prompt);
662
- buildBtn.addEventListener('click', function() {
663
- chatInput.value = this.getAttribute('data-prompt');
664
- closeBrainstorm();
665
- chatInput.focus();
666
- });
667
- btnRow.appendChild(buildBtn);
668
- div.appendChild(btnRow);
669
- }
670
- } else {
671
- div.textContent = text;
672
- }
673
- document.getElementById('synthos-brainstormMessages').appendChild(div);
674
- scrollBrainstormToBottom();
675
- }
676
-
677
- function submitSuggestion(text) {
678
- // Disable old suggestion chips so they can't be double-clicked
679
- var oldChips = document.querySelectorAll('#synthos-brainstormMessages .brainstorm-suggestion-chip');
680
- for (var i = 0; i < oldChips.length; i++) {
681
- oldChips[i].disabled = true;
682
- }
683
- sendBrainstormText(text, false);
684
- }
685
-
686
- function getBrainstormContext() {
687
- var chatEl = document.getElementById('chatMessages');
688
- if (!chatEl) return '<CHAT_HISTORY>\n';
689
- var msgs = chatEl.querySelectorAll('.chat-message');
690
- var lines = [];
691
- var started = false;
692
- for (var i = 0; i < msgs.length; i++) {
693
- var text = msgs[i].innerText;
694
- if (!started && /^User:/i.test(text.trim())) started = true;
695
- if (started) lines.push(text);
696
- }
697
- return '<CHAT_HISTORY>\n' + lines.join('\n');
698
- }
699
-
700
- // Send from the input field
701
- function sendBrainstormMessage() {
702
- var input = document.getElementById('synthos-brainstormInput');
703
- var text = input.value.trim();
704
- if (!text) return;
705
- input.value = '';
706
- sendBrainstormText(text, false);
707
- }
708
-
709
- // Core fetch — isOpener=true means this is the initial call when brainstorm opens
710
- function sendBrainstormText(text, isOpener) {
711
- var input = document.getElementById('synthos-brainstormInput');
712
- var userMsg = text || (isOpener ? 'Look at the conversation so far and suggest what we could build or improve.' : '');
713
- if (!userMsg) return;
714
-
715
- // Show user message in chat (skip for auto-generated opener)
716
- if (text) {
717
- appendBrainstormMessage('user', text);
718
- }
719
- brainstormHistory.push({ role: 'user', content: userMsg });
720
-
721
- var thinking = document.createElement('div');
722
- thinking.className = 'brainstorm-thinking';
723
- thinking.id = 'synthos-brainstormThinking';
724
- thinking.textContent = 'Thinking...';
725
- document.getElementById('synthos-brainstormMessages').appendChild(thinking);
726
- scrollBrainstormToBottom();
727
-
728
- input.disabled = true;
729
- document.getElementById('synthos-brainstormSendBtn').disabled = true;
730
-
731
- fetch('/api/brainstorm', {
732
- method: 'POST',
733
- headers: { 'Content-Type': 'application/json' },
734
- body: JSON.stringify({
735
- context: getBrainstormContext(),
736
- messages: brainstormHistory
737
- })
738
- })
739
- .then(function(res) {
740
- if (!res.ok) throw new Error('Brainstorm request failed');
741
- return res.json();
742
- })
743
- .then(function(data) {
744
- var thinkingEl = document.getElementById('synthos-brainstormThinking');
745
- if (thinkingEl) thinkingEl.remove();
746
-
747
- var response = data.response || 'Sorry, I didn\'t get a response.';
748
- var prompt = data.prompt || '';
749
- var suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
750
- appendBrainstormMessage('assistant', response, prompt, suggestions, isOpener);
751
- brainstormHistory.push({
752
- role: 'assistant',
753
- content: response + '\n\n[Suggested prompt: ' + prompt + ']'
754
- });
755
- })
756
- .catch(function(err) {
757
- var thinkingEl = document.getElementById('synthos-brainstormThinking');
758
- if (thinkingEl) thinkingEl.remove();
759
- appendBrainstormMessage('assistant', 'Something went wrong: ' + err.message);
760
- })
761
- .finally(function() {
762
- input.disabled = false;
763
- document.getElementById('synthos-brainstormSendBtn').disabled = false;
764
- input.focus();
765
- });
766
- }
767
-
768
- // --- Event listeners ---
769
- brainstormBtn.addEventListener('click', openBrainstorm);
770
- document.getElementById('synthos-brainstormCloseBtn').addEventListener('click', closeBrainstorm);
771
-
772
- var brainstormMouseDownTarget = null;
773
- modal.addEventListener('mousedown', function(e) { brainstormMouseDownTarget = e.target; });
774
- modal.addEventListener('click', function(e) {
775
- if (e.target === modal && brainstormMouseDownTarget === modal) closeBrainstorm();
776
- brainstormMouseDownTarget = null;
777
- });
778
-
779
- document.addEventListener('keydown', function(e) {
780
- if (e.key === 'Escape' && modal.classList.contains('show')) closeBrainstorm();
781
- });
782
-
783
- document.getElementById('synthos-brainstormSendBtn').addEventListener('click', sendBrainstormMessage);
784
- document.getElementById('synthos-brainstormInput').addEventListener('keydown', function(e) {
785
- if (e.key === 'Enter' && !e.shiftKey) {
786
- e.preventDefault();
787
- sendBrainstormMessage();
788
- }
789
- });
790
- })();
791
-
792
- // 8. Attach button + menu, attachment pills, file picker, screenshot tool
793
- (function() {
794
- var chatInput = document.getElementById('chatInput');
795
- if (!chatInput) return;
796
- var chatForm = document.getElementById('chatForm');
797
- var wrapper = chatForm ? chatForm.querySelector('.chat-input-wrapper') : null;
798
- if (!wrapper) return;
799
-
800
- // --- Attachments state ---
801
- if (!window.__synthOSAttachments) window.__synthOSAttachments = [];
802
-
803
- // --- Create pills container (inside #chatForm, before textarea) ---
804
- var pillsContainer = document.createElement('div');
805
- pillsContainer.className = 'attachment-pills';
806
- if (chatForm) {
807
- chatForm.insertBefore(pillsContainer, chatForm.firstChild);
808
- }
809
-
810
- function renderPills() {
811
- pillsContainer.innerHTML = '';
812
- var attachments = window.__synthOSAttachments;
813
- if (!attachments || attachments.length === 0) return;
814
- for (var i = 0; i < attachments.length; i++) {
815
- (function(idx) {
816
- var att = attachments[idx];
817
- var pill = document.createElement('div');
818
- pill.className = 'attachment-pill';
819
-
820
- var thumb = document.createElement('img');
821
- thumb.src = 'data:' + att.mediaType + ';base64,' + att.data;
822
- thumb.style.cssText = 'width:24px;height:24px;object-fit:cover;border-radius:3px;';
823
- pill.appendChild(thumb);
824
-
825
- var nameSpan = document.createElement('span');
826
- nameSpan.textContent = att.name || 'image';
827
- nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;';
828
- pill.appendChild(nameSpan);
829
-
830
- var removeBtn = document.createElement('button');
831
- removeBtn.type = 'button';
832
- removeBtn.className = 'attachment-pill-remove';
833
- removeBtn.textContent = '\u00d7';
834
- removeBtn.addEventListener('click', function() {
835
- window.__synthOSAttachments.splice(idx, 1);
836
- renderPills();
837
- });
838
- pill.appendChild(removeBtn);
839
-
840
- pillsContainer.appendChild(pill);
841
- })(i);
842
- }
843
- }
844
-
845
- // Expose for debugging
846
- window.__synthOSRenderPills = renderPills;
847
-
848
- // --- Helper: add an image attachment from a data URL ---
849
- function addImageFromDataUrl(dataUrl, name) {
850
- var commaIdx = dataUrl.indexOf(',');
851
- if (commaIdx === -1) return;
852
- var meta = dataUrl.substring(0, commaIdx); // data:image/png;base64
853
- var base64 = dataUrl.substring(commaIdx + 1);
854
- var mediaType = meta.replace('data:', '').replace(';base64', '');
855
- window.__synthOSAttachments.push({
856
- mediaType: mediaType,
857
- data: base64,
858
- name: name || 'image'
859
- });
860
- renderPills();
861
- }
862
-
863
- // --- Hidden file input ---
864
- var fileInput = document.createElement('input');
865
- fileInput.type = 'file';
866
- fileInput.accept = 'image/*';
867
- fileInput.style.display = 'none';
868
- document.body.appendChild(fileInput);
869
-
870
- fileInput.addEventListener('change', function() {
871
- var file = fileInput.files && fileInput.files[0];
872
- if (!file) return;
873
- var reader = new FileReader();
874
- reader.onload = function() {
875
- addImageFromDataUrl(reader.result, file.name);
876
- };
877
- reader.readAsDataURL(file);
878
- fileInput.value = '';
879
- });
880
-
881
- // --- Paste image from clipboard (document-level to catch all pastes) ---
882
- document.addEventListener('paste', function(e) {
883
- // Only handle pastes when chat input is focused or no other editable is focused
884
- var active = document.activeElement;
885
- var isEditable = active && (active.isContentEditable ||
886
- (active.tagName === 'TEXTAREA' && active !== chatInput) ||
887
- (active.tagName === 'INPUT' && active !== chatInput));
888
- if (isEditable) return;
889
-
890
- var items = e.clipboardData && e.clipboardData.items;
891
- if (!items) return;
892
- for (var i = 0; i < items.length; i++) {
893
- if (items[i].type.indexOf('image/') === 0) {
894
- var blob = items[i].getAsFile();
895
- if (!blob) continue;
896
- e.preventDefault();
897
- var reader = new FileReader();
898
- reader.onload = function() {
899
- addImageFromDataUrl(reader.result, 'pasted-image.png');
900
- };
901
- reader.readAsDataURL(blob);
902
- return; // only handle the first image
903
- }
904
- }
905
- });
906
-
907
- // --- + button ---
908
- var attachBtn = document.createElement('button');
909
- attachBtn.type = 'button';
910
- attachBtn.className = 'attach-btn';
911
- if (window.__synthOSTooltip) window.__synthOSTooltip(attachBtn, 'Attach file or screenshot');
912
- 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>';
913
- wrapper.insertBefore(attachBtn, wrapper.firstChild);
914
-
915
- // --- Popup menu ---
916
- var menu = document.createElement('div');
917
- menu.className = 'attach-menu';
918
-
919
- var menuAttachFile = document.createElement('div');
920
- menuAttachFile.className = 'attach-menu-item';
921
- 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';
922
- menu.appendChild(menuAttachFile);
923
-
924
- var menuScreenshot = document.createElement('div');
925
- menuScreenshot.className = 'attach-menu-item';
926
- 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';
927
- menu.appendChild(menuScreenshot);
928
-
929
- wrapper.appendChild(menu);
930
-
931
- var menuOpen = false;
932
- function toggleMenu() {
933
- menuOpen = !menuOpen;
934
- menu.style.display = menuOpen ? 'flex' : 'none';
935
- }
936
- function closeMenu() {
937
- menuOpen = false;
938
- menu.style.display = 'none';
939
- }
940
-
941
- attachBtn.addEventListener('click', function(e) {
942
- e.stopPropagation();
943
- toggleMenu();
944
- });
945
-
946
- document.addEventListener('click', function() {
947
- if (menuOpen) closeMenu();
948
- });
949
- menu.addEventListener('click', function(e) { e.stopPropagation(); });
950
-
951
- menuAttachFile.addEventListener('click', function() {
952
- closeMenu();
953
- fileInput.click();
954
- });
955
-
956
- // --- Screenshot annotation flow (multi-rectangle) ---
957
- menuScreenshot.addEventListener('click', function() {
958
- closeMenu();
959
- startScreenshotAnnotation();
960
- });
961
-
962
- function startScreenshotAnnotation() {
963
- var viewerPanel = document.getElementById('viewerPanel');
964
- if (!viewerPanel) return;
965
-
966
- // Create overlay
967
- var overlay = document.createElement('div');
968
- overlay.className = 'screenshot-overlay';
969
-
970
- // Instructions bar
971
- var instrBar = document.createElement('div');
972
- 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;';
973
- instrBar.textContent = 'Draw rectangles to highlight areas, then click Capture';
974
- overlay.appendChild(instrBar);
975
-
976
- // Persistent action buttons (always visible)
977
- var actions = document.createElement('div');
978
- actions.className = 'screenshot-actions';
979
- actions.style.cssText = 'position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:10;';
980
-
981
- var captureBtn = document.createElement('button');
982
- captureBtn.type = 'button';
983
- captureBtn.textContent = 'Capture';
984
- captureBtn.className = 'brainstorm-send-btn';
985
- captureBtn.style.cssText = 'padding:6px 16px;font-size:13px;';
986
-
987
- var cancelBtn = document.createElement('button');
988
- cancelBtn.type = 'button';
989
- cancelBtn.textContent = 'Cancel';
990
- cancelBtn.className = 'brainstorm-send-btn';
991
- cancelBtn.style.cssText = 'padding:6px 16px;font-size:13px;background:transparent;border:1px solid rgba(255,255,255,0.3);color:white;';
992
-
993
- actions.appendChild(captureBtn);
994
- actions.appendChild(cancelBtn);
995
- overlay.appendChild(actions);
996
-
997
- // Drawing state
998
- var currentRect = null;
999
- var startX, startY, isDrawing = false;
1000
- var allRects = []; // Array of {el, x, y, w, h}
1001
-
1002
- overlay.addEventListener('mousedown', function(e) {
1003
- if (e.target.tagName === 'BUTTON') return;
1004
- isDrawing = true;
1005
- var overlayBounds = overlay.getBoundingClientRect();
1006
- startX = e.clientX - overlayBounds.left;
1007
- startY = e.clientY - overlayBounds.top;
1008
-
1009
- // Create a new rectangle element
1010
- currentRect = document.createElement('div');
1011
- currentRect.className = 'screenshot-rect';
1012
- currentRect.style.display = 'block';
1013
- currentRect.style.left = startX + 'px';
1014
- currentRect.style.top = startY + 'px';
1015
- currentRect.style.width = '0';
1016
- currentRect.style.height = '0';
1017
- overlay.appendChild(currentRect);
1018
- });
1019
-
1020
- overlay.addEventListener('mousemove', function(e) {
1021
- if (!isDrawing || !currentRect) return;
1022
- var overlayBounds = overlay.getBoundingClientRect();
1023
- var curX = e.clientX - overlayBounds.left;
1024
- var curY = e.clientY - overlayBounds.top;
1025
- var x = Math.min(startX, curX);
1026
- var y = Math.min(startY, curY);
1027
- var w = Math.abs(curX - startX);
1028
- var h = Math.abs(curY - startY);
1029
- currentRect.style.left = x + 'px';
1030
- currentRect.style.top = y + 'px';
1031
- currentRect.style.width = w + 'px';
1032
- currentRect.style.height = h + 'px';
1033
- });
1034
-
1035
- overlay.addEventListener('mouseup', function() {
1036
- if (!isDrawing || !currentRect) return;
1037
- isDrawing = false;
1038
- var w = parseInt(currentRect.style.width);
1039
- var h = parseInt(currentRect.style.height);
1040
- if (w < 10 || h < 10) {
1041
- // Too small discard
1042
- currentRect.remove();
1043
- currentRect = null;
1044
- return;
1045
- }
1046
- allRects.push({
1047
- el: currentRect,
1048
- x: parseInt(currentRect.style.left),
1049
- y: parseInt(currentRect.style.top),
1050
- w: w,
1051
- h: h
1052
- });
1053
- currentRect = null;
1054
- });
1055
-
1056
- captureBtn.addEventListener('click', function() {
1057
- doCapture();
1058
- });
1059
- cancelBtn.addEventListener('click', function() {
1060
- cleanup();
1061
- });
1062
-
1063
- function doCapture() {
1064
- if (typeof html2canvas === 'undefined') {
1065
- console.error('html2canvas not loaded');
1066
- cleanup();
1067
- return;
1068
- }
1069
-
1070
- // Capture the full viewer panel without the overlay
1071
- overlay.style.visibility = 'hidden';
1072
-
1073
- html2canvas(viewerPanel, { useCORS: true, logging: false }).then(function(fullCanvas) {
1074
- // Draw red rectangles onto the captured canvas
1075
- var vpRect = viewerPanel.getBoundingClientRect();
1076
- var scaleX = fullCanvas.width / vpRect.width;
1077
- var scaleY = fullCanvas.height / vpRect.height;
1078
-
1079
- var ctx = fullCanvas.getContext('2d');
1080
- ctx.strokeStyle = 'red';
1081
- ctx.lineWidth = 3 * Math.max(scaleX, scaleY);
1082
-
1083
- for (var i = 0; i < allRects.length; i++) {
1084
- var r = allRects[i];
1085
- ctx.strokeRect(
1086
- Math.round(r.x * scaleX),
1087
- Math.round(r.y * scaleY),
1088
- Math.round(r.w * scaleX),
1089
- Math.round(r.h * scaleY)
1090
- );
1091
- }
1092
-
1093
- var dataUrl = fullCanvas.toDataURL('image/png');
1094
- var base64 = dataUrl.split(',')[1];
1095
- window.__synthOSAttachments.push({
1096
- mediaType: 'image/png',
1097
- data: base64,
1098
- name: 'screenshot.png'
1099
- });
1100
- renderPills();
1101
- cleanup();
1102
- }).catch(function(err) {
1103
- console.error('Screenshot capture failed:', err);
1104
- cleanup();
1105
- });
1106
- }
1107
-
1108
- function cleanup() {
1109
- if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
1110
- document.removeEventListener('keydown', onKeyDown);
1111
- }
1112
-
1113
- // Escape to cancel
1114
- function onKeyDown(e) {
1115
- if (e.key === 'Escape') {
1116
- cleanup();
1117
- }
1118
- }
1119
- document.addEventListener('keydown', onKeyDown);
1120
-
1121
- viewerPanel.style.position = 'relative';
1122
- viewerPanel.appendChild(overlay);
1123
- }
1124
- })();
1125
-
1126
- // 9. Undo link — shown after the last chat message when a version exists
1127
- (function() {
1128
- var meta = document.querySelector('meta[name="synthos-version"]');
1129
- if (!meta) return;
1130
- var version = parseInt(meta.getAttribute('content'), 10);
1131
- if (!version || version <= 0) return;
1132
-
1133
- var cm = document.getElementById('chatMessages');
1134
- if (!cm) return;
1135
-
1136
- // Remove any existing undo links
1137
- var existing = cm.querySelectorAll('.synthos-undo-link');
1138
- for (var i = 0; i < existing.length; i++) existing[i].remove();
1139
-
1140
- // Create undo link div
1141
- var undoDiv = document.createElement('div');
1142
- undoDiv.className = 'synthos-undo-link';
1143
- undoDiv.style.cssText = 'text-align:right;padding:4px 12px 2px;';
1144
-
1145
- var link = document.createElement('a');
1146
- link.href = '#';
1147
- link.textContent = 'undo';
1148
- link.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
1149
- link.addEventListener('mouseenter', function() { link.style.opacity = '1'; link.style.textDecoration = 'underline'; });
1150
- link.addEventListener('mouseleave', function() { link.style.opacity = '0.7'; link.style.textDecoration = 'none'; });
1151
-
1152
- link.addEventListener('click', function(e) {
1153
- e.preventDefault();
1154
- var overlay = document.getElementById('loadingOverlay');
1155
- if (overlay) overlay.style.display = 'flex';
1156
-
1157
- fetch(window.location.pathname + '/undo', {
1158
- method: 'POST',
1159
- headers: { 'Content-Type': 'application/json' },
1160
- body: '{}'
1161
- })
1162
- .then(function(res) { return res.text(); })
1163
- .then(function(html) {
1164
- window.__synthOSChatPanel = false;
1165
- window.__synthOSPageDirty = false;
1166
- window.__synthOSNavigateTo = null;
1167
- document.open();
1168
- document.write(html);
1169
- document.close();
1170
- })
1171
- .catch(function(err) {
1172
- console.error('Undo failed:', err);
1173
- if (overlay) overlay.style.display = 'none';
1174
- });
1175
- });
1176
-
1177
- undoDiv.appendChild(link);
1178
-
1179
- // "try again" link — re-submits the last user message against the previous page state
1180
- var msgs = cm.querySelectorAll('.chat-message');
1181
- var lastUserMsg = '';
1182
- for (var j = msgs.length - 1; j >= 0; j--) {
1183
- var strong = msgs[j].querySelector('strong');
1184
- if (strong && strong.textContent.trim().replace(/:$/, '') === 'User') {
1185
- // Get the text content after "User:" — everything in the <p> minus the strong
1186
- var p = msgs[j].querySelector('p');
1187
- if (p) {
1188
- var clone = p.cloneNode(true);
1189
- var s = clone.querySelector('strong');
1190
- if (s) s.remove();
1191
- lastUserMsg = clone.textContent.trim();
1192
- }
1193
- break;
1194
- }
1195
- }
1196
-
1197
- if (lastUserMsg) {
1198
- var sep = document.createTextNode(' · ');
1199
- undoDiv.appendChild(sep);
1200
-
1201
- var tryLink = document.createElement('a');
1202
- tryLink.href = '#';
1203
- tryLink.textContent = 'try again';
1204
- tryLink.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
1205
- tryLink.addEventListener('mouseenter', function() { tryLink.style.opacity = '1'; tryLink.style.textDecoration = 'underline'; });
1206
- tryLink.addEventListener('mouseleave', function() { tryLink.style.opacity = '0.7'; tryLink.style.textDecoration = 'none'; });
1207
-
1208
- tryLink.addEventListener('click', function(e) {
1209
- e.preventDefault();
1210
- var overlay = document.getElementById('loadingOverlay');
1211
- if (overlay) overlay.style.display = 'flex';
1212
- var ci = document.getElementById('chatInput');
1213
- if (ci) ci.disabled = true;
1214
- var sb = document.querySelector('.chat-send-btn');
1215
- if (sb) sb.disabled = true;
1216
-
1217
- fetch(window.location.pathname, {
1218
- method: 'POST',
1219
- headers: { 'Content-Type': 'application/json' },
1220
- body: JSON.stringify({ message: lastUserMsg, tryAgain: true })
1221
- })
1222
- .then(function(res) { return res.text(); })
1223
- .then(function(html) {
1224
- window.__synthOSPageDirty = true;
1225
- window.__synthOSChatPanel = false;
1226
- window.__synthOSNavigateTo = null;
1227
- document.open();
1228
- document.write(html);
1229
- document.close();
1230
- })
1231
- .catch(function(err) {
1232
- console.error('Try again failed:', err);
1233
- if (overlay) overlay.style.display = 'none';
1234
- if (ci) ci.disabled = false;
1235
- if (sb) sb.disabled = false;
1236
- });
1237
- });
1238
-
1239
- undoDiv.appendChild(tryLink);
1240
- }
1241
-
1242
- cm.appendChild(undoDiv);
1243
- })();
1244
-
1245
- // 10. Unsaved-changes prompt for required pages (FluentLM dialog)
1246
- (function() {
1247
- // Create the unsaved-changes confirmation dialog
1248
- var overlay = document.createElement('div');
1249
- overlay.className = 'flm-dialog-overlay';
1250
- overlay.id = 'synthos-unsavedDialog';
1251
- overlay.innerHTML =
1252
- '<div class="flm-dialog">' +
1253
- '<div class="flm-dialog-header">' +
1254
- '<h2 class="flm-dialog-title">Unsaved Changes</h2>' +
1255
- '<button class="flm-dialog-close" data-icon="Cancel" aria-label="Close" data-dialog-close></button>' +
1256
- '</div>' +
1257
- '<div class="flm-dialog-body">You have unsaved changes that will be lost if you leave this page.</div>' +
1258
- '<div class="flm-dialog-footer">' +
1259
- '<button class="flm-button" data-dialog-close id="synthos-unsavedStay">Stay</button>' +
1260
- '<button class="flm-button flm-button--primary" id="synthos-unsavedLeave">Leave</button>' +
1261
- '</div>' +
1262
- '</div>';
1263
- document.body.appendChild(overlay);
1264
-
1265
- var pendingUrl = null;
1266
-
1267
- document.getElementById('synthos-unsavedLeave').addEventListener('click', function() {
1268
- overlay.classList.remove('is-open');
1269
- if (pendingUrl) {
1270
- window.__synthOSPageDirty = false;
1271
- window.location.href = pendingUrl;
1272
- }
1273
- });
1274
-
1275
- // Navigate with unsaved-changes guard
1276
- window.__synthOSNavigateTo = function(url) {
1277
- if (window.__synthOSPageDirty && window.pageInfo && window.pageInfo.isRequiredPage) {
1278
- pendingUrl = url;
1279
- overlay.classList.add('is-open');
1280
- } else {
1281
- window.location.href = url;
1282
- }
1283
- };
1284
- })();
1285
-
1286
- // Initial focus — run after all setup (including chat-collapsed check)
1287
- if (chatInput && !document.body.classList.contains('chat-collapsed')) {
1288
- chatInput.focus();
1289
- }
1290
- })();
1
+ (function() {
2
+ // Fallback navigation — overridden in section 10 with unsaved-changes guard
3
+ if (!window.__synthOSNavigateTo) {
4
+ window.__synthOSNavigateTo = function(url) { window.location.href = url; };
5
+ }
6
+
7
+ if (window.__synthOSChatPanel) return;
8
+ window.__synthOSChatPanel = true;
9
+
10
+ // Product name — used for branding throughout the page script
11
+ var pn = (window.pageInfo && window.pageInfo.productName) ? window.pageInfo.productName : 'SynthOS';
12
+
13
+ // 1. Themed tooltips for chat panel controls
14
+ (function() {
15
+ var style = document.createElement('style');
16
+ style.textContent =
17
+ '.synthos-tooltip {' +
18
+ 'position: fixed;' +
19
+ 'padding: 6px 10px;' +
20
+ 'background: var(--bg-tertiary, #0f0f23);' +
21
+ 'color: var(--text-secondary, #b794f6);' +
22
+ 'border: 1px solid var(--border-color, rgba(138,43,226,0.3));' +
23
+ 'border-radius: 6px;' +
24
+ 'font-size: 12px;' +
25
+ 'max-width: 150px;' +
26
+ 'text-align: center;' +
27
+ 'pointer-events: none;' +
28
+ 'z-index: 10000;' +
29
+ 'box-shadow: 0 2px 8px rgba(0,0,0,0.3);' +
30
+ 'opacity: 0;' +
31
+ 'transition: opacity 0.15s;' +
32
+ '}' +
33
+ '.synthos-tooltip.visible { opacity: 1; }';
34
+ document.head.appendChild(style);
35
+
36
+ var tip = document.createElement('div');
37
+ tip.className = 'synthos-tooltip';
38
+ document.body.appendChild(tip);
39
+
40
+ function show(el) {
41
+ tip.textContent = el.getAttribute('data-tooltip');
42
+ tip.style.display = 'block';
43
+ tip.classList.remove('visible');
44
+ var r = el.getBoundingClientRect();
45
+ var tw = tip.offsetWidth;
46
+ var left = r.left + (r.width / 2) - (tw / 2);
47
+ if (left < 4) left = 4;
48
+ if (left + tw > window.innerWidth - 4) left = window.innerWidth - tw - 4;
49
+ tip.style.left = left + 'px';
50
+ tip.style.top = (r.top - tip.offsetHeight - 6) + 'px';
51
+ void tip.offsetWidth;
52
+ tip.classList.add('visible');
53
+ }
54
+
55
+ function hide() {
56
+ tip.classList.remove('visible');
57
+ tip.style.display = 'none';
58
+ }
59
+ hide();
60
+
61
+ function attach(el, text) {
62
+ el.setAttribute('data-tooltip', text);
63
+ el.addEventListener('mouseenter', function() { show(el); });
64
+ el.addEventListener('mouseleave', hide);
65
+ }
66
+
67
+ window.__synthOSTooltip = attach;
68
+ })();
69
+
70
+ var chatInput = document.getElementById('chatInput');
71
+
72
+ // 2. Form submit handler fetch+JSON with attachment support
73
+ var chatForm = document.getElementById('chatForm');
74
+ if (chatForm) {
75
+ chatForm.addEventListener('submit', function(e) {
76
+ e.preventDefault();
77
+
78
+ var ci = document.getElementById('chatInput');
79
+ var messageText = ci ? ci.value : '';
80
+
81
+ // Append any captured console errors to the outgoing message
82
+ var errors = window.__synthOSErrors;
83
+ if (errors && errors.length > 0 && messageText.trim()) {
84
+ messageText = messageText + '\n\nCONSOLE_ERRORS:\n' + errors.join('\n---\n');
85
+ window.__synthOSErrors = [];
86
+ }
87
+
88
+ if (!messageText.trim()) return;
89
+
90
+ // Show overlay and disable inputs
91
+ var overlay = document.getElementById('loadingOverlay');
92
+ if (overlay) overlay.style.display = 'flex';
93
+ setTimeout(function() {
94
+ if (ci) ci.disabled = true;
95
+ var sb = document.querySelector('.chat-send-btn');
96
+ if (sb) sb.disabled = true;
97
+ }, 50);
98
+
99
+ // Build JSON body with optional attachments
100
+ var body = { message: messageText };
101
+ var attachments = window.__synthOSAttachments;
102
+ if (attachments && attachments.length > 0) {
103
+ body.attachments = attachments.map(function(a) {
104
+ return { mediaType: a.mediaType, data: a.data, name: a.name };
105
+ });
106
+ }
107
+
108
+ fetch(window.location.pathname, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify(body)
112
+ })
113
+ .then(function(res) { return res.text(); })
114
+ .then(function(html) {
115
+ // Mark page as dirty (unsaved changes) for required-page prompt
116
+ window.__synthOSPageDirty = true;
117
+ // Reset guards so page-v3.js re-initializes on the new DOM
118
+ window.__synthOSChatPanel = false;
119
+ window.__synthOSNavigateTo = null;
120
+ document.open();
121
+ document.write(html);
122
+ document.close();
123
+ })
124
+ .catch(function(err) {
125
+ console.error('Submit failed:', err);
126
+ if (overlay) overlay.style.display = 'none';
127
+ if (ci) ci.disabled = false;
128
+ var sb = document.querySelector('.chat-send-btn');
129
+ if (sb) sb.disabled = false;
130
+ });
131
+
132
+ // Clear attachments after submit
133
+ window.__synthOSAttachments = [];
134
+ var pillsContainer = document.querySelector('.attachment-pills');
135
+ if (pillsContainer) pillsContainer.innerHTML = '';
136
+ });
137
+ }
138
+
139
+ // 2b. Enter submits, Shift+Enter adds newline
140
+ (function() {
141
+ var ci = document.getElementById('chatInput');
142
+ if (!ci) return;
143
+ ci.addEventListener('keydown', function(e) {
144
+ if (e.key === 'Enter' && !e.shiftKey) {
145
+ e.preventDefault();
146
+ var form = document.getElementById('chatForm');
147
+ if (form) form.requestSubmit();
148
+ }
149
+ });
150
+ })();
151
+
152
+ // 3. Save modal — themed modal with title, categories, greeting
153
+ (function() {
154
+
155
+ // Detect if current page is a Builders, System, or Starters page (start with blank fields)
156
+ var isBuilder = window.pageInfo && Array.isArray(window.pageInfo.categories) &&
157
+ (window.pageInfo.categories.indexOf('Builders') !== -1 ||
158
+ window.pageInfo.categories.indexOf('System') !== -1 ||
159
+ window.pageInfo.categories.indexOf('_Starters') !== -1);
160
+
161
+ // Original title for change detection
162
+ var originalTitle = (window.pageInfo && window.pageInfo.title) ? window.pageInfo.title : '';
163
+
164
+ // --- Create save modal ---
165
+ var modal = document.createElement('div');
166
+ modal.id = 'synthos-saveModal';
167
+ modal.className = 'modal-overlay';
168
+ modal.innerHTML =
169
+ '<div class="modal-content" style="max-width:480px;">' +
170
+ '<div class="modal-header">' +
171
+ '<span>Save Page</span>' +
172
+ '<button type="button" class="brainstorm-close-btn" id="synthos-saveCloseBtn">&times;</button>' +
173
+ '</div>' +
174
+ '<div class="modal-body" style="display:flex;flex-direction:column;gap:12px;padding:16px 20px;">' +
175
+ '<div>' +
176
+ '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Display Title <span style="color:var(--accent-primary);">*</span></label>' +
177
+ '<input type="text" id="synthos-saveTitleInput" class="brainstorm-input" placeholder="Enter a display title..." style="width:100%;box-sizing:border-box;">' +
178
+ '<div id="synthos-saveTitleError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">Title is required</div>' +
179
+ '</div>' +
180
+ '<div>' +
181
+ '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Categories <span style="color:var(--accent-primary);">*</span></label>' +
182
+ '<input type="text" id="synthos-saveCategoriesInput" class="brainstorm-input" placeholder="e.g. Tool, Game, Utility" style="width:100%;box-sizing:border-box;">' +
183
+ '<div id="synthos-saveCategoriesError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">At least one category is required</div>' +
184
+ '</div>' +
185
+ '<div>' +
186
+ '<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Greeting <span style="font-size:11px;opacity:0.7;">(optional)</span></label>' +
187
+ '<input type="text" id="synthos-saveGreetingInput" class="brainstorm-input" placeholder="Available when title changes" style="width:100%;box-sizing:border-box;" disabled>' +
188
+ '<div style="font-size:11px;color:var(--text-secondary);margin-top:4px;opacity:0.7;" id="synthos-saveGreetingHint">Change the title to enable a custom greeting.</div>' +
189
+ '</div>' +
190
+ '</div>' +
191
+ '<div class="modal-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;">' +
192
+ '<button type="button" class="brainstorm-send-btn" id="synthos-saveCancelBtn" style="background:transparent;border:1px solid var(--border-color);color:var(--text-secondary);">Cancel</button>' +
193
+ '<button type="button" class="brainstorm-send-btn" id="synthos-saveConfirmBtn">Save</button>' +
194
+ '</div>' +
195
+ '</div>';
196
+ document.body.appendChild(modal);
197
+
198
+ // --- Create error modal ---
199
+ var errorModal = document.createElement('div');
200
+ errorModal.id = 'synthos-errorModal';
201
+ errorModal.className = 'modal-overlay';
202
+ errorModal.innerHTML =
203
+ '<div class="modal-content" style="max-width:400px;">' +
204
+ '<div class="modal-header">' +
205
+ '<span>Error</span>' +
206
+ '<button type="button" class="brainstorm-close-btn" id="synthos-errorCloseBtn">&times;</button>' +
207
+ '</div>' +
208
+ '<div class="modal-body" style="padding:16px 20px;">' +
209
+ '<p id="synthos-errorMessage" style="margin:0;color:var(--text-primary);"></p>' +
210
+ '</div>' +
211
+ '<div class="modal-footer" style="display:flex;justify-content:flex-end;padding:12px 20px;">' +
212
+ '<button type="button" class="brainstorm-send-btn" id="synthos-errorOkBtn">OK</button>' +
213
+ '</div>' +
214
+ '</div>';
215
+ document.body.appendChild(errorModal);
216
+
217
+ // --- Element references ---
218
+ var titleInput = document.getElementById('synthos-saveTitleInput');
219
+ var categoriesInput = document.getElementById('synthos-saveCategoriesInput');
220
+ var greetingInput = document.getElementById('synthos-saveGreetingInput');
221
+ var greetingHint = document.getElementById('synthos-saveGreetingHint');
222
+ var titleError = document.getElementById('synthos-saveTitleError');
223
+ var categoriesError = document.getElementById('synthos-saveCategoriesError');
224
+
225
+ // --- Greeting enable/disable based on title change ---
226
+ titleInput.addEventListener('input', function() {
227
+ var changed = titleInput.value.trim() !== originalTitle;
228
+ greetingInput.disabled = !changed;
229
+ if (changed) {
230
+ greetingInput.placeholder = 'Enter a custom greeting...';
231
+ greetingHint.textContent = 'Replaces the initial Synthos greeting and removes chat history.';
232
+ } else {
233
+ greetingInput.placeholder = 'Available when title changes';
234
+ greetingInput.value = '';
235
+ greetingHint.textContent = 'Change the title to enable a custom greeting.';
236
+ }
237
+ });
238
+
239
+ // --- Open modal ---
240
+ function openSaveModal() {
241
+ // Pre-fill fields (blank for Builder pages)
242
+ titleInput.value = isBuilder ? '' : originalTitle;
243
+ categoriesInput.value = isBuilder ? '' : (
244
+ (window.pageInfo && Array.isArray(window.pageInfo.categories))
245
+ ? window.pageInfo.categories.join(', ')
246
+ : ''
247
+ );
248
+ greetingInput.value = '';
249
+ greetingInput.disabled = true;
250
+ greetingInput.placeholder = 'Available when title changes';
251
+ greetingHint.textContent = 'Change the title to enable a custom greeting.';
252
+ titleError.style.display = 'none';
253
+ categoriesError.style.display = 'none';
254
+ modal.classList.add('show');
255
+ titleInput.focus();
256
+ }
257
+
258
+ function closeSaveModal() {
259
+ modal.classList.remove('show');
260
+ }
261
+
262
+ function showError(msg) {
263
+ document.getElementById('synthos-errorMessage').textContent = msg;
264
+ errorModal.classList.add('show');
265
+ }
266
+
267
+ function closeError() {
268
+ errorModal.classList.remove('show');
269
+ }
270
+
271
+ // --- Submit ---
272
+ function submitSave() {
273
+ var title = titleInput.value.trim();
274
+ var cats = categoriesInput.value.trim();
275
+ var greeting = greetingInput.value.trim();
276
+ var valid = true;
277
+
278
+ // Validate
279
+ if (!title) {
280
+ titleError.style.display = 'block';
281
+ valid = false;
282
+ } else {
283
+ titleError.style.display = 'none';
284
+ }
285
+ if (!cats) {
286
+ categoriesError.style.display = 'block';
287
+ valid = false;
288
+ } else {
289
+ categoriesError.style.display = 'none';
290
+ }
291
+ if (!valid) return;
292
+
293
+ // Parse categories
294
+ var categories = cats.split(',').map(function(c) { return c.trim(); }).filter(Boolean);
295
+
296
+ // Disable button during save
297
+ var confirmBtn = document.getElementById('synthos-saveConfirmBtn');
298
+ confirmBtn.disabled = true;
299
+ confirmBtn.textContent = 'Saving...';
300
+
301
+ var body = { title: title, categories: categories };
302
+ if (greeting && !greetingInput.disabled) {
303
+ body.greeting = greeting;
304
+ }
305
+
306
+ fetch(window.location.pathname + '/save', {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify(body)
310
+ })
311
+ .then(function(res) {
312
+ return res.json().then(function(data) {
313
+ return { ok: res.ok, data: data };
314
+ });
315
+ })
316
+ .then(function(result) {
317
+ if (result.ok && result.data.redirect) {
318
+ window.__synthOSPageDirty = false;
319
+ window.location.href = result.data.redirect;
320
+ } else {
321
+ closeSaveModal();
322
+ showError(result.data.error || 'An unknown error occurred');
323
+ confirmBtn.disabled = false;
324
+ confirmBtn.textContent = 'Save';
325
+ }
326
+ })
327
+ .catch(function(err) {
328
+ closeSaveModal();
329
+ showError('Network error: ' + err.message);
330
+ confirmBtn.disabled = false;
331
+ confirmBtn.textContent = 'Save';
332
+ });
333
+ }
334
+
335
+ // Expose openSaveModal globally for toolbar button
336
+ window.__synthOSOpenSaveModal = openSaveModal;
337
+
338
+ // --- Event listeners ---
339
+ document.getElementById('synthos-saveCloseBtn').addEventListener('click', closeSaveModal);
340
+ document.getElementById('synthos-saveCancelBtn').addEventListener('click', closeSaveModal);
341
+ document.getElementById('synthos-saveConfirmBtn').addEventListener('click', submitSave);
342
+ document.getElementById('synthos-errorCloseBtn').addEventListener('click', closeError);
343
+ document.getElementById('synthos-errorOkBtn').addEventListener('click', closeError);
344
+
345
+ var saveModalMouseDownTarget = null;
346
+ modal.addEventListener('mousedown', function(e) { saveModalMouseDownTarget = e.target; });
347
+ modal.addEventListener('click', function(e) {
348
+ if (e.target === modal && saveModalMouseDownTarget === modal) closeSaveModal();
349
+ saveModalMouseDownTarget = null;
350
+ });
351
+ var errorModalMouseDownTarget = null;
352
+ errorModal.addEventListener('mousedown', function(e) { errorModalMouseDownTarget = e.target; });
353
+ errorModal.addEventListener('click', function(e) {
354
+ if (e.target === errorModal && errorModalMouseDownTarget === errorModal) closeError();
355
+ errorModalMouseDownTarget = null;
356
+ });
357
+
358
+ document.addEventListener('keydown', function(e) {
359
+ if (e.key === 'Escape') {
360
+ if (modal.classList.contains('show')) closeSaveModal();
361
+ if (errorModal.classList.contains('show')) closeError();
362
+ }
363
+ });
364
+
365
+ // Enter key in title/categories inputs triggers save
366
+ titleInput.addEventListener('keydown', function(e) {
367
+ if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
368
+ });
369
+ categoriesInput.addEventListener('keydown', function(e) {
370
+ if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
371
+ });
372
+ })();
373
+
374
+ // 4. Chat scroll to bottom
375
+ var chatMessages = document.getElementById('chatMessages');
376
+ if (chatMessages) {
377
+ chatMessages.scrollTo({
378
+ top: chatMessages.scrollHeight,
379
+ behavior: 'smooth'
380
+ });
381
+ }
382
+
383
+ // 5. Shell toolbar button wiring
384
+ (function() {
385
+ var DB_NAME = 'synthos-ui';
386
+ var STORE_NAME = 'panel-state';
387
+ var pageName = window.location.pathname.replace(/^\//, '') || 'builder';
388
+ var isBuilderPage = pageName === 'builder';
389
+
390
+ // IndexedDB helpers
391
+ function openDB(cb) {
392
+ var req = indexedDB.open(DB_NAME, 1);
393
+ req.onupgradeneeded = function() {
394
+ var db = req.result;
395
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
396
+ db.createObjectStore(STORE_NAME);
397
+ }
398
+ };
399
+ req.onsuccess = function() { cb(req.result); };
400
+ req.onerror = function() { cb(null); };
401
+ }
402
+
403
+ function saveCollapsed(collapsed) {
404
+ openDB(function(db) {
405
+ if (!db) return;
406
+ var tx = db.transaction(STORE_NAME, 'readwrite');
407
+ tx.objectStore(STORE_NAME).put(collapsed, pageName);
408
+ });
409
+ }
410
+
411
+ function loadCollapsed(cb) {
412
+ openDB(function(db) {
413
+ if (!db) { cb(null); return; }
414
+ var tx = db.transaction(STORE_NAME, 'readonly');
415
+ var req = tx.objectStore(STORE_NAME).get(pageName);
416
+ req.onsuccess = function() { cb(req.result); };
417
+ req.onerror = function() { cb(null); };
418
+ });
419
+ }
420
+
421
+ function setCollapsed(collapsed) {
422
+ if (collapsed) {
423
+ document.body.classList.add('chat-collapsed');
424
+ } else {
425
+ document.body.classList.remove('chat-collapsed');
426
+ }
427
+ saveCollapsed(collapsed);
428
+ }
429
+
430
+ // Restore state — builder page always starts open
431
+ if (isBuilderPage) {
432
+ document.body.classList.remove('chat-collapsed');
433
+ } else {
434
+ loadCollapsed(function(val) {
435
+ if (val === true) {
436
+ document.body.classList.add('chat-collapsed');
437
+ }
438
+ });
439
+ }
440
+
441
+ // Builder toggle — show/hide chat panel
442
+ var builderToggle = document.getElementById('builderToggle');
443
+ if (builderToggle) {
444
+ builderToggle.addEventListener('click', function() {
445
+ var collapsed = !document.body.classList.contains('chat-collapsed');
446
+ setCollapsed(collapsed);
447
+ });
448
+ }
449
+
450
+ // Builder close button — same as collapse
451
+ var builderClose = document.getElementById('builderClose');
452
+ if (builderClose) {
453
+ builderClose.addEventListener('click', function() {
454
+ setCollapsed(true);
455
+ });
456
+ }
457
+
458
+ // Pages button navigate to pages gallery
459
+ var pagesBtn = document.getElementById('pagesBtn');
460
+ if (pagesBtn) {
461
+ pagesBtn.addEventListener('click', function() {
462
+ window.__synthOSNavigateTo('/pages');
463
+ });
464
+ }
465
+
466
+ // Save button — open save modal
467
+ var saveBtn = document.getElementById('saveBtn');
468
+ if (saveBtn) {
469
+ saveBtn.addEventListener('click', function() {
470
+ if (window.__synthOSOpenSaveModal) window.__synthOSOpenSaveModal();
471
+ });
472
+ }
473
+
474
+ // Settings button — navigate to settings page
475
+ var settingsBtn = document.getElementById('settingsBtn');
476
+ if (settingsBtn) {
477
+ settingsBtn.addEventListener('click', function() {
478
+ window.__synthOSNavigateTo('/settings');
479
+ });
480
+ }
481
+ })();
482
+
483
+ // 6. Focus management — prevent viewer content from stealing keystrokes
484
+ (function() {
485
+ var ci = document.getElementById('chatInput');
486
+ var vp = document.getElementById('viewerPanel');
487
+ if (!ci || !vp) return;
488
+
489
+ ci.addEventListener('mousedown', function(e) {
490
+ e.stopPropagation();
491
+ });
492
+
493
+ ['keydown', 'keyup', 'keypress'].forEach(function(type) {
494
+ document.addEventListener(type, function(e) {
495
+ if (document.activeElement === ci) {
496
+ // Allow Enter (without Shift) to reach the submit handler
497
+ if (e.key === 'Enter' && !e.shiftKey) return;
498
+ e.stopImmediatePropagation();
499
+ }
500
+ }, true);
501
+ });
502
+
503
+ vp.setAttribute('tabindex', '-1');
504
+ ci.addEventListener('blur', function() {
505
+ vp.focus();
506
+ });
507
+ })();
508
+
509
+ // 7. Brainstorm — dynamic brainstorming UI (available on every v2 page)
510
+ (function() {
511
+ var chatInput = document.getElementById('chatInput');
512
+ if (!chatInput) return;
513
+
514
+ // --- Create icon row (.chat-input-wrapper) appended to #chatForm ---
515
+ var form = document.getElementById('chatForm');
516
+ var wrapper = document.querySelector('.chat-input-wrapper');
517
+ if (!wrapper) {
518
+ wrapper = document.createElement('div');
519
+ wrapper.className = 'chat-input-wrapper';
520
+ if (form) form.appendChild(wrapper);
521
+ }
522
+
523
+ // --- Create brainstorm icon button ---
524
+ var brainstormBtn = document.createElement('button');
525
+ brainstormBtn.type = 'button';
526
+ brainstormBtn.className = 'brainstorm-icon-btn';
527
+ if (window.__synthOSTooltip) window.__synthOSTooltip(brainstormBtn, 'Brainstorm ideas');
528
+ 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">' +
529
+ '<circle cx="12" cy="12" r="3"></circle>' +
530
+ '<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>' +
531
+ '</svg>';
532
+ wrapper.appendChild(brainstormBtn);
533
+
534
+ // --- Create send button ---
535
+ var sendBtn = document.createElement('button');
536
+ sendBtn.type = 'submit';
537
+ sendBtn.className = 'chat-send-btn';
538
+ if (window.__synthOSTooltip) window.__synthOSTooltip(sendBtn, 'Send message');
539
+ 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">' +
540
+ '<line x1="12" y1="19" x2="12" y2="5"></line>' +
541
+ '<polyline points="5 12 12 5 19 12"></polyline>' +
542
+ '</svg>';
543
+ wrapper.appendChild(sendBtn);
544
+
545
+ // --- Auto-grow textarea (capped) ---
546
+ chatInput.addEventListener('input', function() {
547
+ this.style.height = 'auto';
548
+ var maxH = parseFloat(getComputedStyle(this).maxHeight) || 120;
549
+ this.style.height = Math.min(this.scrollHeight, maxH) + 'px';
550
+ });
551
+
552
+ // --- Create brainstorm modal ---
553
+ var modal = document.createElement('div');
554
+ modal.id = 'synthos-brainstormModal';
555
+ modal.className = 'modal-overlay brainstorm-modal';
556
+ modal.innerHTML =
557
+ '<div class="modal-content">' +
558
+ '<div class="modal-header">' +
559
+ '<span>Brainstorm</span>' +
560
+ '<button type="button" class="brainstorm-close-btn" id="synthos-brainstormCloseBtn">&times;</button>' +
561
+ '</div>' +
562
+ '<div class="brainstorm-messages" id="synthos-brainstormMessages"></div>' +
563
+ '<div class="brainstorm-input-row">' +
564
+ '<input type="text" class="brainstorm-input" id="synthos-brainstormInput" placeholder="What\'s on your mind...">' +
565
+ '<button type="button" class="brainstorm-send-btn" id="synthos-brainstormSendBtn">Send</button>' +
566
+ '</div>' +
567
+ '</div>';
568
+ document.body.appendChild(modal);
569
+
570
+ // --- State ---
571
+ var brainstormHistory = [];
572
+
573
+ // --- Helpers ---
574
+ function openBrainstorm() {
575
+ modal.classList.add('show');
576
+ // Grab text from chat input as initial topic
577
+ var topic = chatInput.value.trim();
578
+ if (topic) {
579
+ chatInput.value = '';
580
+ sendBrainstormText(topic, true);
581
+ } else {
582
+ // No topic — send context-only opener so LLM starts the brainstorm
583
+ sendBrainstormText('', true);
584
+ }
585
+ }
586
+
587
+ function closeBrainstorm() {
588
+ modal.classList.remove('show');
589
+ brainstormHistory = [];
590
+ document.getElementById('synthos-brainstormMessages').innerHTML = '';
591
+ }
592
+
593
+ function scrollBrainstormToBottom() {
594
+ var el = document.getElementById('synthos-brainstormMessages');
595
+ el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
596
+ }
597
+
598
+ function escapeHtml(str) {
599
+ var div = document.createElement('div');
600
+ div.appendChild(document.createTextNode(str));
601
+ return div.innerHTML;
602
+ }
603
+
604
+ function appendBrainstormMessage(role, text, prompt, suggestions, isOpener) {
605
+ var div = document.createElement('div');
606
+ div.className = 'brainstorm-message ' + (role === 'user' ? 'brainstorm-user' : 'brainstorm-assistant');
607
+ if (role === 'assistant') {
608
+ var html;
609
+ if (typeof marked !== 'undefined') {
610
+ html = marked.parse(text);
611
+ } else {
612
+ html = escapeHtml(text);
613
+ }
614
+ div.innerHTML = '<strong>' + pn + ':</strong> ' + html;
615
+ // Clickable suggestion chips
616
+ if (suggestions && suggestions.length > 0) {
617
+ var chips = document.createElement('div');
618
+ chips.className = 'brainstorm-suggestions';
619
+ suggestions.forEach(function(s) {
620
+ var chip = document.createElement('button');
621
+ chip.type = 'button';
622
+ chip.className = 'brainstorm-suggestion-chip';
623
+ chip.textContent = s;
624
+ chip.addEventListener('click', function() {
625
+ submitSuggestion(s);
626
+ });
627
+ chips.appendChild(chip);
628
+ });
629
+ div.appendChild(chips);
630
+ }
631
+ // "Build It" button skip on the opener response
632
+ if (prompt && !isOpener) {
633
+ var btnRow = document.createElement('div');
634
+ btnRow.className = 'brainstorm-build-row';
635
+ var buildBtn = document.createElement('button');
636
+ buildBtn.type = 'button';
637
+ buildBtn.className = 'brainstorm-build-btn';
638
+ buildBtn.textContent = 'Build It';
639
+ buildBtn.setAttribute('data-prompt', prompt);
640
+ buildBtn.addEventListener('click', function() {
641
+ chatInput.value = this.getAttribute('data-prompt');
642
+ closeBrainstorm();
643
+ chatInput.focus();
644
+ });
645
+ btnRow.appendChild(buildBtn);
646
+ div.appendChild(btnRow);
647
+ }
648
+ } else {
649
+ div.textContent = text;
650
+ }
651
+ document.getElementById('synthos-brainstormMessages').appendChild(div);
652
+ scrollBrainstormToBottom();
653
+ }
654
+
655
+ function submitSuggestion(text) {
656
+ // Disable old suggestion chips so they can't be double-clicked
657
+ var oldChips = document.querySelectorAll('#synthos-brainstormMessages .brainstorm-suggestion-chip');
658
+ for (var i = 0; i < oldChips.length; i++) {
659
+ oldChips[i].disabled = true;
660
+ }
661
+ sendBrainstormText(text, false);
662
+ }
663
+
664
+ function getBrainstormContext() {
665
+ var chatEl = document.getElementById('chatMessages');
666
+ if (!chatEl) return '<CHAT_HISTORY>\n';
667
+ var msgs = chatEl.querySelectorAll('.chat-message');
668
+ var lines = [];
669
+ var started = false;
670
+ for (var i = 0; i < msgs.length; i++) {
671
+ var text = msgs[i].innerText;
672
+ if (!started && /^User:/i.test(text.trim())) started = true;
673
+ if (started) lines.push(text);
674
+ }
675
+ return '<CHAT_HISTORY>\n' + lines.join('\n');
676
+ }
677
+
678
+ // Send from the input field
679
+ function sendBrainstormMessage() {
680
+ var input = document.getElementById('synthos-brainstormInput');
681
+ var text = input.value.trim();
682
+ if (!text) return;
683
+ input.value = '';
684
+ sendBrainstormText(text, false);
685
+ }
686
+
687
+ // Core fetch — isOpener=true means this is the initial call when brainstorm opens
688
+ function sendBrainstormText(text, isOpener) {
689
+ var input = document.getElementById('synthos-brainstormInput');
690
+ var userMsg = text || (isOpener ? 'Look at the conversation so far and suggest what we could build or improve.' : '');
691
+ if (!userMsg) return;
692
+
693
+ // Show user message in chat (skip for auto-generated opener)
694
+ if (text) {
695
+ appendBrainstormMessage('user', text);
696
+ }
697
+ brainstormHistory.push({ role: 'user', content: userMsg });
698
+
699
+ var thinking = document.createElement('div');
700
+ thinking.className = 'brainstorm-thinking';
701
+ thinking.id = 'synthos-brainstormThinking';
702
+ thinking.textContent = 'Thinking...';
703
+ document.getElementById('synthos-brainstormMessages').appendChild(thinking);
704
+ scrollBrainstormToBottom();
705
+
706
+ input.disabled = true;
707
+ document.getElementById('synthos-brainstormSendBtn').disabled = true;
708
+
709
+ fetch('/api/brainstorm', {
710
+ method: 'POST',
711
+ headers: { 'Content-Type': 'application/json' },
712
+ body: JSON.stringify({
713
+ context: getBrainstormContext(),
714
+ messages: brainstormHistory
715
+ })
716
+ })
717
+ .then(function(res) {
718
+ if (!res.ok) throw new Error('Brainstorm request failed');
719
+ return res.json();
720
+ })
721
+ .then(function(data) {
722
+ var thinkingEl = document.getElementById('synthos-brainstormThinking');
723
+ if (thinkingEl) thinkingEl.remove();
724
+
725
+ var response = data.response || 'Sorry, I didn\'t get a response.';
726
+ var prompt = data.prompt || '';
727
+ var suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
728
+ appendBrainstormMessage('assistant', response, prompt, suggestions, isOpener);
729
+ brainstormHistory.push({
730
+ role: 'assistant',
731
+ content: response + '\n\n[Suggested prompt: ' + prompt + ']'
732
+ });
733
+ })
734
+ .catch(function(err) {
735
+ var thinkingEl = document.getElementById('synthos-brainstormThinking');
736
+ if (thinkingEl) thinkingEl.remove();
737
+ appendBrainstormMessage('assistant', 'Something went wrong: ' + err.message);
738
+ })
739
+ .finally(function() {
740
+ input.disabled = false;
741
+ document.getElementById('synthos-brainstormSendBtn').disabled = false;
742
+ input.focus();
743
+ });
744
+ }
745
+
746
+ // --- Event listeners ---
747
+ brainstormBtn.addEventListener('click', openBrainstorm);
748
+ document.getElementById('synthos-brainstormCloseBtn').addEventListener('click', closeBrainstorm);
749
+
750
+ var brainstormMouseDownTarget = null;
751
+ modal.addEventListener('mousedown', function(e) { brainstormMouseDownTarget = e.target; });
752
+ modal.addEventListener('click', function(e) {
753
+ if (e.target === modal && brainstormMouseDownTarget === modal) closeBrainstorm();
754
+ brainstormMouseDownTarget = null;
755
+ });
756
+
757
+ document.addEventListener('keydown', function(e) {
758
+ if (e.key === 'Escape' && modal.classList.contains('show')) closeBrainstorm();
759
+ });
760
+
761
+ document.getElementById('synthos-brainstormSendBtn').addEventListener('click', sendBrainstormMessage);
762
+ document.getElementById('synthos-brainstormInput').addEventListener('keydown', function(e) {
763
+ if (e.key === 'Enter' && !e.shiftKey) {
764
+ e.preventDefault();
765
+ sendBrainstormMessage();
766
+ }
767
+ });
768
+ })();
769
+
770
+ // 8. Attach button — + menu, attachment pills, file picker, screenshot tool
771
+ (function() {
772
+ var chatInput = document.getElementById('chatInput');
773
+ if (!chatInput) return;
774
+ var chatForm = document.getElementById('chatForm');
775
+ var wrapper = chatForm ? chatForm.querySelector('.chat-input-wrapper') : null;
776
+ if (!wrapper) return;
777
+
778
+ // --- Attachments state ---
779
+ if (!window.__synthOSAttachments) window.__synthOSAttachments = [];
780
+
781
+ // --- Create pills container (inside #chatForm, before textarea) ---
782
+ var pillsContainer = document.createElement('div');
783
+ pillsContainer.className = 'attachment-pills';
784
+ if (chatForm) {
785
+ chatForm.insertBefore(pillsContainer, chatForm.firstChild);
786
+ }
787
+
788
+ function renderPills() {
789
+ pillsContainer.innerHTML = '';
790
+ var attachments = window.__synthOSAttachments;
791
+ if (!attachments || attachments.length === 0) return;
792
+ for (var i = 0; i < attachments.length; i++) {
793
+ (function(idx) {
794
+ var att = attachments[idx];
795
+ var pill = document.createElement('div');
796
+ pill.className = 'attachment-pill';
797
+
798
+ var thumb = document.createElement('img');
799
+ thumb.src = 'data:' + att.mediaType + ';base64,' + att.data;
800
+ thumb.style.cssText = 'width:24px;height:24px;object-fit:cover;border-radius:3px;';
801
+ pill.appendChild(thumb);
802
+
803
+ var nameSpan = document.createElement('span');
804
+ nameSpan.textContent = att.name || 'image';
805
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;';
806
+ pill.appendChild(nameSpan);
807
+
808
+ var removeBtn = document.createElement('button');
809
+ removeBtn.type = 'button';
810
+ removeBtn.className = 'attachment-pill-remove';
811
+ removeBtn.textContent = '\u00d7';
812
+ removeBtn.addEventListener('click', function() {
813
+ window.__synthOSAttachments.splice(idx, 1);
814
+ renderPills();
815
+ });
816
+ pill.appendChild(removeBtn);
817
+
818
+ pillsContainer.appendChild(pill);
819
+ })(i);
820
+ }
821
+ }
822
+
823
+ // Expose for debugging
824
+ window.__synthOSRenderPills = renderPills;
825
+
826
+ // --- Helper: add an image attachment from a data URL ---
827
+ function addImageFromDataUrl(dataUrl, name) {
828
+ var commaIdx = dataUrl.indexOf(',');
829
+ if (commaIdx === -1) return;
830
+ var meta = dataUrl.substring(0, commaIdx); // data:image/png;base64
831
+ var base64 = dataUrl.substring(commaIdx + 1);
832
+ var mediaType = meta.replace('data:', '').replace(';base64', '');
833
+ window.__synthOSAttachments.push({
834
+ mediaType: mediaType,
835
+ data: base64,
836
+ name: name || 'image'
837
+ });
838
+ renderPills();
839
+ }
840
+
841
+ // --- Hidden file input ---
842
+ var fileInput = document.createElement('input');
843
+ fileInput.type = 'file';
844
+ fileInput.style.display = 'none';
845
+ document.body.appendChild(fileInput);
846
+
847
+ fileInput.addEventListener('change', function() {
848
+ var file = fileInput.files && fileInput.files[0];
849
+ if (!file) return;
850
+ var reader = new FileReader();
851
+ reader.onload = function() {
852
+ addImageFromDataUrl(reader.result, file.name);
853
+ };
854
+ reader.readAsDataURL(file);
855
+ fileInput.value = '';
856
+ });
857
+
858
+ // --- Paste image from clipboard (document-level to catch all pastes) ---
859
+ document.addEventListener('paste', function(e) {
860
+ // Only handle pastes when chat input is focused or no other editable is focused
861
+ var active = document.activeElement;
862
+ var isEditable = active && (active.isContentEditable ||
863
+ (active.tagName === 'TEXTAREA' && active !== chatInput) ||
864
+ (active.tagName === 'INPUT' && active !== chatInput));
865
+ if (isEditable) return;
866
+
867
+ var items = e.clipboardData && e.clipboardData.items;
868
+ if (!items) return;
869
+ for (var i = 0; i < items.length; i++) {
870
+ if (items[i].type.indexOf('image/') === 0) {
871
+ var blob = items[i].getAsFile();
872
+ if (!blob) continue;
873
+ e.preventDefault();
874
+ var reader = new FileReader();
875
+ reader.onload = function() {
876
+ addImageFromDataUrl(reader.result, 'pasted-image.png');
877
+ };
878
+ reader.readAsDataURL(blob);
879
+ return; // only handle the first image
880
+ }
881
+ }
882
+ });
883
+
884
+ // --- Drag-and-drop file attachment ---
885
+ var chatForm = document.getElementById('chatForm');
886
+ if (chatForm) {
887
+ chatForm.addEventListener('dragover', function(e) {
888
+ e.preventDefault();
889
+ e.stopPropagation();
890
+ });
891
+ chatForm.addEventListener('drop', function(e) {
892
+ e.preventDefault();
893
+ e.stopPropagation();
894
+ var files = e.dataTransfer && e.dataTransfer.files;
895
+ if (!files || !files.length) return;
896
+ for (var i = 0; i < files.length; i++) {
897
+ (function(file) {
898
+ var reader = new FileReader();
899
+ reader.onload = function() {
900
+ addImageFromDataUrl(reader.result, file.name);
901
+ };
902
+ reader.readAsDataURL(file);
903
+ })(files[i]);
904
+ }
905
+ });
906
+ }
907
+
908
+ // --- + button ---
909
+ var attachBtn = document.createElement('button');
910
+ attachBtn.type = 'button';
911
+ attachBtn.className = 'attach-btn';
912
+ if (window.__synthOSTooltip) window.__synthOSTooltip(attachBtn, 'Attach file or screenshot');
913
+ 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>';
914
+ wrapper.insertBefore(attachBtn, wrapper.firstChild);
915
+
916
+ // --- Popup menu ---
917
+ var menu = document.createElement('div');
918
+ menu.className = 'attach-menu';
919
+
920
+ var menuAttachFile = document.createElement('div');
921
+ menuAttachFile.className = 'attach-menu-item';
922
+ 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';
923
+ menu.appendChild(menuAttachFile);
924
+
925
+ var menuScreenshot = document.createElement('div');
926
+ menuScreenshot.className = 'attach-menu-item';
927
+ 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';
928
+ menu.appendChild(menuScreenshot);
929
+
930
+ wrapper.appendChild(menu);
931
+
932
+ var menuOpen = false;
933
+ function toggleMenu() {
934
+ menuOpen = !menuOpen;
935
+ menu.style.display = menuOpen ? 'flex' : 'none';
936
+ }
937
+ function closeMenu() {
938
+ menuOpen = false;
939
+ menu.style.display = 'none';
940
+ }
941
+
942
+ attachBtn.addEventListener('click', function(e) {
943
+ e.stopPropagation();
944
+ toggleMenu();
945
+ });
946
+
947
+ document.addEventListener('click', function() {
948
+ if (menuOpen) closeMenu();
949
+ });
950
+ menu.addEventListener('click', function(e) { e.stopPropagation(); });
951
+
952
+ menuAttachFile.addEventListener('click', function() {
953
+ closeMenu();
954
+ fileInput.click();
955
+ });
956
+
957
+ // --- Screenshot annotation flow (multi-rectangle) ---
958
+ menuScreenshot.addEventListener('click', function() {
959
+ closeMenu();
960
+ startScreenshotAnnotation();
961
+ });
962
+
963
+ function startScreenshotAnnotation() {
964
+ var viewerPanel = document.getElementById('viewerPanel');
965
+ if (!viewerPanel) return;
966
+
967
+ // Create overlay
968
+ var overlay = document.createElement('div');
969
+ overlay.className = 'screenshot-overlay';
970
+
971
+ // Instructions bar
972
+ var instrBar = document.createElement('div');
973
+ 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;';
974
+ instrBar.textContent = 'Draw rectangles to highlight areas, then click Capture';
975
+ overlay.appendChild(instrBar);
976
+
977
+ // Persistent action buttons (always visible)
978
+ var actions = document.createElement('div');
979
+ actions.className = 'screenshot-actions';
980
+ actions.style.cssText = 'position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:10;';
981
+
982
+ var captureBtn = document.createElement('button');
983
+ captureBtn.type = 'button';
984
+ captureBtn.textContent = 'Capture';
985
+ captureBtn.className = 'brainstorm-send-btn';
986
+ captureBtn.style.cssText = 'padding:6px 16px;font-size:13px;';
987
+
988
+ var cancelBtn = document.createElement('button');
989
+ cancelBtn.type = 'button';
990
+ cancelBtn.textContent = 'Cancel';
991
+ cancelBtn.className = 'brainstorm-send-btn';
992
+ cancelBtn.style.cssText = 'padding:6px 16px;font-size:13px;background:transparent;border:1px solid rgba(255,255,255,0.3);color:white;';
993
+
994
+ actions.appendChild(captureBtn);
995
+ actions.appendChild(cancelBtn);
996
+ overlay.appendChild(actions);
997
+
998
+ // Drawing state
999
+ var currentRect = null;
1000
+ var startX, startY, isDrawing = false;
1001
+ var allRects = []; // Array of {el, x, y, w, h}
1002
+
1003
+ overlay.addEventListener('mousedown', function(e) {
1004
+ if (e.target.tagName === 'BUTTON') return;
1005
+ isDrawing = true;
1006
+ var overlayBounds = overlay.getBoundingClientRect();
1007
+ startX = e.clientX - overlayBounds.left;
1008
+ startY = e.clientY - overlayBounds.top;
1009
+
1010
+ // Create a new rectangle element
1011
+ currentRect = document.createElement('div');
1012
+ currentRect.className = 'screenshot-rect';
1013
+ currentRect.style.display = 'block';
1014
+ currentRect.style.left = startX + 'px';
1015
+ currentRect.style.top = startY + 'px';
1016
+ currentRect.style.width = '0';
1017
+ currentRect.style.height = '0';
1018
+ overlay.appendChild(currentRect);
1019
+ });
1020
+
1021
+ overlay.addEventListener('mousemove', function(e) {
1022
+ if (!isDrawing || !currentRect) return;
1023
+ var overlayBounds = overlay.getBoundingClientRect();
1024
+ var curX = e.clientX - overlayBounds.left;
1025
+ var curY = e.clientY - overlayBounds.top;
1026
+ var x = Math.min(startX, curX);
1027
+ var y = Math.min(startY, curY);
1028
+ var w = Math.abs(curX - startX);
1029
+ var h = Math.abs(curY - startY);
1030
+ currentRect.style.left = x + 'px';
1031
+ currentRect.style.top = y + 'px';
1032
+ currentRect.style.width = w + 'px';
1033
+ currentRect.style.height = h + 'px';
1034
+ });
1035
+
1036
+ overlay.addEventListener('mouseup', function() {
1037
+ if (!isDrawing || !currentRect) return;
1038
+ isDrawing = false;
1039
+ var w = parseInt(currentRect.style.width);
1040
+ var h = parseInt(currentRect.style.height);
1041
+ if (w < 10 || h < 10) {
1042
+ // Too small — discard
1043
+ currentRect.remove();
1044
+ currentRect = null;
1045
+ return;
1046
+ }
1047
+ allRects.push({
1048
+ el: currentRect,
1049
+ x: parseInt(currentRect.style.left),
1050
+ y: parseInt(currentRect.style.top),
1051
+ w: w,
1052
+ h: h
1053
+ });
1054
+ currentRect = null;
1055
+ });
1056
+
1057
+ captureBtn.addEventListener('click', function() {
1058
+ doCapture();
1059
+ });
1060
+ cancelBtn.addEventListener('click', function() {
1061
+ cleanup();
1062
+ });
1063
+
1064
+ function doCapture() {
1065
+ if (typeof html2canvas === 'undefined') {
1066
+ console.error('html2canvas not loaded');
1067
+ cleanup();
1068
+ return;
1069
+ }
1070
+
1071
+ // Capture the full viewer panel without the overlay
1072
+ overlay.style.visibility = 'hidden';
1073
+
1074
+ html2canvas(viewerPanel, { useCORS: true, logging: false }).then(function(fullCanvas) {
1075
+ // Draw red rectangles onto the captured canvas
1076
+ var vpRect = viewerPanel.getBoundingClientRect();
1077
+ var scaleX = fullCanvas.width / vpRect.width;
1078
+ var scaleY = fullCanvas.height / vpRect.height;
1079
+
1080
+ var ctx = fullCanvas.getContext('2d');
1081
+ ctx.strokeStyle = 'red';
1082
+ ctx.lineWidth = 3 * Math.max(scaleX, scaleY);
1083
+
1084
+ for (var i = 0; i < allRects.length; i++) {
1085
+ var r = allRects[i];
1086
+ ctx.strokeRect(
1087
+ Math.round(r.x * scaleX),
1088
+ Math.round(r.y * scaleY),
1089
+ Math.round(r.w * scaleX),
1090
+ Math.round(r.h * scaleY)
1091
+ );
1092
+ }
1093
+
1094
+ var dataUrl = fullCanvas.toDataURL('image/png');
1095
+ var base64 = dataUrl.split(',')[1];
1096
+ window.__synthOSAttachments.push({
1097
+ mediaType: 'image/png',
1098
+ data: base64,
1099
+ name: 'screenshot.png'
1100
+ });
1101
+ renderPills();
1102
+ cleanup();
1103
+ }).catch(function(err) {
1104
+ console.error('Screenshot capture failed:', err);
1105
+ cleanup();
1106
+ });
1107
+ }
1108
+
1109
+ function cleanup() {
1110
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
1111
+ document.removeEventListener('keydown', onKeyDown);
1112
+ }
1113
+
1114
+ // Escape to cancel
1115
+ function onKeyDown(e) {
1116
+ if (e.key === 'Escape') {
1117
+ cleanup();
1118
+ }
1119
+ }
1120
+ document.addEventListener('keydown', onKeyDown);
1121
+
1122
+ viewerPanel.style.position = 'relative';
1123
+ viewerPanel.appendChild(overlay);
1124
+ }
1125
+ })();
1126
+
1127
+ // 9. Undo link — shown after the last chat message when a version exists
1128
+ (function() {
1129
+ var meta = document.querySelector('meta[name="synthos-version"]');
1130
+ if (!meta) return;
1131
+ var version = parseInt(meta.getAttribute('content'), 10);
1132
+ if (!version || version <= 0) return;
1133
+
1134
+ var cm = document.getElementById('chatMessages');
1135
+ if (!cm) return;
1136
+
1137
+ // Remove any existing undo links
1138
+ var existing = cm.querySelectorAll('.synthos-undo-link');
1139
+ for (var i = 0; i < existing.length; i++) existing[i].remove();
1140
+
1141
+ // Create undo link div
1142
+ var undoDiv = document.createElement('div');
1143
+ undoDiv.className = 'synthos-undo-link';
1144
+ undoDiv.style.cssText = 'text-align:right;padding:4px 12px 2px;';
1145
+
1146
+ var link = document.createElement('a');
1147
+ link.href = '#';
1148
+ link.textContent = 'undo';
1149
+ link.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
1150
+ link.addEventListener('mouseenter', function() { link.style.opacity = '1'; link.style.textDecoration = 'underline'; });
1151
+ link.addEventListener('mouseleave', function() { link.style.opacity = '0.7'; link.style.textDecoration = 'none'; });
1152
+
1153
+ link.addEventListener('click', function(e) {
1154
+ e.preventDefault();
1155
+ var overlay = document.getElementById('loadingOverlay');
1156
+ if (overlay) overlay.style.display = 'flex';
1157
+
1158
+ fetch(window.location.pathname + '/undo', {
1159
+ method: 'POST',
1160
+ headers: { 'Content-Type': 'application/json' },
1161
+ body: '{}'
1162
+ })
1163
+ .then(function(res) { return res.text(); })
1164
+ .then(function(html) {
1165
+ window.__synthOSChatPanel = false;
1166
+ window.__synthOSPageDirty = false;
1167
+ window.__synthOSNavigateTo = null;
1168
+ document.open();
1169
+ document.write(html);
1170
+ document.close();
1171
+ })
1172
+ .catch(function(err) {
1173
+ console.error('Undo failed:', err);
1174
+ if (overlay) overlay.style.display = 'none';
1175
+ });
1176
+ });
1177
+
1178
+ undoDiv.appendChild(link);
1179
+
1180
+ // "try again" link — re-submits the last user message against the previous page state
1181
+ var msgs = cm.querySelectorAll('.chat-message');
1182
+ var lastUserMsg = '';
1183
+ for (var j = msgs.length - 1; j >= 0; j--) {
1184
+ var strong = msgs[j].querySelector('strong');
1185
+ if (strong && strong.textContent.trim().replace(/:$/, '') === 'User') {
1186
+ // Get the text content after "User:" — everything in the <p> minus the strong
1187
+ var p = msgs[j].querySelector('p');
1188
+ if (p) {
1189
+ var clone = p.cloneNode(true);
1190
+ var s = clone.querySelector('strong');
1191
+ if (s) s.remove();
1192
+ lastUserMsg = clone.textContent.trim();
1193
+ }
1194
+ break;
1195
+ }
1196
+ }
1197
+
1198
+ if (lastUserMsg) {
1199
+ var sep = document.createTextNode(' · ');
1200
+ undoDiv.appendChild(sep);
1201
+
1202
+ var tryLink = document.createElement('a');
1203
+ tryLink.href = '#';
1204
+ tryLink.textContent = 'try again';
1205
+ tryLink.style.cssText = 'color:var(--accent-primary,#a78bfa);font-size:12px;text-decoration:none;opacity:0.7;';
1206
+ tryLink.addEventListener('mouseenter', function() { tryLink.style.opacity = '1'; tryLink.style.textDecoration = 'underline'; });
1207
+ tryLink.addEventListener('mouseleave', function() { tryLink.style.opacity = '0.7'; tryLink.style.textDecoration = 'none'; });
1208
+
1209
+ tryLink.addEventListener('click', function(e) {
1210
+ e.preventDefault();
1211
+ var overlay = document.getElementById('loadingOverlay');
1212
+ if (overlay) overlay.style.display = 'flex';
1213
+ var ci = document.getElementById('chatInput');
1214
+ if (ci) ci.disabled = true;
1215
+ var sb = document.querySelector('.chat-send-btn');
1216
+ if (sb) sb.disabled = true;
1217
+
1218
+ fetch(window.location.pathname, {
1219
+ method: 'POST',
1220
+ headers: { 'Content-Type': 'application/json' },
1221
+ body: JSON.stringify({ message: lastUserMsg, tryAgain: true })
1222
+ })
1223
+ .then(function(res) { return res.text(); })
1224
+ .then(function(html) {
1225
+ window.__synthOSPageDirty = true;
1226
+ window.__synthOSChatPanel = false;
1227
+ window.__synthOSNavigateTo = null;
1228
+ document.open();
1229
+ document.write(html);
1230
+ document.close();
1231
+ })
1232
+ .catch(function(err) {
1233
+ console.error('Try again failed:', err);
1234
+ if (overlay) overlay.style.display = 'none';
1235
+ if (ci) ci.disabled = false;
1236
+ if (sb) sb.disabled = false;
1237
+ });
1238
+ });
1239
+
1240
+ undoDiv.appendChild(tryLink);
1241
+ }
1242
+
1243
+ cm.appendChild(undoDiv);
1244
+ })();
1245
+
1246
+ // 10. Unsaved-changes prompt for required pages (FluentLM dialog)
1247
+ (function() {
1248
+ // Create the unsaved-changes confirmation dialog
1249
+ var overlay = document.createElement('div');
1250
+ overlay.className = 'flm-dialog-overlay';
1251
+ overlay.id = 'synthos-unsavedDialog';
1252
+ overlay.innerHTML =
1253
+ '<div class="flm-dialog">' +
1254
+ '<div class="flm-dialog-header">' +
1255
+ '<h2 class="flm-dialog-title">Unsaved Changes</h2>' +
1256
+ '<button class="flm-dialog-close" data-icon="Cancel" aria-label="Close" data-dialog-close></button>' +
1257
+ '</div>' +
1258
+ '<div class="flm-dialog-body">You have unsaved changes that will be lost if you leave this page.</div>' +
1259
+ '<div class="flm-dialog-footer">' +
1260
+ '<button class="flm-button" data-dialog-close id="synthos-unsavedStay">Stay</button>' +
1261
+ '<button class="flm-button flm-button--primary" id="synthos-unsavedLeave">Leave</button>' +
1262
+ '</div>' +
1263
+ '</div>';
1264
+ document.body.appendChild(overlay);
1265
+
1266
+ var pendingUrl = null;
1267
+
1268
+ document.getElementById('synthos-unsavedLeave').addEventListener('click', function() {
1269
+ overlay.classList.remove('is-open');
1270
+ if (pendingUrl) {
1271
+ window.__synthOSPageDirty = false;
1272
+ window.location.href = pendingUrl;
1273
+ }
1274
+ });
1275
+
1276
+ // Navigate with unsaved-changes guard
1277
+ window.__synthOSNavigateTo = function(url) {
1278
+ if (window.__synthOSPageDirty && window.pageInfo && window.pageInfo.isRequiredPage) {
1279
+ pendingUrl = url;
1280
+ overlay.classList.add('is-open');
1281
+ } else {
1282
+ window.location.href = url;
1283
+ }
1284
+ };
1285
+ })();
1286
+
1287
+
1288
+ // Initial focus — run after all setup (including chat-collapsed check)
1289
+ if (chatInput && !document.body.classList.contains('chat-collapsed')) {
1290
+ chatInput.focus();
1291
+ }
1292
+ })();