Merge updated DeepSeek onboarding base

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md
#	packages/client/ui-models/README.i18n.yaml
#	packages/client/ui-models/README.md
#	packages/client/ui-models/README.zh.md
#	packages/client/ui-models/src/client/DeepSeekOnboardingDialog.module.css
#	packages/client/ui-models/src/client/DeepSeekOnboardingDialog.tsx
#	packages/client/ui-models/tests/onboarding-dialog.spec.tsx
This commit is contained in:
NI0317
2026-07-31 10:29:20 +08:00
90 changed files with 2496 additions and 346 deletions

View File

@@ -26,8 +26,7 @@
outline: none;
}
.description,
.diagnostic {
.description {
max-width: 600px;
margin: 22px 0 0;
font-size: 17px;
@@ -73,15 +72,13 @@
.brand,
.title,
.description,
.diagnostic,
.provider,
.actions {
animation: credential-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
.title { animation-delay: 40ms; }
.description,
.diagnostic { animation-delay: 80ms; }
.description { animation-delay: 80ms; }
.provider { animation-delay: 120ms; }
.actions { animation-delay: 160ms; }
@@ -101,7 +98,6 @@
.brand,
.title,
.description,
.diagnostic,
.provider,
.actions {
animation: none;
@@ -118,8 +114,7 @@
margin-bottom: 30px;
}
.description,
.diagnostic {
.description {
font-size: 16px;
line-height: 27px;
}

View File

@@ -9,7 +9,7 @@ import type { ReactNode } from 'react'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { BrandWordmark, Button } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import type { DeepSeekReadiness, ModelsSettingsState, ModelsSettingsStore } from './store.ts'
import type { ModelsSettingsState, ModelsSettingsStore } from './store.ts'
import { deepSeekReadiness } from './store.ts'
import type { en } from './locales.ts'
import styles from './DeepSeekOnboardingDialog.module.css'
@@ -28,35 +28,11 @@ 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.
@@ -74,42 +50,34 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
}, [controller, state.status])
useEffect(() => {
if (readiness.kind === 'adapter-absent' || readiness.kind === 'configured') complete()
if (
readiness.kind === 'adapter-absent'
|| readiness.kind === 'configured'
|| readiness.kind === 'unavailable'
) complete()
}, [complete, readiness.kind])
useEffect(() => {
if (readiness.kind === 'credential-missing') titleRef.current?.focus()
}, [readiness.kind])
const openModels = (): void => {
complete()
openSection('models')
}
useEffect(() => {
if (readiness.kind === 'credential-missing' || readiness.kind === 'unavailable') {
titleRef.current?.focus()
}
}, [readiness.kind])
let unavailableReason: UnavailableReason | undefined
switch (readiness.kind) {
case 'loading':
case 'adapter-absent':
case 'configured':
case 'unavailable':
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)
const title = unavailable ? t('onboardingUnavailableTitle') : t('onboardingTitle')
return (
<section className={styles['page']} role="region" aria-labelledby="deepseek-onboarding-title">
@@ -120,11 +88,9 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
className={styles['title']}
tabIndex={-1}
>
{title}
{t('onboardingTitle')}
</h2>
{unavailable
? <p className={styles['diagnostic']}>{diagnostic}</p>
: <p className={styles['description']}>{t('onboardingDescription')}</p>}
<p className={styles['description']}>{t('onboardingDescription')}</p>
<div className={styles['provider']}>
<span className={styles['providerName']}>DeepSeek</span>
<span className={styles['providerRoute']}>deepseek-official</span>
@@ -133,11 +99,7 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
<Button variant="ghost" className={styles['later']} onClick={complete}>
{t('onboardingLater')}
</Button>
<Button
variant="primary"
className={styles['primary']}
onClick={openModels}
>
<Button variant="primary" className={styles['primary']} onClick={openModels}>
{t('onboardingGoToSettings')}
</Button>
</div>

View File

@@ -32,11 +32,6 @@ export const en = {
onboardingDescription: 'Configure the official DeepSeek provider to start building.',
onboardingGoToSettings: 'Go to settings',
onboardingLater: 'Configure later',
onboardingUnavailableTitle: 'DeepSeek setup is unavailable',
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}). */
@@ -71,9 +66,4 @@ export const zh: typeof en = {
onboardingDescription: '配置 DeepSeek 官方模型,即可开始使用。',
onboardingGoToSettings: '前往配置',
onboardingLater: '稍后配置',
onboardingUnavailableTitle: '无法在此配置 DeepSeek',
onboardingLoadFailed: '无法加载 DeepSeek 配置。请检查连接,然后在模型设置中重试。',
onboardingCredentialsUnavailable: '当前部署无法使用凭据存储。请检查部署配置。',
onboardingReadOnly: '当前部署不允许在此修改 DeepSeek API 密钥。请联系管理员提供凭据。',
onboardingConfigurationUnavailable: '当前部署无法使用 DeepSeek 配置。请检查部署组合。',
}

View File

@@ -215,8 +215,8 @@ 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 navigating to Models.
* join used by the Models page. A missing official configurable-provider
* declaration means the adapter is not repairable by navigating to Models.
* @param state - current shared Models join snapshot.
* @returns the onboarding state without reading a parallel fact source.
*/
@@ -230,7 +230,10 @@ export function deepSeekReadiness(state: ModelsSettingsState): DeepSeekReadiness
reason: 'load-failed',
}
}
const row = state.rows.find(candidate => candidate.entry.provider === 'deepseek-official')
const row = state.rows.find(candidate =>
candidate.entry.provider === 'deepseek-official'
&& candidate.entry.settingsNs === 'llm-deepseek'
&& candidate.entry.settingsPath.length === 0)
if (row === undefined) return { kind: 'adapter-absent' }
if (!row.entry.active) {
return {