/* global React, Icon, SOPAI, SOPStore */ /* Editor — left panel. Inputs that drive the doc state. */ const { useState: useStateE, useRef: useRefE, useCallback: useCallbackE } = React; const { uid: uidE } = window.SOPStore; /* ============ Small reusable bits ============ */ function Field({ label, hint, children }) { return (
{label && } {children} {hint &&
{hint}
}
); } function SectionShell({ num, title, icon, defaultOpen = true, children }) { const [open, setOpen] = useStateE(defaultOpen); return (
setOpen(o => !o)}> {num != null && {num}} {icon && !num && {icon}} {title}
{children}
); } /* ============ EDITOR ROOT ============ */ function Editor({ doc, onChange, onToast }) { // --- helpers const upd = (patch) => onChange({ ...doc, ...patch, updatedAt: Date.now() }); /* ===== AI structurer ===== */ const [rawNotes, setRawNotes] = useStateE(""); const [aiBusy, setAiBusy] = useStateE(false); const [aiError, setAiError] = useStateE(""); // Imágenes adjuntas al prompt (solo Codex las procesa). const [aiImages, setAiImages] = useStateE([]); const aiFileInputRef = useRefE(null); const AI_MAX_IMGS = 6; const AI_MAX_BYTES = 8 * 1024 * 1024; const readAsDataUrl = (file) => new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res({ id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`, name: file.name, size: file.size, dataUrl: r.result, }); r.onerror = () => rej(new Error("No pude leer la imagen")); r.readAsDataURL(file); }); const addAIImages = async (filesLike) => { if (!filesLike || !filesLike.length) return; const files = Array.from(filesLike).filter(f => f.type && f.type.startsWith("image/")); if (!files.length) { setAiError("Solo imágenes (PNG, JPG, etc)."); return; } if (files.some(f => f.size > AI_MAX_BYTES)) { setAiError("Hay una imagen > 8 MB. Reducila antes."); return; } if (aiImages.length + files.length > AI_MAX_IMGS) { setAiError(`Máximo ${AI_MAX_IMGS} imágenes por prompt.`); return; } try { const descs = await Promise.all(files.map(readAsDataUrl)); setAiImages(prev => [...prev, ...descs]); setAiError(""); } catch (e) { setAiError(e.message || "Error leyendo imagen"); } }; const removeAIImage = (id) => setAiImages(prev => prev.filter(im => im.id !== id)); const runAI = async () => { setAiError(""); if (!rawNotes.trim() && aiImages.length === 0) { setAiError("Pega notas o adjuntá capturas."); return; } if (aiImages.length > 0 && (window.__SOP_PROVIDER || "codex") !== "codex") { setAiError("Para procesar imágenes cambiá a Codex en el toggle del header."); return; } setAiBusy(true); try { const imgUrls = aiImages.map(im => im.dataUrl); const text = rawNotes || "Genera un SOP a partir de las capturas adjuntas."; const structured = await window.SOPAI.structure(text, doc, imgUrls); onChange(structured); onToast && onToast("Estructurado por IA ✓"); setRawNotes(""); setAiImages([]); } catch (e) { setAiError(e.message || "Algo salió mal."); } finally { setAiBusy(false); } }; /* ===== Roles / Resources / Definitions / References — generic list helpers ===== */ const addRow = (key) => upd({ [key]: [...(doc[key] || []), { id: uidE(), name: "", desc: "", term: "", meaning: "" }] }); const updRow = (key, id, patch) => upd({ [key]: doc[key].map(r => r.id === id ? { ...r, ...patch } : r) }); const delRow = (key, id) => upd({ [key]: doc[key].filter(r => r.id !== id) }); /* ===== Flow ===== */ // Guards defensivos: la IA puede devolver doc sin alguna key — sin guard // el editor crasheaba (.map de undefined). const updFlow = (i, v) => upd({ flow: (doc.flow || []).map((s, idx) => idx === i ? v : s) }); const addFlow = () => upd({ flow: [...(doc.flow || []), ""] }); const delFlow = (i) => upd({ flow: (doc.flow || []).filter((_, idx) => idx !== i) }); /* ===== Phases ===== */ const updPhase = (id, patch) => upd({ phases: (doc.phases || []).map(p => p.id === id ? { ...p, ...patch } : p) }); const addPhase = () => { const n = (doc.phases?.length || 0) + 1; upd({ phases: [...(doc.phases || []), { id: uidE(), num: n, title: `Fase ${n}`, steps: [{ id: uidE(), num: `${n}.1`, title: "", body: "", time: "", done: false, image: "", substeps: [] }] }] }); }; const delPhase = (id) => upd({ phases: (doc.phases || []).filter(p => p.id !== id) }); /* ===== Steps within a phase ===== */ const updStep = (phaseId, stepId, patch) => { const ph = (doc.phases || []).find(p => p.id === phaseId); if (!ph) return; updPhase(phaseId, { steps: (ph.steps || []).map(s => s.id === stepId ? { ...s, ...patch } : s), }); }; const addStep = (phaseId) => { const ph = (doc.phases || []).find(p => p.id === phaseId); if (!ph) return; const next = (ph.steps?.length || 0) + 1; updPhase(phaseId, { steps: [...(ph.steps || []), { id: uidE(), num: `${ph.num}.${next}`, title: "", body: "", time: "", done: false, image: "", substeps: [] }] }); }; const delStep = (phaseId, stepId) => { const ph = (doc.phases || []).find(p => p.id === phaseId); if (!ph) return; updPhase(phaseId, { steps: (ph.steps || []).filter(s => s.id !== stepId) }); }; /* ===== Substeps ===== */ const addSubstep = (phaseId, stepId) => { const ph = (doc.phases || []).find(p => p.id === phaseId); const st = ph?.steps?.find(s => s.id === stepId); if (!st) return; const next = String.fromCharCode(97 + (st.substeps?.length || 0)); updStep(phaseId, stepId, { substeps: [...(st.substeps || []), { id: uidE(), num: next, text: "" }] }); }; const updSubstep = (phaseId, stepId, ssId, patch) => { const ph = (doc.phases || []).find(p => p.id === phaseId); const st = ph?.steps?.find(s => s.id === stepId); if (!st) return; updStep(phaseId, stepId, { substeps: (st.substeps || []).map(ss => ss.id === ssId ? { ...ss, ...patch } : ss) }); }; const delSubstep = (phaseId, stepId, ssId) => { const ph = (doc.phases || []).find(p => p.id === phaseId); const st = ph?.steps?.find(s => s.id === stepId); if (!st) return; updStep(phaseId, stepId, { substeps: (st.substeps || []).filter(ss => ss.id !== ssId) }); }; /* ===== Image upload (per step) ===== */ const onImageUpload = (phaseId, stepId, file) => { if (!file) return; const reader = new FileReader(); reader.onload = (e) => updStep(phaseId, stepId, { image: e.target.result }); reader.readAsDataURL(file); }; /* ============ RENDER ============ */ return (

Editor del SOP

Rellena cada sección. El documento de la derecha se actualiza en vivo. ¿Tienes notas en bruto? Pega aquí abajo y pulsa Estructurar con IA.

{/* --- AI bar --- */} } defaultOpen={true}>