ai-chatbot/apps/editor/store/use-upload.ts
Aymeric Rabot 64de2a8e63 feat(editor): direct-to-Supabase uploads with progress for large scans
Upload files directly to Supabase Storage via signed URLs, bypassing
the Next.js 100MB body size limit. Supports scans up to 200MB with
real-time progress tracking that persists across sidebar panel switches.

- Add Zustand upload store for persistent upload state
- Add createAssetUploadUrl/confirmAssetUpload server actions
- Add XHR-based upload orchestrator with progress events
- Scene node creation works even if sidebar panel unmounts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:19:06 -05:00

69 lines
2.0 KiB
TypeScript

import { create } from 'zustand'
export type UploadStatus = 'preparing' | 'uploading' | 'confirming' | 'done' | 'error'
export interface UploadEntry {
status: UploadStatus
assetType: 'scan' | 'guide'
fileName: string
progress: number // 0-100
error: string | null
resultUrl: string | null
}
interface UploadState {
uploads: Record<string, UploadEntry>
startUpload: (levelId: string, assetType: 'scan' | 'guide', fileName: string) => void
setProgress: (levelId: string, progress: number) => void
setStatus: (levelId: string, status: UploadStatus) => void
setError: (levelId: string, error: string) => void
setResult: (levelId: string, url: string) => void
clearUpload: (levelId: string) => void
}
export const useUploadStore = create<UploadState>((set) => ({
uploads: {},
startUpload: (levelId, assetType, fileName) =>
set((s) => ({
uploads: {
...s.uploads,
[levelId]: { status: 'preparing', assetType, fileName, progress: 0, error: null, resultUrl: null },
},
})),
setProgress: (levelId, progress) =>
set((s) => {
const entry = s.uploads[levelId]
if (!entry) return s
return { uploads: { ...s.uploads, [levelId]: { ...entry, progress } } }
}),
setStatus: (levelId, status) =>
set((s) => {
const entry = s.uploads[levelId]
if (!entry) return s
return { uploads: { ...s.uploads, [levelId]: { ...entry, status } } }
}),
setError: (levelId, error) =>
set((s) => {
const entry = s.uploads[levelId]
if (!entry) return s
return { uploads: { ...s.uploads, [levelId]: { ...entry, status: 'error' as const, error } } }
}),
setResult: (levelId, url) =>
set((s) => {
const entry = s.uploads[levelId]
if (!entry) return s
return { uploads: { ...s.uploads, [levelId]: { ...entry, status: 'done' as const, resultUrl: url } } }
}),
clearUpload: (levelId) =>
set((s) => {
const { [levelId]: _, ...rest } = s.uploads
return { uploads: rest }
}),
}))