feat(web): configure DeepSeek during onboarding
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-settings",
|
||||
"description": "Settings shell plugin: sidebar trigger + modal panel occupying sidebar.settings; declares the settings.section list slot",
|
||||
"description": "Settings shell plugin: sidebar trigger, modal panel, feature sections, and root-scoped onboarding overlays",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -22,6 +22,8 @@ function navIcon(id: string) {
|
||||
type PanelProps = {
|
||||
rows: readonly SettingsSectionRow[]
|
||||
renderSlot: SettingsRootComponentProps['renderSlot']
|
||||
activeId: string | undefined
|
||||
onSelect: (id: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -30,10 +32,9 @@ type PanelProps = {
|
||||
* header button, a mask click, and document-level Escape (mounted only while
|
||||
* open, so the listener lifetime is the panel's).
|
||||
*/
|
||||
function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
|
||||
// Local selection; entries can unmount underneath it, so the render-time
|
||||
function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelProps) {
|
||||
// Entries can unmount underneath the requested id, so the render-time
|
||||
// projection falls back to the first row when the id is gone.
|
||||
const [activeId, setActiveId] = useState<string | undefined>(undefined)
|
||||
const active = rows.find(r => r.id === activeId)?.id ?? rows[0]?.id
|
||||
const titleId = useId()
|
||||
|
||||
@@ -62,7 +63,7 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
|
||||
type="button"
|
||||
className={clsx(css.navCell, row.id === active && css.active)}
|
||||
aria-current={row.id === active ? 'true' : undefined}
|
||||
onClick={() => { setActiveId(row.id) }}
|
||||
onClick={() => { onSelect(row.id) }}
|
||||
>
|
||||
{navIcon(row.id)}
|
||||
<span className={css.navLabel}>{row.label}</span>
|
||||
@@ -92,14 +93,25 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
|
||||
* @returns the settings shell element tree.
|
||||
*/
|
||||
export function SettingsRoot(props: SettingsRootComponentProps) {
|
||||
const { wide, useSections, renderSlot } = props
|
||||
const { wide, useSections, useSessions, renderSlot } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const close = useCallback(() => { setOpen(false) }, [])
|
||||
const [activeId, setActiveId] = useState<string | undefined>(undefined)
|
||||
const close = useCallback(() => {
|
||||
setOpen(false)
|
||||
setActiveId(undefined)
|
||||
}, [])
|
||||
const openSection = useCallback((id: string) => {
|
||||
setActiveId(id)
|
||||
setOpen(true)
|
||||
}, [])
|
||||
|
||||
// The ledger tick keeps the nav rows fresh: registrants re-register with
|
||||
// freshly localized text on locale change, and the trigger/header/close
|
||||
// seats re-render through their own outlets' subscriptions.
|
||||
const rows = useSections(s => s)
|
||||
const onboardingActive = useSessions(state =>
|
||||
state.phase === 'ready'
|
||||
&& (state.current === undefined || state.byId[state.current]?.blank === true))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -112,7 +124,16 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
|
||||
>
|
||||
{renderSlot('settings.trigger', { wide })}
|
||||
</button>
|
||||
{open && <SettingsPanel rows={rows} renderSlot={renderSlot} onClose={close} />}
|
||||
{open && (
|
||||
<SettingsPanel
|
||||
rows={rows}
|
||||
renderSlot={renderSlot}
|
||||
activeId={activeId}
|
||||
onSelect={setActiveId}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
{renderSlot('settings.onboarding', { active: onboardingActive, openSection })}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* item registrant; the shell neither declares nor renders it.)
|
||||
*/
|
||||
'settings.section': { kind: 'list'; scope: 'root'; owner: SettingsSectionOwnerProps }
|
||||
/**
|
||||
* Root-scoped onboarding overlays contributed by settings features. The
|
||||
* shell supplies whether the current navigation state is the empty Hero
|
||||
* and a private callback that opens one settings section; registrants own
|
||||
* readiness, copy, and dialog behavior.
|
||||
*/
|
||||
'settings.onboarding': { kind: 'list'; scope: 'root'; owner: SettingsOnboardingOwnerProps }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +79,14 @@ export interface SettingsSectionOwnerProps {
|
||||
children?: never
|
||||
}
|
||||
|
||||
/** Owner share of a settings-backed onboarding overlay. */
|
||||
export interface SettingsOnboardingOwnerProps {
|
||||
/** Whether the current UI is in its empty Hero/onboarding state. */
|
||||
active: boolean
|
||||
/** Open the settings panel directly on one registered section. */
|
||||
openSection: (id: string) => void
|
||||
}
|
||||
|
||||
/** One nav row projected from a settings.section registration's options. */
|
||||
export interface SettingsSectionRow {
|
||||
id: string
|
||||
@@ -99,5 +114,7 @@ export type SettingsRootInjected = {
|
||||
*/
|
||||
export type SettingsRootComponentProps =
|
||||
PropsRuntime<'sidebar.settings'>
|
||||
& PropsRenderSlots<'settings.trigger' | 'settings.header' | 'settings.close' | 'settings.section'>
|
||||
& PropsRenderSlots<
|
||||
'settings.trigger' | 'settings.header' | 'settings.close' | 'settings.section' | 'settings.onboarding'
|
||||
>
|
||||
& InjectFace<SettingsRootInjected>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* Settings shell plugin, browser half. A pure composition face: occupies the
|
||||
* sidebar-owned `sidebar.settings` hole with the trigger chrome + modal
|
||||
* panel, declares the `settings.trigger` / `settings.header` /
|
||||
* `settings.section` slots, and projects the section ledger into the panel
|
||||
* navigation. The shell ships no copy and reads no locale state — all text
|
||||
* arrives from registrants (ui-settings-general owns the chrome and General
|
||||
* content; features own their rows and sections). Export discipline:
|
||||
* packages/client/AGENTS.md.
|
||||
* panel, declares its chrome, section, and onboarding slots, and projects the
|
||||
* section ledger into panel navigation. The shell ships no copy and reads no
|
||||
* locale state — all text arrives from registrants (ui-settings-general owns
|
||||
* the chrome and General content; features own their rows, sections, and
|
||||
* onboarding overlays). Export discipline: packages/client/AGENTS.md.
|
||||
*/
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -15,7 +14,7 @@ import { SettingsRoot } from './SettingsRoot.tsx'
|
||||
|
||||
export type {
|
||||
SettingsHeaderOwnerProps, SettingsRootComponentProps, SettingsRootInjected,
|
||||
SettingsSectionOwnerProps, SettingsSectionRow, SettingsTriggerOwnerProps,
|
||||
SettingsOnboardingOwnerProps, SettingsSectionOwnerProps, SettingsSectionRow, SettingsTriggerOwnerProps,
|
||||
} from './contract/slots.ts'
|
||||
|
||||
/**
|
||||
@@ -67,6 +66,7 @@ export function apply(ctx: ClientContext): void {
|
||||
'settings.header': { kind: 'single', scope: 'root' },
|
||||
'settings.close': { kind: 'single', scope: 'root' },
|
||||
'settings.section': { kind: 'list', scope: 'root' },
|
||||
'settings.onboarding': { kind: 'list', scope: 'root' },
|
||||
},
|
||||
inject: injected,
|
||||
}, SettingsRoot))
|
||||
|
||||
@@ -24,12 +24,13 @@ function injectedOf(slots: SlotsService): SettingsRootInjected {
|
||||
return (entry.inject as () => SettingsRootInjected)()
|
||||
}
|
||||
|
||||
/** The shell's four child declarations (chrome seats + the section list). */
|
||||
/** The shell's five child declarations (chrome, sections, and onboarding overlays). */
|
||||
const CHILD_SPECS = {
|
||||
'settings.trigger': { kind: 'single', scope: 'root' },
|
||||
'settings.header': { kind: 'single', scope: 'root' },
|
||||
'settings.close': { kind: 'single', scope: 'root' },
|
||||
'settings.section': { kind: 'list', scope: 'root' },
|
||||
'settings.onboarding': { kind: 'list', scope: 'root' },
|
||||
} as const
|
||||
|
||||
describe('ui-settings apply', () => {
|
||||
@@ -37,7 +38,7 @@ describe('ui-settings apply', () => {
|
||||
expect(inject).toEqual(['slots'])
|
||||
})
|
||||
|
||||
it('registers the shell and declares the four child slots, before or after the declaration', async () => {
|
||||
it('registers the shell and declares the five child slots, before or after the declaration', async () => {
|
||||
const before = await bench()
|
||||
declare(before.slots)
|
||||
await before.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
@@ -100,7 +101,7 @@ describe('ui-settings apply', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('unregisters the shell and collapses all four child slots on teardown', async () => {
|
||||
it('unregisters the shell and collapses all five child slots on teardown', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
||||
|
||||
@@ -18,11 +18,12 @@ const SEAT_CONTENT: Record<string, string> = {
|
||||
|
||||
function mount({
|
||||
wide = true,
|
||||
onboardingActive = true,
|
||||
rows = [
|
||||
{ id: 'general', order: 0, label: 'General' },
|
||||
{ id: 'models', order: 10, label: 'Models' },
|
||||
],
|
||||
}: { wide?: boolean; rows?: Row[] } = {}) {
|
||||
}: { wide?: boolean; onboardingActive?: boolean; rows?: Row[] } = {}) {
|
||||
// Mutable row source standing in for the bound useSections hook; bump()
|
||||
// plays a ledger change through the same observable contract.
|
||||
let current = rows
|
||||
@@ -33,10 +34,16 @@ function mount({
|
||||
return SEAT_CONTENT[key]
|
||||
}) as SettingsRootComponentProps['renderSlot'],
|
||||
)
|
||||
// Global standard kit stubs: the shell consumes neither hook.
|
||||
const useSessions = ((select: (state: unknown) => unknown) => select(onboardingActive
|
||||
? { phase: 'ready', current: undefined, byId: {} }
|
||||
: {
|
||||
phase: 'ready',
|
||||
current: 'active-session',
|
||||
byId: { 'active-session': { blank: false } },
|
||||
})) as never
|
||||
const unusedHook = (() => { throw new Error('unused by SettingsRoot') }) as never
|
||||
const props: SettingsRootComponentProps = {
|
||||
useSessions: unusedHook,
|
||||
useSessions,
|
||||
useWorkspaces: unusedHook,
|
||||
wide,
|
||||
useSections: (select) => {
|
||||
@@ -157,6 +164,22 @@ describe('SettingsPanel navigation', () => {
|
||||
expect(screen.queryByTestId('section-general')).toBeNull()
|
||||
})
|
||||
|
||||
it('hands Hero readiness and a direct section opener to onboarding registrants', () => {
|
||||
const { renderSlot } = mount()
|
||||
const onboardingCall = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
|
||||
expect(onboardingCall?.[1]).toMatchObject({ active: true })
|
||||
act(() => {
|
||||
(onboardingCall?.[1] as { openSection: (id: string) => void }).openSection('models')
|
||||
})
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
expect(screen.getByTestId('section-models')).toBeTruthy()
|
||||
|
||||
cleanup()
|
||||
const active = mount({ onboardingActive: false }).renderSlot.mock.calls
|
||||
.find(call => call[0] === 'settings.onboarding')
|
||||
expect(active?.[1]).toMatchObject({ active: false })
|
||||
})
|
||||
|
||||
it('falls back to the first row when the active entry unregisters', () => {
|
||||
const { bump } = mount()
|
||||
openPanel()
|
||||
|
||||
Reference in New Issue
Block a user