@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
package/tests/AppShell.test.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
3
|
-
import { DocumentProvider } from '../src/context/DocumentContext'
|
|
4
|
-
import { LanguageProvider } from '../src/i18n/LanguageContext'
|
|
5
|
-
import { TranslationProvider } from '../src/i18n/useTranslation'
|
|
6
|
-
import { AppShell } from '../src/shell/AppShell'
|
|
7
|
-
|
|
8
|
-
describe('AppShell', () => {
|
|
9
|
-
it('renders the AppBar and the ElementToolbar when ribbon is visible', () => {
|
|
10
|
-
render(
|
|
11
|
-
<TranslationProvider>
|
|
12
|
-
<DocumentProvider>
|
|
13
|
-
<LanguageProvider>
|
|
14
|
-
<AppShell />
|
|
15
|
-
</LanguageProvider>
|
|
16
|
-
</DocumentProvider>
|
|
17
|
-
</TranslationProvider>
|
|
18
|
-
)
|
|
19
|
-
expect(screen.getByRole('button', { name: /scene navigator/i })).toBeInTheDocument()
|
|
20
|
-
expect(screen.getByRole('toolbar', { name: /editing toolbar/i })).toBeInTheDocument()
|
|
21
|
-
})
|
|
22
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, it, expect } from 'vitest'
|
|
3
|
-
import { render, screen } from '@testing-library/react'
|
|
4
|
-
import { ElementToolbar } from '../src/shell/ElementToolbar'
|
|
5
|
-
import { TranslationProvider } from '../src/i18n/useTranslation'
|
|
6
|
-
|
|
7
|
-
describe('ElementToolbar', () => {
|
|
8
|
-
it('renders element buttons and B/I/U at the end', () => {
|
|
9
|
-
render(
|
|
10
|
-
<TranslationProvider>
|
|
11
|
-
<ElementToolbar />
|
|
12
|
-
</TranslationProvider>
|
|
13
|
-
)
|
|
14
|
-
expect(screen.getByRole('button', { name: /scene heading/i })).toBeInTheDocument()
|
|
15
|
-
expect(screen.getByRole('button', { name: /character/i })).toBeInTheDocument()
|
|
16
|
-
const buttons = screen.getAllByRole('button')
|
|
17
|
-
const labels = buttons.map(b => b.getAttribute('aria-label'))
|
|
18
|
-
expect(labels.indexOf('Bold')).toBeGreaterThan(labels.indexOf('Scene Heading'))
|
|
19
|
-
expect(labels).toContain('Underline')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('renders a button for every screenplay element, including Lyrics', () => {
|
|
23
|
-
render(
|
|
24
|
-
<TranslationProvider>
|
|
25
|
-
<ElementToolbar />
|
|
26
|
-
</TranslationProvider>
|
|
27
|
-
)
|
|
28
|
-
const labels = screen.getAllByRole('button').map(b => b.getAttribute('aria-label'))
|
|
29
|
-
for (const name of [
|
|
30
|
-
'Scene Heading', 'Action', 'Character', 'Dialogue', 'Parenthetical',
|
|
31
|
-
'Transition', 'Centered', 'Lyrics', 'Note', 'Page Break', 'Section',
|
|
32
|
-
]) {
|
|
33
|
-
expect(labels).toContain(name)
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
-
import { LanguageStatus } from '../src/shell/LanguageStatus'
|
|
4
|
-
import { LanguageProvider } from '../src/i18n/LanguageContext'
|
|
5
|
-
import { DocumentProvider } from '../src/context/DocumentContext'
|
|
6
|
-
|
|
7
|
-
function TestWrapper({ children }: { children: React.ReactNode }) {
|
|
8
|
-
return (
|
|
9
|
-
<DocumentProvider>
|
|
10
|
-
<LanguageProvider>{children}</LanguageProvider>
|
|
11
|
-
</DocumentProvider>
|
|
12
|
-
)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('LanguageStatus', () => {
|
|
16
|
-
it('renders with current language code', () => {
|
|
17
|
-
render(
|
|
18
|
-
<TestWrapper>
|
|
19
|
-
<LanguageStatus />
|
|
20
|
-
</TestWrapper>
|
|
21
|
-
)
|
|
22
|
-
// Should show "EN" for English (default)
|
|
23
|
-
expect(screen.getByText('EN')).toBeInTheDocument()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('opens menu on button click', () => {
|
|
27
|
-
render(
|
|
28
|
-
<TestWrapper>
|
|
29
|
-
<LanguageStatus />
|
|
30
|
-
</TestWrapper>
|
|
31
|
-
)
|
|
32
|
-
const btn = screen.getByText('EN')
|
|
33
|
-
fireEvent.click(btn)
|
|
34
|
-
// Menu should be visible with language options
|
|
35
|
-
expect(screen.getByText('English')).toBeInTheDocument()
|
|
36
|
-
expect(screen.getByText('Tamil')).toBeInTheDocument()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('closes menu after language selection', () => {
|
|
40
|
-
const { container } = render(
|
|
41
|
-
<TestWrapper>
|
|
42
|
-
<LanguageStatus />
|
|
43
|
-
</TestWrapper>
|
|
44
|
-
)
|
|
45
|
-
const btn = screen.getByText('EN')
|
|
46
|
-
fireEvent.click(btn)
|
|
47
|
-
const tamilOption = screen.getByText('Tamil')
|
|
48
|
-
fireEvent.click(tamilOption)
|
|
49
|
-
// Menu should close
|
|
50
|
-
expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('highlights current language in menu', () => {
|
|
54
|
-
const { container } = render(
|
|
55
|
-
<TestWrapper>
|
|
56
|
-
<LanguageStatus />
|
|
57
|
-
</TestWrapper>
|
|
58
|
-
)
|
|
59
|
-
const btn = screen.getByText('EN')
|
|
60
|
-
fireEvent.click(btn)
|
|
61
|
-
// Find the English option in the menu
|
|
62
|
-
const menuOptions = container.querySelectorAll('.cs-lang-option')
|
|
63
|
-
// First option should be English and should be active
|
|
64
|
-
expect(menuOptions[0]).toHaveClass('cs-lang-active')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('updates document text with language tag on override', () => {
|
|
68
|
-
const { container } = render(
|
|
69
|
-
<TestWrapper>
|
|
70
|
-
<LanguageStatus />
|
|
71
|
-
</TestWrapper>
|
|
72
|
-
)
|
|
73
|
-
const btn = screen.getByText('EN')
|
|
74
|
-
fireEvent.click(btn)
|
|
75
|
-
const tamilOption = screen.getByText('Tamil')
|
|
76
|
-
fireEvent.click(tamilOption)
|
|
77
|
-
// Text should have been mutated with language tag
|
|
78
|
-
// Menu should be closed
|
|
79
|
-
expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('renders all 10 language options in menu', () => {
|
|
83
|
-
render(
|
|
84
|
-
<TestWrapper>
|
|
85
|
-
<LanguageStatus />
|
|
86
|
-
</TestWrapper>
|
|
87
|
-
)
|
|
88
|
-
const btn = screen.getByText('EN')
|
|
89
|
-
fireEvent.click(btn)
|
|
90
|
-
|
|
91
|
-
const expectedLanguages = [
|
|
92
|
-
'English',
|
|
93
|
-
'Hindi (Devanagari)',
|
|
94
|
-
'Tamil',
|
|
95
|
-
'Telugu',
|
|
96
|
-
'Kannada',
|
|
97
|
-
'Malayalam',
|
|
98
|
-
'Bengali',
|
|
99
|
-
'Gujarati',
|
|
100
|
-
'Gurmukhi',
|
|
101
|
-
'Odia',
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
expectedLanguages.forEach(lang => {
|
|
105
|
-
expect(screen.getByText(lang)).toBeInTheDocument()
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('toggles menu visibility on repeated button clicks', () => {
|
|
110
|
-
const { container } = render(
|
|
111
|
-
<TestWrapper>
|
|
112
|
-
<LanguageStatus />
|
|
113
|
-
</TestWrapper>
|
|
114
|
-
)
|
|
115
|
-
const btn = screen.getByText('EN')
|
|
116
|
-
|
|
117
|
-
// Open menu
|
|
118
|
-
fireEvent.click(btn)
|
|
119
|
-
expect(container.querySelector('.cs-lang-menu')).toBeInTheDocument()
|
|
120
|
-
|
|
121
|
-
// Close menu
|
|
122
|
-
fireEvent.click(btn)
|
|
123
|
-
expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
|
|
124
|
-
|
|
125
|
-
// Open menu again
|
|
126
|
-
fireEvent.click(btn)
|
|
127
|
-
expect(container.querySelector('.cs-lang-menu')).toBeInTheDocument()
|
|
128
|
-
})
|
|
129
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { render, screen, act, fireEvent } from '@testing-library/react';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { SelectionMenu } from '../src/editor/SelectionMenu';
|
|
5
|
-
import { DocumentProvider } from '../src/context/DocumentContext';
|
|
6
|
-
import { LanguageProvider } from '../src/i18n/LanguageContext';
|
|
7
|
-
import { TranslationProvider } from '../src/i18n/useTranslation';
|
|
8
|
-
|
|
9
|
-
describe('SelectionMenu floating toolbar', () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
vi.restoreAllMocks();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('renders nothing when there is no selection', () => {
|
|
15
|
-
render(
|
|
16
|
-
<TranslationProvider>
|
|
17
|
-
<DocumentProvider>
|
|
18
|
-
<LanguageProvider>
|
|
19
|
-
<SelectionMenu />
|
|
20
|
-
</LanguageProvider>
|
|
21
|
-
</DocumentProvider>
|
|
22
|
-
</TranslationProvider>
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
expect(screen.queryByText('Format')).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('renders menu when selection is present inside editor', () => {
|
|
29
|
-
const editorDiv = document.createElement('div');
|
|
30
|
-
editorDiv.className = 'ProseMirror';
|
|
31
|
-
|
|
32
|
-
const mockSelection = {
|
|
33
|
-
isCollapsed: false,
|
|
34
|
-
rangeCount: 1,
|
|
35
|
-
toString: () => 'some unformatted text',
|
|
36
|
-
getRangeAt: () => ({
|
|
37
|
-
commonAncestorContainer: editorDiv,
|
|
38
|
-
getBoundingClientRect: () => ({
|
|
39
|
-
left: 100,
|
|
40
|
-
width: 200,
|
|
41
|
-
top: 150,
|
|
42
|
-
height: 20,
|
|
43
|
-
}),
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
vi.spyOn(window, 'getSelection').mockImplementation(() => mockSelection as any);
|
|
48
|
-
|
|
49
|
-
render(
|
|
50
|
-
<TranslationProvider>
|
|
51
|
-
<DocumentProvider>
|
|
52
|
-
<LanguageProvider>
|
|
53
|
-
<SelectionMenu />
|
|
54
|
-
</LanguageProvider>
|
|
55
|
-
</DocumentProvider>
|
|
56
|
-
</TranslationProvider>
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
act(() => {
|
|
60
|
-
document.dispatchEvent(new Event('selectionchange'));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
expect(screen.getByText('Format')).toBeInTheDocument();
|
|
64
|
-
expect(screen.getByText('Cut')).toBeInTheDocument();
|
|
65
|
-
expect(screen.getByText('Copy')).toBeInTheDocument();
|
|
66
|
-
|
|
67
|
-
// Clicking Format should open the confirm dialog
|
|
68
|
-
fireEvent.click(screen.getByText('Format'));
|
|
69
|
-
expect(screen.getByText('Formatting speech into Sutra screenplay format...')).toBeInTheDocument();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
3
|
-
import { ToolbarButton } from '../src/shell/ToolbarButton'
|
|
4
|
-
|
|
5
|
-
describe('ToolbarButton', () => {
|
|
6
|
-
it('renders label, sigil and shortcut in the tooltip', () => {
|
|
7
|
-
render(
|
|
8
|
-
<ToolbarButton label="Scene Heading" sigil="##" shortcut="" active={false} onClick={() => {}}>
|
|
9
|
-
<span>icon</span>
|
|
10
|
-
</ToolbarButton>
|
|
11
|
-
)
|
|
12
|
-
expect(screen.getByRole('button', { name: /scene heading/i })).toBeInTheDocument()
|
|
13
|
-
expect(screen.getByText('##')).toBeInTheDocument()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('applies active class when active', () => {
|
|
17
|
-
render(
|
|
18
|
-
<ToolbarButton label="Bold" sigil="" shortcut="Mod-B" active={true} onClick={() => {}}>
|
|
19
|
-
<span>B</span>
|
|
20
|
-
</ToolbarButton>
|
|
21
|
-
)
|
|
22
|
-
expect(screen.getByRole('button')).toHaveClass('cs-tb-active')
|
|
23
|
-
})
|
|
24
|
-
})
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { getAIConfig, saveAIConfig, getApiKey, setApiKey, deleteApiKey, fetchProviders, cleanVoiceResponse } from '../src/ai/ai-client';
|
|
3
|
-
|
|
4
|
-
describe('AI Client Key Management & Onboarding', () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
localStorage.clear();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('should return default AIConfig when none is saved', () => {
|
|
10
|
-
const config = getAIConfig();
|
|
11
|
-
expect(config.preferredProvider).toBe('anthropic');
|
|
12
|
-
expect(config.preferredModel).toBe('');
|
|
13
|
-
expect(config.customProviders).toEqual([]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should save and load AIConfig correctly', () => {
|
|
17
|
-
const customConfig = {
|
|
18
|
-
preferredProvider: 'groq',
|
|
19
|
-
preferredModel: 'llama-3.3-70b-versatile',
|
|
20
|
-
customProviders: [
|
|
21
|
-
{
|
|
22
|
-
id: 'local-ollama',
|
|
23
|
-
name: 'Local Ollama',
|
|
24
|
-
baseUrl: 'http://localhost:11434/v1',
|
|
25
|
-
models: [{ id: 'llama3', name: 'Llama 3' }]
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
};
|
|
29
|
-
saveAIConfig(customConfig);
|
|
30
|
-
const loaded = getAIConfig();
|
|
31
|
-
expect(loaded).toEqual(customConfig);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should store and delete API keys in localStorage', async () => {
|
|
35
|
-
const geminiKey = 'test-gemini-key-123';
|
|
36
|
-
await setApiKey('gemini', geminiKey);
|
|
37
|
-
const loadedKey = await getApiKey('gemini');
|
|
38
|
-
expect(loadedKey).toBe(geminiKey);
|
|
39
|
-
|
|
40
|
-
await deleteApiKey('gemini');
|
|
41
|
-
const clearedKey = await getApiKey('gemini');
|
|
42
|
-
expect(clearedKey).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should merge custom providers with base providers', async () => {
|
|
46
|
-
const customConfig = {
|
|
47
|
-
preferredProvider: 'local-ollama',
|
|
48
|
-
preferredModel: 'llama3',
|
|
49
|
-
customProviders: [
|
|
50
|
-
{
|
|
51
|
-
id: 'local-ollama',
|
|
52
|
-
name: 'Local Ollama',
|
|
53
|
-
baseUrl: 'http://localhost:11434/v1',
|
|
54
|
-
models: [{ id: 'llama3', name: 'Llama 3' }]
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
};
|
|
58
|
-
const list = await fetchProviders(customConfig);
|
|
59
|
-
const ollama = list.find(p => p.id === 'local-ollama');
|
|
60
|
-
expect(ollama).toBeDefined();
|
|
61
|
-
expect(ollama?.name).toBe('Local Ollama');
|
|
62
|
-
expect(ollama?.models[0]?.id).toBe('llama3');
|
|
63
|
-
|
|
64
|
-
// Default provider should still exist
|
|
65
|
-
const gemini = list.find(p => p.id === 'gemini');
|
|
66
|
-
expect(gemini).toBeDefined();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('cleanVoiceResponse', () => {
|
|
70
|
-
it('should clean voice responses by stripping code blocks, reasoning, and shorthand comments', () => {
|
|
71
|
-
const inputCluttered = `
|
|
72
|
-
* Input: Raw voice transcript "New Scene scene #2 Exterior swimming pool daytime."
|
|
73
|
-
* Task: Convert to Sutra screenplay format.
|
|
74
|
-
* Sutra Rules:
|
|
75
|
-
* Scene Headings: \`##\` (e.g., \`## INT. COFFEE SHOP - DAY\`).
|
|
76
|
-
* Format: \`## EXT. SWIMMING POOL - DAY {#2}\` (Standard screenplay shorthand for Exterior and Daytime).
|
|
77
|
-
|
|
78
|
-
## EXT. SWIMMING POOL - DAY {#2}
|
|
79
|
-
`;
|
|
80
|
-
expect(cleanVoiceResponse(inputCluttered)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
|
|
81
|
-
|
|
82
|
-
const inputWithCodeBlock = '```cine\n## EXT. SWIMMING POOL - DAY {#2}\n```';
|
|
83
|
-
expect(cleanVoiceResponse(inputWithCodeBlock)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
|
|
84
|
-
|
|
85
|
-
const inputWithTrailingShorthand = '## EXT. SWIMMING POOL - DAY {#2} (Standard screenplay shorthand for Exterior and Daytime)';
|
|
86
|
-
expect(cleanVoiceResponse(inputWithTrailingShorthand)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
|
|
87
|
-
|
|
88
|
-
const userGemmaClutter = `
|
|
89
|
-
Rules:
|
|
90
|
-
Scene ID: Append as \`{#ID}\` at the end.
|
|
91
|
-
|
|
92
|
-
"7" -> Scene ID {#7}
|
|
93
|
-
"exterior" -> EXT.
|
|
94
|
-
"Road" -> ROAD
|
|
95
|
-
"evening" -> EVENING
|
|
96
|
-
|
|
97
|
-
* \`## EXT. ROAD - EVENING {#7}
|
|
98
|
-
`;
|
|
99
|
-
expect(cleanVoiceResponse(userGemmaClutter)).toBe('## EXT. ROAD - EVENING {#7}');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
package/tests/ai-status.test.tsx
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { render, screen, act } from '@testing-library/react'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { formatActiveCalls, subscribeToAICalls, mockActiveCallsList } from '../src/ai/ai-client'
|
|
5
|
-
import { AppShell } from '../src/shell/AppShell'
|
|
6
|
-
import { DocumentProvider } from '../src/context/DocumentContext'
|
|
7
|
-
import { LanguageProvider } from '../src/i18n/LanguageContext'
|
|
8
|
-
import { TranslationProvider } from '../src/i18n/useTranslation'
|
|
9
|
-
|
|
10
|
-
describe('AI Status Bar indicator', () => {
|
|
11
|
-
it('formats active calls correctly', () => {
|
|
12
|
-
expect(formatActiveCalls([])).toBe('')
|
|
13
|
-
expect(formatActiveCalls([{ providerName: 'Gemini', model: 'gemma-31b' }])).toBe('Calling Gemini (gemma-31b)...')
|
|
14
|
-
expect(formatActiveCalls([
|
|
15
|
-
{ providerName: 'Gemini', model: 'gemma-31b' },
|
|
16
|
-
{ providerName: 'Gemini', model: 'gemma-31b' },
|
|
17
|
-
])).toBe('Calling Gemini (gemma-31b) (2 calls)...')
|
|
18
|
-
expect(formatActiveCalls([
|
|
19
|
-
{ providerName: 'Gemini', model: 'gemma-31b' },
|
|
20
|
-
{ providerName: 'Groq', model: 'llama3' },
|
|
21
|
-
])).toBe('Calling Gemini (gemma-31b), Groq (llama3)...')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('subscribes and notifies when calls change', () => {
|
|
25
|
-
const cb = vi.fn()
|
|
26
|
-
const unsubscribe = subscribeToAICalls(cb)
|
|
27
|
-
expect(cb).toHaveBeenCalledWith([])
|
|
28
|
-
|
|
29
|
-
act(() => {
|
|
30
|
-
mockActiveCallsList([{ providerName: 'Groq', model: 'llama3' }])
|
|
31
|
-
})
|
|
32
|
-
expect(cb).toHaveBeenLastCalledWith([{ providerName: 'Groq', model: 'llama3' }])
|
|
33
|
-
|
|
34
|
-
act(() => {
|
|
35
|
-
mockActiveCallsList([])
|
|
36
|
-
})
|
|
37
|
-
expect(cb).toHaveBeenLastCalledWith([])
|
|
38
|
-
|
|
39
|
-
unsubscribe()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('renders status indicator inside AppShell', () => {
|
|
43
|
-
render(
|
|
44
|
-
<TranslationProvider>
|
|
45
|
-
<DocumentProvider>
|
|
46
|
-
<LanguageProvider>
|
|
47
|
-
<AppShell />
|
|
48
|
-
</LanguageProvider>
|
|
49
|
-
</DocumentProvider>
|
|
50
|
-
</TranslationProvider>
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
// Initially, no indicator is rendered (since calls list is empty)
|
|
54
|
-
expect(screen.queryByText(/Calling/)).toBeNull()
|
|
55
|
-
|
|
56
|
-
// Mock active calls
|
|
57
|
-
act(() => {
|
|
58
|
-
mockActiveCallsList([{ providerName: 'Google AI Studio', model: 'gemma-31b' }])
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Now it should be displayed
|
|
62
|
-
expect(screen.getByText('Calling Google AI Studio (gemma-31b)...')).toBeInTheDocument()
|
|
63
|
-
|
|
64
|
-
// Clear calls
|
|
65
|
-
act(() => {
|
|
66
|
-
mockActiveCallsList([])
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
// Indicator should be gone
|
|
70
|
-
expect(screen.queryByText(/Calling/)).toBeNull()
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { extractJson, listModels, hasConfiguredApiKey, callAI, setApiKey, deleteApiKey, ProviderConfig } from '../src/ai/ai-client';
|
|
3
|
-
|
|
4
|
-
describe('extractJson', () => {
|
|
5
|
-
it('parses a clean JSON object', () => {
|
|
6
|
-
expect(extractJson<{ synopsis: string; duration: string }>('{"synopsis":"A meets B.","duration":"01:30"}'))
|
|
7
|
-
.toEqual({ synopsis: 'A meets B.', duration: '01:30' });
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('parses JSON wrapped in a markdown fence', () => {
|
|
11
|
-
const raw = '```json\n{"synopsis":"A meets B.","duration":"01:30"}\n```';
|
|
12
|
-
expect(extractJson<{ synopsis: string; duration: string }>(raw))
|
|
13
|
-
.toEqual({ synopsis: 'A meets B.', duration: '01:30' });
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('recovers a JSON object embedded in stray commentary', () => {
|
|
17
|
-
const raw = 'Sure, here is the result:\n{"synopsis":"A meets B.","duration":"01:30"}\nHope that helps!';
|
|
18
|
-
expect(extractJson<{ synopsis: string; duration: string }>(raw))
|
|
19
|
-
.toEqual({ synopsis: 'A meets B.', duration: '01:30' });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('throws with the raw text attached when nothing parses', () => {
|
|
23
|
-
expect(() => extractJson('not json at all')).toThrow(/AI did not return valid JSON/);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('listModels', () => {
|
|
28
|
-
it('falls back to the static model list when the live endpoint is unreachable', async () => {
|
|
29
|
-
const provider: ProviderConfig = {
|
|
30
|
-
id: 'made-up-provider',
|
|
31
|
-
name: 'Made Up Provider',
|
|
32
|
-
baseUrl: 'not-a-real-url',
|
|
33
|
-
models: [{ id: 'static-model', name: 'Static Model' }],
|
|
34
|
-
};
|
|
35
|
-
const models = await listModels(provider, 'fake-key');
|
|
36
|
-
expect(models).toEqual(provider.models);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('preferred-model resolution (regression: wrong provider/model used)', () => {
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
localStorage.clear();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('sends the user-selected model for the preferred provider, even when that model is not in the static provider list', async () => {
|
|
46
|
-
// Reproduces the reported bug: the user picks a model from a
|
|
47
|
-
// provider's *live* /models listing (e.g. "qwen/qwen3.8-27b" for
|
|
48
|
-
// Groq) via Settings, which is never a member of the narrow static
|
|
49
|
-
// generated-providers.json list. That used to get silently discarded
|
|
50
|
-
// and replaced with the static list's first (often stale/retired)
|
|
51
|
-
// model, making the call fail — or, if another provider's key also
|
|
52
|
-
// happened to be configured, silently fall through to a call against
|
|
53
|
-
// a completely different provider.
|
|
54
|
-
const originalFetch = global.fetch;
|
|
55
|
-
const sentBodies: any[] = [];
|
|
56
|
-
global.fetch = vi.fn(async (url: any, init: any) => {
|
|
57
|
-
if (String(url).includes('api.groq.com')) {
|
|
58
|
-
sentBodies.push(JSON.parse(init.body));
|
|
59
|
-
return new Response(JSON.stringify({ choices: [{ message: { content: 'ok' } }] }), { status: 200 });
|
|
60
|
-
}
|
|
61
|
-
throw new Error('network unavailable in test');
|
|
62
|
-
}) as any;
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await setApiKey('groq', 'test-groq-key');
|
|
66
|
-
const config = { preferredProvider: 'groq', preferredModel: 'qwen/qwen3.8-27b', customProviders: [] };
|
|
67
|
-
|
|
68
|
-
const result = await callAI('hello', 'system prompt', config);
|
|
69
|
-
|
|
70
|
-
expect(result).toBe('ok');
|
|
71
|
-
expect(sentBodies).toHaveLength(1);
|
|
72
|
-
expect(sentBodies[0].model).toBe('qwen/qwen3.8-27b');
|
|
73
|
-
} finally {
|
|
74
|
-
global.fetch = originalFetch;
|
|
75
|
-
await deleteApiKey('groq');
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('hasConfiguredApiKey', () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
localStorage.clear();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('is false with no stored keys and true once one provider has a key', async () => {
|
|
86
|
-
const config = { preferredProvider: 'anthropic', preferredModel: '', customProviders: [] };
|
|
87
|
-
expect(await hasConfiguredApiKey(config)).toBe(false);
|
|
88
|
-
|
|
89
|
-
await setApiKey('anthropic', 'test-key');
|
|
90
|
-
expect(await hasConfiguredApiKey(config)).toBe(true);
|
|
91
|
-
|
|
92
|
-
await deleteApiKey('anthropic');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { createAutocompletePlugin } from '../../src/autocomplete/AutocompletePlugin'
|
|
3
|
-
|
|
4
|
-
describe('AutocompletePlugin', () => {
|
|
5
|
-
it('createAutocompletePlugin returns a ProseMirror Plugin', () => {
|
|
6
|
-
const plugin = createAutocompletePlugin()
|
|
7
|
-
expect(plugin).toBeDefined()
|
|
8
|
-
expect(typeof plugin.spec).toBe('object')
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('plugin has handleKeyDown prop', () => {
|
|
12
|
-
const plugin = createAutocompletePlugin()
|
|
13
|
-
expect(typeof plugin.spec.props?.handleKeyDown).toBe('function')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('plugin has view prop', () => {
|
|
17
|
-
const plugin = createAutocompletePlugin()
|
|
18
|
-
expect(typeof plugin.spec.view).toBe('function')
|
|
19
|
-
})
|
|
20
|
-
})
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
buildCharacterCandidates,
|
|
4
|
-
buildLocationCandidates,
|
|
5
|
-
buildTransitionCandidates,
|
|
6
|
-
buildMetadataKeyCandidates,
|
|
7
|
-
filterCandidates,
|
|
8
|
-
} from '../../src/autocomplete/suggestions'
|
|
9
|
-
|
|
10
|
-
const SAMPLE = `## INT. LIVING ROOM - DAY
|
|
11
|
-
& location: INT. LIVING ROOM
|
|
12
|
-
|
|
13
|
-
@RIYA
|
|
14
|
-
Hello.
|
|
15
|
-
|
|
16
|
-
@ARJUN (V.O.)
|
|
17
|
-
I remember that night.
|
|
18
|
-
|
|
19
|
-
## EXT. STREET - NIGHT
|
|
20
|
-
|
|
21
|
-
>> FADE TO:
|
|
22
|
-
|
|
23
|
-
& status: done
|
|
24
|
-
& mood: tense
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
describe('suggestions', () => {
|
|
28
|
-
describe('buildCharacterCandidates', () => {
|
|
29
|
-
it('returns character names from @ cues', () => {
|
|
30
|
-
const cands = buildCharacterCandidates(SAMPLE)
|
|
31
|
-
const labels = cands.map(c => c.label)
|
|
32
|
-
expect(labels).toContain('RIYA')
|
|
33
|
-
expect(labels).toContain('ARJUN (V.O.)')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('all have type "character"', () => {
|
|
37
|
-
const cands = buildCharacterCandidates(SAMPLE)
|
|
38
|
-
expect(cands.every(c => c.type === 'character')).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('buildLocationCandidates', () => {
|
|
43
|
-
it('extracts locations from scene headings', () => {
|
|
44
|
-
const cands = buildLocationCandidates(SAMPLE)
|
|
45
|
-
const labels = cands.map(c => c.label)
|
|
46
|
-
expect(labels).toContain('INT. LIVING ROOM')
|
|
47
|
-
expect(labels).toContain('EXT. STREET')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('all have type "location"', () => {
|
|
51
|
-
const cands = buildLocationCandidates(SAMPLE)
|
|
52
|
-
expect(cands.every(c => c.type === 'location')).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('buildTransitionCandidates', () => {
|
|
57
|
-
it('includes predefined transitions', () => {
|
|
58
|
-
const cands = buildTransitionCandidates(SAMPLE)
|
|
59
|
-
const labels = cands.map(c => c.label)
|
|
60
|
-
expect(labels).toContain('CUT TO:')
|
|
61
|
-
expect(labels).toContain('FADE IN:')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('includes transitions used in text', () => {
|
|
65
|
-
const cands = buildTransitionCandidates(SAMPLE)
|
|
66
|
-
const labels = cands.map(c => c.label)
|
|
67
|
-
expect(labels).toContain('FADE TO:')
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('buildMetadataKeyCandidates', () => {
|
|
72
|
-
it('includes reserved keys', () => {
|
|
73
|
-
const cands = buildMetadataKeyCandidates(SAMPLE)
|
|
74
|
-
const labels = cands.map(c => c.label)
|
|
75
|
-
expect(labels).toContain('status')
|
|
76
|
-
expect(labels).toContain('location')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('includes keys used in text', () => {
|
|
80
|
-
const cands = buildMetadataKeyCandidates(SAMPLE)
|
|
81
|
-
const labels = cands.map(c => c.label)
|
|
82
|
-
expect(labels).toContain('mood')
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('filterCandidates', () => {
|
|
87
|
-
it('filters by prefix case-insensitively', () => {
|
|
88
|
-
const cands = [
|
|
89
|
-
{ label: 'RIYA', type: 'character' as const },
|
|
90
|
-
{ label: 'ARJUN', type: 'character' as const },
|
|
91
|
-
{ label: 'RAJ', type: 'character' as const },
|
|
92
|
-
]
|
|
93
|
-
const result = filterCandidates(cands, 'ri')
|
|
94
|
-
expect(result.length).toBe(1)
|
|
95
|
-
expect(result[0]?.label).toBe('RIYA')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('returns all candidates for empty prefix', () => {
|
|
99
|
-
const cands = [
|
|
100
|
-
{ label: 'A', type: 'character' as const },
|
|
101
|
-
{ label: 'B', type: 'character' as const },
|
|
102
|
-
]
|
|
103
|
-
expect(filterCandidates(cands, '').length).toBe(2)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
})
|