import { ArrowLeftIcon, ArrowRightIcon, ChevronDownIcon, Cross2Icon, ExclamationTriangleIcon, ImageIcon, InfoCircledIcon, ReloadIcon, TrashIcon, UploadIcon, } from "@radix-ui/react-icons"; import { Badge, Box, Button, Callout, Card, Code, Flex, Heading, IconButton, Spinner, Text, } from "@radix-ui/themes"; import { AlertDialog, Collapsible, Dialog } from "radix-ui"; import { useRef, useState } from "react"; import { imageDropClient } from "./client.js"; import { canMutate, draftPresentation, formatBytes, summarizeBatch, summarizeHistory, visibleItemNotes, } from "./state.js"; import type { ConnectionFailure, ImageDropState, PublicBatchItem, PublicHistoryItem, } from "./types.js"; export const ACCEPTED_IMAGES = "image/png,image/jpeg,image/webp,image/gif,image/bmp,image/tiff,image/heic,image/heif,image/avif,.bmp,.tif,.tiff,.heic,.heif,.avif"; export interface PreviewSelection { collection: "items" | "history"; item: PublicBatchItem | PublicHistoryItem; } export function SessionHeader({ state }: { state?: ImageDropState }) { return (
Pi local image staging Image Drop {state ? state.sessionName ? `${state.projectName} · ${state.sessionName}` : state.projectName : "Connecting to Pi…"} Working directory {state?.cwd ?? "—"}
); } export function LoadingState() { return ( Connecting to this Pi session… ); } export function DropZone({ dragActive, mutable, onFiles, }: { dragActive: boolean; mutable: boolean; onFiles: (files: FileList | null) => void; }) { const inputRef = useRef(null); return (
); } export function DraftSection({ error, highlightedId, onPreview, state, }: { error: string; highlightedId?: string; onPreview: (selection: PreviewSelection, trigger: HTMLElement) => void; state: ImageDropState; }) { const summary = summarizeBatch(state.batch); const presentation = draftPresentation(state.batch); const mutable = canMutate(state.batch); return (
Ready for next message {presentation.status && ( {presentation.status} )} {presentation.guidance} {summary.total > 0 && }
{error && ( {error} )}
); } function DraftGrid({ highlightedId, mutable, onPreview, state, }: { highlightedId?: string; mutable: boolean; onPreview: (selection: PreviewSelection, trigger: HTMLElement) => void; state: ImageDropState; }) { const [draggedId, setDraggedId] = useState(""); if (state.batch.items.length === 0) return null; return (
    {state.batch.items.map((item, index) => (
  1. setDraggedId("")} onDragOver={(event) => { if (!mutable || !draggedId) return; event.preventDefault(); }} onDragStart={() => setDraggedId(item.id)} onDrop={(event) => { event.preventDefault(); if (draggedId) void imageDropClient.moveBefore(draggedId, item.id); setDraggedId(""); }} > void imageDropClient.move(item.id, -1)} title="Move backward" type="button" variant="soft" > void imageDropClient.move(item.id, 1)} title="Move forward" type="button" variant="soft" > {item.status === "error" && ( )}
  2. ))}
); } function DraftPreview({ item, onPreview, revision, }: { item: PublicBatchItem; onPreview: (selection: PreviewSelection, trigger: HTMLElement) => void; revision: number; }) { if (item.status !== "ready") { return (
); } return ( onPreview({ collection: "items", item }, event.currentTarget)} type="button" variant="ghost" > {`Preview ); } function ImageDetails({ index, item }: { index: number; item: PublicBatchItem }) { const dimensions = item.width && item.height ? ` · ${item.width}×${item.height}` : ""; return ( {item.name} {`${index + 1} · ${formatBytes(item.size)}${dimensions}`} {item.status} {item.sourceFormat && item.sourceFormat !== item.outputFormat && ( {`${item.sourceFormat.toUpperCase()} → ${item.outputFormat?.toUpperCase()}`} )} {visibleItemNotes(item.notes).map((note) => ( {note} ))} {item.error && ( {item.error} )} ); } export function HistorySection({ onPreview, state, }: { onPreview: (selection: PreviewSelection, trigger: HTMLElement) => void; state: ImageDropState; }) { const history = summarizeHistory(state.history); const mutable = canMutate(state.batch); return (
Previously sent {history.label} {history.usage}. Images here are not attached again unless you choose Add again. The oldest are removed at the configured memory limit, and all are cleared when this Pi session ends. {history.total > 0 && }
{history.total > 0 && (
    {state.history.items.map((item, index) => (
  1. onPreview({ collection: "history", item }, event.currentTarget) } type="button" variant="ghost" > {`Preview {item.name} {`${index + 1} · ${formatBytes(item.size)} · ${item.width}×${item.height}`} {visibleItemNotes(item.notes).map((note) => ( {note} ))}
  2. ))}
)}
); } function ClearDraftDialog({ disabled }: { disabled: boolean }) { return ( Clear every staged image? This removes the current batch from Pi memory. ); } function ClearHistoryDialog() { return ( Clear sent image history? This releases every retained image from this Pi session. Images already sent to Pi or a model provider are unaffected. ); } export function ImagePreviewDialog({ onOpenChange, open, revision, selection, }: { onOpenChange: (open: boolean) => void; open: boolean; revision: number; selection?: PreviewSelection; }) { const item = selection?.item; return ( {item?.name ?? "Image preview"} {selection && item && ( )} ); } export function PrivacyNotice() { return ( Draft and previously sent images stay in this Pi process until removed, evicted at the retention limit, or the session ends. Sending a Pi message sends its staged images to your configured model provider; deleting local history does not retract images already sent. ); } export function ConnectionBlocker({ failure }: { failure?: ConnectionFailure }) { return ( event.preventDefault()} onPointerDownOutside={(event) => event.preventDefault()} > {failure?.title ?? "Connection lost"} {failure?.message ?? "Run /image-drop in Pi for a new link."} ); } export function MetadataNotice() { return ( Sensitive image metadata removed from processed images. ); }