Merge branch 'master-nj' into 'master'

Master nj

See merge request knowledge-base-bj/deerflow-code!23
This commit is contained in:
董宏凯 committed 2026-08-14 05:01:17 +00:00
commit 96bfdacee5
6 files changed
+2906 -1014

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -22,10 +22,10 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type ComponentProps, type ComponentProps,
type ReactNode, type ReactNode,
} from "react"; } from "react";
import { import {
PromptInput, PromptInput,
@@ -62,10 +62,10 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { apiFetch } from "@/core/api"; import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config"; import { getBackendBaseURL } from "@/core/config";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { isHiddenFromUIMessage } from "@/core/messages/utils"; import { isHiddenFromUIMessage } from "@/core/messages/utils";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import type { Model } from "@/core/models/types"; import type { Model } from "@/core/models/types";
import type { AgentThreadContext } from "@/core/threads"; import type { AgentThreadContext } from "@/core/threads";
import { textOfMessage } from "@/core/threads/utils"; import { textOfMessage } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize"; import { desensitize } from "@/lib/desensitize";
@@ -134,11 +134,11 @@ export function InputBox({
onFollowupsVisibilityChange, onFollowupsVisibilityChange,
onSubmit, onSubmit,
onStop, onStop,
promptPrefix, promptPrefix,
agentSelector, agentSelector,
knowledgeSelector, knowledgeSelector,
extraModels = [], extraModels = [],
assistantId, assistantId,
hideAdminRecommendedQuestions, hideAdminRecommendedQuestions,
compact, compact,
...props ...props
@@ -194,28 +194,28 @@ export function InputBox({
* homepage (``ChatPage``) passes this; ``AgentChatPage`` omits it because the * homepage (``ChatPage``) passes this; ``AgentChatPage`` omits it because the
* thread is already bound to one agent. * thread is already bound to one agent.
*/ */
agentSelector?: { agentSelector?: {
selectedAgentId: string | null; selectedAgentId: string | null;
onAgentChange: (agentId: string | null) => void; onAgentChange: (agentId: string | null) => void;
}; };
/** WeKnora-style knowledge-space picker rendered inside the composer toolbar. */ /** WeKnora-style knowledge-space picker rendered inside the composer toolbar. */
knowledgeSelector?: ReactNode; knowledgeSelector?: ReactNode;
/** Page-scoped model choices implemented outside the DeerFlow model API. */ /** Page-scoped model choices implemented outside the DeerFlow model API. */
extraModels?: Model[]; extraModels?: Model[];
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const [localSettings, setLocalSettings] = useLocalSettings(); const [localSettings, setLocalSettings] = useLocalSettings();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [modelDialogOpen, setModelDialogOpen] = useState(false); const [modelDialogOpen, setModelDialogOpen] = useState(false);
const { models } = useModels(); const { models } = useModels();
const singleRoundRagActive = const singleRoundRagActive =
context.execution_profile === "single_round_rag"; context.execution_profile === "single_round_rag";
const multiRoundRagActive = const multiRoundRagActive =
context.execution_profile === "multi_round_rag"; context.execution_profile === "multi_round_rag";
const frontendRagActive = singleRoundRagActive || multiRoundRagActive; const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
const modelOptions = useMemo(() => { const modelOptions = useMemo(() => {
return singleRoundRagActive ? extraModels : models; return singleRoundRagActive ? extraModels : models;
}, [extraModels, models, singleRoundRagActive]); }, [extraModels, models, singleRoundRagActive]);
const { thread, isMock } = useThread(); const { thread, isMock } = useThread();
const { textInput } = usePromptInputController(); const { textInput } = usePromptInputController();
const promptRootRef = useRef<HTMLDivElement | null>(null); const promptRootRef = useRef<HTMLDivElement | null>(null);
@@ -238,46 +238,46 @@ export function InputBox({
!userPreferences.hide_admin_prompt_prefix && !userPreferences.hide_admin_prompt_prefix &&
recommendedQuestions.length > 0; recommendedQuestions.length > 0;
useEffect(() => { useEffect(() => {
// Multi-round RAG is a frontend execution profile, not a model option. // Multi-round RAG is a frontend execution profile, not a model option.
// Do not let the normal model fallback clear it on every render. // Do not let the normal model fallback clear it on every render.
if (multiRoundRagActive) { if (multiRoundRagActive) {
return; return;
} }
if (modelOptions.length === 0) { if (modelOptions.length === 0) {
return; return;
} }
const currentModel = modelOptions.find((m) => m.name === context.model_name); const currentModel = modelOptions.find((m) => m.name === context.model_name);
const fallbackModel = currentModel ?? modelOptions[0]!; const fallbackModel = currentModel ?? modelOptions[0]!;
const supportsThinking = fallbackModel.supports_thinking ?? false; const supportsThinking = fallbackModel.supports_thinking ?? false;
const nextModelName = fallbackModel.name; const nextModelName = fallbackModel.name;
const nextMode = getResolvedMode(context.mode, supportsThinking); const nextMode = getResolvedMode(context.mode, supportsThinking);
if ( if (
context.model_name === nextModelName && context.model_name === nextModelName &&
context.mode === nextMode && context.mode === nextMode &&
context.execution_profile === fallbackModel.execution_profile context.execution_profile === fallbackModel.execution_profile
) { ) {
return; return;
} }
onContextChange?.({ onContextChange?.({
...context, ...context,
model_name: nextModelName, model_name: nextModelName,
mode: nextMode, mode: nextMode,
execution_profile: fallbackModel.execution_profile, execution_profile: fallbackModel.execution_profile,
}); });
}, [context, modelOptions, multiRoundRagActive, onContextChange]); }, [context, modelOptions, multiRoundRagActive, onContextChange]);
const selectedModel = useMemo(() => {
if (modelOptions.length === 0) {
return undefined;
}
return modelOptions.find((m) => m.name === context.model_name) ?? modelOptions[0];
}, [context.model_name, modelOptions]);
const resolvedModelName = selectedModel?.name; const selectedModel = useMemo(() => {
const selectedSingleRoundRag = singleRoundRagActive; if (modelOptions.length === 0) {
return undefined;
}
return modelOptions.find((m) => m.name === context.model_name) ?? modelOptions[0];
}, [context.model_name, modelOptions]);
const resolvedModelName = selectedModel?.name;
const selectedSingleRoundRag = singleRoundRagActive;
const supportThinking = useMemo( const supportThinking = useMemo(
() => selectedModel?.supports_thinking ?? false, () => selectedModel?.supports_thinking ?? false,
@@ -291,72 +291,72 @@ export function InputBox({
const handleModelSelect = useCallback( const handleModelSelect = useCallback(
(model_name: string) => { (model_name: string) => {
const model = modelOptions.find((m) => m.name === model_name); const model = modelOptions.find((m) => m.name === model_name);
if (!model) { if (!model) {
return; return;
} }
onContextChange?.({ onContextChange?.({
...context, ...context,
model_name, model_name,
mode: getResolvedMode(context.mode, model.supports_thinking ?? false), mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
reasoning_effort: context.reasoning_effort, reasoning_effort: context.reasoning_effort,
execution_profile: model.execution_profile, execution_profile: model.execution_profile,
}); });
setModelDialogOpen(false); setModelDialogOpen(false);
}, },
[onContextChange, context, modelOptions], [onContextChange, context, modelOptions],
); );
const handleModeSelect = useCallback( const handleModeSelect = useCallback(
(mode: InputMode) => { (mode: InputMode) => {
const ordinaryModel = const ordinaryModel =
models.find((model) => model.name === context.model_name) ?? models[0]; models.find((model) => model.name === context.model_name) ?? models[0];
onContextChange?.({ onContextChange?.({
...context, ...context,
model_name: ordinaryModel?.name ?? context.model_name, model_name: ordinaryModel?.name ?? context.model_name,
mode: getResolvedMode( mode: getResolvedMode(
mode, mode,
ordinaryModel?.supports_thinking ?? supportThinking, ordinaryModel?.supports_thinking ?? supportThinking,
), ),
reasoning_effort: reasoning_effort:
mode === "ultra" mode === "ultra"
? "high" ? "high"
: mode === "pro" : mode === "pro"
? "medium" ? "medium"
: mode === "thinking" : mode === "thinking"
? "low" ? "low"
: "minimal", : "minimal",
execution_profile: undefined, execution_profile: undefined,
}); });
}, },
[onContextChange, context, models, supportThinking], [onContextChange, context, models, supportThinking],
); );
const handleSingleRoundRagSelect = useCallback(() => { const handleSingleRoundRagSelect = useCallback(() => {
const ragModel = const ragModel =
extraModels.find((model) => model.name === context.model_name) ?? extraModels.find((model) => model.name === context.model_name) ??
extraModels[0]; extraModels[0];
if (!ragModel) { if (!ragModel) {
return; return;
} }
onContextChange?.({ onContextChange?.({
...context, ...context,
model_name: ragModel.name, model_name: ragModel.name,
mode: getResolvedMode( mode: getResolvedMode(
context.mode, context.mode,
ragModel.supports_thinking ?? false, ragModel.supports_thinking ?? false,
), ),
execution_profile: "single_round_rag", execution_profile: "single_round_rag",
}); });
}, [context, extraModels, onContextChange]); }, [context, extraModels, onContextChange]);
const handleMultiRoundRagSelect = useCallback(() => { const handleMultiRoundRagSelect = useCallback(() => {
onContextChange?.({ onContextChange?.({
...context, ...context,
execution_profile: "multi_round_rag", execution_profile: "multi_round_rag",
}); });
}, [context, onContextChange]); }, [context, onContextChange]);
const handleReasoningEffortSelect = useCallback( const handleReasoningEffortSelect = useCallback(
(effort: "minimal" | "low" | "medium" | "high") => { (effort: "minimal" | "low" | "medium" | "high") => {
@@ -422,11 +422,11 @@ export function InputBox({
onContextChange?.({ onContextChange?.({
...context, ...context,
model_name: resolvedModelName, model_name: resolvedModelName,
mode: getResolvedMode( mode: getResolvedMode(
context.mode, context.mode,
selectedModel?.supports_thinking ?? false, selectedModel?.supports_thinking ?? false,
), ),
execution_profile: selectedModel?.execution_profile, execution_profile: selectedModel?.execution_profile,
}); });
setTimeout(() => onSubmit?.(message), 0); setTimeout(() => onSubmit?.(message), 0);
return; return;
@@ -441,8 +441,8 @@ export function InputBox({
onStop, onStop,
resolvedModelName, resolvedModelName,
ragModeEnabled, ragModeEnabled,
selectedModel?.supports_thinking, selectedModel?.supports_thinking,
selectedModel?.execution_profile, selectedModel?.execution_profile,
status, status,
threadId, threadId,
], ],
@@ -500,9 +500,9 @@ export function InputBox({
}, [pendingSuggestion, requestFormSubmit, textInput]); }, [pendingSuggestion, requestFormSubmit, textInput]);
const showFollowups = const showFollowups =
!disabled && !disabled &&
!isNewThread && !isNewThread &&
!selectedSingleRoundRag && !selectedSingleRoundRag &&
!followupsHidden && !followupsHidden &&
(followupsLoading || followups.length > 0); (followupsLoading || followups.length > 0);
@@ -528,19 +528,19 @@ export function InputBox({
return; return;
} }
if (disabled || isMock || selectedSingleRoundRag) { if (disabled || isMock || selectedSingleRoundRag) {
return; return;
} }
const visibleMessages = thread.messages.filter((m) => !isHiddenFromUIMessage(m)); const visibleMessages = thread.messages.filter((m) => !isHiddenFromUIMessage(m));
const lastAi = [...visibleMessages].reverse().find((m) => m.type === "ai"); const lastAi = [...visibleMessages].reverse().find((m) => m.type === "ai");
const lastAiId = lastAi?.id ?? null; const lastAiId = lastAi?.id ?? null;
if (!lastAiId || lastAiId === lastGeneratedForAiIdRef.current) { if (!lastAiId || lastAiId === lastGeneratedForAiIdRef.current) {
return; return;
} }
lastGeneratedForAiIdRef.current = lastAiId; lastGeneratedForAiIdRef.current = lastAiId;
const recent = visibleMessages const recent = visibleMessages
.filter((m) => m.type === "human" || m.type === "ai") .filter((m) => m.type === "human" || m.type === "ai")
.map((m) => { .map((m) => {
const role = m.type === "human" ? "user" : "assistant"; const role = m.type === "human" ? "user" : "assistant";
@@ -590,7 +590,7 @@ export function InputBox({
}); });
return () => controller.abort(); return () => controller.abort();
}, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]); }, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]);
return ( return (
<div ref={promptRootRef} className="relative flex flex-col gap-6"> <div ref={promptRootRef} className="relative flex flex-col gap-6">
@@ -677,7 +677,7 @@ export function InputBox({
: "*:data-[slot='input-group']:rounded-3xl", : "*:data-[slot='input-group']:rounded-3xl",
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边) // 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
compact && isNewThread compact && isNewThread
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[rgb(var(--legacy-primary-rgb)/35%)] focus-within:ring-4 focus-within:ring-[rgb(var(--legacy-primary-rgb)/5%)] focus-within:shadow-[0_10px_35px_rgb(var(--legacy-primary-rgb)/4%)] dark:border-slate-700 dark:bg-slate-800" ? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[rgb(var(--legacy-primary-rgb)/35%)] focus-within:ring-4 focus-within:ring-[rgb(var(--legacy-primary-rgb)/5%)] focus-within:shadow-[0_10px_35px_rgb(var(--legacy-primary-rgb)/4%)] dark:border-slate-700 dark:bg-slate-800"
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边 : // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
// 对话态:常规白卡 + 中性边框 + 轻阴影 // 对话态:常规白卡 + 中性边框 + 轻阴影
isNewThread isNewThread
@@ -727,16 +727,16 @@ export function InputBox({
/> />
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> */} </PromptInputActionMenu> */}
{!frontendRagActive && ( {!frontendRagActive && (
<AddAttachmentsButton <AddAttachmentsButton
className={cn( className={cn(
"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", "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",
compact && compact &&
"gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium", "gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium",
)} )}
/> />
)} )}
{!frontendRagActive && <Tooltip {!frontendRagActive && <Tooltip
content={ content={
context.writing_mode context.writing_mode
? "写作模式已开启:最终输出会强制保存为 Markdown 文件" ? "写作模式已开启:最终输出会强制保存为 Markdown 文件"
@@ -746,23 +746,23 @@ export function InputBox({
<PromptInputButton <PromptInputButton
className={cn( className={cn(
"gap-1.5! rounded-full px-3! shadow-none", "gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium", compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
context.writing_mode context.writing_mode
? "border border-amber-300 bg-amber-50 text-amber-700 ring-1 ring-amber-300/35 hover:bg-amber-100 dark:border-amber-300/70 dark:bg-amber-400/20 dark:text-amber-100 dark:ring-amber-300/35 dark:shadow-[0_0_0_1px_rgba(251,191,36,0.18),0_0_18px_rgba(251,191,36,0.18)] dark:hover:bg-amber-400/25" ? "border border-amber-300 bg-amber-50 text-amber-700 ring-1 ring-amber-300/35 hover:bg-amber-100 dark:border-amber-300/70 dark:bg-amber-400/20 dark:text-amber-100 dark:ring-amber-300/35 dark:shadow-[0_0_0_1px_rgba(251,191,36,0.18),0_0_18px_rgba(251,191,36,0.18)] dark:hover:bg-amber-400/25"
: compact : compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground" ? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent 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", : "border-0 bg-transparent 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",
)} )}
onClick={handleWritingModeToggle} onClick={handleWritingModeToggle}
> >
{context.writing_mode && ( {context.writing_mode && (
<CheckIcon className="size-3 text-amber-700 dark:text-amber-100" /> <CheckIcon className="size-3 text-amber-700 dark:text-amber-100" />
)} )}
<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>}
{!frontendRagActive && <Tooltip {!frontendRagActive && <Tooltip
content={ content={
memoryInjectionEnabled memoryInjectionEnabled
? "记忆注入已开启:对话前会注入历史记忆" ? "记忆注入已开启:对话前会注入历史记忆"
@@ -771,24 +771,24 @@ export function InputBox({
> >
<PromptInputButton <PromptInputButton
className={cn( className={cn(
"gap-1.5! rounded-full px-3! shadow-none", "gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium", compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
memoryInjectionEnabled memoryInjectionEnabled
? "border border-blue-300 bg-blue-50 text-blue-700 ring-1 ring-blue-300/35 hover:bg-blue-100 dark:border-cyan-300/70 dark:bg-cyan-400/20 dark:text-cyan-50 dark:ring-cyan-300/35 dark:shadow-[0_0_0_1px_rgba(34,211,238,0.18),0_0_18px_rgba(34,211,238,0.18)] dark:hover:bg-cyan-400/25" ? "border border-blue-300 bg-blue-50 text-blue-700 ring-1 ring-blue-300/35 hover:bg-blue-100 dark:border-cyan-300/70 dark:bg-cyan-400/20 dark:text-cyan-50 dark:ring-cyan-300/35 dark:shadow-[0_0_0_1px_rgba(34,211,238,0.18),0_0_18px_rgba(34,211,238,0.18)] dark:hover:bg-cyan-400/25"
: compact : compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground" ? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent 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", : "border-0 bg-transparent 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",
)} )}
onClick={handleMemoryInjectionToggle} onClick={handleMemoryInjectionToggle}
> >
{memoryInjectionEnabled && ( {memoryInjectionEnabled && (
<CheckIcon className="size-3 text-blue-700 dark:text-cyan-50" /> <CheckIcon className="size-3 text-blue-700 dark:text-cyan-50" />
)} )}
<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>}
{!frontendRagActive && <Tooltip {!frontendRagActive && <Tooltip
content={ content={
ragModeEnabled ragModeEnabled
? "参考文献模式已开启:检索类结果会按编号引用展示" ? "参考文献模式已开启:检索类结果会按编号引用展示"
@@ -817,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>}
{!frontendRagActive && <Tooltip {!frontendRagActive && <Tooltip
content={ content={
contextCompressionEnabled contextCompressionEnabled
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token" ? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
@@ -846,70 +846,70 @@ 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={
context.mode === "flash" || context.mode === "flash" ||
context.mode === "thinking" || context.mode === "thinking" ||
context.mode === "pro" || context.mode === "pro" ||
context.mode === "ultra" context.mode === "ultra"
? context.mode ? context.mode
: "flash" : "flash"
} }
tooltipContent={ tooltipContent={
singleRoundRagActive singleRoundRagActive
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料" ? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
: multiRoundRagActive : multiRoundRagActive
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料" ? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
: undefined : undefined
} }
> >
<PromptInputActionMenuTrigger <PromptInputActionMenuTrigger
className={cn( 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", "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 &&
(frontendRagActive (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"
: context.mode === "pro" : context.mode === "pro"
? "border-violet-200 bg-violet-50/60 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200" ? "border-violet-200 bg-violet-50/60 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200"
: context.mode === "ultra" : context.mode === "ultra"
? "border-amber-200 bg-amber-50/60 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200" ? "border-amber-200 bg-amber-50/60 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200"
: "border-transparent bg-muted/40 hover:bg-muted hover:text-foreground"), : "border-transparent bg-muted/40 hover:bg-muted hover:text-foreground"),
)} )}
> >
<div> <div>
{frontendRagActive ? ( {frontendRagActive ? (
<BookMarkedIcon className="size-3" /> <BookMarkedIcon className="size-3" />
) : context.mode === "flash" ? ( ) : context.mode === "flash" ? (
<ZapIcon className="size-3" /> <ZapIcon className="size-3" />
) : context.mode === "thinking" ? ( ) : context.mode === "thinking" ? (
<LightbulbIcon className="size-3" /> <LightbulbIcon className="size-3" />
) : context.mode === "pro" ? ( ) : context.mode === "pro" ? (
<GraduationCapIcon className="size-3" /> <GraduationCapIcon className="size-3" />
) : context.mode === "ultra" ? ( ) : context.mode === "ultra" ? (
<RocketIcon className="size-3 text-[#dabb5e]" /> <RocketIcon className="size-3 text-[#dabb5e]" />
) : null} ) : null}
</div> </div>
<div <div
className={cn( className={cn(
"text-xs font-normal", "text-xs font-normal",
context.mode === "ultra" ? "golden-text" : "", context.mode === "ultra" ? "golden-text" : "",
)} )}
> >
{frontendRagActive {frontendRagActive
? multiRoundRagActive ? multiRoundRagActive
? "多轮 RAG" ? "多轮 RAG"
: "单轮 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) ||
(context.mode === "pro" && t.inputBox.proMode) || (context.mode === "pro" && t.inputBox.proMode) ||
(context.mode === "ultra" && t.inputBox.ultraMode)} (context.mode === "ultra" && t.inputBox.ultraMode)}
</div> </div>
</PromptInputActionMenuTrigger> </PromptInputActionMenuTrigger>
</ModeHoverGuide> </ModeHoverGuide>
<PromptInputActionMenuContent className="w-80"> <PromptInputActionMenuContent className="w-80">
@@ -918,11 +918,11 @@ export function InputBox({
{t.inputBox.mode} {t.inputBox.mode}
</DropdownMenuLabel> </DropdownMenuLabel>
<PromptInputActionMenu> <PromptInputActionMenu>
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "flash" !frontendRagActive && context.mode === "flash"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
onSelect={() => handleModeSelect("flash")} onSelect={() => handleModeSelect("flash")}
> >
@@ -930,9 +930,9 @@ export function InputBox({
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<ZapIcon <ZapIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "flash" && !frontendRagActive && context.mode === "flash" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
{t.inputBox.flashMode} {t.inputBox.flashMode}
@@ -941,7 +941,7 @@ export function InputBox({
{t.inputBox.flashModeDescription} {t.inputBox.flashModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && 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" />
@@ -949,9 +949,9 @@ export function InputBox({
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
{supportThinking && ( {supportThinking && (
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "thinking" !frontendRagActive && context.mode === "thinking"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
onSelect={() => handleModeSelect("thinking")} onSelect={() => handleModeSelect("thinking")}
@@ -960,9 +960,9 @@ export function InputBox({
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<LightbulbIcon <LightbulbIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "thinking" && !frontendRagActive && context.mode === "thinking" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
{t.inputBox.reasoningMode} {t.inputBox.reasoningMode}
@@ -971,7 +971,7 @@ export function InputBox({
{t.inputBox.reasoningModeDescription} {t.inputBox.reasoningModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && 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" />
@@ -979,8 +979,8 @@ export function InputBox({
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
)} )}
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "pro" !frontendRagActive && context.mode === "pro"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -990,8 +990,8 @@ export function InputBox({
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<GraduationCapIcon <GraduationCapIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "pro" && !frontendRagActive && context.mode === "pro" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
@@ -1001,15 +1001,15 @@ export function InputBox({
{t.inputBox.proModeDescription} {t.inputBox.proModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && 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" />
)} )}
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "ultra" !frontendRagActive && context.mode === "ultra"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -1019,17 +1019,17 @@ export function InputBox({
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<RocketIcon <RocketIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && !frontendRagActive &&
context.mode === "ultra" && context.mode === "ultra" &&
"text-[#dabb5e]", "text-[#dabb5e]",
)} )}
/> />
<div <div
className={cn( className={cn(
!frontendRagActive && !frontendRagActive &&
context.mode === "ultra" && context.mode === "ultra" &&
"golden-text", "golden-text",
)} )}
> >
{t.inputBox.ultraMode} {t.inputBox.ultraMode}
@@ -1039,78 +1039,78 @@ export function InputBox({
{t.inputBox.ultraModeDescription} {t.inputBox.ultraModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && 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" />
)} )}
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
{extraModels.length > 0 && ( {extraModels.length > 0 && (
<> <>
<DropdownMenuSeparator className="my-2" /> <DropdownMenuSeparator className="my-2" />
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
selectedSingleRoundRag selectedSingleRoundRag
? "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" ? "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", : "text-muted-foreground/65",
)} )}
onSelect={handleSingleRoundRagSelect} onSelect={handleSingleRoundRagSelect}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<BookMarkedIcon <BookMarkedIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
selectedSingleRoundRag && "text-emerald-700 dark:text-emerald-200", selectedSingleRoundRag && "text-emerald-700 dark:text-emerald-200",
)} )}
/> />
单轮 RAG 问答 单轮 RAG 问答
</div> </div>
<div className="pl-7 text-xs"> <div className="pl-7 text-xs">
基于知识库检索生成答案,并展示思考内容与参考资料 基于知识库检索生成答案,并展示思考内容与参考资料
</div> </div>
</div> </div>
{selectedSingleRoundRag ? ( {selectedSingleRoundRag ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
)} )}
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
</> </>
)} )}
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
multiRoundRagActive 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" ? "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", : "text-muted-foreground/65",
)} )}
onSelect={handleMultiRoundRagSelect} onSelect={handleMultiRoundRagSelect}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold"> <div className="flex items-center gap-1 font-bold">
<BookMarkedIcon <BookMarkedIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
multiRoundRagActive && "text-emerald-700 dark:text-emerald-200", multiRoundRagActive && "text-emerald-700 dark:text-emerald-200",
)} )}
/> />
多轮 RAG 问答 多轮 RAG 问答
</div> </div>
<div className="pl-7 text-xs"> <div className="pl-7 text-xs">
多次检索、展示过程与参考资料,适合复杂问题 多次检索、展示过程与参考资料,适合复杂问题
</div> </div>
</div> </div>
{multiRoundRagActive ? ( {multiRoundRagActive ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
)} )}
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
</PromptInputActionMenu> </PromptInputActionMenu>
</DropdownMenuGroup> </DropdownMenuGroup>
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> </PromptInputActionMenu>
{!frontendRagActive && 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">
@@ -1226,10 +1226,9 @@ export function InputBox({
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> </PromptInputActionMenu>
)} )}
</PromptInputTools> </PromptInputTools>
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}> <PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
{!frontendRagActive && agentSelector && ( {!frontendRagActive && agentSelector && (
{!frontendRagActive && knowledgeSelector}
<AgentSelector <AgentSelector
selectedAgentId={agentSelector.selectedAgentId} selectedAgentId={agentSelector.selectedAgentId}
onAgentChange={agentSelector.onAgentChange} onAgentChange={agentSelector.onAgentChange}
@@ -1237,7 +1236,8 @@ export function InputBox({
compact={compact} compact={compact}
/> />
)} )}
{!frontendRagActive && promptPrefix && ( {!frontendRagActive && knowledgeSelector}
{!frontendRagActive && promptPrefix && (
<Tooltip <Tooltip
content={ content={
promptPrefix.on promptPrefix.on
@@ -1265,7 +1265,7 @@ export function InputBox({
</PromptInputButton> </PromptInputButton>
</Tooltip> </Tooltip>
)} )}
{!multiRoundRagActive && <ModelSelector {!multiRoundRagActive && <ModelSelector
open={modelDialogOpen} open={modelDialogOpen}
onOpenChange={setModelDialogOpen} onOpenChange={setModelDialogOpen}
> >
@@ -1292,7 +1292,7 @@ export function InputBox({
<ModelSelectorContent> <ModelSelectorContent>
<ModelSelectorInput placeholder={t.inputBox.searchModels} /> <ModelSelectorInput placeholder={t.inputBox.searchModels} />
<ModelSelectorList> <ModelSelectorList>
{modelOptions.map((m) => ( {modelOptions.map((m) => (
<ModelSelectorItem <ModelSelectorItem
key={m.name} key={m.name}
value={m.name} value={m.name}
@@ -1300,11 +1300,11 @@ export function InputBox({
> >
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
<ModelSelectorName>{m.display_name}</ModelSelectorName> <ModelSelectorName>{m.display_name}</ModelSelectorName>
<span className="text-muted-foreground truncate text-[10px]"> <span className="text-muted-foreground truncate text-[10px]">
{selectedSingleRoundRag {selectedSingleRoundRag
? `单轮知识库问答 · ${m.name}` ? `单轮知识库问答 · ${m.name}`
: m.model} : m.model}
</span> </span>
</div> </div>
{m.name === context.model_name ? ( {m.name === context.model_name ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
@@ -1312,15 +1312,15 @@ export function InputBox({
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
)} )}
</ModelSelectorItem> </ModelSelectorItem>
))} ))}
</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",
compact && compact &&
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--button-foreground,var(--primary-foreground))] hover:opacity-90 disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--button-foreground,var(--primary-foreground))] dark:hover:opacity-90 dark:disabled:bg-muted", "size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--button-foreground,var(--primary-foreground))] hover:opacity-90 disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--button-foreground,var(--primary-foreground))] dark:hover:opacity-90 dark:disabled:bg-muted",
)} )}
disabled={disabled} disabled={disabled}
variant="outline" variant="outline"
File diff suppressed because it is too large. Load diff
@@ -30,12 +30,13 @@ import { useSkills } from "@/core/skills/hooks";
import { import {
buildSkillDisplayMap, buildSkillDisplayMap,
collectRuntimeDisplaySources, collectRuntimeDisplaySources,
type RuntimeDisplaySource,
} from "@/core/skill-display/runtime"; } from "@/core/skill-display/runtime";
import type { Subtask } from "@/core/tasks"; 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 { 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";
@@ -99,18 +100,18 @@ export function MessageList({
tokenUsageEnabled = false, tokenUsageEnabled = false,
extraRehypePlugins, extraRehypePlugins,
scrollButtonClassName, scrollButtonClassName,
onClarificationSubmit, onClarificationSubmit,
onEditResubmit, onEditResubmit,
onEditSave, onEditSave,
messageContentOverrides, messageContentOverrides,
externallyHiddenMessageIds, externallyHiddenMessageIds,
onSetupAgentApprove, onSetupAgentApprove,
onSetupAgentReject, onSetupAgentReject,
onSetupWritingApprove, onSetupWritingApprove,
onSetupWritingReject, onSetupWritingReject,
onSetupWritingStart, onSetupWritingStart,
onOpenArtifact, onOpenArtifact,
}: { }: {
className?: string; className?: string;
contentClassName?: string; contentClassName?: string;
fitMessageRowsToContent?: boolean; fitMessageRowsToContent?: boolean;
@@ -121,19 +122,19 @@ export function MessageList({
/** 额外定位「回到最新」悬浮按钮——不同页面输入框高度不同,需上移让位。 */ /** 额外定位「回到最新」悬浮按钮——不同页面输入框高度不同,需上移让位。 */
scrollButtonClassName?: string; scrollButtonClassName?: string;
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>; extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
onClarificationSubmit?: (answer: string) => void | Promise<void>; onClarificationSubmit?: (answer: string) => void | Promise<void>;
onEditResubmit?: (text: string) => void; onEditResubmit?: (text: string) => void;
onEditSave?: (messageId: string, text: string) => void | Promise<void>; onEditSave?: (messageId: string, text: string) => void | Promise<void>;
messageContentOverrides?: Record<string, string>; messageContentOverrides?: Record<string, string>;
externallyHiddenMessageIds?: Set<string>; externallyHiddenMessageIds?: Set<string>;
onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>; onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>; onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>; onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>; onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
/** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */ /** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */
onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>; onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onOpenArtifact?: (filepath: string) => void; onOpenArtifact?: (filepath: string) => void;
}) { }) {
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
const rehypePlugins = useMemo( const rehypePlugins = useMemo(
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins), () => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins),
@@ -165,33 +166,33 @@ export function MessageList({
}); });
}, []); }, []);
const allMessages = useMemo(() => { const allMessages = useMemo(() => {
const hasOverrides = messageContentOverrides && Object.keys(messageContentOverrides).length > 0; const hasOverrides = messageContentOverrides && Object.keys(messageContentOverrides).length > 0;
return thread.messages return thread.messages
.filter((m) => !isHiddenFromUIMessage(m)) .filter((m) => !isHiddenFromUIMessage(m))
.map((message) => { .map((message) => {
const override = const override =
hasOverrides && message.id hasOverrides && message.id
? messageContentOverrides?.[message.id] ? messageContentOverrides?.[message.id]
: undefined; : undefined;
return override === undefined return override === undefined
? message ? message
: ({ ...message, content: override } as Message); : ({ ...message, content: override } as Message);
}); });
}, [messageContentOverrides, thread.messages]); }, [messageContentOverrides, thread.messages]);
const messages = useMemo( const messages = useMemo(
() => { () => {
const externalHidden = externallyHiddenMessageIds ?? new Set<string>(); const externalHidden = externallyHiddenMessageIds ?? new Set<string>();
return hiddenMessageIds.size === 0 && externalHidden.size === 0 return hiddenMessageIds.size === 0 && externalHidden.size === 0
? allMessages ? allMessages
: allMessages.filter( : allMessages.filter(
(m) => (m) =>
!m.id || !m.id ||
(!hiddenMessageIds.has(m.id) && !externalHidden.has(m.id)), (!hiddenMessageIds.has(m.id) && !externalHidden.has(m.id)),
); );
}, },
[allMessages, externallyHiddenMessageIds, hiddenMessageIds], [allMessages, externallyHiddenMessageIds, hiddenMessageIds],
); );
const askTimes = useChatAskTimes(); const askTimes = useChatAskTimes();
@@ -237,21 +238,21 @@ export function MessageList({
}, [messages, askTimes]); }, [messages, askTimes]);
const [settings] = useLocalSettings(); const [settings] = useLocalSettings();
const ragModeEnabled = const ragModeEnabled =
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId); settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
const hasSingleRoundRag = useMemo( const hasSingleRoundRag = useMemo(
() => () =>
messages.some( messages.some(
(message) => (message) =>
isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message), isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message),
), ),
[messages], [messages],
); );
const referencesEnabled = ragModeEnabled || hasSingleRoundRag; const referencesEnabled = ragModeEnabled || hasSingleRoundRag;
const { skills } = useSkills(); const { skills } = useSkills();
const skillDisplayMap = useMemo( const skillDisplayMap = useMemo(
() => (referencesEnabled ? buildSkillDisplayMap(skills) : new Map()), () => (referencesEnabled ? buildSkillDisplayMap(skills) : new Map()),
[referencesEnabled, skills], [referencesEnabled, skills],
); );
// Same per-turn reference batches the right-side panel renders (backend // Same per-turn reference batches the right-side panel renders (backend
// batches preferred). Inline [n] tokens must resolve against these so badge // batches preferred). Inline [n] tokens must resolve against these so badge
@@ -259,6 +260,53 @@ export function MessageList({
// frontend tool-message scan below comes up empty (e.g. after a reload). // frontend tool-message scan below comes up empty (e.g. after a reload).
const referenceBatches = useContext(ReferenceBatchesContext); const referenceBatches = useContext(ReferenceBatchesContext);
// 单轮/多轮 RAG 的参考资料存放在一条 hide_from_ui 的 tool 消息里(紧挨在
// assistant 消息之前)。allMessages 会把 hide_from_ui 消息过滤掉,导致通用
// 的 collectRuntimeDisplaySources 扫描不到它们。这里直接在原始消息流里按
// assistant 消息提取其对应的参考资料,保证 `[1]` 等编号能解析成可点击引用。
const frontendRagSourcesByAiId = useMemo(() => {
const map = new Map<string, RuntimeDisplaySource[]>();
const raw = thread.messages;
for (let i = 0; i < raw.length; i++) {
const msg = raw[i];
if (!msg || msg.type !== "ai" || !msg.id) continue;
const kwargs = msg.additional_kwargs;
if (!kwargs || (kwargs.single_round_rag !== true && kwargs.multi_round_rag !== true)) {
continue;
}
let sources: RuntimeDisplaySource[] = [];
for (let j = i - 1; j >= 0; j--) {
const prev = raw[j];
if (!prev) break;
// 前一条 human/ai 消息即为本轮边界,跨边界即停止。
if (prev.type === "human" || prev.type === "ai") break;
if (prev.type !== "tool") continue;
const prevKwargs = prev.additional_kwargs;
if (
!prevKwargs ||
(prevKwargs.single_round_rag !== true && prevKwargs.multi_round_rag !== true)
) {
continue;
}
const results = extractFrontendRagResults(prev.content);
if (results && results.length > 0) {
const toolName = String(
(prev as { name?: unknown }).name ??
(prevKwargs.multi_round_rag === true
? "multi_round_rag_references"
: "knowledge_base_search"),
);
sources = results.map((item, index) =>
toRuntimeDisplaySource(item, index + 1, toolName),
);
}
break;
}
map.set(msg.id, sources);
}
return map;
}, [thread.messages]);
// 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).
@@ -384,7 +432,7 @@ export function MessageList({
if (group.type === "human" || group.type === "assistant") { if (group.type === "human" || group.type === "assistant") {
return group.messages.map((msg) => { return group.messages.map((msg) => {
const isAssistantMessage = const isAssistantMessage =
referencesEnabled && referencesEnabled &&
group.type === "assistant" && group.type === "assistant" &&
msg.type === "ai"; msg.type === "ai";
const batchSources = isAssistantMessage const batchSources = isAssistantMessage
@@ -392,14 +440,21 @@ export function MessageList({
(batch) => batch.assistant_message_id === msg.id, (batch) => batch.assistant_message_id === msg.id,
)?.sources ?? [] )?.sources ?? []
: []; : [];
// 单轮/多轮 RAG 的参考资料来自隐藏的 tool 消息,优先于通用扫描。
const frontendRagSources =
isAssistantMessage && msg.id
? frontendRagSourcesByAiId.get(msg.id) ?? []
: [];
const displaySources = isAssistantMessage const displaySources = isAssistantMessage
? batchSources.length > 0 ? batchSources.length > 0
? batchSources ? batchSources
: collectRuntimeDisplaySources(messages, skillDisplayMap, { : frontendRagSources.length > 0
afterMessageId: findPreviousHumanMessageId(messages, msg.id), ? frontendRagSources
beforeMessageId: msg.id, : collectRuntimeDisplaySources(messages, skillDisplayMap, {
includeBeforeMessage: true, afterMessageId: findPreviousHumanMessageId(messages, msg.id),
}) beforeMessageId: msg.id,
includeBeforeMessage: true,
})
: []; : [];
return ( return (
<MessageListItem <MessageListItem
@@ -415,11 +470,11 @@ export function MessageList({
threadId={threadId} threadId={threadId}
tokenUsageEnabled={tokenUsageEnabled} tokenUsageEnabled={tokenUsageEnabled}
displaySources={displaySources} displaySources={displaySources}
extraRehypePlugins={extraRehypePlugins} extraRehypePlugins={extraRehypePlugins}
onEditResubmit={onEditResubmit} onEditResubmit={onEditResubmit}
onEditSave={onEditSave} onEditSave={onEditSave}
onMessageDeleted={ onMessageDeleted={
allowDelete ? handleMessageDeleted : undefined allowDelete ? handleMessageDeleted : undefined
} }
turnTime={msg.id ? turnTimes.get(msg.id) : undefined} turnTime={msg.id ? turnTimes.get(msg.id) : undefined}
/> />
@@ -524,11 +579,11 @@ export function MessageList({
className="mb-4" className="mb-4"
/> />
)} )}
<ArtifactFileList <ArtifactFileList
files={files} files={files}
threadId={threadId} threadId={threadId}
onOpenArtifact={onOpenArtifact} onOpenArtifact={onOpenArtifact}
/> />
<MessageTokenUsageList <MessageTokenUsageList
enabled={tokenUsageEnabled} enabled={tokenUsageEnabled}
isLoading={thread.isLoading} isLoading={thread.isLoading}
@@ -728,6 +783,46 @@ function findPreviousHumanMessageId(
return undefined; return undefined;
} }
/** 从单轮/多轮 RAG 的 tool 消息体里取出 `content.results`。 */
function extractFrontendRagResults(content: unknown): unknown[] | null {
if (content && typeof content === "object" && !Array.isArray(content)) {
const results = (content as { results?: unknown }).results;
if (Array.isArray(results)) return results;
}
return null;
}
/** 把单轮/多轮 RAG 的一条参考资料归一化为展示用的 RuntimeDisplaySource。 */
function toRuntimeDisplaySource(
item: unknown,
index: number,
skillName: string,
): RuntimeDisplaySource {
const record =
item && typeof item === "object" && !Array.isArray(item)
? (item as Record<string, unknown>)
: {};
const raw =
(record.raw as Record<string, unknown> | undefined) ?? record;
const content = String(record.content ?? record.snippet ?? "");
return {
id: typeof record.id === "string" && record.id ? record.id : undefined,
title: String(record.title ?? `参考资料 ${index}`),
type: String(record.type ?? "result"),
snippet: String(record.snippet ?? content),
content,
source:
typeof record.source === "string" && record.source ? record.source : undefined,
author: undefined,
time: typeof record.time === "string" && record.time ? record.time : undefined,
url: typeof record.url === "string" && record.url ? record.url : undefined,
raw,
index,
skillName,
mode: "citation",
};
}
function getSetupAgentApprovalPayload( function getSetupAgentApprovalPayload(
message: unknown, message: unknown,
): SetupAgentApprovalPayload | null { ): SetupAgentApprovalPayload | null {
+1 -5
View File
@@ -815,11 +815,7 @@ export default function ChatPage() {
: "ready" : "ready"
} }
context={settings.context} context={settings.context}
extraModels={ extraModels={SINGLE_ROUND_RAG_MODELS}
singleRoundRagSelected
? SINGLE_ROUND_RAG_MODELS
: undefined
}
disabled={ disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading || isUploading ||
+3 -9
View File
@@ -364,8 +364,7 @@ auth_login:
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。 # 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
database: database:
backend: postgres backend: sqlite
postgres_url: $POSTGRES_DATABASE_URL
sqlite_dir: .deer-flow/data sqlite_dir: .deer-flow/data
pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略) pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略)
max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow
@@ -374,13 +373,8 @@ database:
echo_sql: false echo_sql: false
checkpointer: checkpointer:
type: postgres type: sqlite
connection_string: $POSTGRES_CHECKPOINT_URL connection_string: .deer-flow/data/deerflow.db
use_postgres: true
# PostgreSQL 优先;SQLite 仅作历史记录读取兜底,确认稳定后可移除。
dual_read_enabled: true
dual_read_sqlite_paths:
- .deer-flow/data/deerflow.db
# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。 # 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。
# 三层覆盖优先级(高->低): # 三层覆盖优先级(高->低):