optimize chat ui

This commit is contained in:
07akioni
2026-07-27 12:02:28 +08:00
parent f12a014481
commit 3ee2982f85
31 changed files with 759 additions and 186 deletions

View File

@@ -1,12 +1,10 @@
// CodeBlock: one code surface for every consumer — markdown fences, the
// run_code program body, and the details panel's raw args/output — with
// shiki highlighting for the registered grammars and an identical-geometry
// plain fallback for everything else. Shiki emits a single <pre class="shiki">
// tree of nested spans whose colors are --shiki-* custom properties
// (token sheets own the values); it produces no scripts or event handlers,
// so injecting its output is safe by construction.
// plain fallback for everything else. Chrome (language banner + copy) matches
// deepsuite `@deepseek/md` code blocks; token colors stay on `--shiki-*`.
import { useMemo } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { highlightToHtml } from './highlight.ts'
import css from './CodeBlock.module.css'
@@ -20,18 +18,72 @@ export interface CodeBlockProps {
className?: string | undefined
}
async function writeClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return
}
// jsdom and older hosts: best-effort execCommand path when present.
const exec = typeof document.execCommand === 'function'
? document.execCommand.bind(document)
: undefined
if (exec === undefined) return
const el = document.createElement('textarea')
el.value = text
el.setAttribute('readonly', '')
el.style.position = 'fixed'
el.style.left = '-9999px'
document.body.appendChild(el)
el.select()
try {
exec('copy')
} catch {
// Clipboard unavailable (sandboxed iframe / denied permission); UI still
// flips to the ok label so the gesture is acknowledged.
}
el.remove()
}
export function CodeBlock({ code, lang, className }: CodeBlockProps) {
const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code
const html = useMemo(() => highlightToHtml(trimmed, lang), [trimmed, lang])
if (html === undefined) {
return (
<div className={clsx(css.block, className)}>
const rootRef = useRef<HTMLDivElement>(null)
const [copied, setCopied] = useState(false)
const onCopy = useCallback(() => {
if (copied) return
/* v8 ignore next -- both arms always mount a <pre>; trimmed is the
typed fallback if the DOM shape ever diverges. */
const text = rootRef.current?.querySelector('pre')?.textContent ?? trimmed
void writeClipboard(text)
setCopied(true)
window.setTimeout(() => setCopied(false), 1000)
}, [copied, trimmed])
const body = html === undefined
? (
<pre className={css.plain}><code>{trimmed}</code></pre>
)
: (
// eslint-disable-next-line react/no-danger -- shiki's output is a static
// span tree it generated from `code` (no user HTML passes through), the
// sanctioned innerHTML consumption path per shiki's own docs.
<div dangerouslySetInnerHTML={{ __html: html }} />
)
return (
<div ref={rootRef} className={clsx(css.block, 'md-code-block', className)}>
<div className={css.bannerWrap}>
<div className={css.banner}>
<div className={css.infostring}>{lang ?? ''}</div>
<div className={css.action}>
<button type="button" className={css.copyButton} onClick={onCopy}>
{copied ? '复制成功' : '复制'}
</button>
</div>
</div>
</div>
)
}
// eslint-disable-next-line react/no-danger -- shiki's output is a static
// span tree it generated from `code` (no user HTML passes through), the
// sanctioned innerHTML consumption path per shiki's own docs.
return <div className={clsx(css.block, className)} dangerouslySetInnerHTML={{ __html: html }} />
{body}
</div>
)
}