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:
commit
ed962104c8
46 changed files with 2002 additions and 10 deletions
|
|
@ -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'
|
||||
|
|
|
|||
56
apps/desktop/src/lib/svg-image.ts
Normal file
56
apps/desktop/src/lib/svg-image.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue