import { IconArrowUp, IconPhotoPlus, IconX } from "@tabler/icons-react" import { type ClipboardEvent as ReactClipboardEvent, type DragEvent as ReactDragEvent, type KeyboardEvent as ReactKeyboardEvent, useRef, } from "react" import { useTranslation } from "react-i18next" import TextareaAutosize from "react-textarea-autosize" import { ContextUsageRing } from "@/components/chat/context-usage-ring" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import type { ChatAttachment, ContextUsage } from "@/store/chat" export type ChatInputDisabledReason = | "gatewayUnknown" | "gatewayStarting" | "gatewayRestarting" | "gatewayStopping" | "gatewayStopped" | "gatewayError" | "websocketConnecting" | "websocketDisconnected" | "websocketError" | "noDefaultModel" interface ChatComposerProps { input: string attachments: ChatAttachment[] onInputChange: (value: string) => void onAddImages: () => void onPaste: (event: ReactClipboardEvent) => void onDragEnter: (event: ReactDragEvent) => void onDragLeave: (event: ReactDragEvent) => void onDragOver: (event: ReactDragEvent) => void onDrop: (event: ReactDragEvent) => void onRemoveAttachment: (index: number) => void onSend: () => void onContextDetail?: () => void inputDisabledReason: ChatInputDisabledReason | null canSend: boolean isDragActive: boolean contextUsage?: ContextUsage } export function ChatComposer({ input, attachments, onInputChange, onAddImages, onPaste, onDragEnter, onDragLeave, onDragOver, onDrop, onRemoveAttachment, onSend, onContextDetail, inputDisabledReason, canSend, isDragActive, contextUsage, }: ChatComposerProps) { const { t } = useTranslation() const canInput = inputDisabledReason === null const composingRef = useRef(false) const hasInput = input.trim().length > 0 const disabledMessage = inputDisabledReason === null ? null : t(`chat.disabledPlaceholder.${inputDisabledReason}`) const placeholder = disabledMessage ?? t("chat.placeholder") const handleKeyDown = (e: ReactKeyboardEvent) => { const nativeEvent = e.nativeEvent as Event & { isComposing?: boolean keyCode?: number } if ( composingRef.current || nativeEvent.isComposing || nativeEvent.keyCode === 229 ) { return } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() onSend() } } return (
{isDragActive && (
{t("chat.dropImagesActive")}
)} {attachments.length > 0 && (
{attachments.map((attachment, index) => (
{attachment.filename
))}
)} onInputChange(e.target.value)} onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }} onPaste={onPaste} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={!canInput} title={disabledMessage || undefined} className={cn( "placeholder:text-muted-foreground/50 max-h-[200px] min-h-[64px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent", !canInput && "cursor-not-allowed", )} minRows={1} maxRows={8} />
{contextUsage && ( )} {canInput ? ( {t("chat.sendHint")} ) : null}
{t("chat.composeHint")}
) }