fix(web): address onboarding review feedback

This commit is contained in:
Yichen Jiang
2026-07-30 18:56:56 +08:00
parent 234018032d
commit fc8f992cde
12 changed files with 168 additions and 87 deletions

View File

@@ -9,7 +9,7 @@ import type { ReactNode } from 'react'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
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 type { DeepSeekReadiness, ModelsSettingsState, ModelsSettingsStore } from './store.ts'
import { deepSeekReadiness } from './store.ts'
import type { en } from './locales.ts'
import styles from './DeepSeekOnboardingDialog.module.css'
@@ -28,6 +28,35 @@ export interface DeepSeekOnboardingInjected {
export type DeepSeekOnboardingDialogProps =
PropsRuntime<'settings.onboarding'> & DeepSeekOnboardingInjected
type UnavailableReason = Extract<DeepSeekReadiness, { kind: 'unavailable' }>['reason']
/* v8 ignore next 3 -- closed-union defaults only defend future source widening */
function assertNever(_value: never): never {
throw new Error('unexpected DeepSeek onboarding state')
}
function unavailableDiagnostic(
reason: UnavailableReason,
t: DeepSeekOnboardingInjected['t'],
): string {
switch (reason) {
case 'load-failed':
return t('onboardingLoadFailed')
case 'credentials-unavailable':
return t('onboardingCredentialsUnavailable')
case 'settings-read-only':
case 'credential-read-only':
return t('onboardingReadOnly')
case 'provider-inactive':
case 'settings-unavailable':
case 'credential-ref-unavailable':
return t('onboardingConfigurationUnavailable')
/* v8 ignore next -- every current unavailable reason is handled above */
default:
return assertNever(reason)
}
}
/**
* Prompt a first-run user to open Models while the official adapter exists
* and its effective credential is not configured.
@@ -53,13 +82,28 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
openSection('models')
}
if (!active || dismissed || readiness.kind === 'loading'
|| readiness.kind === 'adapter-absent' || readiness.kind === 'configured') return null
if (!active || dismissed) return null
const unavailable = readiness.kind === 'unavailable'
const diagnostic = unavailable && readiness.reason === 'credentials-unavailable'
? t('onboardingCredentialsUnavailable')
: t('onboardingConfigurationUnavailable')
let unavailableReason: UnavailableReason | undefined
switch (readiness.kind) {
case 'loading':
case 'adapter-absent':
case 'configured':
return null
case 'credential-missing':
unavailableReason = undefined
break
case 'unavailable':
unavailableReason = readiness.reason
break
/* v8 ignore next -- every current readiness variant is handled above */
default:
return assertNever(readiness)
}
const unavailable = unavailableReason !== undefined
const diagnostic = unavailableReason === undefined
? undefined
: unavailableDiagnostic(unavailableReason, t)
return (
<Modal
@@ -73,13 +117,14 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
<Button
variant="primary"
className={styles['primary']}
autoFocus
onClick={openModels}
>
{t('onboardingGoToSettings')}
</Button>
)}
>
{unavailable ? <p className={styles['diagnostic']}>{diagnostic}</p> : undefined}
{diagnostic === undefined ? undefined : <p className={styles['diagnostic']}>{diagnostic}</p>}
</Modal>
)
}

View File

@@ -32,8 +32,10 @@ export const en = {
onboardingGoToSettings: 'Go to settings',
onboardingLater: 'Configure later',
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.',
onboardingLoadFailed: 'DeepSeek configuration could not be loaded. Check the connection and try again in Models.',
onboardingCredentialsUnavailable: 'Credential storage is unavailable in this deployment. Check the deployment configuration.',
onboardingReadOnly: 'This deployment does not allow the DeepSeek API key to be changed here. Ask an administrator to provide the credential.',
onboardingConfigurationUnavailable: 'DeepSeek configuration is unavailable in this deployment. Check the deployment composition.',
}
/** Chinese strings (same keys as {@link en}). */
@@ -68,6 +70,8 @@ export const zh: typeof en = {
onboardingGoToSettings: '前往配置',
onboardingLater: '稍后配置',
onboardingUnavailableTitle: '无法在此配置 DeepSeek',
onboardingCredentialsUnavailable: '当前部署没有可写的凭据存储。请挂载 @deepseek-ai/dsh-credentials-local 后重试。',
onboardingConfigurationUnavailable: '无法在此解析 DeepSeek 的实时配置能力。请检查部署组合后重试。',
onboardingLoadFailed: '无法加载 DeepSeek 配置。请检查连接,然后在模型设置中重试。',
onboardingCredentialsUnavailable: '当前部署无法使用凭据存储。请检查部署配置。',
onboardingReadOnly: '当前部署不允许在此修改 DeepSeek API 密钥。请联系管理员提供凭据。',
onboardingConfigurationUnavailable: '当前部署无法使用 DeepSeek 配置。请检查部署组合。',
}

View File

@@ -180,17 +180,18 @@ export class ModelsSettingsStore {
export type DeepSeekReadiness =
| { kind: 'loading' }
| { kind: 'adapter-absent' }
| { kind: 'configured'; source: 'literal' | 'credential'; ref?: string; credential?: CredentialView }
| { kind: 'configured' }
| { kind: 'credential-missing' }
| {
kind: 'unavailable'
reason:
| 'load-failed'
| 'provider-inactive'
| 'settings-unavailable'
| 'credential-ref-unavailable'
| 'credentials-unavailable'
| 'settings-read-only'
| 'credential-read-only'
message: string
}
/**
@@ -207,8 +208,7 @@ export function deepSeekReadiness(state: ModelsSettingsState): DeepSeekReadiness
if (state.status === 'error') {
return {
kind: 'unavailable',
reason: 'settings-unavailable',
message: state.error ?? 'provider/settings describe failed',
reason: 'load-failed',
}
}
const row = state.rows.find(candidate => candidate.entry.provider === 'deepseek-official')
@@ -217,51 +217,46 @@ export function deepSeekReadiness(state: ModelsSettingsState): DeepSeekReadiness
return {
kind: 'unavailable',
reason: 'provider-inactive',
message: 'the deepseek-official route is not active',
}
}
if (!row.configured) {
return {
kind: 'unavailable',
reason: 'settings-unavailable',
message: `settings namespace "${row.entry.settingsNs}" did not resolve the provider profile`,
}
}
if (row.literalApiKeyConfigured) return { kind: 'configured', source: 'literal' }
if (row.literalApiKeyConfigured) return { kind: 'configured' }
if (row.apiKeyEnv === undefined) {
return {
kind: 'unavailable',
reason: 'credential-ref-unavailable',
message: 'the resolved DeepSeek settings do not name an apiKeyEnv credential reference',
}
}
if (state.credentialError !== null) {
return {
kind: 'unavailable',
reason: 'credentials-unavailable',
message: state.credentialError,
}
}
if (row.credential === undefined) {
return {
kind: 'unavailable',
reason: 'credentials-unavailable',
message: `credential reference "${row.apiKeyEnv}" was not described`,
}
}
if (row.credential.configured) {
return { kind: 'configured' }
}
if (!state.writable) {
return {
kind: 'configured',
source: 'credential',
ref: row.apiKeyEnv,
credential: row.credential,
kind: 'unavailable',
reason: 'settings-read-only',
}
}
if (!row.credential.writable) {
return {
kind: 'unavailable',
reason: 'credential-read-only',
message: `credential reference "${row.apiKeyEnv}" is missing and read-only`,
}
}
return { kind: 'credential-missing' }