mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-28 16:24:47 +08:00
feat: enhance message display
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { Message } from "@langchain/langgraph-sdk";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
import { memo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message as AIElementMessage,
|
Message as AIElementMessage,
|
||||||
@@ -9,25 +9,20 @@ import {
|
|||||||
} from "@/components/ai-elements/message";
|
} from "@/components/ai-elements/message";
|
||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
hasReasoning,
|
extractReasoningContentFromMessage,
|
||||||
hasToolCalls,
|
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { CopyButton } from "../copy-button";
|
import { CopyButton } from "../copy-button";
|
||||||
|
|
||||||
import { MessageGroup } from "./message-group";
|
|
||||||
|
|
||||||
export function MessageListItem({
|
export function MessageListItem({
|
||||||
className,
|
className,
|
||||||
message,
|
message,
|
||||||
messagesInGroup,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
message: Message;
|
message: Message;
|
||||||
messagesInGroup: Message[];
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -38,7 +33,6 @@ export function MessageListItem({
|
|||||||
<MessageContent
|
<MessageContent
|
||||||
className={message.type === "human" ? "w-fit" : "w-full"}
|
className={message.type === "human" ? "w-fit" : "w-full"}
|
||||||
message={message}
|
message={message}
|
||||||
messagesInGroup={messagesInGroup}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
<MessageToolbar
|
<MessageToolbar
|
||||||
@@ -49,7 +43,13 @@ export function MessageListItem({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<CopyButton clipboardData={extractContentFromMessage(message)} />
|
<CopyButton
|
||||||
|
clipboardData={
|
||||||
|
extractContentFromMessage(message)
|
||||||
|
? extractContentFromMessage(message)
|
||||||
|
: (extractReasoningContentFromMessage(message) ?? "")
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MessageToolbar>
|
</MessageToolbar>
|
||||||
</AIElementMessage>
|
</AIElementMessage>
|
||||||
@@ -59,26 +59,26 @@ export function MessageListItem({
|
|||||||
function MessageContent_({
|
function MessageContent_({
|
||||||
className,
|
className,
|
||||||
message,
|
message,
|
||||||
messagesInGroup,
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
message: Message;
|
message: Message;
|
||||||
messagesInGroup: Message[];
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
|
const content = useMemo(() => {
|
||||||
|
const reasoningContent = extractReasoningContentFromMessage(message);
|
||||||
|
const content = extractContentFromMessage(message);
|
||||||
|
if (!isLoading && reasoningContent && !content) {
|
||||||
|
return reasoningContent;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}, [isLoading, message]);
|
||||||
return (
|
return (
|
||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
{hasReasoning(message) && (
|
|
||||||
<MessageGroup messages={messagesInGroup} isLoading={isLoading} />
|
|
||||||
)}
|
|
||||||
<AIElementMessageResponse rehypePlugins={rehypePlugins}>
|
<AIElementMessageResponse rehypePlugins={rehypePlugins}>
|
||||||
{extractContentFromMessage(message)}
|
{content}
|
||||||
</AIElementMessageResponse>
|
</AIElementMessageResponse>
|
||||||
{hasToolCalls(message) && (
|
|
||||||
<MessageGroup messages={messagesInGroup} isLoading={isLoading} />
|
|
||||||
)}
|
|
||||||
</AIElementMessageContent>
|
</AIElementMessageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
extractPresentFilesFromMessage,
|
extractPresentFilesFromMessage,
|
||||||
groupMessages,
|
groupMessages,
|
||||||
hasContent,
|
|
||||||
hasPresentFiles,
|
hasPresentFiles,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import type { AgentThreadState } from "@/core/threads";
|
import type { AgentThreadState } from "@/core/threads";
|
||||||
@@ -39,28 +38,26 @@ export function MessageList({
|
|||||||
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-10 pt-12">
|
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-10 pt-12">
|
||||||
{groupMessages(
|
{groupMessages(
|
||||||
thread.messages,
|
thread.messages,
|
||||||
(groupedMessages) => {
|
(group) => {
|
||||||
if (groupedMessages[0] && hasContent(groupedMessages[0])) {
|
if (group.type === "human" || group.type === "assistant") {
|
||||||
const message = groupedMessages[0];
|
|
||||||
return (
|
return (
|
||||||
<MessageListItem
|
<MessageListItem
|
||||||
key={message.id}
|
key={group.id}
|
||||||
message={message}
|
message={group.messages[0]!}
|
||||||
messagesInGroup={groupedMessages}
|
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (groupedMessages[0] && hasPresentFiles(groupedMessages[0])) {
|
if (group.type === "assistant:present-files") {
|
||||||
const files = [];
|
const files = [];
|
||||||
for (const message of groupedMessages) {
|
for (const message of group.messages) {
|
||||||
if (hasPresentFiles(message)) {
|
if (hasPresentFiles(message)) {
|
||||||
files.push(...extractPresentFilesFromMessage(message));
|
files.push(...extractPresentFilesFromMessage(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
key={groupedMessages[0].id}
|
key={group.id}
|
||||||
files={files}
|
files={files}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
/>
|
/>
|
||||||
@@ -68,8 +65,8 @@ export function MessageList({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MessageGroup
|
<MessageGroup
|
||||||
key={groupedMessages[0]!.id}
|
key={group.id}
|
||||||
messages={groupedMessages}
|
messages={group.messages}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,58 +1,110 @@
|
|||||||
import type { Message } from "@langchain/langgraph-sdk";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
|
interface GenericMessageGroup<T = string> {
|
||||||
|
type: T;
|
||||||
|
id: string | undefined;
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HumanMessageGroup extends GenericMessageGroup<"human"> {}
|
||||||
|
|
||||||
|
interface AssistantProcessingGroup extends GenericMessageGroup<"assistant:processing"> {}
|
||||||
|
|
||||||
|
interface AssistantMessageGroup extends GenericMessageGroup<"assistant"> {}
|
||||||
|
|
||||||
|
interface AssistantPresentFilesGroup extends GenericMessageGroup<"assistant:present-files"> {}
|
||||||
|
|
||||||
|
type MessageGroup =
|
||||||
|
| HumanMessageGroup
|
||||||
|
| AssistantProcessingGroup
|
||||||
|
| AssistantMessageGroup
|
||||||
|
| AssistantPresentFilesGroup;
|
||||||
|
|
||||||
export function groupMessages<T>(
|
export function groupMessages<T>(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
mapper: (groupedMessages: Message[]) => T,
|
mapper: (group: MessageGroup) => T,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
): T[] {
|
): T[] {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const groups: Message[][] = [];
|
const groups: MessageGroup[] = [];
|
||||||
let currentGroup: Message[] = [];
|
|
||||||
const yieldCurrentGroup = () => {
|
|
||||||
if (currentGroup.length > 0) {
|
|
||||||
groups.push(currentGroup);
|
|
||||||
currentGroup = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let messageIndex = 0;
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
|
const lastGroup = groups[groups.length - 1];
|
||||||
if (message.type === "human") {
|
if (message.type === "human") {
|
||||||
// Human messages are always shown as a individual group
|
groups.push({
|
||||||
yieldCurrentGroup();
|
id: message.id,
|
||||||
currentGroup.push(message);
|
type: "human",
|
||||||
yieldCurrentGroup();
|
messages: [message],
|
||||||
|
});
|
||||||
} else if (message.type === "tool") {
|
} else if (message.type === "tool") {
|
||||||
// Tool messages are always shown with the assistant messages that contains the tool calls
|
|
||||||
currentGroup.push(message);
|
|
||||||
} else if (message.type === "ai") {
|
|
||||||
if (
|
if (
|
||||||
hasToolCalls(message) ||
|
lastGroup &&
|
||||||
(extractTextFromMessage(message) === "" &&
|
lastGroup.type !== "human" &&
|
||||||
extractReasoningContentFromMessage(message) !== "" &&
|
lastGroup.type !== "assistant"
|
||||||
messageIndex === messages.length - 1 &&
|
|
||||||
isLoading)
|
|
||||||
) {
|
) {
|
||||||
if (message.tool_calls?.[0]?.name === "present_files") {
|
lastGroup.messages.push(message);
|
||||||
// When `present_files` called, put them into an individual group
|
|
||||||
yieldCurrentGroup();
|
|
||||||
currentGroup.push(message);
|
|
||||||
} else {
|
} else {
|
||||||
// Assistant messages without any content are folded into the previous group
|
throw new Error(
|
||||||
// Normally, these are tool calls (with or without thinking)
|
"Tool message must be matched with a previous assistant message with tool calls",
|
||||||
currentGroup.push(message);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (message.type === "ai") {
|
||||||
|
if (hasReasoning(message) || hasToolCalls(message)) {
|
||||||
|
if (hasPresentFiles(message)) {
|
||||||
|
groups.push({
|
||||||
|
id: message.id,
|
||||||
|
type: "assistant:present-files",
|
||||||
|
messages: [message],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Assistant messages with content (text or images) are shown as a group if they have content
|
if (lastGroup?.type !== "assistant:processing") {
|
||||||
// No matter whether it has tool calls or not
|
groups.push({
|
||||||
yieldCurrentGroup();
|
id: message.id,
|
||||||
currentGroup.push(message);
|
type: "assistant:processing",
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentGroup = groups[groups.length - 1];
|
||||||
|
if (currentGroup?.type === "assistant:processing") {
|
||||||
|
currentGroup.messages.push(message);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Assistant message with reasoning or tool calls must be preceded by a processing group",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasContent(message) && !hasToolCalls(message)) {
|
||||||
|
groups.push({
|
||||||
|
id: message.id,
|
||||||
|
type: "assistant",
|
||||||
|
messages: [message],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading) {
|
||||||
|
const lastGroup: MessageGroup | undefined = groups[groups.length - 1];
|
||||||
|
if (
|
||||||
|
lastGroup?.type === "assistant:processing" &&
|
||||||
|
lastGroup.messages.length > 0
|
||||||
|
) {
|
||||||
|
const reasoningContent = extractReasoningContentFromMessage(
|
||||||
|
lastGroup.messages[lastGroup.messages.length - 1]!,
|
||||||
|
);
|
||||||
|
const content = extractContentFromMessage(
|
||||||
|
lastGroup.messages[lastGroup.messages.length - 1]!,
|
||||||
|
);
|
||||||
|
if (reasoningContent && !content) {
|
||||||
|
const group = groups.pop()!;
|
||||||
|
group.type = "assistant";
|
||||||
|
groups.push(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messageIndex++;
|
|
||||||
}
|
}
|
||||||
yieldCurrentGroup();
|
|
||||||
|
|
||||||
const resultsOfGroups: T[] = [];
|
const resultsOfGroups: T[] = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
|
|||||||
Reference in New Issue
Block a user