synthos 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) 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 +150 -25
  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 +10 -1
  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 +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
@@ -0,0 +1,970 @@
1
+ <!DOCTYPE html><html lang="en"><head>
2
+ <meta charset="UTF-8">
3
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4
+ <title>SynthOS - Presentation Builder</title>
5
+ <script id="theme-info" src="/api/theme-info.js" data-locked="true"></script>
6
+ <link id="theme-css" rel="stylesheet" href="/api/theme.css" data-locked="true">
7
+ <style id="presentation-styles">
8
+ /* ── Viewer panel override for presentation layout ── */
9
+ .viewer-panel { justify-content: flex-start; align-items: stretch; overflow: visible; }
10
+ /* ── Presentation-specific layout ── */
11
+ .presentation-container { width: 100%; height: 100%; display: flex; flex-direction: column; padding: 10px 15px; position: relative; }
12
+ .presentation-controls { display: flex; justify-content: center; align-items: center; gap: 15px; padding: 10px; background: var(--pres-controlbar-bg); border-radius: var(--roundedCorner6); margin-top: 8px; border: var(--pres-controlbar-border); box-shadow: var(--pres-controlbar-shadow); order: 2; position: relative; }
13
+ .control-btn { padding: 8px 16px; border: none; border-radius: var(--roundedCorner6); background: var(--primaryButtonBackground); color: var(--primaryButtonText); cursor: pointer; font-size: 13px; font-weight: 600; transition: all .2s ease; box-shadow: var(--elevation4); }
14
+ .control-btn:hover { background: var(--primaryButtonBackgroundHovered); transform: translateY(-1px); box-shadow: var(--elevation8); }
15
+ .control-btn:disabled { opacity: var(--pres-btn-disabled-opacity); cursor: not-allowed; transform: none; background: var(--pres-btn-disabled-bg); color: var(--pres-btn-disabled-text); border: 1px solid var(--pres-btn-disabled-border); box-shadow: none; }
16
+ .present-btn { background: var(--themeTertiary); color: var(--white); }
17
+ .present-btn:hover { background: var(--themeDarkAlt); }
18
+ .pause-btn { background: var(--orange); color: var(--white); }
19
+ .pause-btn:hover { background: var(--orangeLight); }
20
+ .slide-counter { color: var(--bodySubtext); font-size: 14px; font-weight: 600; min-width: 80px; text-align: center; }
21
+ .slide-container { flex: 1; display: flex; justify-content: center; align-items: center; overflow: hidden; order: 1; min-height: 0; }
22
+ .slide { width: 100%; height: 100%; max-width: 900px; max-height: calc(100vh - 200px); background: var(--pres-slide-bg); border-radius: var(--pres-slide-radius); border: var(--pres-slide-border); box-shadow: var(--pres-slide-shadow); padding: 30px; display: none; flex-direction: column; justify-content: center; opacity: 0; transform: translateX(50px); transition: opacity .5s ease, transform .5s ease; overflow-y: auto; }
23
+ .slide.active { display: flex; opacity: 1; transform: translateX(0); }
24
+ .slide-content ul { list-style: none; padding-left: 0; }
25
+ .slide-content li { padding: 6px 0 6px 30px; position: relative; opacity: 0; transform: translateX(-20px); animation: slideIn .5s ease forwards; }
26
+ .slide.active .slide-content li:nth-child(1) { animation-delay: .1s; }
27
+ .slide.active .slide-content li:nth-child(2) { animation-delay: .2s; }
28
+ .slide.active .slide-content li:nth-child(3) { animation-delay: .3s; }
29
+ .slide.active .slide-content li:nth-child(4) { animation-delay: .4s; }
30
+ .slide.active .slide-content li:nth-child(5) { animation-delay: .5s; }
31
+ .slide.active .slide-content li:nth-child(6) { animation-delay: .6s; }
32
+ @keyframes slideIn { to { opacity: 1; transform: translateX(0); } }
33
+ @keyframes boxIn { to { opacity: 1; transform: scale(1); } }
34
+ @keyframes stepIn { to { opacity: 1; transform: translateY(0); } }
35
+ .slide.active .quote-block { animation-delay: .3s; }
36
+ @keyframes quoteIn { to { opacity: 1; } }
37
+ .slide.active .feature-box:nth-child(1) { animation-delay: .2s; }
38
+ .slide.active .feature-box:nth-child(2) { animation-delay: .3s; }
39
+ .slide.active .feature-box:nth-child(3) { animation-delay: .4s; }
40
+ .slide.active .feature-box:nth-child(4) { animation-delay: .5s; }
41
+ @keyframes fadeIn { to { opacity: 1; } }
42
+ .slide.active .final-cta { animation-delay: .5s; }
43
+ @keyframes finalIn { to { opacity: 1; } }
44
+ .slide.active .network-box:nth-child(1) { animation-delay: .2s; }
45
+ .slide.active .network-box:nth-child(2) { animation-delay: .4s; }
46
+ .slide.active .network-box:nth-child(3) { animation-delay: .6s; }
47
+ .slide.active .network-box:nth-child(4) { animation-delay: .8s; }
48
+ .slide.active .step:nth-child(1) { animation-delay: .2s; }
49
+ .slide.active .step:nth-child(2) { animation-delay: .4s; }
50
+ .slide.active .step:nth-child(3) { animation-delay: .6s; }
51
+ .slide.active .step:nth-child(4) { animation-delay: .8s; }
52
+ /* ── TTS indicator ── */
53
+ .tts-indicator { display: none; position: fixed; top: 20px; right: 20px; z-index: 10001; align-items: center; gap: 8px; padding: 8px 16px; background: var(--defaultStateBackground); border: 1px solid var(--neutralLight); border-radius: 25px; font-size: 13px; color: var(--themeTertiary); box-shadow: var(--elevation8); }
54
+ .tts-indicator.active { display: flex; }
55
+ .tts-wave { display: flex; gap: 3px; align-items: center; }
56
+ .tts-wave span { width: 3px; height: 14px; background: var(--themeTertiary); border-radius: 2px; animation: wave 1s ease-in-out infinite; }
57
+ .tts-wave span:nth-child(2) { animation-delay: .1s; }
58
+ .tts-wave span:nth-child(3) { animation-delay: .2s; }
59
+ .tts-wave span:nth-child(4) { animation-delay: .3s; }
60
+ @keyframes wave { 0%, 100% { height: 6px; } 50% { height: 14px; } }
61
+ /* ── Fullscreen mode ── */
62
+ .fullscreen-mode { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999; background: var(--bodyBackground); }
63
+ .fullscreen-mode .presentation-controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(0); z-index: 10000; margin-top: 0; transition: transform .4s ease, opacity .4s ease; }
64
+ .fullscreen-mode .presentation-controls.controls-hidden { transform: translateX(-50%) translateY(calc(100% + 30px)); opacity: 0; pointer-events: none; }
65
+ .fullscreen-mode .slide-container { width: 100%; height: 100%; padding: 0; }
66
+ .fullscreen-mode .slide { width: 100%; height: 100%; max-width: none; max-height: none; box-shadow: none; border: none; border-radius: 0; padding: 60px 80px 100px; }
67
+ .fullscreen-mode .slide-title { font-size: 44px; }
68
+ .fullscreen-mode .slide-content { font-size: 22px; }
69
+ /* ── Voice picker popup ── */
70
+ .voice-picker { display: none; position: absolute; bottom: calc(100% + 8px); right: 0; min-width: 260px; max-width: 320px; background: var(--menuBackground); border: 1px solid var(--neutralLight); border-radius: var(--roundedCorner6); box-shadow: var(--elevation16); z-index: 100; overflow: hidden; }
71
+ .voice-picker.open { display: block; }
72
+ .voice-picker-header { padding: 10px 14px; font-size: 12px; font-weight: 600; color: var(--bodySubtext); text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid var(--neutralLight); }
73
+ .voice-picker-list { max-height: 240px; overflow-y: auto; padding: 4px 0; }
74
+ .voice-picker-item { display: flex; align-items: center; gap: 10px; padding: 8px 14px; cursor: pointer; transition: background .15s; font-size: 13px; color: var(--bodyText); }
75
+ .voice-picker-item:hover { background: var(--menuItemBackgroundHovered); }
76
+ .voice-picker-item.selected { background: var(--themeLighter); font-weight: 600; }
77
+ .voice-picker-item.selected::before { content: '\2713'; color: var(--themePrimary); font-weight: 700; margin-right: 2px; }
78
+ .voice-picker-item-name { flex: 1; }
79
+ .voice-picker-item-meta { font-size: 11px; color: var(--bodySubtext); }
80
+ .voice-picker-empty { padding: 16px 14px; text-align: center; font-size: 13px; color: var(--bodySubtext); }
81
+ .voice-picker-empty a { color: var(--link); text-decoration: none; }
82
+ .voice-picker-empty a:hover { color: var(--linkHovered); text-decoration: underline; }
83
+ .voice-picker-footer { padding: 6px 14px 8px; border-top: 1px solid var(--neutralLight); display: flex; justify-content: flex-end; }
84
+ .voice-picker-clear { background: none; border: none; font-size: 12px; color: var(--bodySubtext); cursor: pointer; padding: 4px 8px; border-radius: var(--roundedCorner4); }
85
+ .voice-picker-clear:hover { background: var(--menuItemBackgroundHovered); color: var(--bodyText); }
86
+ /* ── Speaker Notes Modal ── */
87
+ .notes-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,.45); z-index: 20000; justify-content: center; align-items: center; }
88
+ .notes-modal-overlay.open { display: flex; }
89
+ .notes-modal { background: var(--menuBackground); border-radius: var(--roundedCorner6); box-shadow: var(--elevation16); width: 520px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; }
90
+ .notes-modal-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: var(--bodyText); border-bottom: 1px solid var(--neutralLight); }
91
+ .notes-modal-body { padding: 16px 20px; flex: 1; overflow: auto; }
92
+ .notes-modal-body textarea { width: 100%; min-height: 180px; resize: vertical; border: 1px solid var(--neutralLight); border-radius: var(--roundedCorner4); padding: 10px; font-size: 14px; font-family: inherit; background: var(--inputBackground); color: var(--bodyText); }
93
+ .notes-modal-body textarea:focus { outline: none; border-color: var(--themePrimary); }
94
+ .notes-modal-footer { padding: 12px 20px; border-top: 1px solid var(--neutralLight); display: flex; justify-content: flex-end; gap: 8px; }
95
+ .notes-modal-footer .flm-button { padding: 6px 16px; border: 1px solid var(--neutralLight); border-radius: var(--roundedCorner4); background: var(--defaultStateBackground); color: var(--bodyText); cursor: pointer; font-size: 13px; font-weight: 600; }
96
+ .notes-modal-footer .flm-button:hover { background: var(--menuItemBackgroundHovered); }
97
+ .notes-modal-footer .flm-button--primary { background: var(--primaryButtonBackground); color: var(--primaryButtonText); border-color: transparent; }
98
+ .notes-modal-footer .flm-button--primary:hover { background: var(--primaryButtonBackgroundHovered); }
99
+ /* ── Presentation CSS Classes ── */
100
+ /* Callout boxes (shared structure) */
101
+ .pres-note, .pres-example, .pres-tip, .pres-warning, .pres-quote { padding: 12px 16px; border-radius: var(--pres-card-radius); border-left: 4px solid; margin: 12px 0; }
102
+ .pres-note { background: var(--pres-note-bg); border-left-color: var(--pres-note-accent); color: var(--pres-note-text); }
103
+ .pres-example { background: var(--pres-example-bg); border-left-color: var(--pres-example-accent); color: var(--pres-example-text); }
104
+ .pres-example strong { color: var(--pres-example-accent); }
105
+ .pres-tip { background: var(--pres-tip-bg); border-left-color: var(--pres-tip-accent); color: var(--pres-tip-text); }
106
+ .pres-tip strong { color: var(--pres-tip-accent); }
107
+ .pres-warning { background: var(--pres-warning-bg); border-left-color: var(--pres-warning-accent); color: var(--pres-warning-text); }
108
+ .pres-warning strong { color: var(--pres-warning-accent); }
109
+ .pres-quote { background: var(--pres-quote-bg); border-left-color: var(--pres-quote-accent); color: var(--pres-quote-text); font-style: italic; }
110
+ /* Content cards */
111
+ .pres-card { padding: 16px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); border: var(--pres-card-border); box-shadow: var(--pres-card-shadow); }
112
+ .pres-card-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px; }
113
+ .pres-card-title { font-weight: 600; color: var(--pres-title-color); margin-bottom: 4px; }
114
+ .pres-card-desc { font-size: 13px; color: var(--pres-muted-color); }
115
+ .pres-card-icon { font-size: 24px; margin-bottom: 8px; }
116
+ /* Steps / Process */
117
+ .pres-steps { display: flex; justify-content: center; gap: 16px; margin-top: 16px; flex-wrap: wrap; }
118
+ .pres-step { padding: 16px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); text-align: center; flex: 1; max-width: 180px; border: var(--pres-card-border); opacity: 0; transform: translateY(20px); animation: stepIn .5s var(--pres-ease-enter) forwards; }
119
+ .slide.active .pres-step:nth-child(1) { animation-delay: .2s; }
120
+ .slide.active .pres-step:nth-child(2) { animation-delay: .4s; }
121
+ .slide.active .pres-step:nth-child(3) { animation-delay: .6s; }
122
+ .slide.active .pres-step:nth-child(4) { animation-delay: .8s; }
123
+ .pres-step-number { font-size: 24px; font-weight: 700; color: var(--pres-highlight-color); margin-bottom: 8px; }
124
+ .pres-step-title { font-weight: 600; color: var(--pres-title-color); margin-bottom: 4px; }
125
+ .pres-step-desc { font-size: 12px; color: var(--pres-muted-color); }
126
+ /* Code */
127
+ .pres-code { background: var(--defaultStateBackground); border: 1px solid var(--neutralLight); border-radius: var(--pres-card-radius); padding: 12px; font-family: var(--fontFamilyMonospace, 'Courier New', monospace); font-size: 13px; color: var(--pres-code-color); margin-top: 12px; overflow-x: auto; }
128
+ .pres-code-keyword { color: var(--pres-code-keyword); }
129
+ .pres-code-string { color: var(--pres-code-string); }
130
+ /* Data / Charts */
131
+ .pres-chart { --chart-1: var(--pres-chart-1); --chart-2: var(--pres-chart-2); --chart-3: var(--pres-chart-3); --chart-4: var(--pres-chart-4); --chart-5: var(--pres-chart-5); --chart-6: var(--pres-chart-6); }
132
+ .pres-table { width: 100%; border-collapse: collapse; margin-top: 12px; }
133
+ .pres-table th { background: var(--pres-card-bg); color: var(--pres-title-color); font-weight: 600; padding: 10px 12px; text-align: left; border-bottom: 2px solid var(--pres-highlight-color); }
134
+ .pres-table td { padding: 8px 12px; color: var(--pres-body-color); border-bottom: 1px solid var(--neutralLight); }
135
+ .pres-table tr:nth-child(even) td { background: var(--pres-card-bg); }
136
+ .pres-table tr:nth-child(odd) td { background: transparent; }
137
+ .pres-stat { text-align: center; padding: 16px; }
138
+ .pres-stat-value { font-size: 36px; font-weight: 700; color: var(--pres-title-color); }
139
+ .pres-stat-label { font-size: 13px; color: var(--pres-muted-color); margin-top: 4px; }
140
+ /* Layout utilities */
141
+ .pres-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
142
+ .pres-three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 24px; }
143
+ .pres-center { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
144
+ .pres-spacer { height: 16px; }
145
+ /* Animation classes (only activate on .active slides) */
146
+ .pres-fade-in { opacity: 0; }
147
+ .slide.active .pres-fade-in { animation: fadeIn var(--pres-duration-normal) var(--pres-ease-enter) forwards; }
148
+ .pres-slide-in { opacity: 0; transform: translateX(-20px); }
149
+ .slide.active .pres-slide-in { animation: slideIn var(--pres-duration-normal) var(--pres-ease-enter) forwards; }
150
+ .pres-scale-in { opacity: 0; transform: scale(.9); }
151
+ .slide.active .pres-scale-in { animation: boxIn var(--pres-duration-normal) var(--pres-ease-enter) forwards; }
152
+ .pres-step-in { opacity: 0; transform: translateY(20px); }
153
+ .slide.active .pres-step-in { animation: stepIn var(--pres-duration-normal) var(--pres-ease-enter) forwards; }
154
+ .pres-stagger > *:nth-child(1) { animation-delay: calc(var(--pres-stagger-delay) * 1); }
155
+ .pres-stagger > *:nth-child(2) { animation-delay: calc(var(--pres-stagger-delay) * 2); }
156
+ .pres-stagger > *:nth-child(3) { animation-delay: calc(var(--pres-stagger-delay) * 3); }
157
+ .pres-stagger > *:nth-child(4) { animation-delay: calc(var(--pres-stagger-delay) * 4); }
158
+ .pres-stagger > *:nth-child(5) { animation-delay: calc(var(--pres-stagger-delay) * 5); }
159
+ .pres-stagger > *:nth-child(6) { animation-delay: calc(var(--pres-stagger-delay) * 6); }
160
+ /* ── Old class aliases (mapped to new tokens) ── */
161
+ .tip-box { background: var(--pres-tip-bg); border: none; border-left: 4px solid var(--pres-tip-accent); border-radius: var(--pres-card-radius); padding: 12px 16px; margin: 12px 0; color: var(--pres-tip-text); }
162
+ .tip-box strong { color: var(--pres-tip-accent); }
163
+ .quote-block { background: var(--pres-quote-bg); border-left: 3px solid var(--pres-quote-accent); border-radius: 0 var(--pres-card-radius) var(--pres-card-radius) 0; padding: 12px 20px; margin: 12px 0; font-style: italic; color: var(--pres-quote-text); opacity: 0; animation: quoteIn .6s var(--pres-ease-enter) forwards; }
164
+ .feature-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px; }
165
+ .feature-box { padding: 16px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); border: var(--pres-card-border); opacity: 0; animation: fadeIn .5s var(--pres-ease-enter) forwards; }
166
+ .feature-icon { font-size: 24px; margin-bottom: 8px; }
167
+ .feature-title { font-weight: 600; color: var(--pres-title-color); margin-bottom: 4px; }
168
+ .feature-desc { font-size: 13px; color: var(--pres-muted-color); }
169
+ .steps-container { display: flex; justify-content: center; gap: 16px; margin-top: 16px; flex-wrap: wrap; }
170
+ .step { padding: 16px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); text-align: center; flex: 1; max-width: 180px; border: var(--pres-card-border); opacity: 0; transform: translateY(20px); animation: stepIn .5s var(--pres-ease-enter) forwards; }
171
+ .step-number { font-size: 24px; font-weight: 700; color: var(--pres-highlight-color); margin-bottom: 8px; }
172
+ .step-title { font-weight: 600; color: var(--pres-title-color); margin-bottom: 4px; }
173
+ .step-desc { font-size: 12px; color: var(--pres-muted-color); }
174
+ .network-boxes { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
175
+ .network-box { padding: 12px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); border: var(--pres-card-border); text-align: center; opacity: 0; transform: scale(.9); animation: boxIn .4s var(--pres-ease-enter) forwards; }
176
+ .network-box-title { font-weight: 600; color: var(--pres-title-color); margin-bottom: 4px; }
177
+ .network-box-desc { font-size: 13px; color: var(--pres-muted-color); }
178
+ .slide-example { margin-top: 16px; padding: 12px 16px; background: var(--pres-example-bg); border-radius: var(--pres-card-radius); border-left: 4px solid var(--pres-example-accent); font-style: italic; color: var(--pres-example-text); }
179
+ .code-block { background: var(--defaultStateBackground); border: 1px solid var(--neutralLight); border-radius: var(--pres-card-radius); padding: 12px; font-family: var(--fontFamilyMonospace, 'Courier New', monospace); font-size: 13px; color: var(--pres-code-color); margin-top: 12px; overflow-x: auto; }
180
+ .code-keyword { color: var(--pres-code-keyword); }
181
+ .code-string { color: var(--pres-code-string); }
182
+ .final-cta { margin-top: 20px; padding: 20px; background: var(--pres-card-bg); border-radius: var(--pres-card-radius); border: var(--pres-card-border); text-align: center; opacity: 0; animation: finalIn .8s var(--pres-ease-enter) forwards; }
183
+ .final-cta p { font-size: 15px; color: var(--pres-body-color); line-height: 1.6; }
184
+ .final-cta p:first-child { color: var(--pres-highlight-color); font-weight: 600; margin-bottom: 8px; }
185
+ .two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
186
+ .column-title { font-size: 15px; font-weight: 600; color: var(--pres-highlight-color); margin-bottom: 8px; }
187
+ .slide-title { font-size: 28px; font-weight: 700; color: var(--pres-title-color); margin-bottom: 20px; text-align: center; }
188
+ .slide-subtitle { font-size: 16px; color: var(--pres-subtitle-color); text-align: center; margin-bottom: 16px; font-style: italic; }
189
+ .slide-content { font-size: 16px; line-height: 1.8; color: var(--pres-body-color); }
190
+ .highlight { color: var(--pres-highlight-color); font-weight: 600; }
191
+ .slide-content li::before { content: '\25B8'; position: absolute; left: 0; color: var(--pres-highlight-color); font-size: 16px; }
192
+ /* ── Presentation Design Tokens (light defaults) ── */
193
+ :root {
194
+ /* Surface & Depth */
195
+ --pres-slide-bg: var(--defaultStateBackground);
196
+ --pres-slide-border: none;
197
+ --pres-slide-shadow: var(--elevation16);
198
+ --pres-slide-glow: transparent;
199
+ --pres-slide-radius: var(--roundedCorner6);
200
+ --pres-controlbar-bg: var(--defaultStateBackground);
201
+ --pres-controlbar-border: 1px solid var(--neutralLight);
202
+ --pres-controlbar-shadow: var(--elevation4);
203
+ /* Card / Box */
204
+ --pres-card-bg: var(--themeLighter);
205
+ --pres-card-border: 1px solid var(--neutralLight);
206
+ --pres-card-shadow: none;
207
+ --pres-card-radius: var(--roundedCorner6);
208
+ /* Callout — Note */
209
+ --pres-note-bg: var(--themeLighterAlt);
210
+ --pres-note-accent: var(--themeTertiary);
211
+ --pres-note-text: var(--bodySubtext);
212
+ /* Callout — Example */
213
+ --pres-example-bg: var(--themeLighterAlt);
214
+ --pres-example-accent: var(--themePrimary);
215
+ --pres-example-text: var(--bodySubtext);
216
+ /* Callout — Tip */
217
+ --pres-tip-bg: var(--successBackground);
218
+ --pres-tip-accent: var(--green);
219
+ --pres-tip-text: var(--bodySubtext);
220
+ /* Callout — Warning */
221
+ --pres-warning-bg: var(--warningBackground);
222
+ --pres-warning-accent: var(--orange);
223
+ --pres-warning-text: var(--bodySubtext);
224
+ /* Callout — Quote */
225
+ --pres-quote-bg: var(--themeLighterAlt);
226
+ --pres-quote-accent: var(--themeTertiary);
227
+ --pres-quote-text: var(--bodySubtext);
228
+ /* Disabled State */
229
+ --pres-btn-disabled-bg: var(--themeLighter);
230
+ --pres-btn-disabled-text: var(--themeTertiary);
231
+ --pres-btn-disabled-border: var(--themeLight);
232
+ --pres-btn-disabled-opacity: 1;
233
+ /* Typography */
234
+ --pres-title-color: var(--themePrimary);
235
+ --pres-subtitle-color: var(--bodySubtext);
236
+ --pres-body-color: var(--bodyText);
237
+ --pres-highlight-color: var(--themeTertiary);
238
+ --pres-muted-color: var(--bodySubtext);
239
+ --pres-code-color: var(--bodySubtext);
240
+ --pres-code-keyword: var(--themeTertiary);
241
+ --pres-code-string: var(--green);
242
+ /* Animation & Motion */
243
+ --pres-ease-enter: var(--easeDecelerate, cubic-bezier(0, 0, 0, 1));
244
+ --pres-ease-exit: var(--easeAccelerate, cubic-bezier(1, 0, 1, 1));
245
+ --pres-ease-move: var(--easeStandard, cubic-bezier(0.8, 0, 0.2, 1));
246
+ --pres-ease-subtle: ease;
247
+ --pres-duration-fast: var(--duration1, 100ms);
248
+ --pres-duration-normal: var(--duration3, 300ms);
249
+ --pres-duration-slow: var(--duration4, 400ms);
250
+ --pres-stagger-delay: 100ms;
251
+ /* Chart & Data Viz */
252
+ --pres-chart-1: var(--themePrimary);
253
+ --pres-chart-2: var(--themeTertiary);
254
+ --pres-chart-3: var(--themeSecondary);
255
+ --pres-chart-4: var(--orange);
256
+ --pres-chart-5: var(--green);
257
+ --pres-chart-6: var(--red);
258
+ --pres-chart-axis: var(--bodySubtext);
259
+ --pres-chart-grid: var(--neutralLight);
260
+ --pres-chart-label: var(--bodyText);
261
+ }
262
+ /* ── Presentation Design Tokens (dark overrides) ── */
263
+ .theme-dark {
264
+ /* Surface & Depth — stronger brand border + dramatic glow (à la Star Trek deck) */
265
+ --pres-slide-border: 1px solid rgba(from var(--themePrimary) r g b / 0.30);
266
+ --pres-slide-shadow: 0 10px 40px rgba(0,0,0,.5), 0 0 30px rgba(from var(--themePrimary) r g b / 0.20);
267
+ --pres-slide-glow: rgba(from var(--themePrimary) r g b / 0.20);
268
+ --pres-slide-radius: 20px;
269
+ --pres-controlbar-bg: rgba(15, 15, 25, 0.80);
270
+ --pres-controlbar-border: 1px solid rgba(from var(--themePrimary) r g b / 0.30);
271
+ --pres-controlbar-shadow: 0 4px 20px rgba(0,0,0,.4);
272
+ /* Card / Box — gradient brand tint for depth */
273
+ --pres-card-bg: linear-gradient(135deg, rgba(from var(--themePrimary) r g b / 0.15) 0%, rgba(from var(--themePrimary) r g b / 0.08) 100%);
274
+ --pres-card-border: 1px solid rgba(from var(--themePrimary) r g b / 0.30);
275
+ --pres-card-shadow: 0 2px 12px rgba(0,0,0,.3), 0 0 8px rgba(from var(--themePrimary) r g b / 0.05);
276
+ --pres-card-radius: 12px;
277
+ /* Callout backgrounds — translucent tints matching accent */
278
+ --pres-note-bg: rgba(from var(--themeTertiary) r g b / 0.12);
279
+ --pres-note-text: var(--bodyText);
280
+ --pres-example-bg: rgba(from var(--themePrimary) r g b / 0.12);
281
+ --pres-example-text: var(--bodyText);
282
+ --pres-tip-bg: rgba(from var(--green) r g b / 0.12);
283
+ --pres-tip-text: var(--bodyText);
284
+ --pres-warning-bg: rgba(from var(--orange) r g b / 0.12);
285
+ --pres-warning-text: var(--bodyText);
286
+ --pres-quote-bg: rgba(from var(--themePrimary) r g b / 0.10);
287
+ --pres-quote-text: var(--bodyText);
288
+ /* Disabled State */
289
+ --pres-btn-disabled-bg: var(--themeDarker);
290
+ --pres-btn-disabled-text: var(--themeTertiary);
291
+ --pres-btn-disabled-border: var(--themeDark);
292
+ /* Chart (brighter accents for dark bg) */
293
+ --pres-chart-4: var(--orangeLight);
294
+ --pres-chart-5: var(--greenLight);
295
+ --pres-chart-6: var(--redLight);
296
+ --pres-chart-grid: var(--neutralSecondary);
297
+ }
298
+ </style>
299
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js"></script>
300
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
301
+ <script id="page-info" src="/api/page-info.js?page=presentation-builder"></script>
302
+ </head>
303
+ <body>
304
+ <div class="viewer-panel" id="viewerPanel" data-locked="true">
305
+ <div class="tts-indicator" id="ttsIndicator" data-locked="true">
306
+ <div class="tts-wave"><span></span><span></span><span></span><span></span></div>
307
+ <span>Speaking...</span>
308
+ </div>
309
+
310
+ <div class="presentation-container" id="presentationContainer">
311
+ <div class="slide-container">
312
+ <div class="slide active" id="slide1" data-notes="Welcome to Presentation Builder! This quick tutorial will show you how to create stunning presentations in minutes. Just describe your topic in the chat, and I'll generate a complete deck with animations, speaker notes, and voice narration. Let's get started!">
313
+ <div class="slide-title">Presentation Builder</div>
314
+ <div class="slide-subtitle">Create Beautiful Presentations with AI</div>
315
+ <div class="slide-content" style="text-align:center;margin-top:16px">
316
+ <div class="quote-block">"Describe your topic. Get a stunning presentation."</div>
317
+ <p style="margin-top:16px;font-size:13px;color:var(--bodySubtext)">A Quick Tutorial</p>
318
+ </div>
319
+ </div>
320
+ <div class="slide" id="slide2" data-notes="Using the builder is simple. Type your presentation topic in the chat panel on the left. Be as specific as you like. Include your target audience, the number of slides you want, or any specific points to cover. The more detail you provide, the better your presentation will be.">
321
+ <div class="slide-title">How It Works</div>
322
+ <div class="slide-content">
323
+ <div class="steps-container">
324
+ <div class="step"><div class="step-number">1</div><div class="step-title">Describe</div><div class="step-desc">Type your topic in the chat</div></div>
325
+ <div class="step"><div class="step-number">2</div><div class="step-title">Generate</div><div class="step-desc">AI creates your deck</div></div>
326
+ <div class="step"><div class="step-number">3</div><div class="step-title">Present</div><div class="step-desc">Click Present to go fullscreen</div></div>
327
+ <div class="step"><div class="step-number">4</div><div class="step-title">Refine</div><div class="step-desc">Ask for changes anytime</div></div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ <div class="slide" id="slide3" data-notes="Every presentation comes packed with features. Smooth animations bring your content to life. Speaker notes are generated for each slide to guide your delivery. Text-to-speech narration reads your notes aloud in presentation mode. And keyboard shortcuts let you navigate effortlessly.">
332
+ <div class="slide-title">Built-In Features</div>
333
+ <div class="slide-content">
334
+ <div class="feature-grid">
335
+ <div class="feature-box"><div class="feature-icon">&#x2728;</div><div class="feature-title">Smooth Animations</div><div class="feature-desc">Content animates in beautifully</div></div>
336
+ <div class="feature-box"><div class="feature-icon">&#x1F4DD;</div><div class="feature-title">Speaker Notes</div><div class="feature-desc">Detailed notes for each slide</div></div>
337
+ <div class="feature-box"><div class="feature-icon">&#x1F50A;</div><div class="feature-title">Voice Narration</div><div class="feature-desc">TTS reads notes in present mode</div></div>
338
+ <div class="feature-box"><div class="feature-icon">&#x2328;</div><div class="feature-title">Keyboard Shortcuts</div><div class="feature-desc">Arrow keys, Space, F, Escape</div></div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ <div class="slide" id="slide4" data-notes="Here are some tips for great prompts. Mention your audience so the tone matches. Specify the number of slides if you have a preference. Include key points you want covered. Ask for comparisons, pros and cons, or specific visualizations. The more context you give, the better the result.">
343
+ <div class="slide-title">Tips for Great Prompts</div>
344
+ <div class="slide-content">
345
+ <ul>
346
+ <li>Mention your <span class="highlight">target audience</span></li>
347
+ <li>Specify the <span class="highlight">number of slides</span> if needed</li>
348
+ <li>Include <span class="highlight">key points</span> to cover</li>
349
+ <li>Request specific <span class="highlight">visualizations</span></li>
350
+ <li>Ask for <span class="highlight">comparisons</span> or analysis</li>
351
+ </ul>
352
+ <div class="pres-example"><strong>Example:</strong> "Create a 10-slide presentation on machine learning for business executives, covering ROI, use cases, and implementation challenges."</div>
353
+ </div>
354
+ </div>
355
+ <div class="slide" id="slide5" data-notes="You're all set! Head back to the chat and describe the presentation you want to create. I'll generate a complete deck with all the styling, animations, and features you've just seen. Let's build something amazing together!">
356
+ <div class="slide-title">Ready to Build!</div>
357
+ <div class="slide-content" style="text-align:center">
358
+ <div class="quote-block">"The best presentations tell a story. Let's tell yours."</div>
359
+ <div class="final-cta">
360
+ <p>Head to the chat panel</p>
361
+ <p><em>Describe your presentation topic and watch the magic happen!</em></p>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <div class="presentation-controls" id="controls" data-locked="true">
368
+ <button class="control-btn" id="prevBtn" disabled>&#x2190; Previous</button>
369
+ <span class="slide-counter" id="slideCounter">1 / 5</span>
370
+ <button class="control-btn" id="nextBtn">Next &#x2192;</button>
371
+ <button class="control-btn pause-btn" id="pauseBtn" style="display:none">&#x23F8; Pause</button>
372
+ <button class="control-btn present-btn" id="presentBtn">&#x1F5A5; Present</button>
373
+ <button class="control-btn" id="notesBtn" title="Speaker Notes">&#x1F4DD; Notes</button>
374
+ <button class="control-btn" id="voiceSettingsBtn" title="Voice Settings"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg></button>
375
+
376
+ <!-- Voice picker popup -->
377
+ <div class="voice-picker" id="voicePicker">
378
+ <div class="voice-picker-header">Voice</div>
379
+ <div class="voice-picker-list" id="voicePickerList">
380
+ <div class="voice-picker-empty">Loading voices...</div>
381
+ </div>
382
+ <div class="voice-picker-footer">
383
+ <button class="voice-picker-clear" id="voicePickerClearBtn">Disable TTS</button>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ <!-- Speaker Notes Modal -->
390
+ <div class="notes-modal-overlay" id="notesModalOverlay" data-light-dismiss>
391
+ <div class="notes-modal" id="notesModal">
392
+ <div class="notes-modal-header" id="notesModalTitle">Speaker Notes — Slide 1</div>
393
+ <div class="notes-modal-body">
394
+ <textarea id="notesTextarea" placeholder="Enter speaker notes for this slide..."></textarea>
395
+ </div>
396
+ <div class="notes-modal-footer">
397
+ <button class="flm-button" id="notesCancelBtn">Cancel</button>
398
+ <button class="flm-button flm-button--primary" id="notesSaveBtn">Save</button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+
403
+ </div>
404
+
405
+ <div id="instructions" style="display: none;" data-locked="true">You are the Presentation Builder assistant. When the user describes a topic, generate a full slide deck as HTML.
406
+
407
+ IMPORTANT RULES:
408
+ 1. Replace ALL slide content inside .slide-container ONLY. Remove existing slides and create new ones. Do NOT touch or remove the .presentation-controls div — it contains the navigation buttons (Previous, Next, Present, Voice, Pause) and must remain intact.
409
+ 2. Each slide must follow this structure:
410
+ <div class="slide" id="slideN" data-notes="Speaker notes for TTS narration...">
411
+ <div class="slide-title">Title</div>
412
+ <div class="slide-content">...content...</div>
413
+ </div>
414
+ 3. The FIRST slide must have class "slide active", all others just "slide".
415
+ 4. Slide IDs must be sequential: slide1, slide2, slide3, etc.
416
+ 5. Every slide MUST have a data-notes attribute with 2-3 sentences of speaker notes.
417
+ 6. After the slides, update the slide counter: document.getElementById('slideCounter').textContent = '1 / N';
418
+ 7. Also update the JS variable: totalSlides = N; currentSlide = 1;
419
+
420
+ AVAILABLE CSS CLASSES for slide content:
421
+
422
+ CALLOUTS: .pres-note, .pres-example, .pres-tip, .pres-warning, .pres-quote
423
+ CARDS: .pres-card, .pres-card-grid, .pres-card-title, .pres-card-desc, .pres-card-icon
424
+ STEPS: .pres-steps, .pres-step, .pres-step-number, .pres-step-title, .pres-step-desc
425
+ CODE: .pres-code, .pres-code-keyword, .pres-code-string
426
+ DATA: .pres-chart, .pres-table, .pres-stat, .pres-stat-value, .pres-stat-label
427
+ LAYOUT: .pres-two-col, .pres-three-col, .pres-center, .pres-spacer
428
+ ANIMATION: .pres-fade-in, .pres-slide-in, .pres-scale-in, .pres-step-in, .pres-stagger
429
+ TYPOGRAPHY: .highlight (inline emphasis)
430
+
431
+ STRUCTURE:
432
+ - .slide-title — themed heading
433
+ - .slide-subtitle — italic subheading
434
+ - .slide-content — main content area
435
+ - .final-cta — call-to-action box
436
+ - ul/li — animated bullet list (auto-animated in active slide)
437
+
438
+ LEGACY (still work but prefer new names above):
439
+ - .tip-box → .pres-tip
440
+ - .quote-block → .pres-quote
441
+ - .feature-grid/.feature-box → .pres-card-grid/.pres-card
442
+ - .steps-container/.step → .pres-steps/.pres-step
443
+ - .network-boxes/.network-box → .pres-card-grid/.pres-card
444
+ - .two-column → .pres-two-col
445
+ - .code-block → .pres-code
446
+ - .slide-example → .pres-example
447
+
448
+ All styles are theme-aware. Do not use inline styles for colors or backgrounds.
449
+
450
+ CONTENT TIPS:
451
+ - Use 5-10 slides for most topics
452
+ - Mix different layouts across slides for visual variety
453
+ - Include a title slide and a conclusion/CTA slide
454
+ - Make speaker notes conversational — they will be read aloud by TTS
455
+ - Use emojis in feature-icon divs for visual appeal
456
+ - Keep bullet points concise (under 10 words each)
457
+
458
+ Use theme tokens (avoid the natural pallet) unless the user requests an alternative color.</div>
459
+ <div id="thoughts" style="display: none;" data-locked="true"></div>
460
+
461
+ <script id="presentation-logic">
462
+ /* ── Dark mode detection ── */
463
+ if (window.__themeInfo?.isDark) document.documentElement.classList.add('theme-dark');
464
+
465
+ /* ── State ── */
466
+ const MIN_SLIDE_DURATION = 10000;
467
+ const TRANSITION_PAUSE = 2000;
468
+ const TTS_TIMEOUT = 30000; // 30s max wait for TTS generation
469
+ let currentSlide = 1;
470
+ let totalSlides = 5;
471
+ let isFullscreen = false;
472
+ let isPaused = false;
473
+ let audioElement = new Audio();
474
+ let audioSlideIdx = -1;
475
+ let resolveAudioPlay = null;
476
+ let readSlides = new Set();
477
+ let audioCache = {};
478
+ let slideStartTime = null;
479
+ let advanceGeneration = 0;
480
+
481
+ /* ── Voice settings state ── */
482
+ let selectedVoiceId = '';
483
+ let selectedVoiceName = '';
484
+ let selectedModel = 'eleven_turbo_v2_5';
485
+ let selectedStability = 0.5;
486
+ let selectedSimilarity = 0.75;
487
+ let selectedStyle = 0;
488
+ let configuredVoices = [];
489
+
490
+ const DATA_TABLE = 'presentation_voice';
491
+ const DATA_ID = 'voice_config';
492
+ const SHARED_VOICES_TABLE = 'elevenlabs_voices';
493
+
494
+ /* ── Focus management ── */
495
+ document.getElementById('viewerPanel').addEventListener('mousedown', function(e) {
496
+ if (e.target.closest('.presentation-controls') || e.target.closest('.voice-picker')) return;
497
+ e.preventDefault();
498
+ });
499
+
500
+ /* ── Utility: timeout-wrapped promise ── */
501
+ function withTimeout(promise, ms) {
502
+ return Promise.race([
503
+ promise,
504
+ new Promise(function(resolve) { setTimeout(function() { resolve(null); }, ms); })
505
+ ]);
506
+ }
507
+
508
+ /* ── TTS via ElevenLabs connector ── */
509
+ async function generateAudio(text) {
510
+ if (!selectedVoiceId) return null;
511
+ try {
512
+ var res = await withTimeout(fetch('/api/connectors', {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({
516
+ connector: 'elevenlabs',
517
+ method: 'POST',
518
+ path: '/v1/text-to-speech/' + selectedVoiceId,
519
+ headers: { 'Accept': 'audio/mpeg' },
520
+ body: {
521
+ text: text,
522
+ model_id: selectedModel,
523
+ voice_settings: {
524
+ stability: selectedStability,
525
+ similarity_boost: selectedSimilarity,
526
+ style: selectedStyle,
527
+ use_speaker_boost: true
528
+ }
529
+ }
530
+ })
531
+ }), TTS_TIMEOUT);
532
+ if (!res || !res.ok) return null;
533
+ var blob = await res.blob();
534
+ if (!blob || blob.size < 1000) return null; // too small = likely error
535
+ return URL.createObjectURL(blob);
536
+ } catch (e) {
537
+ console.error('TTS error:', e);
538
+ return null;
539
+ }
540
+ }
541
+
542
+ function showTTSIndicator(show) {
543
+ document.getElementById('ttsIndicator').classList.toggle('active', show);
544
+ }
545
+
546
+ async function preGenerateNext(idx) {
547
+ var next = idx + 1;
548
+ if (next > totalSlides || audioCache[next]) return;
549
+ var slide = document.getElementById('slide' + next);
550
+ var notes = slide ? slide.dataset.notes : '';
551
+ if (notes) {
552
+ try {
553
+ var url = await generateAudio(notes);
554
+ if (url) audioCache[next] = url;
555
+ } catch (e) { /* ignore pre-gen failures */ }
556
+ }
557
+ }
558
+
559
+ function playAudio(url, gen, slideIdx) {
560
+ return new Promise(resolve => {
561
+ if (gen !== advanceGeneration) { resolve(); return; }
562
+ if (resolveAudioPlay) { resolveAudioPlay(); resolveAudioPlay = null; }
563
+ resolveAudioPlay = resolve;
564
+ audioSlideIdx = slideIdx;
565
+ audioElement.onended = () => { audioSlideIdx = -1; resolveAudioPlay = null; resolve(); };
566
+ audioElement.onerror = () => { audioSlideIdx = -1; resolveAudioPlay = null; resolve(); };
567
+ audioElement.src = url;
568
+ audioElement.play().then(() => {
569
+ if (gen !== advanceGeneration) { audioElement.pause(); resolveAudioPlay = null; resolve(); }
570
+ }).catch(() => { resolveAudioPlay = null; resolve(); });
571
+ });
572
+ }
573
+
574
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
575
+
576
+ async function speakAndAdvance(idx) {
577
+ var gen = advanceGeneration;
578
+ if (!isFullscreen || isPaused || readSlides.has(idx)) return;
579
+ slideStartTime = Date.now();
580
+ var slide = document.getElementById('slide' + idx);
581
+ var notes = slide ? slide.dataset.notes : '';
582
+
583
+ if (notes && selectedVoiceId) {
584
+ showTTSIndicator(true);
585
+
586
+ var audioUrl = audioCache[idx] || null;
587
+ if (!audioUrl) {
588
+ try {
589
+ audioUrl = await generateAudio(notes);
590
+ } catch (e) {
591
+ console.error('TTS generation failed for slide ' + idx, e);
592
+ audioUrl = null;
593
+ }
594
+ }
595
+ if (gen !== advanceGeneration) { showTTSIndicator(false); return; }
596
+
597
+ if (audioUrl) {
598
+ // Start pre-generating next slide audio in background
599
+ preGenerateNext(idx);
600
+ if (currentSlide !== idx) { showTTSIndicator(false); return; }
601
+ await playAudio(audioUrl, gen, idx);
602
+ if (gen !== advanceGeneration) { showTTSIndicator(false); return; }
603
+ if (audioCache[idx]) { URL.revokeObjectURL(audioCache[idx]); delete audioCache[idx]; }
604
+ }
605
+
606
+ showTTSIndicator(false);
607
+ }
608
+
609
+ // Mark slide as read regardless of TTS success
610
+ readSlides.add(idx);
611
+
612
+ // Wait for transition pause
613
+ await sleep(TRANSITION_PAUSE);
614
+ if (gen !== advanceGeneration) return;
615
+
616
+ // Ensure minimum time on slide
617
+ await waitForMinDuration();
618
+ if (gen !== advanceGeneration) return;
619
+
620
+ // Check we're still in presentation mode and not paused
621
+ if (!isPaused && isFullscreen && idx < totalSlides && idx === currentSlide) {
622
+ currentSlide++;
623
+ updateSlideDisplay();
624
+ speakAndAdvance(currentSlide);
625
+ }
626
+ }
627
+
628
+ async function waitForMinDuration() {
629
+ if (!slideStartTime) return;
630
+ var elapsed = Date.now() - slideStartTime;
631
+ var remaining = MIN_SLIDE_DURATION - elapsed;
632
+ if (remaining > 0) await sleep(remaining);
633
+ }
634
+
635
+ /* ── Slide navigation ── */
636
+ function updateSlideDisplay() {
637
+ document.querySelectorAll('.slide').forEach(s => s.classList.remove('active'));
638
+ const el = document.getElementById('slide' + currentSlide);
639
+ if (el) el.classList.add('active');
640
+ document.getElementById('slideCounter').textContent = currentSlide + ' / ' + totalSlides;
641
+ document.getElementById('prevBtn').disabled = currentSlide === 1;
642
+ document.getElementById('nextBtn').disabled = currentSlide === totalSlides;
643
+ }
644
+
645
+ function updateSlide() {
646
+ updateSlideDisplay();
647
+ if (isFullscreen && !readSlides.has(currentSlide)) speakAndAdvance(currentSlide);
648
+ }
649
+
650
+ function stopCurrentAudio() {
651
+ advanceGeneration++;
652
+ audioElement.pause();
653
+ audioElement.onended = null;
654
+ audioElement.onerror = null;
655
+ audioElement.removeAttribute('src');
656
+ try { audioElement.load(); } catch(e) {}
657
+ audioSlideIdx = -1;
658
+ if (resolveAudioPlay) { resolveAudioPlay(); resolveAudioPlay = null; }
659
+ showTTSIndicator(false);
660
+ }
661
+
662
+ function nextSlide() { if (currentSlide < totalSlides) { stopCurrentAudio(); currentSlide++; updateSlide(); } }
663
+ function prevSlide() { if (currentSlide > 1) { stopCurrentAudio(); currentSlide--; updateSlideDisplay(); } }
664
+
665
+ function togglePause() {
666
+ isPaused = !isPaused;
667
+ var btn = document.getElementById('pauseBtn');
668
+ btn.textContent = isPaused ? '\u25B6 Resume' : '\u23F8 Pause';
669
+ if (isPaused) {
670
+ audioElement.pause();
671
+ } else {
672
+ if (audioSlideIdx === currentSlide && audioElement.src) {
673
+ audioElement.play();
674
+ } else if (!readSlides.has(currentSlide)) {
675
+ speakAndAdvance(currentSlide);
676
+ } else if (currentSlide < totalSlides) {
677
+ // If current slide was already read, advance to next
678
+ currentSlide++;
679
+ updateSlideDisplay();
680
+ speakAndAdvance(currentSlide);
681
+ }
682
+ }
683
+ }
684
+
685
+ function toggleFullscreen() {
686
+ var container = document.getElementById('presentationContainer');
687
+ var btn = document.getElementById('presentBtn');
688
+ var pauseBtn = document.getElementById('pauseBtn');
689
+ if (isFullscreen) {
690
+ container.classList.remove('fullscreen-mode');
691
+ btn.textContent = '\u{1F5A5} Present';
692
+ btn.classList.add('present-btn');
693
+ pauseBtn.style.display = 'none';
694
+ isFullscreen = false;
695
+ stopCurrentAudio();
696
+ isPaused = false;
697
+ if (controlsHideTimer) clearTimeout(controlsHideTimer);
698
+ document.getElementById('controls').classList.remove('controls-hidden');
699
+ if (window.synthos && window.synthos.shell && window.synthos.shell.openBuilder) {
700
+ window.synthos.shell.openBuilder();
701
+ }
702
+ } else {
703
+ if (window.synthos && window.synthos.shell && window.synthos.shell.closeBuilder) {
704
+ window.synthos.shell.closeBuilder();
705
+ }
706
+ container.classList.add('fullscreen-mode');
707
+ btn.textContent = '\u2715 Exit';
708
+ btn.classList.remove('present-btn');
709
+ pauseBtn.style.display = 'inline-block';
710
+ isFullscreen = true;
711
+ isPaused = false;
712
+ pauseBtn.textContent = '\u23F8 Pause';
713
+ // Clear read slides so presentation starts fresh
714
+ readSlides.clear();
715
+ if (!readSlides.has(currentSlide)) speakAndAdvance(currentSlide);
716
+ resetControlsTimer();
717
+ }
718
+ }
719
+
720
+ /* ── Fullscreen controls auto-hide ── */
721
+ let controlsHideTimer = null;
722
+ const CONTROLS_HIDE_DELAY = 2500;
723
+ const CONTROLS_SHOW_ZONE = 80; // px from bottom edge
724
+
725
+ function hideControls() {
726
+ if (!isFullscreen) return;
727
+ document.getElementById('controls').classList.add('controls-hidden');
728
+ }
729
+
730
+ function showControls() {
731
+ document.getElementById('controls').classList.remove('controls-hidden');
732
+ resetControlsTimer();
733
+ }
734
+
735
+ function resetControlsTimer() {
736
+ if (controlsHideTimer) clearTimeout(controlsHideTimer);
737
+ if (!isFullscreen) return;
738
+ controlsHideTimer = setTimeout(hideControls, CONTROLS_HIDE_DELAY);
739
+ }
740
+
741
+ document.addEventListener('mousemove', function(e) {
742
+ if (!isFullscreen) return;
743
+ if (e.clientY >= window.innerHeight - CONTROLS_SHOW_ZONE) {
744
+ showControls();
745
+ }
746
+ });
747
+
748
+ // In fullscreen, controls only appear when mouse enters the bottom zone
749
+
750
+ /* ── Keyboard shortcuts ── */
751
+ document.addEventListener('keydown', function(e) {
752
+ var tag = document.activeElement && document.activeElement.tagName;
753
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
754
+ if (document.activeElement && document.activeElement.isContentEditable) return;
755
+ if (document.getElementById('voicePicker').classList.contains('open')) return;
756
+ if (document.getElementById('notesModalOverlay').classList.contains('open')) return;
757
+ if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); nextSlide(); }
758
+ if (e.key === 'ArrowLeft') { prevSlide(); }
759
+ if (e.key === 'Escape' && isFullscreen) { toggleFullscreen(); }
760
+ if (e.key === 'f' || e.key === 'F') { toggleFullscreen(); }
761
+ if ((e.key === 'p' || e.key === 'P') && isFullscreen) { togglePause(); }
762
+ });
763
+
764
+ /* ── Voice picker ── */
765
+ function toggleVoicePicker() {
766
+ var picker = document.getElementById('voicePicker');
767
+ if (picker.classList.contains('open')) {
768
+ picker.classList.remove('open');
769
+ } else {
770
+ picker.classList.add('open');
771
+ loadConfiguredVoices();
772
+ }
773
+ }
774
+
775
+ // Close picker when clicking outside
776
+ document.addEventListener('click', function(e) {
777
+ var picker = document.getElementById('voicePicker');
778
+ if (picker.classList.contains('open') && !e.target.closest('.voice-picker') && !e.target.closest('#voiceSettingsBtn')) {
779
+ picker.classList.remove('open');
780
+ }
781
+ });
782
+
783
+ async function loadConfiguredVoices() {
784
+ var list = document.getElementById('voicePickerList');
785
+ list.innerHTML = '<div class="voice-picker-empty">Loading voices...</div>';
786
+
787
+ try {
788
+ configuredVoices = await synthos.shared.data.list(SHARED_VOICES_TABLE);
789
+ renderVoicePicker();
790
+ } catch (e) {
791
+ list.innerHTML = '<div class="voice-picker-empty">Failed to load voices.</div>';
792
+ }
793
+ }
794
+
795
+ function renderVoicePicker() {
796
+ var list = document.getElementById('voicePickerList');
797
+
798
+ if (!configuredVoices || configuredVoices.length === 0) {
799
+ list.innerHTML = '<div class="voice-picker-empty">No voices configured.<br><a href="/pages/elevenlabs_voice_studio">Set up voices in Voice Studio</a></div>';
800
+ return;
801
+ }
802
+
803
+ list.innerHTML = configuredVoices.map(function(v) {
804
+ var isSelected = v.voiceId === selectedVoiceId;
805
+ return '<div class="voice-picker-item' + (isSelected ? ' selected' : '') + '" data-id="' + (v.id || '') + '" data-voice-id="' + (v.voiceId || '') + '" data-voice-name="' + (v.voiceName || v.name || '').replace(/"/g, '&quot;') + '" data-model="' + (v.model || 'eleven_turbo_v2_5') + '" data-stability="' + (v.stability != null ? v.stability : 50) + '" data-similarity="' + (v.similarity != null ? v.similarity : 75) + '" data-style="' + (v.style != null ? v.style : 0) + '">' +
806
+ '<span class="voice-picker-item-name">' + (v.name || v.voiceName || 'Unnamed') + '</span>' +
807
+ '<span class="voice-picker-item-meta">' + (v.voiceName || '') + '</span>' +
808
+ '</div>';
809
+ }).join('');
810
+ }
811
+
812
+ function selectVoiceFromPicker(el) {
813
+ selectedVoiceId = el.dataset.voiceId;
814
+ selectedVoiceName = el.dataset.voiceName;
815
+ selectedModel = el.dataset.model || 'eleven_turbo_v2_5';
816
+ selectedStability = (parseInt(el.dataset.stability, 10) || 50) / 100;
817
+ selectedSimilarity = (parseInt(el.dataset.similarity, 10) || 75) / 100;
818
+ selectedStyle = (parseInt(el.dataset.style, 10) || 0) / 100;
819
+
820
+ document.querySelectorAll('.voice-picker-item').forEach(function(v) { v.classList.remove('selected'); });
821
+ el.classList.add('selected');
822
+ saveVoiceConfig();
823
+
824
+ // Clear cached audio since voice changed
825
+ readSlides.clear();
826
+ Object.keys(audioCache).forEach(function(k) { URL.revokeObjectURL(audioCache[k]); delete audioCache[k]; });
827
+
828
+ // Close picker after selection
829
+ document.getElementById('voicePicker').classList.remove('open');
830
+ }
831
+
832
+ function clearVoice() {
833
+ selectedVoiceId = '';
834
+ selectedVoiceName = '';
835
+ document.querySelectorAll('.voice-picker-item').forEach(function(v) { v.classList.remove('selected'); });
836
+ saveVoiceConfig();
837
+ readSlides.clear();
838
+ Object.keys(audioCache).forEach(function(k) { URL.revokeObjectURL(audioCache[k]); delete audioCache[k]; });
839
+ document.getElementById('voicePicker').classList.remove('open');
840
+ }
841
+
842
+ /* ── Persist voice per page ── */
843
+ async function saveVoiceConfig() {
844
+ try {
845
+ await synthos.data.save(DATA_TABLE, {
846
+ id: DATA_ID,
847
+ voiceId: selectedVoiceId,
848
+ voiceName: selectedVoiceName,
849
+ model: selectedModel,
850
+ stability: selectedStability,
851
+ similarity: selectedSimilarity,
852
+ style: selectedStyle
853
+ });
854
+ } catch (e) {
855
+ console.error('Failed to save voice config:', e);
856
+ }
857
+ }
858
+
859
+ async function loadVoiceConfig() {
860
+ try {
861
+ var saved = await synthos.data.get(DATA_TABLE, DATA_ID);
862
+ if (saved) {
863
+ selectedVoiceId = saved.voiceId || '';
864
+ selectedVoiceName = saved.voiceName || '';
865
+ selectedModel = saved.model || 'eleven_turbo_v2_5';
866
+ selectedStability = saved.stability != null ? saved.stability : 0.5;
867
+ selectedSimilarity = saved.similarity != null ? saved.similarity : 0.75;
868
+ selectedStyle = saved.style != null ? saved.style : 0;
869
+ }
870
+ } catch (e) {
871
+ // No saved config yet
872
+ }
873
+
874
+ // Auto-enable TTS with first voice if ElevenLabs is configured but no voice selected
875
+ if (!selectedVoiceId) {
876
+ try {
877
+ var connectors = await synthos.connector.list({ id: 'elevenlabs' });
878
+ if (connectors && connectors.length > 0) {
879
+ var voices = await synthos.shared.data.list(SHARED_VOICES_TABLE);
880
+ if (voices && voices.length > 0) {
881
+ var first = voices[0];
882
+ selectedVoiceId = first.voiceId || '';
883
+ selectedVoiceName = first.voiceName || first.name || '';
884
+ selectedModel = first.model || 'eleven_turbo_v2_5';
885
+ selectedStability = (first.stability != null ? first.stability : 50) / 100;
886
+ selectedSimilarity = (first.similarity != null ? first.similarity : 75) / 100;
887
+ selectedStyle = (first.style != null ? first.style : 0) / 100;
888
+ saveVoiceConfig();
889
+ }
890
+ }
891
+ } catch (e) {
892
+ // ElevenLabs not available, TTS stays disabled
893
+ }
894
+ }
895
+ }
896
+
897
+ /* ── Speaker Notes Modal ── */
898
+ function openNotesModal() {
899
+ var slide = document.getElementById('slide' + currentSlide);
900
+ if (!slide) return;
901
+ document.getElementById('notesModalTitle').textContent = 'Speaker Notes \u2014 Slide ' + currentSlide;
902
+ document.getElementById('notesTextarea').value = slide.getAttribute('data-notes') || '';
903
+ document.getElementById('notesModalOverlay').classList.add('open');
904
+ document.getElementById('notesTextarea').focus();
905
+ }
906
+
907
+ function closeNotesModal() {
908
+ document.getElementById('notesModalOverlay').classList.remove('open');
909
+ }
910
+
911
+ function saveNotes() {
912
+ var slide = document.getElementById('slide' + currentSlide);
913
+ if (!slide) return;
914
+ var value = document.getElementById('notesTextarea').value;
915
+ var pageName = window.pageInfo ? window.pageInfo.name : window.location.pathname.replace(/^\//, '');
916
+ closeNotesModal();
917
+ var overlay = document.getElementById('loadingOverlay');
918
+ if (overlay) overlay.style.display = 'flex';
919
+ fetch('/' + encodeURIComponent(pageName) + '/patch', {
920
+ method: 'POST',
921
+ headers: { 'Content-Type': 'application/json' },
922
+ body: JSON.stringify({
923
+ operations: [{ op: 'set-attr', selector: '#slide' + currentSlide, attr: 'data-notes', value: value }],
924
+ message: 'Speaker notes for slide ' + currentSlide + ' updated'
925
+ })
926
+ }).then(function(r) {
927
+ if (!r.ok) throw new Error('Patch failed');
928
+ return r.json();
929
+ }).then(function(data) {
930
+ document.open();
931
+ document.write(data.html);
932
+ document.close();
933
+ }).catch(function(err) {
934
+ if (overlay) overlay.style.display = 'none';
935
+ console.error('Failed to save notes:', err);
936
+ });
937
+ }
938
+
939
+ // Light dismiss — close modal when clicking overlay background
940
+ document.getElementById('notesModalOverlay').addEventListener('click', function(e) {
941
+ if (e.target === this) closeNotesModal();
942
+ });
943
+
944
+ // Ctrl+Enter to save, Escape to cancel in notes modal
945
+ document.addEventListener('keydown', function(e) {
946
+ var overlay = document.getElementById('notesModalOverlay');
947
+ if (!overlay || !overlay.classList.contains('open')) return;
948
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeNotesModal(); }
949
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); e.stopPropagation(); saveNotes(); }
950
+ }, true);
951
+
952
+ /* ── Wire up control handlers ── */
953
+ document.getElementById('prevBtn').addEventListener('click', prevSlide);
954
+ document.getElementById('nextBtn').addEventListener('click', nextSlide);
955
+ document.getElementById('pauseBtn').addEventListener('click', togglePause);
956
+ document.getElementById('presentBtn').addEventListener('click', toggleFullscreen);
957
+ document.getElementById('notesBtn').addEventListener('click', openNotesModal);
958
+ document.getElementById('voiceSettingsBtn').addEventListener('click', toggleVoicePicker);
959
+ document.getElementById('voicePickerClearBtn').addEventListener('click', clearVoice);
960
+ document.getElementById('notesCancelBtn').addEventListener('click', closeNotesModal);
961
+ document.getElementById('notesSaveBtn').addEventListener('click', saveNotes);
962
+ document.getElementById('voicePickerList').addEventListener('click', function(e) {
963
+ var item = e.target.closest('.voice-picker-item');
964
+ if (item) selectVoiceFromPicker(item);
965
+ });
966
+
967
+ /* ── Init ── */
968
+ loadVoiceConfig();
969
+ </script>
970
+ </body></html>