/* global React, PR */ const { useState, useEffect, useRef, useCallback } = React; const { I, Avatar } = PR; const USER_COLORS = [ "#7C8BA1","#A37BC8","#5BA89E","#C8895B", "#5B7BC8","#B85B7B","#5BA860","#C8B45B", ]; // ── Shared helpers ───────────────────────────────────────────────────────── async function api(url, opts = {}) { const resp = await fetch(url, { headers: { "Content-Type": "application/json" }, ...opts, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } return resp.json(); } // ── Cover image picker ──────────────────────────────────────────────────── function CoverImagePicker({ value, onChange }) { const fileInputRef = useRef(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [dragOver, setDragOver] = useState(false); const uploadFile = async (file) => { if (!file || !file.type.startsWith("image/")) { setError("Only images are allowed"); return; } setUploading(true); setError(null); try { const form = new FormData(); form.append("file", file); const resp = await fetch("/api/upload", { method: "POST", body: form }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || "Upload failed"); } const { url } = await resp.json(); onChange(url); } catch (e) { setError(e.message); } finally { setUploading(false); } }; const handlePaste = useCallback((e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith("image/")) { e.preventDefault(); uploadFile(item.getAsFile()); return; } } }, []); useEffect(() => { document.addEventListener("paste", handlePaste); return () => document.removeEventListener("paste", handlePaste); }, [handlePaste]); const handleDrop = (e) => { e.preventDefault(); setDragOver(false); const file = e.dataTransfer.files[0]; if (file) uploadFile(file); }; return (
{value ? (
Cover { e.target.style.display = "none"; }} />
) : (
!uploading && fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} > {uploading ? ( Uploading… ) : ( <> {I.plus} Click to upload · drag & drop · Ctrl+V to paste )}
)} { const f = e.target.files[0]; if (f) uploadFile(f); e.target.value = ""; }} /> {error &&
{error}
}
); } // ── Overlay ──────────────────────────────────────────────────────────────── function Overlay({ onClose, children, wide }) { const ref = useRef(null); useEffect(() => { const handler = (e) => e.key === "Escape" && onClose(); document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [onClose]); return (
e.target === ref.current && onClose()} ref={ref} >
{children}
); } function ModalHead({ title, onClose }) { return (

{title}

); } function ModalFoot({ onClose, submitLabel, submitting, danger }) { return (
); } function ErrorBanner({ msg }) { if (!msg) return null; return (
{msg}
); } // ── Tag selector ─────────────────────────────────────────────────────────── function TagSelector({ selected, options, onChange }) { const toggle = (tag) => onChange(selected.includes(tag) ? selected.filter(t => t !== tag) : [...selected, tag]); return (
{selected.map(t => ( {t} toggle(t)}>× ))} {options.filter(t => !selected.includes(t)).map(t => ( toggle(t)}>{t} ))}
); } // ── New Hypothesis modal ─────────────────────────────────────────────────── function NewHypothesisModal({ data, user, onClose, onSuccess }) { const [form, setForm] = useState({ title: "", core_loop: "", tags: [], key_metric: "", target: "", risk: "med", summary: "", cover_url: "", author_id: user?.id || "", }); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const submit = async (e) => { e.preventDefault(); if (!form.title.trim()) { setError("Title is required"); return; } setSubmitting(true); setError(null); try { const h = await api("/api/hypotheses", { method: "POST", body: JSON.stringify({ ...form, author_id: parseInt(form.author_id) || user?.id }), }); onSuccess(h.id); } catch (err) { setError(err.message); setSubmitting(false); } }; return (
set("title", e.target.value)} placeholder="e.g. Merge Dungeon — auto-battle variant" autoFocus />