From cdb8aa5f81556478cf5b089febcc1207a4ac24c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 24 Aug 2026 14:45:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deep-research/WorkflowConfigDialog.tsx | 180 +++++------------- 1 file changed, 46 insertions(+), 134 deletions(-) diff --git a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx index 09be8c0a..6af9c7bb 100644 --- a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx +++ b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx @@ -9,7 +9,7 @@ * 与 标题 筛选;卡片上可 switch 启停、编辑、删除(二次确认),并显示 * 最后修改时间与创建用户名;新增时不填状态(服务端强制默认启用)。 * - 详情视图:点击卡片即「选择」该结构(内容回填到高级配置的报告结构 - * custom_outline),弹窗切换为展示 标题 + 接口内容,右上角「返回」回列表。 + * custom_outline),弹窗切换为展示 标题 + Markdown 大纲预览,右上角「返回」回列表。 */ import { useEffect, useMemo, useState } from "react"; @@ -39,6 +39,8 @@ import { } from "lucide-react"; import { toast } from "sonner"; +import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; +import { reasoningPlugins } from "@/core/streamdown"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -102,16 +104,19 @@ function isSystemStructure(structure: ReportStructure): boolean { return creator === "系统内置" || creator.toLowerCase() === "system"; } -type OutlinePreviewLine = { - label?: string; - content: string; -}; - -type OutlinePreviewSection = { - number: string; - title: string; - lines: OutlinePreviewLine[]; -}; +const outlineMarkdownClassName = + "!h-auto !w-full max-w-none min-w-0 text-sm leading-7 text-neutral-700 dark:text-slate-300 " + + "[&_h1]:mt-0 [&_h1]:mb-3 [&_h1]:text-xl [&_h1]:font-bold [&_h1]:tracking-tight [&_h1]:text-neutral-900 dark:[&_h1]:text-slate-100 " + + "[&_h2]:mt-6 [&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-neutral-900 dark:[&_h2]:text-slate-100 " + + "[&_h3]:mt-4 [&_h3]:mb-1.5 [&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-neutral-900 dark:[&_h3]:text-slate-100 " + + "[&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-sm [&_h4]:font-semibold " + + "[&_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 " + + "[&_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"; function cleanOutlineText(line: string): string { return line @@ -123,34 +128,6 @@ function cleanOutlineText(line: string): string { .trim(); } -function extractMajorSection(text: string, fallbackNumber: number): { number: string; title: string } | null { - const decimalSubsection = /^\d+\.\d+/.test(text); - if (decimalSubsection) return null; - - const arabic = text.match(/^(\d+)[..、]\s*(.+)$/); - if (arabic) return { number: arabic[1], title: arabic[2].trim() }; - - const chineseOrder = text.match(/^([一二三四五六七八九十]+)[..、]\s*(.+)$/); - if (chineseOrder) return { number: String(fallbackNumber), title: chineseOrder[2].trim() }; - - const chapter = text.match(/^第[一二三四五六七八九十百千万〇零\d]+[章节篇部][::\s]*(.+)$/); - if (chapter) return { number: String(fallbackNumber), title: chapter[1].trim() }; - - return null; -} - -function parseOutlineLine(text: string): OutlinePreviewLine { - const withoutSubsectionNumber = text.replace(/^\d+(?:\.\d+)+\s*/, "").trim(); - const colonIndex = withoutSubsectionNumber.search(/[::]/); - if (colonIndex > 0 && colonIndex <= 18) { - return { - label: withoutSubsectionNumber.slice(0, colonIndex).trim(), - content: withoutSubsectionNumber.slice(colonIndex + 1).trim(), - }; - } - return { content: withoutSubsectionNumber }; -} - function extractOutlineDocumentTitle(content: string, fallbackTitle: string): string { for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) { const raw = rawLine.trim(); @@ -160,7 +137,7 @@ function extractOutlineDocumentTitle(content: string, fallbackTitle: string): st const text = cleanOutlineText(heading ? heading[2] : raw); if (!text || text === "报告标题") continue; if (/^(日期|类型|密级)\s*[::]/.test(text)) continue; - if (extractMajorSection(text, 1) || /^\d+\.\d+/.test(text)) break; + if (/^\d+([..、]|\.\d+)/.test(text) || /^[一二三四五六七八九十]+[..、]/.test(text)) break; const wrapped = text.match(/^《(.+)》$/); if (wrapped?.[1]) return wrapped[1].trim(); @@ -171,58 +148,19 @@ function extractOutlineDocumentTitle(content: string, fallbackTitle: string): st return fallbackTitle; } -function isLeadingDocumentTitle(text: string): boolean { - return /^《.+》$/.test(text) || /报告/.test(text); -} - -function parseOutlinePreview(content: string, documentTitle: string, structureTitle: string): OutlinePreviewSection[] { - const sections: OutlinePreviewSection[] = []; - let current: OutlinePreviewSection | null = null; - let nextNumber = 1; - +function countOutlineMajorHeadings(content: string, skipTitles: string[]): number { + const skip = new Set(skipTitles.filter(Boolean)); + let h1 = 0; + let h2 = 0; for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) { - const raw = rawLine.trim(); - if (!raw) continue; - - const heading = raw.match(/^(#{1,6})\s+(.+)$/); - const headingLevel = heading ? heading[1].length : null; - const text = cleanOutlineText(heading ? heading[2] : raw); - if (!text) continue; - if (text === documentTitle || text === structureTitle || text === "报告标题") continue; - if (/^(日期|类型|密级)\s*[::]/.test(text)) continue; - - const major = extractMajorSection(text, nextNumber); - const isHeadingMajor = headingLevel !== null && headingLevel <= 2 && !/^\d+\.\d+/.test(text); - if (!current && sections.length === 0 && !major && isLeadingDocumentTitle(text)) continue; - - if (major || isHeadingMajor) { - current = { - number: major?.number ?? String(nextNumber), - title: major?.title ?? text, - lines: [], - }; - sections.push(current); - nextNumber += 1; - continue; - } - - const line = parseOutlineLine(text); - if (!current) { - current = { - number: String(nextNumber), - title: line.label ?? line.content, - lines: line.label ? [{ content: line.content }] : [], - }; - sections.push(current); - nextNumber += 1; - continue; - } - current.lines.push(line); + const heading = rawLine.trim().match(/^(#{1,2})\s+(.+)$/); + if (!heading) continue; + const text = cleanOutlineText(heading[2]); + if (!text || text === "报告标题" || skip.has(text)) continue; + if (heading[1].length === 1) h1 += 1; + else h2 += 1; } - - if (sections.length > 0) return sections; - const fallback = cleanOutlineText(content).slice(0, 600); - return [{ number: "1", title: documentTitle || structureTitle, lines: fallback ? [{ content: fallback }] : [] }]; + return h1 > 0 ? h1 : h2; } // ── main dialog ────────────────────────────────────────────────────────────── @@ -289,8 +227,8 @@ export function WorkflowConfigDialog({ () => (detail ? extractOutlineDocumentTitle(detail.content, detail.title) : ""), [detail], ); - const detailOutlineSections = useMemo( - () => (detail ? parseOutlinePreview(detail.content, detailOutlineTitle, detail.title) : []), + const detailOutlineHeadingCount = useMemo( + () => (detail ? countOutlineMajorHeadings(detail.content, [detailOutlineTitle, detail.title]) : 0), [detail, detailOutlineTitle], ); @@ -734,50 +672,24 @@ export function WorkflowConfigDialog({

{detailOutlineTitle}

- - 共 {detailOutlineSections.length} 个主要章节 - + {detailOutlineHeadingCount > 0 && ( + + 共 {detailOutlineHeadingCount} 个主要章节 + + )} -
- {detailOutlineSections.map((section) => ( -
-
- - {section.number} - -

- {section.title} -

-
- - {section.lines.length > 0 && ( -
- {section.lines.map((line, index) => ( -
- {line.label ? ( - <> - - {line.label}: - - {line.content} - - ) : ( - {line.content} - )} -
- ))} -
- )} -
- ))} -
+ {detail.content.trim() ? ( + + ) : ( +

暂无大纲内容

+ )} From ebc727e4fd282e229230739fb077a6cbb5a58abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 24 Aug 2026 17:08:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/deep-research/WorkflowConfigDialog.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx index 6af9c7bb..33cc7d4a 100644 --- a/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx +++ b/frontend-web/src/pages/deep-research/WorkflowConfigDialog.tsx @@ -77,6 +77,7 @@ import { } from "@/strategy-components/api/report-structures"; import { useSkills } from "@/core/skills/hooks"; import { desensitize } from "@/lib/desensitize"; +import { cn } from "@/lib/utils"; // ── shared helpers ─────────────────────────────────────────────────────────── @@ -1213,12 +1214,16 @@ export function WorkflowConfigButton({ return structuresQuery.data?.find((structure) => structure.content.trim() === content)?.title ?? null; }, [appliedContent, structuresQuery.data]); const buttonLabel = selectedWorkflowTitle || "工作流配置"; + const isSelected = Boolean(selectedWorkflowTitle); return ( <>