feat(web): configure DeepSeek during onboarding

This commit is contained in:
Yichen Jiang
2026-07-30 12:41:09 +08:00
parent 65bd54f8b4
commit 9182db00ef
24 changed files with 1051 additions and 53 deletions
@@ -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))