mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-14 20:08:42 +08:00
feat(backend): 添加 Groq供应商支持并优化笔记生成流程- 在 builtin_providers.json 中添加 Groq 供应商信息
- 实现 GroqTranscriber 类以支持 Groq 语音转录服务 - 新增异常处理中间件以提高系统稳定性 - 优化笔记生成流程,增加错误处理和日志记录 - 添加思维导图功能和相关组件 -重构 Markdown 查看器以支持切换视图模式
This commit is contained in:
8
BillNote_frontend/src/lib/markmap.ts
Normal file
8
BillNote_frontend/src/lib/markmap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { loadCSS, loadJS } from 'markmap-common'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
import * as markmap from 'markmap-view'
|
||||
|
||||
export const transformer = new Transformer()
|
||||
const { scripts, styles } = transformer.getAssets()
|
||||
loadCSS(styles)
|
||||
loadJS(scripts, { getMarkmap: () => markmap })
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Copy, Download } from 'lucide-react'
|
||||
import { Copy, Download, BrainCircuit } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
@@ -43,6 +43,8 @@ export function MarkdownHeader({
|
||||
createAt,
|
||||
showTranscribe,
|
||||
setShowTranscribe,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
}: NoteHeaderProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -122,6 +124,24 @@ export function MarkdownHeader({
|
||||
|
||||
{/* 右侧操作按钮 */}
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setViewMode(viewMode == 'preview' ? 'map' : 'preview')
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<BrainCircuit className="mr-1.5 h-4 w-4" />
|
||||
<span className="text-sm">{viewMode == 'preview' ? '思维导图' : 'markdown'}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>思维导图</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import { Copy, Download, ArrowRight, Play, ExternalLink } from 'lucide-react'
|
||||
@@ -22,6 +22,7 @@ import { useTaskStore } from '@/store/taskStore'
|
||||
import { noteStyles } from '@/constant/note.ts'
|
||||
import { MarkdownHeader } from '@/pages/HomePage/components/MarkdownHeader.tsx'
|
||||
import TranscriptViewer from '@/pages/HomePage/components/transcriptViewer.tsx'
|
||||
import MarkmapEditor from '@/pages/HomePage/components/MarkmapComponent.tsx'
|
||||
|
||||
interface VersionNote {
|
||||
ver_id: string
|
||||
@@ -58,6 +59,8 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
|
||||
const retryTask = useTaskStore.getState().retryTask
|
||||
const isMultiVersion = Array.isArray(currentTask?.markdown)
|
||||
const [showTranscribe, setShowTranscribe] = useState(false)
|
||||
const [viewMode, setViewMode] = useState<'map' | 'preview'>('preview')
|
||||
const svgRef = useRef<SVGSVGElement>(null)
|
||||
// 多版本内容处理
|
||||
useEffect(() => {
|
||||
if (!currentTask) return
|
||||
@@ -99,7 +102,33 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
const alertButton = {
|
||||
id: 'alert',
|
||||
title: '测试警告',
|
||||
content: '⚠️',
|
||||
onClick: () => alert('你点击了自定义按钮!'),
|
||||
}
|
||||
const exportButton = {
|
||||
id: 'export',
|
||||
title: '导出思维导图',
|
||||
content: '⤓',
|
||||
onClick: () => {
|
||||
const svgEl = svgRef.current
|
||||
if (!svgEl) return
|
||||
// 同上面的序列化逻辑
|
||||
const serializer = new XMLSerializer()
|
||||
const source = serializer.serializeToString(svgEl)
|
||||
const blob = new Blob(['<?xml version="1.0" encoding="UTF-8"?>', source], {
|
||||
type: 'image/svg+xml;charset=utf-8',
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'mindmap.svg'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
},
|
||||
}
|
||||
const handleDownload = () => {
|
||||
const task = getCurrentTask()
|
||||
const name = task?.audioMeta.title || 'note'
|
||||
@@ -167,270 +196,285 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
|
||||
createAt={createTime}
|
||||
showTranscribe={showTranscribe}
|
||||
setShowTranscribe={setShowTranscribe}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
/>
|
||||
|
||||
{/* 中间内容区域:滚动容器 */}
|
||||
<div className="flex flex-1 overflow-hidden bg-white py-2">
|
||||
{selectedContent && selectedContent !== 'loading' && selectedContent !== 'empty' ? (
|
||||
<>
|
||||
<ScrollArea className="w-full">
|
||||
<div className={'markdown-body w-full px-2'}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[gfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
// Headings with improved styling and anchor links
|
||||
h1: ({ children, ...props }) => (
|
||||
<h1
|
||||
className="text-primary my-6 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, ...props }) => (
|
||||
<h2
|
||||
className="text-primary mt-10 mb-4 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children, ...props }) => (
|
||||
<h3
|
||||
className="text-primary mt-8 mb-4 scroll-m-20 text-xl font-semibold tracking-tight"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children, ...props }) => (
|
||||
<h4
|
||||
className="text-primary mt-6 mb-2 scroll-m-20 text-lg font-semibold tracking-tight"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
|
||||
// Paragraphs with better line height
|
||||
p: ({ children, ...props }) => (
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6" {...props}>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
|
||||
// Enhanced links with special handling for "原片" links
|
||||
a: ({ href, children, ...props }) => {
|
||||
const isOriginLink =
|
||||
typeof children[0] === 'string' &&
|
||||
(children[0] as string).startsWith('原片 @')
|
||||
|
||||
if (isOriginLink) {
|
||||
const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/)
|
||||
const timeText = timeMatch ? timeMatch[1] : '原片'
|
||||
|
||||
return (
|
||||
<span className="origin-link my-2 inline-flex">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
|
||||
{...props}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
<span>原片({timeText})</span>
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Default link styling with external indicator
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{href?.startsWith('http') && (
|
||||
<ExternalLink className="ml-0.5 inline-block h-3 w-3" />
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced image with zoom capability
|
||||
img: ({ node, ...props }) => (
|
||||
<div className="my-8 flex justify-center">
|
||||
<Zoom>
|
||||
<img
|
||||
{...props}
|
||||
className="max-w-full cursor-zoom-in rounded-lg object-cover shadow-md transition-all hover:shadow-lg"
|
||||
style={{ maxHeight: '500px' }}
|
||||
/>
|
||||
</Zoom>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Better strong/bold text
|
||||
strong: ({ children, ...props }) => (
|
||||
<strong className="text-primary font-bold" {...props}>
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
|
||||
// Enhanced list items with support for "fake headings"
|
||||
li: ({ children, ...props }) => {
|
||||
const rawText = String(children)
|
||||
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
|
||||
|
||||
if (isFakeHeading) {
|
||||
return <div className="text-primary my-4 text-lg font-bold">{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="my-1" {...props}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced unordered lists
|
||||
ul: ({ children, ...props }) => (
|
||||
<ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props}>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
|
||||
// Enhanced ordered lists
|
||||
ol: ({ children, ...props }) => (
|
||||
<ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props}>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
|
||||
// Enhanced blockquotes
|
||||
blockquote: ({ children, ...props }) => (
|
||||
<blockquote
|
||||
className="border-primary/20 text-muted-foreground mt-6 border-l-4 pl-4 italic"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// Enhanced code blocks with syntax highlighting and copy button
|
||||
code: ({ inline, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const codeContent = String(children).replace(/\n$/, '')
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<div className="group bg-muted relative my-6 overflow-hidden rounded-lg border shadow-sm">
|
||||
<div className="bg-muted text-muted-foreground flex items-center justify-between px-4 py-1.5 text-sm font-medium">
|
||||
<div>{match[1].toUpperCase()}</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(codeContent)
|
||||
toast.success('代码已复制')
|
||||
}}
|
||||
className="bg-background/80 hover:bg-background flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={codeStyle}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="!bg-muted !m-0 !p-0"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
background: 'transparent',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Inline code styling
|
||||
return (
|
||||
<code
|
||||
className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced tables
|
||||
table: ({ children, ...props }) => (
|
||||
<div className="my-6 w-full overflow-y-auto">
|
||||
<table className="w-full border-collapse text-sm" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Table headers
|
||||
th: ({ children, ...props }) => (
|
||||
<th
|
||||
className="border-muted-foreground/20 border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
|
||||
// Table cells
|
||||
td: ({ children, ...props }) => (
|
||||
<td
|
||||
className="border-muted-foreground/20 border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
|
||||
// Horizontal rule
|
||||
hr: ({ ...props }) => (
|
||||
<hr className="border-muted-foreground/20 my-8" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{selectedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{showTranscribe && (
|
||||
<div className={'ml-2 w-2/4'}>
|
||||
<TranscriptViewer />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="w-[300px] flex-col justify-items-center">
|
||||
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<ArrowRight className="text-primary h-8 w-8" />
|
||||
</div>
|
||||
<p className="mb-2 text-neutral-600">输入视频链接并点击"生成笔记"按钮</p>
|
||||
<p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube等视频网站</p>
|
||||
</div>
|
||||
{viewMode === 'map' ? (
|
||||
<div className="flex w-full flex-1 overflow-hidden bg-white">
|
||||
<div className={'w-full'}>
|
||||
<MarkmapEditor
|
||||
value={selectedContent}
|
||||
onChange={() => {}}
|
||||
height="100%" // 根据需求可以设定百分比或固定高度
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 overflow-hidden bg-white py-2">
|
||||
{selectedContent && selectedContent !== 'loading' && selectedContent !== 'empty' ? (
|
||||
<>
|
||||
<ScrollArea className="w-full">
|
||||
<div className={'markdown-body w-full px-2'}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[gfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
// Headings with improved styling and anchor links
|
||||
h1: ({ children, ...props }) => (
|
||||
<h1
|
||||
className="text-primary my-6 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, ...props }) => (
|
||||
<h2
|
||||
className="text-primary mt-10 mb-4 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children, ...props }) => (
|
||||
<h3
|
||||
className="text-primary mt-8 mb-4 scroll-m-20 text-xl font-semibold tracking-tight"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children, ...props }) => (
|
||||
<h4
|
||||
className="text-primary mt-6 mb-2 scroll-m-20 text-lg font-semibold tracking-tight"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
|
||||
// Paragraphs with better line height
|
||||
p: ({ children, ...props }) => (
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6" {...props}>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
|
||||
// Enhanced links with special handling for "原片" links
|
||||
a: ({ href, children, ...props }) => {
|
||||
const isOriginLink =
|
||||
typeof children[0] === 'string' &&
|
||||
(children[0] as string).startsWith('原片 @')
|
||||
|
||||
if (isOriginLink) {
|
||||
const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/)
|
||||
const timeText = timeMatch ? timeMatch[1] : '原片'
|
||||
|
||||
return (
|
||||
<span className="origin-link my-2 inline-flex">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
|
||||
{...props}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
<span>原片({timeText})</span>
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Default link styling with external indicator
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{href?.startsWith('http') && (
|
||||
<ExternalLink className="ml-0.5 inline-block h-3 w-3" />
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced image with zoom capability
|
||||
img: ({ node, ...props }) => (
|
||||
<div className="my-8 flex justify-center">
|
||||
<Zoom>
|
||||
<img
|
||||
{...props}
|
||||
className="max-w-full cursor-zoom-in rounded-lg object-cover shadow-md transition-all hover:shadow-lg"
|
||||
style={{ maxHeight: '500px' }}
|
||||
/>
|
||||
</Zoom>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Better strong/bold text
|
||||
strong: ({ children, ...props }) => (
|
||||
<strong className="text-primary font-bold" {...props}>
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
|
||||
// Enhanced list items with support for "fake headings"
|
||||
li: ({ children, ...props }) => {
|
||||
const rawText = String(children)
|
||||
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
|
||||
|
||||
if (isFakeHeading) {
|
||||
return (
|
||||
<div className="text-primary my-4 text-lg font-bold">{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="my-1" {...props}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced unordered lists
|
||||
ul: ({ children, ...props }) => (
|
||||
<ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props}>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
|
||||
// Enhanced ordered lists
|
||||
ol: ({ children, ...props }) => (
|
||||
<ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props}>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
|
||||
// Enhanced blockquotes
|
||||
blockquote: ({ children, ...props }) => (
|
||||
<blockquote
|
||||
className="border-primary/20 text-muted-foreground mt-6 border-l-4 pl-4 italic"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// Enhanced code blocks with syntax highlighting and copy button
|
||||
code: ({ inline, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const codeContent = String(children).replace(/\n$/, '')
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<div className="group bg-muted relative my-6 overflow-hidden rounded-lg border shadow-sm">
|
||||
<div className="bg-muted text-muted-foreground flex items-center justify-between px-4 py-1.5 text-sm font-medium">
|
||||
<div>{match[1].toUpperCase()}</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(codeContent)
|
||||
toast.success('代码已复制')
|
||||
}}
|
||||
className="bg-background/80 hover:bg-background flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={codeStyle}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="!bg-muted !m-0 !p-0"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
background: 'transparent',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Inline code styling
|
||||
return (
|
||||
<code
|
||||
className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
|
||||
// Enhanced tables
|
||||
table: ({ children, ...props }) => (
|
||||
<div className="my-6 w-full overflow-y-auto">
|
||||
<table className="w-full border-collapse text-sm" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Table headers
|
||||
th: ({ children, ...props }) => (
|
||||
<th
|
||||
className="border-muted-foreground/20 border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
|
||||
// Table cells
|
||||
td: ({ children, ...props }) => (
|
||||
<td
|
||||
className="border-muted-foreground/20 border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
|
||||
// Horizontal rule
|
||||
hr: ({ ...props }) => (
|
||||
<hr className="border-muted-foreground/20 my-8" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{selectedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{showTranscribe && (
|
||||
<div className={'ml-2 w-2/4'}>
|
||||
<TranscriptViewer />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="w-[300px] flex-col justify-items-center">
|
||||
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<ArrowRight className="text-primary h-8 w-8" />
|
||||
</div>
|
||||
<p className="mb-2 text-neutral-600">输入视频链接并点击"生成笔记"按钮</p>
|
||||
<p className="text-xs text-neutral-500">支持哔哩哔哩、YouTube等视频网站</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { transformer } from '@/lib/markmap.ts'
|
||||
import { Toolbar, ToolbarButton } from 'markmap-toolbar'
|
||||
import 'markmap-toolbar/dist/style.css'
|
||||
|
||||
export interface MarkmapEditorProps {
|
||||
/** 要渲染的 Markdown 文本 */
|
||||
value: string
|
||||
/** 内容变化时的回调 */
|
||||
onChange: (value: string) => void
|
||||
/** Toolbar 上要展示的 item id 列表,默认使用 Toolbar.defaultItems */
|
||||
toolbarItems?: string[]
|
||||
/** 自定义按钮列表,会依次注册 */
|
||||
customButtons?: ToolbarButton[]
|
||||
/** 容器 SVG 的高度,默认为 600px */
|
||||
height?: string
|
||||
}
|
||||
|
||||
export default function MarkmapEditor({
|
||||
value,
|
||||
onChange,
|
||||
toolbarItems,
|
||||
customButtons = [],
|
||||
height = '600px',
|
||||
}: MarkmapEditorProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null)
|
||||
const mmRef = useRef<Markmap>()
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 用于跟踪是否处于全屏状态
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
// 监听全屏状态变化
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement)
|
||||
}
|
||||
document.addEventListener('fullscreenchange', handler)
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 进入全屏
|
||||
const enterFullscreen = () => {
|
||||
const el = svgRef.current?.parentElement
|
||||
if (el && el.requestFullscreen) {
|
||||
el.requestFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
// 退出全屏
|
||||
const exitFullscreen = () => {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 Markmap 实例 + Toolbar
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || mmRef.current) return
|
||||
const mm = Markmap.create(svgRef.current)
|
||||
mmRef.current = mm
|
||||
|
||||
if (toolbarRef.current) {
|
||||
toolbarRef.current.innerHTML = ''
|
||||
const toolbar = new Toolbar()
|
||||
toolbar.attach(mm)
|
||||
customButtons.forEach(btn => toolbar.register(btn))
|
||||
toolbar.setItems(toolbarItems ?? Toolbar.defaultItems)
|
||||
toolbarRef.current.appendChild(toolbar.render())
|
||||
}
|
||||
}, [customButtons, toolbarItems])
|
||||
|
||||
// 当 value 变化时,重新渲染数据
|
||||
useEffect(() => {
|
||||
const mm = mmRef.current
|
||||
if (!mm) return
|
||||
const { root } = transformer.transform(value)
|
||||
mm.setData(root).then(() => mm.fit())
|
||||
}, [value])
|
||||
|
||||
// 文本输入变化回调(如果你自行添加 textarea 编辑区)
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChange(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col bg-white">
|
||||
{/* 全屏/退出全屏 按钮 */}
|
||||
<div className="absolute top-2 right-2 z-20 flex space-x-2">
|
||||
{isFullscreen ? (
|
||||
<button
|
||||
onClick={exitFullscreen}
|
||||
className="rounded p-1 hover:bg-gray-200"
|
||||
title="退出全屏"
|
||||
>
|
||||
🗗
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={enterFullscreen} className="rounded p-1 hover:bg-gray-200" title="全屏">
|
||||
🗖
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 如果需要编辑区,就自己加一个 <textarea> 并把 handleChange 绑上 */}
|
||||
{/* <textarea value={value} onChange={handleChange} className="mb-2 p-2 border rounded" /> */}
|
||||
|
||||
{/* 思维导图区 */}
|
||||
<svg ref={svgRef} className="w-full flex-1" style={{ height, overflow: 'auto' }} />
|
||||
|
||||
{/* 如果你还想保留 markmap-toolbar */}
|
||||
{/* <div ref={toolbarRef} className="absolute right-2 bottom-2 z-10" /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -150,14 +150,33 @@ const NoteForm = () => {
|
||||
return
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const currentTask = getCurrentTask()
|
||||
const { formData } = currentTask || {}
|
||||
if (!currentTask) return
|
||||
const { formData } = currentTask
|
||||
|
||||
console.log('currentTask.formData.platform:', formData.platform)
|
||||
|
||||
form.reset({
|
||||
...formData,
|
||||
extras: formData?.extras || '',
|
||||
platform: formData.platform || 'bilibili',
|
||||
video_url: formData.video_url || '',
|
||||
model_name: formData.model_name || modelList[0]?.model_name || '',
|
||||
style: formData.style || 'minimal',
|
||||
quality: formData.quality || 'medium',
|
||||
extras: formData.extras || '',
|
||||
screenshot: formData.screenshot ?? false,
|
||||
link: formData.link ?? false,
|
||||
video_understanding: formData.video_understanding ?? false,
|
||||
video_interval: formData.video_interval ?? 4,
|
||||
grid_size: formData.grid_size ?? [3, 3],
|
||||
format: formData.format ?? [],
|
||||
})
|
||||
}, [currentTaskId])
|
||||
}, [
|
||||
// 当下面任意一个变了,就重新 reset
|
||||
currentTaskId,
|
||||
// modelList 用来兜底 model_name
|
||||
modelList.length,
|
||||
// 还要加上 formData 的各字段,或者直接 currentTask
|
||||
currentTask?.formData,
|
||||
])
|
||||
|
||||
/* ---- 帮助函数 ---- */
|
||||
const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status)
|
||||
@@ -175,6 +194,7 @@ const NoteForm = () => {
|
||||
}
|
||||
|
||||
const onSubmit = async (values: NoteFormValues) => {
|
||||
console.log('Not even go here')
|
||||
const payload: NoteFormValues = {
|
||||
...values,
|
||||
provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id,
|
||||
@@ -189,6 +209,10 @@ const NoteForm = () => {
|
||||
const { data } = await generateNote(payload)
|
||||
addPendingTask(data.task_id, values.platform, payload)
|
||||
}
|
||||
const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
|
||||
console.warn('表单校验失败:', errors)
|
||||
message.error('请完善所有必填项后再提交')
|
||||
}
|
||||
const handleCreateNew = () => {
|
||||
// 🔁 这里清空当前任务状态
|
||||
// 比如调用 resetCurrentTask() 或者 navigate 到一个新页面
|
||||
@@ -222,7 +246,7 @@ const NoteForm = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<form onSubmit={form.handleSubmit(onSubmit, onInvalid)} className="space-y-4">
|
||||
{/* 顶部按钮 */}
|
||||
<FormButton></FormButton>
|
||||
|
||||
|
||||
@@ -3,5 +3,25 @@ const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
function handleErrorResponse(response: any) {
|
||||
if (!response) return '请求失败,请检查网络连接'
|
||||
if (typeof response.code !== 'number') return '系统异常'
|
||||
|
||||
// 错误码判断
|
||||
switch (response.code) {
|
||||
case 1001:
|
||||
return response.msg || '下载失败,请检查视频链接'
|
||||
case 1002:
|
||||
return response.msg || '转写失败,请稍后重试'
|
||||
case 1003:
|
||||
return response.msg || '总结失败,可能是模型服务异常'
|
||||
case 2001:
|
||||
case 2002:
|
||||
return Array.isArray(response.data)
|
||||
? response.data.map(e => `${e.field}: ${e.error}`).join('\n')
|
||||
: response.msg || '参数错误'
|
||||
default:
|
||||
return response.msg || '系统异常'
|
||||
}
|
||||
}
|
||||
export default request
|
||||
|
||||
Reference in New Issue
Block a user