From 40542db4005bd787b1601a99e00f8e76fbc74d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Mon, 31 Aug 2026 17:49:48 +0800 Subject: [PATCH 1/9] feat: add configurable chat display modes --- .../components/workspace/chats/chat-box.tsx | 7 +- .../src/components/workspace/input-box.tsx | 31 ++++--- .../workspace/messages/message-group.tsx | 68 +++++++------- .../workspace/messages/message-list.tsx | 47 +++++----- .../settings/appearance-settings-page.tsx | 58 ++++++++---- frontend-web/src/core/system-settings/api.ts | 4 + frontend-web/src/core/threads/hooks.ts | 50 +++++++---- frontend-web/src/pages/IframeChatPage.tsx | 14 ++- .../src/pages/PromptPrefixSettingsPage.tsx | 27 +++++- .../pages/iframe-chat-mode-buttons.test.ts | 17 ++++ .../src/pages/iframe-chat-mode-buttons.ts | 7 +- offline-backend-20260512/backend/CLAUDE.md | 15 +++- offline-backend-20260512/backend/README.md | 21 +++++ .../app/gateway/routers/system_settings.py | 31 +++++-- .../deerflow/agents/lead_agent/prompt.py | 30 +++++-- .../deerflow/config/system_settings.py | 10 ++- .../tests/test_es_query_routing_prompt.py | 17 ++++ .../backend/tests/test_prompt_prefix.py | 88 +++++++++++++++++++ 18 files changed, 421 insertions(+), 121 deletions(-) diff --git a/frontend-web/src/components/workspace/chats/chat-box.tsx b/frontend-web/src/components/workspace/chats/chat-box.tsx index e2a6c51a..8de0d29a 100644 --- a/frontend-web/src/components/workspace/chats/chat-box.tsx +++ b/frontend-web/src/components/workspace/chats/chat-box.tsx @@ -20,6 +20,7 @@ import { } from "@/core/artifacts/listing"; import { getBackendBaseURL } from "@/core/config"; import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings"; +import { useCitationDisplaySettings } from "@/core/system-settings/hooks"; import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag"; import { isMultiRoundRagMessage } from "@/core/threads/use-multi-round-rag"; import { useSkills } from "@/core/skills/hooks"; @@ -101,8 +102,12 @@ const ChatBox: React.FC<{ const [settings] = useLocalSettings(); const artifactDirectoryModeEnabled = settings.display.artifactDirectoryModeEnabled ?? false; + const { data: citationDisplay } = useCitationDisplaySettings(); + const referenceModeEntryEnabled = + citationDisplay?.reference_mode_enabled ?? false; const ragModeEnabled = - settings.display.ragModeEnabled || isThreadRagModeLocked(threadId); + (referenceModeEntryEnabled && settings.display.ragModeEnabled) || + isThreadRagModeLocked(threadId); // Frontend RAG modes write their own hidden source tool messages. They use // the existing reference panel, but must not request or write reference // batches through DeerFlow's backend APIs. diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index 254a18ca..ac6bd91c 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -93,14 +93,14 @@ import { import { useRecommendedQuestions } from "@/core/recommended-questions/hooks"; import { useUserPreferences } from "@/core/user-preferences"; -import { - isThreadRagModeLocked, - lockThreadRagMode, - useLocalSettings, -} from "@/core/settings"; - - -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"; @@ -221,9 +221,12 @@ export function InputBox({ const modelOptions = useMemo(() => { return singleRoundRagActive ? extraModels : models; }, [extraModels, models, singleRoundRagActive]); - const { thread, isMock } = useThread(); - const { textInput } = usePromptInputController(); - const promptRootRef = useRef(null); + const { thread, isMock } = useThread(); + const { textInput } = usePromptInputController(); + const promptRootRef = useRef(null); + const { data: citationDisplay } = useCitationDisplaySettings(); + const referenceModeEntryEnabled = + citationDisplay?.reference_mode_enabled ?? false; const [followups, setFollowups] = useState([]); const [followupsHidden, setFollowupsHidden] = useState(false); @@ -830,9 +833,9 @@ export function InputBox({ 记忆 } - {!frontendRagActive && !hideContextTools && convertToSteps(messages), [messages]); - const [settings] = useLocalSettings(); - const { skills } = useSkills(); - const skillDisplayMap = useMemo( - () => (settings.display.ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()), - [settings.display.ragModeEnabled, skills], - ); + const steps = useMemo(() => convertToSteps(messages), [messages]); + const { skills } = useSkills(); + const skillDisplayMap = useMemo( + () => (ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()), + [ragModeEnabled, skills], + ); const lastToolCallStep = useMemo(() => { const filteredSteps = steps.filter((step) => step.type === "toolCall"); return filteredSteps[filteredSteps.length - 1]; @@ -197,11 +197,11 @@ export function MessageGroup({ - ), + isLoading={isLoading} + skillDisplayMap={skillDisplayMap} + ragModeEnabled={ragModeEnabled} + /> + ), )} {lastToolCallStep && ( @@ -209,10 +209,10 @@ export function MessageGroup({ key={lastToolCallStep.id} {...lastToolCallStep} isLast={true} - isLoading={isLoading} - skillDisplayMap={skillDisplayMap} - ragModeEnabled={settings.display.ragModeEnabled} - /> + isLoading={isLoading} + skillDisplayMap={skillDisplayMap} + ragModeEnabled={ragModeEnabled} + /> )} diff --git a/frontend-web/src/components/workspace/messages/message-list.tsx b/frontend-web/src/components/workspace/messages/message-list.tsx index 097a0baf..15d83aff 100644 --- a/frontend-web/src/components/workspace/messages/message-list.tsx +++ b/frontend-web/src/components/workspace/messages/message-list.tsx @@ -28,11 +28,12 @@ import { hasToolCalls, isHiddenFromUIMessage, } from "@/core/messages/utils"; -import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; -import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings"; -import { useSkills } from "@/core/skills/hooks"; -import { - buildSkillDisplayMap, +import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; +import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings"; +import { useSkills } from "@/core/skills/hooks"; +import { useCitationDisplaySettings } from "@/core/system-settings/hooks"; +import { + buildSkillDisplayMap, collectRuntimeDisplaySources, type RuntimeDisplaySource, } from "@/core/skill-display/runtime"; @@ -299,10 +300,14 @@ export function MessageList({ } return map; }, [messages]); - - const [settings] = useLocalSettings(); - const ragModeEnabled = - settings.display.ragModeEnabled || isThreadRagModeLocked(threadId); + + const [settings] = useLocalSettings(); + const { data: citationDisplay } = useCitationDisplaySettings(); + const referenceModeEntryEnabled = + citationDisplay?.reference_mode_enabled ?? false; + const ragModeEnabled = + (referenceModeEntryEnabled && settings.display.ragModeEnabled) || + isThreadRagModeLocked(threadId); const hasSingleRoundRag = useMemo( () => messages.some( @@ -778,12 +783,13 @@ export function MessageList({ )) { if (hasReasoning(message)) { results.push( - , + , ); } results.push( @@ -831,11 +837,12 @@ export function MessageList({ .find((message) => message.type === "ai"); return (
- + void }) { const [effectiveLayoutMode, setLayoutMode] = useLayoutMode(); const cleanupWords = citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; + const referenceModeEntryEnabled = + citationDisplay?.reference_mode_enabled ?? false; useEffect(() => { setShortcutDraft( @@ -845,23 +847,45 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { aria-label="回答速度指示器" />
-
-
-
RAG 引用模式
-

- 开启后,已配置展示适配的技能结果会在问答中渲染为编号引用或列表,并支持点击跳转。 -

-
- - setSettings("display", { ragModeEnabled: checked }) - } - aria-label="RAG 引用模式" - /> -
- {isAdmin && ( -
+ {isAdmin && ( +
+
+
参考文献模式入口
+

+ 管理员配置。开启后,普通问答和 iframe 问答输入区会显示「参考文献」按钮;默认关闭。 +

+
+ + updateCitationDisplay.mutate({ + reference_mode_enabled: checked, + }) + } + aria-label="参考文献模式入口" + /> +
+ )} + {referenceModeEntryEnabled && ( +
+
+
RAG 引用模式
+

+ 开启后,已配置展示适配的技能结果会在问答中渲染为编号引用或列表,并支持点击跳转。 +

+
+ + setSettings("display", { ragModeEnabled: checked }) + } + aria-label="RAG 引用模式" + /> +
+ )} + {isAdmin && ( +
参考文献去除词

diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index bf59e850..24a212b0 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -6,6 +6,7 @@ export interface PromptPrefixSettings { enabled: boolean; switch_label: string; prompt_prefix: string; + ordinary_qa_markdown_format_enabled: boolean; } export interface PluginDomainAgentMappingDto { @@ -37,6 +38,7 @@ export interface PromptPrefixUpdate { enabled?: boolean; switch_label?: string; prompt_prefix?: string; + ordinary_qa_markdown_format_enabled?: boolean; } function fromPluginManagementDto(data: { @@ -173,10 +175,12 @@ export interface PrivateSquareUpdate { } export interface CitationDisplaySettings { + reference_mode_enabled: boolean; cleanup_words: string[]; } export interface CitationDisplayUpdate { + reference_mode_enabled?: boolean; cleanup_words?: string[]; } diff --git a/frontend-web/src/core/threads/hooks.ts b/frontend-web/src/core/threads/hooks.ts index 5f760442..8bd832e4 100644 --- a/frontend-web/src/core/threads/hooks.ts +++ b/frontend-web/src/core/threads/hooks.ts @@ -16,9 +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 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"; @@ -588,8 +593,11 @@ export function useThreadStream({ onToolStart, onToolEnd, }: ThreadStreamOptions) { - const { t } = useI18n(); - const [localSettings] = useLocalSettings(); + 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, @@ -1540,15 +1548,15 @@ export function useThreadStream({ ...context, ...extraContext, }; - const selectedLlmWikiIds = Array.isArray( - mergedContext.llmwiki_knowledge_base_ids, - ) - ? mergedContext.llmwiki_knowledge_base_ids - : []; - const ragModeEnabled = - localSettings.display.ragModeEnabled || - isThreadRagModeLocked(threadId) || - selectedLlmWikiIds.length > 0; + const selectedLlmWikiIds = Array.isArray( + mergedContext.llmwiki_knowledge_base_ids, + ) + ? mergedContext.llmwiki_knowledge_base_ids + : []; + const ragModeEnabled = + (referenceModeEntryEnabled && localSettings.display.ragModeEnabled) || + isThreadRagModeLocked(threadId) || + selectedLlmWikiIds.length > 0; if (ragModeEnabled) { lockThreadRagMode(threadId); } @@ -1666,8 +1674,18 @@ export function useThreadStream({ sendInFlightRef.current = false; } }, - [thread, _handleOnStart, t.uploads.uploadingFiles, context, 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; diff --git a/frontend-web/src/pages/IframeChatPage.tsx b/frontend-web/src/pages/IframeChatPage.tsx index cc6a010c..11851cfd 100644 --- a/frontend-web/src/pages/IframeChatPage.tsx +++ b/frontend-web/src/pages/IframeChatPage.tsx @@ -39,8 +39,14 @@ import { useLocalSettings, useThreadSettings, } from "@/core/settings"; -import { usePluginManagementSettings } from "@/core/system-settings/hooks"; -import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks"; +import { + useCitationDisplaySettings, + usePluginManagementSettings, +} from "@/core/system-settings/hooks"; +import { + stopActiveThreadBeforeClarificationReply, + useThreadStream, +} from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; import { cn } from "@/lib/utils"; @@ -383,6 +389,9 @@ export default function IframeChatPage() { }, [forceDefaultAssistant, pendingIframeSubmit, selectedAgentId]); const ragModeLocked = isThreadRagModeLocked(threadId); + const { data: citationDisplay } = useCitationDisplaySettings(); + const referenceModeEntryEnabled = + citationDisplay?.reference_mode_enabled ?? false; const ragModeEnabled = localSettings.display.ragModeEnabled || ragModeLocked; const contextCompressionEnabled = @@ -529,6 +538,7 @@ export default function IframeChatPage() { memoryInjectionEnabled: Boolean( settings.context.memory_injection_enabled ?? true, ), + referenceModeEntryEnabled, ragModeEnabled, ragModeLocked, contextCompressionEnabled, diff --git a/frontend-web/src/pages/PromptPrefixSettingsPage.tsx b/frontend-web/src/pages/PromptPrefixSettingsPage.tsx index 65e722c9..55c4af60 100644 --- a/frontend-web/src/pages/PromptPrefixSettingsPage.tsx +++ b/frontend-web/src/pages/PromptPrefixSettingsPage.tsx @@ -46,6 +46,8 @@ export default function PromptPrefixSettingsPage({ const [enabled, setEnabled] = useState(false); const [switchLabel, setSwitchLabel] = useState(""); const [promptPrefix, setPromptPrefix] = useState(""); + const [ordinaryQaMarkdownFormatEnabled, setOrdinaryQaMarkdownFormatEnabled] = + useState(false); // Only seed the draft from server data once. Re-syncing every time react-query // refetches (window focus, route remount, etc.) would silently overwrite the // user's in-progress edits — and made the toggle look unresponsive: a click @@ -57,6 +59,9 @@ export default function PromptPrefixSettingsPage({ setEnabled(data.enabled); setSwitchLabel(data.switch_label); setPromptPrefix(data.prompt_prefix); + setOrdinaryQaMarkdownFormatEnabled( + data.ordinary_qa_markdown_format_enabled === true, + ); initializedRef.current = true; }, [data]); @@ -79,6 +84,8 @@ export default function PromptPrefixSettingsPage({ enabled, switch_label: switchLabel, prompt_prefix: promptPrefix, + ordinary_qa_markdown_format_enabled: + ordinaryQaMarkdownFormatEnabled, }); toast.success("已保存",darkToastOpts.success); } catch (e) { @@ -95,10 +102,9 @@ export default function PromptPrefixSettingsPage({ } >

-

问答前缀配置

+

问答提示词配置

- 管理员可在此配置问答页输入框中的「前缀开关」。开启后,用户在新会话中第一次提问时, - 会将下方配置的前缀文本和原问题拼接后再发送给模型。 + 管理员可在此配置问答前缀和普通问答的固定 Markdown 格式提示词。

@@ -147,6 +153,21 @@ export default function PromptPrefixSettingsPage({ />
+
+
+
固定格式提示词
+

+ 开启后,普通问答的长篇内容会按“# 主标题、## 一、标题、### + (一) 标题、#### 1. 标题”的固定 Markdown 格式生成。默认关闭。 +

+
+ +
+
)} + +
+
+ {compactChrome && !historySidebarOpen && !compactActionsHidden && ( + + )} @@ -797,11 +832,10 @@ export default function ChatPage() { isLandingState && !compactLanding && "-translate-y-[calc(50vh-96px)]", - compactLanding - ? "max-w-[calc(var(--container-width-xl)+150px)]" - : isLandingState - ? "max-w-[calc(var(--container-width-md)+150px)]" - : "max-w-(--container-width-lg)", + // 新对话落地态在全量与简洁模式使用同一输入框宽度。 + isLandingState + ? "max-w-[calc(var(--container-width-md)+150px)]" + : "max-w-(--container-width-lg)", )} >
diff --git a/frontend-web/src/pages/iframe-chat-mode-buttons.test.ts b/frontend-web/src/pages/iframe-chat-mode-buttons.test.ts index e9144784..562f277d 100644 --- a/frontend-web/src/pages/iframe-chat-mode-buttons.test.ts +++ b/frontend-web/src/pages/iframe-chat-mode-buttons.test.ts @@ -4,7 +4,7 @@ import test from "node:test"; // @ts-ignore Node's test runner resolves the local .ts module directly. import { buildIframeModeButtons } from "./iframe-chat-mode-buttons.ts"; -test("includes the context compression toggle in the iframe chat toolbar", () => { +test("hides the memory toggle while retaining the other iframe chat toolbar controls", () => { const buttons = buildIframeModeButtons({ agentSelected: false, writingModeEnabled: false, @@ -20,7 +20,6 @@ test("includes the context compression toggle in the iframe chat toolbar", () => buttons.map((button) => button.key), [ "writing", - "memory", "references", "contextCompression", "pro", @@ -29,6 +28,22 @@ test("includes the context compression toggle in the iframe chat toolbar", () => ); }); +test("can restore the iframe memory toggle when the feature is re-enabled", () => { + const buttons = buildIframeModeButtons({ + agentSelected: false, + writingModeEnabled: false, + memoryInjectionEnabled: true, + hideMemoryButton: false, + referenceModeEntryEnabled: true, + ragModeEnabled: false, + ragModeLocked: false, + contextCompressionEnabled: false, + proModeEnabled: false, + }); + + assert.equal(buttons.some((button) => button.key === "memory"), true); +}); + test("hides the references toggle when reference mode is disabled globally", () => { const buttons = buildIframeModeButtons({ agentSelected: false, diff --git a/frontend-web/src/pages/iframe-chat-mode-buttons.ts b/frontend-web/src/pages/iframe-chat-mode-buttons.ts index 506fde9f..814726a2 100644 --- a/frontend-web/src/pages/iframe-chat-mode-buttons.ts +++ b/frontend-web/src/pages/iframe-chat-mode-buttons.ts @@ -15,6 +15,8 @@ type BuildIframeModeButtonsInput = { agentSelected: boolean; writingModeEnabled: boolean; memoryInjectionEnabled: boolean; + /** 产品当前全局隐藏记忆入口;保留 false 供后续无损恢复。 */ + hideMemoryButton?: boolean; referenceModeEntryEnabled: boolean; ragModeEnabled: boolean; ragModeLocked: boolean; @@ -26,6 +28,7 @@ export function buildIframeModeButtons({ agentSelected, writingModeEnabled, memoryInjectionEnabled, + hideMemoryButton = true, referenceModeEntryEnabled, ragModeEnabled, ragModeLocked, @@ -65,7 +68,9 @@ export function buildIframeModeButtons({ active: agentSelected, }, ]; - return referenceModeEntryEnabled - ? buttons - : buttons.filter((button) => button.key !== "references"); + return buttons.filter( + (button) => + (referenceModeEntryEnabled || button.key !== "references") && + (!hideMemoryButton || button.key !== "memory"), + ); } From aeb9a554977991e175a83bce9788ab4b3f878b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 1 Sep 2026 13:47:31 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat(agent-chat):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E4=BC=9A=E8=AF=9D=E5=BD=92=E5=B1=9E?= =?UTF-8?q?=E4=B8=8E=E5=8E=86=E5=8F=B2=E8=B7=AF=E7=94=B1=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=8C=89=E5=BD=93=E5=89=8D?= =?UTF-8?q?agent=E8=BF=87=E6=BB=A4=E4=BC=9A=E8=AF=9D=E5=B9=B6=E6=90=BA?= =?UTF-8?q?=E5=B8=A6agent=E4=B8=8A=E4=B8=8B=E6=96=87=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=EF=BC=8C=E8=A1=A5=E5=85=85=E6=97=A7=E7=BA=BF=E7=A8=8B=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=9B=9E=E6=BA=AF=E5=88=B0=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA=EF=BC=8C=E9=98=B2=E6=AD=A2=E8=AF=AF?= =?UTF-8?q?=E5=85=A5=E9=80=9A=E7=94=A8=E9=97=AE=E7=AD=94=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=BE=93=E5=85=A5=E6=A1=86=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=98=BE=E9=9A=90=E4=B8=8E=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=B1=95=E5=BC=80=E5=85=A5=E5=8F=A3=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E4=BD=93=E9=AA=8C=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/agents/agent-chat-sidebar.tsx | 23 ++++-- .../src/components/workspace/input-box.tsx | 4 +- frontend-web/src/core/threads/utils.ts | 14 +++- frontend-web/src/pages/AgentChatPage.tsx | 55 +++++++++----- frontend-web/src/pages/WorkspaceRoutes.tsx | 74 ++++++++++++++++++- 5 files changed, 140 insertions(+), 30 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx index 45f39694..4ac99677 100644 --- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx @@ -64,7 +64,7 @@ import { import { useDeleteThread, useRenameThread, - useThreads, + useThreadsPage, useToggleThreadFavorite, useToggleThreadPin, } from "@/core/threads/hooks"; @@ -351,8 +351,8 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) } /** - * 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 - * 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。 + * 当前智能体的「最近对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 + * 按 agent_id 在服务端过滤;支持重命名/置顶/收藏/分享/导出/删除。 */ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const { t } = useI18n(); @@ -361,7 +361,12 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const pathOf = usePathOfThread(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); - const { data: threads = [] } = useThreads(); + const { data: threadPage } = useThreadsPage({ + page: 1, + pageSize: 50, + metadata: { agent_id: activeAgentId ?? "" }, + }); + const threads = threadPage?.threads ?? []; const { mutate: deleteThread } = useDeleteThread(); const { mutate: renameThread } = useRenameThread(); const { toggle: togglePin } = useToggleThreadPin(); @@ -373,8 +378,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const [renameValue, setRenameValue] = useState(""); const [shareThreadId, setShareThreadId] = useState(null); - // 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致; - // 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 + // 服务端先按当前智能体过滤;客户端再排除调度/会商/报告结构&写作配置向导等 + // 系统会话。智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 const visibleThreads = useMemo( () => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))), [threads], @@ -479,7 +484,11 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { ) : ( filteredThreads.map((thread) => { const isBoot = isBootstrapThread(thread); - const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread); + // 正常记录依据自身保存的 agent_id 跳转;早期记录可能尚未写入 + // agent_id,此时保留当前智能体上下文,避免从智能体页误落到通用问答。 + const href = isBoot + ? bootstrapResumePath(thread.thread_id) + : pathOf(thread, { agent_id: activeAgentId }); const isActive = href === pathname; const pinned = isThreadPinned(thread); const favorited = isThreadFavorite(thread); diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index 4d0a0be8..3deab8dc 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -758,7 +758,7 @@ export function InputBox({ )} /> )} - {!frontendRagActive && !hideContextTools && !hideMemoryTool && 文件 } - {!frontendRagActive && !hideContextTools && | null; +}): string | undefined { + return pickAgentRef(thread.context, thread.metadata); +} + export function pathOfThread( thread: ThreadRouteTarget, context?: ThreadAgentRef | null, @@ -36,7 +48,7 @@ export function pathOfThread( const agentRef = typeof thread === "string" ? pickAgentRef(context, null) - : pickAgentRef(thread.context, thread.metadata); + : agentRefOfThread(thread) ?? pickAgentRef(context, null); return agentRef ? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}` diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 54f67752..57e0d7cb 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -3,6 +3,7 @@ import { BotIcon, Compass, + PanelLeftOpenIcon, PanelRightOpenIcon, PlusSquare, } from "lucide-react"; @@ -13,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; +import { useSidebarSafe } from "@/components/ui/sidebar"; import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar"; import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel"; import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips"; @@ -225,6 +227,7 @@ export default function AgentChatPage({ const { t } = useI18n(); const [showFollowups, setShowFollowups] = useState(false); const router = useRouter(); + const workspaceSidebar = useSidebarSafe(); const { agent_id: agentIdFromPath } = useParams<{ agent_id: string; @@ -806,6 +809,23 @@ export default function AgentChatPage({
{/* Header in normal flow, fixed height. */}
+ {/* 工作区左栏收起后,在智能体聊天区左上角提供展开入口。 */} + {showAgentList && + !isEmbedded && + workspaceSidebar && + !workspaceSidebar.open && ( + + )}
)} - - - {/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示 (展开态的「收起」按钮在面板右上角)。桌面端专用(lg+), 窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */} @@ -919,6 +922,23 @@ export default function AgentChatPage({ enabled={tokenUsageEnabled} messages={displayThread.messages} /> + + +
@@ -1101,7 +1121,6 @@ export default function AgentChatPage({ onAskQuestion={(text) => handleSubmit({ text, files: [] })} collapsed={detailCollapsed} onCollapsedChange={setDetailCollapsed} - showRecentChats={!taskCtxOn} compactAppearanceActions={compactAppearanceActions} />
diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index d6edf46a..cf1cb3dc 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -1,7 +1,8 @@ -import type { CSSProperties, ReactNode } from 'react'; +import { useEffect, useState, type CSSProperties, type ReactNode } from 'react'; import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { getStoredAuth } from '@/core/auth'; +import { getAPIClient } from '@/core/api'; import { isSentimentAgentVisible } from '@/core/sentiment-agent/config'; import { resolveChatsIframeEntryRedirect, @@ -25,6 +26,7 @@ import { ArtifactsProvider } from '@/components/workspace/artifacts'; import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context'; import { RouteBaseProvider } from '@/core/route-base'; +import { agentRefOfThread } from '@/core/threads/utils'; import ChatsPage from './ChatsPage'; import ChatPage from './ChatPage'; @@ -341,12 +343,80 @@ function ChatThreadEntryRoute({ return ( - + ); } +/** + * Older bookmarks and a few historic list entries use the generic + * ``/chats/:thread_id`` path even for an agent-owned conversation. Recover the + * owning agent from the persisted thread record before rendering so those + * links keep the agent workspace (agent list and detail panel) instead of + * falling back to the general Q&A page. Task workspace threads intentionally + * remain on their explicit task routes because their surrounding layout is + * part of the task context. + */ +function AgentAwareChatThreadContent() { + const { thread_id: threadId } = useParams<{ thread_id: string }>(); + const location = useLocation(); + const [agentId, setAgentId] = useState(null); + const [resolved, setResolved] = useState(false); + + useEffect(() => { + let cancelled = false; + setAgentId(null); + setResolved(false); + + if (!threadId) { + setResolved(true); + return () => { + cancelled = true; + }; + } + + void getAPIClient() + .threads.get(threadId) + .then((thread) => { + if (cancelled) return; + const record = thread as { + context?: { agent_id?: string; agent_name?: string } | null; + metadata?: Record | null; + }; + // Task-bound threads need their task host to preserve the purpose gate, + // action partitioning, and other task-specific controls. + const isTaskThread = typeof record.metadata?.taskId === 'string'; + setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null); + }) + .catch(() => { + if (!cancelled) setAgentId(null); + }) + .finally(() => { + if (!cancelled) setResolved(true); + }); + + return () => { + cancelled = true; + }; + }, [threadId]); + + if (!resolved) { + return
; + } + + if (threadId && agentId) { + return ( + + ); + } + + return ; +} + /** * `chats_iframe` is reserved for the Brainy postMessage protocol. `from=iframe` * and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding From dbf62e588911b95357f95c97567364dc99257111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 1 Sep 2026 13:47:39 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(deep-research):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E7=A0=94=E7=A9=B6=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E4=B8=8E=E7=BB=93=E6=9E=84=E9=85=8D=E7=BD=AE=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E7=9A=84=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为深度研究沙箱预览区和工作流配置弹窗补充暗色下背景与边框配色,避免在深色模式出现对比度不足和样式不一致。 --- .../deep-research/ResearchSandboxPanel.tsx | 4 +- .../deep-research/WorkflowConfigDialog.tsx | 126 +++++++++--------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx b/frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx index 0d343c95..1aebe09e 100644 --- a/frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx +++ b/frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx @@ -155,12 +155,12 @@ export function ResearchSandboxPanel({ /> -
+
{selectedArtifact && previewThreadId && stubMessages.length > 0 ? ( <> {documentRewriteActive && onCancelDocumentRewrite && ( diff --git a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx index 33cc7d4a..a17d4094 100644 --- a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx +++ b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx @@ -114,7 +114,7 @@ const outlineMarkdownClassName = "[&_p]:my-2.5 [&_ul]:my-2.5 [&_ol]:my-2.5 [&_li]:my-1 " + "[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 " + "[&_strong]:font-semibold [&_strong]:text-neutral-900 dark:[&_strong]:text-slate-100 " + - "[&_blockquote]:my-3 [&_blockquote]:border-l-4 [&_blockquote]:border-rose-200 [&_blockquote]:pl-4 [&_blockquote]:text-neutral-500 dark:[&_blockquote]:border-rose-900/50 dark:[&_blockquote]:text-slate-400 " + + "[&_blockquote]:my-3 [&_blockquote]:border-l-4 [&_blockquote]:border-rose-200 [&_blockquote]:pl-4 [&_blockquote]:text-neutral-500 dark:[&_blockquote]:border-[#2b8ffe]/45 dark:[&_blockquote]:text-slate-400 " + "[&_code]:rounded [&_code]:border [&_code]:bg-neutral-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs dark:[&_code]:border-slate-700 dark:[&_code]:bg-slate-800 " + "[&_pre]:my-4 [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-neutral-50 [&_pre]:p-4 dark:[&_pre]:border-slate-700 dark:[&_pre]:bg-slate-800 " + "[&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0"; @@ -309,17 +309,17 @@ export function WorkflowConfigDialog({ overlayClassName="bg-slate-900/60 backdrop-blur-xs" className={ !detail - ? "flex h-[85vh] max-h-[85vh] w-[calc(100vw-3rem)] max-w-6xl flex-col gap-0 overflow-hidden rounded-3xl border border-slate-200/80 bg-white p-0 shadow-2xl dark:border-slate-800 dark:bg-slate-900 sm:max-w-6xl" - : "flex h-[85vh] max-h-[85vh] w-[calc(100vw-3rem)] max-w-6xl flex-col gap-0 overflow-hidden rounded-2xl border border-neutral-200/90 bg-white p-0 shadow-2xl shadow-neutral-950/20 dark:border-slate-800 dark:bg-slate-900 sm:max-w-6xl" + ? "flex h-[85vh] max-h-[85vh] w-[calc(100vw-3rem)] max-w-6xl flex-col gap-0 overflow-hidden rounded-3xl border border-slate-200/80 bg-white p-0 shadow-2xl dark:border-[#263b5c] dark:bg-[#0f172a] sm:max-w-6xl" + : "flex h-[85vh] max-h-[85vh] w-[calc(100vw-3rem)] max-w-6xl flex-col gap-0 overflow-hidden rounded-2xl border border-neutral-200/90 bg-white p-0 shadow-2xl shadow-neutral-950/20 dark:border-[#263b5c] dark:bg-[#0f172a] sm:max-w-6xl" } > {/* header:列表视图=标题+筛选+新增;详情视图=结构标题 + 返回结构库 */} {!detail ? ( <> -
+
-
+

工作流配置 · 报告结构

@@ -331,7 +331,7 @@ export function WorkflowConfigDialog({
-
+
@@ -347,14 +347,14 @@ export function WorkflowConfigDialog({ value={titleFilter} onChange={(event) => setTitleFilter(event.target.value)} placeholder="按标题或检索词筛选..." - className="h-10 rounded-xl border-slate-200/90 bg-slate-50/60 pr-8 pl-9 text-xs text-slate-800 shadow-2xs placeholder:text-slate-400 focus-visible:border-[#F5222D] focus-visible:ring-[#F5222D]/20 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 sm:text-sm" + className="h-10 rounded-xl border-slate-200/90 bg-slate-50/60 pr-8 pl-9 text-xs text-slate-800 shadow-2xs placeholder:text-slate-400 focus-visible:border-[#F5222D] focus-visible:ring-[#F5222D]/20 dark:border-[#2a4164] dark:bg-[#17243a] dark:text-slate-100 dark:focus-visible:border-[#2b8ffe] dark:focus-visible:ring-[#2b8ffe]/25 sm:text-sm" /> {titleFilter && ( @@ -366,7 +366,7 @@ export function WorkflowConfigDialog({ value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as ReportStructureStatusFilter)} aria-label="启用状态筛选" - className="h-10 cursor-pointer appearance-none rounded-xl border border-slate-200/90 bg-slate-50/60 py-2 pr-8 pl-3 text-xs font-medium text-slate-700 shadow-2xs transition-all hover:border-slate-300 focus:border-[#F5222D] focus:ring-2 focus:ring-[#F5222D]/20 focus:outline-hidden dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 sm:text-sm" + className="h-10 cursor-pointer appearance-none rounded-xl border border-slate-200/90 bg-slate-50/60 py-2 pr-8 pl-3 text-xs font-medium text-slate-700 shadow-2xs transition-all hover:border-slate-300 focus:border-[#F5222D] focus:ring-2 focus:ring-[#F5222D]/20 focus:outline-hidden dark:border-[#2a4164] dark:bg-[#17243a] dark:text-slate-200 dark:hover:border-[#2b8ffe]/70 dark:focus:border-[#2b8ffe] dark:focus:ring-[#2b8ffe]/25 sm:text-sm" > @@ -380,7 +380,7 @@ export function WorkflowConfigDialog({ value={modeFilter} onChange={(event) => setModeFilter(event.target.value as "all" | ReportStructureMode)} aria-label="大纲类型筛选" - className="h-10 cursor-pointer appearance-none rounded-xl border border-slate-200/90 bg-slate-50/60 py-2 pr-8 pl-3 text-xs font-medium text-slate-700 shadow-2xs transition-all hover:border-slate-300 focus:border-[#F5222D] focus:ring-2 focus:ring-[#F5222D]/20 focus:outline-hidden dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 sm:text-sm" + className="h-10 cursor-pointer appearance-none rounded-xl border border-slate-200/90 bg-slate-50/60 py-2 pr-8 pl-3 text-xs font-medium text-slate-700 shadow-2xs transition-all hover:border-slate-300 focus:border-[#F5222D] focus:ring-2 focus:ring-[#F5222D]/20 focus:outline-hidden dark:border-[#2a4164] dark:bg-[#17243a] dark:text-slate-200 dark:hover:border-[#2b8ffe]/70 dark:focus:border-[#2b8ffe] dark:focus:ring-[#2b8ffe]/25 sm:text-sm" > @@ -391,11 +391,11 @@ export function WorkflowConfigDialog({
- + {visible.length} 套
) : ( -
+
-
+
@@ -420,8 +420,8 @@ export function WorkflowConfigDialog({ {detail.title} {detail.id === appliedStructureId && ( - - + + 当前使用 )} @@ -440,7 +440,7 @@ export function WorkflowConfigDialog({
) : ( -
-