From 9abf76f5f23512211dfddd9c2ffda7efef2adb43 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:53:51 +0200 Subject: [PATCH] fix(ai): show answered ask_questions as a card in chat history (#23075) ## Context Feedback on the `ask_questions` (Ask AI) tool: - When answering a select prompt with a free-form message, the answer wasn't surfaced as expected in the conversation history. - The selected value also looked dropped once picked. Root cause: answered `ask_questions` parts were caught by the thinking-steps grouping and rendered as a generic collapsible "Ran ask_questions" tool step (JSON output), so the dedicated renderer was never reached. ## Changes - **Render answered questions as a card** (`AiChatQuestionStatusRenderer`): an "Answers" card that shows each full question with the chosen option label(s) or the free-text answer beneath it, instead of the faint inline `header: value` line. - **Free-text keyboard navigation** (`AiChatQuestionCard`): pressing Enter in the free-text area now advances to the next question, or submits when on the last question (mirroring the option-select flow). Shift+Enter still inserts a newline. ## Notes - No schema/GraphQL changes; display + interaction only. - Existing `thinkingStepsDisplayState` grouping test is unaffected (only `web_search`/`create_task`/`code_interpreter` are used there). Screenshot 2026-07-20 at 17 48 04 Review in cubic fixes https://discord.com/channels/1130383047699738754/1526871110300209282 --- .../ai/components/AiChatQuestionCard.tsx | 7 ++ .../AiChatQuestionStatusRenderer.tsx | 82 +++++++++++-------- .../utils/groupContiguousThinkingStepParts.ts | 3 +- .../ai/utils/isAskQuestionsToolPart.ts | 8 ++ 4 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/utils/isAskQuestionsToolPart.ts diff --git a/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx b/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx index e36ec0eb1b..b181c10f36 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx @@ -330,6 +330,13 @@ export const AiChatQuestionCard = ({ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); + + if (!isLastQuestion) { + setCurrentIndex(currentIndex + 1); + + return; + } + handleSend(); } }; diff --git a/packages/twenty-front/src/modules/ai/components/AiChatQuestionStatusRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AiChatQuestionStatusRenderer.tsx index 6f8e855d69..27fd4b383e 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatQuestionStatusRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatQuestionStatusRenderer.tsx @@ -22,26 +22,40 @@ const StyledContainer = styled.div` } `; -const StyledContent = styled.div` - display: flex; - flex-direction: column; - gap: ${themeCssVariables.spacing['0.5']}; - min-width: 0; -`; - const StyledMessage = styled.span` color: ${themeCssVariables.font.color.tertiary}; font-size: ${themeCssVariables.font.size.md}; font-weight: ${themeCssVariables.font.weight.medium}; `; -const StyledAnswerLine = styled.span` +const StyledAnswersCard = styled.div` + background-color: ${themeCssVariables.background.transparent.lighter}; + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.sm}; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + padding: ${themeCssVariables.spacing[3]}; +`; + +const StyledAnswerBlock = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing['0.5']}; + min-width: 0; +`; + +const StyledAnswerQuestion = styled.span` color: ${themeCssVariables.font.color.tertiary}; font-size: ${themeCssVariables.font.size.sm}; + overflow-wrap: anywhere; `; const StyledAnswerValue = styled.span` color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.sm}; + overflow-wrap: anywhere; `; export const AiChatQuestionStatusRenderer = ({ @@ -79,34 +93,32 @@ export const AiChatQuestionStatusRenderer = ({ const answers = result?.answers ?? []; return ( - - - - {t`Questions answered`} - {questions.map((question, index) => { - const answer = answers.find( - (candidate) => candidate.questionIndex === index, - ); - const selectedLabels = (answer?.selectedOptionIndices ?? []) - .map((optionIndex) => question.options[optionIndex]?.label) - .filter(isNonEmptyString); - const freeTextAnswer = answer?.freeText ?? ''; - const value = - freeTextAnswer.length > 0 - ? freeTextAnswer - : selectedLabels.join(', '); + + {t`Answers`} + {questions.map((question, index) => { + const answer = answers.find( + (candidate) => candidate.questionIndex === index, + ); + const selectedLabels = (answer?.selectedOptionIndices ?? []) + .map((optionIndex) => question.options[optionIndex]?.label) + .filter(isNonEmptyString); + const freeTextAnswer = answer?.freeText ?? ''; + const value = + freeTextAnswer.length > 0 + ? freeTextAnswer + : selectedLabels.join(', '); - if (value.length === 0) { - return null; - } + if (value.length === 0) { + return null; + } - return ( - - {question.header}: {value} - - ); - })} - - + return ( + + {question.question} + {value} + + ); + })} + ); }; diff --git a/packages/twenty-front/src/modules/ai/utils/groupContiguousThinkingStepParts.ts b/packages/twenty-front/src/modules/ai/utils/groupContiguousThinkingStepParts.ts index 663d4c2cbb..f63d92f73b 100644 --- a/packages/twenty-front/src/modules/ai/utils/groupContiguousThinkingStepParts.ts +++ b/packages/twenty-front/src/modules/ai/utils/groupContiguousThinkingStepParts.ts @@ -1,6 +1,7 @@ import { type ExtendedUIMessagePart } from 'twenty-shared/ai'; import { type AssistantMessageRenderItem } from '@/ai/utils/assistantMessageRenderItem'; +import { isAskQuestionsToolPart } from '@/ai/utils/isAskQuestionsToolPart'; import { isThinkingStepPart } from '@/ai/utils/isThinkingStepPart'; import { type ThinkingStepPart } from '@/ai/utils/thinkingStepPart'; @@ -25,7 +26,7 @@ export const groupContiguousThinkingStepParts = ( continue; } - if (isThinkingStepPart(part)) { + if (isThinkingStepPart(part) && !isAskQuestionsToolPart(part)) { currentThinkingParts.push(part); continue; } diff --git a/packages/twenty-front/src/modules/ai/utils/isAskQuestionsToolPart.ts b/packages/twenty-front/src/modules/ai/utils/isAskQuestionsToolPart.ts new file mode 100644 index 0000000000..69fe625e49 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/utils/isAskQuestionsToolPart.ts @@ -0,0 +1,8 @@ +import { getToolName, isToolUIPart } from 'ai'; +import { + ASK_QUESTIONS_TOOL_NAME, + type ExtendedUIMessagePart, +} from 'twenty-shared/ai'; + +export const isAskQuestionsToolPart = (part: ExtendedUIMessagePart): boolean => + isToolUIPart(part) && getToolName(part) === ASK_QUESTIONS_TOOL_NAME;