@sutrata/editor 0.1.1 → 0.1.2
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.
- package/README.md +87 -0
- package/dist/ai/generated-providers.json +1 -1
- package/package.json +30 -3
- package/scripts/copy-assets.mjs +0 -16
- package/scripts/generate-providers.mjs +0 -127
- package/src/ai/OnboardingDialog.tsx +0 -390
- package/src/ai/VoiceConfirmDialog.tsx +0 -137
- package/src/ai/VoiceToolbar.tsx +0 -321
- package/src/ai/ai-client.ts +0 -596
- package/src/ai/commands.ts +0 -164
- package/src/ai/generated-providers.json +0 -3696
- package/src/ai/prompts.ts +0 -62
- package/src/ai/voice-service.ts +0 -96
- package/src/autocomplete/AutocompletePlugin.ts +0 -21
- package/src/autocomplete/suggestions.ts +0 -147
- package/src/context/DocumentContext.tsx +0 -648
- package/src/desktop-api.d.ts +0 -40
- package/src/editor/EditorView.tsx +0 -82
- package/src/editor/SelectionMenu.tsx +0 -152
- package/src/editor/attr-pencil-plugin.ts +0 -172
- package/src/editor/character-decoration-plugin.ts +0 -104
- package/src/editor/editor-bus.ts +0 -51
- package/src/editor/find-highlight-plugin.ts +0 -68
- package/src/editor/frontmatter-field-view.ts +0 -52
- package/src/editor/frontmatter-field.ts +0 -115
- package/src/editor/grapheme-cursor.ts +0 -99
- package/src/editor/ime-plugin.ts +0 -85
- package/src/editor/insert-helper.ts +0 -43
- package/src/editor/keymap.ts +0 -124
- package/src/editor/language-class-plugin.ts +0 -91
- package/src/editor/plugins.ts +0 -26
- package/src/editor/prosemirror-to-sutra.ts +0 -192
- package/src/editor/scene-heading-view.ts +0 -147
- package/src/editor/schema.ts +0 -156
- package/src/editor/sutra-to-prosemirror.ts +0 -156
- package/src/extensions/storage-adapter.ts +0 -65
- package/src/extensions/types.ts +0 -60
- package/src/file/docx-exporter.ts +0 -585
- package/src/file/docx-importer.ts +0 -313
- package/src/file/print-paginator.ts +0 -301
- package/src/file/templates.ts +0 -73
- package/src/file/workflow-reports.ts +0 -820
- package/src/findreplace/FindReplace.tsx +0 -242
- package/src/findreplace/find-engine.ts +0 -97
- package/src/findreplace/scroll-to-match.ts +0 -89
- package/src/fonts/tofu-detect.ts +0 -53
- package/src/i18n/LanguageContext.tsx +0 -207
- package/src/i18n/language-detect.ts +0 -73
- package/src/i18n/useTranslation.tsx +0 -74
- package/src/index.ts +0 -27
- package/src/locales/bn.json +0 -143
- package/src/locales/en.json +0 -143
- package/src/locales/gu.json +0 -143
- package/src/locales/hi.json +0 -143
- package/src/locales/kn.json +0 -143
- package/src/locales/ml.json +0 -143
- package/src/locales/mr.json +0 -143
- package/src/locales/or.json +0 -143
- package/src/locales/pa.json +0 -143
- package/src/locales/si.json +0 -143
- package/src/locales/ta.json +0 -143
- package/src/locales/te.json +0 -143
- package/src/navigator/SceneNavigator.tsx +0 -844
- package/src/navigator/StatisticsDialog.tsx +0 -249
- package/src/navigator/scene-list.ts +0 -84
- package/src/settings/SettingsDialog.tsx +0 -303
- package/src/shell/AppBar.tsx +0 -354
- package/src/shell/AppShell.tsx +0 -71
- package/src/shell/ConfirmDialog.tsx +0 -71
- package/src/shell/ElementToolbar.tsx +0 -175
- package/src/shell/LanguageStatus.tsx +0 -101
- package/src/shell/StatusBar.tsx +0 -135
- package/src/shell/SutrataLogo.tsx +0 -89
- package/src/shell/Toast.tsx +0 -46
- package/src/shell/ToolbarButton.tsx +0 -30
- package/src/shell/VersionHistory.tsx +0 -212
- package/src/shell/Watermark.tsx +0 -9
- package/src/shell/fonts/fonts.css +0 -1133
- package/src/shell/icons.tsx +0 -335
- package/src/shell/toolbar-actions.ts +0 -59
- package/src/source/SourceView.tsx +0 -111
- package/src/source/find-highlight-source.ts +0 -57
- package/src/source/sutra-lang.ts +0 -121
- package/src/spellcheck/dictionaries.ts +0 -21
- package/src/spellcheck/hunspell.ts +0 -110
- package/src/spellcheck/spellcheck-plugin.ts +0 -83
- package/src/styles/StyleDialog.tsx +0 -148
- package/src/styles/builtin-styles.ts +0 -139
- package/src/styles/css-adapter.ts +0 -76
- package/src/styles/docx-adapter.ts +0 -109
- package/src/styles/embed-fonts.ts +0 -81
- package/src/styles/pagination.ts +0 -27
- package/src/styles/print-adapter.ts +0 -288
- package/src/styles/registry.ts +0 -20
- package/src/styles/screenplay.css +0 -3006
- package/src/styles/script-ranges.ts +0 -17
- package/src/styles/types.ts +0 -104
- package/src/styles/validate.ts +0 -114
- package/src/templates/templates.ts +0 -194
- package/src/titlepage/ExportDialog.tsx +0 -494
- package/src/titlepage/TitlePageDialog.tsx +0 -312
- package/src/titlepage/frontmatter-form.ts +0 -280
- package/src/titlepage/languages.ts +0 -37
- package/src/types.ts +0 -7
- package/tests/AppBar.test.tsx +0 -35
- package/tests/AppShell.test.tsx +0 -22
- package/tests/ElementToolbar.test.tsx +0 -36
- package/tests/LanguageStatus.test.tsx +0 -129
- package/tests/SelectionMenu.test.tsx +0 -71
- package/tests/ToolbarButton.test.tsx +0 -24
- package/tests/ai-onboarding.test.ts +0 -102
- package/tests/ai-status.test.tsx +0 -72
- package/tests/ai-structured.test.ts +0 -94
- package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
- package/tests/autocomplete/suggestions.test.ts +0 -106
- package/tests/document-context.test.tsx +0 -565
- package/tests/docx-importer.test.ts +0 -160
- package/tests/editor/character-decoration-plugin.test.ts +0 -101
- package/tests/editor/character-extension.test.ts +0 -44
- package/tests/editor/grapheme-cursor.test.ts +0 -118
- package/tests/editor/ime-plugin.test.ts +0 -72
- package/tests/editor/inline-note-bridge.test.ts +0 -32
- package/tests/editor/language-class-plugin.test.ts +0 -265
- package/tests/editor/title-page.test.ts +0 -124
- package/tests/embed-fonts.test.ts +0 -17
- package/tests/exporters.test.ts +0 -365
- package/tests/findreplace/FindReplace.test.tsx +0 -391
- package/tests/findreplace/find-engine.test.ts +0 -86
- package/tests/findreplace/scroll-to-match.test.ts +0 -50
- package/tests/fonts/tofu-detect.test.ts +0 -122
- package/tests/i18n/LanguageContext.test.tsx +0 -146
- package/tests/i18n/language-detect.test.ts +0 -54
- package/tests/i18n/locale-parity.test.ts +0 -25
- package/tests/i18n/useTranslation.test.tsx +0 -45
- package/tests/keymap.test.ts +0 -18
- package/tests/navigator/SceneNavigator.test.tsx +0 -177
- package/tests/navigator/StatisticsDialog.test.tsx +0 -63
- package/tests/navigator/scene-list.test.ts +0 -71
- package/tests/print-paginator.test.ts +0 -82
- package/tests/shell/VersionHistory.test.tsx +0 -129
- package/tests/spellcheck/hunspell.test.ts +0 -82
- package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
- package/tests/styles.test.ts +0 -315
- package/tests/templates/templates.test.ts +0 -83
- package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
- package/tests/titlepage/apply-title-page.test.ts +0 -61
- package/tests/titlepage/frontmatter-form.test.ts +0 -115
- package/tests/toolbar-actions.test.ts +0 -93
- package/tests/voice-commands.test.ts +0 -58
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -11
- package/vitest.setup.ts +0 -65
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
getApiKey, setApiKey, deleteApiKey, getAIConfig, saveAIConfig,
|
|
4
|
-
fetchProviders, testConnection, listModels, AIConfig, ProviderConfig
|
|
5
|
-
} from './ai-client'
|
|
6
|
-
import { SparklesIcon } from '../shell/icons'
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
onClose: () => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function OnboardingDialog({ onClose }: Props) {
|
|
13
|
-
const [activeTab, setActiveTab] = useState<'intro' | 'keys' | 'custom'>('intro')
|
|
14
|
-
const [providers, setProviders] = useState<ProviderConfig[]>([])
|
|
15
|
-
const [providerId, setProviderId] = useState<string>('anthropic')
|
|
16
|
-
const [keyInput, setKeyInput] = useState<string>('')
|
|
17
|
-
const [showKey, setShowKey] = useState<boolean>(false)
|
|
18
|
-
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle')
|
|
19
|
-
const [testError, setTestError] = useState<string>('')
|
|
20
|
-
const [config, setConfig] = useState<AIConfig>(getAIConfig)
|
|
21
|
-
|
|
22
|
-
// Custom provider state
|
|
23
|
-
const [customId, setCustomId] = useState('')
|
|
24
|
-
const [customName, setCustomName] = useState('')
|
|
25
|
-
const [customBaseUrl, setCustomBaseUrl] = useState('')
|
|
26
|
-
const [customModels, setCustomModels] = useState('')
|
|
27
|
-
|
|
28
|
-
const dialogRef = useRef<HTMLDivElement>(null)
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
void fetchProviders(config).then(setProviders)
|
|
32
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
-
}, [config.customProviders])
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
dialogRef.current?.focus()
|
|
37
|
-
void loadKeyForProvider(providerId)
|
|
38
|
-
}, [providerId])
|
|
39
|
-
|
|
40
|
-
const loadKeyForProvider = async (pid: string) => {
|
|
41
|
-
setTestStatus('idle')
|
|
42
|
-
setTestError('')
|
|
43
|
-
const key = await getApiKey(pid)
|
|
44
|
-
setKeyInput(key || '')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const handleTestKey = async () => {
|
|
48
|
-
if (!keyInput.trim()) {
|
|
49
|
-
setTestStatus('error')
|
|
50
|
-
setTestError('Please enter an API key to test.')
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
setTestStatus('testing')
|
|
54
|
-
setTestError('')
|
|
55
|
-
try {
|
|
56
|
-
const ok = await testConnection(providerId, keyInput.trim(), config)
|
|
57
|
-
if (ok) {
|
|
58
|
-
setTestStatus('success')
|
|
59
|
-
} else {
|
|
60
|
-
setTestStatus('error')
|
|
61
|
-
setTestError('Connection failed: empty or invalid response from endpoint.')
|
|
62
|
-
}
|
|
63
|
-
} catch (e: any) {
|
|
64
|
-
setTestStatus('error')
|
|
65
|
-
setTestError(e.message || String(e))
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const handleSaveKey = async () => {
|
|
70
|
-
const trimmed = keyInput.trim()
|
|
71
|
-
if (trimmed) {
|
|
72
|
-
await setApiKey(providerId, trimmed)
|
|
73
|
-
|
|
74
|
-
const provider = providers.find(p => p.id === providerId)
|
|
75
|
-
let preferredModel = provider?.models[0]?.id || ''
|
|
76
|
-
if (provider) {
|
|
77
|
-
const liveModels = await listModels(provider, trimmed)
|
|
78
|
-
preferredModel = liveModels[0]?.id || preferredModel
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const updated = { ...config, preferredProvider: providerId, preferredModel }
|
|
82
|
-
setConfig(updated)
|
|
83
|
-
saveAIConfig(updated)
|
|
84
|
-
setTestStatus('success')
|
|
85
|
-
} else {
|
|
86
|
-
await deleteApiKey(providerId)
|
|
87
|
-
setTestStatus('idle')
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const handleAddCustomProvider = () => {
|
|
92
|
-
if (!customId || !customName || !customBaseUrl || !customModels) {
|
|
93
|
-
alert('Please fill out all custom provider fields.')
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
const modelsList = customModels.split(',').map(m => {
|
|
97
|
-
const parts = m.trim().split(':')
|
|
98
|
-
const id = parts[0]?.trim() || ''
|
|
99
|
-
const name = parts[1]?.trim() || id
|
|
100
|
-
return { id, name }
|
|
101
|
-
}).filter(m => m.id)
|
|
102
|
-
|
|
103
|
-
const newCustom = {
|
|
104
|
-
id: customId.trim().toLowerCase(),
|
|
105
|
-
name: customName.trim(),
|
|
106
|
-
baseUrl: customBaseUrl.trim(),
|
|
107
|
-
models: modelsList,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const updated = { ...config }
|
|
111
|
-
const index = updated.customProviders.findIndex(p => p.id === newCustom.id)
|
|
112
|
-
if (index >= 0) {
|
|
113
|
-
updated.customProviders[index] = newCustom
|
|
114
|
-
} else {
|
|
115
|
-
updated.customProviders.push(newCustom)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
setConfig(updated)
|
|
119
|
-
saveAIConfig(updated)
|
|
120
|
-
setCustomId('')
|
|
121
|
-
setCustomName('')
|
|
122
|
-
setCustomBaseUrl('')
|
|
123
|
-
setCustomModels('')
|
|
124
|
-
alert(`Custom provider "${newCustom.name}" added. You can now configure its key in the API Keys tab.`)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const handleFinish = () => {
|
|
128
|
-
localStorage.setItem('sutrata_onboarding_completed', 'true')
|
|
129
|
-
onClose()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<div className="cs-dialog-overlay" onClick={onClose} style={{ zIndex: 300 }}>
|
|
134
|
-
<div
|
|
135
|
-
ref={dialogRef}
|
|
136
|
-
className="cs-dialog cs-onboarding-dialog"
|
|
137
|
-
role="dialog"
|
|
138
|
-
aria-modal="true"
|
|
139
|
-
aria-label="Intelligence Setup"
|
|
140
|
-
onClick={e => e.stopPropagation()}
|
|
141
|
-
tabIndex={-1}
|
|
142
|
-
>
|
|
143
|
-
<div className="cs-dialog-header">
|
|
144
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
145
|
-
<SparklesIcon size={16} />
|
|
146
|
-
<span className="cs-dialog-title">Intelligence & Dictation Setup</span>
|
|
147
|
-
</div>
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
className="cs-fr-close"
|
|
151
|
-
onClick={onClose}
|
|
152
|
-
aria-label="Close"
|
|
153
|
-
>
|
|
154
|
-
×
|
|
155
|
-
</button>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{/* Tab Selection */}
|
|
159
|
-
<div className="cs-tabs-header">
|
|
160
|
-
<button
|
|
161
|
-
type="button"
|
|
162
|
-
className={`cs-tab-btn ${activeTab === 'intro' ? 'active' : ''}`}
|
|
163
|
-
onClick={() => setActiveTab('intro')}
|
|
164
|
-
>
|
|
165
|
-
Overview
|
|
166
|
-
</button>
|
|
167
|
-
<button
|
|
168
|
-
type="button"
|
|
169
|
-
className={`cs-tab-btn ${activeTab === 'keys' ? 'active' : ''}`}
|
|
170
|
-
onClick={() => setActiveTab('keys')}
|
|
171
|
-
>
|
|
172
|
-
API Keys
|
|
173
|
-
</button>
|
|
174
|
-
<button
|
|
175
|
-
type="button"
|
|
176
|
-
className={`cs-tab-btn ${activeTab === 'custom' ? 'active' : ''}`}
|
|
177
|
-
onClick={() => setActiveTab('custom')}
|
|
178
|
-
>
|
|
179
|
-
Custom Endpoints
|
|
180
|
-
</button>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<div className="cs-onboarding-body">
|
|
184
|
-
{activeTab === 'intro' && (
|
|
185
|
-
<div className="cs-onboarding-section">
|
|
186
|
-
<h3 className="cs-onboarding-h3">Client-Side AI Formatting & Voice</h3>
|
|
187
|
-
<p className="cs-onboarding-desc">
|
|
188
|
-
Sutrata connects directly from your device to high-performance inference APIs.
|
|
189
|
-
Your screenplays and audio transcripts are processed strictly with your own API credentials.
|
|
190
|
-
</p>
|
|
191
|
-
|
|
192
|
-
<div className="cs-onboarding-card">
|
|
193
|
-
<div className="cs-onboarding-step">
|
|
194
|
-
<div className="cs-onboarding-step-num">1</div>
|
|
195
|
-
<div>
|
|
196
|
-
<strong>Get a Free-Tier Key:</strong> Obtain a free API key from Google AI Studio (Gemini/Gemma) or Groq (Llama 3.3).
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
<div className="cs-onboarding-step">
|
|
200
|
-
<div className="cs-onboarding-step-num">2</div>
|
|
201
|
-
<div>
|
|
202
|
-
<strong>Enter Your Key:</strong> Save the key in the API Keys tab. It is encrypted in your local OS keyring or stored securely in your browser session.
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
<div className="cs-onboarding-step">
|
|
206
|
-
<div className="cs-onboarding-step-num">3</div>
|
|
207
|
-
<div>
|
|
208
|
-
<strong>Start Dictating:</strong> Speak naturally into Sutrata; the formatting engine parses characters, dialogue, and scenes automatically.
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
|
|
213
|
-
<div className="cs-dialog-actions" style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '16px' }}>
|
|
214
|
-
<button
|
|
215
|
-
type="button"
|
|
216
|
-
className="cs-voice-btn-primary"
|
|
217
|
-
onClick={() => setActiveTab('keys')}
|
|
218
|
-
>
|
|
219
|
-
Configure API Keys →
|
|
220
|
-
</button>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
)}
|
|
224
|
-
|
|
225
|
-
{activeTab === 'keys' && (
|
|
226
|
-
<div className="cs-onboarding-section">
|
|
227
|
-
<div className="cs-settings-row" style={{ marginBottom: '12px' }}>
|
|
228
|
-
<label className="cs-settings-label" htmlFor="cs-ob-provider">
|
|
229
|
-
Active Provider:
|
|
230
|
-
</label>
|
|
231
|
-
<select
|
|
232
|
-
id="cs-ob-provider"
|
|
233
|
-
className="cs-settings-select"
|
|
234
|
-
value={providerId}
|
|
235
|
-
onChange={e => setProviderId(e.target.value)}
|
|
236
|
-
>
|
|
237
|
-
{providers.map(p => (
|
|
238
|
-
<option key={p.id} value={p.id}>{p.name}</option>
|
|
239
|
-
))}
|
|
240
|
-
</select>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '16px' }}>
|
|
244
|
-
<label className="cs-settings-label" htmlFor="cs-ob-key">
|
|
245
|
-
API Key for {providerId}:
|
|
246
|
-
</label>
|
|
247
|
-
<div style={{ display: 'flex', gap: '8px' }}>
|
|
248
|
-
<input
|
|
249
|
-
id="cs-ob-key"
|
|
250
|
-
type={showKey ? 'text' : 'password'}
|
|
251
|
-
className="cs-voice-textarea"
|
|
252
|
-
style={{ flex: 1, minHeight: '34px', padding: '6px 10px', height: '34px' }}
|
|
253
|
-
value={keyInput}
|
|
254
|
-
onChange={e => setKeyInput(e.target.value)}
|
|
255
|
-
placeholder={`Enter ${providerId} API key...`}
|
|
256
|
-
/>
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
className="cs-voice-btn-ghost"
|
|
260
|
-
onClick={() => setShowKey(!showKey)}
|
|
261
|
-
style={{ padding: '0 10px', height: '34px' }}
|
|
262
|
-
>
|
|
263
|
-
{showKey ? 'Hide' : 'Show'}
|
|
264
|
-
</button>
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
|
-
|
|
268
|
-
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '16px' }}>
|
|
269
|
-
<button
|
|
270
|
-
type="button"
|
|
271
|
-
className="cs-voice-btn-primary"
|
|
272
|
-
onClick={handleSaveKey}
|
|
273
|
-
>
|
|
274
|
-
Save Key
|
|
275
|
-
</button>
|
|
276
|
-
<button
|
|
277
|
-
type="button"
|
|
278
|
-
className="cs-voice-btn-secondary"
|
|
279
|
-
onClick={handleTestKey}
|
|
280
|
-
disabled={testStatus === 'testing'}
|
|
281
|
-
>
|
|
282
|
-
{testStatus === 'testing' ? 'Testing...' : 'Test Connection'}
|
|
283
|
-
</button>
|
|
284
|
-
{testStatus === 'success' && (
|
|
285
|
-
<span style={{ color: '#10b981', fontSize: '13px', fontWeight: 500 }}>✓ Verified connection</span>
|
|
286
|
-
)}
|
|
287
|
-
{testStatus === 'error' && (
|
|
288
|
-
<span style={{ color: '#ef4444', fontSize: '12px' }}>{testError}</span>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
|
-
<div className="cs-dialog-actions" style={{ display: 'flex', justifyContent: 'space-between', marginTop: '20px' }}>
|
|
293
|
-
<button
|
|
294
|
-
type="button"
|
|
295
|
-
className="cs-voice-btn-ghost"
|
|
296
|
-
onClick={() => setActiveTab('intro')}
|
|
297
|
-
>
|
|
298
|
-
← Back
|
|
299
|
-
</button>
|
|
300
|
-
<button
|
|
301
|
-
type="button"
|
|
302
|
-
className="cs-voice-btn-primary"
|
|
303
|
-
onClick={handleFinish}
|
|
304
|
-
>
|
|
305
|
-
Done & Close
|
|
306
|
-
</button>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
)}
|
|
310
|
-
|
|
311
|
-
{activeTab === 'custom' && (
|
|
312
|
-
<div className="cs-onboarding-section">
|
|
313
|
-
<p className="cs-onboarding-desc">
|
|
314
|
-
Connect to local OpenAI-compatible endpoints such as Ollama, vLLM, or LM Studio.
|
|
315
|
-
</p>
|
|
316
|
-
|
|
317
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
|
|
318
|
-
<div>
|
|
319
|
-
<label className="cs-settings-label" htmlFor="cs-ob-custom-id">Provider ID (e.g. ollama):</label>
|
|
320
|
-
<input
|
|
321
|
-
id="cs-ob-custom-id"
|
|
322
|
-
type="text"
|
|
323
|
-
className="cs-voice-textarea"
|
|
324
|
-
style={{ width: '100%', height: '34px', minHeight: '34px', padding: '6px 10px' }}
|
|
325
|
-
value={customId}
|
|
326
|
-
onChange={e => setCustomId(e.target.value)}
|
|
327
|
-
placeholder="ollama"
|
|
328
|
-
/>
|
|
329
|
-
</div>
|
|
330
|
-
<div>
|
|
331
|
-
<label className="cs-settings-label" htmlFor="cs-ob-custom-name">Display Name:</label>
|
|
332
|
-
<input
|
|
333
|
-
id="cs-ob-custom-name"
|
|
334
|
-
type="text"
|
|
335
|
-
className="cs-voice-textarea"
|
|
336
|
-
style={{ width: '100%', height: '34px', minHeight: '34px', padding: '6px 10px' }}
|
|
337
|
-
value={customName}
|
|
338
|
-
onChange={e => setCustomName(e.target.value)}
|
|
339
|
-
placeholder="Local Ollama"
|
|
340
|
-
/>
|
|
341
|
-
</div>
|
|
342
|
-
<div>
|
|
343
|
-
<label className="cs-settings-label" htmlFor="cs-ob-custom-url">Base URL:</label>
|
|
344
|
-
<input
|
|
345
|
-
id="cs-ob-custom-url"
|
|
346
|
-
type="text"
|
|
347
|
-
className="cs-voice-textarea"
|
|
348
|
-
style={{ width: '100%', height: '34px', minHeight: '34px', padding: '6px 10px' }}
|
|
349
|
-
value={customBaseUrl}
|
|
350
|
-
onChange={e => setCustomBaseUrl(e.target.value)}
|
|
351
|
-
placeholder="http://localhost:11434/v1"
|
|
352
|
-
/>
|
|
353
|
-
</div>
|
|
354
|
-
<div>
|
|
355
|
-
<label className="cs-settings-label" htmlFor="cs-ob-custom-models">Models (id:name comma-separated):</label>
|
|
356
|
-
<input
|
|
357
|
-
id="cs-ob-custom-models"
|
|
358
|
-
type="text"
|
|
359
|
-
className="cs-voice-textarea"
|
|
360
|
-
style={{ width: '100%', height: '34px', minHeight: '34px', padding: '6px 10px' }}
|
|
361
|
-
value={customModels}
|
|
362
|
-
onChange={e => setCustomModels(e.target.value)}
|
|
363
|
-
placeholder="llama3:Llama 3, mistral:Mistral 7B"
|
|
364
|
-
/>
|
|
365
|
-
</div>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '16px' }}>
|
|
369
|
-
<button
|
|
370
|
-
type="button"
|
|
371
|
-
className="cs-voice-btn-secondary"
|
|
372
|
-
onClick={handleAddCustomProvider}
|
|
373
|
-
>
|
|
374
|
-
Register Endpoint
|
|
375
|
-
</button>
|
|
376
|
-
<button
|
|
377
|
-
type="button"
|
|
378
|
-
className="cs-voice-btn-primary"
|
|
379
|
-
onClick={() => setActiveTab('keys')}
|
|
380
|
-
>
|
|
381
|
-
Go to API Keys →
|
|
382
|
-
</button>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
)}
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
)
|
|
390
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { callAI, getAIConfig } from './ai-client'
|
|
3
|
-
import { VOICE_FORMAT_SYSTEM_PROMPT } from './prompts'
|
|
4
|
-
import { SparklesIcon } from '../shell/icons'
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
rawTranscript: string
|
|
8
|
-
langCode: string
|
|
9
|
-
onConfirm: (formattedText: string) => void
|
|
10
|
-
onCancel: () => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function VoiceConfirmDialog({ rawTranscript, langCode, onConfirm, onCancel }: Props) {
|
|
14
|
-
const [formattedText, setFormattedText] = useState<string>('')
|
|
15
|
-
const [isLoading, setIsLoading] = useState<boolean>(true)
|
|
16
|
-
const [error, setError] = useState<string>('')
|
|
17
|
-
const [alwaysAutoInsert, setAlwaysAutoInsert] = useState<boolean>(
|
|
18
|
-
localStorage.getItem('sutrata_always_auto_insert') === 'true'
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
void formatTranscript()
|
|
23
|
-
}, [rawTranscript])
|
|
24
|
-
|
|
25
|
-
const formatTranscript = async () => {
|
|
26
|
-
setIsLoading(true)
|
|
27
|
-
setError('')
|
|
28
|
-
try {
|
|
29
|
-
const config = getAIConfig()
|
|
30
|
-
const prompt = `Raw Voice Transcript (${langCode}):\n"${rawTranscript}"`
|
|
31
|
-
const result = await callAI(prompt, VOICE_FORMAT_SYSTEM_PROMPT, config, { clean: 'voice' })
|
|
32
|
-
setFormattedText(result.trim())
|
|
33
|
-
} catch (e: any) {
|
|
34
|
-
console.error('AI formatting failed:', e)
|
|
35
|
-
setError(e.message || String(e))
|
|
36
|
-
setFormattedText(rawTranscript)
|
|
37
|
-
} finally {
|
|
38
|
-
setIsLoading(false)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const handleConfirm = () => {
|
|
43
|
-
localStorage.setItem('sutrata_always_auto_insert', alwaysAutoInsert ? 'true' : 'false')
|
|
44
|
-
onConfirm(formattedText)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className="cs-dialog-overlay" onClick={onCancel} style={{ zIndex: 310 }}>
|
|
49
|
-
<div
|
|
50
|
-
className="cs-dialog cs-voice-confirm-dialog"
|
|
51
|
-
role="dialog"
|
|
52
|
-
aria-modal="true"
|
|
53
|
-
aria-label="Confirm Voice Dictation Formatting"
|
|
54
|
-
onClick={e => e.stopPropagation()}
|
|
55
|
-
>
|
|
56
|
-
<div className="cs-dialog-header">
|
|
57
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
58
|
-
<SparklesIcon size={16} />
|
|
59
|
-
<span className="cs-dialog-title">Review Voice Formatting</span>
|
|
60
|
-
</div>
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
className="cs-fr-close"
|
|
64
|
-
onClick={onCancel}
|
|
65
|
-
aria-label="Close"
|
|
66
|
-
>
|
|
67
|
-
×
|
|
68
|
-
</button>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div className="cs-voice-confirm-body">
|
|
72
|
-
{isLoading ? (
|
|
73
|
-
<div className="cs-voice-loading-state">
|
|
74
|
-
<div className="cs-spinner" style={{ width: '28px', height: '28px', borderWidth: '2.5px' }} />
|
|
75
|
-
<span style={{ fontSize: '13px', color: 'var(--cs-ui-text-dark-secondary, #a8a69d)' }}>
|
|
76
|
-
Formatting speech into Sutra screenplay format...
|
|
77
|
-
</span>
|
|
78
|
-
</div>
|
|
79
|
-
) : (
|
|
80
|
-
<>
|
|
81
|
-
{error && (
|
|
82
|
-
<div className="cs-voice-error" role="alert">
|
|
83
|
-
AI formatting notice: {error} (showing raw input below)
|
|
84
|
-
</div>
|
|
85
|
-
)}
|
|
86
|
-
|
|
87
|
-
<div className="cs-voice-grid">
|
|
88
|
-
<div className="cs-voice-column">
|
|
89
|
-
<span className="cs-voice-col-title">Raw Dictation</span>
|
|
90
|
-
<div className="cs-voice-box-raw">{rawTranscript}</div>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div className="cs-voice-column">
|
|
94
|
-
<span className="cs-voice-col-title">Formatted Sutra</span>
|
|
95
|
-
<textarea
|
|
96
|
-
className="cs-voice-box-formatted"
|
|
97
|
-
value={formattedText}
|
|
98
|
-
onChange={e => setFormattedText(e.target.value)}
|
|
99
|
-
rows={8}
|
|
100
|
-
/>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<div className="cs-voice-options">
|
|
105
|
-
<label className="cs-voice-checkbox-label">
|
|
106
|
-
<input
|
|
107
|
-
type="checkbox"
|
|
108
|
-
checked={alwaysAutoInsert}
|
|
109
|
-
onChange={e => setAlwaysAutoInsert(e.target.checked)}
|
|
110
|
-
/>
|
|
111
|
-
<span>Always auto-insert directly into screenplay (skip review)</span>
|
|
112
|
-
</label>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div className="cs-dialog-actions" style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' }}>
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
className="cs-voice-btn-ghost"
|
|
119
|
-
onClick={onCancel}
|
|
120
|
-
>
|
|
121
|
-
Discard
|
|
122
|
-
</button>
|
|
123
|
-
<button
|
|
124
|
-
type="button"
|
|
125
|
-
className="cs-voice-btn-primary"
|
|
126
|
-
onClick={handleConfirm}
|
|
127
|
-
>
|
|
128
|
-
Insert Into Screenplay
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
)
|
|
137
|
-
}
|