Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-zl
This commit is contained in:
commit
28eb8f86a7
1 file changed
+46
-134
@@ -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({
|
||||
<h3 className="min-w-0 truncate text-base font-bold tracking-tight text-neutral-900 dark:text-slate-100">
|
||||
{detailOutlineTitle}
|
||||
</h3>
|
||||
<span className="shrink-0 text-xs font-normal text-neutral-400 dark:text-slate-500">
|
||||
共 {detailOutlineSections.length} 个主要章节
|
||||
</span>
|
||||
{detailOutlineHeadingCount > 0 && (
|
||||
<span className="shrink-0 text-xs font-normal text-neutral-400 dark:text-slate-500">
|
||||
共 {detailOutlineHeadingCount} 个主要章节
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{detailOutlineSections.map((section) => (
|
||||
<div
|
||||
key={`${section.number}-${section.title}`}
|
||||
className="group rounded-xl transition-all"
|
||||
>
|
||||
<div className="mb-2.5 flex items-baseline gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-neutral-100 text-xs font-bold text-neutral-800 dark:bg-slate-800 dark:text-slate-200">
|
||||
{section.number}
|
||||
</span>
|
||||
<h4 className="text-sm font-bold tracking-tight text-neutral-900 dark:text-slate-100">
|
||||
{section.title}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{section.lines.length > 0 && (
|
||||
<div className="space-y-2.5 pl-7">
|
||||
{section.lines.map((line, index) => (
|
||||
<div
|
||||
key={`${section.number}-${index}`}
|
||||
className="text-xs leading-relaxed text-neutral-700 dark:text-slate-300"
|
||||
>
|
||||
{line.label ? (
|
||||
<>
|
||||
<span className="font-semibold text-neutral-900 dark:text-slate-100">
|
||||
{line.label}:
|
||||
</span>
|
||||
<span className="text-neutral-600 dark:text-slate-300">{line.content}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-neutral-600 dark:text-slate-300">{line.content}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{detail.content.trim() ? (
|
||||
<MarkdownContent
|
||||
className={outlineMarkdownClassName}
|
||||
content={detail.content}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-neutral-400 dark:text-slate-500">暂无大纲内容</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user