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 { type DragEvent, useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "@/contexts/i18n-context";
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import { useUrlImport } from "@/hooks/use-url-import";
|
import { useUrlImport } from "@/hooks/use-url-import";
|
||||||
@@ -116,11 +116,19 @@ export function Dropzone({
|
|||||||
const checkFile = fileFilter ?? isImageFile;
|
const checkFile = fileFilter ?? isImageFile;
|
||||||
const resolvedAccept = expandAccept(accept);
|
const resolvedAccept = expandAccept(accept);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [urlInput, setUrlInput] = useState("");
|
const [urlInput, setUrlInput] = useState("");
|
||||||
const [urlLoading, setUrlLoading] = useState(false);
|
const [urlLoading, setUrlLoading] = useState(false);
|
||||||
const [urlError, setUrlError] = useState<string | null>(null);
|
const [urlError, setUrlError] = useState<string | null>(null);
|
||||||
const [showBulkModal, setShowBulkModal] = useState(false);
|
const [showBulkModal, setShowBulkModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
const timer = setTimeout(() => setError(null), 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
const { importSingleUrl } = useUrlImport();
|
const { importSingleUrl } = useUrlImport();
|
||||||
|
|
||||||
const handleUrlSubmit = useCallback(async () => {
|
const handleUrlSubmit = useCallback(async () => {
|
||||||
@@ -154,20 +162,32 @@ export function Dropzone({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
const files = Array.from(e.dataTransfer.files).filter(checkFile);
|
setError(null);
|
||||||
if (files.length > 0) onFiles?.(files);
|
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 = () => {
|
const handleClick = () => {
|
||||||
|
setError(null);
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.multiple = multiple;
|
input.multiple = multiple;
|
||||||
if (resolvedAccept) input.accept = resolvedAccept;
|
if (resolvedAccept) input.accept = resolvedAccept;
|
||||||
input.onchange = (e) => {
|
input.onchange = (e) => {
|
||||||
const files = Array.from((e.target as HTMLInputElement).files || []).filter(checkFile);
|
const picked = Array.from((e.target as HTMLInputElement).files || []);
|
||||||
if (files.length > 0) onFiles?.(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();
|
input.click();
|
||||||
};
|
};
|
||||||
@@ -260,6 +280,13 @@ export function Dropzone({
|
|||||||
{acceptDescription ?? t.dropzone.defaultFormats}
|
{acceptDescription ?? t.dropzone.defaultFormats}
|
||||||
</p>
|
</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 && (
|
{!compact && onUrlImport && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 w-full max-w-xs">
|
<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 { MODALITIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -617,10 +618,30 @@ export function ToolPage() {
|
|||||||
// which also match !hasProcessed and would show the canvas instead of the error)
|
// which also match !hasProcessed and would show the canvas instead of the error)
|
||||||
if (hasFile && !hasProcessed && currentEntry?.status === "failed") {
|
if (hasFile && !hasProcessed && currentEntry?.status === "failed") {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-3 h-full text-center px-4">
|
<div className="flex-1 flex items-center justify-center p-6">
|
||||||
<p className="text-sm text-red-500">
|
<div className="text-center max-w-sm">
|
||||||
{currentEntry.error ?? t.toolPage.processingFailed}
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2453,6 +2453,9 @@ export const ar: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "تم حفظ إعداداتك.",
|
||||||
|
tryAgain: "حاول مرة أخرى",
|
||||||
|
tryDifferentFile: "جرب ملفًا مختلفًا",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "جاري إنشاء المعاينة...",
|
generatingPreview: "جاري إنشاء المعاينة...",
|
||||||
|
|||||||
@@ -2467,6 +2467,9 @@ export const de: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Ihre Einstellungen wurden gespeichert.",
|
||||||
|
tryAgain: "Erneut versuchen",
|
||||||
|
tryDifferentFile: "Andere Datei versuchen",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Vorschau wird generiert...",
|
generatingPreview: "Vorschau wird generiert...",
|
||||||
|
|||||||
@@ -2417,6 +2417,9 @@ export const en = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Your settings are saved.",
|
||||||
|
tryAgain: "Try again",
|
||||||
|
tryDifferentFile: "Try a different file",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Generating preview...",
|
generatingPreview: "Generating preview...",
|
||||||
|
|||||||
@@ -2449,6 +2449,9 @@ export const es: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Tu configuracion se ha guardado.",
|
||||||
|
tryAgain: "Reintentar",
|
||||||
|
tryDifferentFile: "Probar con otro archivo",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Generando vista previa...",
|
generatingPreview: "Generando vista previa...",
|
||||||
|
|||||||
@@ -2468,6 +2468,9 @@ export const fr: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Vos parametres sont enregistres.",
|
||||||
|
tryAgain: "Reessayer",
|
||||||
|
tryDifferentFile: "Essayer un autre fichier",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Generation de l'apercu...",
|
generatingPreview: "Generation de l'apercu...",
|
||||||
|
|||||||
@@ -2450,6 +2450,9 @@ export const hi: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "आपकी सेटिंग्स सहेज ली गई हैं।",
|
||||||
|
tryAgain: "पुनः प्रयास करें",
|
||||||
|
tryDifferentFile: "एक अलग फ़ाइल आज़माएं",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "प्रीव्यू जनरेट हो रहा है...",
|
generatingPreview: "प्रीव्यू जनरेट हो रहा है...",
|
||||||
|
|||||||
@@ -2462,6 +2462,9 @@ export const id: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Pengaturan Anda tersimpan.",
|
||||||
|
tryAgain: "Coba lagi",
|
||||||
|
tryDifferentFile: "Coba file lain",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Membuat pratinjau...",
|
generatingPreview: "Membuat pratinjau...",
|
||||||
|
|||||||
@@ -2461,6 +2461,9 @@ export const it: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Le impostazioni sono state salvate.",
|
||||||
|
tryAgain: "Riprova",
|
||||||
|
tryDifferentFile: "Prova un file diverso",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Generazione anteprima...",
|
generatingPreview: "Generazione anteprima...",
|
||||||
|
|||||||
@@ -2420,6 +2420,9 @@ export const ja: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "設定は保存されています。",
|
||||||
|
tryAgain: "再試行",
|
||||||
|
tryDifferentFile: "別のファイルを試す",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "プレビューを生成中...",
|
generatingPreview: "プレビューを生成中...",
|
||||||
|
|||||||
@@ -2405,6 +2405,9 @@ export const ko: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "설정이 저장되었습니다.",
|
||||||
|
tryAgain: "다시 시도",
|
||||||
|
tryDifferentFile: "다른 파일 시도",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "미리보기 생성 중...",
|
generatingPreview: "미리보기 생성 중...",
|
||||||
|
|||||||
@@ -2464,6 +2464,9 @@ export const nl: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Uw instellingen zijn opgeslagen.",
|
||||||
|
tryAgain: "Opnieuw proberen",
|
||||||
|
tryDifferentFile: "Een ander bestand proberen",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Preview genereren...",
|
generatingPreview: "Preview genereren...",
|
||||||
|
|||||||
@@ -2465,6 +2465,9 @@ export const pl: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Twoje ustawienia zostaly zapisane.",
|
||||||
|
tryAgain: "Sprobuj ponownie",
|
||||||
|
tryDifferentFile: "Sprobuj inny plik",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Generowanie podglądu...",
|
generatingPreview: "Generowanie podglądu...",
|
||||||
|
|||||||
@@ -2461,6 +2461,9 @@ export const ptBR: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Suas configuracoes foram salvas.",
|
||||||
|
tryAgain: "Tentar novamente",
|
||||||
|
tryDifferentFile: "Tentar outro arquivo",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Gerando visualizacao...",
|
generatingPreview: "Gerando visualizacao...",
|
||||||
|
|||||||
@@ -2463,6 +2463,9 @@ export const ru: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Ваши настройки сохранены.",
|
||||||
|
tryAgain: "Попробовать снова",
|
||||||
|
tryDifferentFile: "Попробовать другой файл",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Генерация предпросмотра...",
|
generatingPreview: "Генерация предпросмотра...",
|
||||||
|
|||||||
@@ -2460,6 +2460,9 @@ export const sv: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Dina installningar ar sparade.",
|
||||||
|
tryAgain: "Forsok igen",
|
||||||
|
tryDifferentFile: "Prova en annan fil",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Genererar forhandsvisning...",
|
generatingPreview: "Genererar forhandsvisning...",
|
||||||
|
|||||||
@@ -2442,6 +2442,9 @@ export const th: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "บันทึกการตั้งค่าของคุณแล้ว",
|
||||||
|
tryAgain: "ลองอีกครั้ง",
|
||||||
|
tryDifferentFile: "ลองไฟล์อื่น",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "กำลังสร้างตัวอย่าง...",
|
generatingPreview: "กำลังสร้างตัวอย่าง...",
|
||||||
|
|||||||
@@ -2465,6 +2465,9 @@ export const tr: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Ayarlariniz kaydedildi.",
|
||||||
|
tryAgain: "Tekrar dene",
|
||||||
|
tryDifferentFile: "Farkli bir dosya dene",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Önizleme oluşturuluyor...",
|
generatingPreview: "Önizleme oluşturuluyor...",
|
||||||
|
|||||||
@@ -2463,6 +2463,9 @@ export const uk: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Ваші налаштування збережено.",
|
||||||
|
tryAgain: "Спробувати знову",
|
||||||
|
tryDifferentFile: "Спробувати інший файл",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Генерація попереднього перегляду...",
|
generatingPreview: "Генерація попереднього перегляду...",
|
||||||
|
|||||||
@@ -2462,6 +2462,9 @@ export const vi: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "Cai dat cua ban da duoc luu.",
|
||||||
|
tryAgain: "Thu lai",
|
||||||
|
tryDifferentFile: "Thu tep khac",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "Đang tạo bản xem trước...",
|
generatingPreview: "Đang tạo bản xem trước...",
|
||||||
|
|||||||
@@ -2395,6 +2395,9 @@ export const zhCN: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "您的设置已保存。",
|
||||||
|
tryAgain: "重试",
|
||||||
|
tryDifferentFile: "尝试其他文件",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "正在生成预览...",
|
generatingPreview: "正在生成预览...",
|
||||||
|
|||||||
@@ -2393,6 +2393,9 @@ export const zhTW: TranslationKeys = {
|
|||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
noChange: "no change",
|
noChange: "no change",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
|
settingsSaved: "您的設定已儲存。",
|
||||||
|
tryAgain: "重試",
|
||||||
|
tryDifferentFile: "嘗試其他檔案",
|
||||||
},
|
},
|
||||||
homePage: {
|
homePage: {
|
||||||
generatingPreview: "正在產生預覽...",
|
generatingPreview: "正在產生預覽...",
|
||||||
|
|||||||
Reference in New Issue
Block a user