import { For, Show } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions } from '../components/prompt-input'; import { PromptSuggestion } from '../components/prompt-suggestion'; import { SlashCommand, type SlashCommandItem } from '../components/slash-command'; import { Button } from '../ui/button'; import { Tooltip } from '../ui/tooltip'; import { Paperclip, Globe, Mic, Square } from 'lucide-solid'; import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove, type AttachmentData, } from '../components/attachments'; import { actionIcon } from '../ui/action-icons'; import type { CustomAction } from './chat-types'; export interface DefaultPromptInputProps { value: string; placeholder?: string; disabled?: boolean; loading?: boolean; suggestions?: string[]; /** Attachments staged in the input. Provide `onAttachmentsChange` to enable * the attach button + removable previews. */ attachments?: AttachmentData[]; /** Show a Search (Globe) button in the left toolbar; calls `onSearch`. */ search?: boolean; /** Show a Voice (Mic) button in the left toolbar; calls `onVoice`. */ voice?: boolean; /** Slash commands — when set, typing `/` opens the command palette. */ slashCommands?: SlashCommandItem[]; /** Currently-active command ids (highlighted in the palette). */ slashActiveIds?: string[]; /** Single-line palette rows. */ slashCompact?: boolean; onValueChange: (v: string) => void; onSubmit: () => void; onSuggestionClick: (v: string) => void; onAttachmentsChange?: (attachments: AttachmentData[]) => void; onSearch?: () => void; onVoice?: () => void; onSlashSelect?: (command: SlashCommandItem) => void; /** When `true` and `loading` is also `true`, the send button is replaced by * a Stop button that calls `onStop`. */ stoppable?: boolean; /** Called when the user clicks the Stop button. */ onStop?: () => void; /** Custom toolbar action buttons declared as `` light-DOM children. */ toolbarActions?: CustomAction[]; /** Called when a custom toolbar action button is clicked, with the action id. */ onAction?: (id: string) => void; } function fileToAttachment(file: File): AttachmentData { const id = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${file.name}-${file.size}-${file.lastModified}`; return { id, type: 'file', filename: file.name, mediaType: file.type || undefined, url: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined, }; } export function DefaultPromptInput(props: DefaultPromptInputProps) { let fileInput: HTMLInputElement | undefined; const attachments = () => props.attachments ?? []; const canAttach = () => !!props.onAttachmentsChange; const addFiles = (files: FileList | null) => { if (!files?.length || !props.onAttachmentsChange) return; props.onAttachmentsChange([...attachments(), ...Array.from(files).map(fileToAttachment)]); }; const removeAttachment = (id: string) => props.onAttachmentsChange?.(attachments().filter((a) => a.id !== id)); const sendDisabled = () => props.disabled || props.loading || (!props.value.trim() && attachments().length === 0); const showStop = () => !!props.loading && !!props.stoppable; return ( <>
{(s) => ( props.onSuggestionClick(s)}>{s} )}
{/* Rendered inside PromptInput so SlashCommand's usePromptInput() context (input value + textarea ref) resolves; the `relative` root anchors its `absolute bottom-full` palette above the input. */} props.onSlashSelect?.(command)} />
{(att) => ( removeAttachment(att.id)}> )}
{/* Consumer-injected controls rendered before the input area. Native slot; inert outside a shadow root, projected by the custom element. */}
{ addFiles(e.currentTarget.files); e.currentTarget.value = ''; // allow re-picking the same file }} /> {(action) => { const Icon = actionIcon(action.icon); const label = action.tooltip ?? action.label; const btn = ( ); return Icon ? {btn} : btn; }}
{/* Consumer-injected controls rendered after the input area, beside the send button. Native slot; projected by the custom element. */} } >
); }