Core-Agentic-ERP / instinct-ui.mjs
fivetech's picture
Create instinct-ui.mjs
4bb43df verified
Raw History Blame Contribute Delete
12 kB
import {mountCentralUI} from './instinct-central-ui.mjs';
import {GmailBridge,ActivityLog,makeRequest,sign} from './instinct-gmail.mjs';
export function mountInstinctUI(AP){
let key=null,bridge=null,timer=null,reviewTimer=null,prepared=null,originalAsk=AP.ask.bind(AP),enabled=false;
const conversations=Object.fromEntries(['ALPHA','BETA','GAMMA'].map(a=>[a,{id:crypto.randomUUID().replaceAll('-',''),sequence:0,history:[]}]))
let log;try{log=new ActivityLog();}catch{log=null;}
const style=document.createElement('style');style.textContent='#instinct-panel{position:fixed;inset:8% 8%;max-height:84vh;overflow:auto;background:var(--bg,#0b1220);color:var(--text,#edf3ff);border:1px solid #677b91;border-radius:16px;padding:22px;z-index:99999;box-shadow:0 12px 80px #0009}#instinct-panel label{display:block;margin:10px 0}#instinct-panel input,#instinct-panel select,#instinct-panel button{font:inherit;padding:8px;border-radius:6px;max-width:100%;color:#102034;background:#e6eef6}#instinct-panel input[type=text],#instinct-panel input[type=email]{width:90%}#instinct-panel pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:32vh;overflow:auto;background:#101a2e;color:#e9f1ff;padding:12px;border-radius:8px}#instinct-panel button{margin:4px}#instinct-open{position:static;margin-top:12px;border-top:1px solid #304565;border-radius:0;min-height:42px}';document.head.append(style);
const open=document.createElement('button');open.id='instinct-open';open.textContent='⚙ Instinct · Gmail';open.type='button';open.className='nav-item';const sidebar=document.getElementById('ap-sidebar');if(sidebar)sidebar.append(open);else throw Error('Menú de configuración no disponible');
const panel=document.createElement('section');panel.id='instinct-panel';panel.hidden=true;panel.setAttribute('aria-label','Conexión Instinct Gmail');panel.innerHTML=`<h2>Instinct por Gmail</h2><p>Prueba manual. Gmail solo funciona con esta pestaña abierta y OAuth vigente. Ningún correo se envía al autorizar.</p><button id="ig-close">Cerrar</button><label>Client ID público OAuth<input id="ig-client" type="text" placeholder="…apps.googleusercontent.com"></label><label>Buzón ERP<input id="ig-mailbox" type="email" value="fivetech2@gmail.com"></label><label>Destino confirmado de Instinct<input id="ig-destination" type="email" placeholder="Confirma la dirección antes de enviar"></label><label><input id="ig-manual" type="checkbox"> Prueba manual SIN firma: no autenticada. Las respuestas no prueban identidad; solo texto, sin acciones.</label><p id="ig-key">Firma: falta CryptoKey privada de sesión. No pegues contraseñas o claves en este panel.</p><button id="ig-authorize">Autorizar Gmail</button><button id="ig-disconnect">Desconectar esta sesión</button><label><input id="ig-enabled" type="checkbox"> Usar este puente para el chat del ERP (sin llamadas al modelo anterior)</label><label>Agente<select id="ig-agent"><option>ALPHA</option><option>BETA</option><option>GAMMA</option></select></label><p id="ig-status" role="status">Pendiente de OAuth, firma y respondedor.</p><button id="ig-review">Revisar consulta del chat</button><button id="ig-send" disabled>Enviar consulta revisada</button><button id="ig-check">Comprobar respuestas una vez</button><label><input id="ig-poll" type="checkbox"> Comprobar Gmail cada 60 segundos mientras haya turnos pendientes. Esto hace lecturas periódicas de la cuenta.</label><h3>Revisión de actividad</h3><label>Intervalo del resumen local (minutos)<input id="ig-interval" type="number" min="5" max="1440" value="60"></label><button id="ig-schedule">Activar resúmenes locales</button><button id="ig-summary">Preparar resumen ahora</button><p>Los resúmenes se preparan localmente. No se envían solos ni constan como verificados por Instinct. Autoenvío y devolución firmada pendientes.</p><button id="ig-log">Ver log</button><pre id="ig-preview">Sin mensaje preparado.</pre>`;document.body.append(panel);
const el=id=>panel.querySelector('#'+id),status=t=>el('ig-status').textContent=t;
function event(event,extra={}){if(!log){status('Log no disponible: revisa almacenamiento antes de usar el puente.');throw Error('Activity log unavailable');}try{return log.append({event,...extra});}catch{status('No se pudo guardar el log. Puente pausado para evitar actividad sin registro.');enabled=false;el('ig-enabled').checked=false;throw Error('Log write failed');}}
function show(){panel.hidden=false;}open.onclick=show;el('ig-close').onclick=()=>panel.hidden=true;
function showSummary(summary){show();el('ig-preview').textContent=JSON.stringify(summary,null,2);status('Resumen local preparado. Sin enviar ni verificar.');}
async function prepare(text){if(!AP.db)throw Error('La base del ERP no está lista.');if(!bridge||(!key&&!bridge.manualUnauthenticated))throw Error('Falta autorizar Gmail o elegir prueba manual no autenticada.');const agent=el('ig-agent').value,c=conversations[agent];if([...bridge.jobs.values()].some(j=>j.request.conversation===c.id&&!['answered','expired'].includes(j.status)))throw Error('Hay un turno pendiente o incierto. No se reenvía automáticamente.');const context={...AP.context(),mode:AP.mode,suppliers:AP.rows('SELECT s.id,s.name,s.lead_days,sp.sku,sp.unit_cost_cents,sp.min_order_qty FROM suppliers s JOIN supplier_products sp ON sp.supplier_id=s.id WHERE s.active=1')};const draft=makeRequest({conversation:c.id,agent,sequence:c.sequence+1,text,context,replyTo:bridge.mailbox,history:c.history.slice(-6)});prepared=bridge.manualUnauthenticated?{...draft,authentication:'none-manual-test'}:await sign(draft,key);event('request_prepared',{agent,requestId:prepared.id,conversation:prepared.conversation,sequence:prepared.sequence,status:'prepared'});el('ig-preview').textContent='Cuenta: '+bridge.mailbox+'\nDestino: '+bridge.destination+'\n\n'+JSON.stringify(prepared,null,2);el('ig-send').disabled=false;status('Revisa cuenta, destino y contenido completo antes de pulsar Enviar.');show();}
el('ig-authorize').onclick=async()=>{try{if(bridge&&[...bridge.jobs.values()].some(j=>['sent','uncertain','sending'].includes(j.status))){await bridge.authorize();status('Token renovado; pendientes conservados.');return;}if(!globalThis.google?.accounts?.oauth2)throw Error('Google Identity Services no cargó. Revisa conexión o bloqueador.');bridge=new GmailBridge({clientId:el('ig-client').value.trim(),mailbox:el('ig-mailbox').value.trim(),destination:el('ig-destination').value.trim(),key,manualUnauthenticated:el('ig-manual').checked});await bridge.authorize();event('oauth_connected',{status:'connected'});status('Gmail autorizado. '+(bridge.manualUnauthenticated?'PRUEBA NO AUTENTICADA. ': '')+(key?'Firma disponible.':'Firma aún pendiente.'));}catch(e){status(e.message);}};
el('ig-manual').onchange=()=>{if(bridge){el('ig-manual').checked=bridge.manualUnauthenticated;status('Modo fijado al autorizar. No lo cambies con turnos pendientes; desconecta y recarga para otra prueba.');}else status(el('ig-manual').checked?'Prueba manual NO AUTENTICADA seleccionada. Autoriza Gmail después.':'Modo firmado: requiere clave privada.');};
el('ig-disconnect').onclick=()=>{bridge?.disconnect();clearInterval(timer);clearInterval(reviewTimer);el('ig-poll').checked=false;prepared=null;el('ig-send').disabled=true;status('Token eliminado de memoria. Pendientes siguen solo en esta sesión. Revoca permisos en Google si quieres retirar el consentimiento.');};
el('ig-enabled').onchange=()=>{enabled=el('ig-enabled').checked;try{event('bridge_mode_changed',{status:enabled?'enabled':'disabled'});}catch{return;}if(enabled){clearInterval(AP.cycleTimer);AP.startCycle=function(){clearInterval(this.cycleTimer);};status('Chat usa Gmail manual. Ciclo simulado detenido.');}else status('Chat anterior restaurado; ciclo automático continúa detenido.');};
AP.ask=async function(text,quiet=false,...rest){if(!enabled)return originalAsk(text,quiet,...rest);try{if(quiet)return;await prepare(text);}catch(e){status(e.message);show();}};
el('ig-review').onclick=async()=>{try{if(!enabled)throw Error('Activa primero el puente de chat.');const text=document.getElementById('chatInput').value.trim();if(!text)throw Error('Escribe la consulta en el chat del ERP.');await prepare(text);}catch(e){status(e.message);}};
el('ig-send').onclick=async()=>{try{if(!prepared||!enabled)throw Error('Prepara y revisa la consulta primero.');const r=prepared;if(Date.parse(r.expires_at)<Date.now())throw Error('Consulta caducada: prepara una nueva antes de enviar.');event('send_started',{agent:r.agent,requestId:r.id,conversation:r.conversation,sequence:r.sequence,status:'sending'});el('ig-send').disabled=true;const j=await bridge.send(r,{reviewed:true});conversations[r.agent].sequence=r.sequence;AP.tab('chat');AP.append(r.question,'user');event('send_accepted',{agent:r.agent,requestId:r.id,conversation:r.conversation,sequence:r.sequence,status:'sent',durationMs:j.sendMs});prepared=null;status('Gmail aceptó el envío. '+(bridge.manualUnauthenticated?'PRUEBA NO AUTENTICADA: respuesta manual pendiente.':'Respuesta firmada pendiente.')+' No es confirmación de entrega.');}catch(e){if(prepared)try{event('send_uncertain',{agent:prepared.agent,requestId:prepared.id,conversation:prepared.conversation,sequence:prepared.sequence,status:'uncertain'});}catch{}status(e.message);}};
async function check(){try{event('check_started',{status:'checking'});if(!bridge)throw Error('Autoriza Gmail primero.');const start=performance.now();const answers=await bridge.check();for(const j of answers){AP.tab('chat');AP.append((j.authentication==='unverified'?'[NO AUTENTICADA · prueba manual] ':'')+j.request.agent+': '+j.text,'ai');const c=conversations[j.request.agent];c.history.push({role:'user',text:j.request.question.slice(0,3000)},{role:'assistant',text:j.text.slice(0,3000)});c.history=c.history.slice(-6);event(j.authentication==='unverified'?'response_unverified':'response_verified',{agent:j.request.agent,requestId:j.request.id,conversation:j.request.conversation,sequence:j.request.sequence,status:'answered',durationMs:performance.now()-start});}status(answers.length?(bridge.manualUnauthenticated?'Respuestas NO AUTENTICADAS visibles: ':'Respuestas firmadas visibles: ')+answers.length:'Sin respuestas nuevas correlacionadas.');}catch(e){status(e.message);try{event('check_failed',{status:'failed',code:'gmail_check'});}catch{}}}
el('ig-check').onclick=check;el('ig-poll').onchange=()=>{clearInterval(timer);if(el('ig-poll').checked)timer=setInterval(()=>{if(bridge&&[...bridge.jobs.values()].some(j=>['sent','uncertain'].includes(j.status)))check();},60000);};
el('ig-log').onclick=()=>{el('ig-preview').textContent=JSON.stringify(log?.state||{error:'Log no disponible'},null,2);};el('ig-summary').onclick=()=>{if(log)showSummary(log.summary());};el('ig-schedule').onclick=()=>{try{clearInterval(reviewTimer);const n=Number(el('ig-interval').value);if(!Number.isInteger(n)||n<5||n>1440)throw Error('Intervalo válido: 5 a 1440 minutos');reviewTimer=setInterval(()=>showSummary(log.summary()),n*60000);status('Resumen local cada '+n+' minutos. No se envía automáticamente.');}catch(e){status(e.message);}};
// Provisioning is intentionally separate from public config. Never expose raw secrets in the UI.
window.AdaptaProInstinct={setSigningKey(cryptoKey){if(!cryptoKey||cryptoKey.type!=='secret'||cryptoKey.extractable||cryptoKey.algorithm.name!=='HMAC'||!cryptoKey.usages.includes('sign')||!cryptoKey.usages.includes('verify'))throw Error('Nonextractable HMAC signing/verification CryptoKey required');key=cryptoKey;if(bridge)bridge.key=key;el('ig-key').textContent='Firma privada disponible en memoria para esta sesión.';},show,check,prepare,summary:()=>log?.summary(),getState:()=>({enabled,authorized:!!bridge?.token,signingReady:!!key,pending:bridge?[...bridge.jobs.values()].map(j=>({id:j.request.id,agent:j.request.agent,status:j.status})):[]})};
}
if(globalThis.document&&globalThis.AP){mountInstinctUI(globalThis.AP);mountCentralUI(globalThis.AP);}