mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: batch SSE progress and non-AI processing UX (#24)
Batch progress was broken because JobProgress events lacked a `type` field. The frontend checks `data.type === "batch"` to distinguish batch from single-file SSE events, so batch progress was silently discarded and multi-file processing appeared stuck at 15%. Also improves the processing UX for non-AI (Sharp-based) tools: the progress bar now pulses during the server processing phase and shows a "This may take a moment" hint after 10 seconds. Co-authored-by: Siddharth Kumar Sah <siddharth123sk@gmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Siddharth Kumar Sah
parent
3018137548
commit
dc70cdbdd5
@@ -14,6 +14,7 @@ import { db, schema } from "../db/index.js";
|
|||||||
|
|
||||||
export interface JobProgress {
|
export interface JobProgress {
|
||||||
jobId: string;
|
jobId: string;
|
||||||
|
type?: "batch";
|
||||||
status: "processing" | "completed" | "failed";
|
status: "processing" | "completed" | "failed";
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
completedFiles: number;
|
completedFiles: number;
|
||||||
@@ -161,11 +162,13 @@ export function recoverStaleJobs(): void {
|
|||||||
export function updateJobProgress(progress: JobProgress): void {
|
export function updateJobProgress(progress: JobProgress): void {
|
||||||
jobProgressStore.set(progress.jobId, progress);
|
jobProgressStore.set(progress.jobId, progress);
|
||||||
persistJobProgress(progress);
|
persistJobProgress(progress);
|
||||||
// Notify all SSE listeners
|
// Notify all SSE listeners (add type: "batch" so the frontend can distinguish
|
||||||
|
// batch events from single-file events in the shared SSE stream)
|
||||||
const subs = listeners.get(progress.jobId);
|
const subs = listeners.get(progress.jobId);
|
||||||
if (subs) {
|
if (subs) {
|
||||||
|
const event = { ...progress, type: "batch" } as JobProgress & { type: "batch" };
|
||||||
for (const cb of subs) {
|
for (const cb of subs) {
|
||||||
cb(progress);
|
cb(event);
|
||||||
}
|
}
|
||||||
// If the job is done, clean up listeners after a brief delay
|
// If the job is done, clean up listeners after a brief delay
|
||||||
if (progress.status === "completed" || progress.status === "failed") {
|
if (progress.status === "completed" || progress.status === "failed") {
|
||||||
@@ -218,7 +221,7 @@ export async function registerProgressRoutes(app: FastifyInstance): Promise<void
|
|||||||
// If the job already has progress, send it immediately
|
// If the job already has progress, send it immediately
|
||||||
const existing = jobProgressStore.get(jobId);
|
const existing = jobProgressStore.get(jobId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
sendEvent(existing);
|
sendEvent({ ...existing, type: "batch" });
|
||||||
if (existing.status === "completed" || existing.status === "failed") {
|
if (existing.status === "completed" || existing.status === "failed") {
|
||||||
reply.raw.end();
|
reply.raw.end();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ interface ProgressCardProps {
|
|||||||
export function ProgressCard({ active, phase, label, stage, percent, elapsed }: ProgressCardProps) {
|
export function ProgressCard({ active, phase, label, stage, percent, elapsed }: ProgressCardProps) {
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
|
|
||||||
|
// No real-time server progress: non-AI tools sit at 100% while the server works
|
||||||
|
const isIndeterminate = phase === "processing" && percent >= 100;
|
||||||
|
|
||||||
const icon =
|
const icon =
|
||||||
phase === "uploading" ? (
|
phase === "uploading" ? (
|
||||||
<Upload className="h-4 w-4 text-primary" />
|
<Upload className="h-4 w-4 text-primary" />
|
||||||
@@ -19,7 +22,8 @@ export function ProgressCard({ active, phase, label, stage, percent, elapsed }:
|
|||||||
<Loader2 className="h-4 w-4 text-primary animate-spin" />
|
<Loader2 className="h-4 w-4 text-primary animate-spin" />
|
||||||
);
|
);
|
||||||
|
|
||||||
const sublabel = [stage, `${elapsed}s`].filter(Boolean).join(" \u00b7 ");
|
const slowHint = phase === "processing" && elapsed >= 10 ? "This may take a moment" : undefined;
|
||||||
|
const sublabel = [stage, slowHint, `${elapsed}s`].filter(Boolean).join(" \u00b7 ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted/80 border border-border rounded-xl p-3 space-y-2.5">
|
<div className="bg-muted/80 border border-border rounded-xl p-3 space-y-2.5">
|
||||||
@@ -37,7 +41,7 @@ export function ProgressCard({ active, phase, label, stage, percent, elapsed }:
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full h-1 bg-muted rounded-full overflow-hidden">
|
<div className="w-full h-1 bg-muted rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary rounded-full transition-all duration-500 ease-out"
|
className={`h-full bg-primary rounded-full transition-all duration-500 ease-out ${isIndeterminate ? "animate-pulse" : ""}`}
|
||||||
style={{ width: `${Math.min(100, percent)}%` }}
|
style={{ width: `${Math.min(100, percent)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user