{activeQuestion.question}
diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx index d951f1e21fe36..2a95e585c2ce2 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx @@ -28,6 +28,7 @@ function renderPendingActions(isRunning: boolean) { canAdvance: true, isResponding: false, isComplete: true, + unansweredCount: 0, }, isRunning, canInterrupt: isRunning, diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index fc811118aa124..558b6b7da6303 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -23,6 +23,7 @@ interface PendingActionState { canAdvance: boolean; isResponding: boolean; isComplete: boolean; + unansweredCount: number; } interface ComposerPrimaryActionsProps { @@ -63,10 +64,15 @@ const formatPendingPrimaryActionLabel = (input: { isLastQuestion: boolean; isResponding: boolean; questionIndex: number; + unansweredCount: number; }) => { if (input.isResponding) { return "Submitting..."; } + // Submitting with gaps jumps to the first unanswered question instead. + if (input.isLastQuestion && input.unansweredCount > 0) { + return `Submit (${input.unansweredCount} left)`; + } if (input.compact) { return input.isLastQuestion ? "Submit" : "Next"; } @@ -194,7 +200,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ !canOperateThread || isEnvironmentUnavailable || pendingAction.isResponding || - (pendingAction.isLastQuestion ? !pendingAction.isComplete : !pendingAction.canAdvance) + !pendingAction.canAdvance } > {formatPendingPrimaryActionLabel({ @@ -202,6 +208,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ isLastQuestion: pendingAction.isLastQuestion, isResponding: pendingAction.isResponding, questionIndex: pendingAction.questionIndex, + unansweredCount: pendingAction.unansweredCount, })} diff --git a/apps/web/src/pendingUserInput.test.ts b/apps/web/src/pendingUserInput.test.ts index d6d14b0c4cacb..274945beefa62 100644 --- a/apps/web/src/pendingUserInput.test.ts +++ b/apps/web/src/pendingUserInput.test.ts @@ -12,6 +12,7 @@ import { projectPendingUserInputAnswer, resolvePendingUserInputAnswer, setPendingUserInputCustomAnswer, + summarizePendingUserInputAnswer, togglePendingUserInputOptionSelection, } from "./pendingUserInput"; @@ -406,6 +407,44 @@ it("accepts attachment-only answers after every upload finishes", () => { ).toBeNull(); }); +describe("summarizePendingUserInputAnswer", () => { + const question = { + id: "hosting", + header: "Images", + question: "Where should images go?", + options: [ + { label: "GitHub attachments", value: "gh", description: "Upload with gh" }, + { label: "Drop", description: "Upload to Drop" }, + ], + multiSelect: false, + }; + + it("is null while the question is unanswered", () => { + expect(summarizePendingUserInputAnswer(question, undefined)).toBeNull(); + }); + + it("shows the option label rather than its value", () => { + expect(summarizePendingUserInputAnswer(question, { selectedOptionValues: ["gh"] })).toBe( + "GitHub attachments", + ); + }); + + it("joins multi-select labels", () => { + expect( + summarizePendingUserInputAnswer( + { ...question, multiSelect: true }, + { selectedOptionValues: ["Drop", "gh"] }, + ), + ).toBe("Drop, GitHub attachments"); + }); + + it("collapses a typed answer onto one line", () => { + expect( + summarizePendingUserInputAnswer(question, { customAnswer: " Use S3\n with a CDN " }), + ).toBe("Use S3 with a CDN"); + }); +}); + describe("carryDisplacedCustomAnswerIntoPrompt", () => { it("keeps the thread draft when nothing was typed into the answer", () => { expect(carryDisplacedCustomAnswerIntoPrompt("draft", undefined)).toBe("draft"); diff --git a/apps/web/src/pendingUserInput.ts b/apps/web/src/pendingUserInput.ts index 9b5abf195c7a2..ba8eeed85de92 100644 --- a/apps/web/src/pendingUserInput.ts +++ b/apps/web/src/pendingUserInput.ts @@ -86,6 +86,20 @@ export function resolvePendingUserInputAnswer( ); } +/** Short text for a question's answer, as shown on its tab. Null while unanswered. */ +export function summarizePendingUserInputAnswer( + question: UserInputQuestion, + draft: PendingUserInputDraftAnswer | undefined, +): string | null { + const answer = resolvePendingUserInputAnswer(question, draft); + if (answer === null) return null; + const labelFor = (value: string) => + question.options.find((option) => (option.value ?? option.label) === value)?.label ?? value; + if (Array.isArray(answer)) return answer.map(labelFor).join(", "); + if (answer.trim().length === 0) return "Attachment"; + return labelFor(answer).replace(/\s+/g, " ").trim(); +} + export function setPendingUserInputCustomAnswer( draft: PendingUserInputDraftAnswer | undefined, customAnswer: string,