4aaf171d63
## What & why Adds an `ask_questions` tool that lets the in-app **Ask AI** assistant **pause a turn to ask the user one or more multiple-choice questions** (per the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=105959-117153)) and resume once answered — instead of guessing on ambiguous/consequential decisions. The tool is **harness-only**: an interactive question UI is meaningless without a user to answer it, so it must be absent from MCP and from head-less workflow agents. ## Design — true tool-result resume (not a synthetic user message) The user's answer is a **structured tool result bound to the `toolCallId`**, and the **same agent turn resumes** — exactly how Anthropic (`tool_result` by `tool_use_id`) and OpenAI (`function_call_output`) model human-in-the-loop. The naive form of this (leave the tool call in `input-available` to mean "pending") is **impossible** here: `finalizeDanglingToolParts` rewrites `input-available` → `output-error` ("Tool execution was interrupted") on both the persist path (`addMessage`) and the model-reload path (`chat-execution.service.ts`). That util is a load-bearing safety net, so weakening it is the wrong move. Instead: - `ask_questions` is an **inline, chat-only tool with an `execute` that returns a `status: 'pending'` result immediately**, so the tool part is always `output-available` and **immune to `finalizeDanglingToolParts`**. `stopWhen(hasToolCall('ask_questions'))` halts the turn right after the call (the model never sees the placeholder). - A nullable **`thread.pendingQuestionMessageId`** marker records that a turn is awaiting an answer. - The new **`answerAgentChatQuestion`** mutation atomically *claims* the question (clears the marker, marks the thread streaming), **writes the answer onto the same tool part** (`status: 'answered'`), and **re-enqueues the turn via the existing `existingTurnId` plumbing** (`isResume` bypasses the per-turn dedup guard). On resume `finalizeDanglingToolParts` leaves the `output-available` part untouched and `convertToModelMessages` emits `assistant(tool_use)` + `tool_result(answers)`, so the model continues. This achieves the platform-aligned semantics **without** weakening the finalize safety net or inventing a fragile new part state. ### Meets the two requirements - **Survives refresh, scoped per-thread** — the pending state is a normal persisted `output-available` part + the thread marker; the frontend card is derived per-thread from the loaded messages, so it re-appears on reload and only on its own thread. - **Takes priority over the queue** — a unified `isBlocked = activeStreamId || pendingQuestionMessageId` gate is applied in both `sendChatMessage` (new messages queue) and `flushNextQueuedMessage` (the drain). The queue cannot unpile until the question is answered and the resumed turn completes. ### Harness-only by construction `ask_questions` is added **only** to the chat's inline `activeTools` (like `learn_tools`/`execute_tool`/`load_skills`). It never enters the tool registry/catalog, so it is invisible to MCP and to workflow agents — no `MCP_EXCLUDED_TOOL_NAMES` entry needed. ## UX While a question is pending, the **composer is replaced by the question card** (matching the Figma): question title + pager (`1/2`), numbered option rows (`IconSquareNumber*`) with per-option info-icon descriptions and a "Recommended" badge, and the normal composer as the free-text fallback ("Type anything to do differently."). The transcript shows a compact "Asking questions…" status line that becomes an answered summary. ## Changes **twenty-shared** - `ai/types/AskQuestionsToolTypes.ts` — `AskQuestionItem/Option/Answer/Result`, `ASK_QUESTIONS_TOOL_NAME`. **twenty-server** - `ai-chat/tools/ask-questions.tool.ts` — inline tool factory (pending-result `execute`, zod schema, 1–4 questions × 2–4 options). - `chat-execution.service.ts` — add to `activeTools` + `preloadedToolNames`; `hasToolCall` in `stopWhen`. - `chat-system-prompts.const.ts` — when-to-use guidance. - `entities/agent-chat-thread.entity.ts` — `pendingQuestionMessageId` column. - `stream-agent-chat.job.ts` — set the marker on a question pause; bypass the dedup guard on resume; suppress the no-text warning for question pauses. - `agent-chat-streaming.service.ts` — gate `flushNextQueuedMessage`; `enqueueResumeStream`. - `agent-chat.resolver.ts` — gate `sendChatMessage`; `answerAgentChatQuestion` mutation. - `agent-chat.service.ts` — `resolvePendingQuestion` (atomic claim + write answer). - `dtos/agent-chat-question-answer.input.ts`, `ai.exception.ts` (`QUESTION_NOT_PENDING`), `utils/find-pending-question-part.util.ts`. **twenty-front** - `components/AiChatQuestionCard.tsx` — the interactive card (matches Figma tokens) + `__stories__/AiChatQuestionCard.stories.tsx`. - `components/AiChatEditorSection.tsx` — swap the composer for the card while pending. - `components/AiChatQuestionStatusRenderer.tsx` + branch in `AiChatAssistantMessageRenderer.tsx`. - `states/selectors/agentChatPendingQuestionComponentSelector.ts`, `types/AgentChatPendingQuestion.ts`. - `hooks/useSubmitQuestionAnswer.ts` + `utils/markQuestionAnswered.ts` (optimistic) + `graphql/mutations/answerAgentChatQuestion.ts`. A design doc lives at `packages/twenty-server/docs/ASK_USER_QUESTION_TOOL_PLAN.md`. ## Migration Adds a nullable `pendingQuestionMessageId` (uuid) column to `core.agentChatThread`. Needs a generated **fast instance command** (`database:migrate:generate --name addThreadPendingQuestion --type fast`) — see "Verification status". ## Tests - Server: `ask-questions.tool.spec.ts` (pending echo + schema bounds), `find-pending-question-part.util.spec.ts`. - Front: `markQuestionAnswered.test.ts`, plus the Storybook story. ## Verification status (please read) This branch was authored in an environment where the monorepo `yarn install` repeatedly failed on transient TLS resets from the package registry, so I could **not** locally run the mechanical gates. The logic was reviewed by hand and the `ai@6.0.97` exports used (`hasToolCall`, `stepCountIs`, `generateId`) were confirmed against the package's type defs. Still **TODO** (will rely on CI / a follow-up once deps install): - [ ] `nx run twenty-shared:generateBarrels` (the `ai/index.ts` export was added by hand; regen to reconcile) - [ ] `nx run twenty-front:graphql:generate` (new mutation + input type) - [ ] generate the fast instance command (migration) for the new column - [ ] `typecheck` + `lint:diff-with-main` (front + server) — expect minor import-ordering autofixes - [ ] run the unit tests **Screenshots:** reproducing the live flow needs an AI provider API key (to get the model to actually call `ask_questions`), which isn't available here. The card can be screenshotted from its **Storybook story** (`AiChatQuestionCard.stories.tsx`) with no API key — I'll add that image once deps install, or a reviewer can run `nx storybook twenty-front`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB --- _Generated by [Claude Code](https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22346?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. -->
482 lines
14 KiB
TypeScript
482 lines
14 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { type KeyboardEvent, useContext, useMemo, useState } from 'react';
|
|
import { type AskQuestionAnswer, type AskQuestionItem } from 'twenty-shared/ai';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { AppTooltip, TooltipDelay } from 'twenty-ui/surfaces';
|
|
import {
|
|
IconArrowUp,
|
|
IconChevronLeft,
|
|
IconChevronRightPipe,
|
|
IconInfoCircle,
|
|
type IconComponent,
|
|
IconSquareNumber1,
|
|
IconSquareNumber2,
|
|
IconSquareNumber3,
|
|
IconSquareNumber4,
|
|
IconSquareNumber5,
|
|
IconSquareNumber6,
|
|
IconSquareNumber7,
|
|
IconSquareNumber8,
|
|
IconSquareNumber9,
|
|
} from 'twenty-ui/icon';
|
|
import {
|
|
LightIconButton,
|
|
RoundedIconButton,
|
|
type SelectOption,
|
|
} from 'twenty-ui/input';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
import { AgentChatFileUploadButton } from '@/ai/components/internal/AgentChatFileUploadButton';
|
|
import { AiChatContextUsageButton } from '@/ai/components/internal/AiChatContextUsageButton';
|
|
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
|
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
|
|
import { useSubmitQuestionAnswer } from '@/ai/hooks/useSubmitQuestionAnswer';
|
|
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
|
import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState';
|
|
import { type AgentChatPendingQuestion } from '@/ai/types/AgentChatPendingQuestion';
|
|
import { Select } from '@/ui/input/components/Select';
|
|
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
|
|
|
const NUMBER_ICONS: IconComponent[] = [
|
|
IconSquareNumber1,
|
|
IconSquareNumber2,
|
|
IconSquareNumber3,
|
|
IconSquareNumber4,
|
|
IconSquareNumber5,
|
|
IconSquareNumber6,
|
|
IconSquareNumber7,
|
|
IconSquareNumber8,
|
|
IconSquareNumber9,
|
|
];
|
|
|
|
const StyledCard = 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;
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledQuestionSection = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[3]};
|
|
padding: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledQuestionHeaderRow = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
justify-content: space-between;
|
|
min-height: 24px;
|
|
padding-left: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledQuestionText = styled.p`
|
|
color: ${themeCssVariables.font.color.primary};
|
|
flex: 1 0 0;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
`;
|
|
|
|
const StyledPager = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledPagerLabel = styled.span`
|
|
color: ${themeCssVariables.font.color.light};
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
`;
|
|
|
|
const StyledOptionsList = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing['0.5']};
|
|
`;
|
|
|
|
const StyledOptionRow = styled.div<{ isHighlighted: boolean }>`
|
|
align-items: center;
|
|
background: ${({ isHighlighted }) =>
|
|
isHighlighted
|
|
? themeCssVariables.background.transparent.light
|
|
: 'transparent'};
|
|
border-radius: ${themeCssVariables.border.radius.sm};
|
|
box-sizing: border-box;
|
|
cursor: pointer;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
height: 32px;
|
|
justify-content: space-between;
|
|
overflow: hidden;
|
|
padding: 0 ${themeCssVariables.spacing[1]};
|
|
|
|
&:hover {
|
|
background: ${themeCssVariables.background.transparent.light};
|
|
}
|
|
`;
|
|
|
|
const StyledOptionLeft = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
flex: 1 0 0;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const StyledOptionLabel = styled.span`
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
flex-shrink: 0;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
line-height: 1.4;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const StyledRecommended = styled.span`
|
|
color: ${themeCssVariables.font.color.light};
|
|
flex-shrink: 0;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
line-height: 1.4;
|
|
`;
|
|
|
|
const StyledDivider = styled.div`
|
|
background: ${themeCssVariables.border.color.light};
|
|
height: 1px;
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledComposerSection = styled.div`
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
min-height: 80px;
|
|
padding: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledFreeTextArea = styled.textarea`
|
|
background: transparent;
|
|
border: none;
|
|
color: ${themeCssVariables.font.color.primary};
|
|
flex: 1 0 0;
|
|
font-family: inherit;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
line-height: 1.4;
|
|
min-height: 24px;
|
|
outline: none;
|
|
resize: none;
|
|
|
|
&::placeholder {
|
|
color: ${themeCssVariables.font.color.light};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
}
|
|
`;
|
|
|
|
const StyledActionsRow = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledLeftActions = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing['0.5']};
|
|
`;
|
|
|
|
const StyledRightActions = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const areAllQuestionsAnswered = (
|
|
questions: AskQuestionItem[],
|
|
selectedByQuestion: Record<number, number[]>,
|
|
freeTextByQuestion: Record<number, string>,
|
|
) =>
|
|
questions.every(
|
|
(_, index) =>
|
|
(selectedByQuestion[index]?.length ?? 0) > 0 ||
|
|
(freeTextByQuestion[index] ?? '').trim().length > 0,
|
|
);
|
|
|
|
type AiChatQuestionCardProps = {
|
|
pendingQuestion: AgentChatPendingQuestion;
|
|
};
|
|
|
|
export const AiChatQuestionCard = ({
|
|
pendingQuestion,
|
|
}: AiChatQuestionCardProps) => {
|
|
const { t } = useLingui();
|
|
const { theme } = useContext(ThemeContext);
|
|
const { messageId, toolCallId, questions } = pendingQuestion;
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [selectedByQuestion, setSelectedByQuestion] = useState<
|
|
Record<number, number[]>
|
|
>({});
|
|
const [freeTextByQuestion, setFreeTextByQuestion] = useState<
|
|
Record<number, string>
|
|
>({});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const { submitAnswer } = useSubmitQuestionAnswer();
|
|
|
|
const { options: modelOptions, pinnedOption } = useAiModelOptions({
|
|
variant: 'pinned-default',
|
|
});
|
|
const { enabledModels } = useWorkspaceAiModelAvailability();
|
|
const hasNoEnabledModels = enabledModels.length === 0;
|
|
const { selectedModelId } = useAgentChatModelId();
|
|
const setAgentChatUserSelectedModel = useSetAtomState(
|
|
agentChatUserSelectedModelState,
|
|
);
|
|
const defaultPinnedOption: SelectOption<string | null> | undefined =
|
|
pinnedOption ? { ...pinnedOption, value: null } : undefined;
|
|
|
|
const currentQuestion = questions[currentIndex];
|
|
const hasMultipleQuestions = questions.length > 1;
|
|
const isLastQuestion = currentIndex === questions.length - 1;
|
|
|
|
const buildAnswers = (
|
|
selected: Record<number, number[]>,
|
|
): AskQuestionAnswer[] =>
|
|
questions.map((_, index) => {
|
|
const trimmedFreeText = (freeTextByQuestion[index] ?? '').trim();
|
|
|
|
return {
|
|
questionIndex: index,
|
|
selectedOptionIndices: selected[index] ?? [],
|
|
freeText: trimmedFreeText.length > 0 ? trimmedFreeText : undefined,
|
|
};
|
|
});
|
|
|
|
const submit = async (answers: AskQuestionAnswer[]) => {
|
|
if (isSubmitting) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
await submitAnswer({ messageId, toolCallId, answers });
|
|
setIsSubmitting(false);
|
|
};
|
|
|
|
const handleSelectOption = (optionIndex: number) => {
|
|
if (currentQuestion.allowMultiSelect === true) {
|
|
setSelectedByQuestion((previous) => {
|
|
const current = previous[currentIndex] ?? [];
|
|
const next = current.includes(optionIndex)
|
|
? current.filter((value) => value !== optionIndex)
|
|
: [...current, optionIndex];
|
|
|
|
return { ...previous, [currentIndex]: next };
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const nextSelected = {
|
|
...selectedByQuestion,
|
|
[currentIndex]: [optionIndex],
|
|
};
|
|
|
|
setSelectedByQuestion(nextSelected);
|
|
|
|
if (!isLastQuestion) {
|
|
setCurrentIndex(currentIndex + 1);
|
|
|
|
return;
|
|
}
|
|
|
|
if (areAllQuestionsAnswered(questions, nextSelected, freeTextByQuestion)) {
|
|
void submit(buildAnswers(nextSelected));
|
|
}
|
|
};
|
|
|
|
const allQuestionsAnswered = useMemo(
|
|
() =>
|
|
areAllQuestionsAnswered(
|
|
questions,
|
|
selectedByQuestion,
|
|
freeTextByQuestion,
|
|
),
|
|
[questions, selectedByQuestion, freeTextByQuestion],
|
|
);
|
|
|
|
const handleSend = () => {
|
|
if (!allQuestionsAnswered) {
|
|
return;
|
|
}
|
|
|
|
void submit(buildAnswers(selectedByQuestion));
|
|
};
|
|
|
|
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
handleSend();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<StyledCard>
|
|
<StyledQuestionSection>
|
|
<StyledQuestionHeaderRow>
|
|
<StyledQuestionText>{currentQuestion.question}</StyledQuestionText>
|
|
{hasMultipleQuestions && (
|
|
<StyledPager>
|
|
<LightIconButton
|
|
Icon={IconChevronLeft}
|
|
size="small"
|
|
disabled={currentIndex === 0}
|
|
onClick={() =>
|
|
setCurrentIndex((index) => Math.max(0, index - 1))
|
|
}
|
|
/>
|
|
<StyledPagerLabel>
|
|
{currentIndex + 1}/{questions.length}
|
|
</StyledPagerLabel>
|
|
<LightIconButton
|
|
Icon={IconChevronRightPipe}
|
|
size="small"
|
|
disabled={isLastQuestion}
|
|
onClick={() =>
|
|
setCurrentIndex((index) =>
|
|
Math.min(questions.length - 1, index + 1),
|
|
)
|
|
}
|
|
/>
|
|
</StyledPager>
|
|
)}
|
|
</StyledQuestionHeaderRow>
|
|
|
|
<StyledOptionsList>
|
|
{currentQuestion.options.map((option, optionIndex) => {
|
|
const NumberIcon =
|
|
NUMBER_ICONS[optionIndex] ??
|
|
NUMBER_ICONS[NUMBER_ICONS.length - 1];
|
|
const isSelected = (
|
|
selectedByQuestion[currentIndex] ?? []
|
|
).includes(optionIndex);
|
|
const hasSelection =
|
|
(selectedByQuestion[currentIndex] ?? []).length > 0;
|
|
const isHighlighted =
|
|
isSelected || (!hasSelection && option.isRecommended === true);
|
|
const tooltipId = `ask-question-option-${toolCallId}-${currentIndex}-${optionIndex}`;
|
|
|
|
return (
|
|
<StyledOptionRow
|
|
key={optionIndex}
|
|
isHighlighted={isHighlighted}
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={() => handleSelectOption(optionIndex)}
|
|
onKeyDown={(event) => {
|
|
if (event.target !== event.currentTarget) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
handleSelectOption(optionIndex);
|
|
}
|
|
}}
|
|
>
|
|
<StyledOptionLeft>
|
|
<NumberIcon
|
|
size={theme.icon.size.sm}
|
|
color={themeCssVariables.font.color.tertiary}
|
|
/>
|
|
<StyledOptionLabel>{option.label}</StyledOptionLabel>
|
|
{option.isRecommended === true && (
|
|
<StyledRecommended>· {t`Recommended`}</StyledRecommended>
|
|
)}
|
|
</StyledOptionLeft>
|
|
{isDefined(option.description) && (
|
|
<>
|
|
<span
|
|
id={tooltipId}
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<LightIconButton
|
|
Icon={IconInfoCircle}
|
|
size="small"
|
|
accent="tertiary"
|
|
/>
|
|
</span>
|
|
<AppTooltip
|
|
anchorSelect={`#${tooltipId}`}
|
|
content={option.description}
|
|
delay={TooltipDelay.shortDelay}
|
|
place="left"
|
|
/>
|
|
</>
|
|
)}
|
|
</StyledOptionRow>
|
|
);
|
|
})}
|
|
</StyledOptionsList>
|
|
</StyledQuestionSection>
|
|
|
|
<StyledDivider />
|
|
|
|
<StyledComposerSection>
|
|
<StyledFreeTextArea
|
|
value={freeTextByQuestion[currentIndex] ?? ''}
|
|
placeholder={t`Type anything to do differently.`}
|
|
onChange={(event) =>
|
|
setFreeTextByQuestion((previous) => ({
|
|
...previous,
|
|
[currentIndex]: event.target.value,
|
|
}))
|
|
}
|
|
onKeyDown={handleKeyDown}
|
|
autoFocus
|
|
/>
|
|
<StyledActionsRow>
|
|
<StyledLeftActions>
|
|
<AgentChatFileUploadButton />
|
|
<AiChatContextUsageButton />
|
|
</StyledLeftActions>
|
|
<StyledRightActions>
|
|
<Select
|
|
dropdownId="ai-chat-question-model-select"
|
|
value={selectedModelId}
|
|
onChange={setAgentChatUserSelectedModel}
|
|
options={modelOptions}
|
|
pinnedOption={defaultPinnedOption}
|
|
disabled={hasNoEnabledModels}
|
|
selectSizeVariant="small"
|
|
showContextualTextInControl={false}
|
|
withSearchInput
|
|
dropdownOffset={{ x: 0, y: 8 }}
|
|
/>
|
|
<RoundedIconButton
|
|
Icon={IconArrowUp}
|
|
size="medium"
|
|
onClick={handleSend}
|
|
disabled={!allQuestionsAnswered || isSubmitting}
|
|
/>
|
|
</StyledRightActions>
|
|
</StyledActionsRow>
|
|
</StyledComposerSection>
|
|
</StyledCard>
|
|
);
|
|
};
|