import { useBreakpoint } from 'common' import { AnimatePresence, motion } from 'framer-motion' import { ArrowUp, Loader2, Square } from 'lucide-react' import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, ReactNode, useRef } from 'react' import { Button, cn, ExpandingTextArea } from 'ui' import { ButtonTooltip } from '../ButtonTooltip' import { formatAttachedSnippets } from './AIAssistant.utils' import { ModelSelector } from './ModelSelector' import { SnippetRow } from './SnippetRow' import type { AssistantModelId } from '@/lib/ai/model.utils' import { type SqlSnippet } from '@/state/ai-assistant-state' export interface FormProps { /* The ref for the textarea, optional. Exposed for the CommandsPopover to attach events. */ textAreaRef?: React.RefObject /* The loading state of the form */ loading: boolean /* The disabled state of the form */ disabled?: boolean /* The value of the textarea */ value?: string /* The function to handle the value change */ onValueChange: (value: ChangeEvent) => void /** * If true, include SQL snippets in the message sent to onSubmit */ includeSnippetsInMessage?: boolean /** * The function to handle the form submission */ onSubmit: (message: string) => void /** * The function to handle stopping the stream */ onStop?: () => void /* The placeholder of the textarea */ placeholder?: string /* SQL snippets to display above the form - can be strings or objects with label and content */ sqlSnippets?: SqlSnippet[] /* Function to handle removing a SQL snippet */ onRemoveSnippet?: (index: number) => void /* Additional class name for the snippets container */ snippetsClassName?: string /* Additional class name for the form wrapper */ className?: string /* If currently editing an existing message */ isEditing?: boolean /* The currently selected AI model */ selectedModel?: AssistantModelId /* Callback when a model is chosen */ onSelectModel?: (model: AssistantModelId) => void /* Icon for the submit button when not loading, defaults to an up arrow */ submitIcon?: ReactNode /* Label for the submit button when not loading, used for both aria-label and tooltip text, defaults to "Send message" */ submitLabel?: string /** * An additional action shown beside the main submit button, transitioned in/out with * framer-motion. Pass undefined to hide it (e.g. when the input doesn't match its condition). */ secondaryAction?: { label: string onClick: () => void } } const AssistantChatFormComponent = forwardRef( ( { loading = false, disabled = false, value = '', textAreaRef, onValueChange, onSubmit, onStop, placeholder, sqlSnippets, onRemoveSnippet, snippetsClassName, includeSnippetsInMessage = false, className, isEditing = false, selectedModel, onSelectModel, submitIcon = , submitLabel = 'Send message', secondaryAction, ...props }, _ref ) => { const formRef = useRef(null) const isMobile = useBreakpoint('md') const handleSubmit = (event?: FormEvent) => { if (event) event.preventDefault() if (disabled || !value || (loading && !isEditing)) return let finalMessage = value if (includeSnippetsInMessage && sqlSnippets && sqlSnippets.length > 0) { finalMessage = [value, formatAttachedSnippets(sqlSnippets)].filter(Boolean).join('\n\n') } onSubmit(finalMessage) } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() handleSubmit() } else if (event.key === 'Escape') { event.currentTarget.blur() } } const canSubmit = !disabled && !loading && !!value const showModelSelector = selectedModel !== undefined && onSelectModel !== undefined return (
{sqlSnippets && sqlSnippets.length > 0 && ( )} 0 && 'pt-10' )} placeholder={placeholder} spellCheck={false} rows={3} value={value} onChange={(event) => onValueChange(event)} onKeyDown={handleKeyDown} />
{showModelSelector && (
)}
{!loading && secondaryAction && ( )} {loading ? ( onStop ? ( } onClick={onStop} className="w-7 h-7 rounded-full p-0 text-center flex items-center justify-center" tooltip={{ content: { side: 'top', text: 'Stop response' } }} /> ) : ( ) ) : ( )}
) } ) AssistantChatFormComponent.displayName = 'AssistantChatFormComponent' export const AssistantChatForm = memo(AssistantChatFormComponent)