diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index 2eb61214768..fcdee3be2d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -59,6 +59,7 @@ import { import { type ContentSegment, type CredentialSubmissionPayload, + type InteractionAnswerHandler, parseSpecialTags, type SourceTagData, SpecialTags, @@ -520,7 +521,7 @@ interface ChatContentProps { credentialSubmission?: CredentialSubmissionPayload /** The user moved on without submitting this message's credential card. */ credentialAbandoned?: boolean - onOptionSelect?: (id: string) => void + onOptionSelect?: InteractionAnswerHandler onQuestionDismiss?: () => void onWorkspaceResourceSelect?: (resource: WorkspaceResourceRef) => void onRevealStateChange?: (isRevealing: boolean) => void diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx index 4ab09fcf173..c54caef74f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx @@ -19,7 +19,14 @@ import { InteractionCardInputRow, InteractionCardRecap, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/interaction-card' -import type { QuestionItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { ResourceQuestionRows } from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question-rows' +import type { + InteractionAnswerHandler, + QuestionItem, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { mapResourceToContext } from '@/app/workspace/[workspaceId]/home/components/user-input/components/constants' +import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' +import type { ChatContext } from '@/stores/panel' /** * Builds the single user message sent after the final question is answered: @@ -88,8 +95,11 @@ interface QuestionDisplayProps { * — it IS the user turn; the paired message bubble is hidden by the chat. */ answers?: string[] - /** Reports the combined answer; undefined renders the card inert. */ - onSelect?: (message: string) => void + /** + * Reports the combined answer, with any resources picked by + * `resource_select` steps attached as contexts; undefined renders the card inert. + */ + onSelect?: InteractionAnswerHandler /** Reports that the active card was dismissed so its message actions can return. */ onDismiss?: () => void /** Whether the active card can be dismissed without answering. */ @@ -102,7 +112,9 @@ interface QuestionDisplayProps { * (and a `‹ N of M ›` stepper for multi-step batches) at the top right, and * suggested-action option rows beneath, always followed by a custom-answer * text field whose placeholder reads "Something else". `single_select` - * answers and advances on click (or on submitting typed text); `multi_select` + * answers and advances on click (or on submitting typed text); + * `resource_select` does the same over the workspace's live resources of one + * family, with the text field filtering them; `multi_select` * rows toggle checkboxes and an option-styled Submit row confirms the step. * Answering the last question sends one combined user message and collapses * the div to a question/answer recap. @@ -121,6 +133,10 @@ export function QuestionDisplay({ const [step, setStep] = useState(0) const [selectedByStep, setSelectedByStep] = useState(() => data.map(() => [])) const [customByStep, setCustomByStep] = useState(() => data.map(() => '')) + // resource_select only: the resource behind a step's picked row, sent as a + // chat context so the agent gets its id, not just the displayed name. + // Only read on submit, so it never needs to re-render the card. + const resourceByStep = useRef<(MothershipResource | null)[]>(data.map(() => null)) const [freeText, setFreeText] = useState('') // multi_select only: whether the typed "Something else" text is included in // the answer. Unchecking keeps the text; it just stops counting. @@ -157,7 +173,8 @@ export function QuestionDisplay({ const question = data[step] const isLast = step === data.length - 1 - const options = question.options + const isResource = question.type === 'resource_select' + const options = isResource ? [] : question.options const selected = selectedByStep[step] ?? [] const isMulti = question.type === 'multi_select' @@ -183,21 +200,26 @@ export function QuestionDisplay({ return } setPhase('answered') + const contexts = resourceByStep.current + .map((resource) => (resource ? mapResourceToContext(resource) : null)) + .filter((context): context is ChatContext => context !== null) onSelect?.( formatQuestionAnswerMessage( data, data.map((q, i) => answerFor(q, selections[i] ?? [], customFor(i, customs))) - ) + ), + contexts.length > 0 ? contexts : undefined ) } - const handleSingleSelect = (label: string) => { + const handleSingleSelect = (label: string, resource: MothershipResource | null = null) => { const selections = [...selectedByStep] selections[step] = [label] setSelectedByStep(selections) const customs = [...customByStep] customs[step] = '' setCustomByStep(customs) + resourceByStep.current[step] = resource setFreeText('') finishStep(selections, customs) } @@ -239,6 +261,7 @@ export function QuestionDisplay({ const selections = [...selectedByStep] selections[step] = [] setSelectedByStep(selections) + resourceByStep.current[step] = null finishStep(selections, customs) } @@ -316,6 +339,14 @@ export function QuestionDisplay({ } >
+ {question.type === 'resource_select' && ( + handleSingleSelect(resource.title, resource)} + /> + )} {options.map((option, i) => { const isSelected = selected.includes(option.label) return ( @@ -343,7 +374,7 @@ export function QuestionDisplay({ })} 0} + divided={options.length > 0 || isResource} leading={ isMulti ? (
@@ -389,7 +420,7 @@ export function QuestionDisplay({ } type='text' value={freeText} - placeholder='Something else' + placeholder={isResource ? 'Search or type something else' : 'Something else'} disabled={disabled} onFocus={() => { if (isMulti) setCustomChecked(true) @@ -438,9 +469,12 @@ export function QuestionDisplay({ /** * A step's combined answer: selected option labels in option order, with the * typed "Something else" entry appended last. single_select carries at most - * one selection, so this collapses to the chosen label or the typed text. + * one selection, so this collapses to the chosen label or the typed text; a + * resource_select pick is not one of the question's options, so it is the + * picked resource's name or the typed text. */ function answerFor(question: QuestionItem, selected: string[], custom: string): string { + if (question.type === 'resource_select') return selected[0] ?? custom.trim() const ordered = question.options .map((option) => option.label) .filter((label) => selected.includes(label)) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question-rows.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question-rows.tsx new file mode 100644 index 00000000000..b7177cb2d11 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question-rows.tsx @@ -0,0 +1,124 @@ +'use client' + +import { useMemo } from 'react' +import { ArrowRight, cn } from '@sim/emcn' +import { useParams } from 'next/navigation' +import { INTERACTION_CARD_ROW_CLASSES } from '@/app/workspace/[workspaceId]/home/components/message-content/components/interaction-card' +import type { QuestionResourceType } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { resourceFromItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' +import type { AvailableItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/resource-folder-tree' +import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' +import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' +import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' +import { useTablesList } from '@/hooks/queries/tables' +import { useWorkflows } from '@/hooks/queries/workflows' +import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' + +/** + * The current workspace's live resources of one family. Only the requested + * family's list is fetched; the others stay disabled. Outside a workspace (an + * organization chat) there is nothing to list and the card falls back to its + * free-text row. + */ +function useResourceCandidates(type: QuestionResourceType): { + candidates: AvailableItem[] + isPending: boolean +} { + const { workspaceId } = useParams<{ workspaceId?: string }>() + const has = Boolean(workspaceId) + const workflows = useWorkflows(workspaceId, { enabled: has && type === 'workflow' }) + const tables = useTablesList(workspaceId, 'active', { enabled: has && type === 'table' }) + const files = useWorkspaceFiles(workspaceId ?? '', 'active', { enabled: has && type === 'file' }) + const knowledgeBases = useKnowledgeBasesQuery(workspaceId, { + enabled: has && type === 'knowledgebase', + }) + const query = { workflow: workflows, table: tables, file: files, knowledgebase: knowledgeBases }[ + type + ] + const candidates = useMemo( + () => (query.data ?? []).map((item) => ({ id: item.id, name: item.name })), + [query.data] + ) + return { candidates, isPending: has && query.isPending } +} + +/** Rows mounted at once; the scroll box shows about five, and typing searches the rest. */ +const PREVIEW_LIMIT = 50 + +interface ResourceQuestionRowsProps { + resourceType: QuestionResourceType + /** The card's free-text entry, which doubles as the list's search filter. */ + query: string + disabled: boolean + onPick: (resource: MothershipResource) => void +} + +/** + * Option rows for a `resource_select` question: the workspace's resources of + * the requested family, rendered with the same row the add-resource menu uses + * and filtered by what the user types into the card's free-text row. + */ +export function ResourceQuestionRows({ + resourceType, + query, + disabled, + onPick, +}: ResourceQuestionRowsProps) { + const { candidates, isPending } = useResourceCandidates(resourceType) + const config = getResourceConfig(resourceType) + const needle = query.trim().toLowerCase() + const matches = useMemo( + () => + needle + ? candidates.filter((candidate) => candidate.name.toLowerCase().includes(needle)) + : candidates, + [candidates, needle] + ) + const visible = matches.slice(0, PREVIEW_LIMIT) + const label = config.label.toLowerCase() + + if (visible.length === 0) { + return ( +

+ {emptyMessage(label, isPending, needle.length > 0)} +

+ ) + } + + return ( +
+ {visible.map((candidate, i) => ( + + ))} + {matches.length > visible.length && ( +

+ {`${matches.length - visible.length} more ${label} — type to search`} +

+ )} +
+ ) +} + +function emptyMessage(label: string, isPending: boolean, searching: boolean): string { + if (isPending) return `Loading ${label}…` + if (searching) return `No matching ${label}` + return `No ${label} yet` +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question.test.tsx new file mode 100644 index 00000000000..c102bfbda90 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/resource-question.test.tsx @@ -0,0 +1,175 @@ +/** + * @vitest-environment jsdom + */ + +import { act } from 'react' +import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockUseWorkflows, mockUseTablesList } = vi.hoisted(() => ({ + mockUseWorkflows: vi.fn(), + mockUseTablesList: vi.fn(), +})) + +vi.mock('next/navigation', () => nextNavigationMock) +vi.mock('@/hooks/queries/workflows', () => ({ useWorkflows: mockUseWorkflows })) +vi.mock('@/hooks/queries/tables', () => ({ useTablesList: mockUseTablesList })) +vi.mock('@/hooks/queries/workspace-files', () => ({ + useWorkspaceFiles: () => ({ data: undefined, isPending: false }), +})) +vi.mock('@/hooks/queries/kb/knowledge', () => ({ + useKnowledgeBasesQuery: () => ({ data: undefined, isPending: false }), +})) + +import { + parseQuestionAnswerMessage, + QuestionDisplay, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/question' +import type { + InteractionAnswerHandler, + QuestionItem, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' + +const WORKFLOWS = Array.from({ length: 11 }, (_, i) => ({ + id: `wf-${i + 1}`, + name: `Workflow ${i + 1}`, +})) + +const PICK_WORKFLOW: QuestionItem = { + type: 'resource_select', + prompt: 'Which workflow should I improve?', + resourceType: 'workflow', +} + +let container: HTMLDivElement +let root: Root + +function render(data: QuestionItem[], onSelect?: InteractionAnswerHandler) { + act(() => { + root.render() + }) +} + +function rowLabels(): string[] { + return Array.from(container.querySelectorAll('button')) + .map((button) => button.textContent?.trim() ?? '') + .filter((text) => text.startsWith('Workflow ')) +} + +function typeInto(input: HTMLInputElement, value: string) { + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + act(() => { + setter?.call(input, value) + input.dispatchEvent(new Event('input', { bubbles: true })) + }) +} + +function clickRow(label: string) { + const row = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === label + ) + if (!row) throw new Error(`no row ${label}`) + act(() => { + row.click() + }) +} + +describe('resource_select question', () => { + beforeEach(() => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + nextNavigationMockFns.mockUseParams.mockReturnValue({ workspaceId: 'ws-1' }) + mockUseWorkflows.mockReturnValue({ data: WORKFLOWS, isPending: false }) + mockUseTablesList.mockReturnValue({ data: undefined, isPending: false }) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + act(() => root.unmount()) + container.remove() + vi.clearAllMocks() + }) + + it('lists every workspace resource of the family, past the four-option cap', () => { + render([PICK_WORKFLOW], vi.fn()) + expect(rowLabels()).toEqual(WORKFLOWS.map((workflow) => workflow.name)) + expect(mockUseWorkflows).toHaveBeenCalledWith('ws-1', { enabled: true }) + expect(mockUseTablesList).toHaveBeenCalledWith('ws-1', 'active', { enabled: false }) + }) + + it('mounts a bounded preview of a large workspace and searches the rest', () => { + const many = Array.from({ length: 60 }, (_, i) => ({ + id: `wf-${i + 1}`, + name: `Workflow ${i + 1}`, + })) + mockUseWorkflows.mockReturnValue({ data: many, isPending: false }) + render([PICK_WORKFLOW], vi.fn()) + expect(rowLabels()).toHaveLength(50) + expect(container.textContent).toContain('10 more workflows — type to search') + + typeInto(container.querySelector('input') as HTMLInputElement, 'workflow 60') + expect(rowLabels()).toEqual(['Workflow 60']) + }) + + it('answers with the picked name and attaches the resource as a chat context', () => { + const onSelect = vi.fn() + render([PICK_WORKFLOW], onSelect) + clickRow('Workflow 7') + + expect(onSelect).toHaveBeenCalledTimes(1) + const [message, contexts] = onSelect.mock.calls[0] + expect(message).toBe('Which workflow should I improve? — Workflow 7') + expect(contexts).toEqual([{ kind: 'workflow', workflowId: 'wf-7', label: 'Workflow 7' }]) + expect(parseQuestionAnswerMessage([PICK_WORKFLOW], message)).toEqual(['Workflow 7']) + }) + + it('filters by the typed text and sends unmatched text as a plain answer', () => { + const onSelect = vi.fn() + render([PICK_WORKFLOW], onSelect) + const input = container.querySelector('input') as HTMLInputElement + + typeInto(input, 'workflow 1') + expect(rowLabels()).toEqual(['Workflow 1', 'Workflow 10', 'Workflow 11']) + + typeInto(input, 'the billing one') + expect(container.textContent).toContain('No matching workflows') + act(() => { + container.querySelector('button[aria-label="Submit answer"]')?.click() + }) + expect(onSelect).toHaveBeenCalledWith( + 'Which workflow should I improve? — the billing one', + undefined + ) + }) + + it('carries only the picked resources through a mixed multi-step card', () => { + const onSelect = vi.fn() + const confirm: QuestionItem = { + type: 'single_select', + prompt: 'Deploy after the change?', + options: [ + { id: 'yes', label: 'Yes' }, + { id: 'no', label: 'No' }, + ], + } + render([PICK_WORKFLOW, confirm], onSelect) + clickRow('Workflow 2') + clickRow('No') + + expect(onSelect).toHaveBeenCalledWith( + 'Which workflow should I improve? — Workflow 2\nDeploy after the change? — No', + [{ kind: 'workflow', workflowId: 'wf-2', label: 'Workflow 2' }] + ) + }) + + it('renders the answered recap from the transcript', () => { + act(() => { + root.render() + }) + expect(container.textContent).toContain('Which workflow should I improve?') + expect(container.textContent).toContain('Workflow 3') + expect(rowLabels()).toEqual([]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts index 2f43b29539e..a52a786f3a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts @@ -6,13 +6,14 @@ export type { CredentialTagData, CredentialTagType, FileTagData, + InteractionAnswerHandler, MothershipErrorTagData, OptionsTagData, ParsedSpecialContent, QuestionItem, QuestionOption, + QuestionResourceType, QuestionTagData, - QuestionType, RuntimeSpecialTagName, SourceTagData, UsageUpgradeAction, @@ -38,7 +39,6 @@ export { parseSpecialTags, parseTagAttributes, parseTextTagBody, - QUESTION_TYPES, SpecialTags, USAGE_UPGRADE_ACTIONS, WORKSPACE_RESOURCE_TAG_TYPES, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 00894654cba..c3248083719 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -183,6 +183,27 @@ describe('parseQuestionTagBody', () => { } expect(parseQuestionTagBody(JSON.stringify(onlyOther))).toBe(null) }) + + it('accepts a resource_select question without options and drops stray fields', () => { + const pick = { type: 'resource_select', prompt: 'Which workflow?', resourceType: 'workflow' } + expect( + parseQuestionTagBody(JSON.stringify({ ...pick, options: [{ id: 'x', label: 'X' }] })) + ).toEqual([pick]) + expect(parseQuestionTagBody(JSON.stringify([pick, SINGLE_SELECT]))).toEqual([ + pick, + SINGLE_SELECT, + ]) + }) + + it('rejects a resource_select question without a listable resource type', () => { + for (const resourceType of [undefined, 'dashboard', 'credential', 42]) { + expect( + parseQuestionTagBody( + JSON.stringify({ type: 'resource_select', prompt: 'Which one?', resourceType }) + ) + ).toBe(null) + } + }) }) describe('parseSpecialTags with ', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index fd03be2d91a..c1ddbfafc96 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -117,6 +117,7 @@ import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' import { useWorkspaceUsageGate } from '@/hooks/queries/workspace-usage' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' +import type { ChatContext } from '@/stores/panel' export interface OptionsItemData { title: string @@ -333,9 +334,14 @@ export interface FileTagData { content: string } -export const QUESTION_TYPES = ['single_select', 'multi_select'] as const +const CHOICE_QUESTION_TYPES = ['single_select', 'multi_select'] as const -export type QuestionType = (typeof QUESTION_TYPES)[number] +type ChoiceQuestionType = (typeof CHOICE_QUESTION_TYPES)[number] + +/** Workspace resource families a `resource_select` question can list. */ +const QUESTION_RESOURCE_TYPES = ['workflow', 'table', 'file', 'knowledgebase'] as const + +export type QuestionResourceType = (typeof QUESTION_RESOURCE_TYPES)[number] export interface QuestionOption { id: string @@ -343,17 +349,38 @@ export interface QuestionOption { } /** - * One question in a `` tag: a single_select or multi_select with at - * least one real option. The card always appends its own free-text "Something - * else" row, so agent-supplied catch-all options ("Other", "Something else", - * ...) are stripped during parsing. + * A single_select or multi_select question with at least one real option. The + * card always appends its own free-text "Something else" row, so + * agent-supplied catch-all options ("Other", "Something else", ...) are + * stripped during parsing. */ -export interface QuestionItem { - type: QuestionType +interface ChoiceQuestionItem { + type: ChoiceQuestionType prompt: string options: QuestionOption[] } +/** + * Picks one existing workspace resource. The card lists and searches the + * workspace's live resources of `resourceType` itself, so the agent supplies + * no options and the choice is not capped at what fits in a handful of rows. + */ +interface ResourceQuestionItem { + type: 'resource_select' + prompt: string + resourceType: QuestionResourceType +} + +/** One question in a `` tag. */ +export type QuestionItem = ChoiceQuestionItem | ResourceQuestionItem + +/** + * Sends an interactive card's answer as the user's next message. A card that + * picked workspace resources attaches them as chat contexts, the same way a + * typed mention would, so the agent receives their ids alongside the answer. + */ +export type InteractionAnswerHandler = (message: string, contexts?: ChatContext[]) => void + /** Normalized `` payload: single-object bodies become a one-element array. */ export type QuestionTagData = QuestionItem[] @@ -701,13 +728,19 @@ const SELF_PROVIDED_OPTION_LABELS = new Set([ function isQuestionItem(value: unknown): value is QuestionItem { if (!isRecordLike(value)) return false + if (typeof value.prompt !== 'string' || value.prompt.trim().length === 0) return false + if (value.type === 'resource_select') { + return ( + typeof value.resourceType === 'string' && + (QUESTION_RESOURCE_TYPES as readonly string[]).includes(value.resourceType) + ) + } if ( typeof value.type !== 'string' || - !(QUESTION_TYPES as readonly string[]).includes(value.type) + !(CHOICE_QUESTION_TYPES as readonly string[]).includes(value.type) ) { return false } - if (typeof value.prompt !== 'string' || value.prompt.trim().length === 0) return false return ( Array.isArray(value.options) && value.options.length > 0 && @@ -715,8 +748,14 @@ function isQuestionItem(value: unknown): value is QuestionItem { ) } -/** Strips agent-supplied catch-all options; null when none remain. */ +/** + * Strips agent-supplied catch-all options; null when none remain. A resource + * question keeps only its own fields, so stray agent keys never reach the card. + */ function sanitizeQuestionItem(item: QuestionItem): QuestionItem | null { + if (item.type === 'resource_select') { + return { type: item.type, prompt: item.prompt, resourceType: item.resourceType } + } const options = item.options.filter( (option) => !SELF_PROVIDED_OPTION_LABELS.has(option.label.trim().toLowerCase()) ) @@ -1799,7 +1838,7 @@ interface SpecialTagsProps { credentialSubmission?: CredentialSubmissionPayload /** The user moved on without submitting this message's credential card. */ credentialAbandoned?: boolean - onOptionSelect?: (id: string) => void + onOptionSelect?: InteractionAnswerHandler onQuestionDismiss?: () => void onWorkspaceResourceSelect?: (resource: WorkspaceResourceRef) => void } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index e04ea68b9bd..b017c37c36d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -37,7 +37,10 @@ import { ownsTurnWait, type TurnLiveIndicators, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/lane-activity' -import type { CredentialSubmissionPayload } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import type { + CredentialSubmissionPayload, + InteractionAnswerHandler, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' import { WatchActivity } from '@/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity' import { collectMessageSources } from '@/app/workspace/[workspaceId]/home/components/message-content/message-sources' import { resolveMessageCitations } from '@/app/workspace/[workspaceId]/home/components/message-content/resolve-citations' @@ -960,7 +963,7 @@ interface MessageContentProps { credentialSubmission?: CredentialSubmissionPayload /** The user moved on without submitting this message's credential card. */ credentialAbandoned?: boolean - onOptionSelect?: (id: string) => void + onOptionSelect?: InteractionAnswerHandler onQuestionDismiss?: () => void onPhaseChange?: (phase: MessagePhase) => void /** diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index f93063bd6be..771635a9d41 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -39,6 +39,7 @@ import { parseQuestionAnswerMessage } from '@/app/workspace/[workspaceId]/home/c import { type CredentialSubmissionPayload, credentialTagHasVisibleCard, + type InteractionAnswerHandler, parseCredentialSubmissionProgress, parseLastCredentialTag, parseLastQuestionTag, @@ -247,7 +248,7 @@ interface AssistantMessageRowProps { /** The user moved on without submitting this message's credential card. */ credentialAbandoned?: boolean rowClassName: string - onOptionSelect?: (id: string) => void + onOptionSelect?: InteractionAnswerHandler onAnimatingChange?: (animating: boolean) => void } @@ -739,8 +740,8 @@ export function MothershipChat({ useEffect(() => { onSubmitRef.current = onSubmit }, [onSubmit]) - const stableOnOptionSelect = useCallback((id: string) => { - onSubmitRef.current(id) + const stableOnOptionSelect = useCallback((message, contexts) => { + onSubmitRef.current(message, undefined, contexts) }, []) const handleSendQueuedHead = useCallback(() => { diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index 84e2a8a407d..f0052769874 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -464,10 +464,8 @@ "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#MothershipErrorTagData", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#OptionsTagData", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#ParsedSpecialContent", - "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#QUESTION_TYPES", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#QuestionOption", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#QuestionTagData", - "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#QuestionType", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#RuntimeSpecialTagName", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#USAGE_UPGRADE_ACTIONS", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts#UsageUpgradeAction", @@ -488,9 +486,7 @@ "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#MothershipErrorTagData", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#OptionsItemData", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#OptionsTagData", - "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#QUESTION_TYPES", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#QuestionOption", - "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#QuestionType", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#RuntimeSpecialTagName", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#SECRET_INPUT_SCOPES", "apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx#SecretInputScope",