Merge pull request #52935 from NousResearch/bb/desktop-inline-rendering

feat(desktop): inline rich embeds, diagrams & alerts in assistant markdown
This commit is contained in:
brooklyn! 2026-06-26 13:36:43 -05:00 committed by GitHub
commit ed962104c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2002 additions and 10 deletions

View file

@ -56,6 +56,7 @@ import {
IconLock as Lock,
IconLogin as LogIn,
IconMail as Mail,
IconMaximize as Maximize,
IconMessageCircle as MessageCircle,
IconMessageQuestion as MessageQuestion,
IconMessage2 as MessageSquareText,
@ -105,7 +106,9 @@ import {
IconX as X,
IconX as XIcon,
IconBolt as Zap,
IconBoltFilled as ZapFilled
IconBoltFilled as ZapFilled,
IconZoomIn as ZoomIn,
IconZoomOut as ZoomOut
} from '@tabler/icons-react'
export {
@ -166,6 +169,7 @@ export {
Lock,
LogIn,
Mail,
Maximize,
MessageCircle,
MessageQuestion,
MessageSquareText,
@ -215,7 +219,9 @@ export {
X,
XIcon,
Zap,
ZapFilled
ZapFilled,
ZoomIn,
ZoomOut
}
export type { Icon as IconComponent } from '@tabler/icons-react'

View file

@ -0,0 +1,56 @@
// Rasterise an SVG string to PNG and copy it to the clipboard. Self-contained
// SVGs only (inline styles) — mermaid output qualifies. Falls back to copying
// the SVG markup as text where image clipboard writes aren't permitted.
function svgSize(svg: string): { height: number; width: number } {
const el = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement
const width = parseFloat(el.getAttribute('width') || '')
const height = parseFloat(el.getAttribute('height') || '')
if (width && height) {
return { height, width }
}
const [, , vbW, vbH] = (el.getAttribute('viewBox') || '').split(/[\s,]+/).map(Number)
return vbW && vbH ? { height: vbH, width: vbW } : { height: 600, width: 800 }
}
export function svgToPngBlob(svg: string, scale = 2): Promise<Blob> {
const { height, width } = svgSize(svg)
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = Math.max(1, Math.round(width * scale))
canvas.height = Math.max(1, Math.round(height * scale))
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('no 2d context'))
return
}
ctx.scale(scale, scale)
ctx.drawImage(image, 0, 0, width, height)
canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('toBlob failed'))), 'image/png')
}
image.onerror = () => reject(new Error('svg load failed'))
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
})
}
export async function copySvgAsPng(svg: string): Promise<void> {
try {
const blob = await svgToPngBlob(svg)
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
} catch {
await navigator.clipboard.writeText(svg)
}
}