fix(web): address onboarding review feedback
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 配置。请检查部署组合。',
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
|
||||
Reference in New Issue
Block a user