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). <img width="421" height="301" alt="Screenshot 2026-07-20 at 17 48 04" src="https://github.com/user-attachments/assets/3845055d-7061-40c0-b263-aa1c670329cd" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23075?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> fixes https://discord.com/channels/1130383047699738754/1526871110300209282
This commit is contained in:
@@ -330,6 +330,13 @@ export const AiChatQuestionCard = ({
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isLastQuestion) {
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<StyledContainer>
|
||||
<IconHelpCircle size={theme.icon.size.sm} />
|
||||
<StyledContent>
|
||||
<StyledMessage>{t`Questions answered`}</StyledMessage>
|
||||
{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(', ');
|
||||
<StyledAnswersCard>
|
||||
<StyledMessage>{t`Answers`}</StyledMessage>
|
||||
{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 (
|
||||
<StyledAnswerLine key={index}>
|
||||
{question.header}: <StyledAnswerValue>{value}</StyledAnswerValue>
|
||||
</StyledAnswerLine>
|
||||
);
|
||||
})}
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
return (
|
||||
<StyledAnswerBlock key={index}>
|
||||
<StyledAnswerQuestion>{question.question}</StyledAnswerQuestion>
|
||||
<StyledAnswerValue>{value}</StyledAnswerValue>
|
||||
</StyledAnswerBlock>
|
||||
);
|
||||
})}
|
||||
</StyledAnswersCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user