@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
@@ -1,249 +0,0 @@
1
- import React from 'react'
2
- import { useDocument } from '../context/DocumentContext'
3
- import { extractWorkflowData } from '../file/workflow-reports'
4
- import { getEditorView } from '../editor/editor-bus'
5
- import { schema } from '../editor/schema'
6
-
7
- interface Props {
8
- onClose: () => void
9
- }
10
-
11
- export function StatisticsDialog({ onClose }: Props) {
12
- const { ast, text, setText } = useDocument()
13
- const { scenes, characters } = extractWorkflowData(ast)
14
-
15
- const handleUpdateActor = (charName: string, actorName: string) => {
16
- const cleanChar = charName.trim().toUpperCase()
17
- if (!cleanChar) return
18
-
19
- const view = getEditorView()
20
- const key = `characters.${cleanChar}`
21
-
22
- if (view) {
23
- let tr = view.state.tr
24
- let titlePagePos: number | null = null
25
- view.state.doc.forEach((node, offset) => {
26
- if (node.type.name === 'title_page') titlePagePos = offset
27
- })
28
-
29
- if (titlePagePos !== null) {
30
- const tpNode = view.state.doc.nodeAt(titlePagePos)!
31
- let existingPos: number | null = null
32
- let pos = titlePagePos + 1 // inside title_page
33
-
34
- tpNode.forEach((field) => {
35
- if (field.attrs['fmKey'] === key) {
36
- existingPos = pos
37
- }
38
- pos += field.nodeSize
39
- })
40
-
41
- if (existingPos !== null) {
42
- const fieldNode = view.state.doc.nodeAt(existingPos)!
43
- if (actorName.trim() === '') {
44
- tr = tr.delete(existingPos, existingPos + fieldNode.nodeSize)
45
- } else {
46
- tr = tr.replaceWith(
47
- existingPos + 1,
48
- existingPos + fieldNode.nodeSize - 1,
49
- schema.text(actorName)
50
- )
51
- }
52
- } else if (actorName.trim() !== '') {
53
- const insertPos = titlePagePos + tpNode.nodeSize - 1 // end of title_page
54
- const newField = schema.nodes['frontmatter_field']!.create(
55
- { fmKey: key },
56
- schema.text(actorName)
57
- )
58
- tr = tr.insert(insertPos, newField)
59
- }
60
- view.dispatch(tr)
61
- } else {
62
- const freshText = updateActorInText(text, cleanChar, actorName)
63
- setText(freshText)
64
- }
65
- } else {
66
- const freshText = updateActorInText(text, cleanChar, actorName)
67
- setText(freshText)
68
- }
69
- }
70
-
71
- // Word count helper
72
- const wordCount = text.split(/\s+/).filter(Boolean).length
73
-
74
- // Sum up estimated duration. Each scene's est-duration is "MM:SS" — sum
75
- // in seconds and only convert to minutes/hours once, at the end. Summing
76
- // the MM parts alone (the previous approach) silently dropped every
77
- // scene's seconds component, undercounting the total by tens of minutes
78
- // on a script with many scenes.
79
- let totalSeconds = 0
80
- scenes.forEach(s => {
81
- const match = /^(\d+):(\d{1,2})$/.exec(s.estDuration.trim())
82
- if (match && match[1] && match[2]) {
83
- totalSeconds += Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10)
84
- }
85
- })
86
- const totalMinutes = Math.round(totalSeconds / 60)
87
- const totalDurationLabel = totalSeconds === 0
88
- ? 'TBD'
89
- : totalMinutes >= 60
90
- ? `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`
91
- : `${totalMinutes} min`
92
-
93
- return (
94
- <div
95
- className="cs-dialog-overlay"
96
- role="presentation"
97
- onClick={e => {
98
- if (e.target === e.currentTarget) onClose()
99
- }}
100
- >
101
- <div
102
- className="cs-dialog"
103
- role="dialog"
104
- aria-modal="true"
105
- aria-labelledby="cs-stats-dialog-title"
106
- style={{ maxWidth: '640px', width: '92%', maxHeight: '85vh', display: 'flex', flexDirection: 'column' }}
107
- >
108
- <div className="cs-panel-header">
109
- <span id="cs-stats-dialog-title" className="cs-panel-title">Script Statistics</span>
110
- <button className="cs-panel-close" onClick={onClose} aria-label="Close">
111
- ×
112
- </button>
113
- </div>
114
- <div className="cs-panel-body" style={{ overflowY: 'auto', flex: 1, padding: '20px' }}>
115
- <div className="cs-stats-summary-grid">
116
- <div className="cs-stats-card">
117
- <div className="cs-stats-card-val">{scenes.length}</div>
118
- <div className="cs-stats-card-lbl">Scenes</div>
119
- </div>
120
- <div className="cs-stats-card">
121
- <div className="cs-stats-card-val">{wordCount.toLocaleString()}</div>
122
- <div className="cs-stats-card-lbl">Words</div>
123
- </div>
124
- <div className="cs-stats-card">
125
- <div className="cs-stats-card-val">
126
- {totalDurationLabel}
127
- </div>
128
- <div className="cs-stats-card-lbl">Est. Duration</div>
129
- </div>
130
- </div>
131
-
132
- <div className="cs-stats-section-hdr">
133
- Cast List &amp; Occurrences ({characters.length})
134
- </div>
135
-
136
- {characters.length === 0 ? (
137
- <div style={{ color: 'var(--cs-ui-text-light-muted, #8c897f)', fontStyle: 'italic', fontSize: '12px' }}>
138
- No character cues detected in the screenplay yet.
139
- </div>
140
- ) : (
141
- <table className="cs-stats-table">
142
- <thead>
143
- <tr>
144
- <th style={{ textAlign: 'left', width: '35%' }}>Character</th>
145
- <th style={{ textAlign: 'left' }}>Actor</th>
146
- <th style={{ textAlign: 'center', width: '15%' }}>Scenes</th>
147
- </tr>
148
- </thead>
149
- <tbody>
150
- {characters.map((c, i) => (
151
- <tr key={i}>
152
- <td style={{ fontWeight: '600' }}>{c.name}</td>
153
- <td>
154
- <input
155
- type="text"
156
- className="cs-stats-actor-input"
157
- value={c.actor}
158
- onChange={(e) => handleUpdateActor(c.name, e.target.value)}
159
- placeholder="Assign actor..."
160
- aria-label={`Actor for ${c.name}`}
161
- />
162
- </td>
163
- <td style={{ textAlign: 'center', fontWeight: '500' }}>{c.sceneCount}</td>
164
- </tr>
165
- ))}
166
- </tbody>
167
- </table>
168
- )}
169
- </div>
170
- <div
171
- style={{
172
- borderTop: '1px solid var(--cs-ui-border-light-subtle, #eeece4)',
173
- display: 'flex',
174
- justifyContent: 'flex-end',
175
- padding: '12px 20px',
176
- background: 'var(--cs-ui-bg-panel-subtle, #f4f2ea)',
177
- }}
178
- >
179
- <button
180
- type="button"
181
- className="cs-confirm-btn-primary"
182
- onClick={onClose}
183
- >
184
- Close
185
- </button>
186
- </div>
187
- </div>
188
- </div>
189
- )
190
- }
191
-
192
- function updateActorInText(text: string, charName: string, actorName: string): string {
193
- const lines = text.split('\n')
194
- const fmStartIndex = lines.indexOf('---')
195
- if (fmStartIndex === -1) {
196
- return `---\ncharacters:\n ${charName}: ${actorName}\n---\n\n${text}`
197
- }
198
- const fmEndIndex = lines.indexOf('---', fmStartIndex + 1)
199
- if (fmEndIndex === -1) return text
200
-
201
- const fmLines = lines.slice(fmStartIndex + 1, fmEndIndex)
202
- let charsLineIdx = -1
203
- for (let i = 0; i < fmLines.length; i++) {
204
- if (fmLines[i]!.trim() === 'characters:') {
205
- charsLineIdx = i
206
- break
207
- }
208
- }
209
-
210
- const cleanChar = charName.toUpperCase()
211
- if (charsLineIdx === -1) {
212
- if (actorName.trim() !== '') {
213
- fmLines.push('characters:')
214
- fmLines.push(` ${cleanChar}: ${actorName}`)
215
- }
216
- } else {
217
- let insertIdx = charsLineIdx + 1
218
- let charLineIdx = -1
219
- while (insertIdx < fmLines.length) {
220
- const line = fmLines[insertIdx]!
221
- if (!line.startsWith(' ') && line.trim() !== '') {
222
- break
223
- }
224
- const match = /^\s+([A-Za-z0-9_ -]+)\s*:(.*)/.exec(line)
225
- if (match && match[1]?.trim().toUpperCase() === cleanChar) {
226
- charLineIdx = insertIdx
227
- break
228
- }
229
- insertIdx++
230
- }
231
-
232
- if (charLineIdx !== -1) {
233
- if (actorName.trim() === '') {
234
- fmLines.splice(charLineIdx, 1)
235
- } else {
236
- fmLines[charLineIdx] = ` ${cleanChar}: ${actorName}`
237
- }
238
- } else if (actorName.trim() !== '') {
239
- fmLines.splice(insertIdx, 0, ` ${cleanChar}: ${actorName}`)
240
- }
241
- }
242
-
243
- const newLines = [
244
- ...lines.slice(0, fmStartIndex + 1),
245
- ...fmLines,
246
- ...lines.slice(fmEndIndex)
247
- ]
248
- return newLines.join('\n')
249
- }
@@ -1,84 +0,0 @@
1
- export interface SceneEntry {
2
- id: string | null
3
- heading: string
4
- synopsis: string
5
- status: string
6
- tags: string[]
7
- location: string
8
- index: number // 1-based position (internal, used for PM navigation)
9
- textOffset: number // character offset in source text where this scene starts
10
- estDuration?: string
11
- }
12
-
13
- /**
14
- * Derive a flat scene list from Sutra source text.
15
- * Scenes are identified by scene heading lines (## prefix).
16
- */
17
- export function buildSceneList(text: string): SceneEntry[] {
18
- const lines = text.split('\n')
19
- const scenes: SceneEntry[] = []
20
- let sceneIndex = 0
21
- let currentOffset = 0
22
-
23
- for (let i = 0; i < lines.length; i++) {
24
- const line = lines[i] ?? ''
25
- const trimmed = line.trim()
26
-
27
- // Scene headings start with ## (Sutra sigil)
28
- if (trimmed.startsWith('## ') || trimmed.startsWith('##\t')) {
29
- const headingRaw = trimmed.slice(3).trim()
30
- const sceneStart = currentOffset
31
-
32
- // Extract scene id from inline {#...} on the heading line, then strip it
33
- const idMatch = /\{#([^}]+)\}/.exec(headingRaw)
34
- const sceneId: string | null = idMatch?.[1] ?? null
35
- const heading = headingRaw.replace(/\s*\{#[^}]+\}/, '').trim()
36
-
37
- // Collect metadata (& key: value) until next heading
38
- let synopsis = ''
39
- let status = ''
40
- let tags: string[] = []
41
- let location = ''
42
- let estDuration = ''
43
-
44
- let j = i + 1
45
- while (j < lines.length) {
46
- const nextLine = (lines[j] ?? '').trim()
47
- if (nextLine.startsWith('## ') || nextLine.startsWith('##\t')) break
48
- if (nextLine.startsWith('& synopsis:')) {
49
- if (!synopsis) synopsis = nextLine.replace('& synopsis:', '').trim()
50
- } else if (nextLine.startsWith('& status:')) {
51
- status = nextLine.replace('& status:', '').trim()
52
- } else if (nextLine.startsWith('& tags:')) {
53
- tags = nextLine
54
- .replace('& tags:', '')
55
- .trim()
56
- .split(',')
57
- .map((t) => t.trim())
58
- .filter(Boolean)
59
- } else if (nextLine.startsWith('& location:')) {
60
- location = nextLine.replace('& location:', '').trim()
61
- } else if (nextLine.startsWith('& est-duration:')) {
62
- estDuration = nextLine.replace('& est-duration:', '').trim()
63
- }
64
- j++
65
- }
66
-
67
- scenes.push({
68
- id: sceneId,
69
- heading,
70
- synopsis,
71
- status,
72
- tags,
73
- location,
74
- estDuration: estDuration || undefined,
75
- index: ++sceneIndex,
76
- textOffset: sceneStart,
77
- })
78
- }
79
-
80
- currentOffset += (line?.length ?? 0) + 1 // +1 for \n
81
- }
82
-
83
- return scenes
84
- }
@@ -1,303 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react'
2
- import { useTranslation } from '../i18n/useTranslation'
3
- import { useDocument } from '../context/DocumentContext'
4
- import {
5
- getAIConfig,
6
- saveAIConfig,
7
- fetchProviders,
8
- getApiKey,
9
- setApiKey,
10
- deleteApiKey,
11
- testConnection,
12
- listModels,
13
- ProviderConfig,
14
- ModelConfig,
15
- AIConfig,
16
- } from '../ai/ai-client'
17
-
18
- const UI_LANGUAGES = [
19
- { code: 'en', label: 'English' },
20
- { code: 'hi', label: 'हिन्दी' },
21
- { code: 'ta', label: 'தமிழ்' },
22
- { code: 'te', label: 'తెలుగు' },
23
- { code: 'ml', label: 'മലയാളം' },
24
- { code: 'kn', label: 'ಕನ್ನಡ' },
25
- { code: 'bn', label: 'বাংলা' },
26
- { code: 'gu', label: 'ગુજરાતી' },
27
- { code: 'mr', label: 'मराठी' },
28
- { code: 'pa', label: 'ਪੰਜਾਬੀ' },
29
- { code: 'or', label: 'ଓଡ଼ିଆ' },
30
- { code: 'si', label: 'සිංහල' },
31
- ]
32
-
33
- interface Props {
34
- onClose: () => void
35
- }
36
-
37
- export function SettingsDialog({ onClose }: Props) {
38
- const { t, locale, setLocale } = useTranslation()
39
- const { setAIOnboardingVisible } = useDocument()
40
- const dialogRef = useRef<HTMLDivElement>(null)
41
-
42
- // AI states
43
- const [aiConfig, setAiConfig] = useState<AIConfig>(getAIConfig())
44
- const [providers, setProviders] = useState<ProviderConfig[]>([])
45
- const [activeKey, setActiveKey] = useState<string>('')
46
- const [showKey, setShowKey] = useState<boolean>(false)
47
- const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle')
48
- const [testError, setTestError] = useState<string>('')
49
- const [dynamicModels, setDynamicModels] = useState<ModelConfig[] | null>(null)
50
- const [modelsLoading, setModelsLoading] = useState<boolean>(false)
51
-
52
- useEffect(() => {
53
- dialogRef.current?.focus()
54
- void loadProviders()
55
- }, [])
56
-
57
- const loadProviders = async () => {
58
- const p = await fetchProviders(aiConfig)
59
- setProviders(p)
60
- }
61
-
62
- // Loads the stored key for the selected provider, then refreshes its
63
- // model list from the provider's live /models endpoint (falls back to the
64
- // static list on any error — see listModels). Depends on `providers` too
65
- // so it re-runs once fetchProviders() resolves on mount, instead of
66
- // racing it.
67
- useEffect(() => {
68
- let cancelled = false
69
- const run = async () => {
70
- setTestStatus('idle')
71
- setTestError('')
72
- const key = await getApiKey(aiConfig.preferredProvider)
73
- if (cancelled) return
74
- setActiveKey(key || '')
75
- setDynamicModels(null)
76
-
77
- const provider = providers.find(p => p.id === aiConfig.preferredProvider)
78
- if (key && provider) {
79
- setModelsLoading(true)
80
- const models = await listModels(provider, key)
81
- if (!cancelled) {
82
- setDynamicModels(models)
83
- setModelsLoading(false)
84
- }
85
- }
86
- }
87
- void run()
88
- return () => { cancelled = true }
89
- }, [aiConfig.preferredProvider, providers])
90
-
91
- const handleKeyDown = (e: React.KeyboardEvent) => {
92
- if (e.key === 'Escape') onClose()
93
- }
94
-
95
- const handleProviderChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
96
- const nextProvider = e.target.value
97
- const provider = providers.find(p => p.id === nextProvider)
98
- const nextModel = provider?.models[0]?.id || ''
99
-
100
- const updated = {
101
- ...aiConfig,
102
- preferredProvider: nextProvider,
103
- preferredModel: nextModel,
104
- }
105
- setAiConfig(updated)
106
- saveAIConfig(updated)
107
- }
108
-
109
- const handleModelChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
110
- const updated = {
111
- ...aiConfig,
112
- preferredModel: e.target.value,
113
- }
114
- setAiConfig(updated)
115
- saveAIConfig(updated)
116
- }
117
-
118
- const handleKeyChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
119
- const val = e.target.value
120
- setActiveKey(val)
121
- if (val.trim()) {
122
- await setApiKey(aiConfig.preferredProvider, val.trim())
123
- } else {
124
- await deleteApiKey(aiConfig.preferredProvider)
125
- }
126
- setTestStatus('idle')
127
- }
128
-
129
- const handleTestKey = async () => {
130
- if (!activeKey.trim()) {
131
- setTestStatus('error')
132
- setTestError('Please enter an API key to test.')
133
- return
134
- }
135
- setTestStatus('testing')
136
- setTestError('')
137
- try {
138
- const ok = await testConnection(aiConfig.preferredProvider, activeKey.trim(), aiConfig)
139
- if (ok) {
140
- setTestStatus('success')
141
- const provider = providers.find(p => p.id === aiConfig.preferredProvider)
142
- if (provider) {
143
- setModelsLoading(true)
144
- setDynamicModels(await listModels(provider, activeKey.trim()))
145
- setModelsLoading(false)
146
- }
147
- } else {
148
- setTestStatus('error')
149
- setTestError('Connection failed: empty or invalid response.')
150
- }
151
- } catch (err: any) {
152
- setTestStatus('error')
153
- setTestError(err.message || String(err))
154
- }
155
- }
156
-
157
- const currentProvider = providers.find(p => p.id === aiConfig.preferredProvider)
158
- const models = dynamicModels ?? currentProvider?.models ?? []
159
-
160
- return (
161
- <div className="cs-dialog-overlay" onClick={onClose}>
162
- <div
163
- ref={dialogRef}
164
- className="cs-dialog cs-settings-dialog"
165
- role="dialog"
166
- aria-modal="true"
167
- aria-label={t('settings.title')}
168
- tabIndex={-1}
169
- onClick={e => e.stopPropagation()}
170
- onKeyDown={handleKeyDown}
171
- style={{ maxWidth: '480px' }}
172
- >
173
- <div className="cs-dialog-header">
174
- <span className="cs-dialog-title">{t('settings.title')}</span>
175
- <button className="cs-fr-close" aria-label={t('settings.close')} onClick={onClose}>×</button>
176
- </div>
177
-
178
- <div className="cs-settings-body" style={{ maxHeight: '80vh', overflowY: 'auto' }}>
179
- <div className="cs-settings-section-label">{t('settings.general')}</div>
180
-
181
- <div className="cs-settings-row">
182
- <label className="cs-settings-label" htmlFor="cs-ui-lang-select">
183
- {t('settings.uiLanguage')}
184
- </label>
185
- <select
186
- id="cs-ui-lang-select"
187
- className="cs-settings-select"
188
- value={locale}
189
- onChange={e => setLocale(e.target.value)}
190
- >
191
- {UI_LANGUAGES.map(lang => (
192
- <option key={lang.code} value={lang.code}>{lang.label}</option>
193
- ))}
194
- </select>
195
- </div>
196
-
197
- <div style={{ margin: '14px 0 6px 0', borderTop: '1px solid var(--cs-ui-border-light, #e8e8e4)', paddingTop: '14px' }} className="cs-settings-section-label">
198
- Intelligence &amp; Voice Settings
199
- </div>
200
-
201
- <div className="cs-settings-row">
202
- <label className="cs-settings-label" htmlFor="cs-ai-provider-select">
203
- Preferred Provider
204
- </label>
205
- <select
206
- id="cs-ai-provider-select"
207
- className="cs-settings-select"
208
- value={aiConfig.preferredProvider}
209
- onChange={handleProviderChange}
210
- >
211
- {providers.map(p => (
212
- <option key={p.id} value={p.id}>{p.name}</option>
213
- ))}
214
- </select>
215
- </div>
216
-
217
- <div className="cs-settings-row">
218
- <label className="cs-settings-label" htmlFor="cs-ai-model-select">
219
- Preferred Model{modelsLoading ? ' (refreshing...)' : ''}
220
- </label>
221
- <select
222
- id="cs-ai-model-select"
223
- className="cs-settings-select"
224
- value={aiConfig.preferredModel}
225
- onChange={handleModelChange}
226
- disabled={models.length === 0}
227
- >
228
- {models.map(m => (
229
- <option key={m.id} value={m.id}>{m.name}</option>
230
- ))}
231
- </select>
232
- </div>
233
-
234
- <div className="cs-settings-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: '6px' }}>
235
- <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
236
- <label className="cs-settings-label" htmlFor="cs-ai-key-input">
237
- API Key for {currentProvider?.name || aiConfig.preferredProvider}
238
- </label>
239
- <button
240
- type="button"
241
- style={{ background: 'none', border: 'none', color: 'var(--cs-ui-accent, #c4760a)', cursor: 'pointer', fontSize: '11px', padding: 0 }}
242
- onClick={() => setShowKey(!showKey)}
243
- >
244
- {showKey ? 'Hide' : 'Show'}
245
- </button>
246
- </div>
247
- <input
248
- id="cs-ai-key-input"
249
- type={showKey ? 'text' : 'password'}
250
- style={{
251
- width: '100%',
252
- padding: '7px 10px',
253
- border: '1px solid var(--cs-ui-border-light, #ccc)',
254
- borderRadius: '4px',
255
- boxSizing: 'border-box',
256
- fontFamily: 'monospace',
257
- fontSize: '12px',
258
- }}
259
- value={activeKey}
260
- onChange={handleKeyChange}
261
- placeholder={`Enter API Key for ${aiConfig.preferredProvider}`}
262
- />
263
- <div style={{ display: 'flex', gap: '8px', marginTop: '4px' }}>
264
- <button
265
- type="button"
266
- className="cs-voice-btn-secondary"
267
- style={{ padding: '4px 10px', fontSize: '12px' }}
268
- onClick={handleTestKey}
269
- disabled={testStatus === 'testing'}
270
- >
271
- {testStatus === 'testing' ? 'Testing...' : 'Test Connection'}
272
- </button>
273
- </div>
274
- {testStatus === 'success' && (
275
- <span style={{ color: '#10b981', fontSize: '12px', marginTop: '2px' }}>
276
- ✓ Key verified! Connection succeeded.
277
- </span>
278
- )}
279
- {testStatus === 'error' && (
280
- <span style={{ color: '#ef4444', fontSize: '12px', marginTop: '2px', whiteSpace: 'pre-wrap' }}>
281
- ✗ Error: {testError}
282
- </span>
283
- )}
284
- </div>
285
-
286
- <div style={{ marginTop: '16px', display: 'flex', justifyContent: 'flex-start' }}>
287
- <button
288
- type="button"
289
- className="cs-voice-btn-primary"
290
- style={{ padding: '6px 12px', fontSize: '12px' }}
291
- onClick={() => {
292
- onClose()
293
- setAIOnboardingVisible(true)
294
- }}
295
- >
296
- Open Setup Wizard
297
- </button>
298
- </div>
299
- </div>
300
- </div>
301
- </div>
302
- )
303
- }