Skip to content

Commit 5f3baa4

Browse files
fix(file): zero byte codegen file format + zoomable preview wrapper + mermaid errors loopback (#4400)
* fix(file): zero byte codegen file format * address comments * distinguish streaming from actual empty pdf * fix mermaid diagram state mgmt * mermaid linter errors sent back to llm * address comments * fix autozoom
1 parent 778f4a5 commit 5f3baa4

9 files changed

Lines changed: 511 additions & 152 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) {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ const IframePreview = memo(function IframePreview({
225225
)
226226

227227
if (streamingContent !== undefined) {
228-
if (!streamingSource) {
228+
if (
229+
!streamingSource ||
230+
streamingSource.kind !== 'buffer' ||
231+
streamingSource.buffer.byteLength === 0
232+
) {
229233
return <div className='relative flex flex-1 overflow-hidden'>{PDF_PAGE_SKELETON}</div>
230234
}
231235
return <PdfViewerCore key={streamingBufferSeq} source={streamingSource} filename={file.name} />
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' contentClassName='h-full w-full'>
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/pdf-viewer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const PdfViewerCore = memo(function PdfViewerCore({ source, filename }: P
7979

8080
const sourceValue = source.kind === 'url' ? source.url : source.buffer
8181
const file = useMemo(
82-
() => (source.kind === 'url' ? source.url : { data: new Uint8Array(source.buffer) }),
82+
() => (source.kind === 'url' ? source.url : { data: new Uint8Array(source.buffer.slice(0)) }),
8383
[sourceValue]
8484
)
8585

0 commit comments

Comments
 (0)