diff --git a/apps/web/src/components/tools/collage-preview.tsx b/apps/web/src/components/tools/collage-preview.tsx
index 5283974a..20d90b76 100644
--- a/apps/web/src/components/tools/collage-preview.tsx
+++ b/apps/web/src/components/tools/collage-preview.tsx
@@ -61,12 +61,7 @@ export function CollagePreview() {
}
if (phase === "processing") {
- return (
-
-
-
Creating your collage...
-
- );
+ return ;
}
if (phase === "result" && resultUrl) {
@@ -150,6 +145,29 @@ function UploadArea() {
);
}
+function ProcessingView() {
+ const progress = useCollageStore((s) => s.progress);
+ const label = progress < 80 ? "Uploading images..." : "Processing collage...";
+
+ return (
+
+
+
+
+ {label}
+ {progress}%
+
+
+
+
+ );
+}
+
/** The live CSS Grid collage canvas. */
function CollageCanvas({ template }: { template: CollageTemplate }) {
const store = useCollageStore();
diff --git a/apps/web/src/components/tools/collage-settings.tsx b/apps/web/src/components/tools/collage-settings.tsx
index 65955640..cf2b6174 100644
--- a/apps/web/src/components/tools/collage-settings.tsx
+++ b/apps/web/src/components/tools/collage-settings.tsx
@@ -63,11 +63,11 @@ export function CollageSettings() {
if (!hasImages || !template) return;
store.setPhase("processing");
+ store.setProgress(0);
store.setError(null);
try {
const formData = new FormData();
- // Send images in cell-assignment order
for (let i = 0; i < template.cells.length; i++) {
const imgIdx = cellAssignments[i] ?? -1;
if (imgIdx >= 0 && images[imgIdx]) {
@@ -94,18 +94,50 @@ export function CollageSettings() {
}),
);
- const res = await fetch("/api/v1/tools/collage", {
- method: "POST",
- headers: formatHeaders(),
- body: formData,
+ const result = await new Promise<{
+ downloadUrl: string;
+ processedSize: number;
+ originalSize: number;
+ jobId: string;
+ }>((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", "/api/v1/tools/collage");
+
+ const headers = formatHeaders();
+ headers.forEach((value, key) => {
+ xhr.setRequestHeader(key, value);
+ });
+
+ xhr.upload.onprogress = (e) => {
+ if (e.lengthComputable) {
+ store.setProgress(Math.round((e.loaded / e.total) * 80));
+ }
+ };
+
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ store.setProgress(100);
+ try {
+ resolve(JSON.parse(xhr.responseText));
+ } catch {
+ reject(new Error("Invalid response"));
+ }
+ } else {
+ try {
+ const body = JSON.parse(xhr.responseText);
+ reject(new Error(body.error || `Failed: ${xhr.status}`));
+ } catch {
+ reject(new Error(`Failed: ${xhr.status}`));
+ }
+ }
+ };
+
+ xhr.onerror = () => reject(new Error("Network error"));
+ xhr.send(formData);
+
+ store.setProgress(5);
});
- if (!res.ok) {
- const body = await res.json().catch(() => ({}));
- throw new Error(body.error || `Failed: ${res.status}`);
- }
-
- const result = await res.json();
store.setResult(result.downloadUrl, result.processedSize, result.originalSize, result.jobId);
} catch (err) {
store.setError(err instanceof Error ? err.message : "Collage failed");
diff --git a/apps/web/src/stores/collage-store.ts b/apps/web/src/stores/collage-store.ts
index fbc2abb8..62aa1e1a 100644
--- a/apps/web/src/stores/collage-store.ts
+++ b/apps/web/src/stores/collage-store.ts
@@ -50,6 +50,7 @@ interface CollageState {
// UI state
selectedCell: number | null;
phase: Phase;
+ progress: number;
resultUrl: string | null;
resultSize: number | null;
originalSize: number | null;
@@ -74,6 +75,7 @@ interface CollageState {
setQuality: (v: number) => void;
setSelectedCell: (v: number | null) => void;
setPhase: (v: Phase) => void;
+ setProgress: (v: number) => void;
setResult: (url: string, size: number, originalSize: number, jobId: string) => void;
setError: (e: string | null) => void;
reset: () => void;
@@ -105,6 +107,7 @@ export const useCollageStore = create((set, get) => ({
quality: 90,
selectedCell: null,
phase: "upload",
+ progress: 0,
resultUrl: null,
resultSize: null,
originalSize: null,
@@ -263,6 +266,7 @@ export const useCollageStore = create((set, get) => ({
setQuality: (v) => set({ quality: v, resultUrl: null }),
setSelectedCell: (v) => set({ selectedCell: v }),
setPhase: (v) => set({ phase: v }),
+ setProgress: (v) => set({ progress: v }),
setResult: (url, size, originalSize, jobId) =>
set({ resultUrl: url, resultSize: size, originalSize, jobId, phase: "result", error: null }),
setError: (e) => set({ error: e, phase: "editing" }),
@@ -287,6 +291,7 @@ export const useCollageStore = create((set, get) => ({
quality: 90,
selectedCell: null,
phase: "upload",
+ progress: 0,
resultUrl: null,
resultSize: null,
originalSize: null,