import React, { useState, useCallback, useRef, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Upload, File, Trash2, ShieldCheck, AlertTriangle, Image as ImageIcon } from "lucide-react"; import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from "@/components/ui"; import { PiiDiffViewer } from "@/components/PiiDiffViewer"; import { useSessionStore } from "@/stores/sessionStore"; import { uploadLogFileCmd, detectPiiCmd, uploadImageAttachmentCmd, uploadPasteImageCmd, uploadFileToDatastoreCmd, listImageAttachmentsCmd, deleteImageAttachmentCmd, type LogFile, type PiiSpan, type PiiDetectionResult, type ImageAttachment, } from "@/lib/tauriCommands"; import ImageGallery from "@/components/ImageGallery"; import { open } from "@tauri-apps/plugin-dialog"; export default function LogUpload() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { piiSpans, approvedRedactions, setPiiSpans, setApprovedRedactions } = useSessionStore(); const [files, setFiles] = useState<{ file: File; uploaded?: LogFile }[]>([]); const [images, setImages] = useState([]); const [piiResult, setPiiResult] = useState(null); const [isUploading, setIsUploading] = useState(false); const [isDetecting, setIsDetecting] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); const droppedFiles = Array.from(e.dataTransfer.files); setFiles((prev) => [...prev, ...droppedFiles.map((f) => ({ file: f }))]); }, [] ); const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { const selected = Array.from(e.target.files); setFiles((prev) => [...prev, ...selected.map((f) => ({ file: f }))]); } }; const removeFile = (index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }; const handleUpload = async () => { if (!id || files.length === 0) return; setIsUploading(true); setError(null); try { const uploaded = await Promise.all( files.map(async (entry) => { if (entry.uploaded) return entry; const content = await entry.file.text(); const logFile = await uploadLogFileCmd(id, entry.file.name); return { ...entry, uploaded: logFile }; }) ); setFiles(uploaded); } catch (err) { setError(String(err)); } finally { setIsUploading(false); } }; const handleDetectPii = async () => { const allContent = files .map((f) => f.uploaded?.file_name) .filter(Boolean) .join("\n---\n"); if (!allContent) return; setIsDetecting(true); setError(null); try { const result = files[0]?.uploaded ? await detectPiiCmd(files[0].uploaded.id) : null; setPiiResult(result); if (result) { setPiiSpans(result.detections); setApprovedRedactions(result.detections); }; } catch (err) { setError(String(err)); } finally { setIsDetecting(false); } }; const handleToggleSpan = (span: PiiSpan, approved: boolean) => { if (approved) { setApprovedRedactions([...approvedRedactions, span]); } else { setApprovedRedactions( approvedRedactions.filter( (s) => !(s.start === span.start && s.end === span.end) ) ); } }; const handleImageDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); // Use file dialog to get actual paths (drag-and-drop doesn't give paths in Tauri v2) try { const selected = await open({ multiple: true, filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }], }); if (!selected) return; const paths = Array.isArray(selected) ? selected : [selected]; if (paths.length > 0) { handleImagesUpload(paths as unknown as File[]); } } catch (err) { setError(`Attachment failed: ${String(err)}`); } }, [id] ); const handleImageFileSelect = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { try { const selected = await open({ multiple: true, filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }], }); if (selected) { const paths = Array.isArray(selected) ? selected : [selected]; if (paths.length > 0) { handleImagesUpload(paths as unknown as File[]); } } } catch (err) { setError(`Attachment failed: ${String(err)}`); } } }; const handlePaste = useCallback( async (e: React.ClipboardEvent) => { const items = e.clipboardData?.items; const imageItems = items ? Array.from(items).filter((item: DataTransferItem) => item.type.startsWith("image/")) : []; for (const item of imageItems) { const file = item.getAsFile(); if (file) { const reader = new FileReader(); reader.onload = async () => { const base64Data = reader.result as string; try { const result = await uploadPasteImageCmd(id || "", base64Data, file.type); setImages((prev) => [...prev, result]); } catch (err) { setError(String(err)); } }; reader.readAsDataURL(file); } } }, [id] ); const handleImagesUpload = async (imageFiles: File[]) => { if (!id || imageFiles.length === 0) return; setIsUploading(true); setError(null); try { const uploaded: ImageAttachment[] = await Promise.all( imageFiles.map(async (file) => { // Extract the path from the file name (which contains the full path from file dialog) const filePath = file.name; // Get just the filename for display const fileName = filePath.split(/[\/\\]/).pop() || "unknown"; // Use uploadLogFileCmd which properly handles file paths const result = await uploadLogFileCmd(id, filePath); // Convert LogFile to ImageAttachment for the state return { id: result.id, issue_id: result.issue_id, file_name: result.file_name, file_path: result.file_path, file_size: result.file_size, mime_type: result.mime_type, upload_hash: result.content_hash, uploaded_at: result.uploaded_at, pii_warning_acknowledged: true, is_paste: false, }; }) ); setImages((prev) => [...prev, ...uploaded]); } catch (err) { setError(String(err)); } finally { setIsUploading(false); } }; const handleDeleteImage = async (image: ImageAttachment) => { try { await deleteImageAttachmentCmd(image.id); setImages((prev) => prev.filter((img) => img.id !== image.id)); } catch (err) { setError(String(err)); } }; const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = (err) => reject(err); reader.readAsDataURL(file); }); }; const allUploaded = files.length > 0 && files.every((f) => f.uploaded); const piiReviewed = piiResult != null; useEffect(() => { const handleGlobalPaste = (e: ClipboardEvent) => { if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA" || (document.activeElement as HTMLElement)?.isContentEditable || false) { return; } const items = e.clipboardData?.items; const imageItems = items ? Array.from(items).filter((item: DataTransferItem) => item.type.startsWith("image/")) : []; for (const item of imageItems) { const file = item.getAsFile(); if (file) { e.preventDefault(); const reader = new FileReader(); reader.onload = async () => { const base64Data = reader.result as string; try { const result = await uploadPasteImageCmd(id || "", base64Data, file.type); setImages((prev) => [...prev, result]); } catch (err) { setError(String(err)); } }; reader.readAsDataURL(file); break; } } }; window.addEventListener("paste", handleGlobalPaste); return () => window.removeEventListener("paste", handleGlobalPaste); }, [id]); useEffect(() => { if (id) { listImageAttachmentsCmd(id).then(setImages).catch(setError); } }, [id]); return (

Upload Logs

Upload log files for PII detection and redaction before triage.

{/* Drop zone */}
e.preventDefault()} onDrop={handleDrop} className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary transition-colors cursor-pointer" onClick={() => document.getElementById("file-input")?.click()} >

Drag and drop log files here, or click to browse

{/* File list */} {files.length > 0 && ( Files ({files.length}) {files.map((entry, idx) => (
{entry.file.name} ({(entry.file.size / 1024).toFixed(1)} KB) {entry.uploaded && ( Uploaded )}
))} {!allUploaded && ( )}
)} {/* Image Upload */} {id && ( <>

Image Attachments

Upload or paste screenshots and images.

{/* Image drop zone */}
e.preventDefault()} onDrop={handleImageDrop} className="border-2 border-dashed border-primary/30 rounded-lg p-8 text-center hover:border-primary transition-colors cursor-pointer bg-primary/5" onClick={() => document.getElementById("image-input")?.click()} >

Drag and drop images here, or click to browse

Supported: PNG, JPEG, GIF, WebP, SVG

{/* Paste button */}
Use Ctrl+V / Cmd+V or the button above to paste images
{/* PII warning for images */}
⚠️ PII cannot be automatically redacted from images. Use at your own risk.
{/* Image Gallery */} {images.length > 0 && ( Attached Images ({images.length}) )} )} {/* PII Detection */} {allUploaded && ( PII Detection {!piiResult && ( )} {piiResult && ( )} )} {/* Error */} {error && (
{error}
)} {/* Continue */}
); }