/* global React, lucide */ const { useState, useEffect, useMemo, useRef, useCallback } = React; // ─── parsing helpers ──────────────────────────────────────────── function extractBlock(text, tag) { if (!text) return null; const re = new RegExp(`<${tag}>\\s*([\\s\\S]*?)\\s*`, 'i'); const m = text.match(re); return m ? m[1].trim() : null; } function parseModelResponse(content) { const think = extractBlock(content, 'think'); const dagText = extractBlock(content, 'dag'); const code = extractBlock(content, 'code'); let dag = null; if (dagText) { try { dag = JSON.parse(dagText); } catch (e) {} } return { think, dag, code, hasAll: !!(think && dagText && code) }; } // ─── tiny SQL highlighter ─────────────────────────────────────── function highlightSQL(sql) { if (!sql) return []; const KW = /\b(WITH|AS|SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|GROUP|BY|ORDER|LIMIT|HAVING|UNION|DISTINCT|AND|OR|NOT|IS|NULL|CASE|WHEN|THEN|ELSE|END|IN|EXISTS|COUNT|SUM|AVG|MIN|MAX|CAST|INTEGER|REAL|TEXT|DESC|ASC|EXCEPT|INTERSECT|ALL)\b/gi; const lines = sql.split('\n'); return lines.map((line, i) => { const isComment = /^\s*--/.test(line); if (isComment) { return
{line || ' '}
; } // Tokenize const parts = []; let rest = line; let safety = 0; while (rest.length && safety++ < 200) { // strings let m = rest.match(/^'([^']*)'/); if (m) { parts.push({t:'str', v:`'${m[1]}'`}); rest = rest.slice(m[0].length); continue; } // numbers m = rest.match(/^\b(\d+(?:\.\d+)?)\b/); if (m) { parts.push({t:'num', v:m[0]}); rest = rest.slice(m[0].length); continue; } // keyword m = rest.match(KW); if (m && rest.indexOf(m[0]) === 0) { parts.push({t:'kw', v:m[0]}); rest = rest.slice(m[0].length); continue; } // word m = rest.match(/^[A-Za-z_][\w.]*/); if (m) { parts.push({t:'id', v:m[0]}); rest = rest.slice(m[0].length); continue; } // single parts.push({t:'p', v: rest[0]}); rest = rest.slice(1); } return (
{parts.length ? parts.map((p, j) => p.t === 'p' || p.t === 'id' ? {p.v} : {p.v}) : ' '}
); }); } // ─── think text mini-format (markdown-lite for **bold**) ──────── function formatThink(t) { if (!t) return null; // Convert **x** to x, `x` to x (re-purpose) const parts = []; const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; let last = 0; let m; let key = 0; while ((m = re.exec(t)) !== null) { if (m.index > last) parts.push(t.slice(last, m.index)); if (m[0].startsWith('**')) parts.push({m[0].slice(2, -2)}); else parts.push({m[0].slice(1, -1)}); last = m.index + m[0].length; } if (last < t.length) parts.push(t.slice(last)); return parts; } // ─── DAG layout (simple longest-path layered) ─────────────────── function layoutDAG(dag) { if (!dag || !dag.nodes || !dag.edges) return null; const nodes = dag.nodes.map(n => ({ ...n })); const edges = dag.edges.map(e => ({ from: e.from ?? e[0], to: e.to ?? e[1] })); const byId = new Map(nodes.map(n => [n.id, n])); // longest path layering const incoming = new Map(nodes.map(n => [n.id, 0])); edges.forEach(e => incoming.set(e.to, (incoming.get(e.to) || 0) + 1)); const layer = new Map(); // BFS-ish const queue = nodes.filter(n => (incoming.get(n.id) || 0) === 0).map(n => n.id); queue.forEach(id => layer.set(id, 0)); // Iteratively assign max(parent layer)+1 let changed = true, guard = 0; while (changed && guard++ < 100) { changed = false; edges.forEach(e => { const fl = layer.get(e.from); if (fl == null) return; const cur = layer.get(e.to); const want = fl + 1; if (cur == null || want > cur) { layer.set(e.to, want); changed = true; } }); } // place const layers = {}; nodes.forEach(n => { const l = layer.get(n.id) ?? 0; (layers[l] = layers[l] || []).push(n); }); const layerKeys = Object.keys(layers).map(Number).sort((a,b)=>a-b); const colW = 200; const rowH = 86; const padX = 30; const padY = 30; let maxRows = 0; const positions = new Map(); layerKeys.forEach((l, ci) => { const arr = layers[l]; arr.forEach((n, ri) => { positions.set(n.id, { x: padX + ci * colW, y: padY + ri * rowH }); }); maxRows = Math.max(maxRows, arr.length); }); const width = padX * 2 + Math.max(1, layerKeys.length) * colW - 60; const height = padY * 2 + Math.max(1, maxRows) * rowH - 30; return { nodes, edges, byId, positions, width, height }; } function DAGView({ dag, activeId }) { const layout = useMemo(() => layoutDAG(dag), [dag]); if (!layout) { return
DAG not parseable
; } const { nodes, edges, positions, width, height } = layout; const nodeW = 158, nodeH = 56; return (
{edges.map((e, i) => { const a = positions.get(e.from); const b = positions.get(e.to); if (!a || !b) return null; const x1 = a.x + nodeW; const y1 = a.y + nodeH/2; const x2 = b.x; const y2 = b.y + nodeH/2; const mx = (x1 + x2) / 2; const path = `M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`; return ; })} {nodes.map(n => { const p = positions.get(n.id); if (!p) return null; const type = (n.type || 'select').toLowerCase().replace('agg','aggregate'); const isActive = activeId === n.id; const label = n.id.length > 18 ? n.id.slice(0,17) + '…' : n.id; return ( {type} {label} ); })}
); } // ─── Result table ────────────────────────────────────────────── function ResultTable({ columns, rows, max = 8 }) { if (!columns || !rows) return null; const shown = rows.slice(0, max); return (
{columns.map((c, i) => )} {shown.map((r, i) => ( {(Array.isArray(r) ? r : [r]).map((v, j) => ( ))} ))}
{c}
{v == null ? NULL : String(v)}
{rows.length > max && (
+ {rows.length - max} more rows · showing first {max}
)}
); } // ─── Lucide icon ─────────────────────────────────────────────── function Icon({ name, size = 16, stroke = 1.5, ...rest }) { // Use span wrapper so React owns the node; inject SVG via innerHTML const ref = useRef(null); useEffect(() => { if (!ref.current) return; const node = window.lucide?.icons?.[name] || window.lucide?.icons?.[toCamel(name)]; if (node && node.toSvg) { ref.current.innerHTML = node.toSvg({ width: size, height: size, 'stroke-width': stroke }); } else if (window.lucide?.createIcons) { // fallback: stamp and let lucide.createIcons replace it ref.current.innerHTML = ``; window.lucide.createIcons(); } }, [name, size, stroke]); return ; } function toCamel(s) { return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^(.)/, m => m.toUpperCase()); } // ─── Step block ──────────────────────────────────────────────── function Step({ num, title, meta, state, open, onToggle, children }) { return (
{state === 'done' ? : num} {title} {meta && · {meta}} {state === 'active' && }
{open && (
{children}
)}
); } // expose Object.assign(window, { parseModelResponse, highlightSQL, formatThink, DAGView, ResultTable, Icon, Step, });