diff --git a/frontend-web/src/components/workspace/chats/chat-box.tsx b/frontend-web/src/components/workspace/chats/chat-box.tsx index 4589c288..19fad244 100644 --- a/frontend-web/src/components/workspace/chats/chat-box.tsx +++ b/frontend-web/src/components/workspace/chats/chat-box.tsx @@ -22,6 +22,7 @@ import { latestInFlightWriteFilePreview } from "@/core/artifacts/write-file-in-f import { shouldAutoOpenWritingArtifact } from "@/core/threads/writing-mode"; 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"; @@ -87,7 +88,11 @@ const ChatBox: React.FC<{ threadId, disableArtifacts = false, }) => { - const { isMock, thread } = useThread(); + const { + isMock, + thread, + knowledgeReferencesEnabled = false, + } = useThread(); const pathname = usePathname(); const threadIdRef = useRef(threadId); const artifactsCountRef = useRef(0); @@ -106,8 +111,13 @@ const ChatBox: React.FC<{ const writingModeEnabled = !!settings.context.writing_mode; 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) || + knowledgeReferencesEnabled; // 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. @@ -121,7 +131,7 @@ const ChatBox: React.FC<{ ); const referencesEnabled = ragModeEnabled || hasFrontendRag; const shouldFetchBackendReferenceBatches = - referencesEnabled && !hasFrontendRag; + referencesEnabled && !hasFrontendRag && !!threadId && threadId !== "new"; const { skills } = useSkills(); const canScanArtifactDirectory = 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 && ; - isMock?: boolean; - /** - * When set, the conversation is rendered from a public read-only share. +export interface ThreadContextType { + thread: BaseStream; + isMock?: boolean; + /** + * True when the active conversation has selected LLMWiki knowledge bases. + * Knowledge-base Q&A should always expose its source documents, independent + * from the admin-controlled manual reference-mode button. + */ + knowledgeReferencesEnabled?: boolean; + /** + * When set, the conversation is rendered from a public read-only share. * Artifact/image URLs then resolve to the unauthenticated * `/api/public/shares/{code}/files/...` endpoint instead of the * auth-gated thread artifact route. diff --git a/frontend-web/src/components/workspace/messages/message-group.tsx b/frontend-web/src/components/workspace/messages/message-group.tsx index ddd7bd20..33c03220 100644 --- a/frontend-web/src/components/workspace/messages/message-group.tsx +++ b/frontend-web/src/components/workspace/messages/message-group.tsx @@ -83,16 +83,18 @@ import { SourceCard } from "../skill-display-sources"; import { MarkdownContent } from "./markdown-content"; import { SingleRoundRagCitationDialog } from "./message-list-item"; -export function MessageGroup({ - className, - messages, - isLoading = false, - displaySources = [], -}: { - className?: string; - messages: Message[]; - isLoading?: boolean; - displaySources?: RuntimeDisplaySource[]; +export function MessageGroup({ + className, + messages, + isLoading = false, + displaySources = [], + ragModeEnabled = false, +}: { + className?: string; + messages: Message[]; + isLoading?: boolean; + displaySources?: RuntimeDisplaySource[]; + ragModeEnabled?: boolean; }) { const { t } = useI18n(); const [showAbove, setShowAbove] = useState( @@ -110,9 +112,9 @@ export function MessageGroup({ [messages, writingModeEnabled], ); const { skills } = useSkills(); - const skillDisplayMap = useMemo( - () => (settings.display.ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()), - [settings.display.ragModeEnabled, skills], + const skillDisplayMap = useMemo( + () => (ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()), + [ragModeEnabled, skills], ); const lastToolCallStep = useMemo(() => { const filteredSteps = steps.filter((step) => step.type === "toolCall"); @@ -207,7 +209,7 @@ export function MessageGroup({ {...step} isLoading={isLoading} skillDisplayMap={skillDisplayMap} - ragModeEnabled={settings.display.ragModeEnabled} + ragModeEnabled={ragModeEnabled} /> ), )} @@ -219,7 +221,7 @@ export function MessageGroup({ isLast={true} isLoading={isLoading} skillDisplayMap={skillDisplayMap} - ragModeEnabled={settings.display.ragModeEnabled} + 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..e5cae494 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,17 @@ 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 knowledgeReferencesEnabled = + threadContext?.knowledgeReferencesEnabled === true; + const ragModeEnabled = + (referenceModeEntryEnabled && settings.display.ragModeEnabled) || + isThreadRagModeLocked(threadId) || + knowledgeReferencesEnabled; const hasSingleRoundRag = useMemo( () => messages.some( @@ -778,12 +786,13 @@ export function MessageList({ )) { if (hasReasoning(message)) { results.push( - , + , ); } results.push( @@ -831,11 +840,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 47a81023..b6ad8102 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"; @@ -566,8 +571,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, @@ -1499,15 +1507,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); } @@ -1625,8 +1633,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/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 1cf53d5a..54f67752 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -777,7 +777,13 @@ export default function AgentChatPage({ : "ready"; return ( - + 0, + }} + > {/* Root: fixed-height horizontal split. ChatBox (chat + artifact preview) lives in the left flex cell; the agent detail panel is a separate flex sibling OUTSIDE ChatBox, so the artifact preview diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index 9873ff47..33819cf9 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -684,7 +684,14 @@ export default function ChatPage() { !isWujieEnv(); return ( - + 0, + }} + >

-

问答前缀配置

+

问答提示词配置

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

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

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

+
+ +
+