diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 0072461209..1bf80b9c82 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3380,6 +3380,7 @@ type Mutation { createChatThread: AgentChatThread! sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult! retryChatMessage(threadId: UUID!, modelId: String): SendChatMessageResult! + answerAgentChatQuestion(threadId: UUID!, messageId: UUID!, answers: [AgentChatQuestionAnswerInput!]!, modelId: String): SendChatMessageResult! stopAgentChatStream(threadId: UUID!): Boolean! renameChatThread(id: UUID!, title: String!): AgentChatThread! archiveChatThread(id: UUID!): AgentChatThread! @@ -4486,6 +4487,12 @@ input FileAttachmentInput { filename: String! } +input AgentChatQuestionAnswerInput { + questionIndex: Int! + selectedOptionIndices: [Int!]! + freeText: String +} + input CreateSkillInput { id: UUID name: String! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index d8bce60e1c..e7bc8b0567 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2912,6 +2912,7 @@ export interface Mutation { createChatThread: AgentChatThread sendChatMessage: SendChatMessageResult retryChatMessage: SendChatMessageResult + answerAgentChatQuestion: SendChatMessageResult stopAgentChatStream: Scalars['Boolean'] renameChatThread: AgentChatThread archiveChatThread: AgentChatThread @@ -6088,6 +6089,7 @@ export interface MutationGenqlSelection{ createChatThread?: AgentChatThreadGenqlSelection sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} }) retryChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], modelId?: (Scalars['String'] | null)} }) + answerAgentChatQuestion?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], messageId: Scalars['UUID'], answers: AgentChatQuestionAnswerInput[], modelId?: (Scalars['String'] | null)} }) stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} } renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} }) archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -6509,6 +6511,8 @@ export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChanne export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']} +export interface AgentChatQuestionAnswerInput {questionIndex: Scalars['Int'],selectedOptionIndices: Scalars['Int'][],freeText?: (Scalars['String'] | null)} + export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']} export interface UpdateSkillInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 3dbd67db62..effee6c2ab 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -81,8 +81,8 @@ export default { 349, 356, 439, - 484, - 493 + 485, + 494 ], "types": { "BillingProductDTO": { @@ -8318,6 +8318,26 @@ export default { ] } ], + "answerAgentChatQuestion": [ + 318, + { + "threadId": [ + 3, + "UUID!" + ], + "messageId": [ + 3, + "UUID!" + ], + "answers": [ + 475, + "[AgentChatQuestionAnswerInput!]!" + ], + "modelId": [ + 1 + ] + } + ], "stopAgentChatStream": [ 6, { @@ -8380,7 +8400,7 @@ export default { 311, { "input": [ - 475, + 476, "CreateSkillInput!" ] } @@ -8389,7 +8409,7 @@ export default { 311, { "input": [ - 476, + 477, "UpdateSkillInput!" ] } @@ -8447,7 +8467,7 @@ export default { 243, { "input": [ - 477, + 478, "GetAuthorizationUrlForSSOInput!" ] } @@ -8613,7 +8633,7 @@ export default { 246, { "input": [ - 478 + 479 ] } ], @@ -8768,7 +8788,7 @@ export default { 6, { "input": [ - 479, + 480, "UpdateWorkspaceMemberSettingsInput!" ] } @@ -8802,7 +8822,7 @@ export default { 221, { "input": [ - 480, + 481, "SetupOIDCSsoInput!" ] } @@ -8811,7 +8831,7 @@ export default { 221, { "input": [ - 481, + 482, "SetupSAMLSsoInput!" ] } @@ -8820,7 +8840,7 @@ export default { 217, { "input": [ - 482, + 483, "DeleteSsoInput!" ] } @@ -8829,7 +8849,7 @@ export default { 218, { "input": [ - 483, + 484, "EditSsoInput!" ] } @@ -8858,7 +8878,7 @@ export default { 307, { "type": [ - 484, + 485, "AnalyticsType!" ], "name": [ @@ -8898,7 +8918,7 @@ export default { 297, { "input": [ - 485, + 486, "CreateCalendarEventInput!" ] } @@ -8907,7 +8927,7 @@ export default { 306, { "input": [ - 486, + 487, "SendEmailInput!" ] } @@ -8929,7 +8949,7 @@ export default { "String!" ], "connectionParameters": [ - 488, + 489, "EmailAccountConnectionParameters!" ], "id": [ @@ -8941,7 +8961,7 @@ export default { 167, { "input": [ - 490, + 491, "UpdateLabPublicFeatureFlagInput!" ] } @@ -8981,7 +9001,7 @@ export default { 74, { "input": [ - 491, + 492, "CreateOneAppTokenInput!" ] } @@ -9059,7 +9079,7 @@ export default { "String!" ], "fileFolder": [ - 493, + 494, "FileFolder!" ], "filePath": [ @@ -11443,6 +11463,20 @@ export default { 1 ] }, + "AgentChatQuestionAnswerInput": { + "questionIndex": [ + 21 + ], + "selectedOptionIndices": [ + 21 + ], + "freeText": [ + 1 + ], + "__typename": [ + 1 + ] + }, "CreateSkillInput": { "id": [ 3 @@ -11649,7 +11683,7 @@ export default { 1 ], "files": [ - 487 + 488 ], "__typename": [ 1 @@ -11668,13 +11702,13 @@ export default { }, "EmailAccountConnectionParameters": { "IMAP": [ - 489 + 490 ], "SMTP": [ - 489 + 490 ], "CALDAV": [ - 489 + 490 ], "__typename": [ 1 @@ -11713,7 +11747,7 @@ export default { }, "CreateOneAppTokenInput": { "appToken": [ - 492 + 493 ], "__typename": [ 1 @@ -11742,7 +11776,7 @@ export default { 231, { "input": [ - 495, + 496, "LogicFunctionLogsInput!" ] } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 2a870825e2..1695671555 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -57,6 +57,12 @@ export type AgentChatEvent = { threadId: Scalars['String']['output']; }; +export type AgentChatQuestionAnswerInput = { + freeText?: InputMaybe; + questionIndex: Scalars['Int']['input']; + selectedOptionIndices: Array; +}; + export type AgentChatThread = { __typename?: 'AgentChatThread'; contextWindowTokens?: Maybe; @@ -2454,6 +2460,7 @@ export type Mutation = { activateSkill: Skill; activateWorkspace: Workspace; addQueryToEventStream: Scalars['Boolean']['output']; + answerAgentChatQuestion: SendChatMessageResult; archiveChatThread: AgentChatThread; assignRoleToAgent: Scalars['Boolean']['output']; assignRoleToApiKey: Scalars['Boolean']['output']; @@ -2693,6 +2700,14 @@ export type MutationAddQueryToEventStreamArgs = { }; +export type MutationAnswerAgentChatQuestionArgs = { + answers: Array; + messageId: Scalars['UUID']['input']; + modelId?: InputMaybe; + threadId: Scalars['UUID']['input']; +}; + + export type MutationArchiveChatThreadArgs = { id: Scalars['UUID']['input']; }; @@ -6373,6 +6388,16 @@ export type ActivateSkillMutationVariables = Exact<{ export type ActivateSkillMutation = { __typename?: 'Mutation', activateSkill: { __typename?: 'Skill', id: string, name: string, label: string, description?: string | null, icon?: string | null, content: string, isCustom: boolean, isActive: boolean, createdAt: string, updatedAt: string } }; +export type AnswerAgentChatQuestionMutationVariables = Exact<{ + threadId: Scalars['UUID']['input']; + messageId: Scalars['UUID']['input']; + answers: Array | AgentChatQuestionAnswerInput; + modelId?: InputMaybe; +}>; + + +export type AnswerAgentChatQuestionMutation = { __typename?: 'Mutation', answerAgentChatQuestion: { __typename?: 'SendChatMessageResult', messageId: string, queued: boolean, streamId?: string | null } }; + export type ArchiveChatThreadMutationVariables = Exact<{ id: Scalars['UUID']['input']; }>; @@ -8711,6 +8736,7 @@ export const UnsubscribeTopicsDocument = {"kind":"Document","definitions":[{"kin export const SendEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SendEmailInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"messageThreadId"}}]}}]}}]} as unknown as DocumentNode; export const SendMessageCampaignDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendMessageCampaign"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SendMessageCampaignInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendMessageCampaign"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"campaignId"}},{"kind":"Field","name":{"kind":"Name","value":"queuedCount"}},{"kind":"Field","name":{"kind":"Name","value":"skipped"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"noEmail"}},{"kind":"Field","name":{"kind":"Name","value":"deduped"}},{"kind":"Field","name":{"kind":"Name","value":"overCap"}}]}}]}}]}}]} as unknown as DocumentNode; export const ActivateSkillDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ActivateSkill"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activateSkill"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SkillFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SkillFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Skill"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const AnswerAgentChatQuestionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AnswerAgentChatQuestion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"answers"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AgentChatQuestionAnswerInput"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"answerAgentChatQuestion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"answers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"answers"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode; export const ArchiveChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveChatThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveChatThread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const AssignRoleToAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AssignRoleToAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignRoleToAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"roleId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}}}]}]}}]} as unknown as DocumentNode; export const CreateChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateChatThread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createChatThread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx index 246d7c0260..96c3b2f88c 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx @@ -4,13 +4,17 @@ import { RoutingStatusDisplay } from '@/ai/components/RoutingStatusDisplay'; import { ThinkingStepsDisplay } from '@/ai/components/ThinkingStepsDisplay'; import { IconDotsVertical } from 'twenty-ui/icon'; +import { AiChatQuestionStatusRenderer } from '@/ai/components/AiChatQuestionStatusRenderer'; import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer'; import { ToolStepRenderer } from '@/ai/components/ToolStepRenderer'; import { groupContiguousThinkingStepParts } from '@/ai/utils/groupContiguousThinkingStepParts'; import { isCodeInterpreterToolPart } from '@/ai/utils/isCodeInterpreterToolPart'; import { styled } from '@linaria/react'; -import { isToolUIPart } from 'ai'; -import { type ExtendedUIMessagePart } from 'twenty-shared/ai'; +import { getToolName, isToolUIPart } from 'ai'; +import { + ASK_QUESTIONS_TOOL_NAME, + type ExtendedUIMessagePart, +} from 'twenty-shared/ai'; import { useContext } from 'react'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; @@ -76,6 +80,15 @@ const MessagePartRenderer = ({ ); default: if (isToolUIPart(part)) { + if (getToolName(part) === ASK_QUESTIONS_TOOL_NAME) { + return ( + + ); + } + return ; } return null; diff --git a/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx b/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx index 64777935ca..af6d2a3263 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx @@ -3,8 +3,11 @@ import { EditorContent } from '@tiptap/react'; import { useLingui } from '@lingui/react/macro'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { isDefined } from 'twenty-shared/utils'; + import { AiChatBanner } from '@/ai/components/AiChatBanner'; import { AiChatEmptyState } from '@/ai/components/AiChatEmptyState'; +import { AiChatQuestionCard } from '@/ai/components/AiChatQuestionCard'; import { AIChatNoMoreBillingCreditsBanner } from '@/ai/components/AIChatNoMoreBillingCreditsBanner'; import { AiChatStandaloneError } from '@/ai/components/AiChatStandaloneError'; import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatContextPreview'; @@ -18,8 +21,10 @@ import { useAiChatEditor } from '@/ai/hooks/useAiChatEditor'; import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions'; import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability'; import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState'; +import { agentChatPendingQuestionComponentSelector } from '@/ai/states/selectors/agentChatPendingQuestionComponentSelector'; import { Select } from '@/ui/input/components/Select'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { hasReachedCurrentBillingPeriodCapSelector } from '@/workspace/states/hasReachedCurrentBillingPeriodCapSelector'; @@ -137,6 +142,10 @@ export const AiChatEditorSection = () => { const { editor, handleSendAndClear } = useAiChatEditor(); + const pendingQuestion = useAtomComponentSelectorValue( + agentChatPendingQuestionComponentSelector, + ); + return ( <> @@ -155,35 +164,39 @@ export const AiChatEditorSection = () => { {hasReachedCurrentBillingPeriodCap && ( )} - - - - - - - - - - - + + + + + )} ); diff --git a/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx b/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx new file mode 100644 index 0000000000..e36ec0eb1b --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/AiChatQuestionCard.tsx @@ -0,0 +1,481 @@ +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, + freeTextByQuestion: Record, +) => + 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 + >({}); + const [freeTextByQuestion, setFreeTextByQuestion] = useState< + Record + >({}); + 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 | 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, + ): 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) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSend(); + } + }; + + return ( + + + + {currentQuestion.question} + {hasMultipleQuestions && ( + + + setCurrentIndex((index) => Math.max(0, index - 1)) + } + /> + + {currentIndex + 1}/{questions.length} + + + setCurrentIndex((index) => + Math.min(questions.length - 1, index + 1), + ) + } + /> + + )} + + + + {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 ( + handleSelectOption(optionIndex)} + onKeyDown={(event) => { + if (event.target !== event.currentTarget) { + return; + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleSelectOption(optionIndex); + } + }} + > + + + {option.label} + {option.isRecommended === true && ( + · {t`Recommended`} + )} + + {isDefined(option.description) && ( + <> + event.stopPropagation()} + > + + + + + )} + + ); + })} + + + + + + + + setFreeTextByQuestion((previous) => ({ + ...previous, + [currentIndex]: event.target.value, + })) + } + onKeyDown={handleKeyDown} + autoFocus + /> + + + + + + +