mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: inline error messages with recovery actions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { FileImage, FileUp, Upload } from "lucide-react";
|
||||
import { AlertCircle, FileImage, FileUp, Upload } from "lucide-react";
|
||||
import { type DragEvent, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useUrlImport } from "@/hooks/use-url-import";
|
||||
@@ -116,11 +116,19 @@ export function Dropzone({
|
||||
const checkFile = fileFilter ?? isImageFile;
|
||||
const resolvedAccept = expandAccept(accept);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [urlInput, setUrlInput] = useState("");
|
||||
const [urlLoading, setUrlLoading] = useState(false);
|
||||
const [urlError, setUrlError] = useState<string | null>(null);
|
||||
const [showBulkModal, setShowBulkModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const timer = setTimeout(() => setError(null), 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const { importSingleUrl } = useUrlImport();
|
||||
|
||||
const handleUrlSubmit = useCallback(async () => {
|
||||
@@ -154,20 +162,32 @@ export function Dropzone({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
const files = Array.from(e.dataTransfer.files).filter(checkFile);
|
||||
if (files.length > 0) onFiles?.(files);
|
||||
setError(null);
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
const validFiles = droppedFiles.filter(checkFile);
|
||||
if (validFiles.length > 0) {
|
||||
onFiles?.(validFiles);
|
||||
} else if (droppedFiles.length > 0) {
|
||||
setError(acceptDescription || `This tool accepts ${accept || "image files"}`);
|
||||
}
|
||||
},
|
||||
[onFiles, checkFile],
|
||||
[onFiles, checkFile, acceptDescription, accept],
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
setError(null);
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = multiple;
|
||||
if (resolvedAccept) input.accept = resolvedAccept;
|
||||
input.onchange = (e) => {
|
||||
const files = Array.from((e.target as HTMLInputElement).files || []).filter(checkFile);
|
||||
if (files.length > 0) onFiles?.(files);
|
||||
const picked = Array.from((e.target as HTMLInputElement).files || []);
|
||||
const validFiles = picked.filter(checkFile);
|
||||
if (validFiles.length > 0) {
|
||||
onFiles?.(validFiles);
|
||||
} else if (picked.length > 0) {
|
||||
setError(acceptDescription || `This tool accepts ${accept || "image files"}`);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
@@ -260,6 +280,13 @@ export function Dropzone({
|
||||
{acceptDescription ?? t.dropzone.defaultFormats}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<p className="mt-3 text-sm text-destructive flex items-center gap-1.5">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!compact && onUrlImport && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 w-full max-w-xs">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MODALITIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
@@ -617,10 +618,30 @@ export function ToolPage() {
|
||||
// which also match !hasProcessed and would show the canvas instead of the error)
|
||||
if (hasFile && !hasProcessed && currentEntry?.status === "failed") {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 h-full text-center px-4">
|
||||
<p className="text-sm text-red-500">
|
||||
{currentEntry.error ?? t.toolPage.processingFailed}
|
||||
</p>
|
||||
<div className="flex-1 flex items-center justify-center p-6">
|
||||
<div className="text-center max-w-sm">
|
||||
<AlertCircle className="mx-auto h-10 w-10 text-destructive mb-3" />
|
||||
<p className="font-medium text-foreground mb-1">
|
||||
{currentEntry.error || t.toolPage.processingFailed}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">{t.toolPage.settingsSaved}</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUndo}
|
||||
className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
{t.toolPage.tryAgain}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startOver}
|
||||
className="px-4 py-2 rounded-md text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t.toolPage.tryDifferentFile}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user