fix code block

This commit is contained in:
Vikhyath Mondreti
2026-04-09 11:35:27 -07:00
parent 817833c3a3
commit 81ac66f104
5 changed files with 66 additions and 43 deletions

View File

@@ -1,5 +1,4 @@
import React, { type HTMLAttributes, memo, type ReactNode, useMemo } from 'react'
import { code } from '@streamdown/code'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { Tooltip } from '@/components/emcn'
@@ -26,8 +25,6 @@ export function LinkWithPreview({ href, children }: { href: string; children: Re
)
}
const STREAMDOWN_PLUGINS = { code }
function createCustomComponents(LinkComponent: typeof LinkWithPreview) {
return {
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
@@ -195,7 +192,7 @@ const MarkdownRenderer = memo(function MarkdownRenderer({
return (
<div className='space-y-4 break-words font-sans text-[var(--landing-text)] text-base leading-relaxed'>
<Streamdown mode='static' plugins={STREAMDOWN_PLUGINS} components={components}>
<Streamdown mode='static' components={components}>
{processedContent}
</Streamdown>
</div>

View File

@@ -1,7 +1,6 @@
'use client'
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
import { code } from '@streamdown/code'
import { useRouter } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import remarkBreaks from 'remark-breaks'
@@ -77,7 +76,6 @@ export const PreviewPanel = memo(function PreviewPanel({
const REMARK_PLUGINS = [remarkBreaks]
const REHYPE_PLUGINS = [rehypeSlug]
const STREAMDOWN_PLUGINS = { code }
/**
* Carries the contentRef and toggle handler from MarkdownPreview down to the
@@ -380,7 +378,6 @@ const MarkdownPreview = memo(function MarkdownPreview({
mode='static'
remarkPlugins={REMARK_PLUGINS}
rehypePlugins={REHYPE_PLUGINS}
plugins={STREAMDOWN_PLUGINS}
components={MARKDOWN_COMPONENTS}
>
{content}
@@ -398,7 +395,6 @@ const MarkdownPreview = memo(function MarkdownPreview({
mode='static'
remarkPlugins={REMARK_PLUGINS}
rehypePlugins={REHYPE_PLUGINS}
plugins={STREAMDOWN_PLUGINS}
components={MARKDOWN_COMPONENTS}
>
{content}

View File

@@ -1,11 +1,17 @@
'use client'
import { type ComponentPropsWithoutRef, useMemo } from 'react'
import { code } from '@streamdown/code'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { Checkbox } from '@/components/emcn'
import 'prismjs/components/prism-typescript'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-css'
import 'prismjs/components/prism-markup'
import '@/components/emcn/components/code/code.css'
import { Checkbox, highlight, languages } from '@/components/emcn'
import { CopyCodeButton } from '@/components/ui/copy-code-button'
import { cn } from '@/lib/core/utils/cn'
import { extractTextContent } from '@/lib/core/utils/react-node-text'
import {
PendingTagIndicator,
parseSpecialTags,
@@ -13,6 +19,19 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
import { useStreamingText } from '@/hooks/use-streaming-text'
const LANG_ALIASES: Record<string, string> = {
js: 'javascript',
ts: 'typescript',
tsx: 'typescript',
jsx: 'javascript',
sh: 'bash',
shell: 'bash',
html: 'markup',
xml: 'markup',
yml: 'yaml',
py: 'python',
}
const PROSE_CLASSES = cn(
'prose prose-base dark:prose-invert max-w-none',
'font-[family-name:var(--font-inter)] antialiased break-words font-[430] tracking-[0]',
@@ -24,8 +43,6 @@ const PROSE_CLASSES = cn(
'prose-ul:my-4 prose-ol:my-4',
'prose-strong:font-[600] prose-strong:text-[var(--text-primary)]',
'prose-a:text-[var(--text-primary)] prose-a:underline prose-a:decoration-dashed prose-a:underline-offset-4',
'prose-code:rounded prose-code:bg-[var(--surface-5)] prose-code:px-1.5 prose-code:py-0.5 prose-code:text-small prose-code:font-mono prose-code:font-[400] prose-code:text-[var(--text-primary)]',
'prose-code:before:content-none prose-code:after:content-none',
'prose-hr:border-[var(--divider)] prose-hr:my-6',
'prose-table:my-0'
)
@@ -33,8 +50,6 @@ const PROSE_CLASSES = cn(
type TdProps = ComponentPropsWithoutRef<'td'>
type ThProps = ComponentPropsWithoutRef<'th'>
const STREAMDOWN_PLUGINS = { code }
const MARKDOWN_COMPONENTS = {
table({ children }: { children?: React.ReactNode }) {
return (
@@ -68,6 +83,41 @@ const MARKDOWN_COMPONENTS = {
</td>
)
},
code({ children, className }: { children?: React.ReactNode; className?: string }) {
const langMatch = className?.match(/language-(\w+)/)
const language = langMatch ? langMatch[1] : ''
const codeString = extractTextContent(children)
if (!codeString) {
return (
<pre className='not-prose my-6 overflow-x-auto rounded-lg bg-[var(--surface-5)] p-4 font-[430] font-mono text-[var(--text-primary)] text-small leading-[21px] dark:bg-[var(--code-bg)]'>
<code>{children}</code>
</pre>
)
}
const resolved = LANG_ALIASES[language] || language || 'javascript'
const grammar = languages[resolved] || languages.javascript
const html = highlight(codeString.trimEnd(), grammar, resolved)
return (
<div className='not-prose my-6 overflow-hidden rounded-lg border border-[var(--divider)]'>
<div className='flex items-center justify-between border-[var(--divider)] border-b bg-[var(--surface-4)] px-4 py-2 dark:bg-[var(--surface-4)]'>
<span className='text-[var(--text-tertiary)] text-xs'>{language || 'code'}</span>
<CopyCodeButton
code={codeString}
className='text-[var(--text-tertiary)] hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]'
/>
</div>
<div className='code-editor-theme bg-[var(--surface-5)] dark:bg-[var(--code-bg)]'>
<pre
className='m-0 overflow-x-auto whitespace-pre p-4 font-[430] font-mono text-[var(--text-primary)] text-small leading-[21px]'
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</div>
)
},
a({ children, href }: { children?: React.ReactNode; href?: string }) {
return (
<a
@@ -103,6 +153,13 @@ const MARKDOWN_COMPONENTS = {
</li>
)
},
inlineCode({ children }: { children?: React.ReactNode }) {
return (
<code className='rounded bg-[var(--surface-5)] px-1.5 py-0.5 font-mono text-small font-[400] text-[var(--text-primary)] before:content-none after:content-none'>
{children}
</code>
)
},
input({ type, checked }: { type?: string; checked?: boolean }) {
if (type === 'checkbox') {
return <Checkbox checked={checked || false} disabled size='sm' className='mt-1.5 shrink-0' />
@@ -133,11 +190,7 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch
key={`${segment.type}-${i}`}
className={cn(PROSE_CLASSES, '[&>:first-child]:mt-0 [&>:last-child]:mb-0')}
>
<Streamdown
mode='static'
plugins={STREAMDOWN_PLUGINS}
components={MARKDOWN_COMPONENTS}
>
<Streamdown mode='static' components={MARKDOWN_COMPONENTS}>
{segment.content}
</Streamdown>
</div>
@@ -154,12 +207,7 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch
return (
<div className={cn(PROSE_CLASSES, '[&>:first-child]:mt-0 [&>:last-child]:mb-0')}>
<Streamdown
isAnimating={isStreaming}
animated
plugins={STREAMDOWN_PLUGINS}
components={MARKDOWN_COMPONENTS}
>
<Streamdown isAnimating={isStreaming} animated components={MARKDOWN_COMPONENTS}>
{rendered}
</Streamdown>
</div>

View File

@@ -92,7 +92,6 @@
"@react-email/render": "2.0.0",
"@sim/logger": "workspace:*",
"@socket.io/redis-adapter": "8.3.0",
"@streamdown/code": "1.1.1",
"@t3-oss/env-nextjs": "0.13.4",
"@tanstack/react-query": "5.90.8",
"@tanstack/react-query-devtools": "5.90.2",

View File

@@ -113,7 +113,6 @@
"@react-email/render": "2.0.0",
"@sim/logger": "workspace:*",
"@socket.io/redis-adapter": "8.3.0",
"@streamdown/code": "1.1.1",
"@t3-oss/env-nextjs": "0.13.4",
"@tanstack/react-query": "5.90.8",
"@tanstack/react-query-devtools": "5.90.2",
@@ -1458,8 +1457,6 @@
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.4", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-zVOiYO0+CF7EnBScz8s0O5JnJLPTU0lrUi8qhKXfIxIJXvI/jcppSiXXsEJwfB4A6XZawY/Wg/EQGKANi/aPmQ=="],
@@ -4630,8 +4627,6 @@
"@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"@streamdown/code/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
@@ -5298,18 +5293,6 @@
"@shikijs/rehype/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@streamdown/code/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@streamdown/code/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
"@streamdown/code/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
"@streamdown/code/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@streamdown/code/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@streamdown/code/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
"@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="],