-
Notifications
You must be signed in to change notification settings - Fork 3.5k
fix(files): streaming preview invariant + OOXML style extraction #4335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c32360f
fix(files): suppress transient streaming preview errors for docx and …
waleedlatif1 a0aa8ec
feat(files): add OOXML style extraction for uploaded docx/pptx files
waleedlatif1 0b607ab
chore(files): polish style extraction — type narrowing + empty-styles…
waleedlatif1 82c4c58
fix(files): tighten streaming preview invariant and component consist…
waleedlatif1 24d5306
fix(files): remove setRenderError(null) from PPTX and PDF streaming p…
waleedlatif1 d0eacf1
feat(files): add compiled-check endpoint and VFS path for binary docu…
waleedlatif1 118484a
fix(files): remove dead renderError state from IframePreview
waleedlatif1 5798b87
refactor(files): hoist BINARY_DOC_TASKS to module scope in compiled-c…
waleedlatif1 11c6d7d
fix(files): deduplicate BINARY_DOC_TASKS and add size guard to VFS co…
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { toError } from '@sim/utils/errors' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' | ||
| import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task' | ||
| import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace' | ||
| import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
| export const runtime = 'nodejs' | ||
|
|
||
| const logger = createLogger('WorkspaceFileCompiledCheckAPI') | ||
|
|
||
| /** | ||
| * GET /api/workspaces/[id]/files/[fileId]/compiled-check | ||
| * | ||
| * Compiles the saved JavaScript source of a .docx / .pptx / .pdf file and | ||
| * returns whether it succeeds. Used by the file agent to self-verify generated | ||
| * code before finalising an edit. | ||
| * | ||
| * Returns: | ||
| * 200 { ok: true } | ||
| * 200 { ok: false, error: string, errorName: string } — user code error | ||
| * 4xx on auth / missing file / unsupported extension | ||
| * 500 on system (sandbox infra) failure | ||
| */ | ||
| export const GET = withRouteHandler( | ||
| async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { | ||
| const { id: workspaceId, fileId } = await params | ||
|
|
||
| const session = await getSession() | ||
| if (!session?.user?.id) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const membership = await verifyWorkspaceMembership(session.user.id, workspaceId) | ||
| if (!membership) { | ||
| return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) | ||
| } | ||
|
|
||
| const fileRecord = await getWorkspaceFile(workspaceId, fileId) | ||
| if (!fileRecord) { | ||
| return NextResponse.json({ error: 'File not found' }, { status: 404 }) | ||
| } | ||
|
|
||
| const ext = fileRecord.name.split('.').pop()?.toLowerCase() ?? '' | ||
| const taskId = BINARY_DOC_TASKS[ext] | ||
| if (!taskId) { | ||
| return NextResponse.json( | ||
| { error: `Compiled check only supports .docx, .pptx, and .pdf files` }, | ||
| { status: 422 } | ||
| ) | ||
| } | ||
|
|
||
| let buffer: Buffer | ||
| try { | ||
| buffer = await downloadWorkspaceFile(fileRecord) | ||
| } catch (err) { | ||
| logger.error('Failed to download file for compiled check', { | ||
| fileId, | ||
| error: toError(err).message, | ||
| }) | ||
| return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }) | ||
| } | ||
|
|
||
| const code = buffer.toString('utf-8') | ||
|
|
||
| if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) { | ||
| return NextResponse.json({ error: 'File source exceeds maximum size' }, { status: 413 }) | ||
| } | ||
|
|
||
| try { | ||
| await runSandboxTask(taskId, { code, workspaceId }, { ownerKey: `user:${session.user.id}` }) | ||
| return NextResponse.json({ ok: true }) | ||
| } catch (err) { | ||
| if (err instanceof SandboxUserCodeError) { | ||
| logger.info('Compiled check failed with user code error', { | ||
| fileId, | ||
| taskId, | ||
| error: toError(err).message, | ||
| errorName: err.name, | ||
| }) | ||
| return NextResponse.json({ ok: false, error: toError(err).message, errorName: err.name }) | ||
| } | ||
| throw err | ||
| } | ||
| } | ||
| ) |
81 changes: 81 additions & 0 deletions
81
apps/sim/app/api/workspaces/[id]/files/[fileId]/style/route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { toError } from '@sim/utils/errors' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { extractDocumentStyle } from '@/lib/copilot/vfs/document-style' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace' | ||
| import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
| export const runtime = 'nodejs' | ||
|
|
||
| const logger = createLogger('WorkspaceFileStyleAPI') | ||
|
|
||
| /** | ||
| * GET /api/workspaces/[id]/files/[fileId]/style | ||
| * Extract a compact JSON style summary from an uploaded .docx or .pptx file. | ||
| * Uses OOXML theme XML to return theme colors, font pair, and named styles. | ||
| * Only works on binary OOXML files (ZIP format) — not on JS source files. | ||
| */ | ||
| export const GET = withRouteHandler( | ||
| async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { | ||
| const { id: workspaceId, fileId } = await params | ||
|
|
||
| const session = await getSession() | ||
| if (!session?.user?.id) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const membership = await verifyWorkspaceMembership(session.user.id, workspaceId) | ||
| if (!membership) { | ||
| return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) | ||
| } | ||
|
|
||
| const fileRecord = await getWorkspaceFile(workspaceId, fileId) | ||
| if (!fileRecord) { | ||
| return NextResponse.json({ error: 'File not found' }, { status: 404 }) | ||
| } | ||
|
|
||
| const rawExt = fileRecord.name.split('.').pop()?.toLowerCase() | ||
| if (rawExt !== 'docx' && rawExt !== 'pptx') { | ||
| return NextResponse.json( | ||
| { error: 'Style extraction only supports .docx and .pptx files' }, | ||
| { status: 422 } | ||
| ) | ||
| } | ||
| const ext: 'docx' | 'pptx' = rawExt | ||
|
|
||
| let buffer: Buffer | ||
| try { | ||
| buffer = await downloadWorkspaceFile(fileRecord) | ||
| } catch (err) { | ||
| logger.error('Failed to download file for style extraction', { | ||
| fileId, | ||
| error: toError(err).message, | ||
| }) | ||
| return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }) | ||
| } | ||
|
|
||
| const summary = await extractDocumentStyle(buffer, ext) | ||
| if (!summary) { | ||
| return NextResponse.json( | ||
| { | ||
| error: | ||
| 'File is not a compiled binary document — style extraction requires an uploaded or compiled .docx/.pptx file', | ||
| }, | ||
| { status: 422 } | ||
| ) | ||
| } | ||
|
|
||
| logger.info('Extracted style summary via API', { | ||
| fileId, | ||
| format: ext, | ||
| themeName: summary.theme.name, | ||
| }) | ||
|
|
||
| return NextResponse.json(summary, { | ||
| headers: { 'Cache-Control': 'private, max-age=300' }, | ||
| }) | ||
| } | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.