'use client' import { useAuiState } from '@assistant-ui/react' import { type StreamdownTextComponents, StreamdownTextPrimitive, type SyntaxHighlighterProps } from '@assistant-ui/react-streamdown' import { code } from '@streamdown/code' import { type ComponentProps, memo, useEffect, useMemo, useState } from 'react' import { PreviewAttachment } from '@/components/assistant-ui/preview-attachment' import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter' import { ZoomableImage } from '@/components/assistant-ui/zoomable-image' import { CopyButton } from '@/components/ui/copy-button' import { isLikelyProseCodeBlock, sanitizeLanguageTag } from '@/lib/markdown-code' import { preprocessMarkdown } from '@/lib/markdown-preprocess' import { filePathFromMediaPath, mediaExternalUrl, mediaKind, mediaMime, mediaName, mediaPathFromMarkdownHref } from '@/lib/media' import { previewTargetFromMarkdownHref } from '@/lib/preview-targets' import { cn } from '@/lib/utils' function CodeHeader({ language, code }: { language?: string; code?: string }) { const normalizedCode = (code ?? '').replace(/^\n+/, '').trimEnd() // Streamdown can transiently parse stray backticks / incomplete fences as // an empty code block while text is streaming, e.g. "200.``` http://...". // Rendering our header + empty body for that looks like a giant blank // code card. Hide the whole block until there's actual code content. if (!normalizedCode.trim() || isLikelyProseCodeBlock(language, normalizedCode)) { return null } const cleanLanguage = sanitizeLanguageTag(language || '') const label = cleanLanguage && cleanLanguage !== 'unknown' ? cleanLanguage : '' return (
{label || 'code'} Copy
) } async function typedBlobUrl(dataUrl: string, mime: string): Promise { const blob = await fetch(dataUrl).then(response => response.blob()) return URL.createObjectURL(new Blob([await blob.arrayBuffer()], { type: mime })) } async function mediaSrc(path: string): Promise { if (/^(?:https?|data):/i.test(path)) { return path } if (!window.hermesDesktop?.readFileDataUrl) { return mediaExternalUrl(path) } const dataUrl = await window.hermesDesktop.readFileDataUrl(filePathFromMediaPath(path)) return ['audio', 'video'].includes(mediaKind(path)) ? typedBlobUrl(dataUrl, mediaMime(path)) : dataUrl } function OpenMediaButton({ kind, path }: { kind: 'audio' | 'video'; path: string }) { return ( ) } function MediaAttachment({ path }: { path: string }) { const [src, setSrc] = useState('') const [failed, setFailed] = useState(false) const kind = mediaKind(path) const name = mediaName(path) useEffect(() => { let cancelled = false let objectUrl = '' setFailed(false) setSrc('') void mediaSrc(path) .then(value => { if (value.startsWith('blob:')) { objectUrl = value } if (!cancelled) { setSrc(value) } else if (objectUrl) { URL.revokeObjectURL(objectUrl) } }) .catch(() => { if (!cancelled) { setFailed(true) } }) return () => { cancelled = true if (objectUrl) { URL.revokeObjectURL(objectUrl) } } }, [path]) if (kind === 'image' && src) { return ( ) } if (kind === 'audio' && src) { return ( {name} ) } if (kind === 'video' && src) { return ( {name} ) } return ( { event.preventDefault() void window.hermesDesktop?.openExternal(mediaExternalUrl(path)) }} > {failed ? `Open ${name}` : `Loading ${name}...`} ) } function MarkdownLink({ className, href, ...props }: ComponentProps<'a'>) { const mediaPath = mediaPathFromMarkdownHref(href) const previewTarget = previewTargetFromMarkdownHref(href) if (mediaPath) { return } if (previewTarget) { return } return ( ) } function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>) { return ( ) } const MarkdownTextImpl = () => { const isStreaming = useAuiState(s => s.message.status?.type === 'running') const components = useMemo( () => ({ h1: ({ className, ...props }: ComponentProps<'h1'>) => (

), h2: ({ className, ...props }: ComponentProps<'h2'>) => (

), h3: ({ className, ...props }: ComponentProps<'h3'>) => (

), h4: ({ className, ...props }: ComponentProps<'h4'>) => (

), p: ({ className, ...props }: ComponentProps<'p'>) => (

), a: MarkdownLink, hr: ({ className, ...props }: ComponentProps<'hr'>) => (


), blockquote: ({ className, ...props }: ComponentProps<'blockquote'>) => (
), ul: ({ className, ...props }: ComponentProps<'ul'>) => (
    ), ol: ({ className, ...props }: ComponentProps<'ol'>) => (
      ), li: ({ className, ...props }: ComponentProps<'li'>) => (
    1. ), table: ({ className, ...props }: ComponentProps<'table'>) => (
      ), thead: ({ className, ...props }: ComponentProps<'thead'>) => ( ), th: ({ className, ...props }: ComponentProps<'th'>) => (
      ), td: ({ className, ...props }: ComponentProps<'td'>) => ( ), img: MarkdownImage, SyntaxHighlighter: (props: SyntaxHighlighterProps) => , CodeHeader }) as StreamdownTextComponents, [isStreaming] ) return ( ) } export const MarkdownText = memo(MarkdownTextImpl)