@sutrata/editor 0.1.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 (337) hide show
  1. package/LICENSE +674 -0
  2. package/dist/ai/OnboardingDialog.d.ts +6 -0
  3. package/dist/ai/OnboardingDialog.js +115 -0
  4. package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
  5. package/dist/ai/VoiceConfirmDialog.js +37 -0
  6. package/dist/ai/VoiceToolbar.d.ts +2 -0
  7. package/dist/ai/VoiceToolbar.js +177 -0
  8. package/dist/ai/ai-client.d.ts +71 -0
  9. package/dist/ai/ai-client.js +494 -0
  10. package/dist/ai/commands.d.ts +12 -0
  11. package/dist/ai/commands.js +140 -0
  12. package/dist/ai/generated-providers.json +3696 -0
  13. package/dist/ai/prompts.d.ts +25 -0
  14. package/dist/ai/prompts.js +59 -0
  15. package/dist/ai/voice-service.d.ts +15 -0
  16. package/dist/ai/voice-service.js +81 -0
  17. package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
  18. package/dist/autocomplete/AutocompletePlugin.js +19 -0
  19. package/dist/autocomplete/suggestions.d.ts +25 -0
  20. package/dist/autocomplete/suggestions.js +126 -0
  21. package/dist/context/DocumentContext.d.ts +68 -0
  22. package/dist/context/DocumentContext.js +605 -0
  23. package/dist/desktop-api.d.ts +40 -0
  24. package/dist/editor/EditorView.d.ts +2 -0
  25. package/dist/editor/EditorView.js +78 -0
  26. package/dist/editor/SelectionMenu.d.ts +2 -0
  27. package/dist/editor/SelectionMenu.js +102 -0
  28. package/dist/editor/attr-pencil-plugin.d.ts +17 -0
  29. package/dist/editor/attr-pencil-plugin.js +158 -0
  30. package/dist/editor/character-decoration-plugin.d.ts +26 -0
  31. package/dist/editor/character-decoration-plugin.js +77 -0
  32. package/dist/editor/character-view.d.ts +35 -0
  33. package/dist/editor/character-view.js +112 -0
  34. package/dist/editor/editor-bus.d.ts +14 -0
  35. package/dist/editor/editor-bus.js +39 -0
  36. package/dist/editor/find-highlight-plugin.d.ts +7 -0
  37. package/dist/editor/find-highlight-plugin.js +57 -0
  38. package/dist/editor/frontmatter-field-view.d.ts +10 -0
  39. package/dist/editor/frontmatter-field-view.js +48 -0
  40. package/dist/editor/frontmatter-field.d.ts +21 -0
  41. package/dist/editor/frontmatter-field.js +116 -0
  42. package/dist/editor/grapheme-cursor.d.ts +10 -0
  43. package/dist/editor/grapheme-cursor.js +104 -0
  44. package/dist/editor/ime-plugin.d.ts +18 -0
  45. package/dist/editor/ime-plugin.js +71 -0
  46. package/dist/editor/insert-helper.d.ts +1 -0
  47. package/dist/editor/insert-helper.js +42 -0
  48. package/dist/editor/keymap.d.ts +3 -0
  49. package/dist/editor/keymap.js +131 -0
  50. package/dist/editor/language-class-plugin.d.ts +7 -0
  51. package/dist/editor/language-class-plugin.js +75 -0
  52. package/dist/editor/plugins.d.ts +1 -0
  53. package/dist/editor/plugins.js +25 -0
  54. package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
  55. package/dist/editor/prosemirror-to-sutra.js +208 -0
  56. package/dist/editor/scene-heading-view.d.ts +27 -0
  57. package/dist/editor/scene-heading-view.js +136 -0
  58. package/dist/editor/schema.d.ts +3 -0
  59. package/dist/editor/schema.js +135 -0
  60. package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
  61. package/dist/editor/sutra-to-prosemirror.js +154 -0
  62. package/dist/editor/transliterate-input.d.ts +10 -0
  63. package/dist/editor/transliterate-input.js +88 -0
  64. package/dist/extensions/storage-adapter.d.ts +59 -0
  65. package/dist/extensions/storage-adapter.js +9 -0
  66. package/dist/extensions/types.d.ts +73 -0
  67. package/dist/extensions/types.js +12 -0
  68. package/dist/file/docx-exporter.d.ts +8 -0
  69. package/dist/file/docx-exporter.js +494 -0
  70. package/dist/file/docx-importer.d.ts +10 -0
  71. package/dist/file/docx-importer.js +283 -0
  72. package/dist/file/print-paginator.d.ts +29 -0
  73. package/dist/file/print-paginator.js +289 -0
  74. package/dist/file/templates.d.ts +2 -0
  75. package/dist/file/templates.js +66 -0
  76. package/dist/file/workflow-reports.d.ts +34 -0
  77. package/dist/file/workflow-reports.js +719 -0
  78. package/dist/findreplace/FindReplace.d.ts +2 -0
  79. package/dist/findreplace/FindReplace.js +164 -0
  80. package/dist/findreplace/find-engine.d.ts +29 -0
  81. package/dist/findreplace/find-engine.js +69 -0
  82. package/dist/findreplace/scroll-to-match.d.ts +29 -0
  83. package/dist/findreplace/scroll-to-match.js +74 -0
  84. package/dist/fonts/tofu-detect.d.ts +1 -0
  85. package/dist/fonts/tofu-detect.js +48 -0
  86. package/dist/i18n/LanguageContext.d.ts +10 -0
  87. package/dist/i18n/LanguageContext.js +174 -0
  88. package/dist/i18n/language-detect.d.ts +1 -0
  89. package/dist/i18n/language-detect.js +58 -0
  90. package/dist/i18n/useTranslation.d.ts +11 -0
  91. package/dist/i18n/useTranslation.js +60 -0
  92. package/dist/index.d.ts +11 -0
  93. package/dist/index.js +5 -0
  94. package/dist/locales/bn.json +143 -0
  95. package/dist/locales/en.json +143 -0
  96. package/dist/locales/gu.json +143 -0
  97. package/dist/locales/hi.json +143 -0
  98. package/dist/locales/kn.json +143 -0
  99. package/dist/locales/ml.json +143 -0
  100. package/dist/locales/mr.json +143 -0
  101. package/dist/locales/or.json +143 -0
  102. package/dist/locales/pa.json +143 -0
  103. package/dist/locales/si.json +143 -0
  104. package/dist/locales/ta.json +143 -0
  105. package/dist/locales/te.json +143 -0
  106. package/dist/navigator/SceneNavigator.d.ts +2 -0
  107. package/dist/navigator/SceneNavigator.js +612 -0
  108. package/dist/navigator/StatisticsDialog.d.ts +6 -0
  109. package/dist/navigator/StatisticsDialog.js +146 -0
  110. package/dist/navigator/scene-list.d.ts +16 -0
  111. package/dist/navigator/scene-list.js +70 -0
  112. package/dist/settings/SettingsDialog.d.ts +6 -0
  113. package/dist/settings/SettingsDialog.js +147 -0
  114. package/dist/shell/AppBar.d.ts +2 -0
  115. package/dist/shell/AppBar.js +34 -0
  116. package/dist/shell/AppShell.d.ts +2 -0
  117. package/dist/shell/AppShell.js +30 -0
  118. package/dist/shell/ConfirmDialog.d.ts +16 -0
  119. package/dist/shell/ConfirmDialog.js +23 -0
  120. package/dist/shell/ElementToolbar.d.ts +2 -0
  121. package/dist/shell/ElementToolbar.js +30 -0
  122. package/dist/shell/LanguageStatus.d.ts +2 -0
  123. package/dist/shell/LanguageStatus.js +65 -0
  124. package/dist/shell/StatusBar.d.ts +2 -0
  125. package/dist/shell/StatusBar.js +54 -0
  126. package/dist/shell/SutrataLogo.d.ts +8 -0
  127. package/dist/shell/SutrataLogo.js +7 -0
  128. package/dist/shell/Toast.d.ts +12 -0
  129. package/dist/shell/Toast.js +21 -0
  130. package/dist/shell/ToolbarButton.d.ts +11 -0
  131. package/dist/shell/ToolbarButton.js +4 -0
  132. package/dist/shell/VersionHistory.d.ts +12 -0
  133. package/dist/shell/VersionHistory.js +89 -0
  134. package/dist/shell/Watermark.d.ts +3 -0
  135. package/dist/shell/Watermark.js +10 -0
  136. package/dist/shell/fonts/fonts.css +1133 -0
  137. package/dist/shell/icons.d.ts +45 -0
  138. package/dist/shell/icons.js +51 -0
  139. package/dist/shell/toolbar-actions.d.ts +6 -0
  140. package/dist/shell/toolbar-actions.js +58 -0
  141. package/dist/source/SourceView.d.ts +1 -0
  142. package/dist/source/SourceView.js +106 -0
  143. package/dist/source/find-highlight-source.d.ts +14 -0
  144. package/dist/source/find-highlight-source.js +47 -0
  145. package/dist/source/sutra-lang.d.ts +2 -0
  146. package/dist/source/sutra-lang.js +97 -0
  147. package/dist/spellcheck/dictionaries.d.ts +9 -0
  148. package/dist/spellcheck/dictionaries.js +16 -0
  149. package/dist/spellcheck/hunspell.d.ts +23 -0
  150. package/dist/spellcheck/hunspell.js +94 -0
  151. package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
  152. package/dist/spellcheck/spellcheck-plugin.js +75 -0
  153. package/dist/styles/StyleDialog.d.ts +6 -0
  154. package/dist/styles/StyleDialog.js +63 -0
  155. package/dist/styles/builtin-styles.d.ts +43 -0
  156. package/dist/styles/builtin-styles.js +131 -0
  157. package/dist/styles/css-adapter.d.ts +32 -0
  158. package/dist/styles/css-adapter.js +71 -0
  159. package/dist/styles/docx-adapter.d.ts +7 -0
  160. package/dist/styles/docx-adapter.js +84 -0
  161. package/dist/styles/embed-fonts.d.ts +20 -0
  162. package/dist/styles/embed-fonts.js +81 -0
  163. package/dist/styles/pagination.d.ts +19 -0
  164. package/dist/styles/pagination.js +22 -0
  165. package/dist/styles/print-adapter.d.ts +6 -0
  166. package/dist/styles/print-adapter.js +288 -0
  167. package/dist/styles/registry.d.ts +6 -0
  168. package/dist/styles/registry.js +14 -0
  169. package/dist/styles/screenplay.css +3006 -0
  170. package/dist/styles/script-ranges.d.ts +6 -0
  171. package/dist/styles/script-ranges.js +17 -0
  172. package/dist/styles/types.d.ts +93 -0
  173. package/dist/styles/types.js +18 -0
  174. package/dist/styles/validate.d.ts +12 -0
  175. package/dist/styles/validate.js +105 -0
  176. package/dist/templates/templates.d.ts +10 -0
  177. package/dist/templates/templates.js +181 -0
  178. package/dist/titlepage/ExportDialog.d.ts +1 -0
  179. package/dist/titlepage/ExportDialog.js +152 -0
  180. package/dist/titlepage/TitlePageDialog.d.ts +6 -0
  181. package/dist/titlepage/TitlePageDialog.js +96 -0
  182. package/dist/titlepage/frontmatter-form.d.ts +56 -0
  183. package/dist/titlepage/frontmatter-form.js +245 -0
  184. package/dist/titlepage/languages.d.ts +10 -0
  185. package/dist/titlepage/languages.js +31 -0
  186. package/dist/types.d.ts +6 -0
  187. package/dist/types.js +1 -0
  188. package/package.json +47 -0
  189. package/scripts/copy-assets.mjs +16 -0
  190. package/scripts/generate-providers.mjs +127 -0
  191. package/src/ai/OnboardingDialog.tsx +390 -0
  192. package/src/ai/VoiceConfirmDialog.tsx +137 -0
  193. package/src/ai/VoiceToolbar.tsx +321 -0
  194. package/src/ai/ai-client.ts +596 -0
  195. package/src/ai/commands.ts +164 -0
  196. package/src/ai/generated-providers.json +3696 -0
  197. package/src/ai/prompts.ts +62 -0
  198. package/src/ai/voice-service.ts +96 -0
  199. package/src/autocomplete/AutocompletePlugin.ts +21 -0
  200. package/src/autocomplete/suggestions.ts +147 -0
  201. package/src/context/DocumentContext.tsx +648 -0
  202. package/src/desktop-api.d.ts +40 -0
  203. package/src/editor/EditorView.tsx +82 -0
  204. package/src/editor/SelectionMenu.tsx +152 -0
  205. package/src/editor/attr-pencil-plugin.ts +172 -0
  206. package/src/editor/character-decoration-plugin.ts +104 -0
  207. package/src/editor/editor-bus.ts +51 -0
  208. package/src/editor/find-highlight-plugin.ts +68 -0
  209. package/src/editor/frontmatter-field-view.ts +52 -0
  210. package/src/editor/frontmatter-field.ts +115 -0
  211. package/src/editor/grapheme-cursor.ts +99 -0
  212. package/src/editor/ime-plugin.ts +85 -0
  213. package/src/editor/insert-helper.ts +43 -0
  214. package/src/editor/keymap.ts +124 -0
  215. package/src/editor/language-class-plugin.ts +91 -0
  216. package/src/editor/plugins.ts +26 -0
  217. package/src/editor/prosemirror-to-sutra.ts +192 -0
  218. package/src/editor/scene-heading-view.ts +147 -0
  219. package/src/editor/schema.ts +156 -0
  220. package/src/editor/sutra-to-prosemirror.ts +156 -0
  221. package/src/extensions/storage-adapter.ts +65 -0
  222. package/src/extensions/types.ts +60 -0
  223. package/src/file/docx-exporter.ts +585 -0
  224. package/src/file/docx-importer.ts +313 -0
  225. package/src/file/print-paginator.ts +301 -0
  226. package/src/file/templates.ts +73 -0
  227. package/src/file/workflow-reports.ts +820 -0
  228. package/src/findreplace/FindReplace.tsx +242 -0
  229. package/src/findreplace/find-engine.ts +97 -0
  230. package/src/findreplace/scroll-to-match.ts +89 -0
  231. package/src/fonts/tofu-detect.ts +53 -0
  232. package/src/i18n/LanguageContext.tsx +207 -0
  233. package/src/i18n/language-detect.ts +73 -0
  234. package/src/i18n/useTranslation.tsx +74 -0
  235. package/src/index.ts +27 -0
  236. package/src/locales/bn.json +143 -0
  237. package/src/locales/en.json +143 -0
  238. package/src/locales/gu.json +143 -0
  239. package/src/locales/hi.json +143 -0
  240. package/src/locales/kn.json +143 -0
  241. package/src/locales/ml.json +143 -0
  242. package/src/locales/mr.json +143 -0
  243. package/src/locales/or.json +143 -0
  244. package/src/locales/pa.json +143 -0
  245. package/src/locales/si.json +143 -0
  246. package/src/locales/ta.json +143 -0
  247. package/src/locales/te.json +143 -0
  248. package/src/navigator/SceneNavigator.tsx +844 -0
  249. package/src/navigator/StatisticsDialog.tsx +249 -0
  250. package/src/navigator/scene-list.ts +84 -0
  251. package/src/settings/SettingsDialog.tsx +303 -0
  252. package/src/shell/AppBar.tsx +354 -0
  253. package/src/shell/AppShell.tsx +71 -0
  254. package/src/shell/ConfirmDialog.tsx +71 -0
  255. package/src/shell/ElementToolbar.tsx +175 -0
  256. package/src/shell/LanguageStatus.tsx +101 -0
  257. package/src/shell/StatusBar.tsx +135 -0
  258. package/src/shell/SutrataLogo.tsx +89 -0
  259. package/src/shell/Toast.tsx +46 -0
  260. package/src/shell/ToolbarButton.tsx +30 -0
  261. package/src/shell/VersionHistory.tsx +212 -0
  262. package/src/shell/Watermark.tsx +9 -0
  263. package/src/shell/fonts/fonts.css +1133 -0
  264. package/src/shell/icons.tsx +335 -0
  265. package/src/shell/toolbar-actions.ts +59 -0
  266. package/src/source/SourceView.tsx +111 -0
  267. package/src/source/find-highlight-source.ts +57 -0
  268. package/src/source/sutra-lang.ts +121 -0
  269. package/src/spellcheck/dictionaries.ts +21 -0
  270. package/src/spellcheck/hunspell.ts +110 -0
  271. package/src/spellcheck/spellcheck-plugin.ts +83 -0
  272. package/src/styles/StyleDialog.tsx +148 -0
  273. package/src/styles/builtin-styles.ts +139 -0
  274. package/src/styles/css-adapter.ts +76 -0
  275. package/src/styles/docx-adapter.ts +109 -0
  276. package/src/styles/embed-fonts.ts +81 -0
  277. package/src/styles/pagination.ts +27 -0
  278. package/src/styles/print-adapter.ts +288 -0
  279. package/src/styles/registry.ts +20 -0
  280. package/src/styles/screenplay.css +3006 -0
  281. package/src/styles/script-ranges.ts +17 -0
  282. package/src/styles/types.ts +104 -0
  283. package/src/styles/validate.ts +114 -0
  284. package/src/templates/templates.ts +194 -0
  285. package/src/titlepage/ExportDialog.tsx +494 -0
  286. package/src/titlepage/TitlePageDialog.tsx +312 -0
  287. package/src/titlepage/frontmatter-form.ts +280 -0
  288. package/src/titlepage/languages.ts +37 -0
  289. package/src/types.ts +7 -0
  290. package/tests/AppBar.test.tsx +35 -0
  291. package/tests/AppShell.test.tsx +22 -0
  292. package/tests/ElementToolbar.test.tsx +36 -0
  293. package/tests/LanguageStatus.test.tsx +129 -0
  294. package/tests/SelectionMenu.test.tsx +71 -0
  295. package/tests/ToolbarButton.test.tsx +24 -0
  296. package/tests/ai-onboarding.test.ts +102 -0
  297. package/tests/ai-status.test.tsx +72 -0
  298. package/tests/ai-structured.test.ts +94 -0
  299. package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
  300. package/tests/autocomplete/suggestions.test.ts +106 -0
  301. package/tests/document-context.test.tsx +565 -0
  302. package/tests/docx-importer.test.ts +160 -0
  303. package/tests/editor/character-decoration-plugin.test.ts +101 -0
  304. package/tests/editor/character-extension.test.ts +44 -0
  305. package/tests/editor/grapheme-cursor.test.ts +118 -0
  306. package/tests/editor/ime-plugin.test.ts +72 -0
  307. package/tests/editor/inline-note-bridge.test.ts +32 -0
  308. package/tests/editor/language-class-plugin.test.ts +265 -0
  309. package/tests/editor/title-page.test.ts +124 -0
  310. package/tests/embed-fonts.test.ts +17 -0
  311. package/tests/exporters.test.ts +365 -0
  312. package/tests/findreplace/FindReplace.test.tsx +391 -0
  313. package/tests/findreplace/find-engine.test.ts +86 -0
  314. package/tests/findreplace/scroll-to-match.test.ts +50 -0
  315. package/tests/fonts/tofu-detect.test.ts +122 -0
  316. package/tests/i18n/LanguageContext.test.tsx +146 -0
  317. package/tests/i18n/language-detect.test.ts +54 -0
  318. package/tests/i18n/locale-parity.test.ts +25 -0
  319. package/tests/i18n/useTranslation.test.tsx +45 -0
  320. package/tests/keymap.test.ts +18 -0
  321. package/tests/navigator/SceneNavigator.test.tsx +177 -0
  322. package/tests/navigator/StatisticsDialog.test.tsx +63 -0
  323. package/tests/navigator/scene-list.test.ts +71 -0
  324. package/tests/print-paginator.test.ts +82 -0
  325. package/tests/shell/VersionHistory.test.tsx +129 -0
  326. package/tests/spellcheck/hunspell.test.ts +82 -0
  327. package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
  328. package/tests/styles.test.ts +315 -0
  329. package/tests/templates/templates.test.ts +83 -0
  330. package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
  331. package/tests/titlepage/apply-title-page.test.ts +61 -0
  332. package/tests/titlepage/frontmatter-form.test.ts +115 -0
  333. package/tests/toolbar-actions.test.ts +93 -0
  334. package/tests/voice-commands.test.ts +58 -0
  335. package/tsconfig.json +17 -0
  336. package/vitest.config.ts +11 -0
  337. package/vitest.setup.ts +65 -0
@@ -0,0 +1,494 @@
1
+ import generatedProvidersData from './generated-providers.json';
2
+ /** Compiled-in last resort when both the remote and local models-config.json
3
+ * fetches fail (e.g. fully offline). Generated by scripts/generate-providers.mjs
4
+ * from the same source as models-config.json, so this can no longer drift
5
+ * from the runtime-fetched list the way the old hand-written array did. */
6
+ const FALLBACK_PROVIDERS = generatedProvidersData.providers
7
+ .filter(p => p.essential)
8
+ .map(({ id, name, baseUrl, models }) => ({ id, name, baseUrl, models }));
9
+ export async function getApiKey(provider) {
10
+ if (window.desktopAPI?.getApiKey) {
11
+ try {
12
+ return (await window.desktopAPI.getApiKey(provider)) || null;
13
+ }
14
+ catch (e) {
15
+ console.error('Failed to get key from tauri keyring:', e);
16
+ }
17
+ }
18
+ return localStorage.getItem(`sutrata_api_key_${provider}`);
19
+ }
20
+ export async function setApiKey(provider, key) {
21
+ if (window.desktopAPI?.setApiKey) {
22
+ try {
23
+ await window.desktopAPI.setApiKey(provider, key);
24
+ return;
25
+ }
26
+ catch (e) {
27
+ console.error('Failed to set key in tauri keyring:', e);
28
+ }
29
+ }
30
+ localStorage.setItem(`sutrata_api_key_${provider}`, key);
31
+ }
32
+ export async function deleteApiKey(provider) {
33
+ if (window.desktopAPI?.deleteApiKey) {
34
+ try {
35
+ await window.desktopAPI.deleteApiKey(provider);
36
+ return;
37
+ }
38
+ catch (e) {
39
+ console.error('Failed to delete key from tauri keyring:', e);
40
+ }
41
+ }
42
+ localStorage.removeItem(`sutrata_api_key_${provider}`);
43
+ }
44
+ export function getAIConfig() {
45
+ const stored = localStorage.getItem('sutrata_ai_config');
46
+ if (stored) {
47
+ try {
48
+ return JSON.parse(stored);
49
+ }
50
+ catch {
51
+ // ignore
52
+ }
53
+ }
54
+ return {
55
+ // No model id is hardcoded here — callAI/callAIStructured already fall
56
+ // back to the chosen provider's first available model when
57
+ // preferredModel doesn't match one of its models.
58
+ preferredProvider: 'anthropic',
59
+ preferredModel: '',
60
+ customProviders: [],
61
+ };
62
+ }
63
+ export function saveAIConfig(config) {
64
+ localStorage.setItem('sutrata_ai_config', JSON.stringify(config));
65
+ }
66
+ let cachedProviders = null;
67
+ export async function fetchProviders(config) {
68
+ if (cachedProviders) {
69
+ return mergeCustomProviders(cachedProviders, config?.customProviders);
70
+ }
71
+ const isProd = import.meta.env?.PROD;
72
+ const githubUrl = 'https://raw.githubusercontent.com/sivarajd/sutrata/main/packages/app/public/models-config.json';
73
+ let providers = [];
74
+ if (isProd) {
75
+ try {
76
+ const res = await fetch(githubUrl);
77
+ if (res.ok) {
78
+ const data = await res.json();
79
+ providers = data.providers || [];
80
+ }
81
+ }
82
+ catch (e) {
83
+ console.warn('Failed to fetch models config from github raw URL, falling back to local:', e);
84
+ }
85
+ }
86
+ if (providers.length === 0) {
87
+ try {
88
+ const res = await fetch('/models-config.json');
89
+ if (res.ok) {
90
+ const data = await res.json();
91
+ providers = data.providers || [];
92
+ }
93
+ }
94
+ catch (e) {
95
+ console.error('Failed to fetch local models-config.json:', e);
96
+ }
97
+ }
98
+ if (providers.length === 0) {
99
+ // Both the remote and local fetches failed (or returned nothing) — use
100
+ // the compiled-in snapshot generated at build time. See FALLBACK_PROVIDERS.
101
+ providers = FALLBACK_PROVIDERS;
102
+ }
103
+ cachedProviders = providers;
104
+ return mergeCustomProviders(providers, config?.customProviders);
105
+ }
106
+ function mergeCustomProviders(base, custom) {
107
+ if (!custom || custom.length === 0)
108
+ return base;
109
+ const list = [...base];
110
+ for (const c of custom) {
111
+ const index = list.findIndex(p => p.id === c.id);
112
+ if (index >= 0) {
113
+ const existing = list[index];
114
+ list[index] = {
115
+ ...existing,
116
+ name: c.name,
117
+ baseUrl: c.baseUrl,
118
+ models: c.models,
119
+ };
120
+ }
121
+ else {
122
+ list.push({
123
+ id: c.id,
124
+ name: c.name,
125
+ baseUrl: c.baseUrl,
126
+ models: c.models,
127
+ });
128
+ }
129
+ }
130
+ return list;
131
+ }
132
+ async function requestHttp(url, method, headers, body) {
133
+ if (window.desktopAPI?.callProviderApi) {
134
+ try {
135
+ const res = await window.desktopAPI.callProviderApi(url, method, headers, body);
136
+ return res;
137
+ }
138
+ catch (e) {
139
+ throw new Error(`Desktop API Error: ${e}`);
140
+ }
141
+ }
142
+ else {
143
+ try {
144
+ const res = await fetch(url, {
145
+ method,
146
+ headers,
147
+ body: body ? JSON.stringify(body) : undefined,
148
+ });
149
+ let data;
150
+ const text = await res.text();
151
+ try {
152
+ data = JSON.parse(text);
153
+ }
154
+ catch {
155
+ data = text;
156
+ }
157
+ return {
158
+ status: res.status,
159
+ data,
160
+ };
161
+ }
162
+ catch (e) {
163
+ throw new Error(`Web Fetch Error: ${e}`);
164
+ }
165
+ }
166
+ }
167
+ const STRUCTURED_TOOL_NAME = 'emit_result';
168
+ /**
169
+ * Anthropic has no OpenAI-compatible endpoint, so it's the only provider
170
+ * that still needs a bespoke request/response shape here — everyone else
171
+ * (including Gemini, via its official /v1beta/openai compat base URL) goes
172
+ * through the generic OpenAI-compatible branch below.
173
+ */
174
+ async function invokeProvider(provider, model, key, prompt, systemPrompt, responseSchema) {
175
+ let url = '';
176
+ let headers = {};
177
+ let body = null;
178
+ const method = 'POST';
179
+ if (provider.id === 'anthropic') {
180
+ url = `${provider.baseUrl}/messages`;
181
+ headers = {
182
+ 'x-api-key': key,
183
+ 'anthropic-version': '2023-06-01',
184
+ 'content-type': 'application/json',
185
+ };
186
+ body = {
187
+ model,
188
+ max_tokens: 2000,
189
+ system: systemPrompt,
190
+ messages: [
191
+ { role: 'user', content: prompt }
192
+ ],
193
+ temperature: 0.1,
194
+ };
195
+ if (responseSchema) {
196
+ // Force a schema-valid tool call instead of parsing free text —
197
+ // guarantees the fields we asked for, in the shape we asked for.
198
+ body.tools = [{
199
+ name: STRUCTURED_TOOL_NAME,
200
+ description: 'Return the structured result for this request.',
201
+ input_schema: responseSchema,
202
+ strict: true,
203
+ }];
204
+ body.tool_choice = { type: 'tool', name: STRUCTURED_TOOL_NAME };
205
+ }
206
+ }
207
+ else {
208
+ url = `${provider.baseUrl}/chat/completions`;
209
+ headers = {
210
+ 'Authorization': `Bearer ${key}`,
211
+ 'Content-Type': 'application/json',
212
+ };
213
+ const effectiveSystemPrompt = responseSchema
214
+ ? `${systemPrompt}\n\n[OUTPUT FORMAT]\nRespond with ONLY a single JSON object matching this JSON schema — no markdown fences, no commentary, no extra text before or after it:\n${JSON.stringify(responseSchema)}`
215
+ : systemPrompt;
216
+ body = {
217
+ model,
218
+ messages: [
219
+ { role: 'system', content: effectiveSystemPrompt },
220
+ { role: 'user', content: prompt }
221
+ ],
222
+ temperature: 0.1,
223
+ };
224
+ if (responseSchema) {
225
+ // Broadest-compatibility structured-output mode — many self-hosted/
226
+ // aggregator OpenAI-compatible backends don't support the stricter
227
+ // json_schema mode, so this is paired with the prompt instruction
228
+ // above and extractJson()'s tolerant parsing on the caller side.
229
+ body.response_format = { type: 'json_object' };
230
+ }
231
+ }
232
+ const res = await requestHttp(url, method, headers, body);
233
+ if (res.status === 429) {
234
+ throw new Error(`Rate limit exceeded (HTTP 429)`);
235
+ }
236
+ if (res.status < 200 || res.status >= 300) {
237
+ const errMsg = typeof res.data === 'object'
238
+ ? JSON.stringify(res.data)
239
+ : res.data;
240
+ throw new Error(`HTTP ${res.status}: ${errMsg}`);
241
+ }
242
+ if (provider.id === 'anthropic') {
243
+ if (responseSchema) {
244
+ const toolUse = res.data?.content?.find((block) => block?.type === 'tool_use' && block?.name === STRUCTURED_TOOL_NAME);
245
+ if (!toolUse)
246
+ throw new Error('Invalid response from Anthropic: missing tool_use block');
247
+ return JSON.stringify(toolUse.input);
248
+ }
249
+ const text = res.data?.content?.[0]?.text;
250
+ if (!text)
251
+ throw new Error('Invalid response from Anthropic: missing content text');
252
+ return text;
253
+ }
254
+ else {
255
+ const text = res.data?.choices?.[0]?.message?.content;
256
+ if (!text)
257
+ throw new Error(`Invalid response from ${provider.name}: missing message content`);
258
+ return text;
259
+ }
260
+ }
261
+ /**
262
+ * Parses a structured AI response into T, tolerating a model that wraps the
263
+ * JSON in a markdown fence or adds stray text around it despite being asked
264
+ * not to (some providers don't strictly honor response_format hints).
265
+ */
266
+ export function extractJson(raw) {
267
+ const attempts = [raw.trim()];
268
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
269
+ if (fenceMatch?.[1])
270
+ attempts.push(fenceMatch[1].trim());
271
+ const firstBrace = raw.indexOf('{');
272
+ const lastBrace = raw.lastIndexOf('}');
273
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
274
+ attempts.push(raw.slice(firstBrace, lastBrace + 1));
275
+ }
276
+ for (const attempt of attempts) {
277
+ try {
278
+ return JSON.parse(attempt);
279
+ }
280
+ catch {
281
+ continue;
282
+ }
283
+ }
284
+ throw new Error(`AI did not return valid JSON: ${raw.slice(0, 200)}`);
285
+ }
286
+ export function cleanVoiceResponse(text) {
287
+ let cleaned = text.trim();
288
+ // Strip markdown code block wrapper if present
289
+ cleaned = cleaned.replace(/^```[a-zA-Z0-9]*\n/, '').replace(/\n```$/, '');
290
+ const lines = cleaned.split('\n');
291
+ const filteredLines = lines.map(line => {
292
+ let l = line.trim();
293
+ // Strip leading list bullet (e.g. *, -, +, •) and spaces
294
+ l = l.replace(/^[*•\-+]\s*/, '').trim();
295
+ // Strip leading/trailing backticks if any
296
+ l = l.replace(/^`+/, '').replace(/`+$/, '').trim();
297
+ // Strip common explanatory trailing comments in parens
298
+ l = l.replace(/\s*\(Standard screenplay shorthand[^)]*\)/gi, '');
299
+ l = l.replace(/\s*\(Standard shorthand[^)]*\)/gi, '');
300
+ return l;
301
+ }).filter(line => {
302
+ const trimmed = line.toLowerCase();
303
+ if (!trimmed)
304
+ return false;
305
+ // Discard reasoning/mapping/explanation patterns
306
+ if (trimmed.includes('->') ||
307
+ trimmed.includes('=>') ||
308
+ trimmed.startsWith('input:') ||
309
+ trimmed.startsWith('task:') ||
310
+ trimmed.startsWith('rules:') ||
311
+ trimmed.startsWith('instructions:') ||
312
+ trimmed.startsWith('sutra rules:') ||
313
+ trimmed.startsWith('scene headings:') ||
314
+ trimmed.startsWith('scene id:') ||
315
+ trimmed.startsWith('characters:') ||
316
+ trimmed.startsWith('parentheticals:') ||
317
+ trimmed.startsWith('dialogue:') ||
318
+ trimmed.startsWith('transitions:') ||
319
+ trimmed.startsWith('lyrics:') ||
320
+ trimmed.startsWith('action:') ||
321
+ trimmed.startsWith('format:') ||
322
+ trimmed.startsWith('components:') ||
323
+ trimmed.startsWith('output only') ||
324
+ trimmed.startsWith('no markdown') ||
325
+ trimmed.includes('convert it into valid sutra') ||
326
+ trimmed.includes('sutra syntax rules') ||
327
+ trimmed.includes('clearly a scene heading') ||
328
+ trimmed.includes('screenplay shorthand') ||
329
+ trimmed.includes('append as')) {
330
+ return false;
331
+ }
332
+ return true;
333
+ });
334
+ return filteredLines.join('\n').trim();
335
+ }
336
+ let nextCallId = 0;
337
+ const activeCalls = new Map();
338
+ const subscribers = new Set();
339
+ export function subscribeToAICalls(callback) {
340
+ subscribers.add(callback);
341
+ callback(Array.from(activeCalls.values()));
342
+ return () => {
343
+ subscribers.delete(callback);
344
+ };
345
+ }
346
+ function notifySubscribers() {
347
+ const list = Array.from(activeCalls.values());
348
+ for (const sub of subscribers) {
349
+ sub(list);
350
+ }
351
+ }
352
+ export function formatActiveCalls(calls) {
353
+ if (calls.length === 0)
354
+ return '';
355
+ if (calls.length === 1) {
356
+ return `Calling ${calls[0].providerName} (${calls[0].model})...`;
357
+ }
358
+ const unique = Array.from(new Set(calls.map(c => `${c.providerName} (${c.model})`)));
359
+ if (unique.length === 1) {
360
+ return `Calling ${unique[0]} (${calls.length} calls)...`;
361
+ }
362
+ return `Calling ${unique.join(', ')}...`;
363
+ }
364
+ /**
365
+ * Resolves the provider/key/model fallback chain and invokes it, retrying
366
+ * the next configured provider on failure. Shared by callAI and
367
+ * callAIStructured so the two don't duplicate this logic.
368
+ */
369
+ async function resolveAndInvoke(prompt, systemPrompt, config, responseSchema) {
370
+ const providers = await fetchProviders(config);
371
+ const availableProviders = [];
372
+ for (const p of providers) {
373
+ const key = await getApiKey(p.id);
374
+ if (key) {
375
+ availableProviders.push(p.id);
376
+ }
377
+ }
378
+ if (availableProviders.length === 0) {
379
+ throw new Error('No AI API keys configured. Please configure keys in Settings.');
380
+ }
381
+ let providersToTry = [config.preferredProvider, ...availableProviders.filter(p => p !== config.preferredProvider)];
382
+ providersToTry = providersToTry.filter(p => availableProviders.includes(p));
383
+ if (providersToTry.length === 0) {
384
+ providersToTry = [availableProviders[0]];
385
+ }
386
+ let lastError = '';
387
+ for (const providerId of providersToTry) {
388
+ const provider = providers.find(p => p.id === providerId);
389
+ if (!provider)
390
+ continue;
391
+ const key = await getApiKey(providerId);
392
+ if (!key)
393
+ continue;
394
+ // config.preferredModel is only meaningful for config.preferredProvider
395
+ // — Settings/Onboarding always save them together, and the model is
396
+ // picked from that provider's *live* listModels() result, which is
397
+ // usually not a member of `provider.models` (the static generated
398
+ // list, kept only as a last-resort default). Validating it against
399
+ // that static list here would silently discard a perfectly valid
400
+ // live-picked model and substitute a possibly-stale/retired one
401
+ // instead. For any other (fallback) provider in this loop,
402
+ // preferredModel genuinely doesn't apply, so fall back to its first
403
+ // static model.
404
+ const model = providerId === config.preferredProvider
405
+ ? (config.preferredModel || provider.models[0]?.id || '')
406
+ : (provider.models[0]?.id || '');
407
+ const callId = `call-${nextCallId++}`;
408
+ activeCalls.set(callId, { providerName: provider.name, model });
409
+ notifySubscribers();
410
+ try {
411
+ return await invokeProvider(provider, model, key, prompt, systemPrompt, responseSchema);
412
+ }
413
+ catch (e) {
414
+ console.warn(`Provider ${providerId} failed:`, e);
415
+ lastError = `[${provider.name} / ${model}] ${e.message || String(e)}`;
416
+ continue;
417
+ }
418
+ finally {
419
+ activeCalls.delete(callId);
420
+ notifySubscribers();
421
+ }
422
+ }
423
+ throw new Error(`AI generation failed. Last error: ${lastError}`);
424
+ }
425
+ export async function callAI(prompt, systemPrompt, config, options) {
426
+ const response = await resolveAndInvoke(prompt, systemPrompt, config);
427
+ return options?.clean === 'voice' ? cleanVoiceResponse(response) : response;
428
+ }
429
+ /**
430
+ * Like callAI, but requests and validates a JSON response matching `schema`
431
+ * instead of returning free text — the explicit contract that replaced
432
+ * sniffing the system prompt for keywords like "duration"/"synopsis".
433
+ */
434
+ export async function callAIStructured(prompt, systemPrompt, config, schema) {
435
+ const response = await resolveAndInvoke(prompt, systemPrompt, config, schema);
436
+ return extractJson(response);
437
+ }
438
+ /** Any provider (from the fetched + custom list) with a stored API key. */
439
+ export async function hasConfiguredApiKey(config) {
440
+ const providers = await fetchProviders(config);
441
+ for (const p of providers) {
442
+ if (await getApiKey(p.id))
443
+ return true;
444
+ }
445
+ return false;
446
+ }
447
+ /**
448
+ * Fetches the provider's live model list so Settings/Onboarding show
449
+ * current models instead of a potentially stale static one. Never throws —
450
+ * falls back to `provider.models` on any error (missing endpoint, bad key,
451
+ * network failure), since this is a progressive enhancement.
452
+ */
453
+ export async function listModels(provider, apiKey) {
454
+ try {
455
+ const isAnthropic = provider.id === 'anthropic';
456
+ const url = isAnthropic ? 'https://api.anthropic.com/v1/models' : `${provider.baseUrl}/models`;
457
+ const headers = isAnthropic
458
+ ? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }
459
+ : { 'Authorization': `Bearer ${apiKey}` };
460
+ const res = await requestHttp(url, 'GET', headers, null);
461
+ if (res.status < 200 || res.status >= 300) {
462
+ throw new Error(`HTTP ${res.status}`);
463
+ }
464
+ const data = res.data?.data;
465
+ if (!Array.isArray(data) || data.length === 0) {
466
+ throw new Error('Provider returned an empty model list');
467
+ }
468
+ return data.map((m) => ({
469
+ id: m.id,
470
+ name: m.display_name || m.id,
471
+ }));
472
+ }
473
+ catch (e) {
474
+ console.warn(`Failed to list live models for ${provider.id}, using static list:`, e);
475
+ return provider.models;
476
+ }
477
+ }
478
+ export async function testConnection(providerId, apiKey, config) {
479
+ const providers = await fetchProviders(config);
480
+ const provider = providers.find(p => p.id === providerId);
481
+ if (!provider)
482
+ throw new Error('Unknown provider');
483
+ const liveModels = await listModels(provider, apiKey);
484
+ const testModel = liveModels[0]?.id;
485
+ if (!testModel)
486
+ throw new Error('No models found for provider');
487
+ const resText = await invokeProvider(provider, testModel, apiKey, "Hello, reply with one word: OK", "Connection test verification.");
488
+ return resText.trim().length > 0;
489
+ }
490
+ export function mockActiveCallsList(calls) {
491
+ activeCalls.clear();
492
+ calls.forEach((c, idx) => activeCalls.set(`mock-${idx}`, c));
493
+ notifySubscribers();
494
+ }
@@ -0,0 +1,12 @@
1
+ export type CommandType = 'scene' | 'character' | 'dialogue' | 'parenthetical' | 'transition' | 'lyrics' | 'action' | 'literal' | 'plain';
2
+ export interface CommandDefinition {
3
+ type: CommandType;
4
+ keywords: string[];
5
+ }
6
+ export declare const LOCALIZED_COMMANDS: Record<string, CommandDefinition[]>;
7
+ export interface ParsedVoiceResult {
8
+ type: CommandType | 'action';
9
+ text: string;
10
+ isLiteral: boolean;
11
+ }
12
+ export declare function parseVoiceCommand(transcript: string, langCode: string): ParsedVoiceResult;
@@ -0,0 +1,140 @@
1
+ export const LOCALIZED_COMMANDS = {
2
+ en: [
3
+ { type: 'literal', keywords: ['literal', 'plain', 'bypass'] },
4
+ { type: 'scene', keywords: ['new scene', 'scene heading', 'scene'] },
5
+ { type: 'character', keywords: ['character name', 'character', 'char'] },
6
+ { type: 'dialogue', keywords: ['dialogue', 'dialog', 'says'] },
7
+ { type: 'parenthetical', keywords: ['parenthetical', 'bracket', 'brackets'] },
8
+ { type: 'transition', keywords: ['transition', 'fade out', 'fade in', 'cut to'] },
9
+ { type: 'lyrics', keywords: ['lyrics', 'sing lyrics', 'song'] },
10
+ { type: 'action', keywords: ['action description', 'action'] },
11
+ ],
12
+ hi: [
13
+ { type: 'literal', keywords: ['शब्दशः', 'वास्तविक', 'प्लेन'] },
14
+ { type: 'scene', keywords: ['नया दृश्य', 'दृश्य'] },
15
+ { type: 'character', keywords: ['पात्र नाम', 'पात्र', 'चरित्र'] },
16
+ { type: 'dialogue', keywords: ['संवाद', 'बोलता है', 'बोलती है'] },
17
+ { type: 'parenthetical', keywords: ['कोष्ठक', 'ब्रैकेट'] },
18
+ { type: 'transition', keywords: ['बदलाव', 'कट टू'] },
19
+ { type: 'lyrics', keywords: ['गीत', 'गाना'] },
20
+ { type: 'action', keywords: ['विवरण', 'एक्शन'] },
21
+ ],
22
+ ta: [
23
+ { type: 'literal', keywords: ['உண்மையான', 'வார்த்தை', 'நேரடி'] },
24
+ { type: 'scene', keywords: ['புதிய காட்சி', 'காட்சி'] },
25
+ { type: 'character', keywords: ['கதாபாத்திரம் பெயர்', 'கதாபாத்திரம்'] },
26
+ { type: 'dialogue', keywords: ['வசனம்', 'பேசுகிறார்'] },
27
+ { type: 'parenthetical', keywords: ['அடைப்புக்குறி', 'பிராக்கெட்'] },
28
+ { type: 'transition', keywords: ['மாற்றம்', 'கட் டூ'] },
29
+ { type: 'lyrics', keywords: ['பாடல்', 'பாட்டு'] },
30
+ { type: 'action', keywords: ['செயல்', 'விளக்கம்'] },
31
+ ],
32
+ te: [
33
+ { type: 'literal', keywords: ['యథార్థం', 'సాధారణ'] },
34
+ { type: 'scene', keywords: ['కొత్త సన్నివేశం', 'సన్నివేశం'] },
35
+ { type: 'character', keywords: ['పాత్ర పేరు', 'పాత్ర'] },
36
+ { type: 'dialogue', keywords: ['సంభాషణ', 'మాట్లాడు'] },
37
+ { type: 'parenthetical', keywords: ['బ్రాకెట్', 'వరణము'] },
38
+ { type: 'transition', keywords: ['మార్పు', 'కట్ టు'] },
39
+ { type: 'lyrics', keywords: ['పాట', 'గీతం'] },
40
+ { type: 'action', keywords: ['చర్య', 'వివరణ'] },
41
+ ],
42
+ ml: [
43
+ { type: 'literal', keywords: ['യഥാർത്ഥ', 'സാധാരണ'] },
44
+ { type: 'scene', keywords: ['പുതിയ രംഗം', 'രംഗം'] },
45
+ { type: 'character', keywords: ['കഥാപാത്രം പേര്', 'കഥാപാത്രം'] },
46
+ { type: 'dialogue', keywords: ['സംഭാഷണം', 'പറയുന്നു'] },
47
+ { type: 'parenthetical', keywords: ['ബ്രാക്കറ്റ്'] },
48
+ { type: 'transition', keywords: ['മാറ്റം', 'കട്ട് ടു'] },
49
+ { type: 'lyrics', keywords: ['പാട്ട്', 'ഗാനം'] },
50
+ { type: 'action', keywords: ['പ്രവർത്തി', 'വിവരണം'] },
51
+ ],
52
+ kn: [
53
+ { type: 'literal', keywords: ['ನಿಜವಾದ', 'ಸಾಮಾನ್ಯ'] },
54
+ { type: 'scene', keywords: ['ಹೊಸ ದೃಶ್ಯ', 'ದೃಶ್ಯ'] },
55
+ { type: 'character', keywords: ['ಪಾತ್ರ ಹೆಸರು', 'ಪಾತ್ರ'] },
56
+ { type: 'dialogue', keywords: ['ಸಂಭಾಷಣೆ', 'ಹೇಳುತ್ತದೆ'] },
57
+ { type: 'parenthetical', keywords: ['ವರಣಚಿಹ್ನೆ', 'ಬ್ರಾಕೆಟ್'] },
58
+ { type: 'transition', keywords: ['ಬದಲಾವಣೆ', 'ಕಟ್ ಟು'] },
59
+ { type: 'lyrics', keywords: ['ಹಾಡು', 'ಗೀತೆ'] },
60
+ { type: 'action', keywords: ['ಕ್ರಿಯೆ', 'ವಿವರಣೆ'] },
61
+ ],
62
+ bn: [
63
+ { type: 'literal', keywords: ['আক্ষরিক', 'সাধারণ'] },
64
+ { type: 'scene', keywords: ['নতুন দৃশ্য', 'দৃশ্য'] },
65
+ { type: 'character', keywords: ['চরিত্রের নাম', 'চরিত্র'] },
66
+ { type: 'dialogue', keywords: ['সংলাপ', 'বলে'] },
67
+ { type: 'parenthetical', keywords: ['বন্ধনী', 'ব্র্যাকেট'] },
68
+ { type: 'transition', keywords: ['পরিবর্তন', 'কাট টু'] },
69
+ { type: 'lyrics', keywords: ['গান', 'গীতি'] },
70
+ { type: 'action', keywords: ['ক্রিয়া', 'বর্ণনা'] },
71
+ ],
72
+ gu: [
73
+ { type: 'literal', keywords: ['શાબ્દિક', 'સામાન્ય'] },
74
+ { type: 'scene', keywords: ['નવું દ્રશ્ય', 'દ્રશ્ય'] },
75
+ { type: 'character', keywords: ['પાત્રનું નામ', 'પાત્ર'] },
76
+ { type: 'dialogue', keywords: ['સંવાદ', 'બોલે છે'] },
77
+ { type: 'parenthetical', keywords: ['કૌંસ', 'બ્રેકેટ'] },
78
+ { type: 'transition', keywords: ['પરિવર્તન', 'કટ ટુ'] },
79
+ { type: 'lyrics', keywords: ['ગીત', 'ગાણું'] },
80
+ { type: 'action', keywords: ['ક્રિયા', 'વર્ણન'] },
81
+ ],
82
+ mr: [
83
+ { type: 'literal', keywords: ['शाब्दिक', 'साधे'] },
84
+ { type: 'scene', keywords: ['नवीन दृश्य', 'दृश्य'] },
85
+ { type: 'character', keywords: ['पात्राचे नाव', 'पात्र'] },
86
+ { type: 'dialogue', keywords: ['संवाद', 'बोलतो', 'बोलते'] },
87
+ { type: 'parenthetical', keywords: ['कंसात', 'ब्रॅकेट'] },
88
+ { type: 'transition', keywords: ['बदल', 'कट टू'] },
89
+ { type: 'lyrics', keywords: ['गाणे', 'गीत'] },
90
+ { type: 'action', keywords: ['कृती', 'वर्णन'] },
91
+ ],
92
+ };
93
+ export function parseVoiceCommand(transcript, langCode) {
94
+ const normText = transcript.trim();
95
+ // Get language mapping, default to English if not found
96
+ const baseLang = langCode.split('-')[0] || 'en';
97
+ const defs = LOCALIZED_COMMANDS[baseLang] || LOCALIZED_COMMANDS.en;
98
+ // 1. Check for literal bypass sequence
99
+ const literalDef = defs.find(d => d.type === 'literal');
100
+ if (literalDef) {
101
+ for (const kw of literalDef.keywords) {
102
+ if (normText.toLowerCase().startsWith(kw + ' ')) {
103
+ return {
104
+ type: 'action',
105
+ text: normText.substring(kw.length + 1).trim(),
106
+ isLiteral: true,
107
+ };
108
+ }
109
+ }
110
+ }
111
+ // 2. Scan other formatting keywords
112
+ for (const def of defs) {
113
+ if (def.type === 'literal')
114
+ continue;
115
+ for (const kw of def.keywords) {
116
+ // Check if text starts with the keyword followed by space or is exactly the keyword
117
+ const prefix = kw + ' ';
118
+ if (normText.toLowerCase().startsWith(prefix)) {
119
+ return {
120
+ type: def.type,
121
+ text: normText.substring(kw.length + 1).trim(),
122
+ isLiteral: false,
123
+ };
124
+ }
125
+ else if (normText.toLowerCase() === kw) {
126
+ return {
127
+ type: def.type,
128
+ text: '',
129
+ isLiteral: false,
130
+ };
131
+ }
132
+ }
133
+ }
134
+ // 3. Fallback to default action text
135
+ return {
136
+ type: 'action',
137
+ text: normText,
138
+ isLiteral: false,
139
+ };
140
+ }