From 421b78695320dbb2e88070275b6fb2d55d0fbaf5 Mon Sep 17 00:00:00 2001 From: kitos Date: Fri, 5 Jun 2026 13:08:55 +0200 Subject: [PATCH] feat(rt-import): add Image to Base64 converter utility New drag-and-drop section at the bottom of the Import RT page so operators can convert screenshots to base64 without leaving the page. Includes thumbnail preview, copy-base64 and copy-JSON-snippet buttons with 2s feedback, per-image delete and clear-all. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/ImportRTPage.tsx | 189 +++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/ImportRTPage.tsx b/frontend/src/pages/ImportRTPage.tsx index 7f00399..6dd1b5c 100644 --- a/frontend/src/pages/ImportRTPage.tsx +++ b/frontend/src/pages/ImportRTPage.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useCallback } from "react"; import { useMutation } from "@tanstack/react-query"; import { Upload, @@ -10,6 +10,11 @@ import { Loader2, ChevronDown, ChevronUp, + ImageIcon, + Copy, + Check, + Trash2, + Braces, } from "lucide-react"; import { importRT, type RTImportPayload, type RTTechniqueEntry, type RTEvidenceEntry } from "../api/tests"; @@ -438,6 +443,188 @@ export default function ImportRTPage() { {(importMutation.error as Error)?.message || "Import failed"} )} + + {/* ── Base64 Image Converter ─────────────────────────────────── */} + + + ); +} + +/* ── Base64 Converter Component ────────────────────────────────────── */ + +interface ConvertedImage { + id: string; + filename: string; + dataUrl: string; // data:image/xxx;base64,... — used for preview + b64: string; // raw base64 data — used in the JSON evidence field + sizeKb: number; +} + +function Base64Converter() { + const [images, setImages] = useState([]); + const [dragging, setDragging] = useState(false); + const [copiedId, setCopiedId] = useState(null); + const inputRef = useRef(null); + + const processFiles = useCallback((files: FileList | File[]) => { + const fileArr = Array.from(files); + const allowed = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp"]; + fileArr.forEach((file) => { + if (!allowed.includes(file.type)) return; + const reader = new FileReader(); + reader.onload = (e) => { + const dataUrl = e.target?.result as string; + // dataUrl = "data:image/png;base64,iVBORw..." + const b64 = dataUrl.split(",")[1] ?? ""; + setImages((prev) => [ + ...prev, + { + id: crypto.randomUUID(), + filename: file.name, + dataUrl, + b64, + sizeKb: Math.round(file.size / 1024), + }, + ]); + }; + reader.readAsDataURL(file); + }); + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setDragging(false); + if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files); + }, + [processFiles], + ); + + const copyToClipboard = (text: string, id: string) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); + }); + }; + + const jsonSnippet = (img: ConvertedImage) => + JSON.stringify({ filename: img.filename, data: img.b64, caption: "" }, null, 2); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Image → Base64 Converter

+

+ Drop screenshots here to get their base64 — paste directly into your JSON +

+
+
+ {images.length > 0 && ( + + )} +
+ + {/* Drop zone */} +
{ e.preventDefault(); setDragging(true); }} + onDragLeave={() => setDragging(false)} + onDrop={handleDrop} + onClick={() => inputRef.current?.click()} + className={`flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed py-8 transition-colors ${ + dragging + ? "border-purple-400 bg-purple-500/10" + : "border-gray-700 hover:border-purple-500/50 hover:bg-purple-500/5" + }`} + > + +

+ Click or drag images here +

+

PNG · JPG · GIF · WebP · BMP

+ { if (e.target.files) { processFiles(e.target.files); e.target.value = ""; } }} + /> +
+ + {/* Converted images */} + {images.length > 0 && ( +
+ {images.map((img) => ( +
+ {/* Thumbnail */} + {img.filename} + + {/* Info + actions */} +
+
+

{img.filename}

+ {img.sizeKb} KB +
+ + {/* base64 preview (truncated) */} +
+

+ {img.b64.slice(0, 80)}… +

+
+ + {/* Copy buttons */} +
+ + + + + +
+
+
+ ))} +
+ )}
); }