"use client";
import { ArrowUp, Paperclip, Square, X } from "lucide-react";
import { useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { MAX_CHAT_MESSAGE_CHARACTERS } from "@/lib/chat/request-limits";
import type { Attachment } from "@/lib/chat/types";
import { authFetch } from "@/lib/auth/client";
type ChatComposerProps = {
isGenerating: boolean;
onSend: (content: string, attachments?: Attachment[]) => Promise<void> | void;
onStop: () => void;
quotaExhausted: boolean;
};
export function ChatComposer({
isGenerating,
onSend,
onStop,
quotaExhausted,
}: ChatComposerProps) {
const [content, setContent] = useState("");
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [isUploading, setIsUploading] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const messageLength = content.trim().length;
const isTooLong = messageLength > MAX_CHAT_MESSAGE_CHARACTERS;
const canSend = (messageLength > 0 || attachments.length > 0) && !isTooLong && !isGenerating && !isUploading && !quotaExhausted;
function resize(event: ChangeEvent<HTMLTextAreaElement>) {
const textarea = event.currentTarget;
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
}
function submit() {
const message = content.trim();
if ((!message && attachments.length === 0) || message.length > MAX_CHAT_MESSAGE_CHARACTERS || isGenerating || isUploading || quotaExhausted) {
return;
}
setContent("");
const submittedAttachments = attachments;
setAttachments([]);
if (textareaRef.current) textareaRef.current.style.height = "auto";
void (submittedAttachments.length
? onSend(message, submittedAttachments)
: onSend(message));
}
async function uploadFiles(files: FileList | null) {
if (!files?.length || isUploading) return;
setIsUploading(true);
try {
for (const file of Array.from(files)) {
const form = new FormData();
form.set("file", file);
const response = await authFetch("/api/attachments", { method: "POST", body: form });
if (!response.ok) throw new Error("upload failed");
const body = (await response.json()) as { attachment: Attachment };
setAttachments((current) => [...current, body.attachment]);
}
} finally {
setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = "";
}
}
function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit();
}
}
return (
<div className="shrink-0 bg-background px-3 pb-3 md:px-5 md:pb-4">
<div className="chat-readable mx-auto">
<div className="chat-composer-surface flex items-end gap-2 rounded-2xl border bg-card p-2 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30">
<input accept="image/png,image/jpeg,image/webp,application/pdf,text/plain,text/markdown,.md,.docx" aria-label="Прикрепить файлы" className="sr-only" multiple onChange={(event) => void uploadFiles(event.target.files)} ref={fileInputRef} type="file" />
<Button aria-label="Прикрепить файлы" className="mb-1 rounded-full" disabled={isGenerating || isUploading} onClick={() => fileInputRef.current?.click()} size="icon" type="button"><Paperclip aria-hidden="true" /></Button>
<Textarea
aria-describedby={isTooLong ? "chat-message-limit" : undefined}
aria-invalid={isTooLong}
aria-label="Сообщение"
className="max-h-[200px] min-h-11 flex-1 border-0 px-2.5 py-2.5 shadow-none focus-visible:border-transparent focus-visible:ring-0"
disabled={isGenerating}
onChange={(event) => {
setContent(event.target.value);
resize(event);
}}
onKeyDown={handleKeyDown}
placeholder="Напишите сообщение"
ref={textareaRef}
rows={1}
value={content}
/>
{isGenerating ? (
<Button
aria-label="Остановить генерацию"
className="mb-1 rounded-full"
onClick={onStop}
size="icon"
type="button"
>
<Square aria-hidden="true" className="size-3 fill-current" />
</Button>
) : (
<Button
aria-label="Отправить сообщение"
className="mb-1 rounded-full"
disabled={!canSend}
onClick={submit}
size="icon"
type="button"
>
<ArrowUp aria-hidden="true" />
</Button>
)}
</div>
{attachments.length ? <div className="mt-2 flex flex-wrap gap-2">{attachments.map((attachment) => <span className="flex items-center gap-1 rounded-lg border px-2 py-1 text-xs" key={attachment.id}>{attachment.name}<button aria-label={`Удалить ${attachment.name}`} onClick={() => setAttachments((current) => current.filter((item) => item.id !== attachment.id))} type="button"><X className="size-3" /></button></span>)}</div> : null}
{isUploading ? <p className="mt-2 text-xs text-muted-foreground">Загружаю вложение…</p> : null}
{quotaExhausted ? (
<p className="mt-2 text-xs text-destructive" role="status">
Лимит токенов исчерпан. Отправка недоступна.
</p>
) : null}
{isTooLong ? (
<p
className="mt-1.5 px-2 text-xs text-destructive"
id="chat-message-limit"
role="alert"
>
Сообщение не должно превышать 32 000 символов.
</p>
) : null}
<p className="mt-2 text-center text-[11px] leading-4 text-muted-foreground">
ИИ может допускать ошибки. Проверяйте важную информацию.
</p>
</div>
</div>
);
}