fix(web): deduplicate plan review rendering
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Web `ask_user_question` feature plugin. Its host half mounts `dsh-tool-ask-user` only when the Web feature is selected; its browser half registers the `question` entry in the conversation-owned `conversation.composer` keyed slot.
|
||||
|
||||
The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Single-select choices advance immediately, and Enter submits once every question is answered or skipped; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`.
|
||||
The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. Question detail reuses the assistant-output `MarkdownText` primitive, including its GFM rendering and untrusted-content policy. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Single-select choices advance immediately, and Enter submits once every question is answered or skipped; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`.
|
||||
|
||||
Selection state is local to a component keyed by the request rpcId. A replay with the same id preserves a still-mounted draft, while `question/resolved` from the host removes the composer. The host remains authoritative: successful HTTP delivery does not remove pending state locally.
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@
|
||||
|
||||
.detail {
|
||||
margin: 0 2px 8px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.headerActions,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useState, type KeyboardEvent } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button, IconCheckOutline16, IconChevronLeftOutline14, IconChevronRightOutline14,
|
||||
IconCloseOutline16, IconEditOutline16,
|
||||
IconCloseOutline16, IconEditOutline16, MarkdownText,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { PendingQuestion, type QuestionAnswer, type QuestionComposerProps } from './contract/slots.ts'
|
||||
import css from './QuestionComposer.module.css'
|
||||
@@ -199,7 +199,9 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) {
|
||||
</header>
|
||||
|
||||
<div className={css.body} data-question-scroll>
|
||||
{question.detail !== undefined && <p className={css.detail}>{question.detail}</p>}
|
||||
{question.detail !== undefined && (
|
||||
<div className={css.detail}><MarkdownText text={question.detail} /></div>
|
||||
)}
|
||||
<div className={css.options} role={question.multiSelect === true ? 'group' : 'radiogroup'}>
|
||||
{(question.options ?? []).map((option, optionIndex) => {
|
||||
const selected = draft.selected.includes(option.label)
|
||||
|
||||
@@ -101,6 +101,29 @@ describe('QuestionComposer', () => {
|
||||
expect((screen.getByRole('button', { name: '正在提交…' }) as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('renders plan detail through the shared assistant Markdown primitive', () => {
|
||||
const carrier = new PendingWait(
|
||||
'question',
|
||||
RpcId('markdown-plan'),
|
||||
SID,
|
||||
{
|
||||
questions: [{
|
||||
id: 'plan',
|
||||
question: '批准这个计划吗?',
|
||||
detail: '# 实施计划\n\n- **先验证**现状\n- 修改 `QuestionComposer`',
|
||||
options: [{ label: '批准' }],
|
||||
}],
|
||||
} as PendingWait<'question'>['payload'],
|
||||
vi.fn(),
|
||||
)
|
||||
const view = render(<QuestionComposer matched={carrier} interactions={[carrier]} {...kit} />)
|
||||
|
||||
expect(screen.getByRole('heading', { level: 1, name: '实施计划' })).toBeTruthy()
|
||||
expect(view.container.querySelector('strong')?.textContent).toBe('先验证')
|
||||
expect(view.container.querySelector('code')?.textContent).toBe('QuestionComposer')
|
||||
expect(view.container.querySelectorAll('li')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('skips individual questions without discarding earlier answers', () => {
|
||||
const { carrier, respond } = wait()
|
||||
render(<QuestionComposer matched={carrier} interactions={[carrier]} {...kit} />)
|
||||
|
||||
Reference in New Issue
Block a user