66 lines
3.9 KiB
TypeScript
66 lines
3.9 KiB
TypeScript
import { useCallback, useRef, type ComponentProps, type FormEvent, type HTMLAttributes, type KeyboardEventHandler, type MouseEvent } from "react";
|
|
import type { ChatStatus } from "ai";
|
|
import { CornerDownLeftIcon, SquareIcon, XIcon } from "lucide-react";
|
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from "@/shared/ui/input-group";
|
|
import { Spinner } from "@/shared/ui/spinner";
|
|
import { cn } from "@/shared/ui/cn";
|
|
|
|
export type PromptInputMessage = { text: string };
|
|
export type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> & {
|
|
onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
};
|
|
|
|
/** Text composition only. The owning panel controls drafts and restores failed submissions. */
|
|
export function PromptInput({ className, onSubmit, children, ...props }: PromptInputProps) {
|
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
const text = String(new FormData(form).get("message") ?? "");
|
|
form.reset();
|
|
// Keep submission after the form event, matching the controlled panel's draft lifecycle.
|
|
await Promise.resolve();
|
|
try { await onSubmit({ text }, event); }
|
|
catch { /* The owner keeps the draft and exposes its submission error. */ }
|
|
};
|
|
return <form className={cn("w-full", className)} onSubmit={handleSubmit} {...props}>
|
|
<InputGroup className="overflow-hidden">{children}</InputGroup>
|
|
</form>;
|
|
}
|
|
|
|
export function PromptInputBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
return <div className={cn("contents", className)} {...props} />;
|
|
}
|
|
|
|
export function PromptInputTextarea({ onKeyDown, onCompositionStart, onCompositionEnd, className, placeholder = "What would you like to know?", ...props }: ComponentProps<typeof InputGroupTextarea>) {
|
|
const composing = useRef(false);
|
|
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
|
|
onKeyDown?.(event);
|
|
if (event.defaultPrevented || event.key !== "Enter" || event.shiftKey || composing.current || event.nativeEvent.isComposing) return;
|
|
event.preventDefault();
|
|
const form = event.currentTarget.form;
|
|
const submit = form?.querySelector<HTMLButtonElement>('button[type="submit"]');
|
|
if (!submit?.disabled) form?.requestSubmit();
|
|
};
|
|
return <InputGroupTextarea className={cn("field-sizing-content max-h-48 min-h-16", className)}
|
|
name="message" placeholder={placeholder} {...props} onKeyDown={handleKeyDown}
|
|
onCompositionStart={(event) => { composing.current = true; onCompositionStart?.(event); }}
|
|
onCompositionEnd={(event) => { composing.current = false; onCompositionEnd?.(event); }} />;
|
|
}
|
|
|
|
export function PromptInputFooter({ className, ...props }: Omit<ComponentProps<typeof InputGroupAddon>, "align">) {
|
|
return <InputGroupAddon align="block-end" className={cn("justify-between gap-1", className)} {...props} />;
|
|
}
|
|
|
|
export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & { status?: ChatStatus; onStop?: () => void };
|
|
export function PromptInputSubmit({ className, variant = "default", size = "icon-sm", status, onStop, onClick, children, ...props }: PromptInputSubmitProps) {
|
|
const generating = status === "submitted" || status === "streaming";
|
|
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
|
if (generating && onStop) { event.preventDefault(); onStop(); }
|
|
else onClick?.(event);
|
|
}, [generating, onStop, onClick]);
|
|
const icon = status === "submitted" ? <Spinner /> : status === "streaming" ? <SquareIcon className="size-4" />
|
|
: status === "error" ? <XIcon className="size-4" /> : <CornerDownLeftIcon className="size-4" />;
|
|
return <InputGroupButton aria-label={generating ? "Stop" : "Submit"} className={className} onClick={handleClick}
|
|
size={size} type={generating && onStop ? "button" : "submit"} variant={variant} {...props}>{children ?? icon}</InputGroupButton>;
|
|
}
|