@sutrata/editor 0.1.1 → 0.2.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 (268) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  3. package/dist/ai/VoiceConfirmDialog.js +6 -6
  4. package/dist/ai/VoiceToolbar.d.ts +1 -0
  5. package/dist/ai/VoiceToolbar.js +22 -16
  6. package/dist/ai/clean-response.d.ts +7 -0
  7. package/dist/ai/clean-response.js +58 -0
  8. package/dist/ai/format-transcript.d.ts +3 -0
  9. package/dist/ai/format-transcript.js +7 -0
  10. package/dist/ai/prompts.d.ts +8 -1
  11. package/dist/ai/prompts.js +34 -7
  12. package/dist/autocomplete/suggestions.js +1 -1
  13. package/dist/context/DocumentContext.d.ts +28 -4
  14. package/dist/context/DocumentContext.js +67 -37
  15. package/dist/editor/EditorView.js +32 -4
  16. package/dist/editor/SelectionMenu.js +5 -3
  17. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  18. package/dist/editor/external-decorations-plugin.js +151 -0
  19. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  20. package/dist/editor/omitted-scene-plugin.js +44 -0
  21. package/dist/editor/plugins.js +4 -0
  22. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  23. package/dist/editor/private-metadata-plugin.js +60 -0
  24. package/dist/editor/prosemirror-to-sutra.js +179 -74
  25. package/dist/editor/scene-heading-view.js +2 -0
  26. package/dist/editor/schema.js +69 -12
  27. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  28. package/dist/editor/sutra-to-prosemirror.js +32 -34
  29. package/dist/extensions/ai-provider.d.ts +44 -0
  30. package/dist/extensions/ai-provider.js +27 -0
  31. package/dist/extensions/collab.d.ts +21 -0
  32. package/dist/extensions/collab.js +6 -0
  33. package/dist/extensions/command-registry.d.ts +49 -0
  34. package/dist/extensions/command-registry.js +28 -0
  35. package/dist/extensions/decorations.d.ts +48 -0
  36. package/dist/extensions/decorations.js +6 -0
  37. package/dist/extensions/export-registry.d.ts +59 -0
  38. package/dist/extensions/export-registry.js +10 -0
  39. package/dist/extensions/panel-registry.d.ts +30 -0
  40. package/dist/extensions/panel-registry.js +16 -0
  41. package/dist/extensions/registries.d.ts +15 -0
  42. package/dist/extensions/registries.js +12 -0
  43. package/dist/extensions/registry-store.d.ts +26 -0
  44. package/dist/extensions/registry-store.js +35 -0
  45. package/dist/extensions/session.d.ts +29 -0
  46. package/dist/extensions/session.js +20 -0
  47. package/dist/extensions/speech-provider.d.ts +35 -0
  48. package/dist/extensions/speech-provider.js +17 -0
  49. package/dist/file/builtin-exporters.d.ts +9 -0
  50. package/dist/file/builtin-exporters.js +72 -0
  51. package/dist/file/docx-exporter.js +7 -4
  52. package/dist/file/docx-importer.d.ts +9 -0
  53. package/dist/file/docx-importer.js +47 -19
  54. package/dist/file/scene-number.d.ts +5 -0
  55. package/dist/file/scene-number.js +8 -0
  56. package/dist/file/workflow-reports.js +13 -7
  57. package/dist/findreplace/FindReplace.js +5 -2
  58. package/dist/import/AIImportDialog.d.ts +11 -0
  59. package/dist/import/AIImportDialog.js +112 -0
  60. package/dist/import/ai-import.d.ts +51 -0
  61. package/dist/import/ai-import.js +200 -0
  62. package/dist/import/apply-import.d.ts +7 -0
  63. package/dist/import/apply-import.js +35 -0
  64. package/dist/import/extract.d.ts +17 -0
  65. package/dist/import/extract.js +18 -0
  66. package/dist/index.d.ts +17 -2
  67. package/dist/index.js +7 -0
  68. package/dist/locales/bn.json +8 -0
  69. package/dist/locales/en.json +8 -0
  70. package/dist/locales/gu.json +8 -0
  71. package/dist/locales/hi.json +8 -0
  72. package/dist/locales/kn.json +8 -0
  73. package/dist/locales/ml.json +8 -0
  74. package/dist/locales/mr.json +8 -0
  75. package/dist/locales/or.json +8 -0
  76. package/dist/locales/pa.json +8 -0
  77. package/dist/locales/si.json +8 -0
  78. package/dist/locales/ta.json +8 -0
  79. package/dist/locales/te.json +8 -0
  80. package/dist/navigator/SceneNavigator.js +125 -131
  81. package/dist/navigator/StatisticsDialog.js +5 -1
  82. package/dist/navigator/scene-edits.d.ts +18 -0
  83. package/dist/navigator/scene-edits.js +143 -0
  84. package/dist/navigator/scene-list.js +4 -4
  85. package/dist/navigator/scene-numbering.d.ts +37 -0
  86. package/dist/navigator/scene-numbering.js +88 -0
  87. package/dist/settings/SettingsDialog.js +8 -120
  88. package/dist/shell/AppBar.js +21 -2
  89. package/dist/shell/AppShell.js +18 -4
  90. package/dist/shell/ElementToolbar.d.ts +5 -0
  91. package/dist/shell/ElementToolbar.js +35 -25
  92. package/dist/shell/LanguageStatus.js +6 -0
  93. package/dist/shell/StatusBar.js +11 -6
  94. package/dist/shell/builtin-commands.d.ts +10 -0
  95. package/dist/shell/builtin-commands.js +43 -0
  96. package/dist/shell/icons.d.ts +6 -0
  97. package/dist/shell/icons.js +6 -0
  98. package/dist/shell/use-commands.d.ts +7 -0
  99. package/dist/shell/use-commands.js +24 -0
  100. package/dist/shell/use-panel-context.d.ts +3 -0
  101. package/dist/shell/use-panel-context.js +39 -0
  102. package/dist/source/SourceView.js +18 -1
  103. package/dist/styles/screenplay.css +113 -1
  104. package/dist/titlepage/ExportDialog.d.ts +5 -0
  105. package/dist/titlepage/ExportDialog.js +59 -96
  106. package/package.json +32 -5
  107. package/dist/ai/OnboardingDialog.d.ts +0 -6
  108. package/dist/ai/OnboardingDialog.js +0 -115
  109. package/dist/ai/ai-client.d.ts +0 -71
  110. package/dist/ai/ai-client.js +0 -494
  111. package/dist/ai/generated-providers.json +0 -3696
  112. package/dist/ai/voice-service.d.ts +0 -15
  113. package/dist/ai/voice-service.js +0 -81
  114. package/dist/editor/character-view.d.ts +0 -35
  115. package/dist/editor/character-view.js +0 -112
  116. package/dist/editor/transliterate-input.d.ts +0 -10
  117. package/dist/editor/transliterate-input.js +0 -88
  118. package/dist/extensions/types.d.ts +0 -73
  119. package/dist/extensions/types.js +0 -12
  120. package/scripts/copy-assets.mjs +0 -16
  121. package/scripts/generate-providers.mjs +0 -127
  122. package/src/ai/OnboardingDialog.tsx +0 -390
  123. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  124. package/src/ai/VoiceToolbar.tsx +0 -321
  125. package/src/ai/ai-client.ts +0 -596
  126. package/src/ai/commands.ts +0 -164
  127. package/src/ai/generated-providers.json +0 -3696
  128. package/src/ai/prompts.ts +0 -62
  129. package/src/ai/voice-service.ts +0 -96
  130. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  131. package/src/autocomplete/suggestions.ts +0 -147
  132. package/src/context/DocumentContext.tsx +0 -648
  133. package/src/desktop-api.d.ts +0 -40
  134. package/src/editor/EditorView.tsx +0 -82
  135. package/src/editor/SelectionMenu.tsx +0 -152
  136. package/src/editor/attr-pencil-plugin.ts +0 -172
  137. package/src/editor/character-decoration-plugin.ts +0 -104
  138. package/src/editor/editor-bus.ts +0 -51
  139. package/src/editor/find-highlight-plugin.ts +0 -68
  140. package/src/editor/frontmatter-field-view.ts +0 -52
  141. package/src/editor/frontmatter-field.ts +0 -115
  142. package/src/editor/grapheme-cursor.ts +0 -99
  143. package/src/editor/ime-plugin.ts +0 -85
  144. package/src/editor/insert-helper.ts +0 -43
  145. package/src/editor/keymap.ts +0 -124
  146. package/src/editor/language-class-plugin.ts +0 -91
  147. package/src/editor/plugins.ts +0 -26
  148. package/src/editor/prosemirror-to-sutra.ts +0 -192
  149. package/src/editor/scene-heading-view.ts +0 -147
  150. package/src/editor/schema.ts +0 -156
  151. package/src/editor/sutra-to-prosemirror.ts +0 -156
  152. package/src/extensions/storage-adapter.ts +0 -65
  153. package/src/extensions/types.ts +0 -60
  154. package/src/file/docx-exporter.ts +0 -585
  155. package/src/file/docx-importer.ts +0 -313
  156. package/src/file/print-paginator.ts +0 -301
  157. package/src/file/templates.ts +0 -73
  158. package/src/file/workflow-reports.ts +0 -820
  159. package/src/findreplace/FindReplace.tsx +0 -242
  160. package/src/findreplace/find-engine.ts +0 -97
  161. package/src/findreplace/scroll-to-match.ts +0 -89
  162. package/src/fonts/tofu-detect.ts +0 -53
  163. package/src/i18n/LanguageContext.tsx +0 -207
  164. package/src/i18n/language-detect.ts +0 -73
  165. package/src/i18n/useTranslation.tsx +0 -74
  166. package/src/index.ts +0 -27
  167. package/src/locales/bn.json +0 -143
  168. package/src/locales/en.json +0 -143
  169. package/src/locales/gu.json +0 -143
  170. package/src/locales/hi.json +0 -143
  171. package/src/locales/kn.json +0 -143
  172. package/src/locales/ml.json +0 -143
  173. package/src/locales/mr.json +0 -143
  174. package/src/locales/or.json +0 -143
  175. package/src/locales/pa.json +0 -143
  176. package/src/locales/si.json +0 -143
  177. package/src/locales/ta.json +0 -143
  178. package/src/locales/te.json +0 -143
  179. package/src/navigator/SceneNavigator.tsx +0 -844
  180. package/src/navigator/StatisticsDialog.tsx +0 -249
  181. package/src/navigator/scene-list.ts +0 -84
  182. package/src/settings/SettingsDialog.tsx +0 -303
  183. package/src/shell/AppBar.tsx +0 -354
  184. package/src/shell/AppShell.tsx +0 -71
  185. package/src/shell/ConfirmDialog.tsx +0 -71
  186. package/src/shell/ElementToolbar.tsx +0 -175
  187. package/src/shell/LanguageStatus.tsx +0 -101
  188. package/src/shell/StatusBar.tsx +0 -135
  189. package/src/shell/SutrataLogo.tsx +0 -89
  190. package/src/shell/Toast.tsx +0 -46
  191. package/src/shell/ToolbarButton.tsx +0 -30
  192. package/src/shell/VersionHistory.tsx +0 -212
  193. package/src/shell/Watermark.tsx +0 -9
  194. package/src/shell/fonts/fonts.css +0 -1133
  195. package/src/shell/icons.tsx +0 -335
  196. package/src/shell/toolbar-actions.ts +0 -59
  197. package/src/source/SourceView.tsx +0 -111
  198. package/src/source/find-highlight-source.ts +0 -57
  199. package/src/source/sutra-lang.ts +0 -121
  200. package/src/spellcheck/dictionaries.ts +0 -21
  201. package/src/spellcheck/hunspell.ts +0 -110
  202. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  203. package/src/styles/StyleDialog.tsx +0 -148
  204. package/src/styles/builtin-styles.ts +0 -139
  205. package/src/styles/css-adapter.ts +0 -76
  206. package/src/styles/docx-adapter.ts +0 -109
  207. package/src/styles/embed-fonts.ts +0 -81
  208. package/src/styles/pagination.ts +0 -27
  209. package/src/styles/print-adapter.ts +0 -288
  210. package/src/styles/registry.ts +0 -20
  211. package/src/styles/screenplay.css +0 -3006
  212. package/src/styles/script-ranges.ts +0 -17
  213. package/src/styles/types.ts +0 -104
  214. package/src/styles/validate.ts +0 -114
  215. package/src/templates/templates.ts +0 -194
  216. package/src/titlepage/ExportDialog.tsx +0 -494
  217. package/src/titlepage/TitlePageDialog.tsx +0 -312
  218. package/src/titlepage/frontmatter-form.ts +0 -280
  219. package/src/titlepage/languages.ts +0 -37
  220. package/src/types.ts +0 -7
  221. package/tests/AppBar.test.tsx +0 -35
  222. package/tests/AppShell.test.tsx +0 -22
  223. package/tests/ElementToolbar.test.tsx +0 -36
  224. package/tests/LanguageStatus.test.tsx +0 -129
  225. package/tests/SelectionMenu.test.tsx +0 -71
  226. package/tests/ToolbarButton.test.tsx +0 -24
  227. package/tests/ai-onboarding.test.ts +0 -102
  228. package/tests/ai-status.test.tsx +0 -72
  229. package/tests/ai-structured.test.ts +0 -94
  230. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  231. package/tests/autocomplete/suggestions.test.ts +0 -106
  232. package/tests/document-context.test.tsx +0 -565
  233. package/tests/docx-importer.test.ts +0 -160
  234. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  235. package/tests/editor/character-extension.test.ts +0 -44
  236. package/tests/editor/grapheme-cursor.test.ts +0 -118
  237. package/tests/editor/ime-plugin.test.ts +0 -72
  238. package/tests/editor/inline-note-bridge.test.ts +0 -32
  239. package/tests/editor/language-class-plugin.test.ts +0 -265
  240. package/tests/editor/title-page.test.ts +0 -124
  241. package/tests/embed-fonts.test.ts +0 -17
  242. package/tests/exporters.test.ts +0 -365
  243. package/tests/findreplace/FindReplace.test.tsx +0 -391
  244. package/tests/findreplace/find-engine.test.ts +0 -86
  245. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  246. package/tests/fonts/tofu-detect.test.ts +0 -122
  247. package/tests/i18n/LanguageContext.test.tsx +0 -146
  248. package/tests/i18n/language-detect.test.ts +0 -54
  249. package/tests/i18n/locale-parity.test.ts +0 -25
  250. package/tests/i18n/useTranslation.test.tsx +0 -45
  251. package/tests/keymap.test.ts +0 -18
  252. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  253. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  254. package/tests/navigator/scene-list.test.ts +0 -71
  255. package/tests/print-paginator.test.ts +0 -82
  256. package/tests/shell/VersionHistory.test.tsx +0 -129
  257. package/tests/spellcheck/hunspell.test.ts +0 -82
  258. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  259. package/tests/styles.test.ts +0 -315
  260. package/tests/templates/templates.test.ts +0 -83
  261. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  262. package/tests/titlepage/apply-title-page.test.ts +0 -61
  263. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  264. package/tests/toolbar-actions.test.ts +0 -93
  265. package/tests/voice-commands.test.ts +0 -58
  266. package/tsconfig.json +0 -17
  267. package/vitest.config.ts +0 -11
  268. package/vitest.setup.ts +0 -65
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @sutrata/editor
2
+
3
+ The React screenplay editor behind [Sutrata](https://github.com/sutrata/sutrata), for writing scripts in **Sutra**, a plain-text screenplay format for world languages. It's built on ProseMirror and handles Indic and other complex scripts properly:
4
+ - Text is stored in logical order.
5
+ - The cursor and selection move by grapheme cluster (user-perceived character).
6
+ - Find/replace matches after NFC normalization, so differently encoded forms of the same text still match.
7
+
8
+ This package contains the complete editor:
9
+ - WYSIWYG and source modes, with smart Enter that cycles through screenplay elements.
10
+ - A scene navigator, find/replace, a title-page form and screenplay styles.
11
+ - Statistics, autocomplete and spellcheck.
12
+ - Export to PDF, DOCX, Fountain and workflow reports, including romanized exports.
13
+ - Localized UI.
14
+
15
+ Sutra parsing and serialization come from [`@sutrata/parser`](https://www.npmjs.com/package/@sutrata/parser).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @sutrata/editor react react-dom
21
+ ```
22
+
23
+ `react` and `react-dom` (^18.3) are peer dependencies.
24
+
25
+ ## Usage
26
+
27
+ The editor doesn't read or write files itself. You pass it a `StorageAdapter`, and it saves, opens and keeps version history through that adapter. The same editor can therefore run on local files, in a desktop shell or against a server.
28
+
29
+ ```tsx
30
+ import React from 'react'
31
+ import { createRoot } from 'react-dom/client'
32
+ import {
33
+ TranslationProvider,
34
+ LanguageProvider,
35
+ DocumentProvider,
36
+ AppShell,
37
+ type StorageAdapter,
38
+ } from '@sutrata/editor'
39
+ import '@sutrata/editor/dist/shell/fonts/fonts.css'
40
+ import '@sutrata/editor/dist/styles/screenplay.css'
41
+
42
+ const storageAdapter: StorageAdapter = {
43
+ // documents and version history
44
+ saveDocument: async (path, content) => { /* ... */ },
45
+ loadDocument: async (path) => null,
46
+ listVersions: async (path) => [],
47
+ // file dialogs
48
+ openFile: async () => null, // { name, content, handle }
49
+ openDocx: async () => null, // { name, buffer }
50
+ openStyleJson: async () => null, // { name, text }
51
+ saveFile: async (name, content, handle) => null, // { savedName, handle }
52
+ // custom screenplay styles
53
+ saveStyle: async (style) => {},
54
+ loadAllStyles: async () => [],
55
+ deleteStyle: async (id) => {},
56
+ }
57
+
58
+ createRoot(document.getElementById('root')!).render(
59
+ <TranslationProvider>
60
+ <LanguageProvider>
61
+ <DocumentProvider storageAdapter={storageAdapter}>
62
+ <AppShell />
63
+ </DocumentProvider>
64
+ </LanguageProvider>
65
+ </TranslationProvider>,
66
+ )
67
+ ```
68
+
69
+ `StorageAdapter` also has optional methods: `saveVersion`, `deleteAllVersions`, `persistFileHandle` and `restoreFileHandle`. For a complete implementation built on IndexedDB and the File System Access API, see [`local-storage-adapter.ts`](https://github.com/sutrata/sutrata/blob/main/packages/app/src/local-storage-adapter.ts) in the Sutrata app.
70
+
71
+ Inside the providers, `useDocument()` gives you the current Sutra text, its parsed AST and the editor state.
72
+
73
+ ### Fonts
74
+
75
+ `fonts.css` declares Noto font faces for every supported script, loaded from `/fonts/*.woff2` on your own site. This package does not include the font files. Copy them from [`packages/app/public/fonts`](https://github.com/sutrata/sutrata/tree/main/packages/app/public/fonts) into your app's `/fonts/` path, or use your own `@font-face` rules. Without them, some scripts may display as missing-glyph boxes (tofu).
76
+
77
+ ## Extension points
78
+
79
+ Beyond `StorageAdapter`, the package exports type definitions for more extension points: `AIProvider`, `SpeechProvider`, `SessionContext`, `PanelRegistry`, `CommandRegistry`, `ExportRegistry`, `CollabBinding` and `DecorationProvider`. **These are type-only previews and are not wired into the editor yet.** Their shapes may change before 1.0.
80
+
81
+ ## Status
82
+
83
+ Pre-1.0. The API may change between minor versions.
84
+
85
+ ## License
86
+
87
+ [GPL-3.0-or-later](https://github.com/sutrata/sutrata/blob/main/packages/editor/LICENSE).
@@ -5,5 +5,6 @@ interface Props {
5
5
  onConfirm: (formattedText: string) => void;
6
6
  onCancel: () => void;
7
7
  }
8
+ /** Rendered only where useAI() is non-null (VoiceToolbar, SelectionMenu's Format). */
8
9
  export declare function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCancel }: Props): React.JSX.Element;
9
10
  export {};
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
- import { callAI, getAIConfig } from './ai-client';
4
- import { VOICE_FORMAT_SYSTEM_PROMPT } from './prompts';
3
+ import { useAI } from '../extensions/ai-provider';
4
+ import { formatTranscript as formatWithAI } from './format-transcript';
5
5
  import { SparklesIcon } from '../shell/icons';
6
+ /** Rendered only where useAI() is non-null (VoiceToolbar, SelectionMenu's Format). */
6
7
  export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCancel }) {
8
+ const ai = useAI();
7
9
  const [formattedText, setFormattedText] = useState('');
8
10
  const [isLoading, setIsLoading] = useState(true);
9
11
  const [error, setError] = useState('');
@@ -15,9 +17,7 @@ export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCance
15
17
  setIsLoading(true);
16
18
  setError('');
17
19
  try {
18
- const config = getAIConfig();
19
- const prompt = `Raw Voice Transcript (${langCode}):\n"${rawTranscript}"`;
20
- const result = await callAI(prompt, VOICE_FORMAT_SYSTEM_PROMPT, config, { clean: 'voice' });
20
+ const result = await formatWithAI(ai, rawTranscript, langCode);
21
21
  setFormattedText(result.trim());
22
22
  }
23
23
  catch (e) {
@@ -33,5 +33,5 @@ export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCance
33
33
  localStorage.setItem('sutrata_always_auto_insert', alwaysAutoInsert ? 'true' : 'false');
34
34
  onConfirm(formattedText);
35
35
  };
36
- return (_jsx("div", { className: "cs-dialog-overlay", onClick: onCancel, style: { zIndex: 310 }, children: _jsxs("div", { className: "cs-dialog cs-voice-confirm-dialog", role: "dialog", "aria-modal": "true", "aria-label": "Confirm Voice Dictation Formatting", onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(SparklesIcon, { size: 16 }), _jsx("span", { className: "cs-dialog-title", children: "Review Voice Formatting" })] }), _jsx("button", { type: "button", className: "cs-fr-close", onClick: onCancel, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { className: "cs-voice-confirm-body", children: isLoading ? (_jsxs("div", { className: "cs-voice-loading-state", children: [_jsx("div", { className: "cs-spinner", style: { width: '28px', height: '28px', borderWidth: '2.5px' } }), _jsx("span", { style: { fontSize: '13px', color: 'var(--cs-ui-text-dark-secondary, #a8a69d)' }, children: "Formatting speech into Sutra screenplay format..." })] })) : (_jsxs(_Fragment, { children: [error && (_jsxs("div", { className: "cs-voice-error", role: "alert", children: ["AI formatting notice: ", error, " (showing raw input below)"] })), _jsxs("div", { className: "cs-voice-grid", children: [_jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Raw Dictation" }), _jsx("div", { className: "cs-voice-box-raw", children: rawTranscript })] }), _jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Formatted Sutra" }), _jsx("textarea", { className: "cs-voice-box-formatted", value: formattedText, onChange: e => setFormattedText(e.target.value), rows: 8 })] })] }), _jsx("div", { className: "cs-voice-options", children: _jsxs("label", { className: "cs-voice-checkbox-label", children: [_jsx("input", { type: "checkbox", checked: alwaysAutoInsert, onChange: e => setAlwaysAutoInsert(e.target.checked) }), _jsx("span", { children: "Always auto-insert directly into screenplay (skip review)" })] }) }), _jsxs("div", { className: "cs-dialog-actions", style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' }, children: [_jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: onCancel, children: "Discard" }), _jsx("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleConfirm, children: "Insert Into Screenplay" })] })] })) })] }) }));
36
+ return (_jsx("div", { className: "cs-dialog-overlay", onClick: onCancel, style: { zIndex: 310 }, children: _jsxs("div", { className: "cs-dialog cs-voice-confirm-dialog", role: "dialog", "aria-modal": "true", "aria-label": "Review Formatting", onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(SparklesIcon, { size: 16 }), _jsx("span", { className: "cs-dialog-title", children: "Review Formatting" })] }), _jsx("button", { type: "button", className: "cs-fr-close", onClick: onCancel, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { className: "cs-voice-confirm-body", children: isLoading ? (_jsxs("div", { className: "cs-voice-loading-state", children: [_jsx("div", { className: "cs-spinner", style: { width: '28px', height: '28px', borderWidth: '2.5px' } }), _jsx("span", { style: { fontSize: '13px', color: 'var(--cs-ui-text-dark-secondary, #a8a69d)' }, children: "Formatting into Sutra screenplay format..." })] })) : (_jsxs(_Fragment, { children: [error && (_jsxs("div", { className: "cs-voice-error", role: "alert", children: ["AI formatting notice: ", error, " (showing raw input below)"] })), _jsxs("div", { className: "cs-voice-grid", children: [_jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Raw Content" }), _jsx("div", { className: "cs-voice-box-raw", tabIndex: 0, children: rawTranscript })] }), _jsxs("div", { className: "cs-voice-column", children: [_jsx("span", { className: "cs-voice-col-title", children: "Formatted Sutra" }), _jsx("textarea", { className: "cs-voice-box-formatted", value: formattedText, onChange: e => setFormattedText(e.target.value), rows: 8 })] })] }), _jsx("div", { className: "cs-voice-options", children: _jsxs("label", { className: "cs-voice-checkbox-label", children: [_jsx("input", { type: "checkbox", checked: alwaysAutoInsert, onChange: e => setAlwaysAutoInsert(e.target.checked) }), _jsx("span", { children: "Always auto-insert directly into screenplay (skip review)" })] }) }), _jsxs("div", { className: "cs-dialog-actions", style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' }, children: [_jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: onCancel, children: "Discard" }), _jsx("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleConfirm, children: "Insert Into Screenplay" })] })] })) })] }) }));
37
37
  }
@@ -1,2 +1,3 @@
1
1
  import React from 'react';
2
+ /** Rendered by AppShell only when useSpeech() (which implies useAI()) is non-null. */
2
3
  export declare function VoiceToolbar(): React.JSX.Element;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef } from 'react';
3
3
  import { useDocument } from '../context/DocumentContext';
4
- import { voiceServiceInstance } from './voice-service';
5
- import { getAIConfig, callAI, hasConfiguredApiKey } from './ai-client';
6
- import { VOICE_FORMAT_SYSTEM_PROMPT } from './prompts';
4
+ import { formatTranscript } from './format-transcript';
5
+ import { useAI } from '../extensions/ai-provider';
6
+ import { useSpeech } from '../extensions/speech-provider';
7
7
  import { insertSutra } from '../editor/insert-helper';
8
8
  import { VoiceConfirmDialog } from './VoiceConfirmDialog';
9
9
  import { LOCALIZED_COMMANDS } from './commands';
@@ -20,8 +20,12 @@ const VOICE_LANGUAGES = [
20
20
  { code: 'gu-IN', label: 'ગુજરાતી (Gujarati)', base: 'gu' },
21
21
  { code: 'mr-IN', label: 'मराठी (Marathi)', base: 'mr' },
22
22
  ];
23
+ /** Rendered by AppShell only when useSpeech() (which implies useAI()) is non-null. */
23
24
  export function VoiceToolbar() {
24
- const { mode, setVoiceActive, setAIOnboardingVisible, showToast } = useDocument();
25
+ const { mode, setVoiceActive, showToast } = useDocument();
26
+ const ai = useAI();
27
+ const speech = useSpeech();
28
+ const sessionRef = useRef(null);
25
29
  const { locale } = useTranslation();
26
30
  const defaultLang = VOICE_LANGUAGES.find(l => l.base === locale)?.code || 'en-US';
27
31
  const [langCode, setLangCode] = useState(defaultLang);
@@ -36,7 +40,7 @@ export function VoiceToolbar() {
36
40
  setLangCode(matched);
37
41
  }, [locale]);
38
42
  const canvasRef = useRef(null);
39
- const isSupported = voiceServiceInstance.isSupported();
43
+ const isSupported = speech.isSupported();
40
44
  // Audio waveform animation
41
45
  useEffect(() => {
42
46
  if (!isListening)
@@ -77,9 +81,7 @@ export function VoiceToolbar() {
77
81
  if (alwaysAuto) {
78
82
  setIsFormatting(true);
79
83
  try {
80
- const config = getAIConfig();
81
- const prompt = `Raw Voice Transcript (${langCode}):\n"${fullTranscript}"`;
82
- const result = await callAI(prompt, VOICE_FORMAT_SYSTEM_PROMPT, config, { clean: 'voice' });
84
+ const result = await formatTranscript(ai, fullTranscript, langCode);
83
85
  insertSutra(result.trim(), mode);
84
86
  setVoiceActive(false);
85
87
  showToast('Voice dictation inserted.', 'success');
@@ -97,16 +99,20 @@ export function VoiceToolbar() {
97
99
  }
98
100
  };
99
101
  const handleStart = async () => {
100
- const hasKey = await hasConfiguredApiKey(getAIConfig());
101
- if (!hasKey) {
102
- showToast('AI keys are required to format dictated text. Opening Setup...', 'info');
103
- setAIOnboardingVisible(true);
102
+ if (!(await ai.isConfigured())) {
103
+ if (ai.openSetup) {
104
+ showToast('AI must be set up to format dictated text. Opening Setup...', 'info');
105
+ ai.openSetup();
106
+ }
107
+ else {
108
+ showToast(`${ai.displayName} is not available right now.`, 'warn');
109
+ }
104
110
  return;
105
111
  }
106
112
  setError('');
107
113
  setInterimResult('');
108
114
  setIsListening(true);
109
- voiceServiceInstance.start(langCode, {
115
+ sessionRef.current = speech.listen(langCode, {
110
116
  onResult: (text, isFinal) => {
111
117
  if (isFinal) {
112
118
  setTranscript(prev => (prev ? prev + ' ' + text : text));
@@ -131,7 +137,7 @@ export function VoiceToolbar() {
131
137
  });
132
138
  };
133
139
  const handleStopListening = async () => {
134
- voiceServiceInstance.stop();
140
+ sessionRef.current?.stop();
135
141
  setIsListening(false);
136
142
  const fullTranscript = (transcript + ' ' + interimResult).trim();
137
143
  if (fullTranscript) {
@@ -147,7 +153,7 @@ export function VoiceToolbar() {
147
153
  await performFormatting(fullTranscript);
148
154
  };
149
155
  const handleCancel = () => {
150
- voiceServiceInstance.abort();
156
+ sessionRef.current?.abort();
151
157
  setIsListening(false);
152
158
  setVoiceActive(false);
153
159
  };
@@ -169,7 +175,7 @@ export function VoiceToolbar() {
169
175
  return (_jsxs("div", { className: "cs-voice-toolbar", role: "region", "aria-label": "Voice Dictation Toolbar", children: [_jsxs("div", { className: "cs-voice-toolbar-row", children: [_jsxs("div", { className: "cs-voice-toolbar-badge", children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: "Voice Dictation" })] }), _jsx("select", { className: "cs-voice-select", value: langCode, onChange: e => setLangCode(e.target.value), disabled: isListening, "aria-label": "Voice Language", children: VOICE_LANGUAGES.map(l => (_jsx("option", { value: l.code, children: l.label }, l.code))) }), isListening ? (_jsxs("div", { className: "cs-voice-listening-group", children: [_jsx("button", { type: "button", className: "cs-voice-btn-danger", onClick: handleStopListening, children: "Stop & Format" }), _jsx("canvas", { ref: canvasRef, width: 110, height: 26, className: "cs-voice-waveform", role: "img", "aria-label": "Audio waveform visualization" })] })) : (_jsxs("div", { className: "cs-voice-actions-group", children: [_jsxs("button", { type: "button", className: "cs-voice-btn-primary", onClick: handleStart, disabled: !isSupported || isFormatting, children: [_jsx(MicIcon, { size: 14 }), _jsx("span", { children: isFormatting ? 'Formatting...' : 'Start Dictation' })] }), transcript && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "cs-voice-btn-secondary", onClick: handleFormatCaptured, disabled: isFormatting, children: "Re-format" }), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: () => {
170
176
  setTranscript('');
171
177
  setError('');
172
- }, children: "Clear" })] }))] })), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: handleCancel, children: "Close" }), !isSupported && (_jsx("span", { className: "cs-voice-warning", children: "Web Speech API is not supported in this browser." }))] }), (isListening || transcript || interimResult) && (isListening ? (_jsxs("div", { className: "cs-voice-transcript-box", children: [transcript, interimResult && _jsxs("span", { className: "cs-voice-interim", children: [" ", interimResult] }), !transcript && !interimResult && _jsx("span", { className: "cs-voice-placeholder", children: "Speak now to dictate..." })] })) : (_jsx("textarea", { className: "cs-voice-textarea", value: transcript, onChange: (e) => setTranscript(e.target.value), placeholder: "Edit captured text before formatting...", rows: 2 }))), error && (_jsx("div", { className: "cs-voice-error", role: "alert", children: error })), _jsxs("div", { className: "cs-voice-cheatsheet", children: [_jsx("span", { className: "cs-voice-cheatsheet-title", children: "Commands:" }), cheatsheet.map(c => {
178
+ }, children: "Clear" })] }))] })), _jsx("button", { type: "button", className: "cs-voice-btn-ghost", onClick: handleCancel, children: "Close" }), !isSupported && (_jsxs("span", { className: "cs-voice-warning", children: [speech.displayName, " is not supported in this browser."] }))] }), (isListening || transcript || interimResult) && (isListening ? (_jsxs("div", { className: "cs-voice-transcript-box", children: [transcript, interimResult && _jsxs("span", { className: "cs-voice-interim", children: [" ", interimResult] }), !transcript && !interimResult && _jsx("span", { className: "cs-voice-placeholder", children: "Speak now to dictate..." })] })) : (_jsx("textarea", { className: "cs-voice-textarea", value: transcript, onChange: (e) => setTranscript(e.target.value), placeholder: "Edit captured text before formatting...", rows: 2 }))), error && (_jsx("div", { className: "cs-voice-error", role: "alert", children: error })), _jsxs("div", { className: "cs-voice-cheatsheet", children: [_jsx("span", { className: "cs-voice-cheatsheet-title", children: "Commands:" }), cheatsheet.map(c => {
173
179
  if (c.type === 'literal')
174
180
  return null;
175
181
  return (_jsxs("span", { className: "cs-voice-tag", children: [_jsx("span", { className: "cs-voice-tag-type", children: c.type }), ": \u201C", c.keywords[0], "\u201D"] }, c.type));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Strip the chatter models wrap around Sutra output (code fences, list
3
+ * bullets, restated rules, "->" reasoning lines), keeping the blank lines
4
+ * that separate Sutra blocks. Applied to every AI formatting result before
5
+ * it is inserted, whichever AIProvider produced it.
6
+ */
7
+ export declare function cleanVoiceResponse(text: string): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Strip the chatter models wrap around Sutra output (code fences, list
3
+ * bullets, restated rules, "->" reasoning lines), keeping the blank lines
4
+ * that separate Sutra blocks. Applied to every AI formatting result before
5
+ * it is inserted, whichever AIProvider produced it.
6
+ */
7
+ export function cleanVoiceResponse(text) {
8
+ let cleaned = text.trim();
9
+ // Strip markdown code block wrapper if present
10
+ cleaned = cleaned.replace(/^```[a-zA-Z0-9]*\n/, '').replace(/\n```$/, '');
11
+ const lines = cleaned.split('\n');
12
+ const filteredLines = lines.map(line => {
13
+ let l = line.trim();
14
+ // Strip leading list bullet (e.g. *, -, +, •) and spaces
15
+ l = l.replace(/^[*•\-+]\s*/, '').trim();
16
+ // Strip leading/trailing backticks if any
17
+ l = l.replace(/^`+/, '').replace(/`+$/, '').trim();
18
+ // Strip common explanatory trailing comments in parens
19
+ l = l.replace(/\s*\(Standard screenplay shorthand[^)]*\)/gi, '');
20
+ l = l.replace(/\s*\(Standard shorthand[^)]*\)/gi, '');
21
+ return l;
22
+ }).filter(line => {
23
+ const trimmed = line.toLowerCase();
24
+ // Keep blank lines: they separate Sutra blocks (a blank line ends dialogue).
25
+ if (!trimmed)
26
+ return true;
27
+ // Discard reasoning/mapping/explanation patterns
28
+ if (trimmed.includes('->') ||
29
+ trimmed.includes('=>') ||
30
+ trimmed.startsWith('input:') ||
31
+ trimmed.startsWith('task:') ||
32
+ trimmed.startsWith('rules:') ||
33
+ trimmed.startsWith('instructions:') ||
34
+ trimmed.startsWith('sutra rules:') ||
35
+ trimmed.startsWith('scene headings:') ||
36
+ trimmed.startsWith('scene id:') ||
37
+ trimmed.startsWith('characters:') ||
38
+ trimmed.startsWith('parentheticals:') ||
39
+ trimmed.startsWith('dialogue:') ||
40
+ trimmed.startsWith('transitions:') ||
41
+ trimmed.startsWith('lyrics:') ||
42
+ trimmed.startsWith('action:') ||
43
+ trimmed.startsWith('format:') ||
44
+ trimmed.startsWith('components:') ||
45
+ trimmed.startsWith('output only') ||
46
+ trimmed.startsWith('no markdown') ||
47
+ trimmed.includes('convert it into valid sutra') ||
48
+ trimmed.includes('sutra syntax rules') ||
49
+ trimmed.includes('clearly a scene heading') ||
50
+ trimmed.includes('screenplay shorthand') ||
51
+ trimmed.includes('append as')) {
52
+ return false;
53
+ }
54
+ return true;
55
+ });
56
+ // Collapse runs of blank lines left behind by discarded explanation lines.
57
+ return filteredLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { AIProvider } from '../extensions/ai-provider';
2
+ /** Format a dictated (or selected) transcript as Sutra with the injected AI provider. */
3
+ export declare function formatTranscript(ai: AIProvider, transcript: string, langCode: string): Promise<string>;
@@ -0,0 +1,7 @@
1
+ import { VOICE_FORMAT_SYSTEM_PROMPT } from './prompts';
2
+ import { cleanVoiceResponse } from './clean-response';
3
+ /** Format a dictated (or selected) transcript as Sutra with the injected AI provider. */
4
+ export async function formatTranscript(ai, transcript, langCode) {
5
+ const prompt = `Raw Voice Transcript (${langCode}):\n"${transcript}"`;
6
+ return cleanVoiceResponse(await ai.complete({ system: VOICE_FORMAT_SYSTEM_PROMPT, prompt }));
7
+ }
@@ -1,4 +1,4 @@
1
- export declare const VOICE_FORMAT_SYSTEM_PROMPT = "You are Sutrata's AI screenplay formatter.\nYour task is to take a raw voice dictation transcript (which may contain explicit formatting keywords like \"scene\", \"character\", \"dialogue\", \"parenthetical\", \"transition\") and convert it into valid Sutra screenplay format.\n\nSutra syntax rules:\n1. Scene Headings: Start with \"##\" (e.g., \"## INT. COFFEE SHOP - DAY\"). If a scene number/ID is specified (e.g., \"scene #2\", \"scene 2\", or \"scene 3A\"), append it as a Sutra attribute ID at the end of the scene heading, like \"{#2}\" or \"{#3A}\" (e.g., \"## EXT. SWIMMING POOL - DAY {#2}\").\n2. Characters: All-caps, alone on a line (e.g., \"RAJ\").\n3. Parentheticals: In parentheses, on a line by itself under character (e.g., \"(whispering)\").\n4. Dialogue: Indented text directly following a character or parenthetical.\n5. Transitions: In uppercase, prefixed with \">\" or ending with \"TO:\" (e.g., \"> CUT TO:\").\n6. Lyrics: Prefixed with \"~\" (e.g., \"~ Sing a song\").\n7. Action: Standard descriptive paragraph.\n\nCRITICAL INSTRUCTIONS:\n- Preserve Indic/Unicode characters and mixed-language code-switching exactly as transcribed. Do NOT translate non-English text to English.\n- Output ONLY the formatted Sutra text.\n- Do NOT include any explanations, steps, reasoning, bullet points, meta-commentary, or introductory/concluding text.\n- Do NOT output headings, drafts, prefixes, or suffixes.\n- Do NOT wrap in markdown code blocks like ```cine.\n- The output must contain nothing but the final formatted Sutra screenplay lines.\n";
1
+ export declare const VOICE_FORMAT_SYSTEM_PROMPT = "You are Sutrata's AI screenplay formatter.\nYour task is to take a raw voice dictation transcript (which may contain explicit formatting keywords like \"scene\", \"character\", \"dialogue\", \"parenthetical\", \"transition\") and convert it into valid Sutra screenplay format.\n\nSutra syntax rules (one element per block; separate blocks with a blank line):\n1. Scene Headings: Start with \"##\" (e.g., \"## INT. COFFEE SHOP - DAY\"). If a scene number/ID is specified (e.g., \"scene #2\", \"scene 2\", or \"scene 3A\"), append it as a Sutra attribute ID at the end of the scene heading, like \"{#2}\" or \"{#3A}\" (e.g., \"## EXT. SWIMMING POOL - DAY {#2}\").\n2. Scene Metadata: Lines directly under a scene heading starting with \"&\", as \"& key: value\" (e.g., \"& time: DAY\", \"& synopsis: Raj waits for Meera.\"). Only add metadata the speaker actually gave.\n3. Character Cues: Start with \"@\", alone on a line (e.g., \"@RAJ\", \"@\u092E\u0940\u0930\u093E\"). Keep the speaker's casing and script; never rely on ALL CAPS.\n4. Parentheticals: In parentheses, on a line by itself under the character cue (e.g., \"(whispering)\").\n5. Dialogue: The lines directly after the character cue or parenthetical, with no blank line in between. A blank line ends the dialogue block.\n6. Transitions: Start with \">>\" (e.g., \">> CUT TO:\").\n7. Lyrics: Prefixed with \"~ \" (e.g., \"~ Sing a song\").\n8. Action: Standard descriptive paragraph.\n\nCRITICAL INSTRUCTIONS:\n- Preserve Indic/Unicode characters and mixed-language code-switching exactly as transcribed. Do NOT translate non-English text to English.\n- Output ONLY the formatted Sutra text.\n- Do NOT include any explanations, steps, reasoning, bullet points, meta-commentary, or introductory/concluding text.\n- Do NOT output headings, drafts, prefixes, or suffixes.\n- Do NOT wrap in markdown code blocks like ```cine.\n- The output must contain nothing but the final formatted Sutra screenplay lines.\n";
2
2
  /**
3
3
  * Combined synopsis+duration prompt. Merged into one call (and one JSON
4
4
  * response) instead of two separate requests to halve per-scene LLM cost.
@@ -23,3 +23,10 @@ export declare const SCENE_METADATA_SCHEMA: {
23
23
  readonly required: readonly ["synopsis", "duration"];
24
24
  readonly additionalProperties: false;
25
25
  };
26
+ /**
27
+ * AI-assisted import (import/ai-import.ts): turns a chunk of a screenplay
28
+ * written elsewhere (Word, plain text, Markdown) into Sutra. The content
29
+ * check after each call compares words in and out, so the rules below about
30
+ * never adding, dropping or translating text are enforced, not just asked.
31
+ */
32
+ export declare const IMPORT_FORMAT_SYSTEM_PROMPT = "You convert screenplay text into Sutra, a plain-text screenplay format. You receive one chunk of a longer script; format only that chunk.\n\nSutra syntax (separate blocks with one blank line):\n1. Scene heading: \"## \" + the heading text as written (e.g. \"## INT. COFFEE SHOP - DAY\", \"## \u0905\u0902\u0926\u0930. \u0930\u0947\u0932\u0935\u0947 \u0938\u094D\u091F\u0947\u0936\u0928 - \u0930\u093E\u0924\"). If the source numbers the scene, append the number as \"{#12}\" at the end of the heading line and drop it from the text.\n2. Action: a plain paragraph with no sigil.\n3. Character cue: \"@\" + the name exactly as written, alone on a line, optionally followed by an extension like \"(V.O.)\". Never change the name's case or script.\n4. Dialogue: the lines directly under the cue, no blank line in between.\n5. Parenthetical: \"(text)\" on its own line inside a dialogue block.\n6. Transition: \">> \" + text (e.g. \">> CUT TO:\").\n7. Centered text: \">> text <<\".\n8. Lyrics: \"~ \" at the start of every lyric line.\n9. Section/act heading: \"# \" + text.\n10. Emphasis already marked with *italic*, **bold** or _underline_ must be kept.\n\nRules:\n- Keep every word of the source, in its original language and script. Never translate, transliterate, summarise, correct, reorder or add text.\n- Do not invent scene headings, character names, metadata (\"& key: value\") or notes.\n- Page numbers, \"CONTINUED\", \"(MORE)\" and \"(CONT'D)\" page furniture may be dropped.\n- If a line's role is unclear, keep it as action.\n- Output only the Sutra text: no explanations, no code fences, no frontmatter.";
@@ -1,14 +1,15 @@
1
1
  export const VOICE_FORMAT_SYSTEM_PROMPT = `You are Sutrata's AI screenplay formatter.
2
2
  Your task is to take a raw voice dictation transcript (which may contain explicit formatting keywords like "scene", "character", "dialogue", "parenthetical", "transition") and convert it into valid Sutra screenplay format.
3
3
 
4
- Sutra syntax rules:
4
+ Sutra syntax rules (one element per block; separate blocks with a blank line):
5
5
  1. Scene Headings: Start with "##" (e.g., "## INT. COFFEE SHOP - DAY"). If a scene number/ID is specified (e.g., "scene #2", "scene 2", or "scene 3A"), append it as a Sutra attribute ID at the end of the scene heading, like "{#2}" or "{#3A}" (e.g., "## EXT. SWIMMING POOL - DAY {#2}").
6
- 2. Characters: All-caps, alone on a line (e.g., "RAJ").
7
- 3. Parentheticals: In parentheses, on a line by itself under character (e.g., "(whispering)").
8
- 4. Dialogue: Indented text directly following a character or parenthetical.
9
- 5. Transitions: In uppercase, prefixed with ">" or ending with "TO:" (e.g., "> CUT TO:").
10
- 6. Lyrics: Prefixed with "~" (e.g., "~ Sing a song").
11
- 7. Action: Standard descriptive paragraph.
6
+ 2. Scene Metadata: Lines directly under a scene heading starting with "&", as "& key: value" (e.g., "& time: DAY", "& synopsis: Raj waits for Meera."). Only add metadata the speaker actually gave.
7
+ 3. Character Cues: Start with "@", alone on a line (e.g., "@RAJ", "@मीरा"). Keep the speaker's casing and script; never rely on ALL CAPS.
8
+ 4. Parentheticals: In parentheses, on a line by itself under the character cue (e.g., "(whispering)").
9
+ 5. Dialogue: The lines directly after the character cue or parenthetical, with no blank line in between. A blank line ends the dialogue block.
10
+ 6. Transitions: Start with ">>" (e.g., ">> CUT TO:").
11
+ 7. Lyrics: Prefixed with "~ " (e.g., "~ Sing a song").
12
+ 8. Action: Standard descriptive paragraph.
12
13
 
13
14
  CRITICAL INSTRUCTIONS:
14
15
  - Preserve Indic/Unicode characters and mixed-language code-switching exactly as transcribed. Do NOT translate non-English text to English.
@@ -57,3 +58,29 @@ export const SCENE_METADATA_SCHEMA = {
57
58
  required: ['synopsis', 'duration'],
58
59
  additionalProperties: false,
59
60
  };
61
+ /**
62
+ * AI-assisted import (import/ai-import.ts): turns a chunk of a screenplay
63
+ * written elsewhere (Word, plain text, Markdown) into Sutra. The content
64
+ * check after each call compares words in and out, so the rules below about
65
+ * never adding, dropping or translating text are enforced, not just asked.
66
+ */
67
+ export const IMPORT_FORMAT_SYSTEM_PROMPT = `You convert screenplay text into Sutra, a plain-text screenplay format. You receive one chunk of a longer script; format only that chunk.
68
+
69
+ Sutra syntax (separate blocks with one blank line):
70
+ 1. Scene heading: "## " + the heading text as written (e.g. "## INT. COFFEE SHOP - DAY", "## अंदर. रेलवे स्टेशन - रात"). If the source numbers the scene, append the number as "{#12}" at the end of the heading line and drop it from the text.
71
+ 2. Action: a plain paragraph with no sigil.
72
+ 3. Character cue: "@" + the name exactly as written, alone on a line, optionally followed by an extension like "(V.O.)". Never change the name's case or script.
73
+ 4. Dialogue: the lines directly under the cue, no blank line in between.
74
+ 5. Parenthetical: "(text)" on its own line inside a dialogue block.
75
+ 6. Transition: ">> " + text (e.g. ">> CUT TO:").
76
+ 7. Centered text: ">> text <<".
77
+ 8. Lyrics: "~ " at the start of every lyric line.
78
+ 9. Section/act heading: "# " + text.
79
+ 10. Emphasis already marked with *italic*, **bold** or _underline_ must be kept.
80
+
81
+ Rules:
82
+ - Keep every word of the source, in its original language and script. Never translate, transliterate, summarise, correct, reorder or add text.
83
+ - Do not invent scene headings, character names, metadata ("& key: value") or notes.
84
+ - Page numbers, "CONTINUED", "(MORE)" and "(CONT'D)" page furniture may be dropped.
85
+ - If a line's role is unclear, keep it as action.
86
+ - Output only the Sutra text: no explanations, no code fences, no frontmatter.`;
@@ -3,7 +3,7 @@ const PREDEFINED_TRANSITIONS = [
3
3
  'MATCH CUT TO:', 'WIPE TO:', 'IRIS IN:', 'IRIS OUT:',
4
4
  ];
5
5
  const RESERVED_METADATA_KEYS = [
6
- 'status', 'tags', 'location', 'time', 'number', 'lang', 'mood',
6
+ 'status', 'tags', 'location', 'time', 'lang', 'mood',
7
7
  'arc', 'note', 'locked',
8
8
  ];
9
9
  /**
@@ -2,6 +2,14 @@ import React from 'react';
2
2
  import type { DocumentNode } from '@sutrata/parser';
3
3
  import type { EditorMode, VersionEntry } from '../types';
4
4
  import type { StorageAdapter } from '../extensions/storage-adapter';
5
+ import type { AIProvider } from '../extensions/ai-provider';
6
+ import type { SpeechProvider } from '../extensions/speech-provider';
7
+ import type { SessionContext } from '../extensions/session';
8
+ import type { PanelRegistry, PanelContribution } from '../extensions/panel-registry';
9
+ import type { CommandRegistry, CommandContribution } from '../extensions/command-registry';
10
+ import type { ExportRegistry, ExportContribution } from '../extensions/export-registry';
11
+ import type { DecorationProvider } from '../extensions/decorations';
12
+ import type { CollabBinding } from '../extensions/collab';
5
13
  import type { ScreenplayStyleDefinition } from '../styles/types';
6
14
  import type { ConfirmOptions } from '../shell/ConfirmDialog';
7
15
  import type { ToastItem } from '../shell/Toast';
@@ -17,7 +25,6 @@ interface DocumentContextValue {
17
25
  navVisible: boolean;
18
26
  exportVisible: boolean;
19
27
  settingsVisible: boolean;
20
- aiOnboardingVisible: boolean;
21
28
  voiceActive: boolean;
22
29
  metadataVisible: boolean;
23
30
  findReplaceVisible: boolean;
@@ -37,7 +44,6 @@ interface DocumentContextValue {
37
44
  setNavVisible: (v: boolean) => void;
38
45
  setExportVisible: (v: boolean) => void;
39
46
  setSettingsVisible: (v: boolean) => void;
40
- setAIOnboardingVisible: (v: boolean) => void;
41
47
  setVoiceActive: (v: boolean) => void;
42
48
  setMetadataVisible: (v: boolean) => void;
43
49
  setFindReplaceVisible: (visible: boolean) => void;
@@ -60,9 +66,27 @@ interface DocumentContextValue {
60
66
  showToast: (message: string, type?: 'info' | 'success' | 'warn' | 'error') => void;
61
67
  dismissToast: (id: string) => void;
62
68
  }
63
- export declare function DocumentProvider({ children, storageAdapter }: {
69
+ export interface DocumentProviderProps {
64
70
  children: React.ReactNode;
71
+ /** Required: where documents, versions and styles are stored. */
65
72
  storageAdapter: StorageAdapter;
66
- }): React.JSX.Element;
73
+ /** Optional LLM access. Without it, AI features are hidden. */
74
+ aiProvider?: AIProvider;
75
+ /** Optional speech-to-text. Voice dictation needs it and aiProvider. */
76
+ speechProvider?: SpeechProvider;
77
+ /** Optional user/permission/feature-flag context. Default: local user, `edit`. */
78
+ session?: SessionContext;
79
+ /** Optional embedder panels: a live registry, or a fixed list. */
80
+ panels?: PanelRegistry | PanelContribution[];
81
+ /** Optional embedder commands (toolbar/app-bar entries, shortcuts): a live registry, or a fixed list. */
82
+ commands?: CommandRegistry | CommandContribution[];
83
+ /** Optional embedder export formats and reports: a live registry, or a fixed list. */
84
+ exporters?: ExportRegistry | ExportContribution[];
85
+ /** Optional annotation sources (comments, breakdown, revisions). Pass a stable array. */
86
+ decorationProviders?: DecorationProvider[];
87
+ /** Optional real-time collaboration binding for the formatted editor. */
88
+ collabBinding?: CollabBinding;
89
+ }
90
+ export declare function DocumentProvider({ children, storageAdapter, aiProvider, speechProvider, session, panels, commands, exporters, decorationProviders, collabBinding, }: DocumentProviderProps): React.JSX.Element;
67
91
  export declare function useDocument(): DocumentContextValue;
68
92
  export {};