feat(web): route onboarding to Models settings
This commit is contained in:
@@ -2,51 +2,6 @@
|
||||
width: min(420px, 100%);
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
padding-inline: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.input > input {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.advanced {
|
||||
align-self: flex-start;
|
||||
padding-inline: 0;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.diagnostic {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Official-DeepSeek first-run dialog. Readiness comes from the same
|
||||
* provider/settings/credential join as the Models page; the component holds
|
||||
* only the write-only draft and viewing state.
|
||||
* provider/settings/credential join as the Models page; the prompt only
|
||||
* routes the user to that page's single credential editor.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { Button, Input, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ModelsSettingsState, ModelsSettingsStore } from './store.ts'
|
||||
import { deepSeekReadiness } from './store.ts'
|
||||
@@ -21,8 +20,6 @@ export interface DeepSeekOnboardingInjected {
|
||||
controller: ModelsSettingsStore
|
||||
/** Subscription hook bound to the shared join snapshot. */
|
||||
useSnapshot: SnapshotSelectorHook<ModelsSettingsState>
|
||||
/** Write-only credential wire face. */
|
||||
credentials: IApiClient['credentials']
|
||||
/** Feature copy. */
|
||||
t: (key: keyof typeof en) => string
|
||||
}
|
||||
@@ -31,40 +28,23 @@ export interface DeepSeekOnboardingInjected {
|
||||
export type DeepSeekOnboardingDialogProps =
|
||||
PropsRuntime<'settings.onboarding'> & DeepSeekOnboardingInjected
|
||||
|
||||
/** Remove the submitted non-empty secret from any error text before it reaches the DOM. */
|
||||
function redactSecret(message: string, secret: string): string {
|
||||
return message.split(secret).join('[redacted]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the first-run credential dialog while the official adapter exists
|
||||
* and its effective reference is writable but unconfigured.
|
||||
* Prompt a first-run user to open Models while the official adapter exists
|
||||
* and its effective credential is not configured.
|
||||
* @param props - settings-shell owner state and Models feature dependencies.
|
||||
* @returns the controlled modal or null when onboarding needs no intervention.
|
||||
*/
|
||||
export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps): ReactNode {
|
||||
const { active, openSection, controller, useSnapshot, credentials, t } = props
|
||||
const { active, openSection, controller, useSnapshot, t } = props
|
||||
const state = useSnapshot(snapshot => snapshot)
|
||||
const readiness = deepSeekReadiness(state)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
const [keyDraft, setKeyDraft] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [failure, setFailure] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (active && !dismissed && state.status === 'idle') void controller.load()
|
||||
}, [active, controller, dismissed, state.status])
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || readiness.kind !== 'credential-missing') {
|
||||
setKeyDraft('')
|
||||
setFailure(undefined)
|
||||
}
|
||||
}, [active, readiness.kind, readiness.kind === 'credential-missing' ? readiness.ref : undefined])
|
||||
|
||||
const close = (): void => {
|
||||
setKeyDraft('')
|
||||
setFailure(undefined)
|
||||
setDismissed(true)
|
||||
}
|
||||
|
||||
@@ -73,42 +53,6 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
openSection('models')
|
||||
}
|
||||
|
||||
const save = async (): Promise<void> => {
|
||||
/* v8 ignore next -- the form only attaches save while missing and disables it for an empty draft */
|
||||
if (readiness.kind !== 'credential-missing' || keyDraft.length === 0) return
|
||||
const secret = keyDraft
|
||||
const ref = readiness.ref
|
||||
setBusy(true)
|
||||
setFailure(undefined)
|
||||
try {
|
||||
const response = await credentials.set({ ref, value: secret })
|
||||
if (!response.result.ok) {
|
||||
setFailure(`${t('onboardingSaveFailed')}: ${redactSecret(response.result.error.message, secret)}`)
|
||||
return
|
||||
}
|
||||
await controller.load()
|
||||
if (deepSeekReadiness(controller.store.getSnapshot()).kind !== 'configured') {
|
||||
setFailure(t('onboardingVerifyFailed'))
|
||||
return
|
||||
}
|
||||
setKeyDraft('')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setFailure(`${t('onboardingSaveFailed')}: ${redactSecret(message, secret)}`)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const retry = async (): Promise<void> => {
|
||||
setBusy(true)
|
||||
try {
|
||||
await controller.load()
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!active || dismissed || readiness.kind === 'loading'
|
||||
|| readiness.kind === 'adapter-absent' || readiness.kind === 'configured') return null
|
||||
|
||||
@@ -116,9 +60,6 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
const diagnostic = unavailable && readiness.reason === 'credentials-unavailable'
|
||||
? t('onboardingCredentialsUnavailable')
|
||||
: t('onboardingConfigurationUnavailable')
|
||||
const displayName = readiness.kind === 'credential-missing'
|
||||
? readiness.displayName
|
||||
: 'DeepSeek'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -132,55 +73,13 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
<Button
|
||||
variant="primary"
|
||||
className={styles['primary']}
|
||||
disabled={busy || (!unavailable && keyDraft.length === 0)}
|
||||
onClick={() => { void (unavailable ? retry() : save()) }}
|
||||
onClick={openModels}
|
||||
>
|
||||
{busy
|
||||
? t('onboardingSaving')
|
||||
: unavailable
|
||||
? t('retry')
|
||||
: t('onboardingSave')}
|
||||
{t('onboardingGoToSettings')}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<div className={styles['fields']}>
|
||||
<label className={styles['field']}>
|
||||
<span className={styles['label']}>{t('provider')}</span>
|
||||
<Input
|
||||
className={styles['input'] as string}
|
||||
type="text"
|
||||
aria-label={t('provider')}
|
||||
value={displayName}
|
||||
readOnly
|
||||
/>
|
||||
</label>
|
||||
{readiness.kind === 'credential-missing'
|
||||
? (
|
||||
<label className={styles['field']}>
|
||||
<span className={styles['label']}>{t('onboardingKey')}</span>
|
||||
<Input
|
||||
className={styles['input'] as string}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
aria-label={t('onboardingKey')}
|
||||
placeholder={t('onboardingKeyPlaceholder')}
|
||||
value={keyDraft}
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
setKeyDraft(event.target.value)
|
||||
setFailure(undefined)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
: <p className={styles['diagnostic']}>{diagnostic}</p>}
|
||||
<Button variant="ghost" size="sm" className={styles['advanced']} onClick={openModels}>
|
||||
{t('onboardingAdvanced')}
|
||||
</Button>
|
||||
{failure !== undefined ? <p className={styles['error']} role="alert">{failure}</p> : null}
|
||||
</div>
|
||||
{unavailable ? <p className={styles['diagnostic']}>{diagnostic}</p> : undefined}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Models settings plugin, browser half. Registers the `models` nav entry and
|
||||
* official-DeepSeek first-run overlay into shell-declared slots. Both consume
|
||||
* one provider/settings/credential join; the full page edits through the
|
||||
* schema-driven form while onboarding exposes only write-only credential
|
||||
* setup. Export discipline: packages/client/AGENTS.md.
|
||||
* one provider/settings/credential join; the overlay routes missing-key users
|
||||
* to the full page's single credential editor. 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'
|
||||
@@ -68,7 +68,6 @@ export function apply(ctx: ClientContext): void {
|
||||
const onboardingInjected = (): DeepSeekOnboardingInjected => ({
|
||||
controller,
|
||||
useSnapshot,
|
||||
credentials: connection.api.credentials,
|
||||
t,
|
||||
})
|
||||
|
||||
|
||||
@@ -27,16 +27,10 @@ export const en = {
|
||||
effort: 'Reasoning effort',
|
||||
effortInherit: 'Default',
|
||||
advancedHint: 'Other fields live in settings.yaml; edit that section directly.',
|
||||
onboardingTitle: 'Add a DeepSeek API key',
|
||||
onboardingTitle: 'Add an API key to get started',
|
||||
onboardingDescription: 'Configure the official DeepSeek provider to start building.',
|
||||
onboardingKey: 'API key',
|
||||
onboardingKeyPlaceholder: 'Enter your DeepSeek API key',
|
||||
onboardingAdvanced: 'Advanced model settings',
|
||||
onboardingSave: 'Save and continue',
|
||||
onboardingSaving: 'Saving…',
|
||||
onboardingGoToSettings: 'Go to settings',
|
||||
onboardingLater: 'Configure later',
|
||||
onboardingSaveFailed: 'Could not save the API key',
|
||||
onboardingVerifyFailed: 'The key was saved, but its configured state could not be verified. Try again.',
|
||||
onboardingUnavailableTitle: 'DeepSeek setup is unavailable',
|
||||
onboardingCredentialsUnavailable: 'This deployment does not expose writable credential storage. Mount @deepseek-ai/dsh-credentials-local, then retry.',
|
||||
onboardingConfigurationUnavailable: 'The live DeepSeek configuration capability cannot be resolved here. Check the deployment composition, then retry.',
|
||||
@@ -69,16 +63,10 @@ export const zh: typeof en = {
|
||||
effort: '推理强度',
|
||||
effortInherit: '默认',
|
||||
advancedHint: '其余字段在 settings.yaml 中,请直接编辑对应段。',
|
||||
onboardingTitle: '添加 DeepSeek API 密钥',
|
||||
onboardingTitle: '添加一个 API Key 开始使用',
|
||||
onboardingDescription: '配置 DeepSeek 官方模型,即可开始使用。',
|
||||
onboardingKey: 'API 密钥',
|
||||
onboardingKeyPlaceholder: '输入 DeepSeek API 密钥',
|
||||
onboardingAdvanced: '模型高级设置',
|
||||
onboardingSave: '保存并继续',
|
||||
onboardingSaving: '保存中…',
|
||||
onboardingGoToSettings: '前往配置',
|
||||
onboardingLater: '稍后配置',
|
||||
onboardingSaveFailed: '无法保存 API 密钥',
|
||||
onboardingVerifyFailed: '密钥已写入,但无法确认配置状态。请重试。',
|
||||
onboardingUnavailableTitle: '无法在此配置 DeepSeek',
|
||||
onboardingCredentialsUnavailable: '当前部署没有可写的凭据存储。请挂载 @deepseek-ai/dsh-credentials-local 后重试。',
|
||||
onboardingConfigurationUnavailable: '无法在此解析 DeepSeek 的实时配置能力。请检查部署组合后重试。',
|
||||
|
||||
@@ -181,7 +181,7 @@ export type DeepSeekReadiness =
|
||||
| { kind: 'loading' }
|
||||
| { kind: 'adapter-absent' }
|
||||
| { kind: 'configured'; source: 'literal' | 'credential'; ref?: string; credential?: CredentialView }
|
||||
| { kind: 'credential-missing'; displayName: string; ref: string }
|
||||
| { kind: 'credential-missing' }
|
||||
| {
|
||||
kind: 'unavailable'
|
||||
reason:
|
||||
@@ -196,7 +196,7 @@ export type DeepSeekReadiness =
|
||||
/**
|
||||
* Project official-DeepSeek readiness from the provider/settings/credential
|
||||
* join used by the Models page. A missing directory entry means the adapter
|
||||
* is not mounted and therefore cannot be repaired by a key form.
|
||||
* is not mounted and therefore cannot be repaired by navigating to Models.
|
||||
* @param state - current shared Models join snapshot.
|
||||
* @returns the onboarding state without reading a parallel fact source.
|
||||
*/
|
||||
@@ -264,9 +264,5 @@ export function deepSeekReadiness(state: ModelsSettingsState): DeepSeekReadiness
|
||||
message: `credential reference "${row.apiKeyEnv}" is missing and read-only`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'credential-missing',
|
||||
displayName: row.entry.displayName,
|
||||
ref: row.apiKeyEnv,
|
||||
}
|
||||
return { kind: 'credential-missing' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user