diff --git a/apps/web/src/components/common/review-panel.tsx b/apps/web/src/components/common/review-panel.tsx
index 3f9d5f13..38036e0c 100644
--- a/apps/web/src/components/common/review-panel.tsx
+++ b/apps/web/src/components/common/review-panel.tsx
@@ -1,8 +1,35 @@
-import { ArrowLeft, CheckCircle2, Download } from "lucide-react";
+import { AlertCircle, ArrowLeft, CheckCircle2, Download, FileText } from "lucide-react";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "@/contexts/i18n-context";
import { formatFileSize, triggerDownload } from "@/lib/download";
+import { format } from "@/lib/format";
+
+/** Tools whose primary output is text/data, not a downloadable file. */
+const DATA_OUTPUT_TOOLS = new Set([
+ "ocr",
+ "barcode-read",
+ "info",
+ "histogram",
+ "color-palette",
+ "transcribe-audio",
+ "extract-subtitles",
+ "image-to-base64",
+ "pdf-to-text",
+ "pdf-metadata",
+ "audio-metadata",
+ "video-metadata",
+]);
+
+/** Tools that produce multiple output files bundled as a ZIP. */
+const MULTI_OUTPUT_TOOLS = new Set([
+ "split",
+ "favicon",
+ "pdf-to-image",
+ "video-to-frames",
+ "split-audio",
+ "split-csv",
+]);
interface ReviewPanelProps {
filename: string;
@@ -13,6 +40,9 @@ interface ReviewPanelProps {
onUndo: () => void;
onStartOver: () => void;
currentToolId: string;
+ totalCount?: number;
+ successCount?: number;
+ failedCount?: number;
}
export function ReviewPanel({
@@ -23,10 +53,16 @@ export function ReviewPanel({
downloadUrl,
onUndo,
onStartOver,
- currentToolId: _currentToolId,
+ currentToolId,
+ totalCount,
+ successCount,
+ failedCount,
}: ReviewPanelProps) {
const { t } = useTranslation();
+ const isDataOutput = DATA_OUTPUT_TOOLS.has(currentToolId);
+ const isMultiOutput = MULTI_OUTPUT_TOOLS.has(currentToolId);
+
const sizeDelta = useMemo(() => {
if (!originalSize || originalSize === 0) return 0;
return Math.round((1 - fileSize / originalSize) * 100);
@@ -36,6 +72,9 @@ export function ReviewPanel({
triggerDownload(downloadUrl, filename);
};
+ const hasBatchStats =
+ totalCount != null && totalCount > 1 && successCount != null && failedCount != null;
+
return (
@@ -46,8 +85,22 @@ export function ReviewPanel({
{t.toolPage.conversionComplete}
- {/* Size delta */}
- {originalSize > 0 && (
+ {/* Batch partial failure summary */}
+ {hasBatchStats && failedCount > 0 && (
+
+
+
+ {format(t.toolPage.batchPartialSuccess, {
+ success: successCount,
+ total: totalCount,
+ failed: failedCount,
+ })}
+
+
+ )}
+
+ {/* Size delta -- hidden for data-output tools */}
+ {!isDataOutput && originalSize > 0 && (
{t.toolPage.original}
@@ -78,16 +131,39 @@ export function ReviewPanel({
)}
- {/* Download button with format + size */}
-
+ {/* Data-output tools: results hint + secondary download */}
+ {isDataOutput && (
+ <>
+
+
+ {t.toolPage.dataResultsHint}
+
+
+ >
+ )}
+
+ {/* Download button -- primary for non-data tools */}
+ {!isDataOutput && (
+
+ )}
{/* Adjust settings */}
;
+ case "failed":
+ return
;
+ case "processing":
+ return
;
+ default:
+ return
;
+ }
+}
+
/** File selection indicator shown in left panel */
function FileSelectionInfo({
files,
+ fileEntries,
selectedIndex,
onSelect,
onClear,
onAddMore,
}: {
files: File[];
+ fileEntries: FileEntry[];
selectedIndex: number;
onSelect: (index: number) => void;
onClear: () => void;
@@ -107,6 +125,9 @@ function FileSelectionInfo({
const showToggle = files.length > COLLAPSED_LIMIT;
const visible = expanded ? files : files.slice(0, COLLAPSED_LIMIT);
+ const hasAnyProcessed = fileEntries.some(
+ (e) => e.status === "completed" || e.status === "failed",
+ );
return (
@@ -124,6 +145,7 @@ function FileSelectionInfo({
{visible.map((file, i) => {
const isSelected = i === selectedIndex;
+ const entry = fileEntries[i];
return (