diff --git a/web/app/components/base/markdown-blocks/music.tsx b/web/app/components/base/markdown-blocks/music.tsx new file mode 100644 index 0000000000..aedddf905c --- /dev/null +++ b/web/app/components/base/markdown-blocks/music.tsx @@ -0,0 +1,36 @@ +import abcjs from 'abcjs' +import { useEffect, useRef } from 'react' +import 'abcjs/abcjs-audio.css' + +const MarkdownMusic = ({ children }: { children: React.ReactNode }) => { + const containerRef = useRef(null) + const controlsRef = useRef(null) + + useEffect(() => { + if (containerRef.current && controlsRef.current) { + if (typeof children === 'string') { + const visualObjs = abcjs.renderAbc(containerRef.current, children) + const synthControl = new abcjs.synth.SynthController() + synthControl.load(controlsRef.current, {}, { displayPlay: true }) + const synth = new abcjs.synth.CreateSynth() + const visualObj = visualObjs[0] + synth.init({ visualObj }).then(() => { + synthControl.setTune(visualObj, false) + }) + containerRef.current.style.overflow = 'auto' + } + } + }, [children]) + + return ( +
+
+
+
+ ) +} +MarkdownMusic.displayName = 'MarkdownMusic' + +export default MarkdownMusic diff --git a/web/app/components/base/markdown.tsx b/web/app/components/base/markdown.tsx index d50c397177..52b880affa 100644 --- a/web/app/components/base/markdown.tsx +++ b/web/app/components/base/markdown.tsx @@ -23,6 +23,7 @@ import VideoGallery from '@/app/components/base/video-gallery' import AudioGallery from '@/app/components/base/audio-gallery' import MarkdownButton from '@/app/components/base/markdown-blocks/button' import MarkdownForm from '@/app/components/base/markdown-blocks/form' +import MarkdownMusic from '@/app/components/base/markdown-blocks/music' import ThinkBlock from '@/app/components/base/markdown-blocks/think-block' import { Theme } from '@/types/app' import useTheme from '@/hooks/use-theme' @@ -51,6 +52,7 @@ const capitalizationLanguageNameMap: Record = { json: 'JSON', latex: 'Latex', svg: 'SVG', + abc: 'ABC', } const getCorrectCapitalizationLanguageName = (language: string) => { if (!language) @@ -137,45 +139,54 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }: any) => const renderCodeContent = useMemo(() => { const content = String(children).replace(/\n$/, '') - if (language === 'mermaid' && isSVG) { - return - } - else if (language === 'echarts') { - return ( -
+ switch (language) { + case 'mermaid': + if (isSVG) + return + break + case 'echarts': + return ( +
+ + + +
+ ) + case 'svg': + if (isSVG) { + return ( + + + + ) + } + break + case 'abc': + return ( - + -
- ) + ) + default: + return ( + + {content} + + ) } - else if (language === 'svg' && isSVG) { - return ( - - - - ) - } - else { - return ( - - {content} - - ) - } - }, [language, match, props, children, chartData, isSVG]) + }, [children, language, isSVG, chartData, props, theme, match]) if (inline || !match) return {children} diff --git a/web/app/styles/markdown.scss b/web/app/styles/markdown.scss index f1f2a7d670..bd9c7343f3 100644 --- a/web/app/styles/markdown.scss +++ b/web/app/styles/markdown.scss @@ -1039,3 +1039,6 @@ .markdown-body .react-syntax-highlighter-line-number { color: var(--color-text-quaternary); } +.markdown-body .abcjs-inline-audio .abcjs-btn { + display: flex !important; +} diff --git a/web/package.json b/web/package.json index b63617f47b..0dd0b72bc2 100644 --- a/web/package.json +++ b/web/package.json @@ -57,6 +57,7 @@ "@tanstack/react-form": "^1.3.3", "@tanstack/react-query": "^5.60.5", "@tanstack/react-query-devtools": "^5.60.5", + "abcjs": "^6.4.4", "ahooks": "^3.8.4", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 28822be807..bf39194eaa 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -103,6 +103,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.60.5 version: 5.72.2(@tanstack/react-query@5.72.2(react@19.0.0))(react@19.0.0) + abcjs: + specifier: ^6.4.4 + version: 6.4.4 ahooks: specifier: ^3.8.4 version: 3.8.4(react@19.0.0) @@ -3416,6 +3419,9 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abcjs@6.4.4: + resolution: {integrity: sha512-dT3Z2vb8yihbiPMzSoup0JOcvO2je4qpFNlTD+kS5VBelE3AASAs18dS5qeMWkZeqCz7kI/hz62B2lpMDugWLA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -12127,6 +12133,8 @@ snapshots: abbrev@1.1.1: optional: true + abcjs@6.4.4: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1