/* Shared primitives: Icon, Badge, Button, crumbs, helpers, constants */
const { useState, useEffect, useRef, useCallback } = React;
const API = '/api';
/* Labels voor contour-subtypes (gedeeld door data-scherm, review-canvas, resultaten) */
const CONTOUR_SUBTYPE_LABEL = {
serre: 'Serre',
aanbouw: 'Aanbouw',
schuur: 'Schuur',
overkapping: 'Overkapping',
boom: 'Boom',
tuinafscheiding: 'Tuinafscheiding',
vegetatie: 'Vegetatie',
geen_bouwwerk: 'Geen bouwwerk',
anders: 'Anders',
niet_geclassificeerd: 'Niet geclassificeerd',
};
/* ---------- Icon: thin wrapper around Lucide ---------- */
function Icon({ name, size, color, style, className, strokeWidth }) {
const ref = React.useRef(null);
React.useEffect(() => {
if (ref.current && window.lucide) {
ref.current.innerHTML = '';
const el = document.createElement('i');
el.setAttribute('data-lucide', name);
ref.current.appendChild(el);
try { window.lucide.createIcons({ nameAttr: 'data-lucide', icons: window.lucide.icons }); } catch(e) {}
}
}, [name]);
const s = { display: 'inline-flex', width: size || 16, height: size || 16, color: color || 'currentColor', ...(style||{}) };
return ;
}
/* ---------- Badge ---------- */
function Badge({ kind = 'neutral', icon, children, style }) {
return (
{icon
?
: }
{children}
);
}
/* ---------- Button ---------- */
function Btn({ kind = 'secondary', size, icon, iconRight, children, className, ...rest }) {
const cls = [
'tc-btn',
'tc-btn--' + kind,
size === 'sm' ? 'tc-btn--sm' : '',
(icon && !children) ? 'tc-btn--icon-only' : '',
className || '',
].filter(Boolean).join(' ');
return (
);
}
/* ---------- Alert ---------- */
function Alert({ kind = 'info', icon, children }) {
const def = { error: 'alert-circle', success: 'check-circle-2', info: 'info', warning: 'alert-triangle' };
return (
);
}
/* ---------- Empty ---------- */
function Empty({ icon, title, hint }) {
return (
);
}
/* ---------- Field ---------- */
function Field({ label, hint, children, style }) {
return (
{label &&
}
{children}
{hint &&
{hint}
}
);
}
/* ---------- Constants shared across screens ---------- */
const MODELLEN_DEFAULTS = {
anthropic: ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-sonnet-4-5', 'claude-haiku-4-5'],
openai: ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4o-mini'],
google: ['gemini-2.0-flash', 'gemini-2.5-pro'],
bedrock: ['qwen.qwen3-vl-235b-a22b', 'qwen.qwen3-vl-235b-a22b-instruct-v1:0', 'anthropic.claude-3-5-sonnet-20241022-v2:0'],
nebul: ['Qwen/Qwen3-VL-235B-A22B-Thinking', 'Qwen/Qwen3-VL-235B-A22B-Instruct'],
};
let MODELLEN = { ...MODELLEN_DEFAULTS };
async function laadModellen() {
try {
const d = await fetch(`${API}/config/providers/models`).then(r => r.ok ? r.json() : null);
if (d) {
const merged = {};
for (const p of Object.keys(MODELLEN_DEFAULTS)) {
merged[p] = (d[p] && d[p].length > 0) ? d[p] : MODELLEN_DEFAULTS[p];
}
MODELLEN = merged;
window.MODELLEN = MODELLEN;
}
} catch (e) {}
}
const DETECTIE_LABEL = {
zonnepanelen: 'Zonnepanelen',
dakkapel: 'Dakkapellen',
contour: 'Contourafwijking',
contour_perceel: 'Perceel-scan',
};
const DETECTIE_ICON = {
zonnepanelen: 'sun',
dakkapel: 'home',
contour: 'triangle-alert',
contour_perceel: 'land-plot',
};
const ZEKERHEID_KIND = { hoog: 'success', midden: 'warning', laag: 'error' };
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s).toLocaleDateString('nl-NL'); } catch(e) { return '—'; }
}
function fmtDateTime(s) {
if (!s) return '—';
try { return new Date(s).toLocaleString('nl-NL'); } catch(e) { return '—'; }
}
function fmtNum(n) {
if (n == null) return '—';
return Number(n).toLocaleString('nl-NL');
}
Object.assign(window, { useState, useEffect, useRef, useCallback,
API, Icon, Badge, Btn, Alert, Empty, Field,
MODELLEN, laadModellen, DETECTIE_LABEL, DETECTIE_ICON, ZEKERHEID_KIND,
fmtDate, fmtDateTime, fmtNum });