Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-zl
This commit is contained in:
commit
8e77e2da1e
3 files changed
+140
-55
No files matched your search
@@ -15,7 +15,7 @@ import {
|
|||||||
SquareTerminalIcon,
|
SquareTerminalIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState, type AnchorHTMLAttributes } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChainOfThought,
|
ChainOfThought,
|
||||||
@@ -58,6 +58,7 @@ import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
|||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
import {
|
import {
|
||||||
|
applyInlineCitations,
|
||||||
buildSkillDisplayMap,
|
buildSkillDisplayMap,
|
||||||
findDisplayConfigForTool,
|
findDisplayConfigForTool,
|
||||||
isDisplaySamplePayload,
|
isDisplaySamplePayload,
|
||||||
@@ -71,6 +72,7 @@ import { env } from "@/env";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { useArtifactsOptional } from "../artifacts";
|
import { useArtifactsOptional } from "../artifacts";
|
||||||
|
import { CitationLink } from "../citations/citation-link";
|
||||||
import { FlipDisplay } from "../flip-display";
|
import { FlipDisplay } from "../flip-display";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import {
|
import {
|
||||||
@@ -79,15 +81,18 @@ import {
|
|||||||
} from "../skill-display-sources";
|
} from "../skill-display-sources";
|
||||||
|
|
||||||
import { MarkdownContent } from "./markdown-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
|
import { SingleRoundRagCitationDialog } from "./message-list-item";
|
||||||
|
|
||||||
export function MessageGroup({
|
export function MessageGroup({
|
||||||
className,
|
className,
|
||||||
messages,
|
messages,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
displaySources = [],
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
displaySources?: RuntimeDisplaySource[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showAbove, setShowAbove] = useState(
|
const [showAbove, setShowAbove] = useState(
|
||||||
@@ -124,11 +129,29 @@ export function MessageGroup({
|
|||||||
}
|
}
|
||||||
}, [lastToolCallStep, steps]);
|
}, [lastToolCallStep, steps]);
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
|
const [selectedCitation, setSelectedCitation] =
|
||||||
|
useState<RuntimeDisplaySource | null>(null);
|
||||||
|
const handleCitationClick = useCallback(
|
||||||
|
(referenceId: string) => {
|
||||||
|
const referenceIndex = Number(referenceId);
|
||||||
|
if (!Number.isInteger(referenceIndex) || referenceIndex < 1) return;
|
||||||
|
const source =
|
||||||
|
displaySources.find((item) => item.index === referenceIndex) ??
|
||||||
|
displaySources[referenceIndex - 1];
|
||||||
|
if (source) setSelectedCitation(source);
|
||||||
|
},
|
||||||
|
[displaySources],
|
||||||
|
);
|
||||||
|
const reasoningId =
|
||||||
|
lastReasoningStep?.id ??
|
||||||
|
messages.find((message) => message.type === "ai")?.id;
|
||||||
return (
|
return (
|
||||||
<ChainOfThought
|
<>
|
||||||
className={cn("w-full gap-2 rounded-lg border p-0.5", className)}
|
<ChainOfThought
|
||||||
open={true}
|
className={cn("w-full gap-2 rounded-lg border p-0.5", className)}
|
||||||
>
|
data-reference-batch-id={reasoningId}
|
||||||
|
open={true}
|
||||||
|
>
|
||||||
{aboveLastToolCallSteps.length > 0 && (
|
{aboveLastToolCallSteps.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
key="above"
|
key="above"
|
||||||
@@ -163,10 +186,12 @@ export function MessageGroup({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={step.id}
|
key={step.id}
|
||||||
label={
|
label={
|
||||||
<MarkdownContent
|
<StepReasoningMarkdown
|
||||||
content={step.reasoning ?? ""}
|
content={step.reasoning ?? ""}
|
||||||
|
displaySources={displaySources}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
|
onCitationClick={handleCitationClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
@@ -223,10 +248,12 @@ export function MessageGroup({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={lastReasoningStep.id}
|
key={lastReasoningStep.id}
|
||||||
label={
|
label={
|
||||||
<MarkdownContent
|
<StepReasoningMarkdown
|
||||||
content={lastReasoningStep.reasoning ?? ""}
|
content={lastReasoningStep.reasoning ?? ""}
|
||||||
|
displaySources={displaySources}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
|
onCitationClick={handleCitationClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
@@ -234,7 +261,60 @@ export function MessageGroup({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ChainOfThought>
|
</ChainOfThought>
|
||||||
|
<SingleRoundRagCitationDialog
|
||||||
|
source={selectedCitation}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setSelectedCitation(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepReasoningMarkdown({
|
||||||
|
content,
|
||||||
|
displaySources,
|
||||||
|
isLoading,
|
||||||
|
rehypePlugins,
|
||||||
|
onCitationClick,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
displaySources: RuntimeDisplaySource[];
|
||||||
|
isLoading: boolean;
|
||||||
|
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||||
|
onCitationClick: (referenceId: string) => void;
|
||||||
|
}) {
|
||||||
|
const markdown = useMemo(
|
||||||
|
() =>
|
||||||
|
applyInlineCitations(content, displaySources, { appendSummary: false }),
|
||||||
|
[content, displaySources],
|
||||||
|
);
|
||||||
|
const components = useMemo(() => {
|
||||||
|
if (displaySources.length === 0) return undefined;
|
||||||
|
return {
|
||||||
|
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
if (href?.startsWith("#reference-")) {
|
||||||
|
return (
|
||||||
|
<CitationLink
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
onReferenceClick={onCitationClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <a href={href} {...props} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [displaySources.length, onCitationClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarkdownContent
|
||||||
|
content={markdown}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -695,13 +695,6 @@ function MessageContent_({
|
|||||||
const withCitations = applyInlineCitations(rawContent ?? '', displaySources);
|
const withCitations = applyInlineCitations(rawContent ?? '', displaySources);
|
||||||
return stripIncompleteCitationMarker(withCitations);
|
return stripIncompleteCitationMarker(withCitations);
|
||||||
}, [rawContent, isHuman, displaySources]);
|
}, [rawContent, isHuman, displaySources]);
|
||||||
const reasoningToDisplay = useMemo(() => {
|
|
||||||
if (!reasoningContent) return '';
|
|
||||||
const withCitations = applyInlineCitations(reasoningContent, displaySources, {
|
|
||||||
appendSummary: false,
|
|
||||||
});
|
|
||||||
return stripIncompleteCitationMarker(withCitations);
|
|
||||||
}, [reasoningContent, displaySources]);
|
|
||||||
const markdownRehypePlugins = useMemo(
|
const markdownRehypePlugins = useMemo(
|
||||||
() => [...rehypePlugins, [rehypeKatex, { output: 'html' as const }]],
|
() => [...rehypePlugins, [rehypeKatex, { output: 'html' as const }]],
|
||||||
[rehypePlugins],
|
[rehypePlugins],
|
||||||
@@ -735,6 +728,23 @@ function MessageContent_({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reasoning-only AI message (no main response content yet)
|
||||||
|
if (!isHuman && reasoningContent && !rawContent && !multiRoundTrace) {
|
||||||
|
return (
|
||||||
|
<AIElementMessageContent className={className}>
|
||||||
|
<Reasoning isStreaming={isLoading}>
|
||||||
|
<ReasoningTrigger />
|
||||||
|
<ReasoningContent>{reasoningContent}</ReasoningContent>
|
||||||
|
</Reasoning>
|
||||||
|
<MessageTokenUsage
|
||||||
|
enabled={tokenUsageEnabled}
|
||||||
|
isLoading={isLoading}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
</AIElementMessageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
// Only the user's own question carries a delete affordance, and only once
|
// Only the user's own question carries a delete affordance, and only once
|
||||||
// it exists server-side (real, non-optimistic id). Deleting it cascades to
|
// it exists server-side (real, non-optimistic id). Deleting it cascades to
|
||||||
@@ -770,24 +780,6 @@ function MessageContent_({
|
|||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
{filesList}
|
{filesList}
|
||||||
<div data-reference-batch-id={message.id} className="contents">
|
<div data-reference-batch-id={message.id} className="contents">
|
||||||
{reasoningToDisplay && !multiRoundTrace && (
|
|
||||||
<Reasoning isStreaming={isLoading && !contentToDisplay}>
|
|
||||||
<ReasoningTrigger />
|
|
||||||
<ReasoningContent>
|
|
||||||
{isSingleRoundRag || displaySources.length > 0 ? (
|
|
||||||
<MarkdownContent
|
|
||||||
content={reasoningToDisplay}
|
|
||||||
isLoading={isLoading}
|
|
||||||
rehypePlugins={markdownRehypePlugins}
|
|
||||||
components={components}
|
|
||||||
className="text-muted-foreground text-sm"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
reasoningToDisplay
|
|
||||||
)}
|
|
||||||
</ReasoningContent>
|
|
||||||
</Reasoning>
|
|
||||||
)}
|
|
||||||
{multiRoundTrace ? (
|
{multiRoundTrace ? (
|
||||||
<MultiRoundRagContent
|
<MultiRoundRagContent
|
||||||
trace={multiRoundTrace}
|
trace={multiRoundTrace}
|
||||||
@@ -797,7 +789,7 @@ function MessageContent_({
|
|||||||
rehypePlugins={markdownRehypePlugins}
|
rehypePlugins={markdownRehypePlugins}
|
||||||
components={components}
|
components={components}
|
||||||
/>
|
/>
|
||||||
) : contentToDisplay ? (
|
) : (
|
||||||
<AssistantDisplayContent
|
<AssistantDisplayContent
|
||||||
content={contentToDisplay}
|
content={contentToDisplay}
|
||||||
displaySources={displaySources}
|
displaySources={displaySources}
|
||||||
@@ -806,7 +798,7 @@ function MessageContent_({
|
|||||||
rehypePlugins={markdownRehypePlugins}
|
rehypePlugins={markdownRehypePlugins}
|
||||||
components={components}
|
components={components}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
|
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
|
||||||
<FrontendRagSourcesPanel
|
<FrontendRagSourcesPanel
|
||||||
@@ -905,7 +897,7 @@ function FrontendRagSourcesPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SingleRoundRagCitationDialog({
|
export function SingleRoundRagCitationDialog({
|
||||||
source,
|
source,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -322,6 +322,32 @@ export function MessageList({
|
|||||||
return map;
|
return map;
|
||||||
}, [thread.messages]);
|
}, [thread.messages]);
|
||||||
|
|
||||||
|
const resolveDisplaySources = useCallback(
|
||||||
|
(msg: Message | undefined): RuntimeDisplaySource[] => {
|
||||||
|
if (!referencesEnabled || !msg || msg.type !== "ai") return [];
|
||||||
|
const batchSources =
|
||||||
|
referenceBatches.find((batch) => batch.assistant_message_id === msg.id)
|
||||||
|
?.sources ?? [];
|
||||||
|
const frontendRagSources = msg.id
|
||||||
|
? frontendRagSourcesByAiId.get(msg.id) ?? []
|
||||||
|
: [];
|
||||||
|
if (batchSources.length > 0) return batchSources;
|
||||||
|
if (frontendRagSources.length > 0) return frontendRagSources;
|
||||||
|
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||||
|
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
||||||
|
beforeMessageId: msg.id,
|
||||||
|
includeBeforeMessage: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
frontendRagSourcesByAiId,
|
||||||
|
messages,
|
||||||
|
referenceBatches,
|
||||||
|
referencesEnabled,
|
||||||
|
skillDisplayMap,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Track which clarification messages are still awaiting an answer.
|
// Track which clarification messages are still awaiting an answer.
|
||||||
// Pending = clarification tool messages that appear after the last human message
|
// Pending = clarification tool messages that appear after the last human message
|
||||||
// (no user reply has been sent for them yet).
|
// (no user reply has been sent for them yet).
|
||||||
@@ -457,26 +483,8 @@ export function MessageList({
|
|||||||
referencesEnabled &&
|
referencesEnabled &&
|
||||||
group.type === "assistant" &&
|
group.type === "assistant" &&
|
||||||
msg.type === "ai";
|
msg.type === "ai";
|
||||||
const batchSources = isAssistantMessage
|
|
||||||
? referenceBatches.find(
|
|
||||||
(batch) => batch.assistant_message_id === msg.id,
|
|
||||||
)?.sources ?? []
|
|
||||||
: [];
|
|
||||||
// 单轮/多轮 RAG 的参考资料来自隐藏的 tool 消息,优先于通用扫描。
|
|
||||||
const frontendRagSources =
|
|
||||||
isAssistantMessage && msg.id
|
|
||||||
? frontendRagSourcesByAiId.get(msg.id) ?? []
|
|
||||||
: [];
|
|
||||||
const displaySources = isAssistantMessage
|
const displaySources = isAssistantMessage
|
||||||
? batchSources.length > 0
|
? resolveDisplaySources(msg)
|
||||||
? batchSources
|
|
||||||
: frontendRagSources.length > 0
|
|
||||||
? frontendRagSources
|
|
||||||
: collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
|
||||||
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
|
||||||
beforeMessageId: msg.id,
|
|
||||||
includeBeforeMessage: true,
|
|
||||||
})
|
|
||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<MessageListItem
|
<MessageListItem
|
||||||
@@ -683,6 +691,7 @@ export function MessageList({
|
|||||||
key={"thinking-group-" + message.id}
|
key={"thinking-group-" + message.id}
|
||||||
messages={[message]}
|
messages={[message]}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
|
displaySources={resolveDisplaySources(message)}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -726,11 +735,15 @@ export function MessageList({
|
|||||||
message.type === "ai" &&
|
message.type === "ai" &&
|
||||||
(hasToolCalls(message) ? true : !hasContent(message)),
|
(hasToolCalls(message) ? true : !hasContent(message)),
|
||||||
);
|
);
|
||||||
|
const processingAi = [...group.messages]
|
||||||
|
.reverse()
|
||||||
|
.find((message) => message.type === "ai");
|
||||||
return (
|
return (
|
||||||
<div key={"group-" + group.id} className="w-full">
|
<div key={"group-" + group.id} className="w-full">
|
||||||
<MessageGroup
|
<MessageGroup
|
||||||
messages={group.messages}
|
messages={group.messages}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
|
displaySources={resolveDisplaySources(processingAi)}
|
||||||
/>
|
/>
|
||||||
<MessageTokenUsageList
|
<MessageTokenUsageList
|
||||||
enabled={tokenUsageEnabled}
|
enabled={tokenUsageEnabled}
|
||||||
|
|||||||
Reference in new issue
Block a user