/* global React, NavIcon, MCP_RULES, MCP_THREAD, MCP_QUICK */ const { useState: useStateM, useRef: useRefM, useEffect: useEffectM } = React; // ============ Widget renderer ============ const Widget = ({ w }) => { if (!w || !w.kind) return null; if (w.kind === "bars") { const data = w.data || []; const max = Math.max(1, ...data.map(d => Number(d.value) || 0)); return (
{w.title || "Gráfico"}
{data.map((d, i) => (
{d.value} {d.label}
))}
{w.note &&

{w.note}

}
); } if (w.kind === "table") { return (
{w.title || "Tabla"}
{(w.cols || []).map((c, i) => )} {(w.rows || []).map((r, i) => ( {r.map((cell, j) => )} ))}
0 ? {textAlign:"right"} : {}}>{c}
0 ? "num" : ""}>{cell}
{w.note &&

{w.note}

}
); } if (w.kind === "rank") { const data = w.data || []; const max = Math.max(1, ...data.map(d => Number(d.count) || 0)); return (
{w.title || "Ranking"}
{data.map((d, i) => (
{i+1} {d.name} {d.count}
))}
{w.note &&

{w.note}

}
); } if (w.kind === "kpis") { const data = w.data || []; return (
{w.title || "Indicadores"}
{data.map((d, i) => (
{d.label}
{d.value}{d.unit && {d.unit}}
))}
{w.note &&

{w.note}

}
); } return null; }; const sourceLabel = { tickets: "Sistema de tickets", kb: "Knowledge Base", equipos: "BD de equipos", contratos: "BD de contratos", calendar: "Calendario técnicos", agenda: "Calendario técnicos", reglas: "Reglas MCP", }; const Msg = ({ m }) => { return (
{m.role === "sofia" ? "S" : "AC"}
{m.role === "sofia" ? "SOF.IA" : "Alejandra Carrillo"} · {m.time} {m.role === "sofia" && MCP}
{m.loading ? (
consultando sistemas conectados…
) : ( <>
$1").replace(/\n/g,"
")}}/> {m.widget && } {m.sources && m.sources.length > 0 && (
{m.sources.map(s => ↘ {sourceLabel[s] || s})}
)} )}
); }; // ============ Prompt for SOF.IA ============ const SYSTEM_PROMPT = `Eres SOF.IA, una agente de IA omnicanal de Optipixel que automatiza el soporte técnico de impresoras para Canella (empresa guatemalteca). Estás respondiendo a Alejandra Carrillo, Dispatcher de Canella, en su consola interna "MCP Console". Hablas en español de Guatemala, profesional, conciso, técnico pero claro. Tienes acceso vía MCP a: - tickets: bandeja activa y resueltos (canales: WhatsApp, llamada, web chat, correo) - equipos: BD de impresoras (HP LaserJet Pro M404dn, HP LaserJet M428fdw, HP Color LaserJet M283fdw, HP OfficeJet Pro 9015e, HP LaserJet M111w, Canon imageRUNNER 2630i, Canon LBP6230dw) - contratos: arrendamiento (ARR-*) y mantenimiento (MANT-*) - kb: Knowledge Base (artículos KB-HP-*, KB-CAN-*, KB-GEN-*) - agenda: calendario técnicos (Eddy R. Marroquín zona 11, Walter A. Galindo zona 10) Reglas activas que debes respetar al razonar: - Si entra un HP sin serie → escalar a Dispatcher - Arrendamiento → Conectividad - Cliente facturado/cotización → "fuera de alcance fase 1" - HW/SW con consulta técnica → KB o Taller - Visita técnica → Dispatcher - Equipos HP tienen series largas alfanuméricas (10 caracteres tipo VNB3K12089) Datos contextuales que puedes usar libremente (inventa cifras realistas si te las piden): - Volumen típico: ~128 tickets/día, 80% resueltos sin humano, ~17% escalados - Canales dominantes: WhatsApp y llamada - Falla más común: atascos de papel en HP M404dn (rodillo bandeja 2, ~80k impresiones) - Tiempo medio captura: 42 segundos - Operadores Canella: Alejandra Carrillo (Dispatcher), Marcos R. (Optipixel) Responde SOLO con JSON válido en este formato exacto: { "text": "tu respuesta en máximo 3 frases cortas, puede usar **negrita** y saltos de línea", "sources": ["tickets","kb","equipos","contratos","agenda","reglas"], "widget": null | { "kind": "bars"|"table"|"rank"|"kpis", ...campos } } Widgets: - bars: { "kind":"bars", "title":"...", "data":[{"label":"L","value":4},...], "note":"breve insight" } - table: { "kind":"table", "title":"...", "cols":["Col1","Col2"], "rows":[["a","b"],...], "note":"..." } - rank: { "kind":"rank", "title":"...", "data":[{"name":"...","count":N},...], "note":"..." } - kpis: { "kind":"kpis", "title":"...", "data":[{"label":"...","value":"...","unit":"..."},...], "note":"..." } Usa widget cuando la pregunta pida datos cuantitativos o comparaciones. Para preguntas conceptuales o de proceso, devuelve "widget": null. Incluye 1-3 sources relevantes. Sé directa, profesional, datos realistas. NUNCA salgas del JSON.`; // ============ MAIN ============ const McpScreen = () => { const [rules, setRules] = useStateM(MCP_RULES); const [newRule, setNewRule] = useStateM(""); const [input, setInput] = useStateM(""); const [messages, setMessages] = useStateM(MCP_THREAD); const [busy, setBusy] = useStateM(false); const threadRef = useRefM(null); useEffectM(() => { if (threadRef.current) { threadRef.current.scrollTop = threadRef.current.scrollHeight; } }, [messages, busy]); const toggleRule = (id) => { setRules(rs => rs.map(r => r.id === id ? {...r, active: !r.active} : r)); }; const addRule = () => { if (!newRule.trim()) return; setRules(rs => [...rs, { id: `R-${String(rs.length + 1).padStart(3,"0")}`, text: newRule.trim(), author: "Alejandra Carrillo", created: "hoy", triggers: 0, active: true, category: "Custom", }]); setNewRule(""); }; const fmtTime = () => { const d = new Date(); return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; }; const send = async (text) => { const q = (text || input).trim(); if (!q || busy) return; setInput(""); setBusy(true); const userMsg = { role:"user", text:q, time:fmtTime() }; const loadingMsg = { role:"sofia", loading:true, time:fmtTime() }; setMessages(ms => [...ms, userMsg, loadingMsg]); try { const raw = await window.claude.complete({ messages: [ { role:"user", content: SYSTEM_PROMPT + "\n\nPregunta del operador:\n" + q } ], }); // Try to parse JSON from response let parsed = null; try { // strip code fences if present let cleaned = raw.trim(); const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/); if (fence) cleaned = fence[1]; // find outermost JSON const first = cleaned.indexOf("{"); const last = cleaned.lastIndexOf("}"); if (first >= 0 && last > first) cleaned = cleaned.slice(first, last+1); parsed = JSON.parse(cleaned); } catch (e) { parsed = { text: raw, sources: ["tickets"], widget: null }; } const sofiaMsg = { role: "sofia", text: parsed.text || "(sin respuesta)", sources: parsed.sources || [], widget: parsed.widget || null, time: fmtTime(), }; setMessages(ms => [...ms.slice(0,-1), sofiaMsg]); } catch (err) { setMessages(ms => [...ms.slice(0,-1), { role: "sofia", text: "No pude consultar los sistemas en este momento. Intenta de nuevo en unos segundos.", sources: [], time: fmtTime(), }]); } finally { setBusy(false); } }; const onKey = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; return (

Preguntar a SOF.IA · Copilot MCP

Lenguaje natural sobre tickets, equipos, KB y agenda · conectado vía MCP
MCP · 4 SISTEMAS CONECTADOS
{/* Chat */}
{messages.map((m, i) => )}
{MCP_QUICK.map(c => ( send(c)}>↳ {c} ))}
setInput(e.target.value)} onKeyDown={onKey} placeholder={busy ? "SOF.IA está respondiendo…" : "Pregúntale a SOF.IA, crea una regla, pide un reporte…"} disabled={busy} />
SOF.IA lee tickets, equipos, KB y calendario. Las consultas son auditadas. ⏎ enviar · ⇧⏎ nueva línea
{/* Side: rules + connected systems */}

Reglas activas

{rules.filter(r => r.active).length}/{rules.length}
{rules.map(r => (
toggleRule(r.id)}/>
{r.text}
{r.category} {r.author} · {r.triggers} disparos
))}
setNewRule(e.target.value)} onKeyDown={e => e.key === "Enter" && addRule()} />

Sistemas conectados (MCP)

en vivo
tickets.canella.gt14 ms
equipos.canella.gt9 ms
contratos.canella.gt22 ms
kb.canella.gt7 ms
calendar.dispatch.gt38 ms

Reportes recientes

Volumen omnicanal · mayohace 2h
Top fallas HP M404ayer
SLA por canal · sem. 2123 may
); }; window.McpScreen = McpScreen;