/* global React, Icon, MicButton, SOPAI, SOPStore */ /* SimpleEditor — single-input mode. User dictates or types whatever they want and AI does the whole structuring. */ const { useState: useStateSE, useRef: useRefSE, useEffect: useEffectSE } = React; const EXAMPLE_PROMPTS = [ { label: "Onboarding cliente nuevo", text: "Necesito un SOP para el onboarding de un cliente nuevo de Ads&Law. Incluye reunión kickoff, recolección de credenciales (Meta Business, GHL, dominio), setup del píxel, configuración de eventos, briefing de mercado y aprobación de creatividades antes del lanzamiento.", }, { label: "Lanzamiento de campaña Meta", text: "SOP para lanzar una campaña nueva de Meta Ads de un despacho. Quiero: validación del brief, creación de adsets, subida de creatividades, configuración del budget, pruebas de tracking y revisión por el líder antes de poner en LIVE.", }, { label: "Reporte semanal de cliente", text: "SOP del reporte semanal a clientes: descarga de datos de Meta + GHL, cálculo de CPL y leads cualificados, redacción del análisis, generación del PDF con la plantilla, envío por email los lunes antes de las 10:00.", }, { label: "Optimización diaria de adsets", text: "SOP de la rutina diaria de optimización: revisar adsets, identificar los que están sobre el CPL objetivo, pausar los muertos, escalar los ganadores, documentar cambios en Slack del cliente.", }, ]; function SimpleEditor({ doc, onChange, onToast }) { const [text, setText] = useStateSE(""); const [interim, setInterim] = useStateSE(""); const [busy, setBusy] = useStateSE(false); const [error, setError] = useStateSE(""); // images: array de { id, name, dataUrl, size }. dataUrl es lo que mandamos al backend. const [images, setImages] = useStateSE([]); const taRef = useRefSE(null); const fileInputRef = useRefSE(null); const hasContent = !!(doc && (doc.title || (doc.phases && doc.phases.length))); // Append voice transcript to the existing text const onVoice = (t) => { setText(prev => { const sep = prev && !/\s$/.test(prev) ? " " : ""; return (prev + sep + t).trim() + " "; }); setInterim(""); }; // ---- imágenes ---- // Lee el archivo como data URL base64 y devuelve un descriptor para state. const readFileAsDataUrl = (file) => new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve({ id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`, name: file.name, size: file.size, dataUrl: r.result, }); r.onerror = () => reject(new Error("No pude leer la imagen")); r.readAsDataURL(file); }); const MAX_IMG_BYTES = 8 * 1024 * 1024; // 8 MB por imagen const MAX_IMGS = 6; const addFiles = async (filesLike) => { if (!filesLike || !filesLike.length) return; const files = Array.from(filesLike).filter(f => f.type && f.type.startsWith("image/")); if (!files.length) { setError("Solo se aceptan imágenes (PNG, JPG, etc)."); return; } const oversized = files.find(f => f.size > MAX_IMG_BYTES); if (oversized) { setError(`"${oversized.name}" pesa más de 8 MB. Reducila antes de subirla.`); return; } if (images.length + files.length > MAX_IMGS) { setError(`Máximo ${MAX_IMGS} imágenes por prompt.`); return; } try { const descriptors = await Promise.all(files.map(readFileAsDataUrl)); setImages(prev => [...prev, ...descriptors]); setError(""); } catch (e) { setError(e.message || "Error leyendo imagen"); } }; const removeImage = (id) => setImages(prev => prev.filter(im => im.id !== id)); // Drag & drop sobre el textarea const onDrop = (e) => { e.preventDefault(); if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files); }; // Paste: si pegás una imagen (Cmd+V de captura), la sumamos const onPaste = (e) => { const items = e.clipboardData && e.clipboardData.items; if (!items) return; const imgs = []; for (const it of items) { if (it.kind === "file" && it.type.startsWith("image/")) { const f = it.getAsFile(); if (f) imgs.push(f); } } if (imgs.length) { e.preventDefault(); addFiles(imgs); } }; const run = async (mode) => { setError(""); if (!text.trim() && images.length === 0) { setError("Cuéntame qué SOP necesitas — habla, escribe o adjuntá capturas."); return; } // Si hay imágenes pero el provider es Ollama → no se procesan. Aviso. if (images.length > 0 && (window.__SOP_PROVIDER || "codex") !== "codex") { setError("Para procesar imágenes cambiá a Codex en el toggle del header."); return; } // Pedimos confirmación antes de reemplazar un SOP con contenido. if (mode === "create" && hasContent) { const ok = window.confirm( "Esto va a reemplazar el SOP actual por uno nuevo basado en tu texto.\n" + "Si querías solo modificarlo, pulsa Cancelar y usa \"Refinar SOP actual\".\n\n" + "El SOP anterior queda en el historial — podrás recuperarlo desde el botón Historial." ); if (!ok) return; } setBusy(true); try { const imgUrls = images.map(im => im.dataUrl); let next; if (mode === "refine" && hasContent) { next = await window.SOPAI.refine(text, doc, imgUrls); onToast && onToast("SOP actualizado ✓"); } else { next = await window.SOPAI.structure(text || "Genera un SOP a partir de las capturas adjuntas.", doc, imgUrls); onToast && onToast("SOP generado ✓"); } onChange(next); setText(""); setImages([]); } catch (e) { setError(e.message || "Algo salió mal."); } finally { setBusy(false); } }; const useExample = (ex) => { setText(ex.text); setTimeout(() => { taRef.current && taRef.current.focus(); }, 0); }; return (
{/* ----- Header ----- */}

Cuéntame qué SOP necesitas

Habla o escribe libremente — la IA detectará título, fases, pasos, roles, herramientas y todo lo demás. No tienes que organizarlo, ni decidir dónde va cada cosa.

{/* ----- Big input ----- */}
{ e.preventDefault(); }} onDrop={onDrop} >