@apiosk/mcp 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +84 -537
  2. package/assets/brand/apiosk-a-20260921.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-dark.ico +0 -0
  5. package/assets/brand/favicon-light.ico +0 -0
  6. package/assets/brand/favicon.ico +0 -0
  7. package/assets/brand/icon-192.png +0 -0
  8. package/assets/brand/icon-512.png +0 -0
  9. package/assets/brand/icon-maskable-512.png +0 -0
  10. package/assets/brand/inter-OFL.txt +93 -0
  11. package/assets/brand/inter-latin-400-normal.woff2 +0 -0
  12. package/assets/brand/inter-latin-500-normal.woff2 +0 -0
  13. package/assets/brand/inter-latin-600-normal.woff2 +0 -0
  14. package/assets/brand/mark-20260905-transparent.svg +1 -0
  15. package/assets/brand/mark-20260918.svg +1 -0
  16. package/assets/brand/mark-black-20260918.svg +1 -0
  17. package/assets/brand/mark-dark-20260905-transparent.png +0 -0
  18. package/assets/brand/mark-dark-20260918.png +0 -0
  19. package/assets/brand/mark-dark-96.png +0 -0
  20. package/assets/brand/mark-light-20260905-transparent.png +0 -0
  21. package/assets/brand/mark-light-20260918.png +0 -0
  22. package/assets/brand/mark-light-96.png +0 -0
  23. package/assets/brand/wordmark-black-320.png +0 -0
  24. package/assets/brand/wordmark-white-320.png +0 -0
  25. package/docs/branding.md +11 -0
  26. package/docs/marketplace-submission-2026-04-08.md +4 -0
  27. package/docs/marketplace-submission-2026-08-20.md +157 -0
  28. package/docs/openai-plugin-submission-2026-09-05.md +143 -0
  29. package/docs/sepa-rail.md +13 -13
  30. package/dxt.json +26 -22
  31. package/index.mjs +3 -1
  32. package/logo-optimized-light.png +0 -0
  33. package/package.json +15 -13
  34. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  35. package/plugin/apiosk/.mcp.json +8 -0
  36. package/plugin/apiosk/assets/icon-dark.png +0 -0
  37. package/plugin/apiosk/assets/icon.png +0 -0
  38. package/plugin/apiosk/assets/icon.svg +1 -0
  39. package/plugin/apiosk/assets/logo.png +0 -0
  40. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  41. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  42. package/server.json +98 -9
  43. package/server.mjs +258 -71
  44. package/src/approval-feedback.mjs +21 -0
  45. package/src/brand-routes.mjs +28 -0
  46. package/src/create-server.mjs +168 -9
  47. package/src/display-money.mjs +20 -0
  48. package/src/display-text.mjs +67 -0
  49. package/src/gateway-client.mjs +42 -0
  50. package/src/gateway-v2-ask.mjs +50 -0
  51. package/src/gateway-v2-card-account.mjs +23 -0
  52. package/src/gateway-v2-card-actions.mjs +67 -0
  53. package/src/gateway-v2-card-answer-text.mjs +130 -0
  54. package/src/gateway-v2-card-answer.mjs +68 -0
  55. package/src/gateway-v2-card-blocks.mjs +166 -0
  56. package/src/gateway-v2-card-body.mjs +259 -0
  57. package/src/gateway-v2-card-budget.mjs +21 -0
  58. package/src/gateway-v2-card-cbs.mjs +56 -0
  59. package/src/gateway-v2-card-choices.mjs +4 -0
  60. package/src/gateway-v2-card-clarification.mjs +25 -0
  61. package/src/gateway-v2-card-compact.mjs +34 -0
  62. package/src/gateway-v2-card-events.mjs +41 -0
  63. package/src/gateway-v2-card-presentation.mjs +125 -0
  64. package/src/gateway-v2-card-research.mjs +63 -0
  65. package/src/gateway-v2-card-result.mjs +32 -0
  66. package/src/gateway-v2-card-search.mjs +39 -0
  67. package/src/gateway-v2-card-sources.mjs +32 -0
  68. package/src/gateway-v2-card-style.mjs +76 -0
  69. package/src/gateway-v2-card-verdict.mjs +66 -0
  70. package/src/gateway-v2-card.mjs +91 -0
  71. package/src/gateway-v2-contracts.json +66 -0
  72. package/src/gateway-v2-instructions.md +113 -0
  73. package/src/gateway-v2-recovery.mjs +18 -0
  74. package/src/gateway-v2-report-links.mjs +14 -0
  75. package/src/gateway-v2-workflows.mjs +17 -0
  76. package/src/gateway-v2.mjs +179 -0
  77. package/src/oauth.mjs +603 -364
  78. package/src/observability.mjs +210 -0
  79. package/src/result-presentation.mjs +9 -0
  80. package/src/runtime.mjs +24 -3091
  81. package/src/settlement-disclosure.mjs +26 -0
  82. package/src/source-groups.mjs +91 -0
  83. package/src/source-value-format.mjs +25 -0
  84. package/src/tool-result.mjs +20 -0
  85. package/src/ui-bridge.mjs +185 -0
  86. package/src/well-known-routes.mjs +132 -0
  87. package/src/funding-options.mjs +0 -255
  88. package/src/gateway-management.mjs +0 -107
  89. package/src/listing-metadata.mjs +0 -287
  90. package/src/local-config.mjs +0 -256
  91. package/src/payment-guidance.mjs +0 -307
  92. package/src/wallet-store.mjs +0 -476
@@ -0,0 +1,34 @@
1
+ // The execution graph stays private; only consent, choices and results are visible.
2
+ export const V2_CARD_COMPACT = `
3
+ renderPlan=function(data){
4
+ const p=data.proposal;if(!p||['cancelled','succeeded','partial','failed','unsupported','needs_selection'].includes(data.status))return;
5
+ const details=p.step_details||[],subjects=[...new Set(details.map(d=>d.subject).filter(Boolean))];
6
+ const sources=[...new Map(details.filter(d=>d.source).map(d=>[d.source.directory_slug||d.source.slug||d.source.name,{...d.source,name:d.source.directory_name||d.source.name}])).values()];
7
+ const s=section(formatDisplayNarrative(subjects.join(' · ')||p.label||'Data request',data));s.classList.add('request-section');planSurface=s;
8
+ if(data.billing?.authorization_active||data.status==='running'){
9
+ s.append(el('p','compact-status','Getting your data…'));
10
+ s.append(el('p','meta','Approved up to '+money(p.max_total_atomic,p.currency,true)));return;
11
+ }
12
+ if(sources.length)s.append(el('p','meta',sources.length+(sources.length===1?' source':' sources')));
13
+ const price=el('div','request-price');price.append(el('span','price-label','Maximum total'),el('strong','',money(p.max_total_atomic,p.currency,true)));s.append(price);
14
+ s.append(el('p','meta','Approve once. The entire request stays within this amount.'));
15
+ const disclosure=el('details','compact-details'),list=el('div','compact-sources');disclosure.append(el('summary','','Details'));
16
+ for(const source of sources)list.append(sourceLine(source));disclosure.append(list);s.append(disclosure);
17
+ };
18
+ const renderExpandedCard=render;
19
+ render=function(data){
20
+ renderExpandedCard(data);if(!data||typeof data!=='object')return;
21
+ for(const section of sections.querySelectorAll(':scope > .section')){
22
+ const heading=section.querySelector('h3');
23
+ if(heading?.textContent==='Payment summary'){
24
+ const dutch=!!cbsAnnualView(data,data.context_view?.results||[]),charged=money(data.billing?.total_charged,data.billing?.currency),disclosure=el('details','compact-details');
25
+ disclosure.append(el('summary','',charged==null?(dutch?'Betalingsdetails':'Payment details'):(dutch?'Betaald ':'Charged ')+charged+(data.billing?.reserved&&data.billing.reserved!=='0'?' · Reserved '+money(data.billing.reserved,data.billing.currency):'')+(dutch?' · Betalingsdetails':' · Payment details')));
26
+ const grid=section.querySelector('.balances');if(grid)disclosure.append(grid);section.replaceChildren(disclosure);
27
+ }
28
+ }
29
+ for(const detail of data.proposal?.step_details||[]){if(detail.error?.message&&!(data.errors||[]).some(e=>e.message===detail.error.message))section('Needs attention').append(el('p','notice error',detail.error.message))}
30
+ if(planSurface){const disclosure=planSurface.querySelector('.compact-details');if(disclosure)planSurface.append(disclosure)}
31
+ if(typeof observeTask==='function')observeTask(data);
32
+ window.apiosk.resize();
33
+ };
34
+ `;
@@ -0,0 +1,41 @@
1
+ // Task-scoped SSE and recovery only read saved state; never authorize spending.
2
+ export const V2_CARD_EVENTS = `
3
+ let taskEvents=null,eventTask=null,eventUrl=null,eventRecovery=null,eventFailures=0;
4
+ function observeTask(data){
5
+ const ref=data.state?.state_ref,url=data.context_view?.events_url;
6
+ const idle=!data.context_view?.worker_active&&['needs_input','cancelled','succeeded','partial','failed','unsupported'].includes(data.status);
7
+ if(eventTask!==ref){eventFailures=0;if(eventRecovery){clearTimeout(eventRecovery);eventRecovery=null}}
8
+ if(eventTask===ref&&eventUrl===url&&taskEvents&&!idle)return;
9
+ if(taskEvents){taskEvents.close();taskEvents=null}
10
+ if(idle){if(eventRecovery){clearTimeout(eventRecovery);eventRecovery=null}return}
11
+ if(!ref||!url||typeof EventSource==='undefined'||eventRecovery||eventFailures>=3)return;
12
+ let parsed;try{parsed=new URL(url)}catch{return}
13
+ if(parsed.origin!=='__APIOSK_GATEWAY_ORIGIN__'||parsed.pathname!=='/v2/tasks/'+ref+'/events')return;
14
+ eventTask=ref;eventUrl=url;
15
+ const recover=()=>{
16
+ if(output?.state?.state_ref!==ref||taskEvents!==stream)return;
17
+ stream.close();taskEvents=null;eventFailures++;
18
+ showFeedback('Live updates are unavailable. Checking the saved request…');
19
+ eventRecovery=setTimeout(async()=>{
20
+ eventRecovery=null;if(output?.state?.state_ref!==ref)return;
21
+ const refreshed=await refreshTask(false);
22
+ if(output?.state?.state_ref!==ref)return;
23
+ if(!refreshed||eventFailures>=3)showFeedback('Use Check status to refresh the saved request.');
24
+ if(refreshed)observeTask(output);
25
+ },2000*eventFailures);
26
+ };
27
+ let stream;try{stream=new EventSource(url);taskEvents=stream}catch{showFeedback('Use Check status to refresh the saved request.');return}
28
+ stream.addEventListener('task',event=>{
29
+ if(taskEvents!==stream||output?.state?.state_ref!==ref)return;
30
+ try{
31
+ const next=JSON.parse(event.data);
32
+ if(next.state?.state_ref!==ref||Number(next.state.revision)<Number(output.state.revision))return;
33
+ if(next.context_view?.events_path)next.context_view.events_url=new URL(next.context_view.events_path,parsed.origin).href;
34
+ const documents=[next.context_view,next.result,...(next.context_view?.results||[]),...(next.context_view?.conversation||[]).flatMap(t=>[t.output,t.output?.result,...(t.output?.results||[])])];
35
+ for(const doc of documents){const path=doc?.report?.download_path;if(typeof path==='string'&&path.startsWith('/v2/tasks/'+ref+'/'))doc.report.url=new URL(path,parsed.origin).href;const evidence=doc?.report?.evidence_download_path;if(typeof evidence==='string'&&evidence.startsWith('/v2/tasks/'+ref+'/reports/')&&evidence.includes('/evidence.zip?'))doc.report.evidence_url=new URL(evidence,parsed.origin).href}
36
+ acceptResponse(next);
37
+ }catch{recover()}
38
+ });
39
+ stream.onerror=recover;
40
+ }
41
+ `;
@@ -0,0 +1,125 @@
1
+ import { sourceCurrency } from './source-value-format.mjs';
2
+ import { answerValue, answerValueContext } from './gateway-v2-card-answer-text.mjs';
3
+ // The App's answer tables for version-1 analyses: `analysis.presentation`
4
+ // (table, comparison, chart) and, without one, only the records and fields the
5
+ // saved answer cites. Never a dump of a provider's raw rows
6
+ // (app/src/lib/domain/answer-presentation.ts). Keep in sync with the App.
7
+ export function presentationTable(p, results = [], locale = 'en-US') {
8
+ const columns = Array.isArray(p.columns) ? p.columns : [], rows = Array.isArray(p.rows) ? p.rows : [];
9
+ const currencyColumn = columns.findIndex(c => /^(currency|valuta)$/i.test(String(c?.value)));
10
+ return {
11
+ title: p.title, columns: columns.map(c => String(c?.value)), rows: rows.map(r => (r || []).map(c => c?.value)), locale,
12
+ displayRows: rows.map(row => (row || []).map((cell, i) => {
13
+ if (!cell || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(cell.value))) return cell?.value;
14
+ const context = cell.result_ref && cell.pointer ? answerValueContext({ result_ref: cell.result_ref, pointer: cell.pointer }, results) : null;
15
+ return answerValue(cell.value, context?.field || String(columns[i]?.value), context?.currency ?? sourceCurrency(row[currencyColumn]?.value), locale);
16
+ })),
17
+ note: p.returned_count !== undefined ? rows.length + ' selected from ' + p.returned_count + ' returned records. This is not necessarily the complete search.' : 'Based on saved source data. Missing values are unknown.',
18
+ };
19
+ }
20
+ function presentationEvidence(row, field, value) {
21
+ if (!row.has(field)) { row.set(field, value);return; }
22
+ const previous = row.get(field);
23
+ if (previous !== value) row.set(field, [...new Set([...(Array.isArray(previous) ? previous : [previous]), value])]);
24
+ }
25
+ export function legacyAnswerTables(question, results, analysis) {
26
+ if (analysis?.presentation || !analysis?.observations?.length || !/\b(overzicht|tabel|table|overview|list|lijst|welke|which|vergelijk|compare|comparison)\b/i.test(question || '')) return [];
27
+ const record = v => v && typeof v === 'object' && !Array.isArray(v) ? v : {}, groups = new Map();
28
+ for (const evidence of analysis.observations.flatMap(o => o?.evidence || [])) {
29
+ const doc = (results || []).map(record).find(d => d.result_ref === evidence?.result_ref), pointer = String(evidence?.pointer || '');
30
+ if (!doc || !pointer.startsWith('/data/')) continue;
31
+ const parts = pointer.split('/').slice(1).map(p => p.replaceAll('~1', '/').replaceAll('~0', '~'));
32
+ let node = { ...doc, data: doc.data ?? doc.preview_data }, groupPath = '', rowPath = '', field = parts.slice(1).join(' / '), rowRecord = {};
33
+ for (let i = 0; i < parts.length; i++) {
34
+ if (Array.isArray(node) && /^(0|[1-9]\d*)$/.test(parts[i])) { groupPath = JSON.stringify(parts.slice(0, i));rowPath = JSON.stringify(parts.slice(0, i + 1));field = parts.slice(i + 1).join(' / ');rowRecord = record(node[Number(parts[i])]);break; }
35
+ node = record(node)[parts[i]];
36
+ }
37
+ const key = JSON.stringify([evidence.result_ref, groupPath]);
38
+ let group = groups.get(key);
39
+ if (!group) { const source = record(doc.source), subject = record(doc.subject);group = { name: String(subject.label ?? source.name ?? source.provider ?? 'Answer'), columns: [], rows: new Map() };groups.set(key, group); }
40
+ if (!rowPath) {
41
+ group.columns = ['Field', 'Source value'];
42
+ const row = group.rows.get(pointer) ?? new Map([['Field', field]]);
43
+ presentationEvidence(row, 'Source value', evidence.value);group.rows.set(pointer, row);continue;
44
+ }
45
+ let row = group.rows.get(rowPath);
46
+ if (!row) { row = new Map();group.rows.set(rowPath, row); }
47
+ if (!group.columns.includes(field)) group.columns.push(field);
48
+ presentationEvidence(row, field, evidence.value);
49
+ const link = rowRecord.url ?? record(rowRecord.link).href ?? record(record(rowRecord._links).self).href;
50
+ if (typeof link === 'string' && /^https?:\/\//i.test(link)) { if (!group.columns.includes('Source')) group.columns.push('Source');row.set('Source', link); }
51
+ }
52
+ const humanize = label => label.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ');
53
+ const tables = [...groups.values()].map(g => ({ title: g.name, columns: g.columns.map(humanize), rows: [...g.rows.values()].map(r => g.columns.map(c => r.get(c))), note: 'Only records and fields cited in this saved answer. Full source data is available in Details.' }));
54
+ if (/\b(vergelijk|compare|comparison)\b/i.test(question) && tables.length > 1 && tables.every(t => t.columns[0] === 'Field') && new Set(tables.map(t => t.title)).size === tables.length) {
55
+ const fields = [...new Set(tables.flatMap(t => t.rows.map(r => String(r[0]))))];
56
+ return [{ title: 'Comparison', columns: ['Feature', ...tables.map(t => t.title)], rows: fields.map(f => [humanize(f), ...tables.map(t => t.rows.find(r => r[0] === f)?.[1])]), note: 'Only facts cited in the saved answer. Missing values are unknown; retain differences in units and reporting periods.' }];
57
+ }
58
+ return tables;
59
+ }
60
+
61
+ // ---- DOM, run inside the card only --------------------------------------------
62
+ function renderAnswerPresentation(question, results, analysis) {
63
+ const p = analysis?.presentation, tables = p && typeof p === 'object' && 'title' in p ? [presentationTable(p, results, answerLocale(question, analysis))] : legacyAnswerTables(question, results, analysis);
64
+ if (!tables.length) return null;
65
+ const wrap = el('div', 'answer-blocks');wrap.setAttribute('aria-label', 'Answer data');
66
+ for (const table of tables) wrap.append(selectedTable(table, p?.kind === 'chart'));
67
+ return wrap;
68
+ }
69
+ function selectedTable(table, chart) {
70
+ const wrap = el('div', 'answer-table');let shown = 10;
71
+ const draw = () => {
72
+ const visible = { ...table, rows: table.rows.slice(0, shown) }, parts = [], figure = chart ? presentationChart(visible, table) : null;
73
+ if (figure) parts.push(figure);
74
+ parts.push(sourceTable(visible));
75
+ if (shown < table.rows.length) parts.push(blockMore('Show more selected rows (' + (table.rows.length - shown) + ' remaining)', () => { shown += 10;draw(); }));
76
+ if (shown > 10) parts.push(blockMore('Show fewer rows', () => { shown = 10;draw(); }));
77
+ wrap.replaceChildren(...parts);
78
+ };
79
+ draw();
80
+ return wrap;
81
+ }
82
+ function presentationChart(table, scale) {
83
+ const points = t => t.rows.filter(r => typeof r[0] === 'string' && typeof r[1] === 'number' && Number.isFinite(r[1]));
84
+ const shown = points(table);
85
+ if (!shown.length || shown.length !== table.rows.length) return null;
86
+ const all = points(scale).map(r => r[1]), min = Math.min(0, ...all), max = Math.max(0, ...all), range = max - min || 1, zero = (0 - min) / range * 100;
87
+ const figure = el('figure', 'answer-block bar-chart');figure.setAttribute('aria-label', table.title + ' chart');
88
+ figure.append(el('figcaption', '', table.title + ' · ' + table.columns[1]));
89
+ shown.forEach(([label, value], i) => {
90
+ const row = el('div', 'bar-row'), line = el('div', 'bar-line');
91
+ line.append(el('span', 'bar-label', label), el('span', 'fact-value', String(table.displayRows?.[i]?.[1] ?? new Intl.NumberFormat(table.locale ?? 'nl-NL', { maximumFractionDigits: 10 }).format(value))));
92
+ row.append(line, answerBar(zero, (value - min) / range * 100));figure.append(row);
93
+ });
94
+ return figure;
95
+ }
96
+ function presentationCell(value, key, locale = 'nl-NL') {
97
+ if (value == null || value === '') return el('span', '', '—');
98
+ if (Array.isArray(value)) { const list = el('ul', 'cell-list');for (const item of value) { const li = el('li');li.append(presentationCell(item, key, locale));list.append(li); }return list; }
99
+ if (typeof value === 'number' || typeof value === 'string' && /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return el('span', '', answerValue(value, key, null, locale));
100
+ if (typeof value === 'boolean') return el('span', '', value ? 'Yes' : 'No');
101
+ if (typeof value !== 'string') return el('span', '', JSON.stringify(displayData(value, key)));
102
+ if (/^https?:\/\//i.test(value)) { try { const url = new URL(value);if (!url.username && !url.password) { const link = bodyLink(url.href, 'Open source ↗');link.title = url.href;link.setAttribute('aria-label', 'Open source: ' + url.hostname);return link; } } catch {} }
103
+ return el('span', '', formatDisplayText(value, key));
104
+ }
105
+ function sourceTable(table) {
106
+ const s = el('section', 'answer-block');s.setAttribute('aria-label', table.title);
107
+ s.append(el('h3', '', table.title), el('p', 'meta', table.rows.length + ' rows shown' + (table.total !== undefined ? ' · ' + new Intl.NumberFormat('nl-NL').format(table.total) + ' total matches' : '') + '. ' + table.note));
108
+ if (!table.rows.length) { s.append(el('p', '', 'No records returned.'));return s; }
109
+ const link = row => { for (const value of row.slice(1)) { if (typeof value !== 'string' || !/^https?:\/\//i.test(value)) continue;try { const url = new URL(value);if (!url.username && !url.password) return url.href; } catch {} }return null; };
110
+ const region = blockTable(table.title + ' table', table.columns, table.rows.map((row, i) => {
111
+ const tr = el('tr'), href = typeof row[0] === 'string' && !/^https?:\/\//i.test(row[0]) ? link(row) : null;
112
+ row.forEach((value, j) => {
113
+ const td = el('td'), shown = table.displayRows?.[i]?.[j] ?? value, cell = presentationCell(shown, j === 0 || table.columns[j] !== 'Source value' ? table.columns[j] : String(row[0]), table.locale);
114
+ td.append(j === 0 && href ? bodyLink(href, cell.textContent) : cell);tr.append(td);
115
+ });
116
+ return tr;
117
+ }));
118
+ if (table.columns.length > 3) region.querySelector('table')?.setAttribute('style', 'min-width:' + table.columns.length * 150 + 'px');
119
+ s.append(region);
120
+ return s;
121
+ }
122
+
123
+ export const V2_CARD_PRESENTATION = [
124
+ presentationTable, presentationEvidence, legacyAnswerTables, renderAnswerPresentation, selectedTable, presentationChart, presentationCell, sourceTable,
125
+ ].map(fn => fn.toString()).join('\n');
@@ -0,0 +1,63 @@
1
+ import { V2_CARD_CBS } from './gateway-v2-card-cbs.mjs';
2
+ import { V2_CARD_VERDICT } from './gateway-v2-card-verdict.mjs';
3
+ import { V2_CARD_ANSWER } from './gateway-v2-card-answer.mjs';
4
+ import { V2_CARD_ANSWER_TEXT } from './gateway-v2-card-answer-text.mjs';
5
+ import { V2_CARD_BLOCKS } from './gateway-v2-card-blocks.mjs';
6
+ import { V2_CARD_PRESENTATION } from './gateway-v2-card-presentation.mjs';
7
+ // All source values remain text nodes. This layer has no purchasing authority.
8
+ export const V2_CARD_RESEARCH = `
9
+ ${V2_CARD_CBS}
10
+ ${V2_CARD_VERDICT}
11
+ ${V2_CARD_ANSWER_TEXT}
12
+ ${V2_CARD_BLOCKS}
13
+ ${V2_CARD_PRESENTATION}
14
+ ${V2_CARD_ANSWER}
15
+ const renderSingleResult=renderResult;
16
+ const sourceDisclosureState=new Map();
17
+ function sourceGroupKey(result){const source=result?.source||{};return source.provider||source.name||source.url||'Source'}
18
+ function sourceMetric(group,names){for(const result of group){const body=displayData(result?.data!=null?result.data:result);if(!body||typeof body!=='object'||Array.isArray(body))continue;for(const name of names){const value=body[name];if(value!=null&&['string','number','boolean'].includes(typeof value))return value}}return null}
19
+ function sourceResultCount(group){const explicit=sourceMetric(group,['total_results','totalResults','total_count','totalCount','number_of_results','numberOfResults']);if(explicit!=null)return explicit;let count=0,found=false;for(const result of group){const body=displayData(result?.data!=null?result.data:result);for(const name of ['results','items','resultaten'])if(Array.isArray(body?.[name])){count+=body[name].length;found=true}if(body?.responses&&typeof body.responses==='object'){for(const response of Object.values(body.responses)){const total=response?.total?.value;if(typeof total==='number'){count+=total;found=true}else if(Array.isArray(response?.results)){count+=response.results.length;found=true}}}}return found?count:null}
20
+ function sourceSummaryCell(label,value){const cell=el('span','source-result-cell');cell.append(el('span','source-result-label',label),el('span','source-result-value',value==null||value===''?'—':value));return cell}
21
+ function renderSourceGroup(data,group){const source=group[0]?.source||{},details=el('details','source-result-group'),summary=el('summary','source-result-summary'),logo=sourceLogo(source);logo.classList.add('mini');summary.append(logo,el('span','source-result-name',source.name||source.provider||'Source'),sourceSummaryCell('Kind',sourceMetric(group,['kind'])),sourceSummaryCell('Results',sourceResultCount(group)),sourceSummaryCell('Items per page',sourceMetric(group,['items_per_page','itemsPerPage','page_size','pageSize'])),sourceSummaryCell('Page number',sourceMetric(group,['page_number','pageNumber','page'])),sourceSummaryCell('Start index',sourceMetric(group,['start_index','startIndex','offset'])));const body=el('div','source-result-details');for(const result of group){renderSingleResult({...data,result});const node=sections.lastElementChild;if(node)body.append(node)}details.append(summary,body);details.ontoggle=()=>window.apiosk.resize();return details}
22
+ function reportButton(label,url){const b=el('button','text-action',label);b.onclick=()=>window.apiosk.openLink(url);return b}
23
+ // The App's details drawer (GatewayV2OutputDetails): downloads, limitations and
24
+ // cited evidence above the source records. The answer text already carries the
25
+ // observations, so they are not repeated here.
26
+ function detailNotes(analysis,data,cbs){
27
+ const notes=el('div','detail-notes'),limitations=(analysis.limitations||[]).filter(t=>typeof t==='string'&&t.trim()),evidence=(analysis.observations||[]).flatMap(o=>o?.evidence||[]);
28
+ if(limitations.length){const list=el('ul');for(const limitation of limitations)list.append(el('li','',formatDisplayNarrative(limitation,data)));notes.append(el('h4','',cbs?'Toelichting':'Source limitations'),list)}
29
+ if(evidence.length){const list=el('ul','evidence-list');for(const item of evidence){const li=el('li');li.append(el('span','',(item.source?.name??formatDisplayNarrative(item.subject?.label||'Source',data))+' · '+(item.field??item.pointer)+': '+formatSourceValue(item.value,item.field||item.pointer,null,data)),el('span','fact-source',item.pointer));list.append(li)}notes.append(el('h4','',cbs?'Onderbouwing':'Source evidence'),list)}
30
+ return notes.children.length?notes:null}
31
+ renderResult=function(data){
32
+ const results=data.context_view?.results?.length?data.context_view.results:data.result==null?[]:[data.result],analysis=data.context_view?.analysis;
33
+ const verdict=supplierVerdictView(supplierVerdictAnalysis(data)),cbs=verdict?null:cbsAnnualView(data,results);const surface=verdict?renderSupplierVerdict(verdict):cbs?renderCbsAnnual(cbs):renderResearchAnswer(data,results);
34
+ // Without an analysis the source result is the answer, drawn as the App's
35
+ // purchase panel draws it: the primary result open, lookups under details.
36
+ let primary=null,primarySection=null;
37
+ if(!surface&&!analysis&&!data.context_view?.analyzing&&results.length){primary=data.result??results.at(-1);renderSingleResult({...data,result:primary});primarySection=sections.lastElementChild}
38
+ const rest=primary?results.filter(r=>r!==primary&&!(r?.result_ref&&r.result_ref===primary.result_ref)):results;
39
+ const report=data.context_view?.report,question=data.context_view?.conversation?.at(-1)?.question||'';
40
+ const pdf=report?.format==='pdf'&&typeof report.url==='string'&&report.url.startsWith('https://')?report.url:null,zip=typeof report?.evidence_url==='string'&&report.evidence_url.startsWith('https://')?report.evidence_url:null;
41
+ const notes=analysis&&!verdict?detailNotes(analysis,data,cbs):null;
42
+ if(rest.length||notes||pdf||zip){
43
+ const sourceCount=new Set(rest.map(result=>result.source?.provider||result.source?.name||'Source')).size;
44
+ const s=section('Sources and details');s.classList.add('source-results-section');
45
+ const disclosure=el('details','source-results-toggle');disclosure.append(el('summary','','Sources and details'+(rest.length?' · '+sourceCount+(sourceCount===1?' source':' sources')+' · '+rest.length+(rest.length===1?' result':' results'):'')));
46
+ const key=data.state?.state_ref;disclosure.open=key?sourceDisclosureState.get(key)===true:false;
47
+ disclosure.ontoggle=()=>{if(key){sourceDisclosureState.set(key,disclosure.open);if(sourceDisclosureState.size>20)sourceDisclosureState.delete(sourceDisclosureState.keys().next().value)}window.apiosk.resize()};
48
+ if(pdf||zip){const links=el('div','result-links');if(pdf)links.append(reportButton('Download PDF',pdf));if(zip)links.append(reportButton('Download evidence',zip));disclosure.append(links)}
49
+ if(notes)disclosure.append(notes);
50
+ const list=el('div','source-results-list'),groups=new Map();disclosure.append(list);s.append(disclosure);
51
+ for(const result of rest){if(cbs?.records.includes(result))continue;const key=sourceGroupKey(result);if(!groups.has(key))groups.set(key,[]);groups.get(key).push(result)}
52
+ for(const group of groups.values())list.append(renderSourceGroup(data,group));
53
+ }
54
+ if(surface?.title){byId('title').textContent=surface.title;byId('subtitle').textContent=surface.subtitle}
55
+ // A report is shown with the answer only when the question asked for a document.
56
+ if(pdf&&answerDocumentRequested(analysis,question)){const target=surface?.links||primarySection;if(target){const links=surface?.links||el('div','result-links');links.append(reportButton('Download PDF',pdf));if(!surface)target.append(links)}}
57
+ if(data.context_view?.analyzing)section('Analysis').append(el('p','notice','Analyzing the saved source results…'));
58
+ const cooldown=data.context_view?.cooldown;
59
+ if(cooldown){const kvk=/KVK/.test(cooldown.message||''),s=section(kvk?'Waiting for KVK':'Waiting for the source'),timer=el('p','notice');s.append(el('p','meta',cooldown.message),timer);const tick=()=>{const seconds=Math.max(0,Math.ceil((Date.parse(cooldown.until)-Date.now())/1000));timer.textContent=seconds?(kvk?'Next annual report in ':'Checking again in ')+Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0'):'Continuing the approved request…'};tick();const interval=setInterval(()=>{if(!timer.isConnected){clearInterval(interval);return}tick()},1000);if(data.billing?.authorization_active&&data.billing.quote_ref===data.proposal?.quote_ref)watchUntil=Math.max(watchUntil,Date.parse(cooldown.until)+300000)}
60
+ };
61
+ const renderOriginalPlan=renderPlan;
62
+ renderPlan=function(data){renderOriginalPlan(data);if(!planSurface)return;const notices=data.context_view?.coverage_notices;if(Array.isArray(notices)&&notices.length){const coverage=el('div','plan-coverage');coverage.append(el('p','key','Scope of this check'));for(const notice of notices.slice(0,8)){if(typeof notice==='string')coverage.append(el('p','notice',notice))}planSurface.append(coverage)}const rows=planSurface.querySelectorAll('.step');for(const [index,row] of rows.entries()){const detail=data.proposal?.step_details?.[index];const copy=row.lastElementChild;if(detail?.subject)copy.prepend(el('p','step-title',formatDisplayNarrative(detail.subject,data)));if(detail?.error?.message)copy.append(el('p','notice error',detail.error.message))}};
63
+ `;
@@ -0,0 +1,32 @@
1
+ import { V2_SOURCE_VALUE_FORMAT } from './source-value-format.mjs';
2
+ import { V2_CARD_BODY } from './gateway-v2-card-body.mjs';
3
+ // One source result, as the App shows it: the readable answer body (purchase
4
+ // panel), filing fields and KVK company rows (GatewayV2Result), then the full
5
+ // source data collapsed. Source JSON stays unchanged; formatting is display-only.
6
+ export const V2_CARD_RESULT = `
7
+ ${V2_SOURCE_VALUE_FORMAT}
8
+ ${V2_CARD_BODY}
9
+ function filingRows(fields,depth=0,rows=[],currency=null){if(depth>12||!Array.isArray(fields))return rows;for(const field of fields.slice(0,200)){if(rows.length>=100)break;if(field&&typeof field.key==='string'&&['string','number','boolean'].includes(typeof field.value))rows.push([field.key.replace(/([a-z])([A-Z])/g,'$1 $2'),formatSourceValue(field.value,field.key,sourceCurrency(field)||currency)]);if(field?.opendataFields)filingRows(field.opendataFields,depth+1,rows,currency)}return rows}
10
+ function resultBody(result){return result&&typeof result==='object'?result.data!==undefined?result.data:result.preview_data!==undefined?result.preview_data:result:result}
11
+ function companyTable(companies,body){
12
+ const wrap=el('div','result-companies'),region=el('div','body-table-wrap'),table=el('table','body-table'),head=el('tr'),thead=el('thead'),tbody=el('tbody');
13
+ wrap.append(el('p','meta',companies.length+' results shown'+(typeof body.totaal==='number'?' of '+body.totaal:'')+(typeof body.pagina==='number'?' · Page '+body.pagina:'')));
14
+ for(const label of ['Company','KVK number','City'])head.append(el('th','',label));thead.append(head);
15
+ for(const row of companies){const tr=el('tr');for(const value of [row.naam,row.kvkNummer,row.adres?.binnenlandsAdres?.plaats??'—'])tr.append(el('td','',value));tbody.append(tr)}
16
+ table.append(thead,tbody);region.append(table);wrap.append(region);
17
+ if(body.volgende)wrap.append(el('p','meta','The source has more results. This response contains the current page.'));
18
+ return wrap}
19
+ function renderResult(data){if(data.result==null)return;byId('title').textContent=data.status==='partial'?'Partial source result':'Source result';byId('subtitle').textContent='Returned by the selected source.';
20
+ const result=data.result,source=result&&result.source||{},subject=result?.subject?.label,s=section(typeof subject==='string'&&subject?'Result · '+formatDisplayNarrative(subject,result):'Result'),wrap=el('div','result'),body=displayData(resultBody(result)),currency=sourceCurrency(body);
21
+ const companies=Array.isArray(body?.resultaten)&&body.resultaten.every(r=>typeof r?.naam==='string'&&typeof r?.kvkNummer==='string')?body.resultaten:null,filing=Array.isArray(body?.opendataFields);
22
+ const report=result?.report,links=el('div','result-links');wrap.append(links);
23
+ if(report?.format==='pdf'&&typeof report.url==='string'&&report.url.startsWith('https://')){const download=el('button','text-action','Download PDF');download.onclick=()=>window.apiosk.openLink(report.url);links.append(download)}
24
+ if(companies)wrap.append(companyTable(companies,body));
25
+ else if(filing){wrap.append(el('p','meta','Selected fields from the deposited annual accounts. '+(currency?'Currency: '+currency+'.':'No currency is specified in this response.')));const dl=el('dl','body-fields');for(const [key,value] of filingRows(body.opendataFields,0,[],currency)){const row=el('div','result-row');row.append(el('dt','key',key),el('dd','value',value));dl.append(row)}wrap.append(dl)}
26
+ else wrap.append(answerBody(resultBody(result)));
27
+ const origin=el('div','result-source');origin.append(sourceLine(source));
28
+ if(typeof source.url==='string'&&source.url.startsWith('https://')){const link=el('button','text-action','View source');link.onclick=()=>window.apiosk.openLink(source.url);origin.append(link)}
29
+ if(source.retrieved_at)origin.append(el('span','meta','Retrieved '+new Date(source.retrieved_at).toLocaleString()));
30
+ const full=el('details','full-result');full.append(el('summary','','Full source data'),el('pre','',JSON.stringify(result,null,2)));full.ontoggle=()=>window.apiosk.resize();
31
+ wrap.append(origin,full);s.append(wrap)}
32
+ `;
@@ -0,0 +1,39 @@
1
+ // Runs inside the card: the source search view (`view: "source_search"`), as
2
+ // the App's Ask v2 source table draws it. Read-only: nothing here quotes,
3
+ // approves or buys; a row only says what a source is, costs and needs.
4
+ export const V2_CARD_SEARCH = `
5
+ function searchPriceAtomic(candidate){
6
+ const exact=candidate?.endpoint?.price?.buyer_atomic;if(typeof exact==='string'&&/^\\d+$/.test(exact))return exact;
7
+ const match=String(candidate?.price??'').trim().match(/^(\\d+)(?:\\.(\\d{1,6}))?(?:\\s+USDC?)?$/i);
8
+ return match?String(BigInt(match[1])*1000000n+BigInt((match[2]||'').padEnd(6,'0'))):null}
9
+ function searchInputLabel(input){
10
+ const names={'company.name':'Company name','company_registry.kvknummer':'KVK number','company.registration.lei':'LEI code','domain.name':'Website domain','web.query':'Search query'};
11
+ return input?.schema?.title||names[input?.field]||pretty(input?.field||'')}
12
+ function searchRow(candidate){
13
+ const source=candidate.endpoint?.source||{},row=el('div','search-row'),copy=el('div','search-copy'),side=el('div','search-side');
14
+ const name=candidate.name||source.name||candidate.source_slug||'Source',supported=candidate.availability==='supported';
15
+ copy.append(el('p','search-name',name));
16
+ const about=candidate.description||candidate.endpoint?.description;if(about)copy.append(el('p','search-about',about));
17
+ const inputs=supported?(candidate.endpoint?.inputs||[]).filter(input=>input?.required):[];
18
+ if(inputs.length)copy.append(el('p','search-inputs','Needs '+inputs.map(searchInputLabel).filter(Boolean).join(', ')));
19
+ const atomic=searchPriceAtomic(candidate),price=atomic&&money(atomic,'USD');
20
+ side.append(el('span','search-price',price||'—'),el('span','search-state'+(supported?' supported':''),({supported:'Available',discovery_only:'Discovery only',unverified:'Unverified'})[candidate.availability]||pretty(candidate.availability||'Unverified')));
21
+ row.append(sourceLogo({name,logo_url:candidate.logo_url||source.logo_url}),copy,side);return row}
22
+ function renderSourceSearch(data){
23
+ const matches=Array.isArray(data.matches)?data.matches:[],capabilities=data.parsed_request?.capabilities||[],seen=new Set();let total=0;
24
+ for(const match of matches)for(const candidate of [...(match.apiosk||[]),...(match.coinbase||[])]){const key=candidate.endpoint_id||candidate.url||(candidate.source_slug||candidate.name)+':'+(candidate.capability||'');if(!seen.has(key)){seen.add(key);total++}}
25
+ byId('title').textContent=total===1?'1 matching source':total+' matching sources';
26
+ byId('subtitle').textContent=Number.isInteger(data.apiosk_sources_searched)?'Searched '+data.apiosk_sources_searched+' Apiosk sources. Nothing is bought from this list.':'Nothing is bought from this list.';
27
+ if(!matches.length)section('Sources').append(el('p','meta','No requested capability was recognised. Rephrase the question or browse all sources.'));
28
+ for(const match of matches){
29
+ const capability=capabilities.find(c=>c?.slug===match.slug),candidates=[...(match.apiosk||[]),...(match.coinbase||[])];
30
+ const s=section(capability?.name||pretty(match.slug||'Requested data'),candidates.length===1?'1 source':candidates.length+' sources');s.classList.add('search-section');
31
+ if(capability?.description)s.append(el('p','meta search-capability',capability.description));
32
+ const list=el('div','search-list');list.setAttribute('role','list');
33
+ for(const candidate of candidates){const row=searchRow(candidate);row.setAttribute('role','listitem');list.append(row)}
34
+ if(candidates.length)s.append(list);else s.append(el('p','meta','No matching source was found for this part of the question.'));
35
+ if(match.coinbase_status==='unavailable')s.append(el('p','meta','Coinbase Bazaar search is unavailable right now.'));
36
+ }
37
+ if(data.notice)section('About these results').append(el('p','meta',data.notice));
38
+ }
39
+ `;
@@ -0,0 +1,32 @@
1
+ // Runs inside the card: the source list, and what it is honest to print on it.
2
+ export const V2_CARD_SOURCES = `
3
+ /* Buyer-facing counts include only runtime-supported functions. Registered
4
+ endpoints belong in the integration audit, never in this overview. */
5
+ function coverage(source){const r=source&&source.readiness,c=(r&&r.contracts)||{};
6
+ if(!r||!r.status)return{label:'Availability unknown',unavailable:false};
7
+ if(r.status==='blocked')return{label:'Not purchasable',unavailable:true};
8
+ const usable=c.supported_endpoints||0;
9
+ if(!usable)return{label:'Not executable',unavailable:true};
10
+ return{label:text(usable)+(usable===1?' available function':' available functions'),unavailable:false}}
11
+ function renderSources(data){
12
+ const rows=Array.isArray(data.sources)?data.sources:[];
13
+ byId('title').textContent=data.total===1?'1 matching source':text(data.total||rows.length)+' matching sources';
14
+ byId('subtitle').textContent='Choose a source or narrow the search.';
15
+ const s=section('Sources',text(data.offset||0)+'–'+text((data.offset||0)+rows.length)+' of '+text(data.total||rows.length)),list=el('div','sources');
16
+ for(const source of rows){
17
+ const group=el('div'),row=el('div','source'),copy=el('div'),name=el('div','source-name',source.name||source.slug),meta=el('div','source-meta',[source.category,...(source.capabilities||[]).slice(0,2)].filter(Boolean).map(pretty).join(' · '));
18
+ copy.append(name,meta);const reach=coverage(source);
19
+ row.append(sourceLogo(source),copy,el('span','count'+(reach.unavailable?' unavailable':''),source.service_count?text(source.service_count)+' services':reach.label));
20
+ group.append(row);
21
+ if(Array.isArray(source.services)&&source.services.length){
22
+ const count=source.matching_service_count||source.services.length,detail=el('details','compact-details');detail.append(el('summary','','View '+text(count)+(count===1?' service':' services')));
23
+ for(const service of source.services){const item=el('div','source-service');item.append(el('strong','',service.name),el('p','meta',service.description));detail.append(item)}
24
+ detail.ontoggle=()=>window.apiosk.resize();group.append(detail);
25
+ }
26
+ list.append(group);
27
+ }
28
+ s.append(list);const actions=el('div','actions');
29
+ if(Number.isInteger(data.next_offset)){const b=el('button','quiet','Next page');b.onclick=async()=>{if(busy)return;busy=true;showFeedback('Loading the next sources…');try{const args={...input,offset:data.next_offset,limit:input.limit||20};render(await window.apiosk.callTool('apiosk_sources',args))}catch(e){showFeedback(e&&e.message||'Could not load the next page.','error')}finally{busy=false}};actions.append(b)}
30
+ if(actions.childNodes.length)s.append(actions)
31
+ }
32
+ `;
@@ -0,0 +1,76 @@
1
+ // The card's stylesheet, on the App's tokens (see APIOSK_UI_STYLE in
2
+ // ui-bridge.mjs). Four sizes only: 12/16 tiny, 14/20 body, 16/24 large
3
+ // semibold, 24/32 display semibold. Weight and colour carry hierarchy; no
4
+ // uppercase micro-caps, no letter-spaced labels, no monospace.
5
+ // Published vendor marks are drawn for a light canvas; the App keeps them on
6
+ // white in dark mode.
7
+ const LOGOS = "img.logo,.body-thumb";
8
+
9
+ export const V2_CARD_STYLE = `
10
+ html,body{min-height:0!important;height:auto!important}body{padding:2px}
11
+ .card{padding:0;overflow:hidden;box-shadow:none}.shell{padding:16px}
12
+ .pill{border:1px solid var(--border-2);border-radius:999px;padding:2px 8px;font-size:12px;line-height:16px;color:var(--muted)}
13
+ .hero{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.hero p{margin:4px 0 0;font-size:14px;line-height:20px;color:var(--muted)}
14
+ .hero-status{display:flex;flex-direction:column;align-items:flex-end;gap:8px}.amount{text-align:right;white-space:nowrap}.amount b{display:block;font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em;font-variant-numeric:tabular-nums}.amount span{display:block;margin-top:4px;font-size:12px;line-height:16px;color:var(--muted)}
15
+ .account-links{display:flex;align-items:center;gap:4px;flex:0 0 auto}.account-links button,.controls button{min-height:32px;padding:0 8px;border-color:transparent;background:transparent;color:var(--muted);font-size:12px;line-height:16px;white-space:nowrap}.account-links button:hover,.controls button:hover{background:var(--hover);color:var(--foreground)}
16
+ .section{border-top:1px solid var(--border);padding:16px}.section-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.section-title h3{font-size:14px;line-height:20px;font-weight:500}.section-title span{font-size:12px;line-height:16px;color:var(--muted)}
17
+ .plan-mode #sections>.section:first-child>.section-title h3{font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em}
18
+ .meta{margin:0}.key{font-size:12px;line-height:16px;color:var(--muted)}p.key{margin:12px 0 4px}.value{overflow-wrap:anywhere}
19
+ .notice{margin:0;padding:12px;border-radius:8px;background:var(--accent-wash);color:var(--foreground);font-size:14px;line-height:20px}.notice.error{border:1px solid var(--danger-border);background:var(--danger-bg);color:var(--danger-fg)}
20
+ .actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.controls{padding:8px 16px}.controls .actions{margin-top:0}
21
+ .quiet{border-color:transparent;background:transparent;color:var(--muted)}.quiet:hover{background:var(--hover);color:var(--foreground)}
22
+ .text-action{min-height:36px;padding:0 16px;border-radius:8px;background:transparent;color:var(--foreground)}.text-action:hover{background:var(--hover)}
23
+ .result-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.result-links:empty{display:none}
24
+ .field{display:flex;gap:8px}.field input{min-width:0;flex:1;min-height:36px;border:1px solid var(--border-2);border-radius:6px;background:var(--input-bg);color:var(--foreground);padding:0 12px;font:inherit;font-size:14px;outline:none}.field input:focus{border-color:var(--accent-line)}
25
+ details>summary{cursor:pointer}.compact-details{margin-top:12px}.compact-details>summary,.full-result>summary,.body-meta>summary{padding:8px 0;font-size:12px;line-height:16px;color:var(--muted)}.compact-details>summary:hover,.full-result>summary:hover{color:var(--foreground)}.full-result{margin-top:12px}
26
+ pre{max-height:320px;overflow:auto;margin:8px 0 0;padding:12px;border:1px solid var(--border-2);border-radius:8px;background:var(--input-bg);color:var(--foreground);white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;line-height:16px}
27
+ .sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
28
+ .logo{display:grid;place-items:center;flex:0 0 auto;width:32px;height:32px;overflow:hidden;border-radius:6px;background:var(--surface-soft);object-fit:contain}.fallback{color:var(--muted);font-size:14px;line-height:1;font-weight:500}.logo.mini{width:24px;height:24px;font-size:12px}
29
+ :root[data-theme=dark] :is(${LOGOS}){background:#fff}@media(prefers-color-scheme:dark){:root:not([data-theme=light]) :is(${LOGOS}){background:#fff}}
30
+ .sources{display:grid;gap:8px}.source{display:grid;grid-template-columns:32px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px;border:1px solid var(--border-2);border-radius:8px;background:var(--card)}.source-name{overflow:hidden;color:var(--heading);font-weight:500;text-overflow:ellipsis;white-space:nowrap}.source-meta{margin-top:2px;overflow:hidden;color:var(--muted);font-size:12px;line-height:16px;text-overflow:ellipsis;white-space:nowrap}.count{color:var(--muted);font-size:12px;line-height:16px;white-space:nowrap}.count.unavailable{color:var(--danger-fg)}.source-service{padding:8px 0;border-top:1px solid var(--border)}.source-service strong{font-weight:500;color:var(--heading)}.source-service p{margin:2px 0 0}
31
+ .steps{display:grid;gap:12px}.step{display:grid;grid-template-columns:24px minmax(0,1fr);gap:8px}.step-no{padding-top:2px;color:var(--muted);font-size:12px}.step-title-line{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.step-title{margin:0;font-weight:500;color:var(--heading)}.step-source,.source-badge{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;line-height:16px}.step-source{margin-bottom:4px}
32
+ .state{padding:0 6px;border:1px solid var(--border-2);border-radius:4px;color:var(--muted);font-size:12px;line-height:16px}.state.completed{border-color:var(--success-border);background:var(--success-bg);color:var(--success-fg)}.state.failed{border-color:var(--danger-border);background:var(--danger-bg);color:var(--danger-fg)}.state.running{border-color:var(--accent-line);background:var(--accent-wash);color:var(--accent)}
33
+ .request-section>.section-title{align-items:flex-start;gap:16px;margin-bottom:16px}.request-price{display:grid;gap:4px;margin-top:12px}.price-label,.price-note{color:var(--muted);font-size:12px;line-height:16px}.request-price strong{font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em;font-variant-numeric:tabular-nums;white-space:nowrap}.plan-actions{margin-top:16px}.plan-actions button{min-width:190px}.plan-coverage{display:grid;gap:8px;margin-top:12px}.compact-status{margin:0;padding:8px 0}.compact-sources{display:grid;gap:8px;margin-top:8px}
34
+ .balances{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.balance{padding:12px;border-radius:8px;background:var(--surface-muted)}.balance span{display:block;color:var(--muted);font-size:12px;line-height:16px}.balance b{display:block;margin-top:4px;font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em;font-variant-numeric:tabular-nums}
35
+ .choice-list{display:grid;gap:8px}.choice-list button{justify-content:space-between;gap:16px;min-height:56px;padding:12px 16px;border-radius:8px;background:var(--card);text-align:left}.choice-list button:hover{background:var(--hover)}.choice-list button>span:first-child{display:grid;gap:2px}.choice-list button>span:last-child{color:var(--accent)}.choice-list strong{font-weight:500;color:var(--heading)}
36
+ .answer-section p{margin:0}.answer-text{display:grid;gap:16px;min-width:0;line-height:1.7;overflow-wrap:break-word}.answer-text p{white-space:pre-line}.answer-text ul,.answer-text ol{display:grid;gap:8px;margin:0;padding-left:20px}.answer-text .answer-heading{color:var(--foreground);font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em}.answer-text .answer-heading:not(:first-child){padding-top:12px}.answer-amount{font-weight:500;font-variant-numeric:tabular-nums}.answer-partial{margin-bottom:12px!important}
37
+ .answer-verdict{margin:4px 0 8px!important;color:var(--heading);font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.011em}.answer-summary{margin:8px 0!important}.answer-checks{margin:12px 0}.answer-check{display:grid;grid-template-columns:112px minmax(0,1fr);gap:12px;padding:6px 0}.answer-check dt{font-weight:500;color:var(--heading)}.answer-check dd{margin:0;overflow-wrap:anywhere}.answer-caveat{margin:8px 0!important}
38
+ .answer-number{margin:16px 0 8px!important;color:var(--heading);font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.019em;font-variant-numeric:tabular-nums}.answer-caption{margin:0 0 8px!important}.answer-source{margin:16px 0 8px!important;color:var(--muted);font-size:12px;line-height:16px}
39
+ .answer-body,.body-group,.body-list,.body-record,.answer-blocks,.answer-table,.answer-block,.bar-list,.bar-row{display:grid;min-width:0}.answer-body,.answer-blocks,.bar-list{gap:12px}.body-group{gap:10px}.body-list,.body-record,.answer-table{gap:8px}.answer-blocks{gap:16px;margin-top:16px}.bar-row{gap:4px}
40
+ .body-fields{display:grid;grid-template-columns:minmax(5rem,auto) minmax(0,1fr);gap:4px 12px;margin:0}.body-fields>.result-row{display:contents}.body-fields dt{overflow:hidden;color:var(--faint);font-size:12px;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.body-fields dd{min-width:0;margin:0;overflow-wrap:anywhere}
41
+ .body-section{display:grid;gap:6px}.body-section>h4{color:var(--faint);font-size:12px;line-height:16px;font-weight:500}.body-muted{color:var(--faint)}.body-number,.fact-value{font-variant-numeric:tabular-nums}.body-text{white-space:pre-wrap}.body-image{display:grid;gap:4px}
42
+ .body-link{display:inline-block;max-width:100%;overflow:hidden;color:var(--accent);text-decoration:none;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.body-link:hover{text-decoration:underline}
43
+ .body-thumb{flex:0 0 auto;width:36px;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--surface-soft);object-fit:contain}
44
+ .body-count{color:var(--faint);font-size:12px;line-height:16px}.body-records{margin:0;padding:0;list-style:none}.body-records>li{padding:12px 0;border-top:1px solid var(--border)}.body-records>li:first-child{padding-top:0;border-top:0}.body-records>li:last-child{padding-bottom:0}
45
+ .body-record-head{display:flex;align-items:center;gap:10px;min-width:0}.body-record-copy{min-width:0}.body-record-name{overflow:hidden;color:var(--heading);font-weight:500;text-overflow:ellipsis;white-space:nowrap}
46
+ .body-chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}.body-chips>li:not(:last-child)::after{content:','}
47
+ .body-bullets{display:grid;gap:4px;max-width:68ch;margin:0;padding:0;list-style:none}.body-bullets>li{display:flex;gap:8px}.body-dot{color:var(--faint)}.body-prose-item{min-width:0;white-space:pre-wrap}
48
+ .body-prose{display:grid;gap:8px;max-width:68ch}.body-prose p{white-space:pre-wrap}.body-prose-heading{color:var(--heading);font-weight:500}.body-prose ul,.body-prose ol{margin:0;padding-left:20px}
49
+ .body-table-wrap,.block-table-wrap,.cbs-table-wrap{max-width:100%;overflow:auto}.block-table-wrap{max-height:36rem;border:1px solid var(--border-2);border-radius:8px}
50
+ .body-table,.block-table,.cbs-table{width:100%;border-collapse:collapse;text-align:left;font-size:14px;line-height:20px}
51
+ .body-table caption{padding-bottom:8px;color:var(--muted);font-size:12px;line-height:16px;text-align:left}.body-table th{padding:8px 12px;border-block:1px solid var(--border);background:var(--surface-soft);color:var(--muted);font-size:12px;line-height:16px;font-weight:500;white-space:nowrap}.body-table td{padding:8px 12px;border-bottom:1px solid var(--border);vertical-align:top;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
52
+ .block-table th{position:sticky;top:0;z-index:1;padding:8px 12px;background:var(--surface);color:var(--heading);font-weight:500}.block-table td{max-width:18rem;padding:8px 12px;border-top:1px solid var(--border-2);vertical-align:top;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
53
+ .cbs-table th,.cbs-table td{padding:8px;border-bottom:1px solid var(--border-2)}.cbs-table th{font-weight:500;color:var(--heading)}.cbs-table td:nth-child(3){white-space:nowrap;font-variant-numeric:tabular-nums}
54
+ .body-more{min-height:0;padding:0;border:0;background:none;color:var(--accent);font-size:12px;line-height:16px;justify-self:start}.body-more:hover{background:none;text-decoration:underline}
55
+ .body-meta{padding-top:8px;border-top:1px solid var(--border)}
56
+ .answer-block{gap:12px;margin:0;padding:12px;border:1px solid var(--border-2);border-radius:8px}.answer-block h3{font-size:14px;line-height:20px}.block-kind{text-transform:capitalize}.answer-block figcaption{color:var(--heading);font-weight:500}.block-note{display:block;margin-top:2px;font-weight:400}
57
+ .block-controls{display:flex;flex-wrap:wrap;align-items:center;gap:12px}.block-filter{min-height:32px;padding:0 8px;border:1px solid var(--border-2);border-radius:6px;background:var(--input-bg);color:var(--foreground);font:inherit;font-size:14px}.block-columns{display:flex;flex-wrap:wrap;gap:12px;margin:0;padding:0;border:0;font-size:12px;line-height:16px}.block-columns label{display:inline-flex;align-items:center;gap:6px}
58
+ .fact-flag{margin-left:6px;padding:2px 6px;border-radius:4px;background:var(--accent-wash);color:var(--muted);font-size:12px;line-height:16px;font-weight:500}.fact-source{display:block;color:var(--faint);font-size:12px;line-height:16px;word-break:break-all}.cell-list{margin:0;padding-left:16px}
59
+ .bar-line{display:flex;justify-content:space-between;gap:12px}.bar-label{overflow-wrap:anywhere}.bar-track{position:relative;height:12px;border-radius:4px;background:var(--accent-wash)}.bar-zero{position:absolute;top:0;bottom:0;width:1px;background:var(--muted)}.bar-fill{position:absolute;top:0;bottom:0;border-radius:4px;background:var(--accent)}
60
+ .source-results-section{padding:8px 16px}.source-results-section>.section-title{display:none}.source-results-toggle>summary{padding:8px 0;color:var(--muted);font-size:12px;line-height:16px;font-weight:500}.source-results-toggle>summary:hover{color:var(--foreground)}
61
+ .source-results-list{display:grid;gap:8px;margin-top:8px;overflow-x:auto}.source-result-group{min-width:640px;margin:0;border:1px solid var(--border-2);border-radius:8px}
62
+ .source-result-summary{display:grid;grid-template-columns:24px minmax(120px,1.25fr) minmax(100px,1fr) repeat(4,minmax(72px,.7fr)) 16px;align-items:center;gap:8px;padding:8px 12px;list-style:none}.source-result-summary::-webkit-details-marker{display:none}.source-result-summary::after{content:'›';grid-column:8;grid-row:1;justify-self:end;color:var(--muted);font-size:16px}.source-result-group[open]>.source-result-summary::after{transform:rotate(90deg)}
63
+ .source-result-name{overflow:hidden;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.source-result-cell{display:grid;min-width:0}.source-result-label{color:var(--faint);font-size:12px;line-height:16px;white-space:nowrap}.source-result-value{overflow:hidden;font-size:12px;line-height:16px;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}.source-result-details{border-top:1px solid var(--border)}.source-result-details>.section{padding:12px}
64
+ .detail-notes{display:grid;gap:8px;margin:8px 0 16px}.detail-notes h4{font-size:14px;line-height:20px}.detail-notes ul{display:grid;gap:6px;margin:0;padding-left:20px;color:var(--muted)}.detail-notes .evidence-list{padding:0;list-style:none;font-size:12px;line-height:16px}.evidence-list li>span:first-child{color:var(--foreground)}
65
+ .result-source{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-top:12px}
66
+ .verdict-banner{display:flex;align-items:center;gap:10px;margin:4px 0 12px;padding:12px 16px;border:1px solid;border-radius:8px;font-size:16px;line-height:24px}.verdict-label{font-weight:600;letter-spacing:-.011em}
67
+ .verdict-banner.hold{border-color:var(--danger-border);background:var(--danger-bg);color:var(--danger-fg)}.verdict-banner.review{border-color:var(--warning-border);background:var(--warning-bg);color:var(--warning-fg)}.verdict-banner.ok{border-color:var(--success-border);background:var(--success-bg);color:var(--success-fg)}
68
+ .verdict-reasons{margin:0 0 12px;padding-left:20px}.verdict-supplier{margin:0 0 12px!important;overflow-wrap:anywhere;color:var(--muted);font-size:12px;line-height:16px}.verdict-checks{margin:8px 0 12px;padding:0;list-style:none}.verdict-check{display:grid;grid-template-columns:24px minmax(0,1fr);gap:8px;padding:8px 0;border-bottom:1px solid var(--border)}.verdict-check:last-child{border:0}
69
+ .check-icon{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;font-size:12px;font-weight:600}.check-icon.pass{background:var(--success-bg);color:var(--success-fg)}.check-icon.warn{background:var(--warning-bg);color:var(--warning-fg)}.check-icon.fail{background:var(--danger-bg);color:var(--danger-fg)}.check-icon.unknown{background:var(--surface-muted);color:var(--faint)}.verdict-check.unknown .check-copy{color:var(--muted)}
70
+ .check-copy p{margin:0}.check-label{font-weight:500}.check-detail{margin-top:2px!important;overflow-wrap:anywhere}.check-source{margin-top:2px!important;color:var(--muted);font-size:12px;line-height:16px}.evidence-details{margin:4px 0 12px;color:var(--muted);font-size:12px;line-height:16px}.evidence-details>summary{padding:4px 0}
71
+ .search-list{display:grid;overflow:hidden;border:1px solid var(--border-2);border-radius:8px}.search-row{display:grid;grid-template-columns:32px minmax(0,1fr) auto;align-items:start;gap:12px;padding:12px;border-top:1px solid var(--border-2)}.search-row:first-child{border-top:0}
72
+ .search-name{margin:0;color:var(--heading);font-weight:500;overflow-wrap:anywhere}.search-about{display:-webkit-box;margin:2px 0 0;overflow:hidden;color:var(--muted);font-size:12px;line-height:16px;-webkit-line-clamp:2;-webkit-box-orient:vertical}.search-inputs{margin:4px 0 0;font-size:12px;line-height:16px}
73
+ .search-side{display:grid;justify-items:end;gap:4px;text-align:right}.search-price,.search-state{font-size:12px;line-height:16px;white-space:nowrap}.search-price{font-variant-numeric:tabular-nums}.search-state{color:var(--muted)}.search-state.supported{color:var(--success-fg)}.search-capability{margin:-4px 0 12px}
74
+ @media(max-width:480px){.section,.shell{padding:16px 12px}.source-results-section{padding:8px 12px}.hero{display:block}.hero-status{align-items:flex-start;margin-top:12px}.amount{text-align:left}.source{grid-template-columns:32px minmax(0,1fr)}.source .count{grid-column:2}.balances{grid-template-columns:1fr}.actions button{min-height:44px}.plan-actions button{width:100%;min-width:0}.answer-check{grid-template-columns:100px minmax(0,1fr);gap:8px}.account-links{width:100%}.account-links button{flex:1}.search-row{grid-template-columns:32px minmax(0,1fr)}.search-side{grid-column:2;grid-auto-flow:column;justify-content:start;justify-items:start;gap:12px;text-align:left}}
75
+ @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
76
+ `;
@@ -0,0 +1,66 @@
1
+ // Display only: the gateway decides. Every value stays a text node; no advice is added.
2
+ export function supplierVerdictAnalysis(data) {
3
+ const current = data?.context_view?.analysis;
4
+ if (current) return current;
5
+ const saved = (data?.context_view?.conversation || []).filter(turn => turn?.output && typeof turn.output === 'object').at(-1);
6
+ return saved?.output?.analysis || null;
7
+ }
8
+
9
+ export function supplierVerdictView(analysis) {
10
+ const verdict = analysis?.verdict;
11
+ if (analysis?.kind !== 'supplier_payment_check' || !verdict || typeof verdict !== 'object') return null;
12
+ const decisions = {
13
+ hold: {icon: '⚠️', en: 'HOLD PAYMENT', nl: 'BETALING TEGENHOUDEN'},
14
+ review: {icon: '🔎', en: 'REVIEW BEFORE PAYING', nl: 'EERST CONTROLEREN'},
15
+ ok: {icon: '✅', en: 'NO RED FLAGS FOUND', nl: 'GEEN RODE VLAGGEN GEVONDEN'},
16
+ };
17
+ const decision = decisions[verdict.decision];
18
+ if (!decision) return null;
19
+ const language = verdict.language === 'nl' ? 'nl' : 'en';
20
+ const words = s => typeof s === 'string' && s.trim() ? s.trim() : null;
21
+ // A label from another decision never reaches the banner.
22
+ const label = [decision.en, decision.nl].includes(verdict.label) ? verdict.label : decision[language];
23
+ const supplier = verdict.supplier && typeof verdict.supplier === 'object' ? verdict.supplier : {};
24
+ const statuses = ['pass', 'warn', 'fail', 'unknown'];
25
+ const checks = (Array.isArray(verdict.checks) ? verdict.checks : []).filter(c => c && typeof c === 'object' && words(c.label)).map(c => ({
26
+ id: words(c.id), label: words(c.label), status: statuses.includes(c.status) ? c.status : 'unknown',
27
+ detail: words(c.detail), source: words(c.source),
28
+ evidence: (Array.isArray(c.evidence) ? c.evidence : []).filter(e => e && typeof e === 'object'),
29
+ }));
30
+ return {
31
+ decision: verdict.decision, icon: decision.icon, label, language,
32
+ partial: analysis.status === 'partial',
33
+ reasons: (Array.isArray(verdict.reasons) ? verdict.reasons : []).map(words).filter(Boolean),
34
+ supplier: {name: words(supplier.name), kvk: words(supplier.kvk_number), vat: words(supplier.vat_number), iban: words(supplier.iban_masked), domain: words(supplier.domain)},
35
+ checks,
36
+ limitations: (Array.isArray(analysis.limitations) ? analysis.limitations : []).map(words).filter(Boolean),
37
+ };
38
+ }
39
+
40
+ export const V2_CARD_VERDICT = `
41
+ ${supplierVerdictAnalysis.toString()}
42
+ ${supplierVerdictView.toString()}
43
+ function renderSupplierVerdict(view){
44
+ const nl=view.language==='nl',t=(dutch,english)=>nl?dutch:english,value=v=>v!=null&&typeof v==='object'?JSON.stringify(v):text(v);
45
+ const title=t('Leverancierscontrole','Supplier payment check'),s=section(title);s.classList.add('answer-section');s.classList.add('verdict-section');
46
+ const banner=el('div','verdict-banner '+view.decision);banner.setAttribute('role','status');banner.append(el('span','verdict-icon',view.icon),el('strong','verdict-label',view.label));s.append(banner);
47
+ if(view.reasons.length){const list=el('ul','verdict-reasons');for(const reason of view.reasons)list.append(el('li','',formatDisplayNarrative(reason,output)));s.append(list)}
48
+ const p=view.supplier,parts=[formatDisplayText(p.name),p.kvk&&'KVK '+p.kvk,p.vat&&t('Btw ','VAT ')+p.vat,p.iban&&'IBAN '+p.iban,p.domain].filter(Boolean);
49
+ if(parts.length)s.append(el('p','verdict-supplier',parts.join(' · ')));
50
+ if(view.partial)s.append(el('p','notice',t('Niet alle controles zijn afgerond.','Not every check could be completed.')));
51
+ const icons={pass:'✓',warn:'!',fail:'✕',unknown:'?'};
52
+ if(view.checks.length){s.append(el('p','key',t('Controles','Checks')));const list=el('ul','verdict-checks');
53
+ for(const check of view.checks){const row=el('li','verdict-check '+check.status),copy=el('div','check-copy');
54
+ const icon=el('span','check-icon '+check.status,icons[check.status]);icon.setAttribute('aria-label',check.status);
55
+ copy.append(el('p','check-label',formatDisplayNarrative(check.label,output)));
56
+ if(check.detail)copy.append(el('p','check-detail',formatDisplayNarrative(check.detail,output)));
57
+ if(check.status==='unknown')copy.append(el('p','check-source',t('Niet geverifieerd','Not verified')));
58
+ if(check.source)copy.append(el('p','check-source',t('Bron: ','Source: ')+check.source));
59
+ if(check.evidence.length){const evidence=el('details','evidence-details');evidence.append(el('summary','',t('Onderbouwing','Source evidence')));evidence.ontoggle=()=>window.apiosk.resize();for(const item of check.evidence)evidence.append(el('p','value',formatSourceValue(item.value,item.field||item.pointer,null,output)+(item.pointer?' · '+item.pointer:'')));copy.append(evidence)}
60
+ row.append(icon,copy);list.append(row)}
61
+ s.append(list)}
62
+ for(const limitation of view.limitations)s.append(el('p','notice',formatDisplayNarrative(limitation,output)));
63
+ const links=el('div','result-links');s.append(links);
64
+ return {section:s,links,title,subtitle:t('Gecontroleerd met de teruggegeven bronnen.','Checked against the returned sources.')};
65
+ }
66
+ `;