feat(turbo): add direct LLM chat mode with runtime toggle
Add 极速 mode that streams via /api/direct-chat without the lead agent stack, plus VITE_TURBO_MODE_VISIBLE in runtime-config.js (default off). Also disable global memory, tighten Qwen/vLLM thinking control for flash mode, and force thinking off on the direct-chat endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4f7f07504a
commit
d765afe2ac
23 files changed
+1295
-394
No files matched your search
@@ -59,6 +59,9 @@
|
||||
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
|
||||
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
||||
# VITE_COMPACT_MENU_TYPE=A
|
||||
# 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。
|
||||
# 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。
|
||||
# VITE_TURBO_MODE_VISIBLE=false
|
||||
# VITE_GITHUB_OAUTH_TOKEN=
|
||||
|
||||
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
|
||||
|
||||
@@ -151,6 +151,11 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
|
||||
VITE_GITHUB_OAUTH_TOKEN: "",
|
||||
|
||||
// 是否在问答输入框「模式」菜单显示「极速」:"true" 显示,"false" 隐藏。
|
||||
// 默认关闭。开启后可直连大模型流式问答,不走智能体工具与技能。
|
||||
// 部署后改 dist/runtime-config.js 再刷新页面即可,无需重新打包。
|
||||
VITE_TURBO_MODE_VISIBLE: "true",
|
||||
|
||||
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
|
||||
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
|
||||
VITE_SENTIMENT_AGENT_VISIBLE: "true",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CheckIcon,
|
||||
FileTextIcon,
|
||||
FolderTreeIcon,
|
||||
GaugeIcon,
|
||||
GraduationCapIcon,
|
||||
LightbulbIcon,
|
||||
PaperclipIcon,
|
||||
@@ -68,6 +69,7 @@ import { isHiddenFromUIMessage } from "@/core/messages/utils";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { Model } from "@/core/models/types";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -93,14 +95,14 @@ import {
|
||||
|
||||
import { useRecommendedQuestions } from "@/core/recommended-questions/hooks";
|
||||
import { useUserPreferences } from "@/core/user-preferences";
|
||||
import {
|
||||
isThreadRagModeLocked,
|
||||
lockThreadRagMode,
|
||||
useLocalSettings,
|
||||
} from "@/core/settings";
|
||||
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||
|
||||
import { CustomPromptBar } from "./custom-prompt-menu";
|
||||
import {
|
||||
isThreadRagModeLocked,
|
||||
lockThreadRagMode,
|
||||
useLocalSettings,
|
||||
} from "@/core/settings";
|
||||
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||
|
||||
import { CustomPromptBar } from "./custom-prompt-menu";
|
||||
import { useThread } from "./messages/context";
|
||||
import { ModeHoverGuide } from "./mode-hover-guide";
|
||||
import { Tooltip } from "./tooltip";
|
||||
@@ -223,16 +225,20 @@ export function InputBox({
|
||||
context.execution_profile === "single_round_rag";
|
||||
const multiRoundRagActive =
|
||||
context.execution_profile === "multi_round_rag";
|
||||
const turboModeVisible = isTurboModeVisible();
|
||||
const directChatActive =
|
||||
turboModeVisible && context.execution_profile === "direct";
|
||||
const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
|
||||
const bypassAgentActive = frontendRagActive || directChatActive;
|
||||
const modelOptions = useMemo(() => {
|
||||
return singleRoundRagActive ? extraModels : models;
|
||||
}, [extraModels, models, singleRoundRagActive]);
|
||||
const { thread, isMock } = useThread();
|
||||
const { textInput } = usePromptInputController();
|
||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||
const referenceModeEntryEnabled =
|
||||
citationDisplay?.reference_mode_enabled ?? false;
|
||||
const { thread, isMock } = useThread();
|
||||
const { textInput } = usePromptInputController();
|
||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||
const referenceModeEntryEnabled =
|
||||
citationDisplay?.reference_mode_enabled ?? false;
|
||||
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
@@ -253,11 +259,37 @@ export function InputBox({
|
||||
recommendedQuestions.length > 0;
|
||||
|
||||
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.
|
||||
// Frontend-only execution profiles are not model options.
|
||||
// Do not let the normal model fallback clear them on every render.
|
||||
if (!turboModeVisible && context.execution_profile === "direct") {
|
||||
onContextChange?.({
|
||||
...context,
|
||||
execution_profile: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (multiRoundRagActive) {
|
||||
return;
|
||||
}
|
||||
if (directChatActive) {
|
||||
if (models.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentModel = models.find((m) => m.name === context.model_name);
|
||||
const fallbackModel = currentModel ?? models[0]!;
|
||||
if (
|
||||
context.model_name === fallbackModel.name &&
|
||||
context.execution_profile === "direct"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onContextChange?.({
|
||||
...context,
|
||||
model_name: fallbackModel.name,
|
||||
execution_profile: "direct",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (modelOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -281,7 +313,7 @@ export function InputBox({
|
||||
mode: nextMode,
|
||||
execution_profile: fallbackModel.execution_profile,
|
||||
});
|
||||
}, [context, modelOptions, multiRoundRagActive, onContextChange]);
|
||||
}, [context, directChatActive, modelOptions, models, multiRoundRagActive, onContextChange, turboModeVisible]);
|
||||
|
||||
const selectedModel = useMemo(() => {
|
||||
if (modelOptions.length === 0) {
|
||||
@@ -314,7 +346,10 @@ export function InputBox({
|
||||
model_name,
|
||||
mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
|
||||
reasoning_effort: context.reasoning_effort,
|
||||
execution_profile: model.execution_profile,
|
||||
execution_profile:
|
||||
context.execution_profile === "direct"
|
||||
? "direct"
|
||||
: model.execution_profile,
|
||||
});
|
||||
setModelDialogOpen(false);
|
||||
},
|
||||
@@ -346,6 +381,16 @@ export function InputBox({
|
||||
[onContextChange, context, models, supportThinking],
|
||||
);
|
||||
|
||||
const handleDirectChatSelect = useCallback(() => {
|
||||
const ordinaryModel =
|
||||
models.find((model) => model.name === context.model_name) ?? models[0];
|
||||
onContextChange?.({
|
||||
...context,
|
||||
model_name: ordinaryModel?.name ?? context.model_name,
|
||||
execution_profile: "direct",
|
||||
});
|
||||
}, [context, models, onContextChange]);
|
||||
|
||||
const handleSingleRoundRagSelect = useCallback(() => {
|
||||
const ragModel =
|
||||
extraModels.find((model) => model.name === context.model_name) ??
|
||||
@@ -749,7 +794,7 @@ export function InputBox({
|
||||
/>
|
||||
</PromptInputActionMenuContent>
|
||||
</PromptInputActionMenu> */}
|
||||
{!frontendRagActive && (
|
||||
{!bypassAgentActive && (
|
||||
<AddAttachmentsButton
|
||||
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",
|
||||
@@ -758,7 +803,7 @@ export function InputBox({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
||||
{!bypassAgentActive && !hideContextTools && <Tooltip
|
||||
content={
|
||||
context.writing_mode
|
||||
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
||||
@@ -784,7 +829,7 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">写作</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>}
|
||||
{!frontendRagActive && <Tooltip
|
||||
{!bypassAgentActive && <Tooltip
|
||||
content={
|
||||
artifactDirectoryModeEnabled
|
||||
? "文件模式已开启:右侧会显示当前对话生成的全部文件目录"
|
||||
@@ -813,7 +858,7 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">文件</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>}
|
||||
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||
{!bypassAgentActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||
content={
|
||||
memoryInjectionEnabled
|
||||
? "记忆注入已开启:对话前会注入历史记忆"
|
||||
@@ -839,9 +884,9 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">记忆</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>}
|
||||
{referenceModeEntryEnabled && !frontendRagActive && !hideContextTools && <Tooltip
|
||||
content={
|
||||
ragModeEnabled
|
||||
{referenceModeEntryEnabled && !bypassAgentActive && !hideContextTools && <Tooltip
|
||||
content={
|
||||
ragModeEnabled
|
||||
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
||||
: "参考文献模式已关闭:点击开启"
|
||||
}
|
||||
@@ -869,7 +914,7 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">参考文献</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>}
|
||||
{!frontendRagActive && <Tooltip
|
||||
{!bypassAgentActive && <Tooltip
|
||||
content={
|
||||
contextCompressionEnabled
|
||||
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
||||
@@ -909,7 +954,9 @@ export function InputBox({
|
||||
: "flash"
|
||||
}
|
||||
tooltipContent={
|
||||
singleRoundRagActive
|
||||
directChatActive
|
||||
? `${t.inputBox.turboMode}:${t.inputBox.turboModeDescription}`
|
||||
: singleRoundRagActive
|
||||
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
||||
: multiRoundRagActive
|
||||
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
|
||||
@@ -921,7 +968,9 @@ 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",
|
||||
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
||||
compact &&
|
||||
(frontendRagActive
|
||||
(directChatActive
|
||||
? "border-sky-200 bg-sky-50/60 text-sky-700 hover:bg-sky-100 dark:border-sky-700 dark:bg-sky-950/40 dark:text-sky-200"
|
||||
: 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"
|
||||
: 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"
|
||||
@@ -933,7 +982,9 @@ export function InputBox({
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{frontendRagActive ? (
|
||||
{directChatActive ? (
|
||||
<GaugeIcon className="size-3" />
|
||||
) : frontendRagActive ? (
|
||||
<BookMarkedIcon className="size-3" />
|
||||
) : context.mode === "flash" ? (
|
||||
<ZapIcon className="size-3" />
|
||||
@@ -948,10 +999,14 @@ export function InputBox({
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs font-normal",
|
||||
context.mode === "ultra" ? "golden-text" : "",
|
||||
context.mode === "ultra" && !directChatActive && !frontendRagActive
|
||||
? "golden-text"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{frontendRagActive
|
||||
{directChatActive
|
||||
? t.inputBox.turboMode
|
||||
: frontendRagActive
|
||||
? multiRoundRagActive
|
||||
? "多轮 RAG"
|
||||
: "单轮 RAG"
|
||||
@@ -969,9 +1024,39 @@ export function InputBox({
|
||||
{t.inputBox.mode}
|
||||
</DropdownMenuLabel>
|
||||
<PromptInputActionMenu>
|
||||
{turboModeVisible && (
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
!frontendRagActive && context.mode === "flash"
|
||||
directChatActive
|
||||
? "bg-sky-50 text-sky-800 hover:bg-sky-100 focus:bg-sky-100 dark:bg-sky-950/40 dark:text-sky-200 dark:hover:bg-sky-950/60 dark:focus:bg-sky-950/60"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
onSelect={handleDirectChatSelect}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-1 font-bold">
|
||||
<GaugeIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
directChatActive && "text-sky-700 dark:text-sky-200",
|
||||
)}
|
||||
/>
|
||||
{t.inputBox.turboMode}
|
||||
</div>
|
||||
<div className="pl-7 text-xs">
|
||||
{t.inputBox.turboModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{directChatActive ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
)}
|
||||
</PromptInputActionMenuItem>
|
||||
)}
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
!bypassAgentActive && context.mode === "flash"
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
@@ -982,7 +1067,7 @@ export function InputBox({
|
||||
<ZapIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
!frontendRagActive && context.mode === "flash" &&
|
||||
!bypassAgentActive && context.mode === "flash" &&
|
||||
"text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
@@ -992,7 +1077,7 @@ export function InputBox({
|
||||
{t.inputBox.flashModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{!frontendRagActive && context.mode === "flash" ? (
|
||||
{!bypassAgentActive && context.mode === "flash" ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
@@ -1001,7 +1086,7 @@ export function InputBox({
|
||||
{supportThinking && (
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
!frontendRagActive && context.mode === "thinking"
|
||||
!bypassAgentActive && context.mode === "thinking"
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
@@ -1012,7 +1097,7 @@ export function InputBox({
|
||||
<LightbulbIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
!frontendRagActive && context.mode === "thinking" &&
|
||||
!bypassAgentActive && context.mode === "thinking" &&
|
||||
"text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
@@ -1022,7 +1107,7 @@ export function InputBox({
|
||||
{t.inputBox.reasoningModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{!frontendRagActive && context.mode === "thinking" ? (
|
||||
{!bypassAgentActive && context.mode === "thinking" ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
@@ -1031,7 +1116,7 @@ export function InputBox({
|
||||
)}
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
!frontendRagActive && context.mode === "pro"
|
||||
!bypassAgentActive && context.mode === "pro"
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
@@ -1042,7 +1127,7 @@ export function InputBox({
|
||||
<GraduationCapIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
!frontendRagActive && context.mode === "pro" &&
|
||||
!bypassAgentActive && context.mode === "pro" &&
|
||||
"text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
@@ -1052,7 +1137,7 @@ export function InputBox({
|
||||
{t.inputBox.proModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{!frontendRagActive && context.mode === "pro" ? (
|
||||
{!bypassAgentActive && context.mode === "pro" ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
@@ -1060,7 +1145,7 @@ export function InputBox({
|
||||
</PromptInputActionMenuItem>
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
!frontendRagActive && context.mode === "ultra"
|
||||
!bypassAgentActive && context.mode === "ultra"
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
@@ -1071,14 +1156,14 @@ export function InputBox({
|
||||
<RocketIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
!frontendRagActive &&
|
||||
!bypassAgentActive &&
|
||||
context.mode === "ultra" &&
|
||||
"text-[#dabb5e]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
!frontendRagActive &&
|
||||
!bypassAgentActive &&
|
||||
context.mode === "ultra" &&
|
||||
"golden-text",
|
||||
)}
|
||||
@@ -1090,7 +1175,7 @@ export function InputBox({
|
||||
{t.inputBox.ultraModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{!frontendRagActive && context.mode === "ultra" ? (
|
||||
{!bypassAgentActive && context.mode === "ultra" ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
@@ -1161,7 +1246,7 @@ export function InputBox({
|
||||
</DropdownMenuGroup>
|
||||
</PromptInputActionMenuContent>
|
||||
</PromptInputActionMenu>
|
||||
{!frontendRagActive && supportReasoningEffort && context.mode !== "flash" && (
|
||||
{!bypassAgentActive && supportReasoningEffort && context.mode !== "flash" && (
|
||||
<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]")}>
|
||||
<div className="text-xs font-normal">
|
||||
@@ -1279,7 +1364,7 @@ export function InputBox({
|
||||
)}
|
||||
</PromptInputTools>
|
||||
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
||||
{!frontendRagActive && agentSelector && (
|
||||
{!bypassAgentActive && agentSelector && (
|
||||
<AgentSelector
|
||||
selectedAgentId={agentSelector.selectedAgentId}
|
||||
onAgentChange={agentSelector.onAgentChange}
|
||||
@@ -1287,8 +1372,8 @@ export function InputBox({
|
||||
compact={compact}
|
||||
/>
|
||||
)}
|
||||
{!frontendRagActive && knowledgeSelector}
|
||||
{!frontendRagActive && promptPrefix && (
|
||||
{!bypassAgentActive && knowledgeSelector}
|
||||
{!bypassAgentActive && promptPrefix && (
|
||||
<Tooltip
|
||||
content={
|
||||
promptPrefix.on
|
||||
|
||||
@@ -96,6 +96,9 @@ export const enUS: Translations = {
|
||||
mode: "Mode",
|
||||
flashMode: "Flash",
|
||||
flashModeDescription: "Fast and efficient, but may not be accurate",
|
||||
turboMode: "Turbo",
|
||||
turboModeDescription:
|
||||
"Talk to the model directly with no agent tools or skills, for lower first-token latency",
|
||||
reasoningMode: "Reasoning",
|
||||
reasoningModeDescription:
|
||||
"Reasoning before action, balance between time and accuracy",
|
||||
@@ -448,17 +451,17 @@ export const enUS: Translations = {
|
||||
system: "System",
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
lightblue: "Light Blue",
|
||||
lightgreen: "Green",
|
||||
iceblue: "Glacier Blue",
|
||||
lightyellow: "Pale Yellow",
|
||||
lightblue: "Light Blue",
|
||||
lightgreen: "Green",
|
||||
iceblue: "Glacier Blue",
|
||||
lightyellow: "Pale Yellow",
|
||||
systemDescription: "Match the operating system preference automatically.",
|
||||
lightDescription: "Bright palette with higher contrast for daytime.",
|
||||
darkDescription: "Dim palette that reduces glare for focus.",
|
||||
lightblueDescription: "A crisp light palette built around blue.",
|
||||
lightgreenDescription: "A crisp light palette built around fresh green tones.",
|
||||
iceblueDescription: "An airy palette built around #9BCEF1 with accessible blue actions.",
|
||||
lightyellowDescription: "A soft palette built around #F3FBD9 with readable olive actions.",
|
||||
lightblueDescription: "A crisp light palette built around blue.",
|
||||
lightgreenDescription: "A crisp light palette built around fresh green tones.",
|
||||
iceblueDescription: "An airy palette built around #9BCEF1 with accessible blue actions.",
|
||||
lightyellowDescription: "A soft palette built around #F3FBD9 with readable olive actions.",
|
||||
languageTitle: "Language",
|
||||
languageDescription: "Switch between languages.",
|
||||
},
|
||||
@@ -545,13 +548,13 @@ export const enUS: Translations = {
|
||||
filesSaveAction: "Save",
|
||||
filesCancelEditAction: "Cancel",
|
||||
filesDownloadAction: "Download",
|
||||
filesCopyPathAction: "Copy path",
|
||||
filesDiscardConfirm: "Discard unsaved changes?",
|
||||
filesSaveSuccess: "File saved",
|
||||
filesDeleteAction: "Delete",
|
||||
filesDeleteConfirm: "Delete \"{path}\"? This cannot be undone.",
|
||||
filesDeleteSuccess: "File deleted",
|
||||
filesReadOnlyHint: "Only administrators can edit or delete built-in skill files.",
|
||||
filesCopyPathAction: "Copy path",
|
||||
filesDiscardConfirm: "Discard unsaved changes?",
|
||||
filesSaveSuccess: "File saved",
|
||||
filesDeleteAction: "Delete",
|
||||
filesDeleteConfirm: "Delete \"{path}\"? This cannot be undone.",
|
||||
filesDeleteSuccess: "File deleted",
|
||||
filesReadOnlyHint: "Only administrators can edit or delete built-in skill files.",
|
||||
viewDetailAction: "View skill details",
|
||||
stateSheet: {
|
||||
archivedTitle: "Archived skills",
|
||||
@@ -591,26 +594,26 @@ export const enUS: Translations = {
|
||||
chooseFile: "Choose file",
|
||||
validating: "Validating archive…",
|
||||
installing: "Installing…",
|
||||
replace: "Upload again and replace",
|
||||
replace: "Upload again and replace",
|
||||
install: "Install",
|
||||
cancel: "Cancel",
|
||||
nameLabel: "Name",
|
||||
descriptionLabel: "Description",
|
||||
licenseLabel: "License",
|
||||
toolsLabel: "Allowed tools",
|
||||
warningsLabel: "Warnings",
|
||||
conflictOwnedByMe:
|
||||
"You already uploaded a skill named '{name}'. You may upload it again and replace your existing copy (its enabled state will be kept).",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"The custom skill '{name}' belongs to you. Switch the target to 'Custom skill' to replace it.",
|
||||
conflictOwnedByOther:
|
||||
"Skill '{name}' was uploaded by user '{owner}' and cannot be replaced. Rename the skill before uploading.",
|
||||
conflictCustom:
|
||||
"A custom skill named '{name}' already exists, but its uploader could not be identified. Rename it to avoid overwriting someone else's skill.",
|
||||
warningsLabel: "Warnings",
|
||||
conflictOwnedByMe:
|
||||
"You already uploaded a skill named '{name}'. You may upload it again and replace your existing copy (its enabled state will be kept).",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"The custom skill '{name}' belongs to you. Switch the target to 'Custom skill' to replace it.",
|
||||
conflictOwnedByOther:
|
||||
"Skill '{name}' was uploaded by user '{owner}' and cannot be replaced. Rename the skill before uploading.",
|
||||
conflictCustom:
|
||||
"A custom skill named '{name}' already exists, but its uploader could not be identified. Rename it to avoid overwriting someone else's skill.",
|
||||
conflictPublic:
|
||||
"A built-in public skill named '{name}' already exists and cannot be overwritten.",
|
||||
conflictPublicAdminHint:
|
||||
"A built-in public skill named '{name}' exists. Click 'Upload again and replace' to overwrite it.",
|
||||
conflictPublicAdminHint:
|
||||
"A built-in public skill named '{name}' exists. Click 'Upload again and replace' to overwrite it.",
|
||||
installSuccess: "Skill '{name}' installed.",
|
||||
installSuccessReplaced: "Skill '{name}' replaced.",
|
||||
rejectInvalidExt: "Only .zip or .skill files are accepted.",
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface Translations {
|
||||
mode: string;
|
||||
flashMode: string;
|
||||
flashModeDescription: string;
|
||||
turboMode: string;
|
||||
turboModeDescription: string;
|
||||
reasoningMode: string;
|
||||
reasoningModeDescription: string;
|
||||
proMode: string;
|
||||
@@ -383,17 +385,17 @@ export interface Translations {
|
||||
system: string;
|
||||
light: string;
|
||||
dark: string;
|
||||
lightblue: string;
|
||||
lightgreen: string;
|
||||
iceblue: string;
|
||||
lightyellow: string;
|
||||
lightblue: string;
|
||||
lightgreen: string;
|
||||
iceblue: string;
|
||||
lightyellow: string;
|
||||
systemDescription: string;
|
||||
lightDescription: string;
|
||||
darkDescription: string;
|
||||
lightblueDescription: string;
|
||||
lightgreenDescription: string;
|
||||
iceblueDescription: string;
|
||||
lightyellowDescription: string;
|
||||
lightblueDescription: string;
|
||||
lightgreenDescription: string;
|
||||
iceblueDescription: string;
|
||||
lightyellowDescription: string;
|
||||
languageTitle: string;
|
||||
languageDescription: string;
|
||||
};
|
||||
@@ -475,13 +477,13 @@ export interface Translations {
|
||||
filesSaveAction: string;
|
||||
filesCancelEditAction: string;
|
||||
filesDownloadAction: string;
|
||||
filesCopyPathAction: string;
|
||||
filesDiscardConfirm: string;
|
||||
filesSaveSuccess: string;
|
||||
filesDeleteAction: string;
|
||||
filesDeleteConfirm: string;
|
||||
filesDeleteSuccess: string;
|
||||
filesReadOnlyHint: string;
|
||||
filesCopyPathAction: string;
|
||||
filesDiscardConfirm: string;
|
||||
filesSaveSuccess: string;
|
||||
filesDeleteAction: string;
|
||||
filesDeleteConfirm: string;
|
||||
filesDeleteSuccess: string;
|
||||
filesReadOnlyHint: string;
|
||||
viewDetailAction: string;
|
||||
stateSheet: {
|
||||
archivedTitle: string;
|
||||
@@ -525,12 +527,12 @@ export interface Translations {
|
||||
nameLabel: string;
|
||||
descriptionLabel: string;
|
||||
licenseLabel: string;
|
||||
toolsLabel: string;
|
||||
warningsLabel: string;
|
||||
conflictOwnedByMe: string;
|
||||
conflictOwnedByMeWrongTarget: string;
|
||||
conflictOwnedByOther: string;
|
||||
conflictCustom: string;
|
||||
toolsLabel: string;
|
||||
warningsLabel: string;
|
||||
conflictOwnedByMe: string;
|
||||
conflictOwnedByMeWrongTarget: string;
|
||||
conflictOwnedByOther: string;
|
||||
conflictCustom: string;
|
||||
conflictPublic: string;
|
||||
conflictPublicAdminHint: string;
|
||||
installSuccess: string;
|
||||
|
||||
@@ -95,6 +95,8 @@ export const zhCN: Translations = {
|
||||
mode: "模式",
|
||||
flashMode: "快速",
|
||||
flashModeDescription: "快速且高效的完成任务,但可能不够精准",
|
||||
turboMode: "极速",
|
||||
turboModeDescription: "直连大模型流式问答,不走智能体工具与技能,首字更快",
|
||||
reasoningMode: "思考",
|
||||
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
||||
proMode: "专业问答",
|
||||
@@ -429,17 +431,17 @@ export const zhCN: Translations = {
|
||||
system: "系统",
|
||||
light: "浅色",
|
||||
dark: "深色",
|
||||
lightblue: "浅蓝",
|
||||
lightgreen: "绿色",
|
||||
iceblue: "冰川蓝",
|
||||
lightyellow: "浅黄色",
|
||||
lightblue: "浅蓝",
|
||||
lightgreen: "绿色",
|
||||
iceblue: "冰川蓝",
|
||||
lightyellow: "浅黄色",
|
||||
systemDescription: "自动跟随系统主题。",
|
||||
lightDescription: "更明亮的配色,适合日间使用。",
|
||||
darkDescription: "更暗的配色,减少眩光方便专注。",
|
||||
lightblueDescription: "以蓝色为主色的清爽浅色配色。",
|
||||
lightgreenDescription: "采用清新绿色的浅色配色。",
|
||||
iceblueDescription: "以 #9BCEF1 为主色,搭配冰蓝留白与沉稳交互色。",
|
||||
lightyellowDescription: "以 #F3FBD9 为主色,搭配柔和浅黄留白与深橄榄交互色。",
|
||||
lightblueDescription: "以蓝色为主色的清爽浅色配色。",
|
||||
lightgreenDescription: "采用清新绿色的浅色配色。",
|
||||
iceblueDescription: "以 #9BCEF1 为主色,搭配冰蓝留白与沉稳交互色。",
|
||||
lightyellowDescription: "以 #F3FBD9 为主色,搭配柔和浅黄留白与深橄榄交互色。",
|
||||
languageTitle: "语言",
|
||||
languageDescription: "在不同语言之间切换。",
|
||||
},
|
||||
@@ -524,11 +526,11 @@ export const zhCN: Translations = {
|
||||
filesDownloadAction: "下载",
|
||||
filesCopyPathAction: "复制路径",
|
||||
filesDiscardConfirm: "有未保存的修改,确定放弃吗?",
|
||||
filesSaveSuccess: "文件已保存",
|
||||
filesDeleteAction: "删除",
|
||||
filesDeleteConfirm: "确定删除“{path}”?该操作不可撤销。",
|
||||
filesDeleteSuccess: "文件已删除",
|
||||
filesReadOnlyHint: "仅管理员可以编辑或删除内置技能文件。",
|
||||
filesSaveSuccess: "文件已保存",
|
||||
filesDeleteAction: "删除",
|
||||
filesDeleteConfirm: "确定删除“{path}”?该操作不可撤销。",
|
||||
filesDeleteSuccess: "文件已删除",
|
||||
filesReadOnlyHint: "仅管理员可以编辑或删除内置技能文件。",
|
||||
viewDetailAction: "查看技能详情",
|
||||
stateSheet: {
|
||||
archivedTitle: "归档的技能",
|
||||
@@ -566,24 +568,24 @@ export const zhCN: Translations = {
|
||||
chooseFile: "选择文件",
|
||||
validating: "正在校验文件…",
|
||||
installing: "安装中…",
|
||||
replace: "重复上传并覆盖",
|
||||
replace: "重复上传并覆盖",
|
||||
install: "安装",
|
||||
cancel: "取消",
|
||||
nameLabel: "名称",
|
||||
descriptionLabel: "描述",
|
||||
licenseLabel: "许可证",
|
||||
toolsLabel: "允许的工具",
|
||||
warningsLabel: "警告",
|
||||
conflictOwnedByMe:
|
||||
"您已上传过同名技能 “{name}”,可重复上传并覆盖原版本(原启用状态保留)。",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"同名技能 “{name}” 是您上传的自定义技能,请切换到「自定义技能」后覆盖。",
|
||||
conflictOwnedByOther:
|
||||
"技能 “{name}” 已由用户 “{owner}” 上传,不能覆盖,请修改技能名称后再上传。",
|
||||
conflictCustom:
|
||||
"已存在同名自定义技能 “{name}”,但无法确认上传用户,为避免误覆盖请修改技能名称。",
|
||||
warningsLabel: "警告",
|
||||
conflictOwnedByMe:
|
||||
"您已上传过同名技能 “{name}”,可重复上传并覆盖原版本(原启用状态保留)。",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"同名技能 “{name}” 是您上传的自定义技能,请切换到「自定义技能」后覆盖。",
|
||||
conflictOwnedByOther:
|
||||
"技能 “{name}” 已由用户 “{owner}” 上传,不能覆盖,请修改技能名称后再上传。",
|
||||
conflictCustom:
|
||||
"已存在同名自定义技能 “{name}”,但无法确认上传用户,为避免误覆盖请修改技能名称。",
|
||||
conflictPublic: `存在同名内置技能 '{name}',无法覆盖。`,
|
||||
conflictPublicAdminHint: `存在同名内置技能 '{name}'。点击「重复上传并覆盖」可替换。`,
|
||||
conflictPublicAdminHint: `存在同名内置技能 '{name}'。点击「重复上传并覆盖」可替换。`,
|
||||
installSuccess: `技能 '{name}' 已安装。`,
|
||||
installSuccessReplaced: `技能 '{name}' 已替换。`,
|
||||
rejectInvalidExt: `只接受 .zip 或 .skill 文件。`,
|
||||
|
||||
@@ -4,11 +4,11 @@ export interface Model {
|
||||
model: string;
|
||||
display_name: string;
|
||||
description?: string | null;
|
||||
supports_thinking?: boolean;
|
||||
supports_reasoning_effort?: boolean;
|
||||
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
||||
execution_profile?: "single_round_rag" | "multi_round_rag";
|
||||
}
|
||||
supports_thinking?: boolean;
|
||||
supports_reasoning_effort?: boolean;
|
||||
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
||||
execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
|
||||
}
|
||||
|
||||
export interface TokenUsageSettings {
|
||||
enabled: boolean;
|
||||
|
||||
@@ -16,14 +16,14 @@ import { rememberAskTime } from "../chat-timestamps";
|
||||
import { getBackendBaseURL, getLangGraphBaseURL } from "../config";
|
||||
import { useI18n } from "../i18n/hooks";
|
||||
import { OptType, writeYLog } from "../log/y-log";
|
||||
import type { FileInMessage } from "../messages/utils";
|
||||
import type { LocalSettings } from "../settings";
|
||||
import {
|
||||
isThreadRagModeLocked,
|
||||
lockThreadRagMode,
|
||||
useLocalSettings,
|
||||
} from "../settings";
|
||||
import { useCitationDisplaySettings } from "../system-settings/hooks";
|
||||
import type { FileInMessage } from "../messages/utils";
|
||||
import type { LocalSettings } from "../settings";
|
||||
import {
|
||||
isThreadRagModeLocked,
|
||||
lockThreadRagMode,
|
||||
useLocalSettings,
|
||||
} from "../settings";
|
||||
import { useCitationDisplaySettings } from "../system-settings/hooks";
|
||||
import { useUpdateSubtask } from "../tasks/context";
|
||||
import type { UploadedFileInfo } from "../uploads";
|
||||
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
|
||||
@@ -571,11 +571,11 @@ export function useThreadStream({
|
||||
onToolStart,
|
||||
onToolEnd,
|
||||
}: ThreadStreamOptions) {
|
||||
const { t } = useI18n();
|
||||
const [localSettings] = useLocalSettings();
|
||||
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||
const referenceModeEntryEnabled =
|
||||
citationDisplay?.reference_mode_enabled ?? false;
|
||||
const { t } = useI18n();
|
||||
const [localSettings] = useLocalSettings();
|
||||
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||
const referenceModeEntryEnabled =
|
||||
citationDisplay?.reference_mode_enabled ?? false;
|
||||
// Track the thread ID that is currently streaming to handle thread changes during streaming
|
||||
const [onStreamThreadId, setOnStreamThreadId] = useState(() => threadId);
|
||||
// Ref to track current thread ID across async callbacks without causing re-renders,
|
||||
@@ -1507,21 +1507,21 @@ export function useThreadStream({
|
||||
...context,
|
||||
...extraContext,
|
||||
};
|
||||
const selectedLlmWikiIds = Array.isArray(
|
||||
mergedContext.llmwiki_knowledge_base_ids,
|
||||
)
|
||||
? mergedContext.llmwiki_knowledge_base_ids
|
||||
: [];
|
||||
const selectedAssistantKnowledgeIds = Array.isArray(
|
||||
mergedContext.assistant_knowledge_base_ids,
|
||||
)
|
||||
? mergedContext.assistant_knowledge_base_ids
|
||||
: [];
|
||||
const ragModeEnabled =
|
||||
(referenceModeEntryEnabled && localSettings.display.ragModeEnabled) ||
|
||||
isThreadRagModeLocked(threadId) ||
|
||||
selectedLlmWikiIds.length > 0 ||
|
||||
selectedAssistantKnowledgeIds.length > 0;
|
||||
const selectedLlmWikiIds = Array.isArray(
|
||||
mergedContext.llmwiki_knowledge_base_ids,
|
||||
)
|
||||
? mergedContext.llmwiki_knowledge_base_ids
|
||||
: [];
|
||||
const selectedAssistantKnowledgeIds = Array.isArray(
|
||||
mergedContext.assistant_knowledge_base_ids,
|
||||
)
|
||||
? mergedContext.assistant_knowledge_base_ids
|
||||
: [];
|
||||
const ragModeEnabled =
|
||||
(referenceModeEntryEnabled && localSettings.display.ragModeEnabled) ||
|
||||
isThreadRagModeLocked(threadId) ||
|
||||
selectedLlmWikiIds.length > 0 ||
|
||||
selectedAssistantKnowledgeIds.length > 0;
|
||||
if (ragModeEnabled) {
|
||||
lockThreadRagMode(threadId);
|
||||
}
|
||||
@@ -1579,6 +1579,7 @@ export function useThreadStream({
|
||||
context: {
|
||||
...mergedContext,
|
||||
thinking_enabled: mergedContext.mode !== "flash",
|
||||
thinking_force_disabled: mergedContext.mode === "flash",
|
||||
is_plan_mode:
|
||||
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
|
||||
subagent_enabled: mergedContext.mode === "ultra",
|
||||
@@ -1639,18 +1640,18 @@ export function useThreadStream({
|
||||
sendInFlightRef.current = false;
|
||||
}
|
||||
},
|
||||
[
|
||||
thread,
|
||||
_handleOnStart,
|
||||
t.uploads.uploadingFiles,
|
||||
context,
|
||||
referenceModeEntryEnabled,
|
||||
localSettings.display.ragModeEnabled,
|
||||
queryClient,
|
||||
runOptions,
|
||||
confirmOrRejoin,
|
||||
],
|
||||
);
|
||||
[
|
||||
thread,
|
||||
_handleOnStart,
|
||||
t.uploads.uploadingFiles,
|
||||
context,
|
||||
referenceModeEntryEnabled,
|
||||
localSettings.display.ragModeEnabled,
|
||||
queryClient,
|
||||
runOptions,
|
||||
confirmOrRejoin,
|
||||
],
|
||||
);
|
||||
|
||||
const handleThreadStop = useCallback(async () => {
|
||||
const tid = threadIdRef.current;
|
||||
@@ -1699,7 +1700,7 @@ export function useThreadStream({
|
||||
? undefined
|
||||
: thread.error;
|
||||
|
||||
const mergedThread = {
|
||||
const mergedThread = {
|
||||
...thread,
|
||||
error: visibleThreadError,
|
||||
values: mergedValues,
|
||||
@@ -1709,7 +1710,7 @@ export function useThreadStream({
|
||||
: mergedMessages,
|
||||
isLoading: thread.isLoading || streamHoldOpen,
|
||||
stop: handleThreadStop,
|
||||
} as typeof thread;
|
||||
} as typeof thread;
|
||||
|
||||
// 第 4 个返回值 `onStreamThreadId` = 底层 useStream 当前实际绑定的线程 id(新建/未保存时为 null)。
|
||||
// 调用方据此判断「流是否已切到全新线程」,避免在切换会话的那一帧把消息发进上一个会话
|
||||
|
||||
@@ -2,18 +2,20 @@ import type { Message, Thread } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { Todo } from "../todos";
|
||||
|
||||
export interface AgentThreadState extends Record<string, unknown> {
|
||||
title: string;
|
||||
title_provisional?: boolean;
|
||||
messages: Message[];
|
||||
artifacts: string[];
|
||||
todos?: Todo[];
|
||||
}
|
||||
export interface AgentThreadState extends Record<string, unknown> {
|
||||
title: string;
|
||||
title_provisional?: boolean;
|
||||
messages: Message[];
|
||||
artifacts: string[];
|
||||
todos?: Todo[];
|
||||
}
|
||||
|
||||
export interface AgentThreadContext extends Record<string, unknown> {
|
||||
thread_id: string;
|
||||
model_name: string | undefined;
|
||||
thinking_enabled: boolean;
|
||||
/** Flash mode: force vLLM/Qwen ``enable_thinking=false`` even without a per-model disable shape. */
|
||||
thinking_force_disabled?: boolean;
|
||||
is_plan_mode: boolean;
|
||||
subagent_enabled: boolean;
|
||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||
@@ -23,15 +25,15 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
||||
rag_mode_enabled?: boolean;
|
||||
/** Per-request switch for context compression (上下文压缩). Default off. */
|
||||
summarization_enabled?: boolean;
|
||||
/** Per-conversation switch for knowledge-base RAG injection (phase 2). */
|
||||
knowledge_rag_enabled?: boolean;
|
||||
/** DeerFlow LLMWiki mapping ids selected for this conversation. */
|
||||
llmwiki_knowledge_base_ids?: string[];
|
||||
/** Assistant knowledge-base ids selected for this conversation. */
|
||||
assistant_knowledge_base_ids?: string[];
|
||||
/** Direct RAG modes handled in the frontend; these requests never enter Lead Agent. */
|
||||
execution_profile?: "single_round_rag" | "multi_round_rag";
|
||||
agent_id?: string;
|
||||
/** Per-conversation switch for knowledge-base RAG injection (phase 2). */
|
||||
knowledge_rag_enabled?: boolean;
|
||||
/** DeerFlow LLMWiki mapping ids selected for this conversation. */
|
||||
llmwiki_knowledge_base_ids?: string[];
|
||||
/** Assistant knowledge-base ids selected for this conversation. */
|
||||
assistant_knowledge_base_ids?: string[];
|
||||
/** Frontend-only execution paths that never enter Lead Agent (RAG / 极速直连). */
|
||||
execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
|
||||
agent_id?: string;
|
||||
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
||||
agent_name?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { readRuntimeFlag } from "@/env";
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
|
||||
import { textOfMessage } from "./utils";
|
||||
|
||||
const STORAGE_PREFIX = "workspace.direct-chat.";
|
||||
const MAX_HISTORY_MESSAGES = 20;
|
||||
|
||||
export function isDirectChatProfile(context: unknown): boolean {
|
||||
return (
|
||||
typeof context === "object" &&
|
||||
context !== null &&
|
||||
(context as { execution_profile?: unknown }).execution_profile === "direct"
|
||||
);
|
||||
}
|
||||
|
||||
/** 是否在模式菜单展示「极速」。仅 runtime-config.js 显式为 "true" 时开启,默认关闭。 */
|
||||
export function isTurboModeVisible(): boolean {
|
||||
return readRuntimeFlag("VITE_TURBO_MODE_VISIBLE");
|
||||
}
|
||||
|
||||
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 {
|
||||
// Quota errors must never interrupt an already-running answer.
|
||||
}
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return error instanceof DOMException
|
||||
? error.name === "AbortError"
|
||||
: error instanceof Error && error.name === "AbortError";
|
||||
}
|
||||
|
||||
function historyFromMessages(messages: Message[]): Array<{
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}> {
|
||||
const history: Array<{ role: "user" | "assistant"; content: string }> = [];
|
||||
for (const message of messages) {
|
||||
if (message.type !== "human" && message.type !== "ai") continue;
|
||||
const content = textOfMessage(message)?.trim();
|
||||
if (!content) continue;
|
||||
history.push({
|
||||
role: message.type === "human" ? "user" : "assistant",
|
||||
content,
|
||||
});
|
||||
}
|
||||
return history.slice(-MAX_HISTORY_MESSAGES);
|
||||
}
|
||||
|
||||
function parseSseBlocks(buffer: string): {
|
||||
events: Array<{ event?: string; data: string }>;
|
||||
rest: string;
|
||||
} {
|
||||
const parts = buffer.split("\n\n");
|
||||
const rest = parts.pop() ?? "";
|
||||
const events: Array<{ event?: string; data: string }> = [];
|
||||
for (const part of parts) {
|
||||
let event: string | undefined;
|
||||
const dataLines: string[] = [];
|
||||
for (const line of part.split("\n")) {
|
||||
if (line.startsWith(":")) continue;
|
||||
if (line.startsWith("event:")) event = line.slice(6).trim();
|
||||
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
||||
}
|
||||
if (dataLines.length > 0) {
|
||||
events.push({ event, data: dataLines.join("\n") });
|
||||
}
|
||||
}
|
||||
return { events, rest };
|
||||
}
|
||||
|
||||
async function readErrorDetail(response: Response): Promise<string> {
|
||||
try {
|
||||
const payload: unknown = await response.json();
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"detail" in payload &&
|
||||
typeof (payload as { detail: unknown }).detail === "string"
|
||||
) {
|
||||
return (payload as { detail: string }).detail;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to status text.
|
||||
}
|
||||
return `极速问答失败(${response.status})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend-only adapter that talks to Gateway ``POST /api/direct-chat/stream``.
|
||||
* It never submits a LangGraph run or touches the lead-agent stack.
|
||||
*/
|
||||
export function useDirectChatMessages(threadId: string | undefined) {
|
||||
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,
|
||||
modelName,
|
||||
}: {
|
||||
targetThreadId: string;
|
||||
query: string;
|
||||
modelName?: string;
|
||||
}) => {
|
||||
if (isLoading) return;
|
||||
const question = query.trim();
|
||||
if (!question) return;
|
||||
|
||||
const humanId = createClientId("direct-human");
|
||||
const assistantId = createClientId("direct-ai");
|
||||
const clientTs = new Date().toISOString();
|
||||
const history = historyFromMessages(messagesRef.current);
|
||||
const userMessage: Message = {
|
||||
id: humanId,
|
||||
type: "human",
|
||||
content: [{ type: "text", text: question }],
|
||||
additional_kwargs: { client_ts: clientTs, direct_chat: true },
|
||||
};
|
||||
const initialAssistant: Message = {
|
||||
id: assistantId,
|
||||
type: "ai",
|
||||
content: "",
|
||||
additional_kwargs: { direct_chat: true },
|
||||
};
|
||||
updateMessages(targetThreadId, (current) => [
|
||||
...current,
|
||||
userMessage,
|
||||
initialAssistant,
|
||||
]);
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
controllerRef.current = controller;
|
||||
let answer = "";
|
||||
|
||||
const refreshAssistant = (content: string) => {
|
||||
updateMessages(targetThreadId, (current) =>
|
||||
current.map((item) =>
|
||||
item.id === assistantId
|
||||
? ({
|
||||
...item,
|
||||
content,
|
||||
additional_kwargs: {
|
||||
...item.additional_kwargs,
|
||||
direct_chat: true,
|
||||
},
|
||||
} as Message)
|
||||
: item,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/direct-chat/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
model_name: modelName,
|
||||
messages: [...history, { role: "user", content: question }],
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorDetail(response));
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error("极速问答未返回数据流。");
|
||||
}
|
||||
|
||||
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 parsed = parseSseBlocks(buffer);
|
||||
buffer = parsed.rest;
|
||||
for (const block of parsed.events) {
|
||||
let payload: unknown = null;
|
||||
try {
|
||||
payload = JSON.parse(block.data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (block.event === "error") {
|
||||
const detail =
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"detail" in payload &&
|
||||
typeof (payload as { detail: unknown }).detail === "string"
|
||||
? (payload as { detail: string }).detail
|
||||
: "极速问答失败。";
|
||||
throw new Error(detail);
|
||||
}
|
||||
if (
|
||||
block.event === "delta" &&
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"text" in payload &&
|
||||
typeof (payload as { text: unknown }).text === "string"
|
||||
) {
|
||||
answer += (payload as { text: string }).text;
|
||||
refreshAssistant(answer);
|
||||
}
|
||||
if (
|
||||
block.event === "result" &&
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"answer" in payload &&
|
||||
typeof (payload as { answer: unknown }).answer === "string"
|
||||
) {
|
||||
answer = (payload as { answer: string }).answer;
|
||||
refreshAssistant(answer);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!answer) {
|
||||
throw new Error("模型未返回任何文本。");
|
||||
}
|
||||
} catch (caught) {
|
||||
if (!isAbortError(caught)) {
|
||||
const message =
|
||||
caught instanceof Error ? caught.message : "极速问答失败。";
|
||||
setError(message);
|
||||
updateMessages(targetThreadId, (current) =>
|
||||
current.map((item) =>
|
||||
item.id === assistantId
|
||||
? ({
|
||||
...item,
|
||||
content: answer || message,
|
||||
additional_kwargs: {
|
||||
...item.additional_kwargs,
|
||||
direct_chat: true,
|
||||
},
|
||||
} as Message)
|
||||
: item,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (controllerRef.current === controller) controllerRef.current = null;
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[isLoading, updateMessages],
|
||||
);
|
||||
|
||||
return { messages, isLoading, error, submit, stop };
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { useLocation } from "react-router-dom";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -48,6 +49,7 @@ import { useNotification } from "@/core/notification/hooks";
|
||||
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
|
||||
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
|
||||
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
|
||||
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
||||
@@ -560,9 +562,26 @@ export default function AgentChatPage({
|
||||
},
|
||||
});
|
||||
|
||||
const directChat = useDirectChatMessages(threadId);
|
||||
const directChatSelected =
|
||||
isDirectChatProfile(settings.context) && isTurboModeVisible();
|
||||
|
||||
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
||||
// 流式 stub),成功且真实产物落地后自动撤出。
|
||||
const { displayThread } = useResearchWritingFeed(threadId, thread);
|
||||
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
|
||||
const displayThread = useMemo(() => {
|
||||
const base = researchDisplayThread;
|
||||
if (directChat.messages.length === 0 && !directChat.isLoading) {
|
||||
return base;
|
||||
}
|
||||
const messages = [...base.messages, ...directChat.messages];
|
||||
return {
|
||||
...base,
|
||||
values: { ...base.values, messages },
|
||||
messages,
|
||||
isLoading: base.isLoading || directChat.isLoading,
|
||||
} as typeof base;
|
||||
}, [directChat.isLoading, directChat.messages, researchDisplayThread]);
|
||||
|
||||
// ── agentfx「入库」协助卡 ──────────────────────────────────────────────────
|
||||
// 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且
|
||||
@@ -743,6 +762,62 @@ export default function AgentChatPage({
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
|
||||
if (directChatSelected) {
|
||||
if (message.files && message.files.length > 0) {
|
||||
toast.error("极速问答暂不支持附件,请移除附件后重试。");
|
||||
return;
|
||||
}
|
||||
setHasStartedConversation(true);
|
||||
saveLastAgentId(agent_id);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||
);
|
||||
void (async () => {
|
||||
try {
|
||||
let targetThreadId = threadId;
|
||||
if (isNewThread) {
|
||||
const created = await getAPIClient().threads.create({
|
||||
metadata: {
|
||||
agent_id,
|
||||
...(Object.keys(conversationMetadata).length > 0
|
||||
? conversationMetadata
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
targetThreadId = created.thread_id;
|
||||
knowledgeSelectionThread.current = targetThreadId;
|
||||
activeThreadIdRef.current = targetThreadId;
|
||||
setThreadId(targetThreadId);
|
||||
setIsNewThread(false);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}${window.location.search}#${agentChatsBase}/${targetThreadId}`,
|
||||
);
|
||||
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
|
||||
if (title) {
|
||||
void getAPIClient().threads
|
||||
.updateState(targetThreadId, { values: { title } })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
if (!targetThreadId) {
|
||||
toast.error("无法创建会话。");
|
||||
return;
|
||||
}
|
||||
await directChat.submit({
|
||||
targetThreadId,
|
||||
query: message.text,
|
||||
modelName: settings.context.model_name,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "极速问答失败。",
|
||||
);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
setHasStartedConversation(true);
|
||||
// Remember this agent so the new-chat landing screen pre-selects it next
|
||||
// time (covers both direct agent chats and the ChatPage handoff replay).
|
||||
@@ -767,7 +842,7 @@ export default function AgentChatPage({
|
||||
...pageReportThemeContext,
|
||||
});
|
||||
},
|
||||
[sendMessage, threadId, agent_id, settings.context.writing_mode, writingArtifactPath, baseMode],
|
||||
[sendMessage, threadId, agent_id, settings.context.writing_mode, settings.context.model_name, writingArtifactPath, baseMode, directChat, directChatSelected, isNewThread, conversationMetadata, agentChatsBase, setThreadId, setIsNewThread],
|
||||
);
|
||||
|
||||
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
|
||||
@@ -852,8 +927,12 @@ export default function AgentChatPage({
|
||||
);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (directChat.isLoading) {
|
||||
directChat.stop();
|
||||
return;
|
||||
}
|
||||
await thread.stop();
|
||||
}, [thread]);
|
||||
}, [directChat, thread]);
|
||||
|
||||
const isLandingState = isNewThread && !hasStartedConversation;
|
||||
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
|
||||
@@ -867,7 +946,7 @@ export default function AgentChatPage({
|
||||
|
||||
const status = thread.error
|
||||
? "error"
|
||||
: thread.isLoading
|
||||
: thread.isLoading || directChat.isLoading
|
||||
? "streaming"
|
||||
: "ready";
|
||||
|
||||
@@ -1084,7 +1163,7 @@ export default function AgentChatPage({
|
||||
autoFocus
|
||||
status={status}
|
||||
context={settings.context}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||
onContextChange={(context) =>
|
||||
setSettings("context", context)
|
||||
}
|
||||
@@ -1104,7 +1183,7 @@ export default function AgentChatPage({
|
||||
pendingKnowledgeSelection.current === null &&
|
||||
pendingAssistantKnowledgeSelection.current === null
|
||||
}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||
label="知识空间"
|
||||
/>
|
||||
)
|
||||
@@ -1175,7 +1254,7 @@ export default function AgentChatPage({
|
||||
assistantId={agent_id}
|
||||
status={status}
|
||||
context={settings.context}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||
onContextChange={(context) =>
|
||||
setSettings("context", context)
|
||||
}
|
||||
@@ -1190,7 +1269,7 @@ export default function AgentChatPage({
|
||||
assistantValue={selectedAssistantKnowledgeBaseIds}
|
||||
onChange={changeKnowledgeSelection}
|
||||
onAssistantChange={changeAssistantKnowledgeSelection}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
||||
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||
readOnly
|
||||
label="知识空间"
|
||||
/>
|
||||
|
||||
@@ -69,6 +69,7 @@ import { clearResearchReportStream } from "@/core/threads/use-research-report-st
|
||||
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
||||
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
|
||||
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
|
||||
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||
import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag";
|
||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
@@ -348,9 +349,12 @@ export default function ChatPage() {
|
||||
});
|
||||
const singleRoundRag = useSingleRoundRagMessages(threadId);
|
||||
const multiRoundRag = useMultiRoundRagMessages(threadId);
|
||||
const directChat = useDirectChatMessages(threadId);
|
||||
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
|
||||
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
|
||||
const frontendRagSelected = isFrontendRagProfile(settings.context);
|
||||
const directChatSelected =
|
||||
isDirectChatProfile(settings.context) && isTurboModeVisible();
|
||||
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
||||
// 流式 stub),成功且真实产物落地后自动撤出。
|
||||
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
|
||||
@@ -360,13 +364,15 @@ export default function ChatPage() {
|
||||
if (
|
||||
singleRoundRag.messages.length === 0 &&
|
||||
ragMessages.length === 0 &&
|
||||
directChat.messages.length === 0 &&
|
||||
!singleRoundRag.isLoading &&
|
||||
!multiRoundRag.isLoading
|
||||
!multiRoundRag.isLoading &&
|
||||
!directChat.isLoading
|
||||
) {
|
||||
return base;
|
||||
}
|
||||
const messages = mergeMessagesByTime(
|
||||
[...base.messages, ...singleRoundRag.messages],
|
||||
[...base.messages, ...singleRoundRag.messages, ...directChat.messages],
|
||||
ragMessages,
|
||||
);
|
||||
return {
|
||||
@@ -374,9 +380,14 @@ export default function ChatPage() {
|
||||
values: { ...base.values, messages },
|
||||
messages,
|
||||
isLoading:
|
||||
base.isLoading || singleRoundRag.isLoading || multiRoundRag.isLoading,
|
||||
base.isLoading ||
|
||||
singleRoundRag.isLoading ||
|
||||
multiRoundRag.isLoading ||
|
||||
directChat.isLoading,
|
||||
} as typeof base;
|
||||
}, [
|
||||
directChat.isLoading,
|
||||
directChat.messages,
|
||||
multiRoundRag.isLoading,
|
||||
multiRoundRag.messages,
|
||||
researchDisplayThread,
|
||||
@@ -392,6 +403,52 @@ export default function ChatPage() {
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(message: PromptInputMessage) => {
|
||||
if (directChatSelected) {
|
||||
if (message.files && message.files.length > 0) {
|
||||
toast.error("极速问答暂不支持附件,请移除附件后重试。");
|
||||
return;
|
||||
}
|
||||
setHasStartedConversation(true);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||
);
|
||||
void (async () => {
|
||||
try {
|
||||
let targetThreadId = threadId;
|
||||
if (isNewThread) {
|
||||
const created = await getAPIClient(isMock).threads.create({});
|
||||
targetThreadId = created.thread_id;
|
||||
setThreadId(targetThreadId);
|
||||
setIsNewThread(false);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}${window.location.search}#${routeBase}/chats/${targetThreadId}`,
|
||||
);
|
||||
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
|
||||
if (title) {
|
||||
void getAPIClient(isMock).threads
|
||||
.updateState(targetThreadId, { values: { title } })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
if (!targetThreadId) {
|
||||
toast.error("无法创建会话。");
|
||||
return;
|
||||
}
|
||||
await directChat.submit({
|
||||
targetThreadId,
|
||||
query: message.text,
|
||||
modelName: settings.context.model_name,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "极速问答失败。",
|
||||
);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (frontendRagSelected) {
|
||||
if (message.files && message.files.length > 0) {
|
||||
toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
|
||||
@@ -560,6 +617,8 @@ export default function ChatPage() {
|
||||
multiRoundRag,
|
||||
multiRoundRagSelected,
|
||||
frontendRagSelected,
|
||||
directChat,
|
||||
directChatSelected,
|
||||
routeBase,
|
||||
setIsNewThread,
|
||||
setThreadId,
|
||||
@@ -655,6 +714,10 @@ export default function ChatPage() {
|
||||
saveLastAgentId(agentId);
|
||||
}, []);
|
||||
const handleStop = useCallback(async () => {
|
||||
if (directChat.isLoading) {
|
||||
directChat.stop();
|
||||
return;
|
||||
}
|
||||
if (singleRoundRag.isLoading) {
|
||||
singleRoundRag.stop();
|
||||
return;
|
||||
@@ -664,7 +727,7 @@ export default function ChatPage() {
|
||||
return;
|
||||
}
|
||||
await thread.stop();
|
||||
}, [multiRoundRag, singleRoundRag, thread]);
|
||||
}, [directChat, multiRoundRag, singleRoundRag, thread]);
|
||||
|
||||
const promptPrefixProp = useMemo(() => {
|
||||
if (!promptPrefixSettings?.enabled) return undefined;
|
||||
@@ -914,7 +977,8 @@ export default function ChatPage() {
|
||||
agentHandoffPending ||
|
||||
knowledgeSelectionLoading ||
|
||||
singleRoundRag.isLoading ||
|
||||
multiRoundRag.isLoading
|
||||
multiRoundRag.isLoading ||
|
||||
directChat.isLoading
|
||||
}
|
||||
onContextChange={(context) =>
|
||||
setSettings("context", context)
|
||||
|
||||
Reference in new issue
Block a user