mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-09-06 16:16:56 +08:00
feat(MarkdownViewer):增强 Markdown 解析和渲染能力
- 添加对 GFM (GitHub Flavored Markdown) 的支持 - 增加数学公式渲染功能 - 实现加粗编号标题的特殊处理 - 优化代码块样式 - 添加图片缩放功能
This commit is contained in:
@@ -30,19 +30,23 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.22",
|
||||||
"lottie-react": "^2.4.1",
|
"lottie-react": "^2.4.1",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.487.0",
|
||||||
|
"markdown-navbar": "^1.4.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^8.0.7",
|
||||||
|
"react-medium-image-zoom": "^5.2.14",
|
||||||
"react-resizable-panels": "^2.1.8",
|
"react-resizable-panels": "^2.1.8",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.1",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"remark-gfm": "1.0.0",
|
"rehype-katex": "^6.0.2",
|
||||||
|
"remark-gfm": "3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tailwindcss": "^4.1.3",
|
"tailwindcss": "^4.1.3",
|
||||||
|
|||||||
@@ -2,16 +2,27 @@ import { useState } from 'react'
|
|||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import { Button } from '@/components/ui/button.tsx'
|
import { Button } from '@/components/ui/button.tsx'
|
||||||
import { Copy, Download, FileText, ArrowRight } from 'lucide-react'
|
import { Copy, Download, FileText, ArrowRight } from 'lucide-react'
|
||||||
import { toast } from 'sonner' // 你可以换成自己的通知组件
|
import { toast } from 'react-hot-toast' // 你可以换成自己的通知组件
|
||||||
import Error from '@/components/Lottie/error.tsx'
|
import Error from '@/components/Lottie/error.tsx'
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import { solarizedlight as codeStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
import { atomDark as codeStyle } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
import Zoom from 'react-medium-image-zoom'
|
||||||
|
import 'react-medium-image-zoom/dist/styles.css'
|
||||||
|
|
||||||
import 'github-markdown-css/github-markdown-light.css'
|
import 'github-markdown-css/github-markdown-light.css'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Loading from '@/components/Lottie/Loading.tsx'
|
import Loading from '@/components/Lottie/Loading.tsx'
|
||||||
import Idle from '@/components/Lottie/Idle.tsx'
|
import Idle from '@/components/Lottie/Idle.tsx'
|
||||||
import { useTaskStore } from '@/store/taskStore'
|
import { useTaskStore } from '@/store/taskStore'
|
||||||
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
|
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
|
||||||
|
import gfm from 'remark-gfm'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
import rehypeKatex from 'rehype-katex'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
interface MarkdownViewerProps {
|
interface MarkdownViewerProps {
|
||||||
content: string
|
content: string
|
||||||
status: 'idle' | 'loading' | 'success' | 'failed'
|
status: 'idle' | 'loading' | 'success' | 'failed'
|
||||||
@@ -31,6 +42,8 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
|||||||
const currentTask = useTaskStore(state => state.getCurrentTask())
|
const currentTask = useTaskStore(state => state.getCurrentTask())
|
||||||
const taskStatus = currentTask?.status || 'PENDING'
|
const taskStatus = currentTask?.status || 'PENDING'
|
||||||
const retryTask = useTaskStore.getState().retryTask
|
const retryTask = useTaskStore.getState().retryTask
|
||||||
|
let firstHeadingRendered = false
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(content)
|
await navigator.clipboard.writeText(content)
|
||||||
@@ -124,7 +137,58 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
|||||||
<div className="markdown-body flex-1 bg-white">
|
<div className="markdown-body flex-1 bg-white">
|
||||||
{' '}
|
{' '}
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[gfm,remarkMath]}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
|
img: ({ node, ...props }) => (
|
||||||
|
<Zoom>
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
className="rounded-lg shadow-md max-w-full cursor-pointer mx-auto my-4 max-h-[300px] "
|
||||||
|
alt={props.alt || ''}
|
||||||
|
/>
|
||||||
|
</Zoom>
|
||||||
|
),
|
||||||
|
strong({ node, children, ...props }){
|
||||||
|
return <strong className="text-lg font-bold my-4 text-blue-600" {...props}>{children}</strong>
|
||||||
|
},
|
||||||
|
li({ node, children, ...props }) {
|
||||||
|
const rawText = String(children)
|
||||||
|
|
||||||
|
// 检测是否是“加粗的编号开头项”,比如 "**2. 算法摄影的兴起**"
|
||||||
|
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
|
||||||
|
|
||||||
|
if (isFakeHeading) {
|
||||||
|
return (
|
||||||
|
<p className="text-lg font-bold my-4 text-gray-800 text-left">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li {...props}>{children}</li>
|
||||||
|
},
|
||||||
|
h1({ node, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
className="text-3xl text-center font-bold my-6 text-blue-600"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h2({ node, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className="text-2xl font-bold my-4 text-blue-600"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
const codeContent = String(children).replace(/\n$/, '')
|
const codeContent = String(children).replace(/\n$/, '')
|
||||||
@@ -140,8 +204,10 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ content, status }) => {
|
|||||||
>
|
>
|
||||||
{codeContent}
|
{codeContent}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
console.log('点击负责')
|
||||||
navigator.clipboard.writeText(codeContent)
|
navigator.clipboard.writeText(codeContent)
|
||||||
toast.success('代码已复制')
|
toast.success('代码已复制')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ BASE_PROMPT = '''
|
|||||||
输出说明:
|
输出说明:
|
||||||
- 仅返回最终的 **Markdown 内容**。
|
- 仅返回最终的 **Markdown 内容**。
|
||||||
- **不要**将输出包裹在代码块中(例如:```` ```markdown ````,```` ``` ````)。
|
- **不要**将输出包裹在代码块中(例如:```` ```markdown ````,```` ``` ````)。
|
||||||
|
请注意,在生成 Markdown 时,避免将编号标题(如“1. **内容**”)写成有序列表的格式,以免解析错误。
|
||||||
|
|
||||||
|
- 如果要加粗并保留编号,应使用 `1\. **内容**`(加反斜杠),防止被误解析为有序列表。
|
||||||
|
- 或者使用 `## 1. 内容` 的形式作为标题。
|
||||||
|
|
||||||
|
请确保以下格式 **不会出现误渲染**:
|
||||||
|
❌ `1. **xxx**`
|
||||||
|
✅ `1\. **xxx**` 或 `## 1. xxx`
|
||||||
|
|
||||||
视频分段(格式:开始时间 - 内容):
|
视频分段(格式:开始时间 - 内容):
|
||||||
|
|
||||||
@@ -27,10 +35,14 @@ BASE_PROMPT = '''
|
|||||||
根据上面的分段转录内容,生成结构化的笔记,遵循以下原则:
|
根据上面的分段转录内容,生成结构化的笔记,遵循以下原则:
|
||||||
|
|
||||||
1. **完整信息**:记录尽可能多的相关细节,确保内容全面。
|
1. **完整信息**:记录尽可能多的相关细节,确保内容全面。
|
||||||
2. **清晰结构**:用合适的标题级别(`##`,`###`)整理内容,概述每个部分的要点。(如果额外重要的任务有格式需求可以不遵守)
|
2. **清晰结构**:用合适的标题级别(`##`,`###`)整理内容,概述每个部分的要点。主标题用`#`来标识(如果额外重要的任务有格式需求可以不遵守)
|
||||||
3. **去除无关内容**:省略广告、填充词、问候语和不相关的言论。
|
3. **去除无关内容**:省略广告、填充词、问候语和不相关的言论。
|
||||||
4. **保留关键细节**:保留重要事实、示例、结论和建议。(如果额外重要的任务有格式需求可以不遵守)
|
4. **保留关键细节**:保留重要事实、示例、结论和建议。(如果额外重要的任务有格式需求可以不遵守)
|
||||||
5. **可读布局**:必要时使用项目符号,并保持段落简短,增强可读性。(如果额外重要的任务有格式需求可以不遵守)
|
5. **可读布局**:必要时使用项目符号,并保持段落简短,增强可读性。(如果额外重要的任务有格式需求可以不遵守)
|
||||||
|
6. 视频中提及的数学公式必须保留,并以 LaTeX 语法形式呈现,适合 Markdown 渲染。
|
||||||
|
|
||||||
|
|
||||||
|
请始终遵循此规则。
|
||||||
|
|
||||||
额外重要的任务如下(每一个都必须严格完成):
|
额外重要的任务如下(每一个都必须严格完成):
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class VideoRequest(BaseModel):
|
|||||||
task_id: Optional[str] = None
|
task_id: Optional[str] = None
|
||||||
format: Optional[list] = []
|
format: Optional[list] = []
|
||||||
style: str = None
|
style: str = None
|
||||||
extras: Optional[str]
|
extras: Optional[str]=None
|
||||||
video_understanding: Optional[bool] = False
|
video_understanding: Optional[bool] = False
|
||||||
video_interval: Optional[int] = 0
|
video_interval: Optional[int] = 0
|
||||||
grid_size: Optional[list] = []
|
grid_size: Optional[list] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user