Preselect recommended question choices without stopping the timer
This commit is contained in:
1 parent
050944fc8e
commit
1c6c462633
5 files changed
+51
-12
No files matched your search
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-client-ui-user-questions/use-this-package:
|
||||
en: 636c8c2ba17dbdc3
|
||||
zh: 88f64832d26eb076
|
||||
en: 8d3815c19858ac85
|
||||
zh: 94fc905fa202d334
|
||||
/deepseek-ai-dsh-client-ui-user-questions/use-this-package/answering:
|
||||
en: cc19c065d0aeac1e
|
||||
zh: 91c8cbe08b438e7f
|
||||
|
||||
@@ -25,7 +25,7 @@ The Web client shows an agent's question beside the chat input. Users choose opt
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
When the agent asks a question, the attached card temporarily occupies the composer seat: answer each question, navigate with the pager, or skip it. Single-select choices advance immediately; Enter continues the flow and submits once every question is answered or skipped, while Shift+Enter breaks a line instead (during IME composition Enter only confirms the input candidate without advancing).
|
||||
When the agent asks a question, the attached card temporarily occupies the composer seat: answer each question, navigate with the pager, or skip it. A first choice marked “Recommended” starts selected, but remains an unsubmitted draft and does not pause a timed countdown. Clicking a single-select choice advances immediately. Enter on a focused option attempts to submit the batch without selecting that option; missing answers return to the first incomplete question. Enter in a text answer continues the flow, while Shift+Enter breaks a line instead (during IME composition Enter only confirms the input candidate without advancing).
|
||||
|
||||
### Answering
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Web 客户端在聊天输入框旁显示 agent(智能体)的提问。用户
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
当 agent 提问时,附着式卡片会暂时占用编辑器位置:回答每个问题、用翻页器导航,或跳过它。选择单选选项后会立即前进;Enter 继续流程,并在所有问题均已回答或跳过后提交,而 Shift+Enter 改为换行(IME 组合输入期间按 Enter 只会确认输入候选,不会前进)。
|
||||
当 agent 提问时,附着式卡片会暂时占用编辑器位置:回答每个问题、用翻页器导航,或跳过它。首个带推荐标记的选项会预先选中,但仍只是未提交的草稿,不会暂停计时倒计时。点击单选选项会立即前进。聚焦选项时按 Enter 会尝试提交整组答案,不会选中该选项;若有缺失答案,则回到第一道未完成的问题。文本答案中的 Enter 会继续流程,Shift+Enter 则换行(IME 组合输入期间按 Enter 只会确认输入候选,不会前进)。
|
||||
|
||||
### 作答
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ export function parseRecommendedLabel(label: string): { label: string; recommend
|
||||
: { label, recommended: false }
|
||||
}
|
||||
|
||||
/** Only a marked first choice is an implicit draft; the user still submits it. */
|
||||
function recommendedFirstOption(question: PendingQuestion['questions'][number]): string | undefined {
|
||||
const label = question.options?.[0]?.label
|
||||
return label !== undefined && parseRecommendedLabel(label).recommended ? label : undefined
|
||||
}
|
||||
|
||||
/** Accept persisted progress only when it still describes this question batch. */
|
||||
function isQuestionDraftProgress(value: unknown, questionCount: number): value is QuestionDraftProgress {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
||||
@@ -165,8 +171,9 @@ function QuestionFlow({ pending, t, useStore, useQuestionCard, actions }: Questi
|
||||
return questions.map((item) => {
|
||||
const answer = recorded.get(item.id)
|
||||
const custom = answer?.custom ?? ''
|
||||
const recommended = review === undefined ? recommendedFirstOption(item) : undefined
|
||||
return {
|
||||
selected: [...answer?.selected ?? []],
|
||||
selected: answer === undefined && recommended !== undefined ? [recommended] : [...answer?.selected ?? []],
|
||||
custom,
|
||||
// A recorded answer with no selection and no custom text was skipped.
|
||||
skipped: answer !== undefined && answer.selected.length === 0 && custom === '',
|
||||
@@ -181,7 +188,10 @@ function QuestionFlow({ pending, t, useStore, useQuestionCard, actions }: Questi
|
||||
const index = validStored?.index ?? 0
|
||||
const drafts = storedProgress?.drafts ?? initialDrafts
|
||||
const restoredWait = storedProgress?.wait
|
||||
?? (storedProgress?.drafts.some(item => item.selected.length > 0 || item.custom !== '' || item.skipped) === true
|
||||
?? (storedProgress?.drafts.some((item, itemIndex) =>
|
||||
item.selected.length !== initialDrafts[itemIndex]?.selected.length
|
||||
|| item.selected.some((label, labelIndex) => label !== initialDrafts[itemIndex]?.selected[labelIndex])
|
||||
|| item.custom !== '' || item.skipped) === true
|
||||
? 'editing' : undefined)
|
||||
const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null)
|
||||
const [error, setError] = useState<Feedback | null>(null)
|
||||
@@ -499,7 +509,7 @@ function QuestionFlow({ pending, t, useStore, useQuestionCard, actions }: Questi
|
||||
disabled={locked}
|
||||
onClick={() => { choose(option.label) }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' || !drafts.every(completed)) return
|
||||
if (event.key !== 'Enter') return
|
||||
event.preventDefault()
|
||||
submitDrafts(drafts)
|
||||
}}
|
||||
|
||||
@@ -182,9 +182,9 @@ const QUESTIONS: PendingQuestion['questions'] = [
|
||||
]
|
||||
|
||||
/** Pending waterfall fixture with observable Client response methods. */
|
||||
function wait(questions: PendingQuestion['questions'] = QUESTIONS) {
|
||||
function wait(questions: PendingQuestion['questions'] = QUESTIONS, deadline?: number) {
|
||||
const carrier = new PendingQuestion(SID, questions)
|
||||
const request = createWaterfallRequest(undefined, undefined, (channel) => { carrier.detachWaterfall(channel) })
|
||||
const request = createWaterfallRequest(deadline, undefined, (channel) => { carrier.detachWaterfall(channel) })
|
||||
carrier.attachWaterfall(request.channel)
|
||||
const answer = vi.spyOn(carrier, 'answer')
|
||||
const dismiss = vi.spyOn(carrier, 'dismiss')
|
||||
@@ -212,6 +212,35 @@ describe('QuestionComposer', () => {
|
||||
expect(screen.getByRole('button', { name: '放弃整组问题' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps a recommended default selected without pausing a timed wait', () => {
|
||||
const { carrier } = wait(QUESTIONS, Date.now() + 30_000)
|
||||
try {
|
||||
const view = render(<QuestionComposer matched={carrier} {...kit} />)
|
||||
expect(screen.getByRole('radio', { name: /工程落地型/ }).getAttribute('aria-checked')).toBe('true')
|
||||
expect(carrier.snapshot().countdown?.running).toBe(true)
|
||||
|
||||
fireEvent.click(screen.getByLabelText('下一题'))
|
||||
view.unmount()
|
||||
render(<QuestionComposer matched={carrier} {...kit} />)
|
||||
expect(carrier.snapshot().countdown?.running).toBe(true)
|
||||
expect(screen.getByText('2 / 3')).toBeTruthy()
|
||||
} finally {
|
||||
carrier.timeout()
|
||||
}
|
||||
})
|
||||
|
||||
it('treats Enter on an unselected option as submit, without selecting it', () => {
|
||||
const { carrier, answer } = wait([{
|
||||
id: 'mode', question: 'Which mode?', options: [{ label: 'Alpha' }, { label: 'Beta' }],
|
||||
}])
|
||||
render(<QuestionComposer matched={carrier} {...kit} />)
|
||||
const option = screen.getByRole('radio', { name: 'Alpha' })
|
||||
expect(fireEvent.keyDown(option, { key: 'Enter' })).toBe(false)
|
||||
expect(option.getAttribute('aria-checked')).toBe('false')
|
||||
expect(screen.getByRole('status').textContent).toBe('请先完成这道问题。')
|
||||
expect(answer).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('collects single, custom, and multi-select answers before one batch submit', () => {
|
||||
const { carrier, answer } = wait()
|
||||
render(<QuestionComposer matched={carrier} {...kit} />)
|
||||
@@ -220,13 +249,12 @@ describe('QuestionComposer', () => {
|
||||
expect(screen.getByText('1 / 3')).toBeTruthy()
|
||||
expect(screen.getByText('推荐')).toBeTruthy()
|
||||
expect(screen.getByText('工程落地型')).toBeTruthy()
|
||||
expect(screen.getByRole('radio', { name: /工程落地型/ }).getAttribute('aria-checked')).toBe('true')
|
||||
const detail = screen.getByText('按当前空缺岗位的优先级选择。')
|
||||
const scrollRegion = detail.closest('[data-question-scroll]')
|
||||
expect(scrollRegion).toBeTruthy()
|
||||
expect(scrollRegion?.contains(screen.getByRole('radio', { name: /工程落地型/ }))).toBe(true)
|
||||
expect(scrollRegion?.contains(screen.getByText('下一题').closest('button'))).toBe(false)
|
||||
fireEvent.keyDown(screen.getByRole('radio', { name: /工程落地型/ }), { key: 'Enter' })
|
||||
expect(answer).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('radio', { name: /工程落地型/ }))
|
||||
|
||||
expect(screen.getByText('2 / 3')).toBeTruthy()
|
||||
@@ -280,7 +308,8 @@ describe('QuestionComposer', () => {
|
||||
const { carrier, answer } = wait()
|
||||
render(<QuestionComposer matched={carrier} {...kit} />)
|
||||
|
||||
expect((screen.getByText('下一题').closest('button') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(screen.getByRole('radio', { name: /工程落地型/ }).getAttribute('aria-checked')).toBe('true')
|
||||
expect((screen.getByText('下一题').closest('button') as HTMLButtonElement).disabled).toBe(false)
|
||||
fireEvent.click(screen.getByRole('radio', { name: '研究潜力型' }))
|
||||
expect(screen.getByText('2 / 3')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: '跳过' }))
|
||||
|
||||
Reference in new issue
Block a user