feat(web): present onboarding as a continuous page

This commit is contained in:
NI0317
2026-07-31 00:28:17 +08:00
parent 04f8b30db1
commit 75a0366a52
25 changed files with 475 additions and 228 deletions

View File

@@ -1,137 +1,161 @@
.overlay {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
padding-top: 80px;
box-sizing: border-box;
}
/* Mask */
.mask {
position: absolute;
left: 0px;
right: 0px;
top: 80px;
bottom: 0px;
background: rgba(0, 0, 0, 0.24);
/* Mask-blur */
backdrop-filter: blur(2px);
}
.dialog {
.page {
position: relative;
z-index: 1;
width: min(600px, calc(100vw - 48px));
max-height: calc(100vh - 128px);
padding: 32px;
width: min(640px, calc(100vw - 64px));
max-height: 100vh;
padding: clamp(64px, 9vh, 104px) 0 40px;
box-sizing: border-box;
overflow-y: auto;
border-radius: 24px;
background: var(--dsw-alias-bg-layer-2);
box-shadow: var(--dsw-shadow-lv3);
color: var(--dsw-alias-label-primary);
--welcome-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
.brand {
display: flex;
align-items: center;
margin-bottom: 42px;
color: var(--dsw-alias-label-primary);
}
.title {
margin: 0;
font-size: 20px;
line-height: 30px;
font-size: 28px;
line-height: 36px;
font-weight: 600;
letter-spacing: -0.01em;
letter-spacing: -0.02em;
outline: none;
}
.lead,
.closing,
.quote,
.feedback p,
.opening,
.status,
.reflection,
.feedback,
.error {
margin: 0;
}
.lead {
margin-top: 12px;
font-size: 16px;
line-height: 25px;
color: var(--dsw-alias-label-secondary);
.opening {
margin-top: 30px;
}
.status {
margin-top: 18px;
}
.reflection {
margin-top: 36px;
padding: 0;
}
.feedback {
margin-top: 20px;
padding: 16px 18px;
border-radius: 14px;
border: 1px solid var(--dsw-alias-border-l1);
background: var(--dsw-alias-bg-module-platform);
font-size: 15px;
line-height: 24px;
margin-top: 30px;
}
.opening,
.status,
.reflection,
.feedback {
font-size: 16px;
line-height: 28px;
color: var(--dsw-alias-label-secondary);
}
.feedback strong {
display: block;
margin-bottom: 4px;
font-weight: 600;
}
.feedback p,
.closing {
color: var(--dsw-alias-label-secondary);
}
.closing {
margin-top: 16px;
font-size: 15px;
line-height: 24px;
}
.quote {
font-size: 14px;
line-height: 22px;
color: var(--dsw-alias-label-secondary);
color: inherit;
font-weight: 500;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--dsw-alias-border-l1);
justify-content: flex-end;
margin-top: 32px;
}
.error {
margin-top: 14px;
font-size: 13px;
line-height: 20px;
margin-top: 20px;
font-size: 14px;
line-height: 22px;
color: var(--dsw-alias-state-error-primary);
}
.primary {
min-width: 104px;
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
min-width: 120px;
transition: transform 140ms var(--welcome-ease-out);
}
.primary:active:not(:disabled) {
transform: scale(0.97);
}
.brand,
.title,
.opening,
.status,
.reflection,
.feedback,
.footer {
animation: welcome-enter 280ms var(--welcome-ease-out) both;
}
.title { animation-delay: 40ms; }
.opening { animation-delay: 80ms; }
.status { animation-delay: 120ms; }
.reflection { animation-delay: 160ms; }
.feedback { animation-delay: 200ms; }
.footer { animation-delay: 240ms; }
@keyframes welcome-enter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.brand,
.title,
.opening,
.status,
.reflection,
.feedback,
.footer {
animation: none;
}
.primary {
transition: none;
}
}
@media (max-width: 560px) {
.dialog {
padding: 24px;
.page {
width: calc(100vw - 40px);
padding-top: 38px;
}
.brand {
margin-bottom: 30px;
}
.opening {
margin-top: 24px;
}
.reflection {
margin-top: 28px;
}
.feedback {
margin-top: 28px;
}
.footer {
align-items: stretch;
flex-direction: column;
gap: 14px;
margin-top: 30px;
}
.primary {

View File

@@ -3,11 +3,24 @@
import { useCallback, useEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { Button } 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 { WelcomeNoticeState, WelcomeNoticeStore } from './welcome-store.ts'
import css from './WelcomeNotice.module.css'
function emphasizedFeedback(paragraph: string, emphasis: string): ReactNode {
const index = paragraph.indexOf(emphasis)
/* v8 ignore next -- both locale values derive from one owner object that contains the emphasis */
if (index < 0) return paragraph
return (
<>
{paragraph.slice(0, index)}
<strong>{emphasis}</strong>
{paragraph.slice(index + emphasis.length)}
</>
)
}
/** Registrant-owned dependencies of {@link WelcomeNotice}. */
export interface WelcomeNoticeInjected {
controller: WelcomeNoticeStore
@@ -23,6 +36,7 @@ export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
const { complete, controller, useSnapshot, t } = props
const state = useSnapshot(snapshot => snapshot)
const finished = useRef(false)
const titleRef = useRef<HTMLHeadingElement | null>(null)
const finish = useCallback((): void => {
if (finished.current) return
finished.current = true
@@ -37,6 +51,10 @@ export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
if (state.acknowledged) finish()
}, [finish, state.acknowledged])
useEffect(() => {
if (state.status === 'ready' && !state.acknowledged) titleRef.current?.focus()
}, [state.acknowledged, state.status])
if (state.status === 'idle' || state.status === 'loading' || state.acknowledged) return null
const acknowledge = async (): Promise<void> => {
@@ -44,30 +62,26 @@ export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
}
return (
<div className={css.overlay} role="presentation">
<div className={css.mask} aria-hidden="true" />
<section className={css.dialog} role="dialog" aria-modal="true" aria-labelledby="welcome-notice-title">
<h2 id="welcome-notice-title" className={css.title}>{t('welcome.title')}</h2>
<p className={css.lead}>{t('welcome.lead')}</p>
<div className={css.feedback}>
<strong>{t('welcome.feedbackTitle')}</strong>
<p>{t('welcome.feedbackBody')}</p>
</div>
<p className={css.closing}>{t('welcome.closing')}</p>
{state.error === null ? null : <p className={css.error} role="alert">{t('welcome.error')}</p>}
<div className={css.footer}>
<p className={css.quote}>{t('welcome.quote')}</p>
<Button
variant="primary"
className={css.primary}
autoFocus
disabled={state.status === 'saving'}
onClick={() => { void acknowledge() }}
>
{t('welcome.continue')}
</Button>
</div>
</section>
</div>
<section className={css.page} role="region" aria-labelledby="welcome-notice-title">
<div className={css.brand} aria-hidden="true"><BrandWordmark size={24} /></div>
<h2 ref={titleRef} id="welcome-notice-title" className={css.title} tabIndex={-1}>{t('welcome.title')}</h2>
<p className={css.opening}>{t('welcome.paragraph.0')}</p>
<p className={css.status}>{t('welcome.paragraph.1')}</p>
<blockquote className={css.reflection}>{t('welcome.paragraph.2')}</blockquote>
<p className={css.feedback}>
{emphasizedFeedback(t('welcome.paragraph.3'), t('welcome.feedbackEmphasis'))}
</p>
{state.error === null ? null : <p className={css.error} role="alert">{t('welcome.error')}</p>}
<div className={css.footer}>
<Button
variant="primary"
className={css.primary}
disabled={state.status === 'saving'}
onClick={() => { void acknowledge() }}
>
{t('welcome.continue')}
</Button>
</div>
</section>
)
}

View File

@@ -27,11 +27,11 @@ export const zh: LocaleDict = {
'permission.desc': '选择默认权限模式',
'toolcall.title': '工具调用',
'welcome.title': WELCOME_NOTICE_COPY.zh.title,
'welcome.lead': WELCOME_NOTICE_COPY.zh.lead,
'welcome.feedbackTitle': WELCOME_NOTICE_COPY.zh.feedbackTitle,
'welcome.feedbackBody': WELCOME_NOTICE_COPY.zh.feedbackBody,
'welcome.closing': WELCOME_NOTICE_COPY.zh.closing,
'welcome.quote': WELCOME_NOTICE_COPY.zh.quote,
'welcome.paragraph.0': WELCOME_NOTICE_COPY.zh.paragraphs[0],
'welcome.paragraph.1': WELCOME_NOTICE_COPY.zh.paragraphs[1],
'welcome.paragraph.2': WELCOME_NOTICE_COPY.zh.paragraphs[2],
'welcome.paragraph.3': WELCOME_NOTICE_COPY.zh.paragraphs[3],
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.zh.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.zh.continueLabel,
'welcome.error': '暂时无法保存确认状态,请重试。',
}
@@ -47,11 +47,11 @@ export const en: LocaleDict = {
'permission.desc': 'Choose default permission mode',
'toolcall.title': 'Tool Call',
'welcome.title': WELCOME_NOTICE_COPY.en.title,
'welcome.lead': WELCOME_NOTICE_COPY.en.lead,
'welcome.feedbackTitle': WELCOME_NOTICE_COPY.en.feedbackTitle,
'welcome.feedbackBody': WELCOME_NOTICE_COPY.en.feedbackBody,
'welcome.closing': WELCOME_NOTICE_COPY.en.closing,
'welcome.quote': WELCOME_NOTICE_COPY.en.quote,
'welcome.paragraph.0': WELCOME_NOTICE_COPY.en.paragraphs[0],
'welcome.paragraph.1': WELCOME_NOTICE_COPY.en.paragraphs[1],
'welcome.paragraph.2': WELCOME_NOTICE_COPY.en.paragraphs[2],
'welcome.paragraph.3': WELCOME_NOTICE_COPY.en.paragraphs[3],
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.en.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.en.continueLabel,
'welcome.error': 'The acknowledgement could not be saved. Please try again.',
}

View File

@@ -8,26 +8,30 @@ export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
* Bump only when the notice changes materially and every user should see it
* again. The acknowledgement is compared for exact equality.
*/
export const WELCOME_NOTICE_VERSION = '2026-07-30.2'
export const WELCOME_NOTICE_VERSION = '2026-07-30.3'
/** The complete editable welcome notice in both supported GUI locales. */
export const WELCOME_NOTICE_COPY = {
zh: {
title: '内测声明',
lead: '感谢您试用 DeepSeek Harness。目前仍处于内部测试阶段部分功能与体验还在持续打磨。',
feedbackTitle: '我们最想听见:失败、困惑和不顺手',
feedbackBody: '如果它没帮到您,甚至给工作添了麻烦,请在企业微信群告诉我们。',
closing: '真实使用中的每一个问题,可能促使我们重新审视,甚至推翻已有设计。',
quote: '“如切如磋,如琢如磨。”',
paragraphs: [
'感谢您愿意拨冗试用 DeepSeek Harness。',
'目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。',
'“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,可能促使我们重新审视,甚至推翻已有设计。',
'我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。',
],
feedbackEmphasis: '如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中留言',
continueLabel: '继续',
},
en: {
title: 'Internal Testing Notice',
lead: 'Thank you for trying DeepSeek Harness. This version is still in internal testing, and some features and experiences remain under refinement.',
feedbackTitle: 'What we most want to hear: failures, confusion, and friction',
feedbackBody: 'If it did not help—or even made your work harder—please tell us in the company WeChat group.',
closing: 'Every problem found in real use may prompt us to reconsider, or even overturn, an existing design.',
quote: '“As one cuts and files, as one chisels and polishes.”',
paragraphs: [
'Thank you for taking the time to try DeepSeek Harness.',
'This version is still in internal testing. Some features remain unfinished, and parts of the experience may feel rough.',
'“As one cuts and files, as one chisels and polishes.” A product grows through real encounters and candid feedback. Problems you uncover in real use may prompt us to reconsideror even overturn—our existing designs.',
'We especially want to hear about failures, confusion, and friction. If it did not help you, or even made your work harder, please leave a message in the company WeChat group and tell us about your experience. Every piece of feedback helps us refine it.',
],
feedbackEmphasis: 'If it did not help you, or even made your work harder, please leave a message in the company WeChat group',
continueLabel: 'Continue',
},
} as const