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 && ( + setImages([])} + className="flex items-center gap-1.5 rounded-lg border border-gray-700 px-2.5 py-1.5 text-xs text-gray-400 hover:border-red-500/40 hover:text-red-400 transition-colors" + > + + Clear all + + )} + + + {/* 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 */} + + + {/* Info + actions */} + + + {img.filename} + {img.sizeKb} KB + + + {/* base64 preview (truncated) */} + + + {img.b64.slice(0, 80)}… + + + + {/* Copy buttons */} + + copyToClipboard(img.b64, `b64-${img.id}`)} + className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-xs font-medium text-gray-300 hover:border-cyan-500/40 hover:text-cyan-400 transition-colors" + > + {copiedId === `b64-${img.id}` ? ( + <>Copied!> + ) : ( + <>Copy base64> + )} + + + copyToClipboard(jsonSnippet(img), `json-${img.id}`)} + className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-xs font-medium text-gray-300 hover:border-purple-500/40 hover:text-purple-400 transition-colors" + > + {copiedId === `json-${img.id}` ? ( + <>Copied!> + ) : ( + <>Copy JSON snippet> + )} + + + setImages((prev) => prev.filter((im) => im.id !== img.id))} + className="ml-auto rounded-lg border border-gray-700 p-1.5 text-gray-600 hover:border-red-500/40 hover:text-red-400 transition-colors" + > + + + + + + ))} + + )} ); }
+ Drop screenshots here to get their base64 — paste directly into your JSON +
+ Click or drag images here +
PNG · JPG · GIF · WebP · BMP
{img.filename}
+ {img.b64.slice(0, 80)}… +