Skip to content

Commit e102fd5

Browse files
Sg312claude
andcommitted
improvement(chat): answered question card becomes the user turn; two select types only
UI ordering: answering a question card no longer echoes a duplicate user bubble. The combined answer still goes on the wire as a user message, but the chat pairs it back to its card (strict 'Prompt — Answer' match, now uniform for single questions too) and renders the card as the answered recap — the card IS the user turn, and the next assistant message streams below it. The pairing is derived from the transcript, so live and reloaded renders are identical; a dismissed card followed by an unrelated typed message does not match and renders normally. Messages ending with a question card also drop the copy/thumbs actions row — the card is an input surface, not a reactable assistant turn. Question types are now single_select and multi_select only: text is removed (the free-text 'Something else' row covers it) and confirm collapses into single_select with Yes/No options. multi_select rows toggle with a check and the free-text row's arrow submits the step; answers are comma-joined labels plus any typed entry. Agent-supplied catch-all options ('Other', 'Something else', 'None of the above') are stripped at parse — the card always provides its own free-text row; a question left with no real options is invalid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7fc6bb0 commit e102fd5

9 files changed

Lines changed: 393 additions & 122 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ const MARKDOWN_COMPONENTS = {
279279
interface ChatContentProps {
280280
content: string
281281
isStreaming?: boolean
282+
/** Transcript-derived answers for this message's question card (renders the recap). */
283+
questionAnswers?: string[]
282284
onOptionSelect?: (id: string) => void
283285
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
284286
onRevealStateChange?: (isRevealing: boolean) => void
@@ -287,6 +289,7 @@ interface ChatContentProps {
287289
function ChatContentInner({
288290
content,
289291
isStreaming = false,
292+
questionAnswers,
290293
onOptionSelect,
291294
onWorkspaceResourceSelect,
292295
onRevealStateChange,
@@ -406,6 +409,7 @@ function ChatContentInner({
406409
<SpecialTags
407410
key={`special-${group.index}`}
408411
segment={group.segment}
412+
questionAnswers={questionAnswers}
409413
onOptionSelect={onOptionSelect}
410414
/>
411415
)
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
export { formatQuestionAnswerMessage, QuestionDisplay } from './question'
1+
export {
2+
formatQuestionAnswerMessage,
3+
parseQuestionAnswerMessage,
4+
QuestionDisplay,
5+
} from './question'

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.test.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import { formatQuestionAnswerMessage } from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/question'
5+
import {
6+
formatQuestionAnswerMessage,
7+
parseQuestionAnswerMessage,
8+
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/question'
69
import type { QuestionItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags'
710

811
const QUESTIONS: QuestionItem[] = [
@@ -12,20 +15,27 @@ const QUESTIONS: QuestionItem[] = [
1215
options: [{ id: 'keep_newest', label: 'Keep the newest entry' }],
1316
},
1417
{
15-
type: 'confirm',
18+
type: 'single_select',
1619
prompt: 'Delete 4 archived workflows?',
1720
options: [
1821
{ id: 'yes', label: 'Delete them' },
1922
{ id: 'no', label: 'Cancel' },
2023
],
2124
},
22-
{ type: 'text', prompt: 'What time zone should the daily report run in?' },
25+
{
26+
type: 'multi_select',
27+
prompt: 'What time zone should the daily report run in?',
28+
options: [
29+
{ id: 'est', label: 'EST' },
30+
{ id: 'pst', label: 'PST' },
31+
],
32+
},
2333
]
2434

2535
describe('formatQuestionAnswerMessage', () => {
26-
it('sends just the answer for a single question', () => {
36+
it('sends a prompt-answer line for a single question', () => {
2737
expect(formatQuestionAnswerMessage([QUESTIONS[0]], ['Keep the newest entry'])).toBe(
28-
'Keep the newest entry'
38+
'How should I handle the duplicates? — Keep the newest entry'
2939
)
3040
})
3141

@@ -37,3 +47,40 @@ describe('formatQuestionAnswerMessage', () => {
3747
)
3848
})
3949
})
50+
51+
describe('parseQuestionAnswerMessage', () => {
52+
it('round-trips what formatQuestionAnswerMessage produces', () => {
53+
const answers = ['Keep the newest entry', 'Cancel', 'EST']
54+
const message = formatQuestionAnswerMessage(QUESTIONS, answers)
55+
expect(parseQuestionAnswerMessage(QUESTIONS, message)).toEqual(answers)
56+
})
57+
58+
it('round-trips a single question', () => {
59+
const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['Merge them'])
60+
expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual(['Merge them'])
61+
})
62+
63+
it('rejects an unrelated user message (dismissed card, typed something else)', () => {
64+
expect(parseQuestionAnswerMessage([QUESTIONS[0]], 'actually, show me the logs')).toBeNull()
65+
})
66+
67+
it('rejects when the line count does not match the question count', () => {
68+
const partial = formatQuestionAnswerMessage(QUESTIONS.slice(0, 2), ['A', 'B'])
69+
expect(parseQuestionAnswerMessage(QUESTIONS, partial)).toBeNull()
70+
})
71+
72+
it('rejects when a line pairs with the wrong prompt', () => {
73+
const swapped =
74+
'Delete 4 archived workflows? — Cancel\n' +
75+
'How should I handle the duplicates? — Keep the newest entry\n' +
76+
'What time zone should the daily report run in? — EST'
77+
expect(parseQuestionAnswerMessage(QUESTIONS, swapped)).toBeNull()
78+
})
79+
80+
it('preserves em-dashes inside the answer text', () => {
81+
const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['newest — but keep backups'])
82+
expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual([
83+
'newest — but keep backups',
84+
])
85+
})
86+
})

0 commit comments

Comments
 (0)