/* global React */
/* InsightBox — live AI summary of a chart.
The page passes the SAME numbers the chart renders (`facts`), so the summary
is always faithful to what's on screen and reflects the selected period.
POSTs to /api/insight (Claude, Turkish, quantified). Client-side cache +
debounce keep it cheap; graceful demo-mode fallback if the backend is down. */
const _insightCache = new Map(); // key -> summary string
// Minimal inline markdown: **bold**, _em_, and "- " bullets (matches copilot).
const _renderMd = (text) => {
const inline = (s) => s
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/(^|[^_])_(.+?)_/g, '$1$2');
const lines = (text || '').split('\n').filter(l => l.trim().length);
return lines.map((line, i) => {
const t = line.trim();
if (t.startsWith('- ') || t.startsWith('• ')) {
return React.createElement('div', { key: i, className: 'insight-li',
dangerouslySetInnerHTML: { __html: inline(t.slice(2)) } });
}
return React.createElement('div', { key: i, style: { marginBottom: 3 },
dangerouslySetInnerHTML: { __html: inline(t) } });
});
};
const InsightBox = ({ label, hint = '', facts, compact = false }) => {
const [state, setState] = useState({ status: 'idle', summary: '' });
const factsKey = useMemo(() => {
try { return label + '::' + JSON.stringify(facts); } catch (e) { return label + '::?'; }
}, [label, facts]);
useEffect(() => {
let alive = true;
if (_insightCache.has(factsKey)) {
setState({ status: 'done', summary: _insightCache.get(factsKey) });
return;
}
setState({ status: 'loading', summary: '' });
// Debounce so dragging the period filter doesn't fire a call per frame.
const timer = setTimeout(async () => {
try {
const base = (typeof window !== 'undefined' && window.API_BASE_URL) || '';
const res = await fetch(base + '/api/insight', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label, hint, facts }),
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (!alive) return;
_insightCache.set(factsKey, data.summary || '');
setState({ status: data.configured === false ? 'demo' : 'done', summary: data.summary || '' });
} catch (e) {
if (alive) setState({ status: 'demo', summary: '' });
}
}, 550);
return () => { alive = false; clearTimeout(timer); };
}, [factsKey]);
const eyebrow = React.createElement('div', { className: 'insight-eyebrow' },
React.createElement('span', { className: 'insight-dot' }),
'AI ÖZET',
React.createElement('span', { className: 'insight-by' }, 'Claude · canlı'));
let body;
if (state.status === 'loading' || state.status === 'idle') {
body = React.createElement('div', { className: 'insight-loading' },
React.createElement('span', { className: 'insight-shimmer' }),
'analiz yazılıyor…');
} else if (state.status === 'demo') {
body = React.createElement('div', { className: 'insight-demo' },
'Bu grafiğin yapay zekâ özeti, canlı veri ambarına bağlandığında otomatik üretilir. ' +
'(Demo modu — Copilot servisi şu anda kapalı.)');
} else {
body = React.createElement('div', { className: 'insight-text' }, _renderMd(state.summary));
}
return React.createElement('div', { className: 'insight-box' + (compact ? ' insight-compact' : '') },
eyebrow, body);
};
window.InsightBox = InsightBox;