synthos 0.7.2 → 0.9.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 (380) hide show
  1. package/README.md +215 -65
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1822 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/oregon_trail/page.html +323 -0
  27. package/default-pages/oregon_trail/page.json +12 -0
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_builder.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +24 -29
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +4 -4
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_builder.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/us_map/page.html +193 -0
  39. package/default-pages/us_map/page.json +12 -0
  40. package/default-pages/us_map_1850/page.html +326 -0
  41. package/default-pages/us_map_1850/page.json +12 -0
  42. package/default-pages/western_cities_1850/page.html +527 -0
  43. package/default-pages/western_cities_1850/page.json +12 -0
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +134 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +128 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
  65. package/dist/agents/a2a/a2aProvider.js +126 -0
  66. package/dist/agents/a2a/a2aProvider.js.map +1 -0
  67. package/dist/agents/discovery.d.ts.map +1 -0
  68. package/dist/agents/discovery.js +52 -0
  69. package/dist/agents/discovery.js.map +1 -0
  70. package/dist/agents/index.d.ts +7 -0
  71. package/dist/agents/index.d.ts.map +1 -0
  72. package/dist/agents/index.js +20 -0
  73. package/dist/agents/index.js.map +1 -0
  74. package/dist/agents/openclaw/gatewayManager.d.ts +117 -0
  75. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
  76. package/dist/agents/openclaw/gatewayManager.js +486 -0
  77. package/dist/agents/openclaw/gatewayManager.js.map +1 -0
  78. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
  79. package/dist/agents/openclaw/openclawProvider.js +237 -0
  80. package/dist/agents/openclaw/openclawProvider.js.map +1 -0
  81. package/dist/agents/openclaw/sshTunnelManager.d.ts +25 -0
  82. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
  83. package/dist/agents/openclaw/sshTunnelManager.js +359 -0
  84. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
  85. package/dist/agents/types.d.ts.map +1 -0
  86. package/dist/agents/types.js +6 -0
  87. package/dist/agents/types.js.map +1 -0
  88. package/dist/builders/anthropic.d.ts +31 -0
  89. package/dist/builders/anthropic.d.ts.map +1 -0
  90. package/dist/builders/anthropic.js +227 -0
  91. package/dist/builders/anthropic.js.map +1 -0
  92. package/dist/builders/fireworksai.d.ts +9 -0
  93. package/dist/builders/fireworksai.d.ts.map +1 -0
  94. package/dist/builders/fireworksai.js +57 -0
  95. package/dist/builders/fireworksai.js.map +1 -0
  96. package/dist/builders/index.d.ts +13 -0
  97. package/dist/builders/index.d.ts.map +1 -0
  98. package/dist/builders/index.js +31 -0
  99. package/dist/builders/index.js.map +1 -0
  100. package/dist/builders/openai.d.ts +8 -0
  101. package/dist/builders/openai.d.ts.map +1 -0
  102. package/dist/builders/openai.js +87 -0
  103. package/dist/builders/openai.js.map +1 -0
  104. package/dist/builders/types.d.ts +54 -0
  105. package/dist/builders/types.d.ts.map +1 -0
  106. package/dist/builders/types.js +211 -0
  107. package/dist/builders/types.js.map +1 -0
  108. package/dist/connectors/index.d.ts.map +1 -1
  109. package/dist/connectors/index.js +3 -2
  110. package/dist/connectors/index.js.map +1 -1
  111. package/dist/connectors/registry.d.ts +2 -1
  112. package/dist/connectors/registry.d.ts.map +1 -1
  113. package/dist/connectors/registry.js +65 -96
  114. package/dist/connectors/registry.js.map +1 -1
  115. package/dist/connectors/types.d.ts.map +1 -1
  116. package/dist/customizer/Customizer.d.ts +57 -0
  117. package/dist/customizer/Customizer.d.ts.map +1 -0
  118. package/dist/customizer/Customizer.js +124 -0
  119. package/dist/customizer/Customizer.js.map +1 -0
  120. package/dist/customizer/index.d.ts.map +1 -0
  121. package/dist/customizer/index.js +9 -0
  122. package/dist/customizer/index.js.map +1 -0
  123. package/dist/files.d.ts +17 -0
  124. package/dist/files.d.ts.map +1 -1
  125. package/dist/files.js +75 -1
  126. package/dist/files.js.map +1 -1
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/init.d.ts +10 -6
  131. package/dist/init.d.ts.map +1 -1
  132. package/dist/init.js +97 -86
  133. package/dist/init.js.map +1 -1
  134. package/dist/migrations.d.ts.map +1 -1
  135. package/dist/migrations.js +142 -145
  136. package/dist/migrations.js.map +1 -1
  137. package/dist/models/anthropic.d.ts +24 -0
  138. package/dist/models/anthropic.d.ts.map +1 -0
  139. package/dist/models/anthropic.js +103 -0
  140. package/dist/models/anthropic.js.map +1 -0
  141. package/dist/models/chainOfThought.d.ts.map +1 -0
  142. package/dist/models/chainOfThought.js +45 -0
  143. package/dist/models/chainOfThought.js.map +1 -0
  144. package/dist/models/fireworksai.d.ts.map +1 -0
  145. package/dist/models/fireworksai.js +141 -0
  146. package/dist/models/fireworksai.js.map +1 -0
  147. package/dist/models/index.d.ts +7 -1
  148. package/dist/models/index.d.ts.map +1 -1
  149. package/dist/models/index.js +20 -1
  150. package/dist/models/index.js.map +1 -1
  151. package/dist/models/logCompletePrompt.d.ts.map +1 -0
  152. package/dist/models/logCompletePrompt.js +23 -0
  153. package/dist/models/logCompletePrompt.js.map +1 -0
  154. package/dist/models/openai.d.ts +24 -0
  155. package/dist/models/openai.d.ts.map +1 -0
  156. package/dist/models/openai.js +101 -0
  157. package/dist/models/openai.js.map +1 -0
  158. package/dist/models/providers.d.ts.map +1 -1
  159. package/dist/models/providers.js +12 -4
  160. package/dist/models/providers.js.map +1 -1
  161. package/dist/models/types.d.ts +53 -2
  162. package/dist/models/types.d.ts.map +1 -1
  163. package/dist/models/types.js +21 -0
  164. package/dist/models/types.js.map +1 -1
  165. package/dist/models/utils.d.ts.map +1 -0
  166. package/dist/models/utils.js +21 -0
  167. package/dist/models/utils.js.map +1 -0
  168. package/dist/pages.d.ts +30 -7
  169. package/dist/pages.d.ts.map +1 -1
  170. package/dist/pages.js +177 -55
  171. package/dist/pages.js.map +1 -1
  172. package/dist/scripts.d.ts.map +1 -1
  173. package/dist/scripts.js +4 -3
  174. package/dist/scripts.js.map +1 -1
  175. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  176. package/dist/service/createCompletePrompt.js +9 -6
  177. package/dist/service/createCompletePrompt.js.map +1 -1
  178. package/dist/service/generateImage.d.ts.map +1 -1
  179. package/dist/service/generateImage.js +3 -3
  180. package/dist/service/generateImage.js.map +1 -1
  181. package/dist/service/server.d.ts.map +1 -1
  182. package/dist/service/server.js +39 -7
  183. package/dist/service/server.js.map +1 -1
  184. package/dist/service/transformPage.d.ts +47 -18
  185. package/dist/service/transformPage.d.ts.map +1 -1
  186. package/dist/service/transformPage.js +559 -270
  187. package/dist/service/transformPage.js.map +1 -1
  188. package/dist/service/useAgentRoutes.d.ts +5 -0
  189. package/dist/service/useAgentRoutes.d.ts.map +1 -0
  190. package/dist/service/useAgentRoutes.js +392 -0
  191. package/dist/service/useAgentRoutes.js.map +1 -0
  192. package/dist/service/useApiRoutes.d.ts.map +1 -1
  193. package/dist/service/useApiRoutes.js +380 -138
  194. package/dist/service/useApiRoutes.js.map +1 -1
  195. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  196. package/dist/service/useConnectorRoutes.js +20 -9
  197. package/dist/service/useConnectorRoutes.js.map +1 -1
  198. package/dist/service/useFileRoutes.d.ts +4 -0
  199. package/dist/service/useFileRoutes.d.ts.map +1 -0
  200. package/dist/service/useFileRoutes.js +122 -0
  201. package/dist/service/useFileRoutes.js.map +1 -0
  202. package/dist/service/usePageRoutes.d.ts.map +1 -1
  203. package/dist/service/usePageRoutes.js +660 -68
  204. package/dist/service/usePageRoutes.js.map +1 -1
  205. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  206. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  207. package/dist/service/useSharedDataRoutes.js +104 -0
  208. package/dist/service/useSharedDataRoutes.js.map +1 -0
  209. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  210. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  211. package/dist/service/useSharedFileRoutes.js +121 -0
  212. package/dist/service/useSharedFileRoutes.js.map +1 -0
  213. package/dist/settings.d.ts +3 -1
  214. package/dist/settings.d.ts.map +1 -1
  215. package/dist/settings.js +5 -8
  216. package/dist/settings.js.map +1 -1
  217. package/dist/synthos-cli.d.ts.map +1 -1
  218. package/dist/synthos-cli.js +4 -3
  219. package/dist/synthos-cli.js.map +1 -1
  220. package/dist/themes.d.ts +15 -0
  221. package/dist/themes.d.ts.map +1 -1
  222. package/dist/themes.js +106 -20
  223. package/dist/themes.js.map +1 -1
  224. package/migration-rules/v1-to-v2.md +193 -0
  225. package/migration-rules/v2-to-v3.md +481 -0
  226. package/package.json +15 -11
  227. package/required-pages/builder/page.html +43 -0
  228. package/required-pages/builder/page.json +10 -0
  229. package/required-pages/pages/page.html +924 -0
  230. package/required-pages/pages/page.json +10 -0
  231. package/required-pages/settings/page.html +1753 -0
  232. package/required-pages/settings/page.json +10 -0
  233. package/required-pages/synthos_apis/page.html +846 -0
  234. package/required-pages/synthos_apis/page.json +10 -0
  235. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  236. package/required-pages/synthos_scripts/page.json +10 -0
  237. package/service-connectors/airtable/connector.json +27 -0
  238. package/service-connectors/alpha-vantage/connector.json +26 -0
  239. package/service-connectors/brave-search/connector.json +26 -0
  240. package/service-connectors/cloudinary/connector.json +27 -0
  241. package/service-connectors/deepl/connector.json +28 -0
  242. package/service-connectors/elevenlabs/connector.json +30 -0
  243. package/service-connectors/giphy/connector.json +27 -0
  244. package/service-connectors/github/connector.json +29 -0
  245. package/service-connectors/huggingface/connector.json +27 -0
  246. package/service-connectors/imgur/connector.json +29 -0
  247. package/service-connectors/instagram/connector.json +43 -0
  248. package/service-connectors/jira/connector.json +28 -0
  249. package/service-connectors/mapbox/connector.json +26 -0
  250. package/service-connectors/nasa/connector.json +27 -0
  251. package/service-connectors/newsapi/connector.json +27 -0
  252. package/service-connectors/notion/connector.json +28 -0
  253. package/service-connectors/open-exchange-rates/connector.json +27 -0
  254. package/service-connectors/openweathermap/connector.json +26 -0
  255. package/service-connectors/pexels/connector.json +27 -0
  256. package/service-connectors/resend/connector.json +29 -0
  257. package/service-connectors/rss2json/connector.json +27 -0
  258. package/service-connectors/sendgrid/connector.json +27 -0
  259. package/service-connectors/spoonacular/connector.json +28 -0
  260. package/service-connectors/stability-ai/connector.json +27 -0
  261. package/service-connectors/twilio/connector.json +28 -0
  262. package/service-connectors/unsplash/connector.json +27 -0
  263. package/service-connectors/wolfram-alpha/connector.json +26 -0
  264. package/service-connectors/youtube-data/connector.json +30 -0
  265. package/src/agents/a2a/a2aProvider.ts +110 -0
  266. package/src/agents/discovery.ts +74 -0
  267. package/src/agents/index.ts +6 -0
  268. package/src/agents/openclaw/gatewayManager.ts +570 -0
  269. package/src/agents/openclaw/openclawProvider.ts +259 -0
  270. package/src/agents/openclaw/sshTunnelManager.ts +393 -0
  271. package/src/agents/types.ts +82 -0
  272. package/src/builders/anthropic.ts +283 -0
  273. package/src/builders/fireworksai.ts +59 -0
  274. package/src/builders/index.ts +33 -0
  275. package/src/builders/openai.ts +89 -0
  276. package/src/builders/types.ts +261 -0
  277. package/src/connectors/index.ts +3 -1
  278. package/src/connectors/registry.ts +40 -96
  279. package/src/connectors/types.ts +25 -0
  280. package/src/customizer/Customizer.ts +151 -0
  281. package/src/customizer/index.ts +5 -0
  282. package/src/files.ts +71 -0
  283. package/src/index.ts +2 -1
  284. package/src/init.ts +138 -97
  285. package/src/migrations.ts +148 -145
  286. package/src/models/anthropic.ts +119 -0
  287. package/src/models/chainOfThought.ts +56 -0
  288. package/src/models/fireworksai.ts +143 -0
  289. package/src/models/index.ts +7 -1
  290. package/src/models/logCompletePrompt.ts +25 -0
  291. package/src/models/openai.ts +110 -0
  292. package/src/models/providers.ts +12 -3
  293. package/src/models/types.ts +97 -2
  294. package/src/models/utils.ts +16 -0
  295. package/src/pages.ts +176 -54
  296. package/src/scripts.ts +2 -2
  297. package/src/service/createCompletePrompt.ts +3 -1
  298. package/src/service/generateImage.ts +2 -2
  299. package/src/service/server.ts +39 -8
  300. package/src/service/transformPage.ts +605 -301
  301. package/src/service/useAgentRoutes.ts +428 -0
  302. package/src/service/useApiRoutes.ts +309 -45
  303. package/src/service/useConnectorRoutes.ts +21 -10
  304. package/src/service/useFileRoutes.ts +127 -0
  305. package/src/service/usePageRoutes.ts +736 -75
  306. package/src/service/useSharedDataRoutes.ts +106 -0
  307. package/src/service/useSharedFileRoutes.ts +126 -0
  308. package/src/settings.ts +8 -10
  309. package/src/synthos-cli.ts +4 -3
  310. package/src/themes.ts +103 -20
  311. package/static-files/favicon.svg +12 -0
  312. package/static-files/fluentlm-instructions.llmd +868 -0
  313. package/static-files/fluentlm-instructions.md +1595 -0
  314. package/static-files/fluentlm.css +4844 -0
  315. package/static-files/fluentlm.js +3602 -0
  316. package/static-files/fluentlm.min.css +1 -0
  317. package/static-files/fluentlm.min.js +1 -0
  318. package/static-files/helpers.v3.js +304 -0
  319. package/static-files/page.v3.js +1290 -0
  320. package/static-files/recommended-frameworks.llmd +81 -0
  321. package/static-files/recommended-frameworks.md +137 -0
  322. package/static-files/retro-game.js +877 -0
  323. package/static-files/shell.css +797 -0
  324. package/static-files/theme-dark.css +169 -0
  325. package/static-files/theme-light.css +169 -0
  326. package/tests/anthropic.spec.ts +84 -0
  327. package/tests/builders.spec.ts +139 -0
  328. package/tests/chainOfThought.spec.ts +108 -0
  329. package/tests/ensureScripts.spec.ts +82 -0
  330. package/tests/files.spec.ts +233 -0
  331. package/tests/fireworksai.spec.ts +92 -0
  332. package/tests/logCompletePrompt.spec.ts +74 -0
  333. package/tests/migrations.spec.ts +79 -1
  334. package/tests/openai.spec.ts +71 -0
  335. package/tests/pages.spec.ts +226 -1
  336. package/tests/providers.spec.ts +144 -0
  337. package/tests/scripts.spec.ts +209 -0
  338. package/tests/transformPage.spec.ts +456 -0
  339. package/tests/types.spec.ts +23 -0
  340. package/default-pages/app_builder.html +0 -40
  341. package/default-pages/app_builder.json +0 -1
  342. package/default-pages/json_tools.json +0 -1
  343. package/default-pages/my_notes.html +0 -33
  344. package/default-pages/neon_asteroids.html +0 -77
  345. package/default-pages/sidebar_builder.json +0 -1
  346. package/default-pages/solar_tutorial.json +0 -1
  347. package/default-pages/two-panel_builder.json +0 -1
  348. package/dist/connectors/index.d.ts +0 -3
  349. package/dist/connectors/types.d.ts +0 -61
  350. package/dist/index.d.ts +0 -7
  351. package/dist/migrations.d.ts +0 -11
  352. package/dist/models/providers.d.ts +0 -7
  353. package/dist/scripts.d.ts +0 -14
  354. package/dist/service/createCompletePrompt.d.ts +0 -5
  355. package/dist/service/debugLog.d.ts +0 -11
  356. package/dist/service/generateImage.d.ts +0 -32
  357. package/dist/service/index.d.ts +0 -8
  358. package/dist/service/modelInstructions.d.ts +0 -7
  359. package/dist/service/requiresSettings.d.ts +0 -3
  360. package/dist/service/server.d.ts +0 -4
  361. package/dist/service/useApiRoutes.d.ts +0 -4
  362. package/dist/service/useConnectorRoutes.d.ts +0 -4
  363. package/dist/service/useDataRoutes.d.ts +0 -4
  364. package/dist/service/usePageRoutes.d.ts +0 -5
  365. package/dist/synthos-cli.d.ts +0 -2
  366. package/images/home.png +0 -0
  367. package/images/page-management.png +0 -0
  368. package/images/settings.png +0 -0
  369. package/images/synthos-square.png +0 -0
  370. package/page-scripts/helpers-v2.js +0 -121
  371. package/page-scripts/page-v2.js +0 -615
  372. package/required-pages/builder.html +0 -74
  373. package/required-pages/builder.json +0 -1
  374. package/required-pages/pages.html +0 -196
  375. package/required-pages/pages.json +0 -1
  376. package/required-pages/settings.html +0 -841
  377. package/required-pages/settings.json +0 -1
  378. package/required-pages/synthos_apis.html +0 -272
  379. package/required-pages/synthos_apis.json +0 -1
  380. package/required-pages/synthos_scripts.json +0 -1
@@ -0,0 +1,801 @@
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 - ElevenLabs Voice Studio</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="voice-studio-layout">
8
+ .vs-app { display: flex; height: 100%; width: 100%; overflow: hidden; }
9
+ .vs-sidebar { width: 280px; min-width: 280px; background: var(--defaultStateBackground); border-right: 2px solid var(--neutralLight); display: flex; flex-direction: column; }
10
+ .vs-sidebar-header { padding: 16px; border-bottom: 1px solid var(--neutralLight); display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; }
11
+ .vs-list { flex: 1; overflow-y: auto; padding: 8px; }
12
+ .vs-main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; background: var(--bodyBackground); }
13
+ .vs-instructions { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px; }
14
+ .vs-detail { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
15
+ .vs-detail-header { display: flex; align-items: center; gap: 12px; padding: 20px 20px 12px; flex-shrink: 0; border-bottom: 1px solid var(--neutralLight); }
16
+ .vs-detail-body { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 16px; }
17
+ .vs-detail-footer { flex-shrink: 0; padding: 12px 20px; border-top: 1px solid var(--neutralLight); }
18
+ .vs-detail-header input { font-size: 22px; font-weight: 600; flex: 1; }
19
+ .vs-section { padding: 16px; background: var(--defaultStateBackground); border-radius: 8px; border: 1px solid var(--neutralLight); }
20
+ .vs-section h3 { color: var(--themePrimary); font-weight: 600; margin: 0 0 12px; font-size: 15px; }
21
+ .slider-row { margin-bottom: 12px; }
22
+ .slider-row label { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 13px; color: var(--bodyText); }
23
+ .slider-row label span { color: var(--themePrimary); font-weight: 600; }
24
+ .slider-row input[type="range"] { width: 100%; height: 6px; border-radius: 3px; background: var(--neutralLight); -webkit-appearance: none; appearance: none; cursor: pointer; }
25
+ .slider-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--themePrimary); cursor: pointer; }
26
+ .slider-row input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--themePrimary); cursor: pointer; border: none; }
27
+ .filter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; margin-bottom: 10px; }
28
+ .filter-grid label { font-size: 11px; color: var(--bodySubtext); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; display: block; }
29
+ .filter-grid select, .filter-grid input { width: 100%; padding: 6px 8px; border: 1px solid var(--neutralLight); border-radius: 6px; background: var(--inputBackground); color: var(--bodyText); font-size: 12px; }
30
+ .selected-voice-field { display: flex; align-items: center; gap: 10px; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; border: 1px solid var(--themePrimary); background: var(--themeLighter); }
31
+ .selected-voice-field-label { font-size: 11px; color: var(--bodySubtext); text-transform: uppercase; letter-spacing: 0.5px; }
32
+ .selected-voice-field-name { font-weight: 600; font-size: 14px; flex: 1; }
33
+ .selected-voice-field--empty { border-color: var(--neutralLight); background: var(--inputBackground); }
34
+ .selected-voice-field--empty .selected-voice-field-name { color: var(--bodySubtext); font-weight: 400; }
35
+ .voice-card-list { display: flex; flex-direction: column; gap: 4px; }
36
+ .voice-card { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; transition: background 0.15s, border-color 0.15s; }
37
+ .voice-card:hover { background: var(--neutralLight); }
38
+ .voice-card--selected { border-color: var(--themePrimary); background: var(--themeLighter); box-shadow: inset 0 0 0 1px var(--themePrimary); }
39
+ .voice-card-select { flex-shrink: 0; padding: 4px 10px; border-radius: 4px; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; font-size: 11px; color: var(--bodyText); transition: background 0.15s; }
40
+ .voice-card-select:hover { background: var(--neutralLight); }
41
+ .voice-card--selected .voice-card-select { background: var(--themePrimary); color: #fff; border-color: var(--themePrimary); }
42
+ .voice-card-info { flex: 1; min-width: 0; }
43
+ .voice-card-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
44
+ .voice-card-meta { font-size: 11px; color: var(--bodySubtext); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
45
+ .voice-card-badge { display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px; background: var(--neutralLight); color: var(--bodySubtext); margin-left: 6px; vertical-align: middle; }
46
+ .voice-card-preview { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--themePrimary); font-size: 14px; transition: background 0.15s; }
47
+ .voice-card-preview:hover { background: var(--neutralLight); }
48
+ .voice-card-list-empty { padding: 20px; text-align: center; font-size: 12px; color: var(--bodySubtext); }
49
+ .pagination-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
50
+ .pagination-info { font-size: 12px; color: var(--bodySubtext); }
51
+ .char-count { text-align: right; font-size: 12px; color: var(--bodySubtext); margin-top: 4px; }
52
+ .waveform-container { height: 50px; display: flex; align-items: center; justify-content: center; gap: 3px; margin-bottom: 10px; }
53
+ .waveform-bar { width: 4px; background: var(--themePrimary); border-radius: 2px; transition: height 0.1s ease; }
54
+ .audio-row { text-align: center; }
55
+ .audio-row audio { width: 100%; margin-bottom: 10px; }
56
+ .vs-actions { display: flex; gap: 12px; justify-content: flex-end; }
57
+ .save-toast { opacity: 0; transform: translateY(4px); transition: opacity .3s, transform .3s; pointer-events: none; }
58
+ .save-toast.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
59
+ </style>
60
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js"></script>
61
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
62
+ </head>
63
+ <body>
64
+ <div class="shell-toolbar" data-locked="true">
65
+ <button class="shell-toolbar-btn" id="builderToggle" aria-label="Page Builder" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M7 18.5H6.2c-1.77 0-3.2-1.43-3.2-3.2V7.7C3 5.93 4.43 4.5 6.2 4.5h11.6c1.77 0 3.2 1.43 3.2 3.2v7.6c0 1.77-1.43 3.2-3.2 3.2H12l-4.2 3.2c-.5.38-1.2.02-1.2-.6V18.5Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="8.5" cy="11.5" r="1" fill="currentColor"/><circle cx="12" cy="11.5" r="1" fill="currentColor"/><circle cx="15.5" cy="11.5" r="1" fill="currentColor"/></svg></button>
66
+ <button class="shell-toolbar-btn" id="pagesBtn" aria-label="View All Pages" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"><rect x="3" y="3" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M6 7.5h5M6 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="18" y="3" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M21 7.5h5M21 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="3" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M6 22.5h5M6 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="18" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M21 22.5h5M21 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
67
+ <button class="shell-toolbar-btn" id="saveBtn" aria-label="Save Page" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M17 21v-8H7v8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M7 3v5h8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button>
68
+ <div class="shell-toolbar-spacer" data-locked="true"></div>
69
+ <button class="shell-toolbar-btn" id="settingsBtn" aria-label="Settings" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" stroke-width="1.8"/><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" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
70
+ </div>
71
+ <div class="chat-panel" data-locked="true">
72
+ <div class="chat-header" data-locked="true"><span>Page Builder</span><button class="chat-header-close" id="builderClose" aria-label="Close builder" data-locked="true">&times;</button></div>
73
+ <div class="chat-messages" id="chatMessages" data-locked="true">
74
+ <div class="chat-message">
75
+ <p><strong>SynthOS:</strong> Welcome to the ElevenLabs Voice Studio! Define voice profiles here and they'll be available to all your pages. Make sure the ElevenLabs connector is configured in Settings &gt; Connectors.</p>
76
+ </div>
77
+ </div> <form action="/" method="POST" id="chatForm" data-locked="true">
78
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..." data-locked="true"></textarea>
79
+ </form>
80
+ </div>
81
+ <div class="viewer-panel" id="viewerPanel" style="justify-content: flex-start; align-items: stretch;">
82
+ <div class="vs-app">
83
+ <!-- Sidebar: voice list -->
84
+ <div class="vs-sidebar">
85
+ <div class="vs-sidebar-header">
86
+ <span class="flm-text flm-text--large flm-text--bold">Voices</span>
87
+ <button class="flm-button flm-button--primary" id="addVoiceBtn" data-icon="Add">New Voice</button>
88
+ </div>
89
+ <div class="vs-list flm-list flm-list--bordered" id="voicesList"></div>
90
+ </div>
91
+
92
+ <!-- Main: detail or instructions -->
93
+ <div class="vs-main">
94
+ <!-- Connector status -->
95
+ <div id="connectorStatus" style="padding:12px 20px 0;"></div>
96
+
97
+ <!-- Empty state -->
98
+ <div class="vs-instructions" id="vsInstructions">
99
+ <i class="flm-icon flm-icon--large" data-icon="Microphone" style="font-size:64px;opacity:0.6;margin-bottom:20px;"></i>
100
+ <span class="flm-text flm-text--xLarge flm-text--bold flm-text--block" style="margin-bottom:12px;">Voice Studio</span>
101
+ <span class="flm-text flm-text--secondary flm-text--block" style="max-width:320px;line-height:1.5;">Select a voice from the sidebar to view its settings and test it, or click "+ New Voice" to create a profile.</span>
102
+ </div>
103
+
104
+ <!-- Detail editor -->
105
+ <div class="vs-detail" id="vsDetail" style="display:none;">
106
+ <!-- Name -->
107
+ <div class="vs-detail-header">
108
+ <div class="flm-textfield" style="flex:1;">
109
+ <input class="flm-textfield-input" id="profileNameInput" placeholder="Voice profile name...">
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Scrollable body -->
114
+ <div class="vs-detail-body">
115
+ <!-- Model -->
116
+ <div class="vs-section">
117
+ <div class="flm-textfield">
118
+ <label class="flm-label" for="modelSelect">Model</label>
119
+ <select class="flm-textfield-input" id="modelSelect">
120
+ <option value="eleven_turbo_v2_5">Eleven Turbo v2.5 (Fastest)</option>
121
+ <option value="eleven_multilingual_v2">Eleven Multilingual v2</option>
122
+ <option value="eleven_turbo_v2">Eleven Turbo v2</option>
123
+ <option value="eleven_monolingual_v1">Eleven Monolingual v1</option>
124
+ </select>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Voice selection -->
129
+ <div class="vs-section">
130
+ <h3>ElevenLabs Voice</h3>
131
+ <div class="selected-voice-field selected-voice-field--empty" id="selectedVoiceField">
132
+ <div style="flex:1;">
133
+ <div class="selected-voice-field-label">Selected Voice</div>
134
+ <div class="selected-voice-field-name" id="selectedVoiceName">No voice selected</div>
135
+ </div>
136
+ <button class="flm-button" id="changeVoiceBtn">Change Voice</button>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Voice Settings -->
141
+ <div class="vs-section">
142
+ <h3>Voice Settings</h3>
143
+ <div class="slider-row"><label>Stability <span id="stabilityValue">50%</span></label><input type="range" id="stability" min="0" max="100" value="50"></div>
144
+ <div class="slider-row"><label>Similarity <span id="similarityValue">75%</span></label><input type="range" id="similarity" min="0" max="100" value="75"></div>
145
+ <div class="slider-row"><label>Style <span id="styleValue">0%</span></label><input type="range" id="style" min="0" max="100" value="0"></div>
146
+ </div>
147
+
148
+ <!-- Test TTS -->
149
+ <div class="vs-section">
150
+ <h3>Test Voice</h3>
151
+ <div class="flm-textfield" style="margin-bottom:12px;">
152
+ <textarea class="flm-textfield-input" id="textInput" rows="3" placeholder="Enter text to test...">Hello! This is a test of the voice profile.</textarea>
153
+ </div>
154
+ <p class="char-count"><span id="charCount">0</span> characters</p>
155
+ <div id="statusMessage" class="flm-messagebar" style="display:none;margin-bottom:12px;"></div>
156
+ <button class="flm-button flm-button--primary" id="generateBtn" style="width:100%;padding:12px;">Generate Speech</button>
157
+ <div id="audioSection" style="display:none;margin-top:12px;">
158
+ <div class="waveform-container" id="waveform"></div>
159
+ <div class="audio-row">
160
+ <audio id="audioPlayer" controls style="width:100%;"></audio>
161
+ <div style="margin-top:8px;">
162
+ <button class="flm-button" id="downloadBtn">Download</button>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Footer: Save / Delete (always visible) -->
170
+ <div class="vs-detail-footer">
171
+ <div id="saveToast" class="flm-messagebar flm-messagebar--success save-toast" style="margin-bottom:8px;"></div>
172
+ <div class="vs-actions">
173
+ <button class="flm-button" id="deleteVoiceBtn" data-icon="Delete" style="color:var(--errorText);">Delete</button>
174
+ <button class="flm-button" id="saveVoiceBtn" data-icon="Save">Save</button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Voice picker dialog -->
182
+ <div id="voicePickerDialog" class="flm-dialog-overlay" data-light-dismiss>
183
+ <div class="flm-dialog" style="max-width:600px;width:95%;">
184
+ <div class="flm-dialog-header">
185
+ <h2 class="flm-dialog-title">Select Voice</h2>
186
+ <button class="flm-dialog-close" id="voicePickerClose">&times;</button>
187
+ </div>
188
+ <div class="flm-dialog-body">
189
+ <div class="filter-grid">
190
+ <div><label>Search</label><input type="text" id="searchFilter" placeholder="Search..."></div>
191
+ <div><label>Type</label><select id="voiceTypeFilter"><option value="">All</option><option value="personal">Personal</option><option value="community">Community</option><option value="default">Default</option><option value="workspace">Workspace</option></select></div>
192
+ <div><label>Category</label><select id="categoryFilter"><option value="">All</option><option value="premade">Premade</option><option value="cloned">Cloned</option><option value="generated">Generated</option><option value="professional">Professional</option></select></div>
193
+ <div><label>Sort</label><select id="sortFilter"><option value="">Default</option><option value="name">Name</option><option value="created_at_unix">Created</option></select></div>
194
+ <div><label>Direction</label><select id="sortDirectionFilter"><option value="asc">Asc</option><option value="desc">Desc</option></select></div>
195
+ </div>
196
+ <div class="voice-card-list" id="voiceCardList">
197
+ <div class="voice-card-list-empty">Loading voices...</div>
198
+ </div>
199
+ <div class="pagination-info" id="voiceCount"></div>
200
+ <div class="pagination-row" id="paginationControls" style="display:none;">
201
+ <button class="flm-button flm-button--subtle" id="prevPageBtn" disabled>&laquo; Prev</button>
202
+ <span class="pagination-info" id="pageInfo">Page 1</span>
203
+ <button class="flm-button flm-button--subtle" id="nextPageBtn" disabled>Next &raquo;</button>
204
+ </div>
205
+ </div>
206
+ <div class="flm-dialog-footer">
207
+ <button class="flm-button" id="voicePickerCancel">Cancel</button>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Delete confirm dialog -->
213
+ <div id="deleteConfirmDialog" class="flm-dialog-overlay" data-light-dismiss>
214
+ <div class="flm-dialog" style="max-width:400px;width:90%;">
215
+ <div class="flm-dialog-header"><h2 class="flm-dialog-title">Delete Voice</h2></div>
216
+ <div class="flm-dialog-body"><p class="flm-text">Are you sure you want to delete this voice profile? This cannot be undone.</p></div>
217
+ <div class="flm-dialog-footer">
218
+ <div style="flex:1;"></div>
219
+ <button class="flm-button" id="deleteConfirmCancel">Cancel</button>
220
+ <button class="flm-button" id="deleteConfirmOk" style="color:var(--errorText);">Delete</button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <div id="loadingOverlay" class="loading-overlay"><div class="spinner"></div></div>
226
+ </div>
227
+ <div id="instructions" style="display:none;" data-locked="true"></div>
228
+ <div id="thoughts" style="display:none;" data-locked="true"></div>
229
+ <script id="voice-studio-logic">
230
+ // ============ STATE ============
231
+ const TABLE = 'elevenlabs_voices';
232
+ const DEFAULT_VOICE = {
233
+ id: 'default',
234
+ name: 'River',
235
+ model: 'eleven_turbo_v2_5',
236
+ voiceName: 'River',
237
+ voiceId: '',
238
+ stability: 50,
239
+ similarity: 75,
240
+ style: 0
241
+ };
242
+
243
+ let profiles = [];
244
+ let currentId = null;
245
+ let currentAudioBlob = null;
246
+ let voicesData = [];
247
+ let currentPageToken = null;
248
+ let pageTokenHistory = [null];
249
+ let currentPageIndex = 0;
250
+ let hasMore = false;
251
+ let totalVoiceCount = 0;
252
+ let isDirty = false;
253
+
254
+ let selectedVoiceId = '';
255
+ let previewAudio = null;
256
+
257
+ const $ = id => document.getElementById(id);
258
+ const profileNameInput = $('profileNameInput');
259
+ const modelSelect = $('modelSelect');
260
+ const voiceCardList = $('voiceCardList');
261
+ const stabilitySlider = $('stability');
262
+ const similaritySlider = $('similarity');
263
+ const styleSlider = $('style');
264
+ const stabilityVal = $('stabilityValue');
265
+ const similarityVal = $('similarityValue');
266
+ const styleVal = $('styleValue');
267
+ const textInput = $('textInput');
268
+ const charCount = $('charCount');
269
+ const generateBtn = $('generateBtn');
270
+ const statusMsg = $('statusMessage');
271
+ const audioSection = $('audioSection');
272
+ const audioPlayer = $('audioPlayer');
273
+ const waveformContainer = $('waveform');
274
+
275
+ // ============ INIT ============
276
+ window.addEventListener('load', async () => {
277
+ updateCharCount();
278
+ await checkConnector();
279
+ await loadProfiles();
280
+ await loadVoices();
281
+ // Auto-select default profile on first load
282
+ if (profiles.length > 0) {
283
+ await selectProfile(profiles[0].id);
284
+ }
285
+ });
286
+
287
+ // ============ CONNECTOR CHECK ============
288
+ async function checkConnector() {
289
+ try {
290
+ const connectors = await synthos.connectors.list({ id: 'elevenlabs' });
291
+ const el = Array.isArray(connectors) ? connectors.find(c => c.id === 'elevenlabs') : null;
292
+ if (!el || !el.configured) {
293
+ $('connectorStatus').innerHTML = '<div class="flm-messagebar flm-messagebar--warning">ElevenLabs connector not configured. Go to <a href="/settings" class="flm-link">Settings</a> &gt; Connectors to set up your API key.</div>';
294
+ }
295
+ } catch (e) {
296
+ $('connectorStatus').innerHTML = '<div class="flm-messagebar flm-messagebar--warning">Could not check connector status.</div>';
297
+ }
298
+ }
299
+
300
+ // ============ PROFILES CRUD ============
301
+ async function loadProfiles() {
302
+ try {
303
+ const rows = await synthos.shared.data.list(TABLE);
304
+ profiles = Array.isArray(rows) ? rows : rows?.items || [];
305
+ } catch (e) {
306
+ profiles = [];
307
+ }
308
+
309
+ // Ensure default "River" profile exists
310
+ if (!profiles.find(p => p.id === 'default')) {
311
+ await synthos.shared.data.save(TABLE, { ...DEFAULT_VOICE });
312
+ profiles.unshift({ ...DEFAULT_VOICE });
313
+ }
314
+
315
+ renderList();
316
+ }
317
+
318
+ function renderList() {
319
+ const list = $('voicesList');
320
+ if (profiles.length === 0) {
321
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--bodySubtext);font-size:13px;">No voices yet.</div>';
322
+ return;
323
+ }
324
+ list.innerHTML = '';
325
+ profiles.forEach(p => {
326
+ const item = document.createElement('div');
327
+ item.className = 'flm-list-item' + (currentId === p.id ? ' flm-list-item--selected' : '');
328
+ item.style.cssText = 'cursor:pointer;padding:12px 14px;';
329
+ item.innerHTML = '<div class="flm-list-item-content">' +
330
+ '<span class="flm-list-item-primary flm-text--semibold flm-text--nowrap">' + escHtml(p.name || 'Untitled') + '</span>' +
331
+ '<span class="flm-list-item-secondary flm-text--small flm-text--secondary">' + escHtml(p.voiceName || p.voiceId || 'No voice') + ' &bull; ' + escHtml(p.model || '') + '</span>' +
332
+ '</div>';
333
+ item.onclick = () => selectProfile(p.id);
334
+ list.appendChild(item);
335
+ });
336
+ }
337
+
338
+ async function selectProfile(id) {
339
+ currentId = id;
340
+ const p = profiles.find(x => x.id === id);
341
+ if (!p) return;
342
+
343
+ $('vsInstructions').style.display = 'none';
344
+ $('vsDetail').style.display = '';
345
+
346
+ profileNameInput.value = p.name || '';
347
+ modelSelect.value = p.model || 'eleven_turbo_v2_5';
348
+ stabilitySlider.value = p.stability ?? 50; stabilityVal.textContent = stabilitySlider.value + '%';
349
+ similaritySlider.value = p.similarity ?? 75; similarityVal.textContent = similaritySlider.value + '%';
350
+ styleSlider.value = p.style ?? 0; styleVal.textContent = styleSlider.value + '%';
351
+
352
+ // Reset test area
353
+ audioSection.style.display = 'none';
354
+ currentAudioBlob = null;
355
+ hideStatus();
356
+ isDirty = false;
357
+ updateSaveBtn();
358
+ renderList();
359
+
360
+ // Reset voice selection to this profile's saved voice
361
+ selectedVoiceId = p.voiceId || '';
362
+ updateSelectedVoiceField(p.voiceName || '');
363
+ renderVoiceCards();
364
+
365
+ // Resolve voice (by ID or name fallback via API)
366
+ await resolveVoiceSelection(p);
367
+ }
368
+
369
+ async function resolveVoiceSelection(p) {
370
+ // Try by saved voice ID first
371
+ if (p.voiceId) {
372
+ const found = voicesData.find(v => v.voice_id === p.voiceId);
373
+ if (found) {
374
+ selectVoiceCard(p.voiceId, true);
375
+ return;
376
+ }
377
+ }
378
+ // Fallback: match by voice name (e.g. default "River" with no saved ID)
379
+ // ElevenLabs v2 API returns names like "River - Expressive, Gentle" so match the prefix before " - "
380
+ if (p.voiceName) {
381
+ const target = p.voiceName.toLowerCase();
382
+ const matchName = (v) => {
383
+ const name = v.name.toLowerCase();
384
+ return name === target || name.split(' - ')[0] === target;
385
+ };
386
+ // Check loaded voices first
387
+ let match = voicesData.find(matchName);
388
+ // Always try API search if not found locally
389
+ if (!match) {
390
+ try {
391
+ const data = await synthos.connectors.call('elevenlabs', 'GET',
392
+ '/v2/voices?search=' + encodeURIComponent(p.voiceName) + '&page_size=10');
393
+ const voices = data.voices || [];
394
+ match = voices.find(matchName);
395
+ if (match) {
396
+ // Add to local data so it's selectable as a card
397
+ if (!voicesData.find(v => v.voice_id === match.voice_id)) {
398
+ voicesData.push(match);
399
+ }
400
+ }
401
+ } catch (e) {
402
+ console.error('[VoiceStudio] Voice search failed:', e);
403
+ }
404
+ }
405
+ if (match) {
406
+ selectedVoiceId = match.voice_id;
407
+ updateSelectedVoiceField(match.name);
408
+ renderVoiceCards();
409
+ // Persist the resolved ID so future loads are instant
410
+ p.voiceId = match.voice_id;
411
+ const idx = profiles.findIndex(x => x.id === p.id);
412
+ if (idx >= 0) profiles[idx] = p;
413
+ synthos.shared.data.save(TABLE, p);
414
+ } else {
415
+ console.warn('[VoiceStudio] Could not resolve voice "' + p.voiceName + '".',
416
+ 'voicesData has', voicesData.length, 'voices.',
417
+ voicesData.length > 0 ? 'First 5: ' + voicesData.slice(0, 5).map(v => v.name).join(', ') : '(empty)');
418
+ }
419
+ }
420
+ }
421
+
422
+ function showInstructions() {
423
+ currentId = null;
424
+ selectedVoiceId = '';
425
+ $('vsInstructions').style.display = '';
426
+ $('vsDetail').style.display = 'none';
427
+ renderList();
428
+ }
429
+
430
+ // Dirty tracking
431
+ function markDirty() { isDirty = true; updateSaveBtn(); }
432
+ function updateSaveBtn() {
433
+ const btn = $('saveVoiceBtn');
434
+ if (isDirty) btn.classList.add('flm-button--primary');
435
+ else btn.classList.remove('flm-button--primary');
436
+ }
437
+ function showSaveToast() {
438
+ const t = $('saveToast');
439
+ t.textContent = 'Voice saved';
440
+ t.classList.add('show');
441
+ setTimeout(() => t.classList.remove('show'), 2500);
442
+ }
443
+
444
+ profileNameInput.addEventListener('input', markDirty);
445
+ modelSelect.addEventListener('change', markDirty);
446
+ stabilitySlider.addEventListener('input', () => { stabilityVal.textContent = stabilitySlider.value + '%'; markDirty(); });
447
+ similaritySlider.addEventListener('input', () => { similarityVal.textContent = similaritySlider.value + '%'; markDirty(); });
448
+ styleSlider.addEventListener('input', () => { styleVal.textContent = styleSlider.value + '%'; markDirty(); });
449
+
450
+ // ============ VOICE CARDS ============
451
+ // ElevenLabs v2 returns "River - Expressive, Gentle"; extract just "River"
452
+ function shortVoiceName(name) { return name ? name.split(' - ')[0] : ''; }
453
+
454
+ function updateSelectedVoiceField(name) {
455
+ const field = $('selectedVoiceField');
456
+ const nameEl = $('selectedVoiceName');
457
+ if (name) {
458
+ field.classList.remove('selected-voice-field--empty');
459
+ nameEl.textContent = name;
460
+ } else {
461
+ field.classList.add('selected-voice-field--empty');
462
+ nameEl.textContent = 'No voice selected';
463
+ }
464
+ }
465
+
466
+ function selectVoiceCard(voiceId, silent) {
467
+ selectedVoiceId = voiceId;
468
+ const voice = voicesData.find(v => v.voice_id === voiceId);
469
+ const short = shortVoiceName(voice?.name || '');
470
+ updateSelectedVoiceField(short);
471
+ if (!silent) {
472
+ // Auto-fill profile name if empty
473
+ if (!profileNameInput.value.trim()) {
474
+ profileNameInput.value = short;
475
+ }
476
+ markDirty();
477
+ closeVoicePickerDialog();
478
+ }
479
+ renderVoiceCards();
480
+ }
481
+
482
+ function renderVoiceCards() {
483
+ voiceCardList.innerHTML = '';
484
+ if (voicesData.length === 0) {
485
+ voiceCardList.innerHTML = '<div class="voice-card-list-empty">No voices found</div>';
486
+ return;
487
+ }
488
+ // Show selected voice first
489
+ const sorted = [...voicesData].sort((a, b) => {
490
+ if (a.voice_id === selectedVoiceId) return -1;
491
+ if (b.voice_id === selectedVoiceId) return 1;
492
+ return 0;
493
+ });
494
+ sorted.forEach(v => {
495
+ const card = document.createElement('div');
496
+ card.className = 'voice-card' + (selectedVoiceId === v.voice_id ? ' voice-card--selected' : '');
497
+ card.dataset.voiceId = v.voice_id;
498
+
499
+ const info = document.createElement('div');
500
+ info.className = 'voice-card-info';
501
+
502
+ const nameRow = document.createElement('div');
503
+ nameRow.className = 'voice-card-name';
504
+ nameRow.textContent = v.name;
505
+ if (v.category) {
506
+ const badge = document.createElement('span');
507
+ badge.className = 'voice-card-badge';
508
+ badge.textContent = v.category;
509
+ nameRow.appendChild(badge);
510
+ }
511
+ info.appendChild(nameRow);
512
+
513
+ const lbls = v.labels || {};
514
+ const metaText = Object.values(lbls).filter(Boolean).join(', ') || v.description || '';
515
+ if (metaText) {
516
+ const meta = document.createElement('div');
517
+ meta.className = 'voice-card-meta';
518
+ meta.textContent = metaText;
519
+ info.appendChild(meta);
520
+ }
521
+
522
+ card.appendChild(info);
523
+
524
+ // Preview button
525
+ if (v.preview_url) {
526
+ const prevBtn = document.createElement('button');
527
+ prevBtn.className = 'voice-card-preview';
528
+ prevBtn.innerHTML = '&#9654;';
529
+ prevBtn.title = 'Preview voice';
530
+ prevBtn.addEventListener('click', (e) => {
531
+ e.stopPropagation();
532
+ if (previewAudio) { previewAudio.pause(); previewAudio = null; }
533
+ previewAudio = new Audio(v.preview_url);
534
+ previewAudio.play();
535
+ });
536
+ card.appendChild(prevBtn);
537
+ }
538
+
539
+ // Select button
540
+ const isSelected = selectedVoiceId === v.voice_id;
541
+ const selBtn = document.createElement('button');
542
+ selBtn.className = 'voice-card-select';
543
+ selBtn.textContent = isSelected ? 'Selected' : 'Select';
544
+ selBtn.addEventListener('click', (e) => {
545
+ e.stopPropagation();
546
+ selectVoiceCard(v.voice_id);
547
+ });
548
+ card.appendChild(selBtn);
549
+
550
+ card.addEventListener('click', () => {
551
+ selectVoiceCard(v.voice_id);
552
+ });
553
+
554
+ voiceCardList.appendChild(card);
555
+ });
556
+ }
557
+
558
+ // ============ ADD / SAVE / DELETE ============
559
+ $('addVoiceBtn').addEventListener('click', async () => {
560
+ const newId = 'voice_' + Date.now();
561
+ const newProfile = {
562
+ id: newId,
563
+ name: '',
564
+ model: 'eleven_turbo_v2_5',
565
+ voiceName: '',
566
+ voiceId: '',
567
+ stability: 50,
568
+ similarity: 75,
569
+ style: 0
570
+ };
571
+ await synthos.shared.data.save(TABLE, newProfile);
572
+ profiles.push(newProfile);
573
+ selectProfile(newId);
574
+ });
575
+
576
+ $('saveVoiceBtn').addEventListener('click', async () => {
577
+ if (!currentId) return;
578
+ const selectedVoice = voicesData.find(v => v.voice_id === selectedVoiceId);
579
+ const data = {
580
+ id: currentId,
581
+ name: profileNameInput.value.trim() || 'Untitled',
582
+ model: modelSelect.value,
583
+ voiceId: selectedVoiceId,
584
+ voiceName: shortVoiceName(selectedVoice?.name || ''),
585
+ stability: parseInt(stabilitySlider.value),
586
+ similarity: parseInt(similaritySlider.value),
587
+ style: parseInt(styleSlider.value)
588
+ };
589
+ await synthos.shared.data.save(TABLE, data);
590
+ const idx = profiles.findIndex(p => p.id === currentId);
591
+ if (idx >= 0) profiles[idx] = data;
592
+ isDirty = false;
593
+ updateSaveBtn();
594
+ showSaveToast();
595
+ renderList();
596
+ });
597
+
598
+ $('deleteVoiceBtn').addEventListener('click', () => {
599
+ if (currentId) $('deleteConfirmDialog').classList.add('flm-dialog-overlay--open');
600
+ });
601
+ $('deleteConfirmCancel').addEventListener('click', () => {
602
+ $('deleteConfirmDialog').classList.remove('flm-dialog-overlay--open');
603
+ });
604
+ $('deleteConfirmOk').addEventListener('click', async () => {
605
+ $('deleteConfirmDialog').classList.remove('flm-dialog-overlay--open');
606
+ if (!currentId) return;
607
+ await synthos.shared.data.remove(TABLE, currentId);
608
+ profiles = profiles.filter(p => p.id !== currentId);
609
+ showInstructions();
610
+ });
611
+
612
+ // ============ VOICE PICKER DIALOG ============
613
+ function openVoicePickerDialog() {
614
+ $('voicePickerDialog').classList.add('flm-dialog-overlay--open');
615
+ }
616
+ function closeVoicePickerDialog() {
617
+ $('voicePickerDialog').classList.remove('flm-dialog-overlay--open');
618
+ }
619
+ $('changeVoiceBtn').addEventListener('click', openVoicePickerDialog);
620
+ $('voicePickerClose').addEventListener('click', closeVoicePickerDialog);
621
+ $('voicePickerCancel').addEventListener('click', closeVoicePickerDialog);
622
+
623
+ // ============ VOICE LOADING ============
624
+ let filterTimeout;
625
+ function onFilterChange() { clearTimeout(filterTimeout); filterTimeout = setTimeout(() => { resetPagination(); loadVoices(); }, 300); }
626
+ $('searchFilter').addEventListener('input', onFilterChange);
627
+ $('voiceTypeFilter').addEventListener('change', onFilterChange);
628
+ $('categoryFilter').addEventListener('change', onFilterChange);
629
+ $('sortFilter').addEventListener('change', onFilterChange);
630
+ $('sortDirectionFilter').addEventListener('change', onFilterChange);
631
+
632
+ $('prevPageBtn').addEventListener('click', () => { if (currentPageIndex > 0) { currentPageIndex--; currentPageToken = pageTokenHistory[currentPageIndex]; loadVoices(); } });
633
+ $('nextPageBtn').addEventListener('click', () => { if (hasMore && pageTokenHistory[currentPageIndex + 1]) { currentPageIndex++; currentPageToken = pageTokenHistory[currentPageIndex]; loadVoices(); } });
634
+
635
+ function resetPagination() { currentPageToken = null; pageTokenHistory = [null]; currentPageIndex = 0; hasMore = false; }
636
+
637
+ async function loadVoices() {
638
+ try {
639
+ voiceCardList.innerHTML = '<div class="voice-card-list-empty">Loading voices...</div>';
640
+ const params = new URLSearchParams();
641
+ params.append('page_size', '50');
642
+ params.append('include_total_count', 'true');
643
+ if (currentPageToken) params.append('next_page_token', currentPageToken);
644
+ const search = $('searchFilter').value.trim();
645
+ if (search) params.append('search', search);
646
+ const vType = $('voiceTypeFilter').value;
647
+ if (vType) params.append('voice_type', vType);
648
+ const cat = $('categoryFilter').value;
649
+ if (cat) params.append('category', cat);
650
+ const sort = $('sortFilter').value;
651
+ if (sort) params.append('sort', sort);
652
+ const dir = $('sortDirectionFilter').value;
653
+ if (dir) params.append('sort_direction', dir);
654
+
655
+ const data = await synthos.connectors.call('elevenlabs', 'GET', '/v2/voices?' + params.toString());
656
+ voicesData = data.voices || [];
657
+ hasMore = data.has_more || false;
658
+ totalVoiceCount = data.total_count || voicesData.length;
659
+ if (data.next_page_token && !pageTokenHistory.includes(data.next_page_token)) {
660
+ pageTokenHistory[currentPageIndex + 1] = data.next_page_token;
661
+ }
662
+
663
+ // Render voice cards
664
+ renderVoiceCards();
665
+
666
+ // Pagination
667
+ const pageSize = 50;
668
+ const totalPages = Math.ceil(totalVoiceCount / pageSize);
669
+ const startNum = currentPageIndex * pageSize + 1;
670
+ const endNum = Math.min(startNum + voicesData.length - 1, totalVoiceCount);
671
+ $('voiceCount').textContent = voicesData.length > 0 ? `${startNum}-${endNum} of ${totalVoiceCount} voices` : '';
672
+ $('paginationControls').style.display = totalPages > 1 ? 'flex' : 'none';
673
+ $('prevPageBtn').disabled = currentPageIndex === 0;
674
+ $('nextPageBtn').disabled = !hasMore;
675
+ $('pageInfo').textContent = `Page ${currentPageIndex + 1} of ${totalPages}`;
676
+
677
+ // Restore voice selection for current profile
678
+ if (currentId) {
679
+ const p = profiles.find(x => x.id === currentId);
680
+ if (p) await resolveVoiceSelection(p);
681
+ }
682
+ } catch (e) {
683
+ voiceCardList.innerHTML = '<div class="voice-card-list-empty">Error loading voices</div>';
684
+ $('voiceCount').textContent = '';
685
+ $('paginationControls').style.display = 'none';
686
+ }
687
+ }
688
+
689
+
690
+ // ============ TEST TTS ============
691
+ function updateCharCount() { charCount.textContent = textInput.value.length; }
692
+ textInput.addEventListener('input', updateCharCount);
693
+
694
+ generateBtn.addEventListener('click', async () => {
695
+ // If voice ID not resolved yet, try once more on demand
696
+ if (!selectedVoiceId && currentId) {
697
+ const p = profiles.find(x => x.id === currentId);
698
+ if (p) await resolveVoiceSelection(p);
699
+ }
700
+ const voiceId = selectedVoiceId;
701
+ const text = textInput.value.trim();
702
+ if (!voiceId) { showStatus('Select a voice first', 'error'); return; }
703
+ if (!text) { showStatus('Enter text to test', 'error'); return; }
704
+
705
+ generateBtn.disabled = true;
706
+ generateBtn.textContent = 'Generating...';
707
+ showStatus('Generating speech...', 'info');
708
+
709
+ try {
710
+ const res = await fetch('/api/connectors', {
711
+ method: 'POST',
712
+ headers: { 'Content-Type': 'application/json' },
713
+ body: JSON.stringify({
714
+ connector: 'elevenlabs',
715
+ method: 'POST',
716
+ path: `/v1/text-to-speech/${voiceId}`,
717
+ headers: { 'Accept': 'audio/mpeg' },
718
+ body: {
719
+ text,
720
+ model_id: modelSelect.value,
721
+ voice_settings: {
722
+ stability: stabilitySlider.value / 100,
723
+ similarity_boost: similaritySlider.value / 100,
724
+ style: styleSlider.value / 100,
725
+ use_speaker_boost: true
726
+ }
727
+ }
728
+ })
729
+ });
730
+
731
+ if (!res.ok) {
732
+ const errText = await res.text();
733
+ let errMsg = 'Generation failed';
734
+ try { const j = JSON.parse(errText); errMsg = j.detail?.message || j.error || errMsg; } catch (e) {}
735
+ throw new Error(errMsg);
736
+ }
737
+
738
+ currentAudioBlob = await res.blob();
739
+ audioPlayer.src = URL.createObjectURL(currentAudioBlob);
740
+ audioSection.style.display = '';
741
+ createWaveform();
742
+ hideStatus();
743
+ audioPlayer.play();
744
+ } catch (e) {
745
+ showStatus('Error: ' + e.message, 'error');
746
+ } finally {
747
+ generateBtn.disabled = false;
748
+ generateBtn.textContent = 'Generate Speech';
749
+ }
750
+ });
751
+
752
+ // ============ WAVEFORM ============
753
+ function createWaveform() {
754
+ waveformContainer.innerHTML = '';
755
+ for (let i = 0; i < 40; i++) {
756
+ const bar = document.createElement('div');
757
+ bar.className = 'waveform-bar';
758
+ bar.style.height = '10px';
759
+ waveformContainer.appendChild(bar);
760
+ }
761
+ audioPlayer.addEventListener('play', animateWaveform);
762
+ audioPlayer.addEventListener('pause', stopWaveform);
763
+ audioPlayer.addEventListener('ended', stopWaveform);
764
+ }
765
+ let waveformInterval;
766
+ function animateWaveform() {
767
+ const bars = waveformContainer.querySelectorAll('.waveform-bar');
768
+ waveformInterval = setInterval(() => { bars.forEach(b => { b.style.height = Math.random() * 40 + 10 + 'px'; }); }, 100);
769
+ }
770
+ function stopWaveform() {
771
+ clearInterval(waveformInterval);
772
+ waveformContainer.querySelectorAll('.waveform-bar').forEach(b => { b.style.height = '10px'; });
773
+ }
774
+
775
+ // ============ DOWNLOAD ============
776
+ $('downloadBtn').addEventListener('click', () => {
777
+ if (!currentAudioBlob) return;
778
+ const url = URL.createObjectURL(currentAudioBlob);
779
+ const a = document.createElement('a');
780
+ a.href = url;
781
+ a.download = 'speech.mp3';
782
+ document.body.appendChild(a);
783
+ a.click();
784
+ document.body.removeChild(a);
785
+ URL.revokeObjectURL(url);
786
+ });
787
+
788
+ // ============ STATUS HELPERS ============
789
+ function showStatus(msg, type) {
790
+ const cls = type === 'error' ? 'flm-messagebar--error' : type === 'success' ? 'flm-messagebar--success' : 'flm-messagebar--info';
791
+ statusMsg.className = 'flm-messagebar ' + cls;
792
+ statusMsg.textContent = msg;
793
+ statusMsg.style.display = '';
794
+ }
795
+ function hideStatus() { statusMsg.style.display = 'none'; }
796
+
797
+ function escHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
798
+ </script>
799
+ <script id="page-helpers" src="/api/page-helpers.js?v=3" data-locked="true"></script>
800
+ <script id="page-script" src="/api/page-script.js?v=3" data-locked="true"></script>
801
+ </body></html>