Переглянути джерело

fix: 修复3个Bug - 面板宽度自适应/保存为大纲提示/HTML标签过滤

Bug1: writing-workspace 添加 window resize 监听,窗口缩小时自动 clamp 面板宽度
Bug2: handleSaveAsOutline 无 project/activeConversationId/空内容时改为 toast 提示
Bug3: stripStructuredMarkers 增加 HTML 标签清理(details/summary/b/strong/div/p 等)
Mochocyang 2 місяців тому
батько
коміт
cba3ab5166

+ 13 - 1
src/components/layout/writing-workspace.tsx

@@ -1,4 +1,4 @@
-import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
+import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
 import { PreviewPanel } from "./preview-panel"
 import { clampChatWidth, getInitialChatWidth } from "@/lib/workspace-layout"
 import { useWikiStore } from "@/stores/wiki-store"
@@ -23,6 +23,18 @@ export function WritingWorkspace() {
     localStorage.setItem("lk-chat-right-width", String(chatWidth))
   }, [chatWidth])
 
+  // 窗口缩小时自动 clamp 面板宽度,避免面板超出窗口
+  useEffect(() => {
+    const handleResize = () => {
+      setChatWidth((prev) => {
+        const clamped = clampChatWidth(prev)
+        return clamped !== prev ? clamped : prev
+      })
+    }
+    window.addEventListener("resize", handleResize)
+    return () => window.removeEventListener("resize", handleResize)
+  }, [])
+
   const startHorizontalResize = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
     event.preventDefault()
     horizontalResizingRef.current = true

+ 14 - 2
src/components/sources/outline-chat-panel.tsx

@@ -3068,11 +3068,23 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
 
   const handleSaveAsOutline = useCallback(
     async (content: string) => {
-      if (!project || !activeConversationId) return;
+      if (!project) {
+        toast.error("请先打开一个项目,再保存大纲");
+        return;
+      }
+      if (!activeConversationId) {
+        toast.error("当前没有活跃的对话,无法保存大纲");
+        return;
+      }
+      const cleanedContent = cleanNextStepArtifacts(content);
+      if (!cleanedContent.trim()) {
+        toast.error("内容为空,无法保存为大纲");
+        return;
+      }
       const capturedConvId = activeConversationId;
       setSaveStatus("");
       try {
-        const draft = prepareOutlineSaveDraft(cleanNextStepArtifacts(content), []);
+        const draft = prepareOutlineSaveDraft(cleanedContent, []);
         const classification = classifyOutlineSaveTarget({
           title: draft.title,
           content: draft.content,

+ 47 - 0
src/lib/novel/outline-intent-clarity.spec.ts

@@ -104,4 +104,51 @@ describe("stripStructuredMarkers", () => {
     expect(result).not.toContain("intent_clarity")
     expect(result).not.toContain("next_step")
   })
+
+  it("移除 <details> 折叠块及其内部内容", () => {
+    const text = "正文内容\n<details>\n<summary>清单</summary>\n内部内容\n</details>\n更多正文"
+    const result = stripStructuredMarkers(text)
+    expect(result).toContain("正文内容")
+    expect(result).toContain("更多正文")
+    expect(result).not.toContain("details")
+    expect(result).not.toContain("summary")
+    expect(result).not.toContain("内部内容")
+  })
+
+  it("移除流式中间态的 <details> 开标签及后续内容", () => {
+    const text = "正文内容\n<details>\n<summary><b>清单</b></summary>\n"
+    const result = stripStructuredMarkers(text)
+    expect(result).toBe("正文内容")
+    expect(result).not.toContain("details")
+  })
+
+  it("移除 <b> <strong> <br> 等行内 HTML 标签但保留文本", () => {
+    const text = "这是<b>粗体</b>文本<br>换行<strong>加粗</strong>"
+    const result = stripStructuredMarkers(text)
+    expect(result).toContain("粗体")
+    expect(result).toContain("加粗")
+    expect(result).toContain("换行")
+    expect(result).not.toContain("<b>")
+    expect(result).not.toContain("</b>")
+    expect(result).not.toContain("<strong>")
+    expect(result).not.toContain("<br>")
+  })
+
+  it("移除 <div> <p> 等块级 HTML 标签但保留文本", () => {
+    const text = "<div>段落一</div><p>段落二</p>"
+    const result = stripStructuredMarkers(text)
+    expect(result).toContain("段落一")
+    expect(result).toContain("段落二")
+    expect(result).not.toContain("<div>")
+    expect(result).not.toContain("<p>")
+  })
+
+  it("移除包含 emoji 的 <summary> 标签", () => {
+    const text = '<details>\n<summary><b>📋 新增设定写回清单</b></summary>\n\n***\n</details>\n正文'
+    const result = stripStructuredMarkers(text)
+    expect(result).toBe("正文")
+    expect(result).not.toContain("📋")
+    expect(result).not.toContain("details")
+    expect(result).not.toContain("summary")
+  })
 })

+ 8 - 1
src/lib/novel/outline-intent-clarity.ts

@@ -112,7 +112,14 @@ export function stripStructuredMarkers(text: string): string {
     // 4. 清理可能残留的裸闭标签
     .replace(/<!--\s*\/intent_clarity\s*-->/gi, "")
     .replace(/<!--\s*\/next_step\s*-->/gi, "")
-    // 5. 清理多余空行
+    // 5. 移除 AI 生成的 HTML 折叠块(<details>...</details>)及其内部内容
+    .replace(/<details[\s\S]*?<\/details>/gi, "")
+    .replace(/<details[^>]*>[\s\S]*$/gi, "")
+    // 6. 移除其他 AI 常见的 HTML 块级标签对(<summary>...</summary> 等)
+    .replace(/<\/?(?:details|summary|fieldset|legend|table|thead|tbody|tr|td|th|caption|colgroup|col)\b[^>]*>/gi, "")
+    // 7. 移除 AI 常见的 HTML 行内标签(保留标签内的文本内容)
+    .replace(/<\/?(?:b|strong|i|em|u|ins|del|s|mark|small|sub|sup|code|kbd|samp|var|abbr|cite|q|span|font|br|hr|wbr|div|p|section|article|header|footer|nav|aside|main|ul|ol|li|dl|dt|dd|h[1-6]|pre|blockquote|figure|figcaption)\b[^>]*>/gi, "")
+    // 8. 清理多余空行
     .replace(/\n{3,}/g, "\n\n")
     .trim()
 }