147 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|