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/2] 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 格式生成。默认关闭。 +

+
+ +
+