diff --git a/apps/api/src/tasks/dto/task-responses.dto.ts b/apps/api/src/tasks/dto/task-responses.dto.ts index 28a240d96..3870fe307 100644 --- a/apps/api/src/tasks/dto/task-responses.dto.ts +++ b/apps/api/src/tasks/dto/task-responses.dto.ts @@ -77,4 +77,12 @@ export class TaskResponseDto { example: '2024-01-15T10:30:00Z', }) updatedAt: Date; + + @ApiProperty({ + description: 'Task template ID', + example: 'frk_tt_68406e353df3bc002994acef', + nullable: true, + required: false, + }) + taskTemplateId?: string | null; } diff --git a/apps/api/src/tasks/tasks.service.ts b/apps/api/src/tasks/tasks.service.ts index 375c64e74..12c6d06a5 100644 --- a/apps/api/src/tasks/tasks.service.ts +++ b/apps/api/src/tasks/tasks.service.ts @@ -29,6 +29,7 @@ export class TasksService { status: task.status, createdAt: task.createdAt, updatedAt: task.updatedAt, + taskTemplateId: task.taskTemplateId, })); } catch (error) { console.error('Error fetching tasks:', error); diff --git a/apps/app/package.json b/apps/app/package.json index 4fa4694a8..e2476b603 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -62,6 +62,7 @@ "@vercel/sandbox": "^0.0.21", "@vercel/sdk": "^1.7.1", "ai": "^5.0.60", + "ai-elements": "^1.6.1", "axios": "^1.9.0", "better-auth": "^1.3.27", "botid": "^1.5.5", diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/EmptyState.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/EmptyState.tsx index ca8988a78..8f6651b6f 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/EmptyState.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/EmptyState.tsx @@ -258,7 +258,7 @@ export function EmptyState({ onBack, connectedProviders = [], onConnected }: Emp // AWS Step 2.5: Region Selection (after credential validation) if (step === 'validate-aws' && provider && selectedProvider === 'aws') { return ( -
+
+ )} + +
+
+ + + + , + document.body, ); } const renderStatusContent = () => { if (!run && !error) { return ( -
- -
-

Initializing Status

-

- Checking the current onboarding status... +

+ +
+

Initializing...

+

+ Checking onboarding status

@@ -99,15 +277,21 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => } if (!run) { return ( -
- {/* Use theme warning color */} -
-

Status Unavailable

{' '} - {/* Use theme warning color */} -

- Could not retrieve current onboarding status. +

+ +
+

Status Unavailable

+

+ Could not retrieve status

+
); } @@ -122,34 +306,354 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => case 'DEQUEUED': case 'DELAYED': return ( -
- -
- - - {PROGRESS_MESSAGES[currentMessageIndex]} - - -

- We are setting up your organization. This may take a few moments. -

+
+ {/* Header */} +
+
+ +

+ Setting up your organization +

+
+ +
+ + {/* Step progress - scrollable */} +
+ {ONBOARDING_STEPS.map((step) => { + const isCompleted = stepStatus[step.key as keyof typeof stepStatus]; + const isCurrent = currentStep?.key === step.key; + const isVendorsStep = step.key === 'vendors'; + const isRisksStep = step.key === 'risk'; + const isPoliciesStep = step.key === 'policies'; + + // Vendors step with expandable dropdown + if (isVendorsStep && stepStatus.vendorsTotal > 0) { + return ( +
+ + + {/* Expanded vendor list */} + {isVendorsExpanded && stepStatus.vendorsInfo.length > 0 && ( + +
+ {stepStatus.vendorsInfo.map((vendor) => { + const vendorStatus = stepStatus.vendorsStatus[vendor.id] || 'pending'; + const isVendorCompleted = vendorStatus === 'completed'; + const isVendorProcessing = vendorStatus === 'processing'; + + return ( +
+ {isVendorCompleted ? ( + + ) : isVendorProcessing ? ( + + ) : ( +
+ )} + + {vendor.name} + +
+ ); + })} +
+ + )} +
+ ); + } + + // Risks step with expandable dropdown + if (isRisksStep && stepStatus.risksTotal > 0) { + return ( +
+ + + {/* Expanded risk list */} + {isRisksExpanded && stepStatus.risksInfo.length > 0 && ( + +
+ {stepStatus.risksInfo.map((risk) => { + const riskStatus = stepStatus.risksStatus[risk.id] || 'pending'; + const isRiskCompleted = riskStatus === 'completed'; + const isRiskProcessing = riskStatus === 'processing'; + + return ( +
+ {isRiskCompleted ? ( + + ) : isRiskProcessing ? ( + + ) : ( +
+ )} + + {risk.name} + +
+ ); + })} +
+ + )} +
+ ); + } + + if (isPoliciesStep && stepStatus.policiesTotal > 0) { + // Policies step with expandable dropdown + return ( +
+ + + {/* Expanded policy list */} + {isPoliciesExpanded && stepStatus.policiesInfo.length > 0 && ( + +
+ {stepStatus.policiesInfo.map((policy) => { + const policyStatus = stepStatus.policiesStatus[policy.id] || 'pending'; + const isPolicyCompleted = policyStatus === 'completed'; + const isPolicyProcessing = policyStatus === 'processing'; + + return ( +
+ {isPolicyCompleted ? ( + + ) : isPolicyProcessing ? ( + + ) : ( +
+ )} + + {policy.name} + +
+ ); + })} +
+ + )} +
+ ); + } + + // Regular step + return ( +
+ {isCompleted ? ( + + ) : isCurrent ? ( + + ) : ( +
+ )} + + {step.label} + +
+ ); + })}
); case 'COMPLETED': return ( -
- -
-

Setup Complete

-

Your organization is ready.

+
+ {/* Header */} +
+
+ +

+ Setup Complete +

+
+ +
+ +
+
+

Your organization is ready!

+

+ All onboarding steps have been completed successfully. +

+
+
+ + {/* Show completed steps */} +
+ {ONBOARDING_STEPS.map((step) => ( +
+ + + {step.label} + +
+ ))}
); @@ -161,16 +665,23 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => case 'TIMED_OUT': { const errorMessage = run.error?.message || 'An unexpected issue occurred.'; const truncatedMessage = - errorMessage.length > 100 ? `${errorMessage.substring(0, 97)}...` : errorMessage; + errorMessage.length > 60 ? `${errorMessage.substring(0, 57)}...` : errorMessage; return ( -
- {' '} -
+
+ +

Setup {friendlyStatus}

-

{truncatedMessage}

+

{truncatedMessage}

+
); } @@ -178,32 +689,43 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => const exhaustiveCheck: never = run.status as never; return ( -
- -
+
+ +

Unknown Status

-

- Received an unhandled status: {exhaustiveCheck} +

+ Status: {exhaustiveCheck}

+
); } } }; - if (run?.status === 'COMPLETED') { - return null; - } - - return ( - - -
{renderStatusContent()}
-
-
+ + +
+ {renderStatusContent()} +
+
+
+ , + document.body, ); }; diff --git a/apps/app/src/app/(app)/[orgId]/integrations/actions/get-relevant-tasks.ts b/apps/app/src/app/(app)/[orgId]/integrations/actions/get-relevant-tasks.ts new file mode 100644 index 000000000..14b67aee1 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/integrations/actions/get-relevant-tasks.ts @@ -0,0 +1,70 @@ +'use server'; + +import { openai } from '@ai-sdk/openai'; +import { generateObject } from 'ai'; +import { z } from 'zod'; + +const RelevantTasksSchema = z.object({ + relevantTasks: z.array( + z.object({ + taskTemplateId: z.string(), + taskName: z.string(), + reason: z.string(), + prompt: z.string(), + }), + ), +}); + +export async function getRelevantTasksForIntegration( + integrationName: string, + integrationDescription: string, + taskTemplates: Array<{ id: string; name: string; description: string }>, +): Promise<{ taskTemplateId: string; taskName: string; reason: string; prompt: string }[]> { + if (taskTemplates.length === 0) { + return []; + } + + // Format task templates for the prompt (truncate descriptions to reduce token usage) + const tasksList = taskTemplates + .map((task) => { + // Truncate description to max 100 chars to reduce token usage + const truncatedDesc = + task.description.length > 100 + ? task.description.substring(0, 100) + '...' + : task.description; + return `${task.id}|${task.name}|${truncatedDesc}`; + }) + .join('\n'); + + const systemPrompt = `GRC expert. Find tasks relevant to integration. Return JSON with: taskTemplateId, taskName, reason (1 sentence), prompt (actionable, ready to use). Be selective.`; + + const userPrompt = `Integration: ${integrationName} - ${integrationDescription} + +Tasks (format: ID|Name|Description): +${tasksList} + +Return only clearly relevant tasks.`; + + const promptSize = (systemPrompt + userPrompt).length; + console.log(`[getRelevantTasks] Prompt size: ${promptSize} chars, ${taskTemplates.length} tasks`); + + try { + const startTime = Date.now(); + const { object, usage } = await generateObject({ + model: openai('gpt-4.1-mini'), + schema: RelevantTasksSchema, + system: systemPrompt, + prompt: userPrompt, + }); + const duration = Date.now() - startTime; + + console.log( + `[getRelevantTasks] Generated ${object.relevantTasks.length} tasks in ${duration}ms (tokens: ${usage?.totalTokens || 'unknown'})`, + ); + + return object.relevantTasks; + } catch (error) { + console.error('Error generating relevant tasks:', error); + return []; + } +} diff --git a/apps/app/src/app/(app)/[orgId]/integrations/components/IntegrationsGrid.tsx b/apps/app/src/app/(app)/[orgId]/integrations/components/IntegrationsGrid.tsx index c73fae35a..eb9cb771d 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/components/IntegrationsGrid.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/components/IntegrationsGrid.tsx @@ -1,5 +1,6 @@ 'use client'; +import { api } from '@/lib/api-client'; import { Badge } from '@comp/ui/badge'; import { Button } from '@comp/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; @@ -10,12 +11,14 @@ import { DialogHeader, DialogTitle, } from '@comp/ui/dialog'; -import { ArrowRight, Sparkles } from 'lucide-react'; +import { Skeleton } from '@comp/ui/skeleton'; +import { ArrowRight, CheckCircle2, Loader2, Plug, Sparkles, Zap } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; -import { useParams } from 'next/navigation'; -import { useMemo, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; +import { getRelevantTasksForIntegration } from '../actions/get-relevant-tasks'; import { CATEGORIES, INTEGRATIONS, @@ -26,11 +29,113 @@ import { SearchInput } from './SearchInput'; const LOGO_TOKEN = 'pk_AZatYxV5QDSfWpRDaBxzRQ'; -export function IntegrationsGrid() { +interface RelevantTask { + taskTemplateId: string; + taskName: string; + reason: string; + prompt: string; +} + +function TaskCard({ task, orgId }: { task: RelevantTask; orgId: string }) { + const [isNavigating, setIsNavigating] = useState(false); + const router = useRouter(); + + const handleCardClick = async () => { + setIsNavigating(true); + toast.loading('Opening task automation...', { id: 'navigating' }); + + try { + // Fetch all tasks and find one with matching template ID + const response = await api.get>( + '/v1/tasks', + orgId, + ); + + if (response.error || !response.data) { + throw new Error(response.error || 'Failed to fetch tasks'); + } + + // Debug logging + console.log('Looking for taskTemplateId:', task.taskTemplateId); + console.log( + 'Available tasks:', + response.data.map((t) => ({ id: t.id, taskTemplateId: t.taskTemplateId })), + ); + + const matchingTask = response.data.find( + (t) => t.taskTemplateId && t.taskTemplateId === task.taskTemplateId, + ); + + if (!matchingTask) { + toast.dismiss('navigating'); + toast.error(`Task "${task.taskName}" not found. Please create it first.`); + setIsNavigating(false); + await router.push(`/${orgId}/tasks`); + return; + } + + const url = `/${orgId}/tasks/${matchingTask.id}/automation/new?prompt=${encodeURIComponent(task.prompt)}`; + toast.dismiss('navigating'); + toast.success('Redirecting...', { duration: 1000 }); + + // Use window.location for immediate navigation + window.location.href = url; + } catch (error) { + console.error('Error finding task:', error); + toast.dismiss('navigating'); + toast.error('Failed to find task'); + setIsNavigating(false); + } + }; + + return ( +
+ {isNavigating && ( +
+ +
+

Opening task...

+

+ Redirecting to automation with prompt pre-filled +

+
+
+ )} +
+
+
+
+
+
+

+ {task.taskName} +

+
+

+ {task.reason} +

+
+ +
+
+
+ ); +} + +export function IntegrationsGrid({ + taskTemplates, +}: { + taskTemplates: Array<{ id: string; name: string; description: string }>; +}) { const { orgId } = useParams<{ orgId: string }>(); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('All'); const [selectedIntegration, setSelectedIntegration] = useState(null); + const [relevantTasks, setRelevantTasks] = useState([]); + const [isLoadingTasks, setIsLoadingTasks] = useState(false); // Filter integrations with fuzzy search const filteredIntegrations = useMemo(() => { @@ -62,24 +167,48 @@ export function IntegrationsGrid() { toast.success('Prompt copied to clipboard!'); }; + useEffect(() => { + if (selectedIntegration && orgId && taskTemplates.length > 0) { + setIsLoadingTasks(true); + getRelevantTasksForIntegration( + selectedIntegration.name, + selectedIntegration.description, + taskTemplates, + ) + .then((tasks) => { + setRelevantTasks(tasks); + }) + .catch((error) => { + console.error('Error fetching relevant tasks:', error); + setRelevantTasks([]); + }) + .finally(() => { + setIsLoadingTasks(false); + }); + } else { + setRelevantTasks([]); + } + }, [selectedIntegration, orgId, taskTemplates]); + return (
{/* Search and Filters */} -
+
{/* Search Bar */} - {/* Category Filters */} -
+ {/* Category Filters - Horizontal scroll on mobile */} +
@@ -89,6 +218,7 @@ export function IntegrationsGrid() { size="sm" variant={selectedCategory === category ? 'default' : 'outline'} onClick={() => setSelectedCategory(category)} + className="flex-shrink-0 whitespace-nowrap min-w-fit px-4" > {category} @@ -232,74 +362,121 @@ export function IntegrationsGrid() { {/* Integration Detail Modal */} {selectedIntegration && ( setSelectedIntegration(null)}> - - -
-
- {`${selectedIntegration.name} -
-
- {selectedIntegration.name} -

- {selectedIntegration.category} -

-
+ +
+ {/* Header with gradient background */} +
+ +
+
+ {`${selectedIntegration.name} +
+
+
+ + {selectedIntegration.name} + + {selectedIntegration.popular && ( + + Popular + + )} +
+
+ + {selectedIntegration.category} + + + Integration +
+
+
+ + {selectedIntegration.description} + +
- - {selectedIntegration.description} - - -
- {/* Setup Instructions */} -
-

How to Connect

-
-

- Use the example prompts below, or describe what you need in your own words. The - agent will handle authentication and setup. -

- {selectedIntegration.setupHint && ( -

- Typically requires:{' '} - {selectedIntegration.setupHint} +

+ {/* Setup Instructions */} +
+
+
+ +
+

How to Connect

+
+
+

+ Click on any relevant task below to create an automation with{' '} + {selectedIntegration.name}. The automation will be pre-configured with a + prompt tailored to that task. The agent will ask you for the necessary + permissions and API keys if required.

- )} + {selectedIntegration.setupHint && ( +
+ +

+ Typically requires:{' '} + {selectedIntegration.setupHint} +

+
+ )} +
-
- {/* Example Prompts */} -
-

Example Prompts

-
- {selectedIntegration.examplePrompts.map((prompt, index) => ( - - ))} -
-
- Click any prompt to copy - - Go to Tasks - - +
+ )}
diff --git a/apps/app/src/app/(app)/[orgId]/integrations/components/SearchInput.tsx b/apps/app/src/app/(app)/[orgId]/integrations/components/SearchInput.tsx index 1f57da770..41e90f8dc 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/components/SearchInput.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/components/SearchInput.tsx @@ -12,25 +12,28 @@ interface SearchInputProps { export function SearchInput({ value, onChange, placeholder, className }: SearchInputProps) { return ( -
-
- +
+
+
+ +
+ onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-10 pr-10 py-2 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground" + /> + {value && ( + + )}
- onChange(e.target.value)} - placeholder={placeholder} - className="pl-10 pr-10" - /> - {value && ( - - )}
); } diff --git a/apps/app/src/app/(app)/[orgId]/integrations/page.tsx b/apps/app/src/app/(app)/[orgId]/integrations/page.tsx index 36b219929..267b10c06 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/page.tsx @@ -1,24 +1,35 @@ +import { db } from '@db'; import { IntegrationsGrid } from './components/IntegrationsGrid'; -export default function IntegrationsPage() { +export default async function IntegrationsPage() { + // Fetch task templates server-side + const taskTemplates = await db.frameworkEditorTaskTemplate.findMany({ + select: { + id: true, + name: true, + description: true, + }, + orderBy: { + name: 'asc', + }, + }); + return ( -
-
- {/* Header */} -
-
-

Integrations

- -
-

- Connect to any system through the AI agent. This directory shows common patterns—the - agent can integrate with anything that has an API or web interface. -

+
+ {/* Header */} +
+
+

Integrations

+
- - {/* Integrations Grid */} - +

+ Connect to any system through the AI agent. This directory shows common patterns—the + agent can integrate with anything that has an API or web interface. +

+ + {/* Integrations Grid */} +
); } diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index e1521c463..c54c4ac87 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -12,6 +12,7 @@ import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { Suspense } from 'react'; import { ConditionalOnboardingTracker } from './components/ConditionalOnboardingTracker'; +import { ConditionalPaddingWrapper } from './components/ConditionalPaddingWrapper'; import { DynamicMinHeight } from './components/DynamicMinHeight'; const HotKeys = dynamic(() => import('@/components/hot-keys').then((mod) => mod.HotKeys), { @@ -92,7 +93,9 @@ export default async function Layout({ } isCollapsed={isCollapsed}> {onboarding?.triggerJobId && }
- {children} + + {children} + diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx index 060830ffd..22c8990af 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx @@ -2,8 +2,19 @@ import { cn } from '@/lib/utils'; import { useChat } from '@ai-sdk/react'; +import { + PromptInput, + PromptInputBody, + PromptInputFooter, + PromptInputProvider, + PromptInputSubmit, + PromptInputTextarea, + PromptInputTools, + usePromptInputController, +} from '@comp/ui'; import Image from 'next/image'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { useCallback, useEffect } from 'react'; import { Conversation, ConversationContent, @@ -14,7 +25,6 @@ import { EmptyState } from './components/chat/EmptyState'; import { Message } from './components/chat/message'; import type { ChatUIMessage } from './components/chat/types'; import { PanelHeader } from './components/panels/panels'; -import { Textarea } from './components/ui/textarea'; import { useChatHandlers } from './hooks/use-chat-handlers'; import { useTaskAutomation } from './hooks/use-task-automation'; import { useSharedChatContext } from './lib/chat-context'; @@ -31,6 +41,109 @@ interface Props { isLoadingSuggestions?: boolean; } +function ChatInput({ + validateAndSubmitMessage, + status, +}: { + validateAndSubmitMessage: (text: string) => void; + status: string; +}) { + const { textInput } = usePromptInputController(); + + return ( +
+ { + validateAndSubmitMessage(text); + }} + > + + + + + + + + +
+ ); +} + +function ChatContent({ + hasMessages, + scriptUrl, + messages, + orgId, + handleSecretAdded, + handleInfoProvided, + validateAndSubmitMessage, + status, + suggestions, + isLoadingSuggestions, +}: { + hasMessages: boolean; + scriptUrl?: string; + messages: ChatUIMessage[]; + orgId: string; + handleSecretAdded: (secretName: string) => void; + handleInfoProvided: (info: Record) => void; + validateAndSubmitMessage: (text: string) => void; + status: string; + suggestions?: { title: string; prompt: string; vendorName?: string; vendorWebsite?: string }[]; + isLoadingSuggestions?: boolean; +}) { + const { textInput } = usePromptInputController(); + + const handleExampleClick = useCallback( + (prompt: string) => { + textInput.setInput(prompt); + }, + [textInput], + ); + + if (!hasMessages) { + return ( +
+ validateAndSubmitMessage(textInput.value)} + suggestions={suggestions} + isLoadingSuggestions={isLoadingSuggestions} + /> +
+ ); + } + + return ( +
+ + + {messages.map((message) => ( + + ))} + + + + + +
+ ); +} + export function Chat({ className, orgId, @@ -40,27 +153,15 @@ export function Chat({ suggestions, isLoadingSuggestions = false, }: Props) { - const [input, setInput] = useState(''); + const searchParams = useSearchParams(); + const initialPrompt = searchParams.get('prompt') || ''; const { chat, updateAutomationId, automationIdRef } = useSharedChatContext(); const { messages, sendMessage, status } = useChat({ chat, }); const { setChatStatus, scriptUrl } = useTaskAutomationStore(); - const inputRef = useRef(null); const { automation } = useTaskAutomation(); - // Auto-resize textarea - const handleInputChange = useCallback( - (e: React.ChangeEvent) => { - setInput(e.target.value); - // Reset height to auto to get the correct scrollHeight - e.target.style.height = 'auto'; - // Set height to scrollHeight (content height) - e.target.style.height = `${e.target.scrollHeight}px`; - }, - [setInput], - ); - // Update shared ref when automation is loaded from hook if (automation?.id && automationIdRef.current === 'new') { automationIdRef.current = automation.id; @@ -72,7 +173,7 @@ export function Chat({ const { validateAndSubmitMessage, handleSecretAdded, handleInfoProvided } = useChatHandlers({ sendMessage, - setInput, + setInput: () => {}, // Not needed with PromptInputProvider orgId, taskId, automationId: automationIdRef.current, @@ -80,14 +181,6 @@ export function Chat({ updateAutomationId, }); - const handleExampleClick = useCallback( - (prompt: string) => { - setInput(prompt); - inputRef.current?.focus(); - }, - [setInput], - ); - useEffect(() => { setChatStatus(status); }, [status, setChatStatus]); @@ -121,71 +214,20 @@ export function Chat({ {/* Messages Area */} - {!hasMessages ? ( -
{ - event.preventDefault(); - validateAndSubmitMessage(input); - }} - > - validateAndSubmitMessage(input)} - suggestions={suggestions} - isLoadingSuggestions={isLoadingSuggestions} - /> - - ) : ( -
- - - {messages.map((message) => ( - - ))} - - - - -
{ - event.preventDefault(); - validateAndSubmitMessage(input); - }} - > -