Browse Source

Message rendering (#6868)

Co-authored-by: luowei <glpat-EjySCyNjWiLqAED-YmwM>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Charlie.Wei 7 months ago
parent
commit
3230f4a0ec

+ 119 - 0
web/app/components/base/audio-gallery/AudioPlayer.module.css

@@ -0,0 +1,119 @@
+.audioPlayer {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background-color: #ffffff;
+  border-radius: 10px;
+  padding: 8px;
+  min-width: 240px;
+  max-width: 420px;
+  max-height: 40px;
+  backdrop-filter: blur(5px);
+  border: 1px solid rgba(16, 24, 40, 0.08);
+  box-shadow: 0 1px 2px rgba(9, 9, 11, 0.05);
+  gap: 8px;
+}
+
+.playButton {
+  display: inline-flex;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background-color: #296DFF;
+  color: white;
+  border: none;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  transition: background-color 0.1s;
+  flex-shrink: 0;
+}
+
+.playButton:hover {
+  background-color: #3367d6;
+}
+
+.playButton:disabled {
+  background-color: #bdbdbf;
+}
+
+.audioControls {
+  flex-grow: 1;
+  
+}
+
+.progressBarContainer {
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.waveform {
+  position: relative;
+  display: flex;
+  cursor: pointer;
+  height: 24px;
+  width: 100%;
+  flex-grow: 1;
+  align-items: center;
+  justify-content: center;
+}
+
+.progressBar {
+  position: absolute;
+  top: 0;
+  left: 0;
+  opacity: 0.5;
+  border-radius: 2px;
+  flex: none;
+  order: 55;
+  flex-grow: 0;
+  height: 100%;
+  background-color: rgba(66, 133, 244, 0.3);
+  pointer-events: none;
+}
+
+.timeDisplay {
+  /* position: absolute; */
+  color: #296DFF;
+  border-radius: 2px;
+  order: 0;
+  height: 100%;
+  width: 50px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* .currentTime {
+    position: absolute;
+    bottom: calc(100% + 5px);
+    transform: translateX(-50%);
+    background-color: rgba(255,255,255,.8);
+    padding: 2px 4px;
+    border-radius:10px;
+    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
+} */
+
+.duration {
+  background-color: rgba(255, 255, 255, 0.8);
+  padding: 2px 4px;
+  border-radius: 10px;
+}
+
+.source_unavailable {
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  color: #bdbdbf;
+}
+
+.playButton svg path,
+.playButton svg rect{
+   fill:currentColor;
+}

+ 320 - 0
web/app/components/base/audio-gallery/AudioPlayer.tsx

@@ -0,0 +1,320 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { t } from 'i18next'
+import styles from './AudioPlayer.module.css'
+import Toast from '@/app/components/base/toast'
+
+type AudioPlayerProps = {
+  src: string
+}
+
+const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
+  const [isPlaying, setIsPlaying] = useState(false)
+  const [currentTime, setCurrentTime] = useState(0)
+  const [duration, setDuration] = useState(0)
+  const [waveformData, setWaveformData] = useState<number[]>([])
+  const [bufferedTime, setBufferedTime] = useState(0)
+  const audioRef = useRef<HTMLAudioElement>(null)
+  const canvasRef = useRef<HTMLCanvasElement>(null)
+  const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
+  const [hoverTime, setHoverTime] = useState(0)
+  const [isAudioAvailable, setIsAudioAvailable] = useState(true)
+
+  useEffect(() => {
+    const audio = audioRef.current
+    if (!audio)
+      return
+
+    const handleError = () => {
+      setIsAudioAvailable(false)
+    }
+
+    const setAudioData = () => {
+      setDuration(audio.duration)
+    }
+
+    const setAudioTime = () => {
+      setCurrentTime(audio.currentTime)
+    }
+
+    const handleProgress = () => {
+      if (audio.buffered.length > 0)
+        setBufferedTime(audio.buffered.end(audio.buffered.length - 1))
+    }
+
+    const handleEnded = () => {
+      setIsPlaying(false)
+    }
+
+    audio.addEventListener('loadedmetadata', setAudioData)
+    audio.addEventListener('timeupdate', setAudioTime)
+    audio.addEventListener('progress', handleProgress)
+    audio.addEventListener('ended', handleEnded)
+    audio.addEventListener('error', handleError)
+
+    // Preload audio metadata
+    audio.load()
+
+    // Delayed generation of waveform data
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    const timer = setTimeout(() => generateWaveformData(src), 1000)
+
+    return () => {
+      audio.removeEventListener('loadedmetadata', setAudioData)
+      audio.removeEventListener('timeupdate', setAudioTime)
+      audio.removeEventListener('progress', handleProgress)
+      audio.removeEventListener('ended', handleEnded)
+      audio.removeEventListener('error', handleError)
+      clearTimeout(timer)
+    }
+  }, [src])
+
+  const generateWaveformData = async (audioSrc: string) => {
+    if (!window.AudioContext && !(window as any).webkitAudioContext) {
+      setIsAudioAvailable(false)
+      Toast.notify({
+        type: 'error',
+        message: 'Web Audio API is not supported in this browser',
+      })
+      return null
+    }
+
+    const url = new URL(src)
+    const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
+    if (!isHttp) {
+      setIsAudioAvailable(false)
+      return null
+    }
+
+    const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
+    const samples = 70
+
+    try {
+      const response = await fetch(audioSrc, { mode: 'cors' })
+      if (!response || !response.ok) {
+        setIsAudioAvailable(false)
+        return null
+      }
+
+      const arrayBuffer = await response.arrayBuffer()
+      const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
+      const channelData = audioBuffer.getChannelData(0)
+      const blockSize = Math.floor(channelData.length / samples)
+      const waveformData: number[] = []
+
+      for (let i = 0; i < samples; i++) {
+        let sum = 0
+        for (let j = 0; j < blockSize; j++)
+          sum += Math.abs(channelData[i * blockSize + j])
+
+        // Apply nonlinear scaling to enhance small amplitudes
+        waveformData.push((sum / blockSize) * 5)
+      }
+
+      // Normalized waveform data
+      const maxAmplitude = Math.max(...waveformData)
+      const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude)
+
+      setWaveformData(normalizedWaveform)
+      setIsAudioAvailable(true)
+    }
+    catch (error) {
+      const waveform: number[] = []
+      let prevValue = Math.random()
+
+      for (let i = 0; i < samples; i++) {
+        const targetValue = Math.random()
+        const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3
+        waveform.push(interpolatedValue)
+        prevValue = interpolatedValue
+      }
+
+      const maxAmplitude = Math.max(...waveform)
+      const randomWaveform = waveform.map(amp => amp / maxAmplitude)
+
+      setWaveformData(randomWaveform)
+      setIsAudioAvailable(true)
+    }
+    finally {
+      await audioContext.close()
+    }
+  }
+
+  const togglePlay = useCallback(() => {
+    const audio = audioRef.current
+    if (audio && isAudioAvailable) {
+      if (isPlaying) {
+        setHasStartedPlaying(false)
+        audio.pause()
+      }
+      else {
+        setHasStartedPlaying(true)
+        audio.play().catch(error => console.error('Error playing audio:', error))
+      }
+
+      setIsPlaying(!isPlaying)
+    }
+    else {
+      Toast.notify({
+        type: 'error',
+        message: 'Audio element not found',
+      })
+      setIsAudioAvailable(false)
+    }
+  }, [isAudioAvailable, isPlaying])
+
+  const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => {
+    e.preventDefault()
+
+    const getClientX = (event: React.MouseEvent | React.TouchEvent): number => {
+      if ('touches' in event)
+        return event.touches[0].clientX
+      return event.clientX
+    }
+
+    const updateProgress = (clientX: number) => {
+      const canvas = canvasRef.current
+      const audio = audioRef.current
+      if (!canvas || !audio)
+        return
+
+      const rect = canvas.getBoundingClientRect()
+      const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width
+      const newTime = percent * duration
+
+      // Removes the buffer check, allowing drag to any location
+      audio.currentTime = newTime
+      setCurrentTime(newTime)
+
+      if (!isPlaying) {
+        setIsPlaying(true)
+        audio.play().catch((error) => {
+          Toast.notify({
+            type: 'error',
+            message: `Error playing audio: ${error}`,
+          })
+          setIsPlaying(false)
+        })
+      }
+    }
+
+    updateProgress(getClientX(e))
+  }, [duration, isPlaying])
+
+  const formatTime = (time: number) => {
+    const minutes = Math.floor(time / 60)
+    const seconds = Math.floor(time % 60)
+    return `${minutes}:${seconds.toString().padStart(2, '0')}`
+  }
+
+  const drawWaveform = useCallback(() => {
+    const canvas = canvasRef.current
+    if (!canvas)
+      return
+
+    const ctx = canvas.getContext('2d')
+    if (!ctx)
+      return
+
+    const width = canvas.width
+    const height = canvas.height
+    const data = waveformData
+
+    ctx.clearRect(0, 0, width, height)
+
+    const barWidth = width / data.length
+    const playedWidth = (currentTime / duration) * width
+    const cornerRadius = 2
+
+    // Draw waveform bars
+    data.forEach((value, index) => {
+      let color
+
+      if (index * barWidth <= playedWidth)
+        color = '#296DFF'
+      else if ((index * barWidth / width) * duration <= hoverTime)
+        color = 'rgba(21,90,239,.40)'
+      else
+        color = 'rgba(21,90,239,.20)'
+
+      const barHeight = value * height
+      const rectX = index * barWidth
+      const rectY = (height - barHeight) / 2
+      const rectWidth = barWidth * 0.5
+      const rectHeight = barHeight
+
+      ctx.lineWidth = 1
+      ctx.fillStyle = color
+      if (ctx.roundRect) {
+        ctx.beginPath()
+        ctx.roundRect(rectX, rectY, rectWidth, rectHeight, cornerRadius)
+        ctx.fill()
+      }
+      else {
+        ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
+      }
+    })
+  }, [currentTime, duration, hoverTime, waveformData])
+
+  useEffect(() => {
+    drawWaveform()
+  }, [drawWaveform, bufferedTime, hasStartedPlaying])
+
+  const handleMouseMove = useCallback((e: React.MouseEvent) => {
+    const canvas = canvasRef.current
+    const audio = audioRef.current
+    if (!canvas || !audio)
+      return
+
+    const rect = canvas.getBoundingClientRect()
+    const percent = Math.min(Math.max(0, e.clientX - rect.left), rect.width) / rect.width
+    const time = percent * duration
+
+    // Check if the hovered position is within a buffered range before updating hoverTime
+    for (let i = 0; i < audio.buffered.length; i++) {
+      if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) {
+        setHoverTime(time)
+        break
+      }
+    }
+  }, [duration])
+
+  return (
+    <div className={styles.audioPlayer}>
+      <audio ref={audioRef} src={src} preload="auto"/>
+      <button className={styles.playButton} onClick={togglePlay} disabled={!isAudioAvailable}>
+        {isPlaying
+          ? (
+            <svg viewBox="0 0 24 24" width="16" height="16">
+              <rect x="7" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
+              <rect x="15" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
+            </svg>
+          )
+          : (
+            <svg viewBox="0 0 24 24" width="16" height="16">
+              <path d="M8 5v14l11-7z" fill="currentColor"/>
+            </svg>
+          )}
+      </button>
+      <div className={isAudioAvailable ? styles.audioControls : styles.audioControls_disabled} hidden={!isAudioAvailable}>
+        <div className={styles.progressBarContainer}>
+          <canvas
+            ref={canvasRef}
+            className={styles.waveform}
+            onClick={handleCanvasInteraction}
+            onMouseMove={handleMouseMove}
+            onMouseDown={handleCanvasInteraction}
+          />
+          {/* <div className={styles.currentTime} style={{ left: `${(currentTime / duration) * 81}%`, bottom: '29px' }}>
+            {formatTime(currentTime)}
+          </div> */}
+          <div className={styles.timeDisplay}>
+            <span className={styles.duration}>{formatTime(duration)}</span>
+          </div>
+        </div>
+      </div>
+      <div className={styles.source_unavailable} hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
+    </div>
+  )
+}
+
+export default AudioPlayer

+ 12 - 0
web/app/components/base/audio-gallery/index.tsx

@@ -0,0 +1,12 @@
+import React from 'react'
+import AudioPlayer from './AudioPlayer'
+
+type Props = {
+  srcs: string[]
+}
+
+const AudioGallery: React.FC<Props> = ({ srcs }) => {
+  return (<><br/>{srcs.map((src, index) => (<AudioPlayer key={`audio_${index}`} src={src}/>))}</>)
+}
+
+export default React.memo(AudioGallery)

+ 38 - 0
web/app/components/base/image-uploader/audio-preview.tsx

@@ -0,0 +1,38 @@
+import type { FC } from 'react'
+import { createPortal } from 'react-dom'
+import { RiCloseLine } from '@remixicon/react'
+
+type AudioPreviewProps = {
+  url: string
+  title: string
+  onCancel: () => void
+}
+const AudioPreview: FC<AudioPreviewProps> = ({
+  url,
+  title,
+  onCancel,
+}) => {
+  return createPortal(
+    <div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
+      <div>
+        <audio controls title={title} autoPlay={false} preload="metadata">
+          <source
+            type="audio/mpeg"
+            src={url}
+            className='max-w-full max-h-full'
+          />
+        </audio>
+      </div>
+      <div
+        className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
+        onClick={onCancel}
+      >
+        <RiCloseLine className='w-4 h-4 text-gray-500'/>
+      </div>
+    </div>
+    ,
+    document.body,
+  )
+}
+
+export default AudioPreview

+ 38 - 2
web/app/components/base/image-uploader/image-preview.tsx

@@ -1,19 +1,43 @@
 import type { FC } from 'react'
+import { useRef } from 'react'
+import { t } from 'i18next'
 import { createPortal } from 'react-dom'
-import { RiCloseLine } from '@remixicon/react'
+import { RiCloseLine, RiExternalLinkLine } from '@remixicon/react'
+import Tooltip from '@/app/components/base/tooltip'
+import { randomString } from '@/utils'
 
 type ImagePreviewProps = {
   url: string
+  title: string
   onCancel: () => void
 }
 const ImagePreview: FC<ImagePreviewProps> = ({
   url,
+  title,
   onCancel,
 }) => {
+  const selector = useRef(`copy-tooltip-${randomString(4)}`)
+
+  const openInNewTab = () => {
+    // Open in a new window, considering the case when the page is inside an iframe
+    if (url.startsWith('http')) {
+      window.open(url, '_blank')
+    }
+    else if (url.startsWith('data:image')) {
+      // Base64 image
+      const win = window.open()
+      win?.document.write(`<img src="${url}" alt="${title}" />`)
+    }
+    else {
+      console.error('Unable to open image', url)
+    }
+  }
+
   return createPortal(
     <div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
+      {/* eslint-disable-next-line @next/next/no-img-element */}
       <img
-        alt='preview image'
+        alt={title}
         src={url}
         className='max-w-full max-h-full'
       />
@@ -23,6 +47,18 @@ const ImagePreview: FC<ImagePreviewProps> = ({
       >
         <RiCloseLine className='w-4 h-4 text-white' />
       </div>
+      <Tooltip
+        selector={selector.current}
+        content={(t('common.operation.openInNewTab') ?? 'Open in new tab')}
+        className='z-10'
+      >
+        <div
+          className='absolute top-6 right-16 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
+          onClick={openInNewTab}
+        >
+          <RiExternalLinkLine className='w-4 h-4 text-white' />
+        </div>
+      </Tooltip>
     </div>,
     document.body,
   )

+ 38 - 0
web/app/components/base/image-uploader/video-preview.tsx

@@ -0,0 +1,38 @@
+import type { FC } from 'react'
+import { createPortal } from 'react-dom'
+import { RiCloseLine } from '@remixicon/react'
+
+type VideoPreviewProps = {
+  url: string
+  title: string
+  onCancel: () => void
+}
+const VideoPreview: FC<VideoPreviewProps> = ({
+  url,
+  title,
+  onCancel,
+}) => {
+  return createPortal(
+    <div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
+      <div>
+        <video controls title={title} autoPlay={false} preload="metadata">
+          <source
+            type="video/mp4"
+            src={url}
+            className='max-w-full max-h-full'
+          />
+        </video>
+      </div>
+      <div
+        className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
+        onClick={onCancel}
+      >
+        <RiCloseLine className='w-4 h-4 text-gray-500'/>
+      </div>
+    </div>
+    ,
+    document.body,
+  )
+}
+
+export default VideoPreview

+ 78 - 27
web/app/components/base/markdown.tsx

@@ -4,6 +4,7 @@ import 'katex/dist/katex.min.css'
 import RemarkMath from 'remark-math'
 import RemarkBreaks from 'remark-breaks'
 import RehypeKatex from 'rehype-katex'
+import RehypeRaw from 'rehype-raw'
 import RemarkGfm from 'remark-gfm'
 import SyntaxHighlighter from 'react-syntax-highlighter'
 import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
@@ -15,6 +16,9 @@ import CopyBtn from '@/app/components/base/copy-btn'
 import SVGBtn from '@/app/components/base/svg'
 import Flowchart from '@/app/components/base/mermaid'
 import ImageGallery from '@/app/components/base/image-gallery'
+import { useChatContext } from '@/app/components/base/chat/chat/context'
+import VideoGallery from '@/app/components/base/video-gallery'
+import AudioGallery from '@/app/components/base/audio-gallery'
 
 // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
 const capitalizationLanguageNameMap: Record<string, string> = {
@@ -33,6 +37,10 @@ const capitalizationLanguageNameMap: Record<string, string> = {
   markdown: 'MarkDown',
   makefile: 'MakeFile',
   echarts: 'ECharts',
+  shell: 'Shell',
+  powershell: 'PowerShell',
+  json: 'JSON',
+  latex: 'Latex',
 }
 const getCorrectCapitalizationLanguageName = (language: string) => {
   if (!language)
@@ -65,6 +73,7 @@ export function PreCode(props: { children: any }) {
   )
 }
 
+// eslint-disable-next-line unused-imports/no-unused-vars
 const useLazyLoad = (ref: RefObject<Element>): boolean => {
   const [isIntersecting, setIntersecting] = useState<boolean>(false)
 
@@ -126,12 +135,7 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
           >
             <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
             <div style={{ display: 'flex' }}>
-              {language === 'mermaid'
-                && <SVGBtn
-                  isSVG={isSVG}
-                  setIsSVG={setIsSVG}
-                />
-              }
+              {language === 'mermaid' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
               <CopyBtn
                 className='mr-1'
                 value={String(children).replace(/\n$/, '')}
@@ -172,36 +176,83 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
 
 CodeBlock.displayName = 'CodeBlock'
 
+const VideoBlock: CodeComponent = memo(({ node }) => {
+  const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src)
+  if (srcs.length === 0)
+    return null
+  return <VideoGallery key={srcs.join()} srcs={srcs} />
+})
+VideoBlock.displayName = 'VideoBlock'
+
+const AudioBlock: CodeComponent = memo(({ node }) => {
+  const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src)
+  if (srcs.length === 0)
+    return null
+  return <AudioGallery key={srcs.join()} srcs={srcs} />
+})
+AudioBlock.displayName = 'AudioBlock'
+
+const Paragraph = (paragraph: any) => {
+  const { node }: any = paragraph
+  const children_node = node.children
+  if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
+    return (
+      <>
+        <ImageGallery srcs={[children_node[0].properties.src]} />
+        <div>{paragraph.children.slice(1)}</div>
+      </>
+    )
+  }
+  return <div>{paragraph.children}</div>
+}
+
+const Img = ({ src }: any) => {
+  return (<ImageGallery srcs={[src]} />)
+}
+
+const Link = ({ node, ...props }: any) => {
+  if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    const { onSend } = useChatContext()
+    const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1])
+
+    return <abbr className="underline decoration-dashed !decoration-primary-700 cursor-pointer" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value}>{node.children[0]?.value}</abbr>
+  }
+  else {
+    return <a {...props} target="_blank" className="underline decoration-dashed !decoration-primary-700 cursor-pointer">{node.children[0] ? node.children[0]?.value : 'Download'}</a>
+  }
+}
+
 export function Markdown(props: { content: string; className?: string }) {
   const latexContent = preprocessLaTeX(props.content)
   return (
     <div className={cn(props.className, 'markdown-body')}>
       <ReactMarkdown
-        remarkPlugins={[[RemarkMath, { singleDollarTextMath: false }], RemarkGfm, RemarkBreaks]}
+        remarkPlugins={[[RemarkGfm, RemarkMath, { singleDollarTextMath: false }], RemarkBreaks]}
         rehypePlugins={[
-          RehypeKatex as any,
+          RehypeKatex,
+          RehypeRaw as any,
+          // The Rehype plug-in is used to remove the ref attribute of an element
+          () => {
+            return (tree) => {
+              const iterate = (node: any) => {
+                if (node.type === 'element' && !node.properties?.src && node.properties?.ref && node.properties.ref.startsWith('{') && node.properties.ref.endsWith('}'))
+                  delete node.properties.ref
+
+                if (node.children)
+                  node.children.forEach(iterate)
+              }
+              tree.children.forEach(iterate)
+            }
+          },
         ]}
         components={{
           code: CodeBlock,
-          img({ src }) {
-            return (
-              <ImageGallery srcs={[src || '']} />
-            )
-          },
-          p: (paragraph) => {
-            const { node }: any = paragraph
-            if (node.children[0].tagName === 'img') {
-              const image = node.children[0]
-
-              return (
-                <>
-                  <ImageGallery srcs={[image.properties.src]} />
-                  <p>{paragraph.children.slice(1)}</p>
-                </>
-              )
-            }
-            return <p>{paragraph.children}</p>
-          },
+          img: Img,
+          video: VideoBlock,
+          audio: AudioBlock,
+          a: Link,
+          p: Paragraph,
         }}
         linkTarget='_blank'
       >

+ 188 - 0
web/app/components/base/video-gallery/VideoPlayer.module.css

@@ -0,0 +1,188 @@
+.videoPlayer {
+  position: relative;
+  width: 100%;
+  max-width: 800px;
+  margin: 0 auto;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.video {
+  width: 100%;
+  display: block;
+}
+
+.controls {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  transition: opacity 0.3s ease;
+}
+
+.controls.hidden {
+  opacity: 0;
+}
+
+.controls.visible {
+  opacity: 1;
+}
+
+.overlay {
+  background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+}
+
+.progressBarContainer {
+  width: 100%;
+  margin-bottom: 10px;
+}
+
+.controlsContent {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.leftControls, .rightControls {
+  display: flex;
+  align-items: center;
+}
+
+.playPauseButton, .muteButton, .fullscreenButton {
+  background: none;
+  border: none;
+  color: white;
+  cursor: pointer;
+  padding: 4px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.playPauseButton:hover, .muteButton:hover, .fullscreenButton:hover {
+  background-color: rgba(255, 255, 255, 0.1);
+  border-radius: 50%;
+}
+
+.time {
+  color: white;
+  font-size: 14px;
+  margin-left: 8px;
+}
+
+.volumeControl {
+  display: flex;
+  align-items: center;
+  margin-right: 16px;
+}
+
+.volumeSlider {
+  width: 60px;
+  height: 4px;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 2px;
+  cursor: pointer;
+  margin-left: 12px;
+  position: relative;
+}
+
+.volumeLevel {
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  background: #ffffff;
+  border-radius: 2px;
+}
+
+.progressBar {
+  position: relative;
+  width: 100%;
+  height: 4px;
+  background: rgba(255, 255, 255, 0.3);
+  cursor: pointer;
+  border-radius: 2px;
+  overflow: visible;
+  transition: height 0.2s ease;
+}
+
+.progressBar:hover {
+  height: 6px;
+}
+
+.progress {
+  height: 100%;
+  background: #ffffff;
+  transition: width 0.1s ease-in-out;
+}
+
+.hoverTimeIndicator {
+  position: absolute;
+  bottom: 100%;
+  transform: translateX(-50%);
+  background-color: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  pointer-events: none;
+  white-space: nowrap;
+  margin-bottom: 8px;
+}
+
+.hoverTimeIndicator::after {
+  content: '';
+  position: absolute;
+  top: 100%;
+  left: 50%;
+  margin-left: -4px;
+  border-width: 4px;
+  border-style: solid;
+  border-color: rgba(0, 0, 0, 0.7) transparent transparent transparent;
+}
+
+.controls.smallSize .controlsContent {
+  justify-content: space-between;
+}
+
+.controls.smallSize .leftControls,
+.controls.smallSize .rightControls {
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+}
+
+.controls.smallSize .rightControls {
+  justify-content: flex-end;
+}
+
+.controls.smallSize .progressBarContainer {
+  margin-bottom: 4px;
+}
+
+.controls.smallSize .playPauseButton,
+.controls.smallSize .muteButton,
+.controls.smallSize .fullscreenButton {
+  padding: 2px;
+  margin-right: 4px;
+}
+
+.controls.smallSize .playPauseButton svg,
+.controls.smallSize .muteButton svg,
+.controls.smallSize .fullscreenButton svg {
+  width: 16px;
+  height: 16px;
+}
+
+.controls.smallSize .muteButton {
+  order: -1;
+}

+ 278 - 0
web/app/components/base/video-gallery/VideoPlayer.tsx

@@ -0,0 +1,278 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import styles from './VideoPlayer.module.css'
+
+type VideoPlayerProps = {
+  src: string
+}
+
+const PlayIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M8 5V19L19 12L8 5Z" fill="currentColor"/>
+  </svg>
+)
+
+const PauseIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M6 19H10V5H6V19ZM14 5V19H18V5H14Z" fill="currentColor"/>
+  </svg>
+)
+
+const MuteIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M3 9V15H7L12 20V4L7 9H3ZM16.5 12C16.5 10.23 15.48 8.71 14 7.97V16.02C15.48 15.29 16.5 13.77 16.5 12ZM14 3.23V5.29C16.89 6.15 19 8.83 19 12C19 15.17 16.89 17.85 14 18.71V20.77C18.01 19.86 21 16.28 21 12C21 7.72 18.01 4.14 14 3.23Z" fill="currentColor"/>
+  </svg>
+)
+
+const UnmuteIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M4.34 2.93L2.93 4.34L7.29 8.7L7 9H3V15H7L12 20V13.41L16.18 17.59C15.69 17.96 15.16 18.27 14.58 18.5V20.58C15.94 20.22 17.15 19.56 18.13 18.67L19.66 20.2L21.07 18.79L4.34 2.93ZM10 15.17L7.83 13H5V11H7.83L10 8.83V15.17ZM19 12C19 12.82 18.85 13.61 18.59 14.34L20.12 15.87C20.68 14.7 21 13.39 21 12C21 7.72 18.01 4.14 14 3.23V5.29C16.89 6.15 19 8.83 19 12ZM12 4L10.12 5.88L12 7.76V4ZM16.5 12C16.5 10.23 15.48 8.71 14 7.97V10.18L16.45 12.63C16.48 12.43 16.5 12.22 16.5 12Z" fill="currentColor"/>
+  </svg>
+)
+
+const FullscreenIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M7 14H5V19H10V17H7V14ZM5 10H7V7H10V5H5V10ZM17 17H14V19H19V14H17V17ZM14 5V7H17V10H19V5H14Z" fill="currentColor"/>
+  </svg>
+)
+
+const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
+  const [isPlaying, setIsPlaying] = useState(false)
+  const [currentTime, setCurrentTime] = useState(0)
+  const [duration, setDuration] = useState(0)
+  const [isMuted, setIsMuted] = useState(false)
+  const [volume, setVolume] = useState(1)
+  const [isDragging, setIsDragging] = useState(false)
+  const [isControlsVisible, setIsControlsVisible] = useState(true)
+  const [hoverTime, setHoverTime] = useState<number | null>(null)
+  const videoRef = useRef<HTMLVideoElement>(null)
+  const progressRef = useRef<HTMLDivElement>(null)
+  const volumeRef = useRef<HTMLDivElement>(null)
+  const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
+  const [isSmallSize, setIsSmallSize] = useState(false)
+  const containerRef = useRef<HTMLDivElement>(null)
+
+  useEffect(() => {
+    const video = videoRef.current
+    if (!video)
+      return
+
+    const setVideoData = () => {
+      setDuration(video.duration)
+      setVolume(video.volume)
+    }
+
+    const setVideoTime = () => {
+      setCurrentTime(video.currentTime)
+    }
+
+    const handleEnded = () => {
+      setIsPlaying(false)
+    }
+
+    video.addEventListener('loadedmetadata', setVideoData)
+    video.addEventListener('timeupdate', setVideoTime)
+    video.addEventListener('ended', handleEnded)
+
+    return () => {
+      video.removeEventListener('loadedmetadata', setVideoData)
+      video.removeEventListener('timeupdate', setVideoTime)
+      video.removeEventListener('ended', handleEnded)
+    }
+  }, [src])
+
+  useEffect(() => {
+    return () => {
+      if (controlsTimeoutRef.current)
+        clearTimeout(controlsTimeoutRef.current)
+    }
+  }, [])
+
+  const showControls = useCallback(() => {
+    setIsControlsVisible(true)
+    if (controlsTimeoutRef.current)
+      clearTimeout(controlsTimeoutRef.current)
+
+    controlsTimeoutRef.current = setTimeout(() => setIsControlsVisible(false), 3000)
+  }, [])
+
+  const togglePlayPause = useCallback(() => {
+    const video = videoRef.current
+    if (video) {
+      if (isPlaying)
+        video.pause()
+      else video.play().catch(error => console.error('Error playing video:', error))
+      setIsPlaying(!isPlaying)
+    }
+  }, [isPlaying])
+
+  const toggleMute = useCallback(() => {
+    const video = videoRef.current
+    if (video) {
+      const newMutedState = !video.muted
+      video.muted = newMutedState
+      setIsMuted(newMutedState)
+      setVolume(newMutedState ? 0 : (video.volume > 0 ? video.volume : 1))
+      video.volume = newMutedState ? 0 : (video.volume > 0 ? video.volume : 1)
+    }
+  }, [])
+
+  const toggleFullscreen = useCallback(() => {
+    const video = videoRef.current
+    if (video) {
+      if (document.fullscreenElement)
+        document.exitFullscreen()
+      else video.requestFullscreen()
+    }
+  }, [])
+
+  const formatTime = (time: number) => {
+    const minutes = Math.floor(time / 60)
+    const seconds = Math.floor(time % 60)
+    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+  }
+
+  const updateVideoProgress = useCallback((clientX: number) => {
+    const progressBar = progressRef.current
+    const video = videoRef.current
+    if (progressBar && video) {
+      const rect = progressBar.getBoundingClientRect()
+      const pos = (clientX - rect.left) / rect.width
+      const newTime = pos * video.duration
+      if (newTime >= 0 && newTime <= video.duration) {
+        setHoverTime(newTime)
+        if (isDragging)
+          video.currentTime = newTime
+      }
+    }
+  }, [isDragging])
+
+  const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    updateVideoProgress(e.clientX)
+  }, [updateVideoProgress])
+
+  const handleMouseLeave = useCallback(() => {
+    if (!isDragging)
+      setHoverTime(null)
+  }, [isDragging])
+
+  const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    e.preventDefault()
+    setIsDragging(true)
+    updateVideoProgress(e.clientX)
+  }, [updateVideoProgress])
+
+  useEffect(() => {
+    const handleGlobalMouseMove = (e: MouseEvent) => {
+      if (isDragging)
+        updateVideoProgress(e.clientX)
+    }
+
+    const handleGlobalMouseUp = () => {
+      setIsDragging(false)
+      setHoverTime(null)
+    }
+
+    if (isDragging) {
+      document.addEventListener('mousemove', handleGlobalMouseMove)
+      document.addEventListener('mouseup', handleGlobalMouseUp)
+    }
+
+    return () => {
+      document.removeEventListener('mousemove', handleGlobalMouseMove)
+      document.removeEventListener('mouseup', handleGlobalMouseUp)
+    }
+  }, [isDragging, updateVideoProgress])
+
+  const checkSize = useCallback(() => {
+    if (containerRef.current)
+      setIsSmallSize(containerRef.current.offsetWidth < 400)
+  }, [])
+
+  useEffect(() => {
+    checkSize()
+    window.addEventListener('resize', checkSize)
+    return () => window.removeEventListener('resize', checkSize)
+  }, [checkSize])
+
+  const handleVolumeChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    const volumeBar = volumeRef.current
+    const video = videoRef.current
+    if (volumeBar && video) {
+      const rect = volumeBar.getBoundingClientRect()
+      const newVolume = (e.clientX - rect.left) / rect.width
+      const clampedVolume = Math.max(0, Math.min(1, newVolume))
+      video.volume = clampedVolume
+      setVolume(clampedVolume)
+      setIsMuted(clampedVolume === 0)
+    }
+  }, [])
+
+  return (
+    <div ref={containerRef} className={styles.videoPlayer} onMouseMove={showControls} onMouseEnter={showControls}>
+      <video ref={videoRef} src={src} className={styles.video} />
+      <div className={`${styles.controls} ${isControlsVisible ? styles.visible : styles.hidden} ${isSmallSize ? styles.smallSize : ''}`}>
+        <div className={styles.overlay}>
+          <div className={styles.progressBarContainer}>
+            <div
+              ref={progressRef}
+              className={styles.progressBar}
+              onClick={handleMouseDown}
+              onMouseMove={handleMouseMove}
+              onMouseLeave={handleMouseLeave}
+              onMouseDown={handleMouseDown}
+            >
+              <div className={styles.progress} style={{ width: `${(currentTime / duration) * 100}%` }} />
+              {hoverTime !== null && (
+                <div
+                  className={styles.hoverTimeIndicator}
+                  style={{ left: `${(hoverTime / duration) * 100}%` }}
+                >
+                  {formatTime(hoverTime)}
+                </div>
+              )}
+            </div>
+          </div>
+          <div className={styles.controlsContent}>
+            <div className={styles.leftControls}>
+              <button className={styles.playPauseButton} onClick={togglePlayPause}>
+                {isPlaying ? <PauseIcon /> : <PlayIcon />}
+              </button>
+              {!isSmallSize && (<span className={styles.time}>{formatTime(currentTime)} / {formatTime(duration)}</span>)}
+            </div>
+            <div className={styles.rightControls}>
+              <button className={styles.muteButton} onClick={toggleMute}>
+                {isMuted ? <UnmuteIcon /> : <MuteIcon />}
+              </button>
+              {!isSmallSize && (
+                <div className={styles.volumeControl}>
+                  <div
+                    ref={volumeRef}
+                    className={styles.volumeSlider}
+                    onClick={handleVolumeChange}
+                    onMouseDown={(e) => {
+                      handleVolumeChange(e)
+                      const handleMouseMove = (e: MouseEvent) => handleVolumeChange(e as unknown as React.MouseEvent<HTMLDivElement>)
+                      const handleMouseUp = () => {
+                        document.removeEventListener('mousemove', handleMouseMove)
+                        document.removeEventListener('mouseup', handleMouseUp)
+                      }
+                      document.addEventListener('mousemove', handleMouseMove)
+                      document.addEventListener('mouseup', handleMouseUp)
+                    }}
+                  >
+                    <div className={styles.volumeLevel} style={{ width: `${volume * 100}%` }} />
+                  </div>
+                </div>
+              )}
+              <button className={styles.fullscreenButton} onClick={toggleFullscreen}>
+                <FullscreenIcon />
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default VideoPlayer

+ 12 - 0
web/app/components/base/video-gallery/index.tsx

@@ -0,0 +1,12 @@
+import React from 'react'
+import VideoPlayer from './VideoPlayer'
+
+type Props = {
+  srcs: string[]
+}
+
+const VideoGallery: React.FC<Props> = ({ srcs }) => {
+  return (<><br/>{srcs.map((src, index) => (<><br/><VideoPlayer key={`video_${index}`} src={src}/></>))}</>)
+}
+
+export default React.memo(VideoGallery)

+ 6 - 3
web/app/components/base/voice-input/index.tsx

@@ -16,11 +16,13 @@ import { audioToText } from '@/service/share'
 type VoiceInputTypes = {
   onConverted: (text: string) => void
   onCancel: () => void
+  wordTimestamps?: string
 }
 
 const VoiceInput = ({
   onCancel,
   onConverted,
+  wordTimestamps,
 }: VoiceInputTypes) => {
   const { t } = useTranslation()
   const recorder = useRef(new Recorder({
@@ -88,6 +90,7 @@ const VoiceInput = ({
     const mp3File = new File([mp3Blob], 'temp.mp3', { type: 'audio/mp3' })
     const formData = new FormData()
     formData.append('file', mp3File)
+    formData.append('word_timestamps', wordTimestamps || 'disabled')
 
     let url = ''
     let isPublic = false
@@ -112,7 +115,7 @@ const VoiceInput = ({
       onConverted('')
       onCancel()
     }
-  }, [])
+  }, [clearInterval, onCancel, onConverted, params.appId, params.token, pathname, wordTimestamps])
   const handleStartRecord = async () => {
     try {
       await recorder.current.start()
@@ -146,7 +149,7 @@ const VoiceInput = ({
       }
     }
   }
-  if (originDuration >= 120 && startRecord)
+  if (originDuration >= 600 && startRecord)
     handleStopRecorder()
 
   useEffect(() => {
@@ -204,7 +207,7 @@ const VoiceInput = ({
             </div>
           )
         }
-        <div className={`w-[45px] pl-1 text-xs font-medium ${originDuration > 110 ? 'text-[#F04438]' : 'text-gray-700'}`}>{`0${minutes.toFixed(0)}:${seconds >= 10 ? seconds : `0${seconds}`}`}</div>
+        <div className={`w-[45px] pl-1 text-xs font-medium ${originDuration > 500 ? 'text-[#F04438]' : 'text-gray-700'}`}>{`0${minutes.toFixed(0)}:${seconds >= 10 ? seconds : `0${seconds}`}`}</div>
       </div>
     </div>
   )

+ 1 - 0
web/i18n/en-US/common.ts

@@ -37,6 +37,7 @@ const translation = {
     params: 'Params',
     duplicate: 'Duplicate',
     rename: 'Rename',
+    audioSourceUnavailable: 'AudioSource is unavailable',
   },
   errorMsg: {
     fieldRequired: '{{field}} is required',

+ 1 - 0
web/i18n/zh-Hans/common.ts

@@ -37,6 +37,7 @@ const translation = {
     params: '参数设置',
     duplicate: '复制',
     rename: '重命名',
+    audioSourceUnavailable: '音源不可用',
   },
   errorMsg: {
     fieldRequired: '{{field}} 为必填项',

+ 1 - 0
web/package.json

@@ -87,6 +87,7 @@
     "reactflow": "^11.11.3",
     "recordrtc": "^5.6.2",
     "rehype-katex": "^6.0.2",
+    "rehype-raw": "^7.0.0",
     "remark-breaks": "^3.0.2",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",

+ 273 - 61
web/yarn.lock

@@ -260,6 +260,13 @@
   resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz"
   integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
 
+"@babel/parser@^7.25.4":
+  version "7.25.6"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f"
+  integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==
+  dependencies:
+    "@babel/types" "^7.25.6"
+
 "@babel/plugin-syntax-async-generators@^7.8.4":
   version "7.8.4"
   resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -406,10 +413,10 @@
     "@babel/helper-validator-identifier" "^7.24.7"
     to-fast-properties "^2.0.0"
 
-"@babel/types@^7.24.0":
-  version "7.25.2"
-  resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125"
-  integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==
+"@babel/types@^7.25.4", "@babel/types@^7.25.6":
+  version "7.25.6"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6"
+  integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==
   dependencies:
     "@babel/helper-string-parser" "^7.24.8"
     "@babel/helper-validator-identifier" "^7.24.7"
@@ -1465,7 +1472,7 @@
 
 "@sindresorhus/is@^4.0.0":
   version "4.6.0"
-  resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
   integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
 
 "@sinonjs/commons@^3.0.0":
@@ -1497,7 +1504,7 @@
 
 "@szmarczak/http-timer@^4.0.5":
   version "4.0.6"
-  resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
   integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==
   dependencies:
     defer-to-connect "^2.0.0"
@@ -1624,7 +1631,7 @@
 
 "@types/cacheable-request@^6.0.1":
   version "6.0.3"
-  resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183"
+  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183"
   integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==
   dependencies:
     "@types/http-cache-semantics" "*"
@@ -1890,9 +1897,16 @@
   dependencies:
     "@types/unist" "*"
 
+"@types/hast@^3.0.0":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
+  integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/http-cache-semantics@*":
   version "4.0.4"
-  resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
+  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
   integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
 
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
@@ -1963,7 +1977,7 @@
 
 "@types/keyv@^3.1.4":
   version "3.1.4"
-  resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
+  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
   integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==
   dependencies:
     "@types/node" "*"
@@ -1987,6 +2001,13 @@
   dependencies:
     "@types/unist" "*"
 
+"@types/mdast@^4.0.0":
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
+  integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/mdx@^2.0.0":
   version "2.0.5"
   resolved "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.5.tgz"
@@ -2080,7 +2101,7 @@
 
 "@types/responselike@^1.0.0":
   version "1.0.3"
-  resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50"
+  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50"
   integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==
   dependencies:
     "@types/node" "*"
@@ -2110,6 +2131,11 @@
   resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz"
   integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
 
+"@types/unist@^3.0.0":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20"
+  integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==
+
 "@types/uuid@^9.0.8":
   version "9.0.8"
   resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz"
@@ -2211,6 +2237,11 @@
     "@typescript-eslint/types" "5.59.9"
     eslint-visitor-keys "^3.3.0"
 
+"@ungap/structured-clone@^1.0.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
 "@vue/compiler-core@3.4.25":
   version "3.4.25"
   resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.25.tgz"
@@ -2630,7 +2661,7 @@ binary-extensions@^2.0.0:
 
 bing-translate-api@^4.0.2:
   version "4.0.2"
-  resolved "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.0.2.tgz#52807a128e883bf074b4174c5e674ffca60685e7"
+  resolved "https://registry.yarnpkg.com/bing-translate-api/-/bing-translate-api-4.0.2.tgz#52807a128e883bf074b4174c5e674ffca60685e7"
   integrity sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1Q==
   dependencies:
     got "^11.8.6"
@@ -2729,12 +2760,12 @@ busboy@1.6.0:
 
 cacheable-lookup@^5.0.3:
   version "5.0.4"
-  resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
+  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
   integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
 
 cacheable-request@^7.0.2:
   version "7.0.4"
-  resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817"
   integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==
   dependencies:
     clone-response "^1.0.2"
@@ -2970,7 +3001,7 @@ cliui@^8.0.1:
 
 clone-response@^1.0.2:
   version "1.0.3"
-  resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3"
   integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==
   dependencies:
     mimic-response "^1.0.0"
@@ -3548,7 +3579,7 @@ decode-named-character-reference@^1.0.0:
 
 decompress-response@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
   integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
   dependencies:
     mimic-response "^3.1.0"
@@ -3612,7 +3643,7 @@ default-browser@^4.0.0:
 
 defer-to-connect@^2.0.0:
   version "2.0.1"
-  resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
   integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
 
 define-data-property@^1.0.1, define-data-property@^1.1.1:
@@ -3665,6 +3696,13 @@ detect-newline@^3.0.0:
   resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
+devlop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
+  integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+  dependencies:
+    dequal "^2.0.0"
+
 didyoumean@^1.2.2:
   version "1.2.2"
   resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
@@ -3816,7 +3854,7 @@ emoji-regex@^9.2.2:
 
 end-of-stream@^1.1.0:
   version "1.4.4"
-  resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
   dependencies:
     once "^1.4.0"
@@ -4660,7 +4698,7 @@ get-package-type@^0.1.0:
 
 get-stream@^5.1.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
   integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
   dependencies:
     pump "^3.0.0"
@@ -4785,7 +4823,7 @@ gopd@^1.0.1:
 
 got@^11.8.6:
   version "11.8.6"
-  resolved "https://registry.npmjs.org/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
   integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==
   dependencies:
     "@sindresorhus/is" "^4.0.0"
@@ -4905,6 +4943,20 @@ hast-util-from-parse5@^7.0.0:
     vfile-location "^4.0.0"
     web-namespaces "^2.0.0"
 
+hast-util-from-parse5@^8.0.0:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651"
+  integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    "@types/unist" "^3.0.0"
+    devlop "^1.0.0"
+    hastscript "^8.0.0"
+    property-information "^6.0.0"
+    vfile "^6.0.0"
+    vfile-location "^5.0.0"
+    web-namespaces "^2.0.0"
+
 hast-util-is-element@^2.0.0:
   version "2.1.3"
   resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz"
@@ -4925,6 +4977,32 @@ hast-util-parse-selector@^3.0.0:
   dependencies:
     "@types/hast" "^2.0.0"
 
+hast-util-parse-selector@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
+  integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
+  dependencies:
+    "@types/hast" "^3.0.0"
+
+hast-util-raw@^9.0.0:
+  version "9.0.4"
+  resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.4.tgz#2da03e37c46eb1a6f1391f02f9b84ae65818f7ed"
+  integrity sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    "@types/unist" "^3.0.0"
+    "@ungap/structured-clone" "^1.0.0"
+    hast-util-from-parse5 "^8.0.0"
+    hast-util-to-parse5 "^8.0.0"
+    html-void-elements "^3.0.0"
+    mdast-util-to-hast "^13.0.0"
+    parse5 "^7.0.0"
+    unist-util-position "^5.0.0"
+    unist-util-visit "^5.0.0"
+    vfile "^6.0.0"
+    web-namespaces "^2.0.0"
+    zwitch "^2.0.0"
+
 hast-util-to-estree@^2.0.0:
   version "2.3.3"
   resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz"
@@ -4946,6 +5024,19 @@ hast-util-to-estree@^2.0.0:
     unist-util-position "^4.0.0"
     zwitch "^2.0.0"
 
+hast-util-to-parse5@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed"
+  integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    comma-separated-tokens "^2.0.0"
+    devlop "^1.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+    web-namespaces "^2.0.0"
+    zwitch "^2.0.0"
+
 hast-util-to-text@^3.1.0:
   version "3.1.2"
   resolved "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz"
@@ -4983,6 +5074,17 @@ hastscript@^7.0.0:
     property-information "^6.0.0"
     space-separated-tokens "^2.0.0"
 
+hastscript@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a"
+  integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    comma-separated-tokens "^2.0.0"
+    hast-util-parse-selector "^4.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+
 heap@^0.2.6:
   version "0.2.7"
   resolved "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz"
@@ -5024,6 +5126,11 @@ html-parse-stringify@^3.0.1:
   dependencies:
     void-elements "3.1.0"
 
+html-void-elements@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
+  integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
+
 htmlparser2@^8.0.1:
   version "8.0.2"
   resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz"
@@ -5036,7 +5143,7 @@ htmlparser2@^8.0.1:
 
 http-cache-semantics@^4.0.0:
   version "4.1.1"
-  resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
   integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
 
 http-proxy-agent@^5.0.0:
@@ -5050,7 +5157,7 @@ http-proxy-agent@^5.0.0:
 
 http2-wrapper@^1.0.0-beta.5.2:
   version "1.0.3"
-  resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
+  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
   integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
   dependencies:
     quick-lru "^5.1.1"
@@ -6045,7 +6152,7 @@ jsesc@~0.5.0:
 
 json-buffer@3.0.1:
   version "3.0.1"
-  resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
   integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
 
 json-parse-even-better-errors@^2.3.0:
@@ -6102,7 +6209,7 @@ katex@^0.16.0, katex@^0.16.10:
 
 keyv@^4.0.0:
   version "4.5.4"
-  resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
   integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
   dependencies:
     json-buffer "3.0.1"
@@ -6280,7 +6387,7 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
 
 lowercase-keys@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
   integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
 
 lowlight@^1.17.0:
@@ -6316,12 +6423,12 @@ lz-string@^1.5.0:
   integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
 
 magicast@^0.3.4:
-  version "0.3.4"
-  resolved "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz#bbda1791d03190a24b00ff3dd18151e7fd381d19"
-  integrity sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739"
+  integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==
   dependencies:
-    "@babel/parser" "^7.24.4"
-    "@babel/types" "^7.24.0"
+    "@babel/parser" "^7.25.4"
+    "@babel/types" "^7.25.4"
     source-map-js "^1.2.0"
 
 make-dir@^4.0.0:
@@ -6549,6 +6656,21 @@ mdast-util-to-hast@^12.1.0:
     unist-util-position "^4.0.0"
     unist-util-visit "^4.0.0"
 
+mdast-util-to-hast@^13.0.0:
+  version "13.2.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
+  integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    "@types/mdast" "^4.0.0"
+    "@ungap/structured-clone" "^1.0.0"
+    devlop "^1.0.0"
+    micromark-util-sanitize-uri "^2.0.0"
+    trim-lines "^3.0.0"
+    unist-util-position "^5.0.0"
+    unist-util-visit "^5.0.0"
+    vfile "^6.0.0"
+
 mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0:
   version "1.5.0"
   resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz"
@@ -6865,6 +6987,14 @@ micromark-util-character@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-util-character@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1"
+  integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-chunked@^1.0.0:
   version "1.1.0"
   resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz"
@@ -6911,6 +7041,11 @@ micromark-util-encode@^1.0.0:
   resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz"
   integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==
 
+micromark-util-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
+  integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
+
 micromark-util-events-to-acorn@^1.0.0:
   version "1.2.3"
   resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz"
@@ -6953,6 +7088,15 @@ micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
     micromark-util-encode "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
+micromark-util-sanitize-uri@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
+  integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-subtokenize@^1.0.0:
   version "1.1.0"
   resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz"
@@ -6968,11 +7112,21 @@ micromark-util-symbol@^1.0.0:
   resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz"
   integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
 
+micromark-util-symbol@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
+  integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
+
 micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
   version "1.1.0"
   resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz"
   integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
 
+micromark-util-types@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
+  integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
+
 micromark@^3.0.0:
   version "3.2.0"
   resolved "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz"
@@ -7036,12 +7190,12 @@ mimic-fn@^4.0.0:
 
 mimic-response@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
 mimic-response@^3.1.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
   integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
 
 min-indent@^1.0.0:
@@ -7186,8 +7340,9 @@ normalize-range@^0.1.2:
 
 normalize-url@^6.0.1:
   version "6.1.0"
-  resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
   integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
 normalize-wheel@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
@@ -7309,7 +7464,7 @@ object.values@^1.1.6, object.values@^1.1.7:
 
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
   dependencies:
     wrappy "1"
@@ -7352,7 +7507,7 @@ optionator@^0.9.1:
 
 p-cancelable@^2.0.0:
   version "2.1.1"
-  resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
   integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
 
 p-limit@^2.2.0:
@@ -7683,7 +7838,7 @@ psl@^1.1.33:
 
 pump@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
   integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
   dependencies:
     end-of-stream "^1.1.0"
@@ -7728,7 +7883,7 @@ queue-microtask@^1.2.2:
 
 quick-lru@^5.1.1:
   version "5.1.1"
-  resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
   integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
 
 rc-input@~1.3.5:
@@ -8063,6 +8218,15 @@ rehype-katex@^6.0.2:
     katex "^0.16.0"
     unist-util-visit "^4.0.0"
 
+rehype-raw@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4"
+  integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    hast-util-raw "^9.0.0"
+    vfile "^6.0.0"
+
 remark-breaks@^3.0.2:
   version "3.0.3"
   resolved "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz"
@@ -8136,7 +8300,7 @@ resize-observer-polyfill@^1.5.1:
 
 resolve-alpn@^1.0.0:
   version "1.2.1"
-  resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
+  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
   integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==
 
 resolve-cwd@^3.0.0:
@@ -8186,7 +8350,7 @@ resolve@^2.0.0-next.4:
 
 responselike@^2.0.0:
   version "2.0.1"
-  resolved "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc"
   integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==
   dependencies:
     lowercase-keys "^2.0.0"
@@ -8590,7 +8754,7 @@ string-length@^4.0.1:
 
 string-width@4.2.3, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
   version "4.2.3"
-  resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
   dependencies:
     emoji-regex "^8.0.0"
@@ -8647,14 +8811,7 @@ stringify-entities@^4.0.0:
     character-entities-html4 "^2.0.0"
     character-entities-legacy "^3.0.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8968,9 +9125,9 @@ tslib@^1.8.1, tslib@^1.9.3:
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 tslib@^2.0.1:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
-  integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
+  integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
 
 tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0:
   version "2.5.3"
@@ -9108,6 +9265,13 @@ unist-util-is@^5.0.0:
   dependencies:
     "@types/unist" "^2.0.0"
 
+unist-util-is@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
+  integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0:
   version "1.1.2"
   resolved "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz"
@@ -9122,6 +9286,13 @@ unist-util-position@^4.0.0:
   dependencies:
     "@types/unist" "^2.0.0"
 
+unist-util-position@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
+  integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-remove-position@^4.0.0:
   version "4.0.2"
   resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz"
@@ -9144,6 +9315,13 @@ unist-util-stringify-position@^3.0.0:
   dependencies:
     "@types/unist" "^2.0.0"
 
+unist-util-stringify-position@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
+  integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
   version "5.1.3"
   resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz"
@@ -9152,6 +9330,14 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
     "@types/unist" "^2.0.0"
     unist-util-is "^5.0.0"
 
+unist-util-visit-parents@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
+  integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+
 unist-util-visit@^4.0.0:
   version "4.1.2"
   resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz"
@@ -9161,6 +9347,15 @@ unist-util-visit@^4.0.0:
     unist-util-is "^5.0.0"
     unist-util-visit-parents "^5.1.1"
 
+unist-util-visit@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
+  integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+    unist-util-visit-parents "^6.0.0"
+
 universalify@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -9267,6 +9462,14 @@ vfile-location@^4.0.0:
     "@types/unist" "^2.0.0"
     vfile "^5.0.0"
 
+vfile-location@^5.0.0:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
+  integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    vfile "^6.0.0"
+
 vfile-message@^3.0.0:
   version "3.1.4"
   resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz"
@@ -9275,6 +9478,14 @@ vfile-message@^3.0.0:
     "@types/unist" "^2.0.0"
     unist-util-stringify-position "^3.0.0"
 
+vfile-message@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
+  integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-stringify-position "^4.0.0"
+
 vfile@^5.0.0:
   version "5.3.7"
   resolved "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz"
@@ -9285,6 +9496,15 @@ vfile@^5.0.0:
     unist-util-stringify-position "^3.0.0"
     vfile-message "^3.0.0"
 
+vfile@^6.0.0:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.2.tgz#ef49548ea3d270097a67011921411130ceae7deb"
+  integrity sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-stringify-position "^4.0.0"
+    vfile-message "^4.0.0"
+
 vite-code-inspector-plugin@0.13.0:
   version "0.13.0"
   resolved "https://registry.npmjs.org/vite-code-inspector-plugin/-/vite-code-inspector-plugin-0.13.0.tgz"
@@ -9428,7 +9648,8 @@ word-wrap@^1.2.3:
   resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
   integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+  name wrap-ansi-cjs
   version "7.0.0"
   resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9446,15 +9667,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
@@ -9581,4 +9793,4 @@ zustand@^4.4.1, zustand@^4.5.2:
 zwitch@^2.0.0:
   version "2.0.4"
   resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"
-  integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
+  integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==