// Generated, do not edit. sha256:9d5e374482da80f40653036b5e206e6d6ff98a8d9dbf55c030df645a418ea367 import { listAiToolCalls, countAiToolCalls, lastAiToolCallAt, aiToolCallMessages, } from '../data/ai-audit.js'; import { tableViewCfg, getPref, setPref } from '../data/preferences.js'; import { t, localeTag } from '../i18n/index.js'; import { fmtInt } from '../ui/format.js'; import { esc } from '../ui/escape.js'; import { alertHtml } from '../ui/alert.js'; import { renderTable } from '../ui/table.js'; import { refreshAttention } from '../ui/attention.js'; import { CHEVRON, COPY, CHECK } from '../ui/icons.js'; import { expanders } from '../ui/expand.js'; import * as dialog from '../ui/dialog.js'; import { cachedPortfolioName } from '../portfolio.js'; import { CONFIG } from '../../shared/config.js'; function moment(iso) { if (!iso) return ''; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleString(localeTag(), { dateStyle: 'short', timeStyle: 'medium' }); } function dag(iso) { if (!iso) return ''; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleDateString(localeTag(), { dateStyle: 'short' }); } function deelGroep(velden) { if (velden == null) return t('ai.sharedUnknown'); return velden.length ? t('ai.sharedSome') : t('ai.sharedNothing'); } function gedeeld(velden) { if (velden == null) return `${t('ai.sharedUnknown')}`; if (!velden.length) return `${t('ai.sharedNothing')}`; return velden.map(v => `${esc(v)}`).join(', '); } function notitie(json) { if (!json) return ''; try { const args = JSON.parse(json); if (args && typeof args.note === 'string' && args.note) return esc(args.note); } catch { } return ''; } async function koppelGegevens() { try { const res = await fetch('/api/mcp/config'); if (!res.ok) return null; const cfg = await res.json(); return cfg && typeof cfg.snippet === 'string' ? cfg : null; } catch { return null; } } async function dryTools() { try { const res = await fetch('/api/mcp/tools'); if (!res.ok) return []; const data = await res.json(); return Array.isArray(data.tools) ? data.tools : []; } catch { return []; } } const scopeTekst = scope => { try { return t(`ai.dryScope.${scope}`); } catch { return scope; } }; const gezieneVerwijzingen = new Set(); const VERWIJZING = /\b[a-z]+:[0-9a-f]{6}\b/g; function onthoudVerwijzingen(tekst) { for (const m of String(tekst || '').matchAll(VERWIJZING)) gezieneVerwijzingen.add(m[0]); } const VERWIJZING_VELDEN = new Set(['handle']); function dryArgVelden(tool) { const schema = tool && tool.inputSchema; const props = schema && schema.properties ? Object.entries(schema.properties) : []; if (!props.length) return ''; const verplicht = new Set(Array.isArray(schema.required) ? schema.required : []); const velden = props.map(([naam, spec]) => { const nodig = verplicht.has(naam); const label = `${esc(naam)}${nodig ? ` (${t('ai.dryArgRequired')})` : ''}`; const bediening = Array.isArray(spec.enum) ? `` : tekstVeld(naam, spec); return ``; }).join(''); const hulp = props.some(([naam]) => VERWIJZING_VELDEN.has(naam)) ? `
${ gezieneVerwijzingen.size ? t('ai.dryArgHandleSome') : t('ai.dryArgHandleNone')}
` : ''; return `
${t('ai.dryArgs')}
${velden}
${hulp}`; } function tekstVeld(naam, spec) { const suggesties = VERWIJZING_VELDEN.has(naam) ? [...gezieneVerwijzingen].sort() : []; const lijstId = `dry-lijst-${naam}`; return ` ${suggesties.length ? `${suggesties.map(v => ``).join('')}` : ''}`; } function dryBlok(tools, expandAttr) { if (!tools.length) return ''; const opties = tools.map(x => ``).join(''); return `
${CHEVRON}${t('ai.dryTitle')}
${t('ai.dryIntro')}
${dryArgVelden(tools[0])}
`; } function dryUitslag(r) { const detail = r.detail ? `
${esc(r.detail)}
` : ''; return `
${t('ai.result')}
${ r.outcome === 'error' ? `${t('ai.error')}` : t(`ai.${r.outcome}`)}
${t('ai.shared')}
${gedeeld(r.fieldsShared)}
${t('ai.characters')}
${ r.charactersOut == null ? '' : fmtInt(r.charactersOut)}
${detail}
${t('ai.dryResult')}
${tekstBlok(r.text)}`; } function tekstBlok(tekst) { return `
${esc(tekst)}
`; } const gesprekLade = (inhoud) => `
${inhoud}
`; function gesprekBlok(regels) { if (!regels.length) return `
${t('ai.noConversation')}
`; const blokken = regels.map(m => { const label = t(m.kind === 'request' ? 'ai.msgRequest' : 'ai.msgResponse'); return `
${label}, ${esc(moment(m.occurredAt))}
${tekstBlok(m.text)}
`; }).join(''); return blokken; } function bindDry(el, tools) { const knop = el.querySelector('#dry-run'); if (!knop) return; const keuze = el.querySelector('#dry-tool'); const melding = el.querySelector('#dry-melding'); const uitslag = el.querySelector('#dry-uitslag'); const argVak = el.querySelector('#dry-args'); const toolVan = naam => tools.find(x => x.name === naam) || null; keuze.addEventListener('change', () => { argVak.innerHTML = dryArgVelden(toolVan(keuze.value)); uitslag.innerHTML = ''; melding.textContent = ''; }); function werkSuggestiesBij() { for (const veld of argVak.querySelectorAll('[data-arg]')) { if (!VERWIJZING_VELDEN.has(veld.dataset.arg)) continue; const lijstId = `dry-lijst-${veld.dataset.arg}`; let lijst = argVak.querySelector(`#${lijstId}`); if (!lijst) { lijst = document.createElement('datalist'); lijst.id = lijstId; veld.after(lijst); veld.setAttribute('list', lijstId); } lijst.innerHTML = [...gezieneVerwijzingen].sort() .map(v => ``).join(''); } } function argumenten() { const uit = {}; for (const veld of argVak.querySelectorAll('[data-arg]')) { const w = veld.value.trim(); if (w) uit[veld.dataset.arg] = w; } return uit; } function ontbreekt(tool) { const nodig = tool?.inputSchema?.required; if (!Array.isArray(nodig)) return false; const ingevuld = argumenten(); return nodig.some(naam => !ingevuld[naam]); } knop.addEventListener('click', async () => { if (ontbreekt(toolVan(keuze.value))) { melding.textContent = t('ai.dryNeedsArg'); return; } knop.disabled = true; melding.textContent = t('ai.dryBusy'); try { const res = await fetch('/api/mcp/dry-run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool: keuze.value, arguments: argumenten() }), }); if (!res.ok) throw new Error(t('ai.dryFail')); const r = await res.json(); uitslag.innerHTML = dryUitslag(r); melding.textContent = ''; const voor = gezieneVerwijzingen.size; onthoudVerwijzingen(r.text); if (gezieneVerwijzingen.size !== voor) werkSuggestiesBij(); } catch (e) { melding.textContent = e.message || t('ai.dryFail'); } finally { knop.disabled = false; } }); } const cel = (kop, waarde) => `${kop}${waarde}`; function schuifCel(kop, id, aan, hint) { return cel(kop, `` + `` + ``); } function tellerCellen(telling, laatste) { if (!telling) return ''; return cel(t('ai.total'), fmtInt(telling.total)) + cel(t('ai.ok'), fmtInt(telling.byOutcome.ok)) + cel(t('ai.refused'), fmtInt(telling.byOutcome.refused)) + cel(t('ai.error'), fmtInt(telling.byOutcome.error)) + cel(t('ai.lastAt'), esc(moment(laatste))); } function bedieningsBlok(standen, telling, laatste) { const { actief, lezen, schrijven, besturen, onbewerkt } = standen; return `` + `` + schuifCel(t('ai.activeHead'), 'ai-active', actief, actief ? t('ai.activeOn') : t('ai.activeOff')) + schuifCel(t('ai.switchHead'), 'ai-read', lezen, lezen ? t('ai.switchOn') : t('ai.switchOff')) + schuifCel(t('ai.writeHead'), 'ai-write', schrijven, schrijven ? t('ai.writeOn') : t('ai.writeOff')) + schuifCel(t('ai.controlHead'), 'ai-control', besturen, besturen ? t('ai.controlOn') : t('ai.controlOff')) + schuifCel(t('ai.rawHead'), 'ai-raw', onbewerkt, onbewerkt ? t('ai.rawOn') : t('ai.rawOff')) + `` + `${tellerCellen(telling, laatste)}` + ``; } function precisieBlok(standen) { const { emmerAandeel, emmerTotaal, emmerBedrag, emmerAantal } = standen; return `
${t('ai.precisionTitle')}
${t('ai.precisionIntro')}
${schuifCel(t('ai.sharesHead'), 'ai-bshares', emmerAandeel, emmerAandeel ? t('ai.sharesOn') : t('ai.sharesOff'))} ${schuifCel(t('ai.totalsHead'), 'ai-btotals', emmerTotaal, emmerTotaal ? t('ai.totalsOn') : t('ai.totalsOff'))} ${schuifCel(t('ai.amountsHead'), 'ai-bamounts', emmerBedrag, emmerBedrag ? t('ai.amountsOn') : t('ai.amountsOff'))} ${schuifCel(t('ai.quantitiesHead'), 'ai-bquantities', emmerAantal, emmerAantal ? t('ai.quantitiesOn') : t('ai.quantitiesOff'))}
${t('ai.precisionCombo')}
`; } function koppelBlok(cfg, expandAttr, bediening, standen) { if (!cfg) return `
${t('ai.connectFail')}
`; return `
${CHEVRON} ${t('ai.connectTitle')} ${bediening}
${esc(portefeuilleRegel())}
${t('ai.revoke')}
${t('ai.rawHint')}
${precisieBlok(standen)}
${t('ai.connectIntro')}
JSON
${esc(cfg.snippet)}
${t('ai.connectFile')} ${t('ai.connectRestart')} ${t('ai.controlHint')}
`; } function bindSchuif(el, { wrapId, vakId, standId, scope, key, aanTekst, uitTekst, faalTekst, bevestigAan = null }) { const wrap = el.querySelector(`#${wrapId}`); if (!wrap) return; const vak = el.querySelector(`#${vakId}`); const stand = el.querySelector(`#${standId}`); const toon = (aan, melding = '') => { if (vak) vak.checked = aan; wrap.setAttribute('aria-checked', aan ? 'true' : 'false'); wrap.title = aan ? aanTekst : uitTekst; if (stand) stand.textContent = melding; }; wrap.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const aan = wrap.getAttribute('aria-checked') !== 'true'; if (aan && bevestigAan && !await bevestigAan()) return; toon(aan); try { await setPref(scope, key, aan ? 1 : 0); } catch { toon(!aan, faalTekst); } }); } function bindSchuiven(el) { bindSchuif(el, { wrapId: 'ai-active-wrap', vakId: 'ai-active', standId: 'ai-active-stand', scope: 'ai', key: 'enabled', aanTekst: t('ai.activeOn'), uitTekst: t('ai.activeOff'), faalTekst: t('ai.switchFail'), }); bindSchuif(el, { wrapId: 'ai-read-wrap', vakId: 'ai-read', standId: 'ai-read-stand', scope: 'ai', key: 'allow_read', aanTekst: t('ai.switchOn'), uitTekst: t('ai.switchOff'), faalTekst: t('ai.switchFail'), }); bindSchuif(el, { wrapId: 'ai-write-wrap', vakId: 'ai-write', standId: 'ai-write-stand', scope: 'ai', key: 'allow_write', aanTekst: t('ai.writeOn'), uitTekst: t('ai.writeOff'), faalTekst: t('ai.switchFail'), }); bindSchuif(el, { wrapId: 'ai-control-wrap', vakId: 'ai-control', standId: 'ai-control-stand', scope: 'ai', key: 'allow_control', aanTekst: t('ai.controlOn'), uitTekst: t('ai.controlOff'), faalTekst: t('ai.switchFail'), }); bindSchuif(el, { wrapId: 'ai-raw-wrap', vakId: 'ai-raw', standId: 'ai-raw-stand', scope: 'ai', key: 'allow_raw', aanTekst: t('ai.rawOn'), uitTekst: t('ai.rawOff'), faalTekst: t('ai.switchFail'), bevestigAan: () => dialog.confirm({ message: t('ai.rawConfirm').replace('{naam}', cachedPortfolioName() || '?'), danger: true, }), }); for (const [id, key] of [ ['ai-bshares', 'bucket_shares'], ['ai-btotals', 'bucket_totals'], ['ai-bamounts', 'bucket_amounts'], ['ai-bquantities', 'bucket_quantities'], ]) { const stam = key.replace('bucket_', ''); bindSchuif(el, { wrapId: `${id}-wrap`, vakId: id, standId: `${id}-stand`, scope: 'ai', key, aanTekst: t(`ai.${stam}On`), uitTekst: t(`ai.${stam}Off`), faalTekst: t('ai.switchFail'), }); } } function portefeuilleRegel() { const naam = cachedPortfolioName(); return naam ? t('ai.scope').replace('{naam}', naam) : t('ai.rawHint'); } const BEVESTIGING_MS = 2000; function bindKopieer(el, cfg) { const knop = el.querySelector('#mcp-config-copy'); if (!knop || !cfg) return; const melding = el.querySelector('#mcp-config-melding'); let terug = null; const herstel = () => { knop.classList.remove('is-done'); knop.innerHTML = COPY; knop.setAttribute('aria-label', t('ai.connectCopy')); knop.setAttribute('title', t('ai.connectCopy')); if (melding) melding.textContent = ''; }; knop.addEventListener('click', () => { navigator.clipboard.writeText(cfg.snippet) .then(() => { knop.classList.add('is-done'); knop.innerHTML = CHECK; knop.setAttribute('aria-label', t('ai.connectCopied')); knop.setAttribute('title', t('ai.connectCopied')); if (melding) melding.textContent = ''; }) .catch(() => { knop.classList.remove('is-done'); knop.innerHTML = COPY; if (melding) melding.textContent = t('ai.connectCopyFail'); }) .finally(() => { clearTimeout(terug); terug = setTimeout(herstel, BEVESTIGING_MS); }); }); } let live = null; let liveGebonden = false; const gesprekken = new Map(); function bindLive() { if (liveGebonden) return; liveGebonden = true; document.addEventListener('ai-activity-changed', async () => { if (!live || !live.el || !live.el.isConnected) { live = null; return; } let regels, telling, laatste; try { [regels, telling, laatste] = await Promise.all([ listAiToolCalls(CONFIG.AI.AUDIT_PAGE_SIZE), countAiToolCalls(), lastAiToolCallAt(), ]); } catch { return; } if (!live.el.isConnected) { live = null; return; } live.tabel.refresh(regels); const tellers = live.el.querySelector('.ai-tellers'); if (tellers) tellers.innerHTML = tellerCellen(telling, laatste); await markeerGezien(regels); }); } async function markeerGezien(regels) { const hoogste = regels.reduce((n, r) => Math.max(n, Number(r.id) || 0), 0); if (!hoogste) return; try { await setPref('ai', 'seen_call_id', hoogste); await refreshAttention(); } catch { } } export async function render(el) { let regels, telling, laatste; try { [regels, telling, laatste] = await Promise.all([ listAiToolCalls(CONFIG.AI.AUDIT_PAGE_SIZE), countAiToolCalls(), lastAiToolCallAt(), ]); } catch (err) { el.innerHTML = alertHtml({ kind: 'error', body: err.message }); return; } const cfg = await koppelGegevens(); const tools = await dryTools(); const lezen = await getPref('ai', 'allow_read', 1).then(v => v !== 0).catch(() => true); const uitBijTwijfel = (sleutel) => getPref('ai', sleutel, 0).then(v => v === 1).catch(() => false); const aanBijTwijfel = (sleutel) => getPref('ai', sleutel, 1).then(v => v !== 0).catch(() => true); const standen = { actief: await getPref('ai', 'enabled', 1).then(v => v !== 0).catch(() => true), lezen, schrijven: await uitBijTwijfel('allow_write'), besturen: await uitBijTwijfel('allow_control'), onbewerkt: await uitBijTwijfel('allow_raw'), emmerAandeel: await aanBijTwijfel('bucket_shares'), emmerTotaal: await aanBijTwijfel('bucket_totals'), emmerBedrag: await aanBijTwijfel('bucket_amounts'), emmerAantal: await aanBijTwijfel('bucket_quantities'), }; const expand = await expanders('ai', { connect: !telling.total, dryRun: false }); if (!telling.total) { el.innerHTML = `${koppelBlok(cfg, expand.attr('connect'), bedieningsBlok(standen, null), standen)}${dryBlok(tools, expand.attr('dryRun'))} ${alertHtml({ kind: 'neutral', body: t('ai.notConnected'), extra: `
${t('ai.howTo')}
` })}`; bindSchuiven(el); bindKopieer(el, cfg); bindDry(el, tools); expand.bind(el); return; } el.innerHTML = `${koppelBlok(cfg, expand.attr('connect'), bedieningsBlok(standen, telling, laatste), standen)}` + `${dryBlok(tools, expand.attr('dryRun'))}
`; bindSchuiven(el); bindKopieer(el, cfg); bindDry(el, tools); expand.bind(el); gesprekken.clear(); const tabel = renderTable(el.querySelector('#tbl'), { columns: [ { key: 'when', label: t('ai.when'), sortable: true, sortValue: r => r.occurredAt || '', render: r => esc(moment(r.occurredAt)), group: { value: r => dag(r.occurredAt), label: v => v }, }, { key: 'what', label: t('ai.what'), sortable: true, text: true, sortValue: r => r.tool || '', render: r => { const note = notitie(r.arguments); const detail = r.detail ? `
${esc(r.detail)}
` : ''; const noteRegel = note ? `
"${note}"
` : ''; return `${esc(r.tool)}${noteRegel}${detail}`; }, group: { value: r => r.tool || '', label: v => v }, }, { key: 'who', label: t('ai.who'), sortable: true, sortValue: r => r.client || '', render: r => (r.client ? esc(r.client) : `${t('ai.unknownClient')}`), group: { value: r => r.client || t('ai.unknownClient'), label: v => v }, }, { key: 'shared', label: t('ai.shared'), sortable: true, text: true, sortValue: r => (r.fieldsShared == null ? -1 : r.fieldsShared.length), render: r => gedeeld(r.fieldsShared), group: { value: r => deelGroep(r.fieldsShared), label: v => v }, }, { key: 'characters', label: t('ai.characters'), align: 'num', sortable: true, sortValue: r => r.charactersOut ?? -1, render: r => (r.charactersOut == null ? '' : fmtInt(r.charactersOut)), }, { key: 'result', label: t('ai.result'), sortable: true, sortValue: r => r.outcome || '', render: r => (r.outcome === 'error' ? `${t(`ai.${r.outcome}`)}` : t(`ai.${r.outcome}`)), group: { value: r => t(`ai.${r.outcome}`), label: v => v }, }, ], rows: regels, idKey: 'id', rowExpansion: (row) => gesprekLade(gesprekken.get(row.id) ?? `
${t('ai.conversation')}
`), onExpansionMount: async (row, host) => { if (gesprekken.has(row.id)) { host.innerHTML = gesprekLade(gesprekken.get(row.id)); return; } try { gesprekken.set(row.id, gesprekBlok(await aiToolCallMessages(row.id))); } catch { host.innerHTML = gesprekLade(`
${t('ai.conversationFail')}
`); return; } host.innerHTML = gesprekLade(gesprekken.get(row.id)); }, ...(await tableViewCfg('ai', { key: 'when', dir: 'desc' })), noneLabel: t('table.none'), collapsibleGroups: true, emptyText: t('ai.noRows'), }); live = { el, tabel }; bindLive(); await markeerGezien(regels); }