Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models

# Conflicts:
#	apps/web/tests/onboarding-deepseek-config.e2e.ts
#	packages/client/ui-models/README.i18n.yaml
#	packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-31 15:12:41 +08:00
88 files changed
+2526 -203

No files matched your search

@@ -1,7 +1,99 @@
.dialog {
width: min(420px, 100%);
.page {
position: relative;
z-index: 1;
width: min(640px, calc(100vw - 64px));
max-height: 100vh;
padding: clamp(104px, 18vh, 156px) 0 40px;
box-sizing: border-box;
overflow-y: auto;
color: var(--dsw-alias-label-primary);
}
.brand {
display: flex;
align-items: center;
margin-bottom: 42px;
color: var(--dsw-alias-label-primary);
}
.title {
margin: 0;
font-size: 28px;
line-height: 36px;
font-weight: 600;
letter-spacing: -0.02em;
outline: none;
}
.description {
margin: 16px 0 0;
font-size: 16px;
line-height: 28px;
color: var(--dsw-alias-label-secondary);
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
}
.primary {
width: 100%;
min-width: 132px;
}
.brand,
.title,
.description,
.actions {
animation: credential-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
.title { animation-delay: 40ms; }
.description { animation-delay: 80ms; }
.actions { animation-delay: 120ms; }
@keyframes credential-enter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.brand,
.title,
.description,
.actions {
animation: none;
}
}
@media (max-width: 560px) {
.page {
width: calc(100vw - 40px);
padding-top: 64px;
}
.brand {
margin-bottom: 30px;
}
.actions {
align-items: stretch;
flex-direction: column-reverse;
margin-top: 32px;
}
.primary,
.later {
width: 100%;
}
}
@@ -1,13 +1,13 @@
/**
* Official-DeepSeek first-run dialog. Readiness comes from the same
* Official-DeepSeek first-run step. Readiness comes from the same
* 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 { useEffect, useRef } from 'react'
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 { BrandWordmark, Button } 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'
@@ -37,29 +37,35 @@ function assertNever(_value: never): never {
* 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.
* @returns the onboarding page or null when onboarding needs no intervention.
*/
export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps): ReactNode {
const { active, openSection, controller, useSnapshot, t } = props
const { complete, openSection, controller, useSnapshot, t } = props
const state = useSnapshot(snapshot => snapshot)
const readiness = deepSeekReadiness(state)
const [dismissed, setDismissed] = useState(false)
const titleRef = useRef<HTMLHeadingElement | null>(null)
useEffect(() => {
if (active && !dismissed && state.status === 'idle') void controller.load()
}, [active, controller, dismissed, state.status])
if (state.status === 'idle') void controller.load()
}, [controller, state.status])
const close = (): void => {
setDismissed(true)
}
useEffect(() => {
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 => {
close()
complete()
openSection('models')
}
if (!active || dismissed) return null
switch (readiness.kind) {
case 'loading':
case 'adapter-absent':
@@ -74,23 +80,25 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
}
return (
<Modal
open
onClose={close}
title={t('onboardingTitle')}
closeLabel={t('onboardingLater')}
description={t('onboardingDescription')}
className={styles['dialog'] as string}
footer={(
<Button
variant="primary"
className={styles['primary']}
autoFocus
onClick={openModels}
>
<section className={styles['page']} role="region" aria-labelledby="deepseek-onboarding-title">
<div className={styles['brand']} aria-hidden="true"><BrandWordmark size={24} /></div>
<h2
ref={titleRef}
id="deepseek-onboarding-title"
className={styles['title']}
tabIndex={-1}
>
{t('onboardingTitle')}
</h2>
<p className={styles['description']}>{t('onboardingDescription')}</p>
<div className={styles['actions']}>
<Button variant="ghost" className={styles['later']} onClick={complete}>
{t('onboardingLater')}
</Button>
<Button variant="primary" className={styles['primary']} onClick={openModels}>
{t('onboardingGoToSettings')}
</Button>
)}
/>
</div>
</section>
)
}