feat(frontend): add workspace single and multi round rag
This commit is contained in:
1 parent
5a4881053d
commit
4f536d9b39
12 files changed
+1102
-99
No files matched your search
@@ -16,6 +16,7 @@ import { apiFetch } from "@/core/api";
|
|||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||||
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
||||||
|
import { isMultiRoundRagMessage } from "@/core/threads/use-multi-round-rag";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
import type { SkillDisplayConfig } from "@/core/skills/type";
|
import type { SkillDisplayConfig } from "@/core/skills/type";
|
||||||
import {
|
import {
|
||||||
@@ -93,13 +94,20 @@ const ChatBox: React.FC<{
|
|||||||
const [settings] = useLocalSettings();
|
const [settings] = useLocalSettings();
|
||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
||||||
// Frontend single-round RAG writes its own source tool message, so it should
|
// Frontend RAG modes write their own hidden source tool messages. They use
|
||||||
// use the same reference panel without changing the user's global RAG mode.
|
// the existing reference panel, but must not request or write reference
|
||||||
const hasSingleRoundRag = useMemo(
|
// batches through DeerFlow's backend APIs.
|
||||||
() => (thread.values.messages ?? []).some(isSingleRoundRagMessage),
|
const hasFrontendRag = useMemo(
|
||||||
|
() =>
|
||||||
|
(thread.values.messages ?? []).some(
|
||||||
|
(message) =>
|
||||||
|
isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message),
|
||||||
|
),
|
||||||
[thread.values.messages],
|
[thread.values.messages],
|
||||||
);
|
);
|
||||||
const referencesEnabled = ragModeEnabled || hasSingleRoundRag;
|
const referencesEnabled = ragModeEnabled || hasFrontendRag;
|
||||||
|
const shouldFetchBackendReferenceBatches =
|
||||||
|
referencesEnabled && !hasFrontendRag;
|
||||||
const { skills } = useSkills();
|
const { skills } = useSkills();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -128,7 +136,7 @@ const ChatBox: React.FC<{
|
|||||||
);
|
);
|
||||||
}, [referencesEnabled, skillDisplayMap, thread.isLoading, thread.values.messages]);
|
}, [referencesEnabled, skillDisplayMap, thread.isLoading, thread.values.messages]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!referencesEnabled) {
|
if (!shouldFetchBackendReferenceBatches) {
|
||||||
setBackendReferenceBatches([]);
|
setBackendReferenceBatches([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,7 +172,12 @@ const ChatBox: React.FC<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [referencesEnabled, thread.isLoading, threadId, thread.values.messages]);
|
}, [
|
||||||
|
shouldFetchBackendReferenceBatches,
|
||||||
|
thread.isLoading,
|
||||||
|
threadId,
|
||||||
|
thread.values.messages,
|
||||||
|
]);
|
||||||
const adaptedBackendReferenceBatches = useMemo(
|
const adaptedBackendReferenceBatches = useMemo(
|
||||||
() =>
|
() =>
|
||||||
adaptBackendReferenceBatches(
|
adaptBackendReferenceBatches(
|
||||||
@@ -175,15 +188,22 @@ const ChatBox: React.FC<{
|
|||||||
);
|
);
|
||||||
const referenceBatches = useMemo(
|
const referenceBatches = useMemo(
|
||||||
() => {
|
() => {
|
||||||
if (adaptedBackendReferenceBatches.length > 0) {
|
if (
|
||||||
|
shouldFetchBackendReferenceBatches &&
|
||||||
|
adaptedBackendReferenceBatches.length > 0
|
||||||
|
) {
|
||||||
return adaptedBackendReferenceBatches;
|
return adaptedBackendReferenceBatches;
|
||||||
}
|
}
|
||||||
return fallbackReferenceBatches;
|
return fallbackReferenceBatches;
|
||||||
},
|
},
|
||||||
[adaptedBackendReferenceBatches, fallbackReferenceBatches],
|
[
|
||||||
|
adaptedBackendReferenceBatches,
|
||||||
|
fallbackReferenceBatches,
|
||||||
|
shouldFetchBackendReferenceBatches,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!referencesEnabled) {
|
if (!shouldFetchBackendReferenceBatches) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const signature = JSON.stringify({
|
const signature = JSON.stringify({
|
||||||
@@ -210,7 +230,14 @@ const ChatBox: React.FC<{
|
|||||||
fallbackBatches: debugReferenceBatches(fallbackReferenceBatches),
|
fallbackBatches: debugReferenceBatches(fallbackReferenceBatches),
|
||||||
finalBatches: debugReferenceBatches(referenceBatches),
|
finalBatches: debugReferenceBatches(referenceBatches),
|
||||||
});
|
});
|
||||||
}, [adaptedBackendReferenceBatches, backendReferenceBatches, fallbackReferenceBatches, referencesEnabled, referenceBatches, threadId]);
|
}, [
|
||||||
|
adaptedBackendReferenceBatches,
|
||||||
|
backendReferenceBatches,
|
||||||
|
fallbackReferenceBatches,
|
||||||
|
referenceBatches,
|
||||||
|
shouldFetchBackendReferenceBatches,
|
||||||
|
threadId,
|
||||||
|
]);
|
||||||
const latestReferenceBatch =
|
const latestReferenceBatch =
|
||||||
referenceBatches.length > 0
|
referenceBatches.length > 0
|
||||||
? referenceBatches[referenceBatches.length - 1]!
|
? referenceBatches[referenceBatches.length - 1]!
|
||||||
@@ -221,7 +248,7 @@ const ChatBox: React.FC<{
|
|||||||
const referenceSources = activeReferenceBatch?.sources ?? [];
|
const referenceSources = activeReferenceBatch?.sources ?? [];
|
||||||
const hasCitationReferences = referenceSources.length > 0;
|
const hasCitationReferences = referenceSources.length > 0;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!referencesEnabled || !activeReferenceBatch) {
|
if (!shouldFetchBackendReferenceBatches || !activeReferenceBatch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logReferenceDebug("frontend-active-reference-batch-payload", threadId, {
|
logReferenceDebug("frontend-active-reference-batch-payload", threadId, {
|
||||||
@@ -230,7 +257,7 @@ const ChatBox: React.FC<{
|
|||||||
sourceCount: activeReferenceBatch.sources.length,
|
sourceCount: activeReferenceBatch.sources.length,
|
||||||
sources: activeReferenceBatch.sources.map(debugReferenceSource),
|
sources: activeReferenceBatch.sources.map(debugReferenceSource),
|
||||||
});
|
});
|
||||||
}, [activeReferenceBatch, referencesEnabled, threadId]);
|
}, [activeReferenceBatch, shouldFetchBackendReferenceBatches, threadId]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasCitationReferences) {
|
if (hasCitationReferences) {
|
||||||
setReferencesOpen(true);
|
setReferencesOpen(true);
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ export function CitationLink({
|
|||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
onReferenceClick,
|
||||||
|
onClick,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<"a">) {
|
}: ComponentProps<"a"> & {
|
||||||
|
/** Lets a message renderer show an in-place source preview instead of scrolling. */
|
||||||
|
onReferenceClick?: (referenceId: string) => void;
|
||||||
|
}) {
|
||||||
const childrenText = normalizeCitationText(children);
|
const childrenText = normalizeCitationText(children);
|
||||||
const displayText = childrenText ?? "";
|
const displayText = childrenText ?? "";
|
||||||
const referenceId = href?.startsWith("#reference-")
|
const referenceId = href?.startsWith("#reference-")
|
||||||
@@ -28,6 +33,11 @@ export function CitationLink({
|
|||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (onReferenceClick) {
|
||||||
|
onReferenceClick(referenceId);
|
||||||
|
onClick?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const batchId =
|
const batchId =
|
||||||
event.currentTarget.closest<HTMLElement>("[data-reference-batch-id]")
|
event.currentTarget.closest<HTMLElement>("[data-reference-batch-id]")
|
||||||
?.dataset.referenceBatchId;
|
?.dataset.referenceBatchId;
|
||||||
@@ -41,6 +51,7 @@ export function CitationLink({
|
|||||||
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
};
|
};
|
||||||
window.setTimeout(scroll, 240);
|
window.setTimeout(scroll, 240);
|
||||||
|
onClick?.(event);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -58,6 +69,7 @@ export function CitationLink({
|
|||||||
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onClick={onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{displayText || children}
|
{displayText || children}
|
||||||
|
|||||||
@@ -210,6 +210,9 @@ export function InputBox({
|
|||||||
const { models } = useModels();
|
const { models } = useModels();
|
||||||
const singleRoundRagActive =
|
const singleRoundRagActive =
|
||||||
context.execution_profile === "single_round_rag";
|
context.execution_profile === "single_round_rag";
|
||||||
|
const multiRoundRagActive =
|
||||||
|
context.execution_profile === "multi_round_rag";
|
||||||
|
const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
return singleRoundRagActive ? extraModels : models;
|
return singleRoundRagActive ? extraModels : models;
|
||||||
}, [extraModels, models, singleRoundRagActive]);
|
}, [extraModels, models, singleRoundRagActive]);
|
||||||
@@ -236,6 +239,11 @@ export function InputBox({
|
|||||||
recommendedQuestions.length > 0;
|
recommendedQuestions.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Multi-round RAG is a frontend execution profile, not a model option.
|
||||||
|
// Do not let the normal model fallback clear it on every render.
|
||||||
|
if (multiRoundRagActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (modelOptions.length === 0) {
|
if (modelOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -259,7 +267,7 @@ export function InputBox({
|
|||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
execution_profile: fallbackModel.execution_profile,
|
execution_profile: fallbackModel.execution_profile,
|
||||||
});
|
});
|
||||||
}, [context, modelOptions, onContextChange]);
|
}, [context, modelOptions, multiRoundRagActive, onContextChange]);
|
||||||
|
|
||||||
const selectedModel = useMemo(() => {
|
const selectedModel = useMemo(() => {
|
||||||
if (modelOptions.length === 0) {
|
if (modelOptions.length === 0) {
|
||||||
@@ -343,6 +351,13 @@ export function InputBox({
|
|||||||
});
|
});
|
||||||
}, [context, extraModels, onContextChange]);
|
}, [context, extraModels, onContextChange]);
|
||||||
|
|
||||||
|
const handleMultiRoundRagSelect = useCallback(() => {
|
||||||
|
onContextChange?.({
|
||||||
|
...context,
|
||||||
|
execution_profile: "multi_round_rag",
|
||||||
|
});
|
||||||
|
}, [context, onContextChange]);
|
||||||
|
|
||||||
const handleReasoningEffortSelect = useCallback(
|
const handleReasoningEffortSelect = useCallback(
|
||||||
(effort: "minimal" | "low" | "medium" | "high") => {
|
(effort: "minimal" | "low" | "medium" | "high") => {
|
||||||
onContextChange?.({
|
onContextChange?.({
|
||||||
@@ -712,14 +727,16 @@ export function InputBox({
|
|||||||
/>
|
/>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu> */}
|
</PromptInputActionMenu> */}
|
||||||
<AddAttachmentsButton
|
{!frontendRagActive && (
|
||||||
className={cn(
|
<AddAttachmentsButton
|
||||||
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
className={cn(
|
||||||
compact &&
|
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||||
"gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
compact &&
|
||||||
)}
|
"gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
||||||
/>
|
)}
|
||||||
<Tooltip
|
/>
|
||||||
|
)}
|
||||||
|
{!frontendRagActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
context.writing_mode
|
context.writing_mode
|
||||||
? "写作模式已开启:最终输出会强制保存为 Markdown 文件"
|
? "写作模式已开启:最终输出会强制保存为 Markdown 文件"
|
||||||
@@ -744,8 +761,8 @@ export function InputBox({
|
|||||||
<FileTextIcon className="size-3" />
|
<FileTextIcon className="size-3" />
|
||||||
<span className="text-xs font-normal">写作</span>
|
<span className="text-xs font-normal">写作</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>}
|
||||||
<Tooltip
|
{!frontendRagActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
memoryInjectionEnabled
|
memoryInjectionEnabled
|
||||||
? "记忆注入已开启:对话前会注入历史记忆"
|
? "记忆注入已开启:对话前会注入历史记忆"
|
||||||
@@ -770,8 +787,8 @@ export function InputBox({
|
|||||||
<BrainCircuitIcon className="size-3" />
|
<BrainCircuitIcon className="size-3" />
|
||||||
<span className="text-xs font-normal">记忆</span>
|
<span className="text-xs font-normal">记忆</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>}
|
||||||
<Tooltip
|
{!frontendRagActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
ragModeEnabled
|
ragModeEnabled
|
||||||
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
||||||
@@ -800,8 +817,8 @@ export function InputBox({
|
|||||||
<BookMarkedIcon className="size-3" />
|
<BookMarkedIcon className="size-3" />
|
||||||
<span className="text-xs font-normal">参考文献</span>
|
<span className="text-xs font-normal">参考文献</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>}
|
||||||
<Tooltip
|
{!frontendRagActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
contextCompressionEnabled
|
contextCompressionEnabled
|
||||||
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
||||||
@@ -829,7 +846,7 @@ export function InputBox({
|
|||||||
<ShrinkIcon className="size-3" />
|
<ShrinkIcon className="size-3" />
|
||||||
<span className="text-xs font-normal">上下文压缩</span>
|
<span className="text-xs font-normal">上下文压缩</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>}
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<ModeHoverGuide
|
<ModeHoverGuide
|
||||||
mode={
|
mode={
|
||||||
@@ -841,9 +858,11 @@ export function InputBox({
|
|||||||
: "flash"
|
: "flash"
|
||||||
}
|
}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
selectedSingleRoundRag
|
singleRoundRagActive
|
||||||
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
||||||
: undefined
|
: multiRoundRagActive
|
||||||
|
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PromptInputActionMenuTrigger
|
<PromptInputActionMenuTrigger
|
||||||
@@ -851,7 +870,7 @@ export function InputBox({
|
|||||||
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||||
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
||||||
compact &&
|
compact &&
|
||||||
(selectedSingleRoundRag
|
(frontendRagActive
|
||||||
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||||
: context.mode === "thinking"
|
: context.mode === "thinking"
|
||||||
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
|
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
|
||||||
@@ -863,7 +882,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{selectedSingleRoundRag ? (
|
{frontendRagActive ? (
|
||||||
<BookMarkedIcon className="size-3" />
|
<BookMarkedIcon className="size-3" />
|
||||||
) : context.mode === "flash" ? (
|
) : context.mode === "flash" ? (
|
||||||
<ZapIcon className="size-3" />
|
<ZapIcon className="size-3" />
|
||||||
@@ -881,8 +900,10 @@ export function InputBox({
|
|||||||
context.mode === "ultra" ? "golden-text" : "",
|
context.mode === "ultra" ? "golden-text" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedSingleRoundRag
|
{frontendRagActive
|
||||||
? "单轮 RAG"
|
? multiRoundRagActive
|
||||||
|
? "多轮 RAG"
|
||||||
|
: "单轮 RAG"
|
||||||
: (context.mode === "flash" && t.inputBox.flashMode) ||
|
: (context.mode === "flash" && t.inputBox.flashMode) ||
|
||||||
(context.mode === "thinking" &&
|
(context.mode === "thinking" &&
|
||||||
t.inputBox.reasoningMode) ||
|
t.inputBox.reasoningMode) ||
|
||||||
@@ -899,7 +920,7 @@ export function InputBox({
|
|||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!selectedSingleRoundRag && context.mode === "flash"
|
!frontendRagActive && context.mode === "flash"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -910,7 +931,7 @@ export function InputBox({
|
|||||||
<ZapIcon
|
<ZapIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!selectedSingleRoundRag && context.mode === "flash" &&
|
!frontendRagActive && context.mode === "flash" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -920,7 +941,7 @@ export function InputBox({
|
|||||||
{t.inputBox.flashModeDescription}
|
{t.inputBox.flashModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!selectedSingleRoundRag && context.mode === "flash" ? (
|
{!frontendRagActive && context.mode === "flash" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -929,7 +950,7 @@ export function InputBox({
|
|||||||
{supportThinking && (
|
{supportThinking && (
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!selectedSingleRoundRag && context.mode === "thinking"
|
!frontendRagActive && context.mode === "thinking"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -940,7 +961,7 @@ export function InputBox({
|
|||||||
<LightbulbIcon
|
<LightbulbIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!selectedSingleRoundRag && context.mode === "thinking" &&
|
!frontendRagActive && context.mode === "thinking" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -950,7 +971,7 @@ export function InputBox({
|
|||||||
{t.inputBox.reasoningModeDescription}
|
{t.inputBox.reasoningModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!selectedSingleRoundRag && context.mode === "thinking" ? (
|
{!frontendRagActive && context.mode === "thinking" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -959,7 +980,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!selectedSingleRoundRag && context.mode === "pro"
|
!frontendRagActive && context.mode === "pro"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -970,7 +991,7 @@ export function InputBox({
|
|||||||
<GraduationCapIcon
|
<GraduationCapIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!selectedSingleRoundRag && context.mode === "pro" &&
|
!frontendRagActive && context.mode === "pro" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -980,7 +1001,7 @@ export function InputBox({
|
|||||||
{t.inputBox.proModeDescription}
|
{t.inputBox.proModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!selectedSingleRoundRag && context.mode === "pro" ? (
|
{!frontendRagActive && context.mode === "pro" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -988,7 +1009,7 @@ export function InputBox({
|
|||||||
</PromptInputActionMenuItem>
|
</PromptInputActionMenuItem>
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!selectedSingleRoundRag && context.mode === "ultra"
|
!frontendRagActive && context.mode === "ultra"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -999,14 +1020,14 @@ export function InputBox({
|
|||||||
<RocketIcon
|
<RocketIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!selectedSingleRoundRag &&
|
!frontendRagActive &&
|
||||||
context.mode === "ultra" &&
|
context.mode === "ultra" &&
|
||||||
"text-[#dabb5e]",
|
"text-[#dabb5e]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
!selectedSingleRoundRag &&
|
!frontendRagActive &&
|
||||||
context.mode === "ultra" &&
|
context.mode === "ultra" &&
|
||||||
"golden-text",
|
"golden-text",
|
||||||
)}
|
)}
|
||||||
@@ -1018,7 +1039,7 @@ export function InputBox({
|
|||||||
{t.inputBox.ultraModeDescription}
|
{t.inputBox.ultraModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!selectedSingleRoundRag && context.mode === "ultra" ? (
|
{!frontendRagActive && context.mode === "ultra" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -1057,11 +1078,39 @@ export function InputBox({
|
|||||||
</PromptInputActionMenuItem>
|
</PromptInputActionMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<PromptInputActionMenuItem
|
||||||
|
className={cn(
|
||||||
|
multiRoundRagActive
|
||||||
|
? "bg-emerald-50 text-emerald-800 hover:bg-emerald-100 focus:bg-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-200 dark:hover:bg-emerald-950/60 dark:focus:bg-emerald-950/60"
|
||||||
|
: "text-muted-foreground/65",
|
||||||
|
)}
|
||||||
|
onSelect={handleMultiRoundRagSelect}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-1 font-bold">
|
||||||
|
<BookMarkedIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 size-4",
|
||||||
|
multiRoundRagActive && "text-emerald-700 dark:text-emerald-200",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
多轮 RAG 问答
|
||||||
|
</div>
|
||||||
|
<div className="pl-7 text-xs">
|
||||||
|
多次检索、展示过程与参考资料,适合复杂问题
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{multiRoundRagActive ? (
|
||||||
|
<CheckIcon className="ml-auto size-4" />
|
||||||
|
) : (
|
||||||
|
<div className="ml-auto size-4" />
|
||||||
|
)}
|
||||||
|
</PromptInputActionMenuItem>
|
||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
{supportReasoningEffort && context.mode !== "flash" && (
|
{!frontendRagActive && supportReasoningEffort && context.mode !== "flash" && (
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
|
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
|
||||||
<div className="text-xs font-normal">
|
<div className="text-xs font-normal">
|
||||||
@@ -1179,8 +1228,8 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
||||||
{knowledgeSelector}
|
{!frontendRagActive && agentSelector && (
|
||||||
{agentSelector && (
|
{!frontendRagActive && knowledgeSelector}
|
||||||
<AgentSelector
|
<AgentSelector
|
||||||
selectedAgentId={agentSelector.selectedAgentId}
|
selectedAgentId={agentSelector.selectedAgentId}
|
||||||
onAgentChange={agentSelector.onAgentChange}
|
onAgentChange={agentSelector.onAgentChange}
|
||||||
@@ -1188,7 +1237,7 @@ export function InputBox({
|
|||||||
compact={compact}
|
compact={compact}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{promptPrefix && (
|
{!frontendRagActive && promptPrefix && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
promptPrefix.on
|
promptPrefix.on
|
||||||
@@ -1216,7 +1265,7 @@ export function InputBox({
|
|||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<ModelSelector
|
{!multiRoundRagActive && <ModelSelector
|
||||||
open={modelDialogOpen}
|
open={modelDialogOpen}
|
||||||
onOpenChange={setModelDialogOpen}
|
onOpenChange={setModelDialogOpen}
|
||||||
>
|
>
|
||||||
@@ -1266,7 +1315,7 @@ export function InputBox({
|
|||||||
))}
|
))}
|
||||||
</ModelSelectorList>
|
</ModelSelectorList>
|
||||||
</ModelSelectorContent>
|
</ModelSelectorContent>
|
||||||
</ModelSelector>
|
</ModelSelector>}
|
||||||
<PromptInputSubmit
|
<PromptInputSubmit
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
|
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import type { Message } from '@langchain/langgraph-sdk';
|
import type { Message } from '@langchain/langgraph-sdk';
|
||||||
import { ClockIcon, FileIcon, Loader2Icon, PencilIcon } from 'lucide-react';
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ClockIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
FileIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
PencilIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -27,6 +35,13 @@ import {
|
|||||||
import { Task, TaskTrigger } from '@/components/ai-elements/task';
|
import { Task, TaskTrigger } from '@/components/ai-elements/task';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import {
|
import {
|
||||||
UploadedFilePreviewDialog,
|
UploadedFilePreviewDialog,
|
||||||
@@ -35,6 +50,7 @@ import {
|
|||||||
import { resolveArtifactURL } from '@/core/artifacts/utils';
|
import { resolveArtifactURL } from '@/core/artifacts/utils';
|
||||||
import { useChatAskTimes } from '@/core/chat-timestamps';
|
import { useChatAskTimes } from '@/core/chat-timestamps';
|
||||||
import { getBackendBaseURL } from '@/core/config';
|
import { getBackendBaseURL } from '@/core/config';
|
||||||
|
import { readRuntimeEnv } from '@/env';
|
||||||
import { useI18n } from '@/core/i18n/hooks';
|
import { useI18n } from '@/core/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
@@ -50,6 +66,11 @@ import {
|
|||||||
type RuntimeDisplaySource,
|
type RuntimeDisplaySource,
|
||||||
} from '@/core/skill-display/runtime';
|
} from '@/core/skill-display/runtime';
|
||||||
import { humanMessagePlugins } from '@/core/streamdown';
|
import { humanMessagePlugins } from '@/core/streamdown';
|
||||||
|
import {
|
||||||
|
getMultiRoundRagTrace,
|
||||||
|
isMultiRoundRagMessage,
|
||||||
|
type MultiRoundRagRound,
|
||||||
|
} from '@/core/threads/use-multi-round-rag';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
import { CitationLink } from '../citations/citation-link';
|
import { CitationLink } from '../citations/citation-link';
|
||||||
@@ -563,7 +584,26 @@ function MessageContent_({
|
|||||||
);
|
);
|
||||||
const askTimes = useChatAskTimes();
|
const askTimes = useChatAskTimes();
|
||||||
const isHuman = message.type === 'human';
|
const isHuman = message.type === 'human';
|
||||||
|
const isSingleRoundRag =
|
||||||
|
message.additional_kwargs?.single_round_rag === true;
|
||||||
|
const isMultiRoundRag = isMultiRoundRagMessage(message);
|
||||||
|
const multiRoundTrace = isMultiRoundRag
|
||||||
|
? getMultiRoundRagTrace(message)
|
||||||
|
: null;
|
||||||
|
const [selectedCitation, setSelectedCitation] =
|
||||||
|
useState<RuntimeDisplaySource | null>(null);
|
||||||
const resolveArtifact = useArtifactResolver(threadId);
|
const resolveArtifact = useArtifactResolver(threadId);
|
||||||
|
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 components = useMemo(
|
const components = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
img: (props: ImgHTMLAttributes<HTMLImageElement>) => (
|
img: (props: ImgHTMLAttributes<HTMLImageElement>) => (
|
||||||
@@ -571,7 +611,17 @@ function MessageContent_({
|
|||||||
),
|
),
|
||||||
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
if (href?.startsWith('#reference-')) {
|
if (href?.startsWith('#reference-')) {
|
||||||
return <CitationLink href={href} {...props} />;
|
return (
|
||||||
|
<CitationLink
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
onReferenceClick={
|
||||||
|
isSingleRoundRag || isMultiRoundRag
|
||||||
|
? handleCitationClick
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof props.children === 'string' &&
|
typeof props.children === 'string' &&
|
||||||
@@ -593,7 +643,13 @@ function MessageContent_({
|
|||||||
return <a {...props} href={href} />;
|
return <a {...props} href={href} />;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[threadId, resolveArtifact],
|
[
|
||||||
|
handleCitationClick,
|
||||||
|
isMultiRoundRag,
|
||||||
|
isSingleRoundRag,
|
||||||
|
resolveArtifact,
|
||||||
|
threadId,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Question carries its own `client_ts` (round-trips through the checkpoint).
|
// Question carries its own `client_ts` (round-trips through the checkpoint).
|
||||||
@@ -661,7 +717,7 @@ function MessageContent_({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reasoning-only AI message (no main response content yet)
|
// Reasoning-only AI message (no main response content yet)
|
||||||
if (!isHuman && reasoningContent && !rawContent) {
|
if (!isHuman && reasoningContent && !rawContent && !multiRoundTrace) {
|
||||||
return (
|
return (
|
||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
<Reasoning isStreaming={isLoading}>
|
<Reasoning isStreaming={isLoading}>
|
||||||
@@ -708,30 +764,282 @@ function MessageContent_({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIElementMessageContent className={className}>
|
<>
|
||||||
{filesList}
|
<AIElementMessageContent className={className}>
|
||||||
<div data-reference-batch-id={message.id} className="contents">
|
{filesList}
|
||||||
<AssistantDisplayContent
|
<div data-reference-batch-id={message.id} className="contents">
|
||||||
content={contentToDisplay}
|
{multiRoundTrace ? (
|
||||||
displaySources={displaySources}
|
<MultiRoundRagContent
|
||||||
|
trace={multiRoundTrace}
|
||||||
|
content={contentToDisplay}
|
||||||
|
displaySources={displaySources}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: 'html' }]]}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AssistantDisplayContent
|
||||||
|
content={contentToDisplay}
|
||||||
|
displaySources={displaySources}
|
||||||
|
isLoading={isLoading}
|
||||||
|
renderSkillDisplayBlocks={false}
|
||||||
|
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: 'html' }]]}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MessageTokenUsage
|
||||||
|
enabled={tokenUsageEnabled}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
renderSkillDisplayBlocks={false}
|
message={message}
|
||||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: 'html' }]]}
|
|
||||||
components={components}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
{!isLoading && contentToDisplay ? (
|
||||||
<MessageTokenUsage
|
<MessageTimeStamp time={messageTime} align="left" />
|
||||||
enabled={tokenUsageEnabled}
|
) : null}
|
||||||
isLoading={isLoading}
|
</AIElementMessageContent>
|
||||||
message={message}
|
{(isSingleRoundRag || isMultiRoundRag) && (
|
||||||
/>
|
<SingleRoundRagCitationDialog
|
||||||
{!isLoading && contentToDisplay ? (
|
source={selectedCitation}
|
||||||
<MessageTimeStamp time={messageTime} align="left" />
|
onOpenChange={(open) => {
|
||||||
) : null}
|
if (!open) setSelectedCitation(null);
|
||||||
</AIElementMessageContent>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SingleRoundRagCitationDialog({
|
||||||
|
source,
|
||||||
|
onOpenChange,
|
||||||
|
}: {
|
||||||
|
source: RuntimeDisplaySource | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const sourceUrl = resolveSingleRoundRagSourceUrl(source);
|
||||||
|
return (
|
||||||
|
<Dialog open={!!source} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-h-[min(42rem,calc(100vh-2rem))] max-w-2xl overflow-hidden p-5 sm:max-w-2xl">
|
||||||
|
<DialogHeader className="pr-8">
|
||||||
|
<DialogTitle className="leading-relaxed">
|
||||||
|
{source?.title || "引用资料"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="space-y-1 text-xs">
|
||||||
|
<span className="block">来源:{source?.source || source?.type || "未知来源"}</span>
|
||||||
|
{source?.time && <span className="block">时间:{source.time}</span>}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{sourceUrl && (
|
||||||
|
<a
|
||||||
|
href={sourceUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="border-input hover:bg-accent inline-flex w-fit items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
查看原文链接
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div className="bg-muted/50 max-h-[min(26rem,calc(100vh-16rem))] overflow-auto rounded-lg p-3 text-sm leading-7 whitespace-pre-wrap">
|
||||||
|
{source?.content || source?.snippet || "暂无引用内容"}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MultiRoundRagContent({
|
||||||
|
trace,
|
||||||
|
content,
|
||||||
|
displaySources,
|
||||||
|
isLoading,
|
||||||
|
rehypePlugins,
|
||||||
|
components,
|
||||||
|
}: {
|
||||||
|
trace: NonNullable<ReturnType<typeof getMultiRoundRagTrace>>;
|
||||||
|
content: string;
|
||||||
|
displaySources: RuntimeDisplaySource[];
|
||||||
|
isLoading: boolean;
|
||||||
|
rehypePlugins: MarkdownContentProps['rehypePlugins'];
|
||||||
|
components: MarkdownContentProps['components'];
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(!trace.isComplete);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!trace.isComplete) setOpen(true);
|
||||||
|
}, [trace.isComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<section className="border-border/70 overflow-hidden rounded-xl border bg-muted/20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:bg-muted/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors"
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
{trace.isComplete ? (
|
||||||
|
<SearchIcon className="text-primary size-4 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Loader2Icon className="text-primary size-4 shrink-0 animate-spin" />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">多轮检索过程</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{trace.isComplete
|
||||||
|
? `已完成 ${trace.rounds.length} 轮检索`
|
||||||
|
: '正在分析问题并检索资料'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn('text-muted-foreground size-4 transition-transform', !open && '-rotate-90')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="border-border/60 space-y-3 border-t px-4 py-3">
|
||||||
|
{trace.rounds.map((round, index) => (
|
||||||
|
<MultiRoundRagRoundCard
|
||||||
|
key={round.id}
|
||||||
|
index={index + 1}
|
||||||
|
round={round}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section className="border-border/70 rounded-xl border p-4">
|
||||||
|
<div className="mb-3 text-sm font-semibold">最终回答</div>
|
||||||
|
{content ? (
|
||||||
|
<AssistantDisplayContent
|
||||||
|
content={content}
|
||||||
|
displaySources={displaySources}
|
||||||
|
isLoading={isLoading}
|
||||||
|
renderSkillDisplayBlocks={false}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||||
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
|
正在生成最终回答…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MultiRoundRagRoundCard({
|
||||||
|
index,
|
||||||
|
round,
|
||||||
|
isLoading,
|
||||||
|
rehypePlugins,
|
||||||
|
components,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
round: MultiRoundRagRound;
|
||||||
|
isLoading: boolean;
|
||||||
|
rehypePlugins: MarkdownContentProps['rehypePlugins'];
|
||||||
|
components: MarkdownContentProps['components'];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="border-border/60 rounded-lg border bg-background/70 p-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<span className="bg-primary/10 text-primary flex size-5 shrink-0 items-center justify-center rounded-full text-xs">
|
||||||
|
{index}
|
||||||
|
</span>
|
||||||
|
<span>{round.title}</span>
|
||||||
|
{round.isSearching && <Loader2Icon className="text-primary size-3.5 animate-spin" />}
|
||||||
|
</div>
|
||||||
|
{round.thought && (
|
||||||
|
<section className="border-border/50 bg-muted/30 mt-3 rounded-md border p-3">
|
||||||
|
<div className="text-muted-foreground mb-2 text-xs font-medium">思考内容</div>
|
||||||
|
<MarkdownContent
|
||||||
|
content={round.thought}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
className="text-muted-foreground text-sm"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{round.body && (
|
||||||
|
<section className="border-border/50 mt-3 rounded-md border p-3">
|
||||||
|
<div className="text-muted-foreground mb-2 text-xs font-medium">本轮正文</div>
|
||||||
|
<MarkdownContent
|
||||||
|
content={round.body}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{round.query && (
|
||||||
|
<div className="border-primary/20 bg-primary/5 text-primary mt-2 inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs">
|
||||||
|
<SearchIcon className="size-3 shrink-0" />
|
||||||
|
<span className="truncate">{round.query}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{round.retrievals.length > 0 && (
|
||||||
|
<section className="border-border/50 mt-3 rounded-md border p-3">
|
||||||
|
<div className="text-muted-foreground mb-2 text-xs font-medium">
|
||||||
|
检索内容 · {round.retrievals.length} 条
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{round.retrievals.map((item, resultIndex) => (
|
||||||
|
<details
|
||||||
|
key={`${item.id ?? item.title}-${resultIndex}`}
|
||||||
|
className="border-border/60 rounded-md border px-3 py-2"
|
||||||
|
open={resultIndex === 0}
|
||||||
|
>
|
||||||
|
<summary className="cursor-pointer list-none text-sm font-medium">
|
||||||
|
<span className="mr-2 text-xs text-muted-foreground">{resultIndex + 1}.</span>
|
||||||
|
{item.title}
|
||||||
|
</summary>
|
||||||
|
<div className="text-muted-foreground mt-2 text-sm leading-6 whitespace-pre-wrap">
|
||||||
|
{item.content || '暂无资料摘要'}
|
||||||
|
</div>
|
||||||
|
{(item.source || item.time) && (
|
||||||
|
<div className="text-muted-foreground mt-2 text-xs">
|
||||||
|
{[item.source, item.time].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSingleRoundRagSourceUrl(
|
||||||
|
source: RuntimeDisplaySource | null,
|
||||||
|
): string | undefined {
|
||||||
|
if (!source) return undefined;
|
||||||
|
if (source.url && /^https?:\/\//i.test(source.url)) return source.url;
|
||||||
|
|
||||||
|
const raw = source.raw ?? {};
|
||||||
|
const recordId =
|
||||||
|
source.id ??
|
||||||
|
readString(raw.recUuid) ??
|
||||||
|
readString(raw.rec_uuid) ??
|
||||||
|
readString(raw.uuid);
|
||||||
|
const knowledgeUrl = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL")?.replace(/\/$/, "");
|
||||||
|
if (!knowledgeUrl || !recordId) return undefined;
|
||||||
|
return `${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${encodeURIComponent(recordId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string" && value.trim()) return value;
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function EmbeddedPageContextCard({
|
function EmbeddedPageContextCard({
|
||||||
pageContext,
|
pageContext,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import type { Subtask } from "@/core/tasks";
|
|||||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
import { useUpdateSubtask } from "@/core/tasks/context";
|
||||||
import type { AgentThreadState } from "@/core/threads";
|
import type { AgentThreadState } from "@/core/threads";
|
||||||
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
||||||
|
import { isMultiRoundRagMessage } from "@/core/threads/use-multi-round-rag";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification";
|
import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification";
|
||||||
|
|
||||||
@@ -239,7 +240,11 @@ export function MessageList({
|
|||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
||||||
const hasSingleRoundRag = useMemo(
|
const hasSingleRoundRag = useMemo(
|
||||||
() => messages.some(isSingleRoundRagMessage),
|
() =>
|
||||||
|
messages.some(
|
||||||
|
(message) =>
|
||||||
|
isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message),
|
||||||
|
),
|
||||||
[messages],
|
[messages],
|
||||||
);
|
);
|
||||||
const referencesEnabled = ragModeEnabled || hasSingleRoundRag;
|
const referencesEnabled = ragModeEnabled || hasSingleRoundRag;
|
||||||
|
|||||||
@@ -19,3 +19,18 @@ export function isSingleRoundRagProfile(
|
|||||||
"single_round_rag"
|
"single_round_rag"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMultiRoundRagProfile(
|
||||||
|
context: unknown,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
typeof context === "object" &&
|
||||||
|
context !== null &&
|
||||||
|
(context as { execution_profile?: unknown }).execution_profile ===
|
||||||
|
"multi_round_rag"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFrontendRagProfile(context: unknown): boolean {
|
||||||
|
return isSingleRoundRagProfile(context) || isMultiRoundRagProfile(context);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export interface Model {
|
|||||||
supports_thinking?: boolean;
|
supports_thinking?: boolean;
|
||||||
supports_reasoning_effort?: boolean;
|
supports_reasoning_effort?: boolean;
|
||||||
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
||||||
execution_profile?: "single_round_rag";
|
execution_profile?: "single_round_rag" | "multi_round_rag";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenUsageSettings {
|
export interface TokenUsageSettings {
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ test("keeps half-width syntax working", () => {
|
|||||||
assert.equal(out.includes(citation(3)), true);
|
assert.equal(out.includes(citation(3)), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("converts double-bracket RAG references", () => {
|
||||||
|
const out = applyInlineCitations("RAG answer [[1]] and [[2]].", makeSources(2));
|
||||||
|
assert.equal(out.includes(citation(1)), true);
|
||||||
|
assert.equal(out.includes(citation(2)), true);
|
||||||
|
});
|
||||||
|
|
||||||
test("accepts spaced and full-width square brackets", () => {
|
test("accepts spaced and full-width square brackets", () => {
|
||||||
const out = applyInlineCitations("参考[ 1 ]和[2]。", makeSources(2));
|
const out = applyInlineCitations("参考[ 1 ]和[2]。", makeSources(2));
|
||||||
assert.equal(out.includes(citation(1)), true);
|
assert.equal(out.includes(citation(1)), true);
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
|||||||
knowledge_rag_enabled?: boolean;
|
knowledge_rag_enabled?: boolean;
|
||||||
/** DeerFlow LLMWiki mapping ids selected for this conversation. */
|
/** DeerFlow LLMWiki mapping ids selected for this conversation. */
|
||||||
llmwiki_knowledge_base_ids?: string[];
|
llmwiki_knowledge_base_ids?: string[];
|
||||||
/** Direct knowledge-base RAG mode; this request never enters Lead Agent. */
|
/** Direct RAG modes handled in the frontend; these requests never enter Lead Agent. */
|
||||||
execution_profile?: "single_round_rag";
|
execution_profile?: "single_round_rag" | "multi_round_rag";
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
||||||
agent_name?: string;
|
agent_name?: string;
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { readRuntimeEnv } from "@/env";
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = "workspace.multi-round-rag.";
|
||||||
|
|
||||||
|
export type MultiRoundRagRetrieval = {
|
||||||
|
id?: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
source?: string;
|
||||||
|
time?: string;
|
||||||
|
recUuid?: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MultiRoundRagRound = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
thought: string;
|
||||||
|
/** Model text emitted for this round, before its retrieval results. */
|
||||||
|
body: string;
|
||||||
|
query: string;
|
||||||
|
isSearching: boolean;
|
||||||
|
retrievals: MultiRoundRagRetrieval[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MultiRoundRagTrace = {
|
||||||
|
rounds: MultiRoundRagRound[];
|
||||||
|
answer: string;
|
||||||
|
isComplete: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MultiRoundRagSource = {
|
||||||
|
id?: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
snippet: string;
|
||||||
|
content: string;
|
||||||
|
source?: string;
|
||||||
|
time?: string;
|
||||||
|
url?: string;
|
||||||
|
raw: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MultiRoundEvent = {
|
||||||
|
event?: unknown;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function storageKey(threadId: string) {
|
||||||
|
return `${STORAGE_PREFIX}${threadId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredMessages(threadId: string): Message[] {
|
||||||
|
if (typeof window === "undefined" || !threadId) return [];
|
||||||
|
try {
|
||||||
|
const raw = window.sessionStorage.getItem(storageKey(threadId));
|
||||||
|
const parsed: unknown = raw ? JSON.parse(raw) : [];
|
||||||
|
return Array.isArray(parsed) ? (parsed as Message[]) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStoredMessages(threadId: string, messages: Message[]) {
|
||||||
|
if (typeof window === "undefined" || !threadId) return;
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(storageKey(threadId), JSON.stringify(messages));
|
||||||
|
} catch {
|
||||||
|
// Storage quota failures must not interrupt a running answer.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createId(prefix: string) {
|
||||||
|
return `${prefix}-${crypto.randomUUID()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbortError(error: unknown) {
|
||||||
|
return error instanceof DOMException && error.name === "AbortError";
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asString(value: unknown): string {
|
||||||
|
return typeof value === "string" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(value: unknown): unknown {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as unknown;
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEventLine(line: string): MultiRoundEvent | null {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("data:")) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed.slice("data:".length).trim()) as MultiRoundEvent;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialTrace(): MultiRoundRagTrace {
|
||||||
|
return {
|
||||||
|
rounds: [
|
||||||
|
{
|
||||||
|
id: createId("multi-rag-round"),
|
||||||
|
title: "正在准备检索",
|
||||||
|
thought: "",
|
||||||
|
body: "",
|
||||||
|
query: "",
|
||||||
|
isSearching: true,
|
||||||
|
retrievals: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answer: "",
|
||||||
|
isComplete: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestRound(trace: MultiRoundRagTrace): MultiRoundRagRound {
|
||||||
|
const current = trace.rounds.at(-1);
|
||||||
|
if (current) return current;
|
||||||
|
const next = initialTrace().rounds[0]!;
|
||||||
|
trace.rounds.push(next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addThinkingRound(trace: MultiRoundRagTrace, thought: string) {
|
||||||
|
const current = latestRound(trace);
|
||||||
|
if (current.title === "正在准备检索" && !current.thought && !current.query) {
|
||||||
|
current.title = "思考过程";
|
||||||
|
current.thought = thought;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current.isSearching = false;
|
||||||
|
trace.rounds.push({
|
||||||
|
id: createId("multi-rag-round"),
|
||||||
|
title: "思考过程",
|
||||||
|
thought,
|
||||||
|
body: "",
|
||||||
|
query: "",
|
||||||
|
isSearching: true,
|
||||||
|
retrievals: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendRoundBody(trace: MultiRoundRagTrace, body: string) {
|
||||||
|
if (!body) return;
|
||||||
|
const current = latestRound(trace);
|
||||||
|
// Stored traces created before `body` was introduced remain readable.
|
||||||
|
current.body = `${current.body ?? ""}${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRetrievals(value: unknown): MultiRoundRagRetrieval[] {
|
||||||
|
const root = asRecord(parseJson(value));
|
||||||
|
const list =
|
||||||
|
(Array.isArray(root?.results) && root.results) ||
|
||||||
|
(Array.isArray(root?.organic) && root.organic) ||
|
||||||
|
[];
|
||||||
|
return list.flatMap((item, index) => {
|
||||||
|
const record = asRecord(item);
|
||||||
|
if (!record) return [];
|
||||||
|
const recUuid = asString(record.recUuid ?? record.rec_uuid);
|
||||||
|
const url = asString(record.url ?? record.link ?? record.href);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: asString(record.id) || recUuid || undefined,
|
||||||
|
title: asString(record.title) || `检索资料 ${index + 1}`,
|
||||||
|
content: asString(record.content ?? record.snippet ?? record.text),
|
||||||
|
source: asString(record.source ?? record.source1 ?? url) || undefined,
|
||||||
|
time: asString(record.publish_time ?? record.m_publish ?? record.date) || undefined,
|
||||||
|
recUuid: recUuid || undefined,
|
||||||
|
url: /^https?:\/\//i.test(url) ? url : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapReferences(value: unknown): MultiRoundRagSource[] {
|
||||||
|
const record = asRecord(value);
|
||||||
|
const list = Array.isArray(record?.references) ? record.references : [];
|
||||||
|
return list.flatMap((item, index) => {
|
||||||
|
const source = asRecord(item);
|
||||||
|
if (!source) return [];
|
||||||
|
const content = asString(source.content ?? source.snippet);
|
||||||
|
const rawUrl = asString(source.url ?? source.link ?? source.href);
|
||||||
|
const recUuid = asString(source.recUuid ?? source.rec_uuid ?? source.uuid);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: asString(source.id) || recUuid || undefined,
|
||||||
|
title: asString(source.title) || `参考资料 ${index + 1}`,
|
||||||
|
type: asString(source.type) || "参考资料",
|
||||||
|
snippet: asString(source.snippet) || content,
|
||||||
|
content,
|
||||||
|
source: asString(source.source ?? source.source1) || undefined,
|
||||||
|
time: asString(source.time ?? source.m_publish ?? source.publish_time) || undefined,
|
||||||
|
url: /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined,
|
||||||
|
raw: source,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneTrace(trace: MultiRoundRagTrace): MultiRoundRagTrace {
|
||||||
|
return {
|
||||||
|
...trace,
|
||||||
|
rounds: trace.rounds.map((round) => ({
|
||||||
|
...round,
|
||||||
|
retrievals: [...round.retrievals],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSingleRoundRagMessage(message: Message) {
|
||||||
|
return message.additional_kwargs?.single_round_rag === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMultiRoundRagMessage(message: Message): boolean {
|
||||||
|
return message.additional_kwargs?.multi_round_rag === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMultiRoundRagTrace(
|
||||||
|
message: Message,
|
||||||
|
): MultiRoundRagTrace | null {
|
||||||
|
const trace = message.additional_kwargs?.multi_round_rag_trace;
|
||||||
|
return asRecord(trace)
|
||||||
|
? (trace as unknown as MultiRoundRagTrace)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend adapter for GeneralQA's multi-round endpoint. It keeps the external
|
||||||
|
* SSE protocol outside DeerFlow and stores only its UI messages in sessionStorage.
|
||||||
|
*/
|
||||||
|
export function useMultiRoundRagMessages(threadId: string) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>(() => readStoredMessages(threadId));
|
||||||
|
const messagesRef = useRef(messages);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const controllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = readStoredMessages(threadId);
|
||||||
|
messagesRef.current = stored;
|
||||||
|
setMessages(stored);
|
||||||
|
setError(null);
|
||||||
|
}, [threadId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesRef.current = messages;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => () => controllerRef.current?.abort(), []);
|
||||||
|
|
||||||
|
const updateMessages = useCallback(
|
||||||
|
(targetThreadId: string, updater: (current: Message[]) => Message[]) => {
|
||||||
|
setMessages((current) => {
|
||||||
|
const next = updater(current);
|
||||||
|
messagesRef.current = next;
|
||||||
|
writeStoredMessages(targetThreadId, next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop = useCallback(() => controllerRef.current?.abort(), []);
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
async ({ targetThreadId, query }: { targetThreadId: string; query: string }) => {
|
||||||
|
if (isLoading) return;
|
||||||
|
const question = query.trim();
|
||||||
|
if (!question) return;
|
||||||
|
|
||||||
|
const humanId = createId("multi-rag-human");
|
||||||
|
const assistantId = createId("multi-rag-ai");
|
||||||
|
const sourceId = createId("multi-rag-sources");
|
||||||
|
const trace = initialTrace();
|
||||||
|
let sources: MultiRoundRagSource[] = [];
|
||||||
|
const clientTs = new Date().toISOString();
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: humanId,
|
||||||
|
type: "human",
|
||||||
|
content: [{ type: "text", text: question }],
|
||||||
|
additional_kwargs: { client_ts: clientTs, multi_round_rag: true },
|
||||||
|
};
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: assistantId,
|
||||||
|
type: "ai",
|
||||||
|
content: "",
|
||||||
|
additional_kwargs: {
|
||||||
|
multi_round_rag: true,
|
||||||
|
multi_round_rag_trace: trace,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateMessages(targetThreadId, (current) => [
|
||||||
|
...current,
|
||||||
|
userMessage,
|
||||||
|
assistantMessage,
|
||||||
|
]);
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
controllerRef.current = controller;
|
||||||
|
const refreshAssistant = () => {
|
||||||
|
const traceSnapshot = cloneTrace(trace);
|
||||||
|
updateMessages(targetThreadId, (current) => {
|
||||||
|
const next = current.filter((message) => message.id !== sourceId);
|
||||||
|
const assistantIndex = next.findIndex((message) => message.id === assistantId);
|
||||||
|
if (assistantIndex < 0) return current;
|
||||||
|
next[assistantIndex] = {
|
||||||
|
...next[assistantIndex],
|
||||||
|
content: traceSnapshot.answer,
|
||||||
|
additional_kwargs: {
|
||||||
|
...next[assistantIndex]!.additional_kwargs,
|
||||||
|
multi_round_rag: true,
|
||||||
|
multi_round_rag_trace: traceSnapshot,
|
||||||
|
reasoning_content: traceSnapshot.rounds
|
||||||
|
.map((round) => round.thought)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n") || null,
|
||||||
|
},
|
||||||
|
} as Message;
|
||||||
|
if (sources.length > 0) {
|
||||||
|
next.splice(assistantIndex, 0, {
|
||||||
|
id: sourceId,
|
||||||
|
type: "tool",
|
||||||
|
name: "multi_round_rag_references",
|
||||||
|
content: { results: sources },
|
||||||
|
additional_kwargs: {
|
||||||
|
hide_from_ui: true,
|
||||||
|
multi_round_rag: true,
|
||||||
|
},
|
||||||
|
} as unknown as Message);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseUrl = readRuntimeEnv("VITE_AIPI_GENERATE_CHAT_URL") || "";
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("未配置 VITE_AIPI_GENERATE_CHAT_URL,无法发起多轮 RAG 问答。");
|
||||||
|
}
|
||||||
|
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/generate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
signal: controller.signal,
|
||||||
|
body: JSON.stringify({ prompt: question }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`多轮 RAG 服务请求失败(${response.status})。`);
|
||||||
|
}
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("多轮 RAG 服务未返回流式响应。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split(/\r?\n/);
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
for (const line of lines) {
|
||||||
|
const event = parseEventLine(line);
|
||||||
|
if (!event) continue;
|
||||||
|
const eventType = asString(event.event);
|
||||||
|
const data = event.data;
|
||||||
|
if (eventType === "tool_call") {
|
||||||
|
const tool = asRecord(data);
|
||||||
|
const toolName = asString(tool?.tool_name);
|
||||||
|
const input = asRecord(tool?.tool_input) ?? {};
|
||||||
|
const current = latestRound(trace);
|
||||||
|
if (toolName === "show_text") {
|
||||||
|
const text = asString(input.text);
|
||||||
|
if (text.includes("<think>")) {
|
||||||
|
const thinkEnd = text.indexOf("</think>");
|
||||||
|
addThinkingRound(
|
||||||
|
trace,
|
||||||
|
(thinkEnd >= 0
|
||||||
|
? text.slice(0, thinkEnd + "</think>".length)
|
||||||
|
: text
|
||||||
|
)
|
||||||
|
.replace(/<\/?think>/g, "")
|
||||||
|
.trim(),
|
||||||
|
);
|
||||||
|
if (thinkEnd >= 0) {
|
||||||
|
const roundBody = text
|
||||||
|
.slice(thinkEnd + "</think>".length)
|
||||||
|
.trim();
|
||||||
|
if (roundBody) {
|
||||||
|
appendRoundBody(trace, roundBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// A non-thinking `show_text` immediately after a thought is
|
||||||
|
// this round's narrative; the terminal `message` event is
|
||||||
|
// still rendered only as the final answer below all rounds.
|
||||||
|
current.isSearching = false;
|
||||||
|
trace.answer += text;
|
||||||
|
trace.isComplete = true;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
toolName === "local_search" ||
|
||||||
|
toolName === "tavily_search" ||
|
||||||
|
toolName === "google_search"
|
||||||
|
) {
|
||||||
|
const queryData = asRecord(parseJson(input.result));
|
||||||
|
current.title =
|
||||||
|
toolName === "local_search" ? "本地知识检索" : "联网检索";
|
||||||
|
current.query =
|
||||||
|
asString(input.q ?? input.query) ||
|
||||||
|
asString(queryData?.query) ||
|
||||||
|
current.query;
|
||||||
|
const retrievals = mapRetrievals(input.result);
|
||||||
|
if (retrievals.length > 0) current.retrievals = retrievals;
|
||||||
|
current.isSearching = false;
|
||||||
|
} else if (toolName === "get_textid_content") {
|
||||||
|
const article = asRecord(parseJson(input.result));
|
||||||
|
const body =
|
||||||
|
asString(article?.content ?? article?.text) ||
|
||||||
|
asString(input.content);
|
||||||
|
if (body) appendRoundBody(trace, body);
|
||||||
|
current.isSearching = false;
|
||||||
|
}
|
||||||
|
refreshAssistant();
|
||||||
|
} else if (eventType === "message" && typeof data === "string") {
|
||||||
|
latestRound(trace).isSearching = false;
|
||||||
|
trace.answer += data;
|
||||||
|
trace.isComplete = true;
|
||||||
|
refreshAssistant();
|
||||||
|
} else if (eventType === "references") {
|
||||||
|
sources = mapReferences(data);
|
||||||
|
refreshAssistant();
|
||||||
|
} else if (eventType === "done") {
|
||||||
|
trace.isComplete = true;
|
||||||
|
for (const round of trace.rounds) round.isSearching = false;
|
||||||
|
refreshAssistant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace.isComplete = true;
|
||||||
|
for (const round of trace.rounds) round.isSearching = false;
|
||||||
|
refreshAssistant();
|
||||||
|
} catch (caught) {
|
||||||
|
trace.isComplete = true;
|
||||||
|
for (const round of trace.rounds) round.isSearching = false;
|
||||||
|
if (!isAbortError(caught)) {
|
||||||
|
const message =
|
||||||
|
caught instanceof Error ? caught.message : "多轮 RAG 问答失败。";
|
||||||
|
setError(message);
|
||||||
|
if (!trace.answer) trace.answer = message;
|
||||||
|
}
|
||||||
|
refreshAssistant();
|
||||||
|
} finally {
|
||||||
|
if (controllerRef.current === controller) controllerRef.current = null;
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isLoading, updateMessages],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { messages, isLoading, error, submit, stop };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFrontendRagMessage(message: Message): boolean {
|
||||||
|
return isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message);
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@ type RagSource = {
|
|||||||
raw: Record<string, unknown>;
|
raw: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RagHistoryEntry = {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_HISTORY_MESSAGES = 12;
|
||||||
|
|
||||||
/** Same JSON-object framing used by the established GeneralQA single-RAG flow. */
|
/** Same JSON-object framing used by the established GeneralQA single-RAG flow. */
|
||||||
function extractJsonObjects(raw: string): { objects: string[]; rest: string } {
|
function extractJsonObjects(raw: string): { objects: string[]; rest: string } {
|
||||||
const objects: string[] = [];
|
const objects: string[] = [];
|
||||||
@@ -199,6 +206,44 @@ function isAbortError(error: unknown) {
|
|||||||
return error instanceof DOMException && error.name === "AbortError";
|
return error instanceof DOMException && error.name === "AbortError";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textFromMessage(message: Message): string {
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
return message.content.trim();
|
||||||
|
}
|
||||||
|
if (!Array.isArray(message.content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return message.content
|
||||||
|
.map((part) => {
|
||||||
|
if (typeof part === "string") return part;
|
||||||
|
if (!part || typeof part !== "object") return "";
|
||||||
|
const text = (part as { text?: unknown }).text;
|
||||||
|
return typeof text === "string" ? text : "";
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general-QA endpoint accepts the usual `{ role, content }` history
|
||||||
|
* format. Keep it scoped to completed single-round-RAG turns so a normal
|
||||||
|
* DeerFlow conversation never leaks into this external endpoint.
|
||||||
|
*/
|
||||||
|
function buildRagHistory(messages: Message[]): RagHistoryEntry[] {
|
||||||
|
return messages
|
||||||
|
.filter(
|
||||||
|
(message) =>
|
||||||
|
message.additional_kwargs?.single_round_rag === true &&
|
||||||
|
(message.type === "human" || message.type === "ai"),
|
||||||
|
)
|
||||||
|
.map((message): RagHistoryEntry => ({
|
||||||
|
role: message.type === "human" ? "user" : "assistant",
|
||||||
|
content: textFromMessage(message),
|
||||||
|
}))
|
||||||
|
.filter((entry) => entry.content.length > 0)
|
||||||
|
.slice(-MAX_HISTORY_MESSAGES);
|
||||||
|
}
|
||||||
|
|
||||||
export function isSingleRoundRagMessage(message: Message): boolean {
|
export function isSingleRoundRagMessage(message: Message): boolean {
|
||||||
return message.additional_kwargs?.single_round_rag === true;
|
return message.additional_kwargs?.single_round_rag === true;
|
||||||
}
|
}
|
||||||
@@ -209,21 +254,29 @@ export function isSingleRoundRagMessage(message: Message): boolean {
|
|||||||
*/
|
*/
|
||||||
export function useSingleRoundRagMessages(threadId: string) {
|
export function useSingleRoundRagMessages(threadId: string) {
|
||||||
const [messages, setMessages] = useState<Message[]>(() => readStoredMessages(threadId));
|
const [messages, setMessages] = useState<Message[]>(() => readStoredMessages(threadId));
|
||||||
|
const messagesRef = useRef(messages);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const controllerRef = useRef<AbortController | null>(null);
|
const controllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMessages(readStoredMessages(threadId));
|
const stored = readStoredMessages(threadId);
|
||||||
|
messagesRef.current = stored;
|
||||||
|
setMessages(stored);
|
||||||
setError(null);
|
setError(null);
|
||||||
}, [threadId]);
|
}, [threadId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesRef.current = messages;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => () => controllerRef.current?.abort(), []);
|
useEffect(() => () => controllerRef.current?.abort(), []);
|
||||||
|
|
||||||
const updateMessages = useCallback(
|
const updateMessages = useCallback(
|
||||||
(targetThreadId: string, updater: (current: Message[]) => Message[]) => {
|
(targetThreadId: string, updater: (current: Message[]) => Message[]) => {
|
||||||
setMessages((current) => {
|
setMessages((current) => {
|
||||||
const next = updater(current);
|
const next = updater(current);
|
||||||
|
messagesRef.current = next;
|
||||||
writeStoredMessages(targetThreadId, next);
|
writeStoredMessages(targetThreadId, next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -251,6 +304,7 @@ export function useSingleRoundRagMessages(threadId: string) {
|
|||||||
const assistantId = createId("single-rag-ai");
|
const assistantId = createId("single-rag-ai");
|
||||||
const sourceId = createId("single-rag-sources");
|
const sourceId = createId("single-rag-sources");
|
||||||
const clientTs = new Date().toISOString();
|
const clientTs = new Date().toISOString();
|
||||||
|
const history = buildRagHistory(messagesRef.current);
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: humanId,
|
id: humanId,
|
||||||
type: "human",
|
type: "human",
|
||||||
@@ -319,7 +373,7 @@ export function useSingleRoundRagMessages(threadId: string) {
|
|||||||
knowledge_base_name: [],
|
knowledge_base_name: [],
|
||||||
top_k: 5,
|
top_k: 5,
|
||||||
score_threshold: 0.5,
|
score_threshold: 0.5,
|
||||||
history: [],
|
history,
|
||||||
stream: true,
|
stream: true,
|
||||||
model_name: modelName,
|
model_name: modelName,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
|
|||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import {
|
import {
|
||||||
|
isFrontendRagProfile,
|
||||||
|
isMultiRoundRagProfile,
|
||||||
isSingleRoundRagProfile,
|
isSingleRoundRagProfile,
|
||||||
SINGLE_ROUND_RAG_MODELS,
|
SINGLE_ROUND_RAG_MODELS,
|
||||||
} from "@/core/models/single-round-rag";
|
} from "@/core/models/single-round-rag";
|
||||||
@@ -59,6 +61,7 @@ import {
|
|||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
|
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
|
||||||
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
|
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
|
||||||
|
import { useMultiRoundRagMessages } from "@/core/threads/use-multi-round-rag";
|
||||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
@@ -310,19 +313,38 @@ export default function ChatPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const singleRoundRag = useSingleRoundRagMessages(threadId);
|
const singleRoundRag = useSingleRoundRagMessages(threadId);
|
||||||
|
const multiRoundRag = useMultiRoundRagMessages(threadId);
|
||||||
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
|
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
|
||||||
|
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
|
||||||
|
const frontendRagSelected = isFrontendRagProfile(settings.context);
|
||||||
const displayThread = useMemo(() => {
|
const displayThread = useMemo(() => {
|
||||||
if (singleRoundRag.messages.length === 0 && !singleRoundRag.isLoading) {
|
if (
|
||||||
|
singleRoundRag.messages.length === 0 &&
|
||||||
|
multiRoundRag.messages.length === 0 &&
|
||||||
|
!singleRoundRag.isLoading &&
|
||||||
|
!multiRoundRag.isLoading
|
||||||
|
) {
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
const messages = [...thread.messages, ...singleRoundRag.messages];
|
const messages = [
|
||||||
|
...thread.messages,
|
||||||
|
...singleRoundRag.messages,
|
||||||
|
...multiRoundRag.messages,
|
||||||
|
];
|
||||||
return {
|
return {
|
||||||
...thread,
|
...thread,
|
||||||
values: { ...thread.values, messages },
|
values: { ...thread.values, messages },
|
||||||
messages,
|
messages,
|
||||||
isLoading: thread.isLoading || singleRoundRag.isLoading,
|
isLoading:
|
||||||
|
thread.isLoading || singleRoundRag.isLoading || multiRoundRag.isLoading,
|
||||||
} as typeof thread;
|
} as typeof thread;
|
||||||
}, [singleRoundRag.isLoading, singleRoundRag.messages, thread]);
|
}, [
|
||||||
|
multiRoundRag.isLoading,
|
||||||
|
multiRoundRag.messages,
|
||||||
|
singleRoundRag.isLoading,
|
||||||
|
singleRoundRag.messages,
|
||||||
|
thread,
|
||||||
|
]);
|
||||||
|
|
||||||
const writingArtifactPath = useMemo(() => {
|
const writingArtifactPath = useMemo(() => {
|
||||||
const isMarkdown = (path: string | null | undefined) =>
|
const isMarkdown = (path: string | null | undefined) =>
|
||||||
@@ -337,9 +359,9 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
if (singleRoundRagSelected) {
|
if (frontendRagSelected) {
|
||||||
if (message.files && message.files.length > 0) {
|
if (message.files && message.files.length > 0) {
|
||||||
toast.error("单轮 RAG 问答暂不支持附件,请移除附件后重试。");
|
toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setHasStartedConversation(true);
|
setHasStartedConversation(true);
|
||||||
@@ -366,13 +388,23 @@ export default function ChatPage() {
|
|||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await singleRoundRag.submit({
|
if (multiRoundRagSelected) {
|
||||||
targetThreadId,
|
await multiRoundRag.submit({ targetThreadId, query: message.text });
|
||||||
query: message.text,
|
} else {
|
||||||
modelName: settings.context.model_name ?? "OpenAI3",
|
await singleRoundRag.submit({
|
||||||
});
|
targetThreadId,
|
||||||
|
query: message.text,
|
||||||
|
modelName: settings.context.model_name ?? "OpenAI3",
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "单轮 RAG 问答失败。");
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: multiRoundRagSelected
|
||||||
|
? "多轮 RAG 问答失败。"
|
||||||
|
: "单轮 RAG 问答失败。",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return;
|
return;
|
||||||
@@ -483,6 +515,9 @@ export default function ChatPage() {
|
|||||||
writingArtifactPath,
|
writingArtifactPath,
|
||||||
singleRoundRag,
|
singleRoundRag,
|
||||||
singleRoundRagSelected,
|
singleRoundRagSelected,
|
||||||
|
multiRoundRag,
|
||||||
|
multiRoundRagSelected,
|
||||||
|
frontendRagSelected,
|
||||||
routeBase,
|
routeBase,
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
setThreadId,
|
setThreadId,
|
||||||
@@ -580,8 +615,12 @@ export default function ChatPage() {
|
|||||||
singleRoundRag.stop();
|
singleRoundRag.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (multiRoundRag.isLoading) {
|
||||||
|
multiRoundRag.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [singleRoundRag, thread]);
|
}, [multiRoundRag, singleRoundRag, thread]);
|
||||||
|
|
||||||
const promptPrefixProp = useMemo(() => {
|
const promptPrefixProp = useMemo(() => {
|
||||||
if (!promptPrefixSettings?.enabled) return undefined;
|
if (!promptPrefixSettings?.enabled) return undefined;
|
||||||
@@ -776,13 +815,18 @@ export default function ChatPage() {
|
|||||||
: "ready"
|
: "ready"
|
||||||
}
|
}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
extraModels={SINGLE_ROUND_RAG_MODELS}
|
extraModels={
|
||||||
|
singleRoundRagSelected
|
||||||
|
? SINGLE_ROUND_RAG_MODELS
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
disabled={
|
disabled={
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||||
isUploading ||
|
isUploading ||
|
||||||
agentHandoffPending ||
|
agentHandoffPending ||
|
||||||
knowledgeSelectionLoading ||
|
knowledgeSelectionLoading ||
|
||||||
singleRoundRag.isLoading
|
singleRoundRag.isLoading ||
|
||||||
|
multiRoundRag.isLoading
|
||||||
}
|
}
|
||||||
onContextChange={(context) =>
|
onContextChange={(context) =>
|
||||||
setSettings("context", context)
|
setSettings("context", context)
|
||||||
|
|||||||
Reference in new issue
Block a user