lovart3/components/message-editor.tsx
2025-12-17 20:15:18 +08:00

147 lines
4.1 KiB
TypeScript

"use client";
import type { UseChatHelpers } from "@ai-sdk/react";
import {
type Dispatch,
type SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import type { ChatMessage, Attachment } from "@/lib/types";
import { getTextFromMessage } from "@/lib/utils";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
import { PreviewAttachment } from "./preview-attachment";
export type MessageEditorProps = {
message: ChatMessage;
setMode: Dispatch<SetStateAction<"view" | "edit">>;
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
};
export function MessageEditor({
message,
setMode,
setMessages,
regenerate,
sendMessage,
}: MessageEditorProps) {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [draftContent, setDraftContent] = useState<string>(
getTextFromMessage(message)
);
// 🆕 管理可编辑的附件列表
const [attachments, setAttachments] = useState<Attachment[]>(() => {
// 从消息的 parts 中提取图片附件
return message.parts
.filter((part): part is Extract<typeof part, { type: "file" }> =>
part.type === "file" &&
'mediaType' in part &&
typeof part.mediaType === 'string' &&
part.mediaType.startsWith("image/")
)
.map((part) => ({
url: part.url,
name: part.filename ?? part.url.split("/").pop() ?? "image",
contentType: part.mediaType ?? "image/jpeg",
}));
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const adjustHeight = useCallback(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`;
}
}, []);
useEffect(() => {
if (textareaRef.current) {
adjustHeight();
}
}, [adjustHeight]);
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setDraftContent(event.target.value);
adjustHeight();
};
return (
<div className="flex w-full flex-col gap-2">
{/* 🆕 显示附件列表(可删除)*/}
{attachments.length > 0 && (
<div className="flex flex-wrap gap-2">
{attachments.map((attachment, index) => (
<PreviewAttachment
key={attachment.url}
attachment={attachment}
onRemove={() => {
setAttachments((prev) => prev.filter((_, i) => i !== index));
}}
/>
))}
</div>
)}
<Textarea
className="w-full resize-none overflow-hidden rounded-xl bg-transparent text-base! outline-hidden"
data-testid="message-editor"
onChange={handleInput}
ref={textareaRef}
value={draftContent}
/>
<div className="flex flex-row justify-end gap-2">
<Button
className="h-fit px-3 py-2"
onClick={() => {
setMode("view");
}}
variant="outline"
>
取消
</Button>
<Button
className="h-fit px-3 py-2"
data-testid="message-editor-send-button"
disabled={isSubmitting}
onClick={async () => {
setIsSubmitting(true);
setMode("view");
// 🆕 将编辑后的内容作为新消息追加到对话框末尾
// 使用与正常发送消息相同的格式
sendMessage({
role: "user",
parts: [
...attachments.map((att) => ({
type: "file" as const,
url: att.url,
name: att.name,
mediaType: att.contentType,
})),
{
type: "text",
text: draftContent,
},
],
});
setIsSubmitting(false);
}}
variant="default"
>
{isSubmitting ? "正在发送..." : "重新生成"}
</Button>
</div>
</div>
);
}