2026-01-15 23:40:21 +08:00
|
|
|
import type { Message } from "@langchain/langgraph-sdk";
|
2026-01-18 11:25:46 +08:00
|
|
|
import { memo, useMemo } from "react";
|
2026-01-15 23:40:21 +08:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Message as AIElementMessage,
|
|
|
|
|
MessageContent as AIElementMessageContent,
|
|
|
|
|
MessageResponse as AIElementMessageResponse,
|
2026-01-16 19:51:39 +08:00
|
|
|
MessageToolbar,
|
2026-01-15 23:40:21 +08:00
|
|
|
} from "@/components/ai-elements/message";
|
2026-01-17 18:02:19 +08:00
|
|
|
import {
|
|
|
|
|
extractContentFromMessage,
|
2026-01-18 11:25:46 +08:00
|
|
|
extractReasoningContentFromMessage,
|
2026-01-17 18:02:19 +08:00
|
|
|
} from "@/core/messages/utils";
|
2026-01-15 23:40:21 +08:00
|
|
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
2026-01-16 19:51:39 +08:00
|
|
|
import { CopyButton } from "../copy-button";
|
2026-01-15 23:40:21 +08:00
|
|
|
|
|
|
|
|
export function MessageListItem({
|
|
|
|
|
className,
|
|
|
|
|
message,
|
|
|
|
|
isLoading,
|
|
|
|
|
}: {
|
|
|
|
|
className?: string;
|
|
|
|
|
message: Message;
|
|
|
|
|
isLoading?: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<AIElementMessage
|
2026-01-16 20:06:30 +08:00
|
|
|
className={cn("group/conversation-message relative w-full", className)}
|
2026-01-15 23:40:21 +08:00
|
|
|
from={message.type === "human" ? "user" : "assistant"}
|
|
|
|
|
>
|
|
|
|
|
<MessageContent
|
2026-01-16 20:06:30 +08:00
|
|
|
className={message.type === "human" ? "w-fit" : "w-full"}
|
2026-01-15 23:40:21 +08:00
|
|
|
message={message}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
/>
|
2026-01-16 19:51:39 +08:00
|
|
|
<MessageToolbar
|
|
|
|
|
className={cn(
|
|
|
|
|
message.type === "human" && "justify-end",
|
|
|
|
|
message.type === "human" ? "-bottom-9" : "-bottom-8",
|
|
|
|
|
"absolute right-0 left-0 z-20 opacity-0 transition-opacity delay-200 duration-300 group-hover/conversation-message:opacity-100",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex gap-1">
|
2026-01-18 11:25:46 +08:00
|
|
|
<CopyButton
|
|
|
|
|
clipboardData={
|
|
|
|
|
extractContentFromMessage(message)
|
|
|
|
|
? extractContentFromMessage(message)
|
|
|
|
|
: (extractReasoningContentFromMessage(message) ?? "")
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-01-16 19:51:39 +08:00
|
|
|
</div>
|
|
|
|
|
</MessageToolbar>
|
2026-01-15 23:40:21 +08:00
|
|
|
</AIElementMessage>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MessageContent_({
|
|
|
|
|
className,
|
|
|
|
|
message,
|
|
|
|
|
isLoading = false,
|
|
|
|
|
}: {
|
|
|
|
|
className?: string;
|
|
|
|
|
message: Message;
|
|
|
|
|
isLoading?: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
2026-01-18 11:25:46 +08:00
|
|
|
const content = useMemo(() => {
|
|
|
|
|
const reasoningContent = extractReasoningContentFromMessage(message);
|
|
|
|
|
const content = extractContentFromMessage(message);
|
|
|
|
|
if (!isLoading && reasoningContent && !content) {
|
|
|
|
|
return reasoningContent;
|
|
|
|
|
}
|
|
|
|
|
return content;
|
|
|
|
|
}, [isLoading, message]);
|
2026-01-15 23:40:21 +08:00
|
|
|
return (
|
|
|
|
|
<AIElementMessageContent className={className}>
|
|
|
|
|
<AIElementMessageResponse rehypePlugins={rehypePlugins}>
|
2026-01-18 11:25:46 +08:00
|
|
|
{content}
|
2026-01-15 23:40:21 +08:00
|
|
|
</AIElementMessageResponse>
|
|
|
|
|
</AIElementMessageContent>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const MessageContent = memo(MessageContent_);
|