Skip to content

Commit 133b4bd

Browse files
committed
mermaid linter errors sent back to llm
1 parent df381e0 commit 133b4bd

6 files changed

Lines changed: 291 additions & 127 deletions

File tree

apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getSession } from '@/lib/auth'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
88
import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
99
import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task'
10+
import { validateMermaidSource } from '@/lib/mermaid/validate'
1011
import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace'
1112
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
1213

@@ -18,9 +19,9 @@ const logger = createLogger('WorkspaceFileCompiledCheckAPI')
1819
/**
1920
* GET /api/workspaces/[id]/files/[fileId]/compiled-check
2021
*
21-
* Compiles the saved JavaScript source of a .docx / .pptx / .pdf file and
22+
* Compiles or validates the saved source for generated document-like files and
2223
* returns whether it succeeds. Used by the file agent to self-verify generated
23-
* code before finalising an edit.
24+
* code or diagram syntax before finalising an edit.
2425
*
2526
* Returns:
2627
* 200 { ok: true }
@@ -51,9 +52,10 @@ export const GET = withRouteHandler(
5152

5253
const ext = fileRecord.name.split('.').pop()?.toLowerCase() ?? ''
5354
const taskId = BINARY_DOC_TASKS[ext]
54-
if (!taskId) {
55+
const isMermaidFile = ext === 'mmd' || ext === 'mermaid'
56+
if (!taskId && !isMermaidFile) {
5557
return NextResponse.json(
56-
{ error: `Compiled check only supports .docx, .pptx, and .pdf files` },
58+
{ error: `Compiled check only supports .docx, .pptx, .pdf, and .mmd files` },
5759
{ status: 422 }
5860
)
5961
}
@@ -75,7 +77,14 @@ export const GET = withRouteHandler(
7577
return NextResponse.json({ error: 'File source exceeds maximum size' }, { status: 413 })
7678
}
7779

80+
if (isMermaidFile) {
81+
return NextResponse.json(await validateMermaidSource(code))
82+
}
83+
7884
try {
85+
if (!taskId) {
86+
return NextResponse.json({ error: 'Unsupported compiled check target' }, { status: 422 })
87+
}
7988
await runSandboxTask(taskId, { code, workspaceId }, { ownerKey: `user:${session.user.id}` })
8089
return NextResponse.json({ ok: true })
8190
} catch (err) {
Lines changed: 11 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,21 @@
11
'use client'
22

3-
import { memo, useEffect, useRef, useState } from 'react'
4-
import { ZoomIn, ZoomOut } from 'lucide-react'
5-
import { Button } from '@/components/emcn'
3+
import { memo } from 'react'
64
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
7-
8-
const ZOOM_MIN = 0.25
9-
const ZOOM_MAX = 4
10-
const ZOOM_WHEEL_SENSITIVITY = 0.005
11-
const ZOOM_BUTTON_FACTOR = 1.2
12-
13-
const clampZoom = (z: number) => Math.min(Math.max(z, ZOOM_MIN), ZOOM_MAX)
5+
import { ZoomablePreview } from './zoomable-preview'
146

157
export const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
168
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
17-
const [zoom, setZoom] = useState(1)
18-
const [offset, setOffset] = useState({ x: 0, y: 0 })
19-
const isDragging = useRef(false)
20-
const dragStart = useRef({ x: 0, y: 0 })
21-
const offsetAtDragStart = useRef({ x: 0, y: 0 })
22-
const offsetRef = useRef(offset)
23-
offsetRef.current = offset
24-
25-
const containerRef = useRef<HTMLDivElement>(null)
26-
27-
const zoomIn = () => setZoom((z) => clampZoom(z * ZOOM_BUTTON_FACTOR))
28-
const zoomOut = () => setZoom((z) => clampZoom(z / ZOOM_BUTTON_FACTOR))
29-
30-
useEffect(() => {
31-
const el = containerRef.current
32-
if (!el) return
33-
const onWheel = (e: WheelEvent) => {
34-
e.preventDefault()
35-
if (e.ctrlKey || e.metaKey) {
36-
setZoom((z) => clampZoom(z * Math.exp(-e.deltaY * ZOOM_WHEEL_SENSITIVITY)))
37-
} else {
38-
setOffset((o) => ({ x: o.x - e.deltaX, y: o.y - e.deltaY }))
39-
}
40-
}
41-
el.addEventListener('wheel', onWheel, { passive: false })
42-
return () => el.removeEventListener('wheel', onWheel)
43-
}, [])
44-
45-
const handleMouseDown = (e: React.MouseEvent) => {
46-
if (e.button !== 0) return
47-
isDragging.current = true
48-
dragStart.current = { x: e.clientX, y: e.clientY }
49-
offsetAtDragStart.current = offsetRef.current
50-
if (containerRef.current) containerRef.current.style.cursor = 'grabbing'
51-
e.preventDefault()
52-
}
53-
54-
const handleMouseMove = (e: React.MouseEvent) => {
55-
if (!isDragging.current) return
56-
setOffset({
57-
x: offsetAtDragStart.current.x + (e.clientX - dragStart.current.x),
58-
y: offsetAtDragStart.current.y + (e.clientY - dragStart.current.y),
59-
})
60-
}
61-
62-
const handleMouseUp = () => {
63-
isDragging.current = false
64-
if (containerRef.current) containerRef.current.style.cursor = 'grab'
65-
}
669

6710
return (
68-
<div
69-
ref={containerRef}
70-
className='relative flex flex-1 cursor-grab overflow-hidden bg-[var(--surface-1)]'
71-
onMouseDown={handleMouseDown}
72-
onMouseMove={handleMouseMove}
73-
onMouseUp={handleMouseUp}
74-
onMouseLeave={handleMouseUp}
75-
>
76-
<div
77-
className='pointer-events-none absolute inset-0 flex items-center justify-center'
78-
style={{
79-
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
80-
transformOrigin: 'center center',
81-
}}
82-
>
83-
<img
84-
src={serveUrl}
85-
alt={file.name}
86-
className='max-h-full max-w-full select-none rounded-md object-contain'
87-
draggable={false}
88-
loading='eager'
89-
/>
90-
</div>
91-
<div
92-
className='absolute right-4 bottom-4 flex items-center gap-1 rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1 shadow-card'
93-
onMouseDown={(e) => e.stopPropagation()}
94-
>
95-
<Button
96-
variant='ghost'
97-
size='sm'
98-
onClick={zoomOut}
99-
disabled={zoom <= ZOOM_MIN}
100-
className='h-6 w-6 p-0'
101-
aria-label='Zoom out'
102-
>
103-
<ZoomOut className='h-3.5 w-3.5' />
104-
</Button>
105-
<span className='min-w-[3rem] text-center text-[11px] text-[var(--text-secondary)]'>
106-
{Math.round(zoom * 100)}%
107-
</span>
108-
<Button
109-
variant='ghost'
110-
size='sm'
111-
onClick={zoomIn}
112-
disabled={zoom >= ZOOM_MAX}
113-
className='h-6 w-6 p-0'
114-
aria-label='Zoom in'
115-
>
116-
<ZoomIn className='h-3.5 w-3.5' />
117-
</Button>
118-
</div>
119-
</div>
11+
<ZoomablePreview className='flex flex-1'>
12+
<img
13+
src={serveUrl}
14+
alt={file.name}
15+
className='max-h-full max-w-full select-none rounded-md object-contain'
16+
draggable={false}
17+
loading='eager'
18+
/>
19+
</ZoomablePreview>
12020
)
12121
})

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { extractTextContent } from '@/lib/core/utils/react-node-text'
2525
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
2626
import { useAutoScroll } from '@/hooks/use-auto-scroll'
2727
import { DataTable } from './data-table'
28+
import { ZoomablePreview } from './zoomable-preview'
2829

2930
interface HastNode {
3031
position?: { start?: { offset?: number } }
@@ -252,15 +253,21 @@ function CalloutBlock({ type, children }: { type: string; children?: React.React
252253
function MermaidSourcePreview({
253254
definition,
254255
isRendering,
256+
status,
255257
}: {
256258
definition: string
257259
isRendering: boolean
260+
status?: string
258261
}) {
259262
return (
260263
<div className='my-4 overflow-hidden rounded-lg border border-[var(--border)]'>
261264
<div className='flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-3)] px-3 py-1.5'>
262265
<span className='text-[11px] text-[var(--text-tertiary)]'>mermaid</span>
263-
{isRendering && <span className='text-[11px] text-[var(--text-muted)]'>Rendering...</span>}
266+
{(isRendering || status) && (
267+
<span className='text-[11px] text-[var(--text-muted)]'>
268+
{isRendering ? 'Rendering...' : status}
269+
</span>
270+
)}
264271
</div>
265272
<div className='code-editor-theme bg-[var(--surface-5)]'>
266273
<pre className='m-0 overflow-x-auto whitespace-pre p-4 font-mono text-[13px] text-[var(--text-primary)] leading-[1.6]'>
@@ -271,12 +278,34 @@ function MermaidSourcePreview({
271278
)
272279
}
273280

281+
function MermaidCodeBlockSkeleton() {
282+
return (
283+
<div className='my-4 overflow-hidden rounded-lg border border-[var(--border)]'>
284+
<div className='flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-3)] px-3 py-1.5'>
285+
<span className='text-[11px] text-[var(--text-tertiary)]'>mermaid</span>
286+
<span className='text-[11px] text-[var(--text-muted)]'>Rendering...</span>
287+
</div>
288+
<div className='code-editor-theme bg-[var(--surface-5)]'>
289+
<div className='space-y-2 p-4'>
290+
<div className='h-3 w-5/6 animate-pulse rounded bg-[var(--surface-2)]' />
291+
<div className='h-3 w-2/3 animate-pulse rounded bg-[var(--surface-2)]' />
292+
<div className='h-3 w-3/4 animate-pulse rounded bg-[var(--surface-2)]' />
293+
</div>
294+
</div>
295+
</div>
296+
)
297+
}
298+
274299
const MermaidDiagram = memo(function MermaidDiagram({
275300
definition,
276301
isStreaming = false,
302+
zoomable = false,
303+
zoomClassName,
277304
}: {
278305
definition: string
279306
isStreaming?: boolean
307+
zoomable?: boolean
308+
zoomClassName?: string
280309
}) {
281310
const [svg, setSvg] = useState<string | null>(null)
282311
const [error, setError] = useState<string | null>(null)
@@ -360,14 +389,23 @@ const MermaidDiagram = memo(function MermaidDiagram({
360389

361390
if (error) {
362391
return (
363-
<div className='my-4 rounded-lg border border-[var(--border)] p-4 text-[13px] text-[var(--text-muted)]'>
364-
<span className='font-medium text-[var(--text-primary)]'>Diagram error: </span>
365-
{error}
366-
</div>
392+
<MermaidSourcePreview definition={definition} isRendering={false} status='Invalid diagram' />
367393
)
368394
}
369395

370396
if (svg && renderedDefinition === trimmedDefinition) {
397+
const diagram = (
398+
<div className='max-h-full max-w-full' dangerouslySetInnerHTML={{ __html: svg }} />
399+
)
400+
401+
if (zoomable) {
402+
return (
403+
<ZoomablePreview className={zoomClassName ?? 'my-4 h-[420px] rounded-lg'}>
404+
{diagram}
405+
</ZoomablePreview>
406+
)
407+
}
408+
371409
return (
372410
<div className='my-4 overflow-auto rounded-lg' dangerouslySetInnerHTML={{ __html: svg }} />
373411
)
@@ -378,7 +416,7 @@ const MermaidDiagram = memo(function MermaidDiagram({
378416
}
379417

380418
if (!trimmedDefinition || !svg) {
381-
return <div className='my-4 h-[100px] animate-pulse rounded-lg bg-[var(--surface-2)]' />
419+
return <MermaidCodeBlockSkeleton />
382420
}
383421
return null
384422
})
@@ -949,21 +987,26 @@ function SvgPreview({ content }: { content: string }) {
949987
const wrappedContent = `<!DOCTYPE html><html><head><style>body{margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:transparent;}svg{max-width:100%;max-height:100vh;}</style></head><body>${content}</body></html>`
950988

951989
return (
952-
<div className='h-full overflow-hidden'>
990+
<ZoomablePreview className='h-full'>
953991
<iframe
954992
srcDoc={wrappedContent}
955993
sandbox=''
956994
title='SVG Preview'
957995
className='h-full w-full border-0'
958996
/>
959-
</div>
997+
</ZoomablePreview>
960998
)
961999
}
9621000

9631001
function MermaidFilePreview({ content, isStreaming }: { content: string; isStreaming?: boolean }) {
9641002
return (
9651003
<div className='h-full overflow-auto p-6'>
966-
<MermaidDiagram definition={content} isStreaming={isStreaming} />
1004+
<MermaidDiagram
1005+
definition={content}
1006+
isStreaming={isStreaming}
1007+
zoomable
1008+
zoomClassName='h-full rounded-lg'
1009+
/>
9671010
</div>
9681011
)
9691012
}

0 commit comments

Comments
 (0)