Files
next-tjwater-frontend/src/shared/ai-elements/prompt-input.tsx
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

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>;
}