diff --git a/web/src/views/SettingsView.tsx b/web/src/views/SettingsView.tsx index 66c2d53..2373a4c 100644 --- a/web/src/views/SettingsView.tsx +++ b/web/src/views/SettingsView.tsx @@ -71,7 +71,25 @@ const BANNER_PRESETS: { label: string; text: string; color: string }[] = [ { label: 'Top Secret / SCI', text: '***** TOP SECRET // SCI *****', color: '#fce83a' }, ] -type SectionId = 'general' | 'access' | 'tools' | 'rag' | 'ext' | 'libraries' +type SectionId = 'general' | 'access' | 'tools' | 'rag' | 'previews' | 'ext' | 'libraries' + +/** + * The settings rail, in order. Model access appears only where the + * deployment signs users in. Feature previews is its own section rather + * than a heading at the foot of another one: a capability nobody can + * find is off for everyone regardless of the switch. + */ +export function settingsSections(opts: { authEnabled: boolean }): { id: SectionId; label: string }[] { + return [ + { id: 'general', label: 'General' }, + ...(opts.authEnabled ? [{ id: 'access' as SectionId, label: 'Model access' }] : []), + { id: 'rag', label: 'External access' }, + { id: 'tools', label: 'Assistant tools' }, + { id: 'previews', label: 'Feature previews' }, + { id: 'ext', label: 'Extensions' }, + { id: 'libraries', label: 'Libraries' }, + ] +} /** Dropdown over the live model catalog; a saved value not in the catalog * stays selectable so settings never silently break. */ @@ -269,14 +287,7 @@ export function SettingsView() { const pickableModels = models.filter(m => !/studio-(agent|rag)/.test(m)) - const sections: { id: SectionId; label: string }[] = [ - { id: 'general', label: 'General' }, - ...(me?.authEnabled ? [{ id: 'access' as SectionId, label: 'Model access' }] : []), - { id: 'rag', label: 'External access' }, - { id: 'tools', label: 'Assistant tools' }, - { id: 'ext', label: 'Extensions' }, - { id: 'libraries', label: 'Libraries' }, - ] + const sections = settingsSections({ authEnabled: !!me?.authEnabled }) const saveRow = (reload = true) => (
@@ -714,25 +725,6 @@ export function SettingsView() { pw code mcp add --transport http studio-kb {location.origin}/api/mcp

pw code signs its platform requests with your existing CLI login, so no token goes in the command.

-
Feature previews
-

- Capabilities that work end to end but are still being shaped. Each is off until switched on here, and each - shows its control in the interface only when it is on and configured. -

-
- setForm({ ...form, voiceEnabled: v })} yesLabel="On" noLabel="Off" /> - Voice conversations: talk with the assistant back and forth, through an Unmute deployment -
-

- Unmute (Kyutai, MIT) wraps a text model with streaming speech recognition, semantic turn-taking, and speech - synthesis. Only the speech services need a GPU; the model that does the talking is a configuration value. - Deploy it with the unmute workflow pointed at this Studio and the studio-voice model, - which is the assistant with its tools and knowledge base answering in spoken sentences, with any API model - behind it (studio-voice/<gateway-model-id>). Then put the deployment's session URL here; - a Voice button appears above the chat. -

- - setForm({ ...form, voiceUrl: e.target.value })} />
Delegation (the assistant working in parallel)

With this on, the assistant may split a request into subtasks and run them concurrently as headless @@ -954,6 +946,31 @@ export function SettingsView() { )} + {section === 'previews' && ( + <> +

Feature previews

+

+ Capabilities that work end to end but are still being shaped. Each is off until switched on here, and each + shows its control in the interface only when it is on and configured. +

+
+ setForm({ ...form, voiceEnabled: v })} yesLabel="On" noLabel="Off" /> + Voice conversations: talk with the assistant back and forth, through an Unmute deployment +
+

+ Unmute (Kyutai, MIT) wraps a text model with streaming speech recognition, semantic turn-taking, and speech + synthesis. Only the speech services need a GPU; the model that does the talking is a configuration value. + Deploy it with the unmute workflow pointed at this Studio and the studio-voice model, + which is the assistant with its tools and knowledge base answering in spoken sentences, with any API model + behind it (studio-voice/<gateway-model-id>). Then put the deployment's session URL here; + a Voice button appears above the chat. The deployment has to be reachable from the browser you use, so a + closed network needs its own Unmute rather than one running elsewhere. +

+ + setForm({ ...form, voiceUrl: e.target.value })} /> + {saveRow()} + + )} {section === 'libraries' && } {section === 'ext' && ( <> diff --git a/web/test/settingsSections.test.tsx b/web/test/settingsSections.test.tsx new file mode 100644 index 0000000..21f51f5 --- /dev/null +++ b/web/test/settingsSections.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { settingsSections } from '../src/views/SettingsView' + +describe('the settings rail', () => { + it('offers Feature previews as its own section, not a heading inside another', () => { + const ids = settingsSections({ authEnabled: false }).map(s => s.id) + expect(ids).toContain('previews') + // It sits with the other capability sections, after the assistant's + // tools and before extensions. + expect(ids.indexOf('previews')).toBeGreaterThan(ids.indexOf('tools')) + expect(ids.indexOf('previews')).toBeLessThan(ids.indexOf('ext')) + }) + + it('shows Model access only where the deployment signs users in', () => { + expect(settingsSections({ authEnabled: false }).map(s => s.id)).not.toContain('access') + expect(settingsSections({ authEnabled: true }).map(s => s.id)).toContain('access') + }) +})