← Files VeraARCHIVED FILE
modules/previdenza-inps/assets/previdenza-inps-review-widget.html
56.5 KB · Oct 5, 2026 · 18:29 UTC
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Revisione previdenza INPS</title>
<style>
:root {
color-scheme: light;
--ink: #1c211f;
--muted: #626a66;
--quiet: #8a918d;
--line: #dfe3df;
--line-strong: #c7cdc8;
--paper: #fbfcfa;
--white: #fefefc;
--wash: #f4f6f3;
--blue: #315f79;
--blue-wash: #edf3f6;
--green: #35634d;
--green-wash: #edf4ef;
--amber: #8a5b22;
--amber-wash: #f7f1e7;
--red: #8a433b;
--red-wash: #f8efed;
--focus: #2d6685;
--sans: "Avenir Next", "Helvetica Neue", Helvetica, sans-serif;
--serif: "Iowan Old Style", Baskerville, "Times New Roman", serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: var(--white); color: var(--ink); }
body { font: 1rem/1.45 var(--sans); }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.shell { min-height: 100vh; display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; }
.topbar {
min-height: 74px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
padding: 16px clamp(18px, 3vw, 42px);
border-bottom: 1px solid var(--line-strong);
background: var(--white);
}
.identity { display: flex; align-items: center; gap: 14px; min-width: 0; }
.seal {
width: 38px;
height: 38px;
display: grid;
place-items: center;
flex: none;
border-radius: 10px;
background: #171816;
color: #f7f0df;
font: 600 0.72rem/1 var(--serif);
letter-spacing: 0.08em;
}
.eyebrow { margin: 0 0 2px; color: var(--muted); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; }
h1 { margin: 0; font: 600 1.32rem/1.15 var(--serif); letter-spacing: -0.01em; }
.actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
.button {
min-height: 36px;
padding: 7px 13px;
border: 1px solid var(--line-strong);
border-radius: 7px;
background: var(--white);
font-weight: 600;
font-size: 0.88rem;
}
.button:hover { border-color: #9da59f; background: var(--wash); }
.button.primary { border-color: #244f66; background: var(--blue); color: #fbfcfa; }
.button.primary:hover { background: #284f66; }
.button.quiet { border-color: transparent; background: transparent; color: var(--muted); }
.button:disabled { cursor: not-allowed; opacity: 0.5; }
.runbar {
display: grid;
grid-template-columns: repeat(4, minmax(110px, auto)) minmax(180px, 1fr);
align-items: stretch;
border-bottom: 1px solid var(--line);
padding: 0 clamp(18px, 3vw, 42px);
background: var(--paper);
}
.metric { padding: 10px 22px 11px 0; }
.metric + .metric { padding-left: 22px; border-left: 1px solid var(--line); }
.metric-label { display: block; margin-bottom: 2px; color: var(--quiet); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; }
.metric-value { display: block; max-width: 28ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; font-variant-numeric: tabular-nums; }
.run-message { align-self: center; justify-self: end; padding: 10px 0 10px 22px; color: var(--muted); font-size: 0.82rem; text-align: right; }
.run-message.error { color: var(--red); }
.run-message.ok { color: var(--green); }
.workspace {
container: review / inline-size;
display: grid;
grid-template-columns: minmax(320px, 39%) minmax(0, 1fr);
min-height: 560px;
}
.queue { min-width: 0; border-right: 1px solid var(--line-strong); background: var(--paper); }
.queue-head { padding: 20px clamp(16px, 2vw, 26px) 14px; border-bottom: 1px solid var(--line); }
.queue-title-row { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; margin-bottom: 12px; }
h2 { margin: 0; font-size: 0.96rem; font-weight: 650; }
.count { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.search {
width: 100%;
min-height: 38px;
border: 1px solid var(--line-strong);
border-radius: 6px;
padding: 8px 11px;
background: var(--white);
}
.filters { display: flex; gap: 7px; overflow-x: auto; padding: 11px 0 1px; scrollbar-width: thin; }
.filter {
flex: none;
padding: 4px 8px;
border: 1px solid transparent;
border-radius: 5px;
background: transparent;
color: var(--muted);
font-size: 0.76rem;
}
.filter.active { border-color: var(--line-strong); background: var(--white); color: var(--ink); }
.queue-tools { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 10px; }
.queue-tools select { min-height: 32px; border: 1px solid var(--line); border-radius: 5px; background: var(--white); padding: 4px 24px 4px 8px; font-size: 0.78rem; }
.text-action { border: 0; background: transparent; color: var(--blue); padding: 4px 0; font-size: 0.78rem; font-weight: 600; }
.rows { max-height: calc(100vh - 226px); overflow: auto; }
.row {
width: 100%;
display: grid;
grid-template-columns: 9px minmax(0, 1fr) auto;
gap: 11px;
align-items: start;
border: 0;
border-bottom: 1px solid var(--line);
padding: 14px clamp(16px, 2vw, 26px);
background: transparent;
text-align: left;
}
.row:hover { background: #f0f3ef; }
.row.selected { background: var(--white); box-shadow: inset 3px 0 0 var(--blue); }
.state-dot { width: 8px; height: 8px; margin-top: 5px; border: 1px solid #aab1ac; border-radius: 50%; background: transparent; }
.state-dot.accepted, .state-dot.edited { border-color: var(--green); background: var(--green); }
.state-dot.needs_evidence { border-color: var(--amber); background: var(--amber); }
.state-dot.rejected { border-color: var(--red); background: var(--red); }
.state-dot.skipped { border-color: var(--quiet); background: var(--quiet); }
.row-title { display: block; overflow: hidden; text-overflow: ellipsis; font-weight: 600; font-size: 0.9rem; }
.row-meta { display: flex; gap: 7px; align-items: center; margin-top: 4px; color: var(--muted); font-size: 0.74rem; }
.row-meta span + span::before { content: "·"; margin-right: 7px; color: var(--quiet); }
.risk { padding-top: 1px; color: var(--quiet); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
.risk.high { color: var(--red); }
.empty { padding: 42px 24px; color: var(--muted); text-align: center; }
.empty strong { display: block; margin-bottom: 6px; color: var(--ink); }
.detail { min-width: 0; padding: 24px clamp(22px, 3vw, 44px) 40px; overflow: auto; }
.detail-head { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; padding-bottom: 18px; border-bottom: 1px solid var(--line-strong); }
.item-type { margin: 0 0 5px; color: var(--blue); font-size: 0.72rem; font-weight: 650; letter-spacing: 0.09em; text-transform: uppercase; }
.detail h3 { max-width: 34ch; margin: 0; font: 600 1.42rem/1.22 var(--serif); }
.status {
flex: none;
border: 1px solid var(--line-strong);
border-radius: 999px;
padding: 4px 8px;
background: var(--wash);
color: var(--muted);
font-size: 0.7rem;
}
.status.needs_evidence { border-color: #d9c6a4; background: var(--amber-wash); color: var(--amber); }
.status.accepted, .status.edited { border-color: #bcd0c2; background: var(--green-wash); color: var(--green); }
.status.rejected { border-color: #dfc0bc; background: var(--red-wash); color: var(--red); }
.safeguards { margin: 14px 0 0; border-bottom: 1px solid var(--line); }
.safeguards summary { padding: 9px 0 13px; color: var(--muted); cursor: pointer; font-size: 0.78rem; }
.safeguards summary strong { color: var(--ink); }
.safeguard-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; padding: 2px 0 15px; }
.safeguard { min-width: 0; }
.safeguard span { display: block; color: var(--quiet); font-size: 0.68rem; letter-spacing: 0.07em; text-transform: uppercase; }
.safeguard strong { display: block; margin-top: 3px; overflow-wrap: anywhere; font-size: 0.78rem; font-weight: 600; }
.trace { padding: 0 0 15px; color: var(--muted); font-size: 0.76rem; }
.trace div { padding: 5px 0; border-top: 1px solid var(--line); }
.trace span { display: block; margin-top: 2px; overflow-wrap: anywhere; color: var(--quiet); }
.section { padding: 20px 0; border-bottom: 1px solid var(--line); }
.section:last-child { border-bottom: 0; }
.section-title { margin: 0 0 12px; color: var(--muted); font-size: 0.7rem; font-weight: 650; letter-spacing: 0.1em; text-transform: uppercase; }
.lead { max-width: 72ch; margin: 0; white-space: pre-wrap; }
.facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px; }
.fact { display: grid; grid-template-columns: minmax(105px, 34%) 1fr; gap: 10px; padding: 8px 0; border-top: 1px solid var(--line); }
.fact:nth-child(-n+2) { border-top: 0; }
.fact-key { color: var(--muted); font-size: 0.78rem; }
.fact-value { min-width: 0; overflow-wrap: anywhere; font-size: 0.86rem; font-variant-numeric: tabular-nums; }
.evidence-list { display: grid; gap: 0; }
.evidence { padding: 11px 0; border-top: 1px solid var(--line); }
.evidence:first-child { border-top: 0; padding-top: 0; }
.evidence-title { display: flex; justify-content: space-between; gap: 14px; font-weight: 600; font-size: 0.88rem; }
.evidence-meta { margin-top: 4px; color: var(--muted); font-size: 0.78rem; overflow-wrap: anywhere; }
.evidence-excerpt { margin: 8px 0 0; padding-left: 12px; border-left: 2px solid var(--line-strong); color: #3f4743; font-family: var(--serif); }
.decision-actions { display: flex; flex-wrap: wrap; gap: 7px; }
.decision-action {
border: 1px solid var(--line-strong);
border-radius: 6px;
padding: 7px 10px;
background: var(--white);
font-size: 0.8rem;
}
.decision-action:hover { background: var(--wash); }
.decision-action.selected { border-color: var(--blue); background: var(--blue-wash); color: #234f67; font-weight: 650; }
.field { display: grid; gap: 5px; margin-top: 13px; }
.field label { color: var(--muted); font-size: 0.76rem; font-weight: 600; }
textarea { width: 100%; min-height: 76px; resize: vertical; border: 1px solid var(--line-strong); border-radius: 6px; padding: 9px 10px; background: var(--white); }
.impact { margin-top: 13px; padding: 10px 12px; border: 1px solid var(--line); background: var(--paper); color: var(--muted); font-size: 0.78rem; }
.artifact-strip { border-top: 1px solid var(--line-strong); padding: 16px clamp(18px, 3vw, 42px) 20px; background: var(--paper); }
.artifact-head { display: flex; justify-content: space-between; align-items: baseline; gap: 18px; margin-bottom: 9px; }
.artifact-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 24px; }
.artifact { min-width: 0; padding: 8px 0; border-top: 1px solid var(--line); }
.artifact strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.82rem; }
.artifact span { color: var(--muted); font-size: 0.72rem; }
@container review (max-width: 820px) {
.workspace { grid-template-columns: 1fr; }
.queue { border-right: 0; border-bottom: 1px solid var(--line-strong); }
.rows { max-height: 360px; }
}
@media (max-width: 760px) {
.topbar { align-items: flex-start; padding: 14px 16px; }
.identity { align-items: flex-start; }
.seal { width: 34px; height: 34px; }
.actions { gap: 5px; }
.actions #copy { display: none; }
.button { min-height: 34px; padding: 6px 9px; }
.runbar { grid-template-columns: repeat(2, minmax(0, 1fr)); padding: 0 16px; }
.metric { padding: 9px 8px 9px 0; }
.metric + .metric { padding-left: 8px; }
.metric:nth-child(3) { border-left: 0; }
.run-message { grid-column: 1 / -1; justify-self: start; padding: 8px 0; text-align: left; }
.facts { grid-template-columns: 1fr; }
.fact:nth-child(2) { border-top: 1px solid var(--line); }
.safeguard-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.artifact-list { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: no-preference) {
.detail > * { animation: arrive 240ms cubic-bezier(.22, 1, .36, 1) both; }
@keyframes arrive { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
}
</style>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="identity">
<div class="seal" aria-hidden="true">V</div>
<div>
<p class="eyebrow" id="eyebrow">Vera · fascicolo previdenziale</p>
<h1 id="page-heading">Revisione previdenza INPS</h1>
</div>
</div>
<div class="actions">
<button class="button quiet" id="copy" type="button">Copia JSON</button>
<button class="button quiet" id="cancel" type="button">Annulla modifiche</button>
<button class="button" id="save" type="button">Salva decisioni</button>
<button class="button primary" id="apply" type="button">Applica</button>
</div>
</header>
<section class="runbar" id="runbar" aria-label="Stato revisione"></section>
<main class="workspace">
<aside class="queue" id="queue" aria-label="Coda di revisione">
<div class="queue-head">
<div class="queue-title-row">
<h2 id="queue-title">Coda di revisione</h2>
<span class="count" id="visible-count"></span>
</div>
<input class="search" id="search" type="search" placeholder="Cerca fatto, fonte, periodo, evidenza">
<div class="filters" id="type-filters" aria-label="Filtri per tipo"></div>
<div class="queue-tools">
<select id="state-filter" aria-label="Filtra per stato">
<option id="state-all" value="all">Tutti gli stati</option>
<option id="state-pending" value="pending">Da decidere</option>
<option id="state-reviewed" value="reviewed">Decisi</option>
<option id="state-evidence" value="needs_evidence">Serve evidenza</option>
</select>
<button class="text-action" id="recommended" type="button">Usa raccomandate visibili</button>
</div>
</div>
<div class="rows" id="rows"></div>
</aside>
<article class="detail" id="detail" aria-live="polite"></article>
</main>
<section class="artifact-strip" id="artifacts" aria-label="Output finali"></section>
</div>
<script>
const UI_COPY = {
it: {
pageTitle: "Revisione previdenza INPS",
eyebrow: "Vera · fascicolo previdenziale",
heading: "Revisione previdenza INPS",
copyButton: "Copia JSON", cancelButton: "Annulla modifiche",
saveButton: "Salva decisioni", applyButton: "Applica",
reviewAria: "Stato revisione", queueAria: "Coda di revisione",
queueTitle: "Coda di revisione", searchPlaceholder: "Cerca fatto, fonte, periodo, evidenza",
typeFiltersAria: "Filtri per tipo", stateFilterAria: "Filtra per stato",
stateAll: "Tutti gli stati", statePending: "Da decidere", stateReviewed: "Decisi",
stateEvidence: "Serve evidenza", recommended: "Usa raccomandate visibili",
artifactsAria: "Output finali",
all: "Tutti", of: "di", none: "Nessuno",
cancelled: "Modifiche non salvate annullate.", localOutput: "Output locale",
jsonOnly: "Solo export JSON", unsaved: "Modifiche non salvate",
appliedCount: (count) => `${count} decisioni applicate`,
savedCount: (count) => `${count} decisioni salvate`,
noneSaved: "Nessuna decisione salvata",
metricRun: "Esecuzione", metricItems: "Elementi", metricDecided: "Decisi", metricGaps: "Da integrare",
notDeclared: "non dichiarata", externalUsed: "percorso oltre Codex usato",
noExternal: "nessun percorso esterno oltre Codex", jsonFallback: "fallback JSON",
command: "Comando", input: "Input", output: "Output", step: "Passaggio",
safeguards: "Script e percorsi oltre Codex", helperExecution: "Esecuzione helper",
beyondCodex: "Oltre Codex", payload: "Payload", persistence: "Persistenza",
codexNotCounted: "Il contesto del modello Codex non è conteggiato in questa sezione.",
noTrace: "Nessuna esecuzione helper dichiarata nel payload.",
noCaseTitle: "Nessun fascicolo caricato",
noCaseBody: "Apri il widget dal risultato di render_previdenza_inps_review.",
noItemsTitle: "Nessun elemento", noItemsBody: "Modifica ricerca o filtri.",
noEvidence: "Nessuna evidenza collegata a questo elemento.", evidence: "Evidenza",
proposedEdit: "Correzione proposta", editPlaceholder: "Inserisci il valore o il testo corretto",
requestedDocuments: "Documenti richiesti · uno per riga", professionalDecision: "Decisione professionale",
reviewerNote: "Nota del revisore", reviewerPlaceholder: "Motivo, cautela o prossima verifica",
editImpact: "La correzione resta registrata nell’esito di revisione; l’eventuale rigenerazione del documento professionale avviene nel workflow locale.",
blockerImpact: "Questa decisione crea un blocco visibile in final_artifacts.json.",
decisionImpact: "La decisione aggiorna lo stato di revisione senza modificare le fonti originali.",
selectTitle: "Seleziona un elemento", selectBody: "Qui compariranno evidenze, autorità, dati e decisione.",
noSummary: "Nessuna sintesi disponibile.", summary: "Sintesi", controlData: "Dati del controllo",
evidenceSources: "Evidenze e fonti", packageOutputs: "Output del fascicolo", files: "file",
blockers: "blocchi", outputsPending: "Gli output compariranno dopo l’esecuzione locale e l’applicazione delle decisioni.",
correctionRequired: (title) => `Inserisci la correzione per “${title}”.`,
jsonCopied: "JSON decisioni copiato.", bridgeApply: "Bridge MCP non disponibile: JSON copiato, applicazione non eseguita.",
bridgeSave: "Bridge MCP non disponibile: JSON copiato.", applying: "Applicazione in corso…",
saving: "Salvataggio in corso…", operationFailed: "Operazione non riuscita.",
applied: "Decisioni applicate.", saved: "Decisioni salvate.", recovery: "Copia il JSON per recuperare le decisioni.",
items: {
fact: "Fatto", finding: "Rilievo", calculation: "Calcolo",
missing_evidence: "Evidenza mancante", authority: "Fonte normativa",
audit_check: "Controllo", artifact: "Output",
},
actions: {
accept: "Accetta", reject: "Rifiuta", edit: "Correggi", mark_unclear: "Segna incerto",
request_more_documents: "Richiedi documenti", skip: "Salta",
},
statuses: {
pending: "Da decidere", accepted: "Accettato", rejected: "Rifiutato",
edited: "Corretto", needs_evidence: "Serve evidenza", skipped: "Saltato",
ready_for_professional_review: "Pronto per revisione professionale", validation_fail: "Validazione non superata",
passed: "Superato", written: "Scritto", pending_review: "Revisione in attesa",
},
fields: {
fact_id: "ID fatto", statement: "Dichiarazione", review_label: "Etichetta revisione",
value: "Valore", value_type: "Tipo valore", review_status: "Stato revisione",
evidence_count: "Numero evidenze", claim_id: "ID conclusione", claim_text: "Conclusione",
claim_type: "Tipo conclusione", verdict: "Esito", source_support: "Supporto fonti",
reasoning_review: "Revisione ragionamento", proposed_fix: "Correzione proposta",
source_count: "Numero fonti", professional_review_status: "Stato revisione professionale",
recipe_id: "ID calcolo", status: "Stato", description: "Descrizione", result: "Risultato",
unit: "Unità", inputs: "Input", request_id: "ID richiesta", request_text: "Testo richiesta",
path: "Percorso",
},
},
es: {
pageTitle: "Revisión de Previdenza INPS",
eyebrow: "Vera · expediente de previsión social",
heading: "Revisión de Previdenza INPS",
copyButton: "Copiar JSON", cancelButton: "Cancelar cambios",
saveButton: "Guardar decisiones", applyButton: "Aplicar",
reviewAria: "Estado de la revisión", queueAria: "Cola de revisión",
queueTitle: "Cola de revisión", searchPlaceholder: "Buscar hecho, fuente, periodo o evidencia",
typeFiltersAria: "Filtros por tipo", stateFilterAria: "Filtrar por estado",
stateAll: "Todos los estados", statePending: "Por decidir", stateReviewed: "Decididos",
stateEvidence: "Requiere evidencia", recommended: "Usar recomendaciones visibles",
artifactsAria: "Salidas finales",
all: "Todos", of: "de", none: "Ninguno",
cancelled: "Se han cancelado los cambios sin guardar.", localOutput: "Salida local",
jsonOnly: "Solo exportación JSON", unsaved: "Cambios sin guardar",
appliedCount: (count) => `${count} decisiones aplicadas`,
savedCount: (count) => `${count} decisiones guardadas`,
noneSaved: "No hay decisiones guardadas",
metricRun: "Ejecución", metricItems: "Elementos", metricDecided: "Decididos", metricGaps: "Pendientes",
notDeclared: "no declarada", externalUsed: "se ha usado una ruta fuera de Codex",
noExternal: "no se ha usado ninguna ruta externa fuera de Codex", jsonFallback: "modo alternativo JSON",
command: "Comando", input: "Entrada", output: "Salida", step: "Paso",
safeguards: "Scripts y rutas fuera de Codex", helperExecution: "Ejecución auxiliar",
beyondCodex: "Fuera de Codex", payload: "Datos", persistence: "Persistencia",
codexNotCounted: "El contexto del modelo Codex no se contabiliza en esta sección.",
noTrace: "No se ha declarado ninguna ejecución auxiliar en los datos.",
noCaseTitle: "No hay ningún expediente cargado",
noCaseBody: "Abra el widget desde el resultado de render_previdenza_inps_review.",
noItemsTitle: "No hay elementos", noItemsBody: "Modifique la búsqueda o los filtros.",
noEvidence: "Este elemento no tiene ninguna evidencia vinculada.", evidence: "Evidencia",
proposedEdit: "Corrección propuesta", editPlaceholder: "Introduzca el valor o el texto correcto",
requestedDocuments: "Documentos solicitados · uno por línea", professionalDecision: "Decisión profesional",
reviewerNote: "Nota del revisor", reviewerPlaceholder: "Motivo, cautela o próxima comprobación",
editImpact: "La corrección queda registrada en el resultado de la revisión; el documento profesional solo se vuelve a generar en el flujo local.",
blockerImpact: "Esta decisión crea un bloqueo visible en final_artifacts.json.",
decisionImpact: "La decisión actualiza el estado de la revisión sin modificar las fuentes originales.",
selectTitle: "Seleccione un elemento", selectBody: "Aquí aparecerán las evidencias, las fuentes, los datos y la decisión.",
noSummary: "No hay ningún resumen disponible.", summary: "Resumen", controlData: "Datos del control",
evidenceSources: "Evidencias y fuentes", packageOutputs: "Salidas del expediente", files: "archivos",
blockers: "bloqueos", outputsPending: "Las salidas aparecerán después de la ejecución local y de aplicar las decisiones.",
correctionRequired: (title) => `Introduzca la corrección para «${title}».`,
jsonCopied: "JSON de decisiones copiado.", bridgeApply: "El puente MCP no está disponible: se ha copiado el JSON y no se ha aplicado.",
bridgeSave: "El puente MCP no está disponible: se ha copiado el JSON.", applying: "Aplicando…",
saving: "Guardando…", operationFailed: "La operación no se ha completado.",
applied: "Decisiones aplicadas.", saved: "Decisiones guardadas.", recovery: "Copie el JSON para recuperar las decisiones.",
items: {
fact: "Hecho", finding: "Conclusión", calculation: "Cálculo",
missing_evidence: "Evidencia pendiente", authority: "Fuente normativa",
audit_check: "Control", artifact: "Salida",
},
actions: {
accept: "Aceptar", reject: "Rechazar", edit: "Corregir", mark_unclear: "Marcar como incierto",
request_more_documents: "Solicitar documentos", skip: "Omitir",
},
statuses: {
pending: "Por decidir", accepted: "Aceptado", rejected: "Rechazado",
edited: "Corregido", needs_evidence: "Requiere evidencia", skipped: "Omitido",
ready_for_professional_review: "Listo para revisión profesional", validation_fail: "Validación no superada",
passed: "Superado", written: "Generado", pending_review: "Pendiente de revisión",
},
fields: {
fact_id: "ID del hecho", statement: "Declaración", review_label: "Etiqueta de revisión",
value: "Valor", value_type: "Tipo de valor", review_status: "Estado de la revisión",
evidence_count: "Número de evidencias", claim_id: "ID de la conclusión", claim_text: "Conclusión",
claim_type: "Tipo de conclusión", verdict: "Resultado", source_support: "Soporte de las fuentes",
reasoning_review: "Revisión del razonamiento", proposed_fix: "Corrección propuesta",
source_count: "Número de fuentes", professional_review_status: "Estado de la revisión profesional",
recipe_id: "ID del cálculo", status: "Estado", description: "Descripción", result: "Resultado",
unit: "Unidad", inputs: "Entradas", request_id: "ID de la solicitud", request_text: "Texto de la solicitud",
path: "Ruta",
},
},
};
function normalizeLanguage(value) {
const language = String(value || "").trim().toLowerCase().replaceAll("_", "-");
return language === "es" || language.startsWith("es-") || language === "spa" ? "es" : "it";
}
function languageFor(payload) {
return normalizeLanguage(
payload?.review_payload?.language
|| payload?.run_intake?.working_language
|| payload?.run_intake?.language
|| "it"
);
}
function copyFor(payload) { return UI_COPY[languageFor(payload)] || UI_COPY.it; }
const ACTION_STATUSES = {
accept: "accepted",
reject: "rejected",
edit: "edited",
mark_unclear: "needs_evidence",
request_more_documents: "needs_evidence",
skip: "skipped",
};
const DEMO = {
widget_type: "previdenza_inps_review",
run_intake: null,
review_payload: {
schema_version: "1.0",
plugin: "previdenza-inps",
workflow: "previdenza_inps_case_review",
run_id: "anteprima",
item_count: 0,
items: [],
status: "ready_for_review",
},
ui_decisions: null,
final_artifacts: null,
decision_policy: {
save_tool: "save_previdenza_inps_decisions",
apply_tool: "apply_previdenza_inps_decisions",
can_persist: false,
persistence_root: "run_intake.output_dir",
fallback: "copy_or_download_json",
},
};
const state = {
payload: null,
decisions: {},
selectedId: null,
type: "all",
status: "all",
query: "",
dirty: false,
busy: false,
message: "",
messageKind: "",
};
function copy() { return copyFor(state.payload); }
function localizedValue(value) {
return copy().statuses[String(value || "")] || value;
}
function fieldLabel(key) { return copy().fields[key] || humanize(key); }
function applyStaticCopy() {
const text = copy();
document.documentElement.lang = languageFor(state.payload);
document.title = text.pageTitle;
document.getElementById("eyebrow").textContent = text.eyebrow;
document.getElementById("page-heading").textContent = text.heading;
document.getElementById("copy").textContent = text.copyButton;
document.getElementById("cancel").textContent = text.cancelButton;
document.getElementById("save").textContent = text.saveButton;
document.getElementById("apply").textContent = text.applyButton;
document.getElementById("runbar").setAttribute("aria-label", text.reviewAria);
document.getElementById("queue").setAttribute("aria-label", text.queueAria);
document.getElementById("queue-title").textContent = text.queueTitle;
document.getElementById("search").placeholder = text.searchPlaceholder;
document.getElementById("type-filters").setAttribute("aria-label", text.typeFiltersAria);
document.getElementById("state-filter").setAttribute("aria-label", text.stateFilterAria);
document.getElementById("state-all").textContent = text.stateAll;
document.getElementById("state-pending").textContent = text.statePending;
document.getElementById("state-reviewed").textContent = text.stateReviewed;
document.getElementById("state-evidence").textContent = text.stateEvidence;
document.getElementById("recommended").textContent = text.recommended;
document.getElementById("artifacts").setAttribute("aria-label", text.artifactsAria);
}
function esc(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function pickPayload(value) {
if (!isObject(value)) return null;
if (isObject(value.review_payload) && Array.isArray(value.review_payload.items)) return value;
for (const key of ["structuredContent", "toolOutput", "result"]) {
const nested = pickPayload(value[key]);
if (nested) return nested;
}
return null;
}
function incomingPayload() {
const host = window.openai || {};
return pickPayload(host.toolOutput) || pickPayload(host.structuredContent) || DEMO;
}
function items() {
return Array.isArray(state.payload?.review_payload?.items) ? state.payload.review_payload.items : [];
}
function itemById(id) {
return items().find((item) => item.id === id) || null;
}
function decisionFor(item) {
return item ? state.decisions[item.id] || null : null;
}
function statusFor(item) {
const decision = decisionFor(item);
return decision ? ACTION_STATUSES[decision.action] || "pending" : "pending";
}
function humanize(value) {
return String(value || "").replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function compactValue(value) {
if (value == null || value === "") return "—";
if (Array.isArray(value)) return value.map(compactValue).join(", ");
if (isObject(value)) return Object.entries(value).map(([key, nested]) => `${fieldLabel(key)}: ${compactValue(nested)}`).join(" · ");
return String(localizedValue(value));
}
function normalizedDecision(decision) {
return {
item_id: decision.item_id,
action: decision.action,
reviewer_note: decision.reviewer_note || "",
edit_value: decision.edit_value || "",
requested_documents: Array.isArray(decision.requested_documents) ? decision.requested_documents : [],
};
}
function savedDecisionMap() {
const loaded = {};
const saved = Array.isArray(state.payload?.ui_decisions?.decisions) ? state.payload.ui_decisions.decisions : [];
for (const decision of saved) {
if (decision?.item_id && decision?.action && itemById(decision.item_id)) {
loaded[decision.item_id] = normalizedDecision(decision);
}
}
return loaded;
}
function loadDecisions() {
const saved = savedDecisionMap();
const loaded = { ...saved };
let recoveredDraft = false;
const draft = window.openai?.widgetState;
if (draft?.run_id === state.payload?.review_payload?.run_id && isObject(draft.decisions)) {
for (const [id, decision] of Object.entries(draft.decisions)) {
if (itemById(id) && decision?.action) {
const normalized = normalizedDecision({ ...decision, item_id: id });
if (JSON.stringify(loaded[id] || null) !== JSON.stringify(normalized)) recoveredDraft = true;
loaded[id] = normalized;
}
}
}
state.decisions = loaded;
state.dirty = recoveredDraft;
}
function clearDraft() {
try {
window.openai?.setWidgetState?.({
run_id: state.payload?.review_payload?.run_id || null,
decisions: {},
});
} catch { }
}
function cancelChanges() {
if (state.busy || !state.dirty) return;
state.decisions = savedDecisionMap();
clearDraft();
state.dirty = false;
state.message = copy().cancelled;
state.messageKind = "";
render();
}
function persistDraft() {
state.dirty = true;
try {
window.openai?.setWidgetState?.({
run_id: state.payload?.review_payload?.run_id || null,
decisions: state.decisions,
});
} catch { }
}
function visibleItems() {
const query = state.query.trim().toLocaleLowerCase(languageFor(state.payload));
return items().filter((item) => {
if (state.type !== "all" && item.item_type !== state.type) return false;
const status = statusFor(item);
if (state.status === "pending" && status !== "pending") return false;
if (state.status === "reviewed" && status === "pending") return false;
if (state.status === "needs_evidence" && status !== "needs_evidence") return false;
if (!query) return true;
const haystack = [item.title, item.summary, item.item_type, item.source_path, compactValue(item.data), compactValue(item.evidence)].join(" ").toLocaleLowerCase(languageFor(state.payload));
return haystack.includes(query);
});
}
function renderRunbar() {
const review = state.payload.review_payload;
const total = items().length;
const decided = Object.keys(state.decisions).length;
const gaps = items().filter((item) => statusFor(item) === "needs_evidence").length;
const persistLabel = state.payload.decision_policy?.can_persist ? copy().localOutput : copy().jsonOnly;
const saved = Array.isArray(state.payload?.ui_decisions?.decisions) ? state.payload.ui_decisions.decisions.length : 0;
const applied = Array.isArray(state.payload?.applied_decisions?.decisions) ? state.payload.applied_decisions.decisions.length : 0;
const persistenceState = state.dirty
? copy().unsaved
: applied
? copy().appliedCount(applied)
: saved
? copy().savedCount(saved)
: copy().noneSaved;
document.getElementById("runbar").innerHTML = `
<div class="metric"><span class="metric-label">${esc(copy().metricRun)}</span><span class="metric-value" title="${esc(review.run_id)}">${esc(review.run_id)}</span></div>
<div class="metric"><span class="metric-label">${esc(copy().metricItems)}</span><span class="metric-value">${total}</span></div>
<div class="metric"><span class="metric-label">${esc(copy().metricDecided)}</span><span class="metric-value">${decided} / ${total}</span></div>
<div class="metric"><span class="metric-label">${esc(copy().metricGaps)}</span><span class="metric-value">${gaps}</span></div>
<div class="run-message ${esc(state.messageKind)}">${esc(state.message || `${persistenceState} · ${persistLabel}`)}</div>`;
}
function hasExternalUse(posture) {
const values = [
posture.external_connectors_used,
posture.external_routes_used,
posture.upload_paths_used,
posture.remote_sql_execution_used,
posture.hosted_notebook_execution_used,
];
return values.some((value) => Array.isArray(value) ? value.length > 0 : Boolean(value));
}
function safeguardsHtml() {
const run = isObject(state.payload?.run_intake) ? state.payload.run_intake : {};
const posture = isObject(run.data_posture) ? run.data_posture : {};
const trace = Array.isArray(run.execution_trace) ? run.execution_trace : [];
const external = hasExternalUse(posture);
const locations = [...new Set(trace.map((entry) => entry?.execution_location).filter(Boolean))];
const location = locations.length ? locations.join(", ") : copy().notDeclared;
const externalLabel = external ? copy().externalUsed : copy().noExternal;
const persistence = state.payload.decision_policy?.can_persist ? "run_intake.output_dir" : copy().jsonFallback;
const traceRows = trace.slice(0, 6).map((entry) => {
const details = [
entry.command ? `${copy().command}: ${entry.command}` : "",
Array.isArray(entry.inputs) && entry.inputs.length ? `${copy().input}: ${entry.inputs.join(", ")}` : "",
Array.isArray(entry.outputs) && entry.outputs.length ? `${copy().output}: ${entry.outputs.join(", ")}` : "",
].filter(Boolean).join(" · ");
return `<div><strong>${esc(entry.step_id || entry.kind || copy().step)}</strong> · ${esc(localizedValue(entry.status || ""))}${entry.execution_location ? ` · ${esc(entry.execution_location)}` : ""}${details ? `<span>${esc(details)}</span>` : ""}</div>`;
}).join("");
return `<details class="safeguards">
<summary><strong>${esc(copy().safeguards)}</strong> · ${esc(location)} · ${esc(externalLabel)} · ${items().length}/${2500} ${esc(copy().metricItems.toLowerCase())}</summary>
<div class="safeguard-grid">
<div class="safeguard"><span>${esc(copy().helperExecution)}</span><strong>${esc(location)}</strong></div>
<div class="safeguard"><span>${esc(copy().beyondCodex)}</span><strong>${esc(externalLabel)}</strong></div>
<div class="safeguard"><span>${esc(copy().payload)}</span><strong>${items().length} ${esc(copy().of)} 2500</strong></div>
<div class="safeguard"><span>${esc(copy().persistence)}</span><strong>${esc(persistence)}</strong></div>
</div>
<div class="trace">${esc(copy().codexNotCounted)}</div>
${traceRows ? `<div class="trace">${traceRows}</div>` : `<div class="trace">${esc(copy().noTrace)}</div>`}
</details>`;
}
function renderFilters() {
const present = [...new Set(items().map((item) => item.item_type))];
const filters = [{ value: "all", label: copy().all }, ...present.map((value) => ({ value, label: copy().items[value] || humanize(value) }))];
document.getElementById("type-filters").innerHTML = filters.map((filter) => `
<button type="button" class="filter ${state.type === filter.value ? "active" : ""}" data-type="${esc(filter.value)}">${esc(filter.label)}</button>`).join("");
}
function renderRows() {
const visible = visibleItems();
document.getElementById("visible-count").textContent = `${visible.length} ${copy().of} ${items().length}`;
if (state.selectedId && !visible.some((item) => item.id === state.selectedId)) state.selectedId = null;
if (!state.selectedId && visible.length) state.selectedId = visible[0].id;
const node = document.getElementById("rows");
if (!items().length) {
node.innerHTML = `<div class="empty"><strong>${esc(copy().noCaseTitle)}</strong>${esc(copy().noCaseBody)}</div>`;
return;
}
if (!visible.length) {
node.innerHTML = `<div class="empty"><strong>${esc(copy().noItemsTitle)}</strong>${esc(copy().noItemsBody)}</div>`;
return;
}
node.innerHTML = visible.map((item) => {
const status = statusFor(item);
const data = isObject(item.data) ? item.data : {};
const risk = String(item.risk || data.risk || item.priority || data.priority || "");
const period = item.period || data.period || item.source_path || "";
return `<button type="button" class="row ${state.selectedId === item.id ? "selected" : ""}" data-id="${esc(item.id)}">
<span class="state-dot ${esc(status)}" aria-hidden="true"></span>
<span><span class="row-title">${esc(item.title)}</span><span class="row-meta"><span>${esc(copy().items[item.item_type] || humanize(item.item_type))}</span>${period ? `<span>${esc(period)}</span>` : ""}</span></span>
<span class="risk ${risk.toLowerCase() === "high" || risk.toLowerCase() === "alta" ? "high" : ""}">${esc(risk)}</span>
</button>`;
}).join("");
}
function factEntries(item) {
const data = isObject(item.data) ? item.data : {};
const omitted = new Set(["summary", "description", "target_artifact", "target_id_field", "target_record_id", "target_field", "edit_hint"]);
return Object.entries(data).filter(([key, value]) => !omitted.has(key) && value != null && value !== "");
}
function evidenceHtml(item) {
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
if (!evidence.length) return `<p class="lead">${esc(copy().noEvidence)}</p>`;
return `<div class="evidence-list">${evidence.map((entry, index) => {
if (!isObject(entry)) return `<div class="evidence"><div class="evidence-title"><span>${esc(copy().evidence)} ${index + 1}</span></div><div class="evidence-meta">${esc(entry)}</div></div>`;
const title = entry.title || entry.authority || entry.source || entry.kind || `${copy().evidence} ${index + 1}`;
const locator = [entry.source_path, entry.document, entry.locator, entry.page != null ? `p. ${entry.page}` : "", entry.date].filter(Boolean).join(" · ");
const confidence = entry.confidence || entry.status || "";
const excerpt = entry.excerpt || entry.summary || entry.support || "";
return `<div class="evidence"><div class="evidence-title"><span>${esc(title)}</span><span>${esc(confidence)}</span></div>${locator ? `<div class="evidence-meta">${esc(locator)}</div>` : ""}${excerpt ? `<p class="evidence-excerpt">${esc(excerpt)}</p>` : ""}</div>`;
}).join("")}</div>`;
}
function explicitDocumentHints(item) {
const hints = [];
const sources = [item, isObject(item.data) ? item.data : {}, ...(Array.isArray(item.evidence) ? item.evidence : [])];
for (const source of sources) {
if (!isObject(source)) continue;
for (const key of ["requested_document", "required_document"]) if (typeof source[key] === "string" && source[key].trim()) hints.push(source[key].trim());
for (const key of ["requested_documents", "missing_documents", "support_documents"]) if (Array.isArray(source[key])) hints.push(...source[key].filter((value) => typeof value === "string" && value.trim()));
}
return [...new Set(hints)];
}
function ensureDecision(item, action) {
const current = state.decisions[item.id] || {};
const nextAction = action || current.action || item.recommended_action || item.allowed_actions?.[0] || "mark_unclear";
state.decisions[item.id] = {
item_id: item.id,
action: nextAction,
reviewer_note: current.reviewer_note || "",
edit_value: current.edit_value || "",
requested_documents: Array.isArray(current.requested_documents) && current.requested_documents.length
? current.requested_documents
: nextAction === "request_more_documents" ? explicitDocumentHints(item) : [],
};
persistDraft();
return state.decisions[item.id];
}
function decisionSection(item) {
const decision = decisionFor(item);
const allowed = Array.isArray(item.allowed_actions) ? item.allowed_actions : [];
const action = decision?.action || "";
const disabled = state.busy ? " disabled" : "";
const editField = action === "edit" ? `<div class="field"><label for="edit-value">${esc(copy().proposedEdit)}</label><textarea id="edit-value" data-field="edit_value" placeholder="${esc(copy().editPlaceholder)}"${disabled}>${esc(decision?.edit_value || "")}</textarea></div>` : "";
const docsField = action === "request_more_documents" ? `<div class="field"><label for="requested-documents">${esc(copy().requestedDocuments)}</label><textarea id="requested-documents" data-field="requested_documents"${disabled}>${esc((decision?.requested_documents || explicitDocumentHints(item)).join("\n"))}</textarea></div>` : "";
const impact = action === "edit"
? copy().editImpact
: action === "request_more_documents" || action === "mark_unclear"
? copy().blockerImpact
: copy().decisionImpact;
return `<section class="section">
<p class="section-title">${esc(copy().professionalDecision)}</p>
<div class="decision-actions">${allowed.map((value) => `<button type="button" class="decision-action ${action === value ? "selected" : ""}" data-action="${esc(value)}"${disabled}>${esc(copy().actions[value] || humanize(value))}</button>`).join("")}</div>
${editField}${docsField}
<div class="field"><label for="reviewer-note">${esc(copy().reviewerNote)}</label><textarea id="reviewer-note" data-field="reviewer_note" placeholder="${esc(copy().reviewerPlaceholder)}">${esc(decision?.reviewer_note || "")}</textarea></div>
<div class="impact">${esc(impact)}</div>
</section>`;
}
function renderDetail() {
const node = document.getElementById("detail");
const item = itemById(state.selectedId);
if (!item) {
node.innerHTML = `<div class="empty"><strong>${esc(copy().selectTitle)}</strong>${esc(copy().selectBody)}</div>`;
return;
}
const status = statusFor(item);
const entries = factEntries(item);
const summary = item.summary || item.description || (isObject(item.data) ? item.data.summary || item.data.description : "") || copy().noSummary;
node.innerHTML = `
<header class="detail-head"><div><p class="item-type">${esc(copy().items[item.item_type] || humanize(item.item_type))}</p><h3>${esc(item.title)}</h3></div><span class="status ${esc(status)}">${esc(copy().statuses[status] || humanize(status))}</span></header>
${safeguardsHtml()}
<section class="section"><p class="section-title">${esc(copy().summary)}</p><p class="lead">${esc(summary)}</p></section>
${entries.length ? `<section class="section"><p class="section-title">${esc(copy().controlData)}</p><div class="facts">${entries.map(([key, value]) => `<div class="fact"><span class="fact-key">${esc(fieldLabel(key))}</span><span class="fact-value">${esc(compactValue(value))}</span></div>`).join("")}</div></section>` : ""}
<section class="section"><p class="section-title">${esc(copy().evidenceSources)}</p>${evidenceHtml(item)}</section>
${decisionSection(item)}`;
}
function renderArtifacts() {
const node = document.getElementById("artifacts");
const outputs = Array.isArray(state.payload?.final_artifacts?.outputs) ? state.payload.final_artifacts.outputs : [];
const blockers = Array.isArray(state.payload?.final_artifacts?.blockers) ? state.payload.final_artifacts.blockers.length : 0;
node.innerHTML = `<div class="artifact-head"><h2>${esc(copy().packageOutputs)}</h2><span class="count">${outputs.length} ${esc(copy().files)} · ${blockers} ${esc(copy().blockers)}</span></div>${outputs.length ? `<div class="artifact-list">${outputs.map((output) => `<div class="artifact"><strong title="${esc(output.path || output.title)}">${esc(output.path || output.title || copy().output)}</strong><span>${esc([output.kind, localizedValue(output.status)].filter(Boolean).join(" · "))}</span></div>`).join("")}</div>` : `<span class="count">${esc(copy().outputsPending)}</span>`}`;
}
function render() {
renderRunbar();
renderFilters();
renderRows();
renderDetail();
renderArtifacts();
for (const button of [document.getElementById("save"), document.getElementById("apply")]) button.disabled = state.busy;
document.getElementById("cancel").disabled = state.busy || !state.dirty;
document.getElementById("recommended").disabled = state.busy;
}
function decisionInputs() {
return Object.values(state.decisions).filter((decision) => decision?.item_id && decision?.action).map((decision) => {
const result = { item_id: decision.item_id, action: decision.action };
if (decision.reviewer_note?.trim()) result.reviewer_note = decision.reviewer_note.trim();
if (decision.edit_value?.trim()) result.edit_value = decision.edit_value.trim();
if (Array.isArray(decision.requested_documents) && decision.requested_documents.length) result.requested_documents = decision.requested_documents;
return result;
});
}
function validateDecisions(decisions) {
for (const decision of decisions) {
if (decision.action === "edit" && !decision.edit_value) throw new Error(copy().correctionRequired(itemById(decision.item_id)?.title || decision.item_id));
}
}
function toolArgs(includeArtifacts = false) {
const token = state.payload.run_intake?.persistence_token || null;
const args = {
persistence_token: token,
...(token ? {} : {
run_intake: state.payload.run_intake || null,
review_payload: state.payload.review_payload,
ui_decisions: state.payload.ui_decisions || null,
}),
decisions: decisionInputs(),
decision_source: "mcp_widget",
};
if (includeArtifacts && !token) args.final_artifacts = state.payload.final_artifacts || null;
return args;
}
function exportPayload() {
const review = state.payload.review_payload;
const now = new Date().toISOString();
const decisions = decisionInputs().map((decision) => {
const item = itemById(decision.item_id);
return {
item_id: decision.item_id,
item_type: item?.item_type || "",
title: item?.title || "",
action: decision.action,
status: ACTION_STATUSES[decision.action] || "reviewed",
decided_at: now,
...(decision.reviewer_note ? { reviewer_note: decision.reviewer_note } : {}),
...(decision.edit_value ? { edit_value: decision.edit_value } : {}),
...(decision.requested_documents?.length ? { requested_documents: decision.requested_documents } : {}),
};
});
return {
schema_version: review.schema_version || "1.0",
plugin: "previdenza-inps",
workflow: review.workflow,
run_id: review.run_id,
decided_at: decisions.length ? now : null,
decision_source: "mcp_widget_export",
review_payload_path: "review_payload.json",
decisions,
decision_count: decisions.length,
item_count: items().length,
status: decisions.length === 0 ? "pending_review" : decisions.length === items().length ? "reviewed" : "partial_review",
};
}
async function copyJson() {
const text = `${JSON.stringify(exportPayload(), null, 2)}\n`;
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
else {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
document.execCommand("copy");
area.remove();
}
state.message = copy().jsonCopied;
state.messageKind = "ok";
renderRunbar();
}
function parseToolResult(result) {
if (isObject(result?.structuredContent)) return result.structuredContent;
if (isObject(result) && (result.ok != null || result.ui_decisions)) return result;
const text = Array.isArray(result?.content) ? result.content.find((entry) => entry?.type === "text")?.text : "";
if (text) {
try { return JSON.parse(text); } catch { return { ok: false, error: text }; }
}
return result || {};
}
async function runWrite(kind) {
const decisions = decisionInputs();
try { validateDecisions(decisions); }
catch (error) { state.message = error.message; state.messageKind = "error"; renderRunbar(); return; }
const isApply = kind === "apply";
const tool = state.payload.decision_policy?.[isApply ? "apply_tool" : "save_tool"] || (isApply ? "apply_previdenza_inps_decisions" : "save_previdenza_inps_decisions");
if (typeof window.openai?.callTool !== "function") {
await copyJson();
state.message = isApply ? copy().bridgeApply : copy().bridgeSave;
state.messageKind = isApply ? "error" : "ok";
renderRunbar();
return;
}
state.busy = true;
state.message = isApply ? copy().applying : copy().saving;
state.messageKind = "";
render();
try {
const result = parseToolResult(await window.openai.callTool(tool, toolArgs(isApply)));
if (!result?.ok) throw new Error(result?.error || copy().operationFailed);
const persisted = result.persisted === true;
if (persisted) {
if (result.ui_decisions) state.payload.ui_decisions = result.ui_decisions;
if (result.applied_decisions) state.payload.applied_decisions = result.applied_decisions;
if (result.final_artifacts) state.payload.final_artifacts = result.final_artifacts;
state.dirty = false;
clearDraft();
} else {
state.dirty = true;
}
state.message = result.message || (isApply ? copy().applied : copy().saved);
state.messageKind = persisted ? "ok" : "";
} catch (error) {
state.message = `${error.message || error} ${copy().recovery}`;
state.messageKind = "error";
} finally {
state.busy = false;
render();
}
}
document.getElementById("search").addEventListener("input", (event) => {
state.query = event.target.value;
renderRows();
renderDetail();
});
document.getElementById("state-filter").addEventListener("change", (event) => {
state.status = event.target.value;
renderRows();
renderDetail();
});
document.getElementById("type-filters").addEventListener("click", (event) => {
const button = event.target.closest("button[data-type]");
if (!button) return;
state.type = button.dataset.type;
renderFilters();
renderRows();
renderDetail();
});
document.getElementById("rows").addEventListener("click", (event) => {
const button = event.target.closest("button[data-id]");
if (!button) return;
state.selectedId = button.dataset.id;
renderRows();
renderDetail();
});
document.getElementById("detail").addEventListener("click", (event) => {
if (state.busy) return;
const button = event.target.closest("button[data-action]");
if (!button) return;
const item = itemById(state.selectedId);
if (!item) return;
ensureDecision(item, button.dataset.action);
render();
});
document.getElementById("detail").addEventListener("input", (event) => {
if (state.busy) return;
const field = event.target.closest("[data-field]");
const item = itemById(state.selectedId);
if (!field || !item) return;
const decision = ensureDecision(item);
if (field.dataset.field === "requested_documents") {
decision.requested_documents = field.value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
} else decision[field.dataset.field] = field.value;
persistDraft();
renderRunbar();
renderRows();
});
document.getElementById("recommended").addEventListener("click", () => {
if (state.busy) return;
for (const item of visibleItems()) ensureDecision(item, item.recommended_action || item.allowed_actions?.[0]);
render();
});
document.getElementById("copy").addEventListener("click", () => copyJson().catch((error) => {
state.message = error.message || String(error);
state.messageKind = "error";
renderRunbar();
}));
document.getElementById("save").addEventListener("click", () => runWrite("save"));
document.getElementById("apply").addEventListener("click", () => runWrite("apply"));
document.getElementById("cancel").addEventListener("click", cancelChanges);
state.payload = incomingPayload();
applyStaticCopy();
loadDecisions();
render();
</script>
</body>
</html>
SHA-256: 988fd301385dc3854796c3684292d333c510319e89fbe18cee63e774eb9a19f4