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
@@ -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