// bundle.jsx — auto-concatenated (do not edit; edit the source .jsx files and rebuild)

/* ===== tweaks-panel.jsx ===== */
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)

/* BEGIN USAGE */
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
// Exports (to window): useTweaks, TweaksPanel, TweakSection, TweakRow, TweakSlider,
//   TweakToggle, TweakRadio, TweakSelect, TweakText, TweakNumber, TweakColor, TweakButton.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw <input type="range">, segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
//   const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
//     "primaryColor": "#D97757",
//     "palette": ["#D97757", "#29261b", "#f6f4ef"],
//     "fontSize": 16,
//     "density": "regular",
//     "dark": false
//   }/*EDITMODE-END*/;
//
//   function App() {
//     const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
//     return (
//       <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
//         Hello
//         <TweaksPanel>
//           <TweakSection label="Typography" />
//           <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
//                        onChange={(v) => setTweak('fontSize', v)} />
//           <TweakRadio  label="Density" value={t.density}
//                        options={['compact', 'regular', 'comfy']}
//                        onChange={(v) => setTweak('density', v)} />
//           <TweakSection label="Theme" />
//           <TweakColor  label="Primary" value={t.primaryColor}
//                        options={['#D97757', '#2A6FDB', '#1F8A5B', '#7A5AE0']}
//                        onChange={(v) => setTweak('primaryColor', v)} />
//           <TweakColor  label="Palette" value={t.palette}
//                        options={[['#D97757', '#29261b', '#f6f4ef'],
//                                  ['#475569', '#0f172a', '#f1f5f9']]}
//                        onChange={(v) => setTweak('palette', v)} />
//           <TweakToggle label="Dark mode" value={t.dark}
//                        onChange={(v) => setTweak('dark', v)} />
//         </TweaksPanel>
//       </div>
//     );
//   }
//
// TweakRadio is the segmented control for 2–3 short options (auto-falls-back to
// TweakSelect past ~16/~10 chars per label); reach for TweakSelect directly when
// options are many or long. For color tweaks always curate 3-4 options rather than
// a free picker; an option can also be a whole 2–5 color palette (the stored value
// is the array). The Tweak* controls are a floor, not a ceiling — build custom
// controls inside the panel if a tweak calls for UI they don't cover.
/* END USAGE */
// ─────────────────────────────────────────────────────────────────────────────

const __TWEAKS_STYLE = `
  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
    max-height:calc(100vh - 32px);display:flex;flex-direction:column;
    transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
    background:rgba(250,249,247,.78);color:#29261b;
    -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
    border:.5px solid rgba(255,255,255,.6);border-radius:14px;
    box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
    font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
  .twk-hd{display:flex;align-items:center;justify-content:space-between;
    padding:10px 8px 10px 14px;cursor:move;user-select:none}
  .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
  .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
    width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
  .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
  .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
    overflow-y:auto;overflow-x:hidden;min-height:0;
    scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
  .twk-body::-webkit-scrollbar{width:8px}
  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
  .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
    border:2px solid transparent;background-clip:content-box}
  .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
    border:2px solid transparent;background-clip:content-box}
  .twk-row{display:flex;flex-direction:column;gap:5px}
  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
    color:rgba(41,38,27,.72)}
  .twk-lbl>span:first-child{font-weight:500}
  .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}

  .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    color:rgba(41,38,27,.45);padding:10px 0 0}
  .twk-sect:first-child{padding-top:0}

  .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;
    background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
  .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
  select.twk-field{padding-right:22px;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
    background-repeat:no-repeat;background-position:right 8px center}

  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
    border-radius:999px;background:rgba(0,0,0,.12);outline:none}
  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:14px;height:14px;border-radius:50%;background:#fff;
    border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
  .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
    background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}

  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
    background:rgba(0,0,0,.06);user-select:none}
  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
    background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
    transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
  .twk-seg.dragging .twk-seg-thumb{transition:none}
  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
    background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
    border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
    overflow-wrap:anywhere}

  .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
    background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
  .twk-toggle[data-on="1"]{background:#34c759}
  .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
    background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
  .twk-toggle[data-on="1"] i{transform:translateX(14px)}

  .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
  .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
    user-select:none;padding-right:8px}
  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
    font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
    outline:none;color:inherit;-moz-appearance:textfield}
  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
    -webkit-appearance:none;margin:0}
  .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}

  .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
    background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
  .twk-btn:hover{background:rgba(0,0,0,.88)}
  .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
  .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}

  .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
    border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
    background:transparent;flex-shrink:0}
  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}

  .twk-chips{display:flex;gap:6px}
  .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
    padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
    box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
    transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
  .twk-chip:hover{transform:translateY(-1px);
    box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
  .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
    0 2px 6px rgba(0,0,0,.15)}
  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
    display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
  .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
  .twk-chip>span>i:first-child{box-shadow:none}
  .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;

// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
  const [values, setValues] = React.useState(defaults);
  // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
  // useState-style call doesn't write a "[object Object]" key into the persisted
  // JSON block.
  const setTweak = React.useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues((prev) => ({ ...prev, ...edits }));
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
    // Same-window signal so in-page listeners (deck-stage rail thumbnails)
    // can react — the parent message only reaches the host, not peers.
    window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
  }, []);
  return [values, setTweak];
}

// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = 'Tweaks', children }) {
  const [open, setOpen] = React.useState(false);
  const dragRef = React.useRef(null);
  const offsetRef = React.useRef({ x: 16, y: 16 });
  const PAD = 16;

  const clampToViewport = React.useCallback(() => {
    const panel = dragRef.current;
    if (!panel) return;
    const w = panel.offsetWidth, h = panel.offsetHeight;
    const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
    const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
    offsetRef.current = {
      x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
      y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
    };
    panel.style.right = offsetRef.current.x + 'px';
    panel.style.bottom = offsetRef.current.y + 'px';
  }, []);

  React.useEffect(() => {
    if (!open) return;
    clampToViewport();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', clampToViewport);
      return () => window.removeEventListener('resize', clampToViewport);
    }
    const ro = new ResizeObserver(clampToViewport);
    ro.observe(document.documentElement);
    return () => ro.disconnect();
  }, [open, clampToViewport]);

  React.useEffect(() => {
    const onMsg = (e) => {
      const t = e?.data?.type;
      if (t === '__activate_edit_mode') setOpen(true);
      else if (t === '__deactivate_edit_mode') setOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const dismiss = () => {
    setOpen(false);
    window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
  };

  const onDragStart = (e) => {
    const panel = dragRef.current;
    if (!panel) return;
    const r = panel.getBoundingClientRect();
    const sx = e.clientX, sy = e.clientY;
    const startRight = window.innerWidth - r.right;
    const startBottom = window.innerHeight - r.bottom;
    const move = (ev) => {
      offsetRef.current = {
        x: startRight - (ev.clientX - sx),
        y: startBottom - (ev.clientY - sy),
      };
      clampToViewport();
    };
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };

  if (!open) return null;
  return (
    <>
      <style>{__TWEAKS_STYLE}</style>
      <div ref={dragRef} className="twk-panel" data-omelette-chrome=""
           style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
        <div className="twk-hd" onMouseDown={onDragStart}>
          <b>{title}</b>
          <button className="twk-x" aria-label="Close tweaks"
                  onMouseDown={(e) => e.stopPropagation()}
                  onClick={dismiss}>✕</button>
        </div>
        <div className="twk-body">
          {children}
        </div>
      </div>
    </>
  );
}

// ── Layout helpers ──────────────────────────────────────────────────────────

function TweakSection({ label, children }) {
  return (
    <>
      <div className="twk-sect">{label}</div>
      {children}
    </>
  );
}

function TweakRow({ label, value, children, inline = false }) {
  return (
    <div className={inline ? 'twk-row twk-row-h' : 'twk-row'}>
      <div className="twk-lbl">
        <span>{label}</span>
        {value != null && <span className="twk-val">{value}</span>}
      </div>
      {children}
    </div>
  );
}

// ── Controls ────────────────────────────────────────────────────────────────

function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
  return (
    <TweakRow label={label} value={`${value}${unit}`}>
      <input type="range" className="twk-slider" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </TweakRow>
  );
}

function TweakToggle({ label, value, onChange }) {
  return (
    <div className="twk-row twk-row-h">
      <div className="twk-lbl"><span>{label}</span></div>
      <button type="button" className="twk-toggle" data-on={value ? '1' : '0'}
              role="switch" aria-checked={!!value}
              onClick={() => onChange(!value)}><i /></button>
    </div>
  );
}

function TweakRadio({ label, value, options, onChange }) {
  const trackRef = React.useRef(null);
  const [dragging, setDragging] = React.useState(false);
  // The active value is read by pointer-move handlers attached for the lifetime
  // of a drag — ref it so a stale closure doesn't fire onChange for every move.
  const valueRef = React.useRef(value);
  valueRef.current = value;

  // Segments wrap mid-word once per-segment width runs out. The track is
  // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
  // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
  // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
  // back to a dropdown rather than wrap.
  const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
  const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
  const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
  if (!fitsAsSegments) {
    // <select> emits strings — map back to the original option value so the
    // fallback stays type-preserving (numbers, booleans) like the segment path.
    const resolve = (s) => {
      const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
      return m === undefined ? s : typeof m === 'object' ? m.value : m;
    };
    return <TweakSelect label={label} value={value} options={options}
                        onChange={(s) => onChange(resolve(s))} />;
  }
  const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
  const idx = Math.max(0, opts.findIndex((o) => o.value === value));
  const n = opts.length;

  const segAt = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    const inner = r.width - 4;
    const i = Math.floor(((clientX - r.left - 2) / inner) * n);
    return opts[Math.max(0, Math.min(n - 1, i))].value;
  };

  const onPointerDown = (e) => {
    setDragging(true);
    const v0 = segAt(e.clientX);
    if (v0 !== valueRef.current) onChange(v0);
    const move = (ev) => {
      if (!trackRef.current) return;
      const v = segAt(ev.clientX);
      if (v !== valueRef.current) onChange(v);
    };
    const up = () => {
      setDragging(false);
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  return (
    <TweakRow label={label}>
      <div ref={trackRef} role="radiogroup" onPointerDown={onPointerDown}
           className={dragging ? 'twk-seg dragging' : 'twk-seg'}>
        <div className="twk-seg-thumb"
             style={{ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
                      width: `calc((100% - 4px) / ${n})` }} />
        {opts.map((o) => (
          <button key={o.value} type="button" role="radio" aria-checked={o.value === value}>
            {o.label}
          </button>
        ))}
      </div>
    </TweakRow>
  );
}

function TweakSelect({ label, value, options, onChange }) {
  return (
    <TweakRow label={label}>
      <select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
        {options.map((o) => {
          const v = typeof o === 'object' ? o.value : o;
          const l = typeof o === 'object' ? o.label : o;
          return <option key={v} value={v}>{l}</option>;
        })}
      </select>
    </TweakRow>
  );
}

function TweakText({ label, value, placeholder, onChange }) {
  return (
    <TweakRow label={label}>
      <input className="twk-field" type="text" value={value} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </TweakRow>
  );
}

function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) {
  const clamp = (n) => {
    if (min != null && n < min) return min;
    if (max != null && n > max) return max;
    return n;
  };
  const startRef = React.useRef({ x: 0, val: 0 });
  const onScrubStart = (e) => {
    e.preventDefault();
    startRef.current = { x: e.clientX, val: value };
    const decimals = (String(step).split('.')[1] || '').length;
    const move = (ev) => {
      const dx = ev.clientX - startRef.current.x;
      const raw = startRef.current.val + dx * step;
      const snapped = Math.round(raw / step) * step;
      onChange(clamp(Number(snapped.toFixed(decimals))));
    };
    const up = () => {
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };
  return (
    <div className="twk-num">
      <span className="twk-num-lbl" onPointerDown={onScrubStart}>{label}</span>
      <input type="number" value={value} min={min} max={max} step={step}
             onChange={(e) => onChange(clamp(Number(e.target.value)))} />
      {unit && <span className="twk-num-unit">{unit}</span>}
    </div>
  );
}

// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
// read on both #111 and #fafafa without per-option configuration. Hex input
// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
function __twkIsLight(hex) {
  const h = String(hex).replace('#', '');
  const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0');
  const n = parseInt(x.slice(0, 6), 16);
  if (Number.isNaN(n)) return true;
  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  return r * 299 + g * 587 + b * 114 > 148000;
}

const __TwkCheck = ({ light }) => (
  <svg viewBox="0 0 14 14" aria-hidden="true">
    <path d="M3 7.2 5.8 10 11 4.2" fill="none" strokeWidth="2.2"
          strokeLinecap="round" strokeLinejoin="round"
          stroke={light ? 'rgba(0,0,0,.78)' : '#fff'} />
  </svg>
);

// TweakColor — curated color/palette picker. Each option is either a single
// hex string or an array of 1-5 hex strings; the card adapts — a lone color
// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
// rest stacked in a sharp column on the right. onChange emits the
// option in the shape it was passed (string stays string, array stays array).
// Without options it falls back to the native color input for back-compat.
function TweakColor({ label, value, options, onChange }) {
  if (!options || !options.length) {
    return (
      <div className="twk-row twk-row-h">
        <div className="twk-lbl"><span>{label}</span></div>
        <input type="color" className="twk-swatch" value={value}
               onChange={(e) => onChange(e.target.value)} />
      </div>
    );
  }
  // Native <input type=color> emits lowercase hex per the HTML spec, so
  // compare case-insensitively. String() guards JSON.stringify(undefined),
  // which returns the primitive undefined (no .toLowerCase).
  const key = (o) => String(JSON.stringify(o)).toLowerCase();
  const cur = key(value);
  return (
    <TweakRow label={label}>
      <div className="twk-chips" role="radiogroup">
        {options.map((o, i) => {
          const colors = Array.isArray(o) ? o : [o];
          const [hero, ...rest] = colors;
          const sup = rest.slice(0, 4);
          const on = key(o) === cur;
          return (
            <button key={i} type="button" className="twk-chip" role="radio"
                    aria-checked={on} data-on={on ? '1' : '0'}
                    aria-label={colors.join(', ')} title={colors.join(' · ')}
                    style={{ background: hero }}
                    onClick={() => onChange(o)}>
              {sup.length > 0 && (
                <span>
                  {sup.map((c, j) => <i key={j} style={{ background: c }} />)}
                </span>
              )}
              {on && <__TwkCheck light={__twkIsLight(hero)} />}
            </button>
          );
        })}
      </div>
    </TweakRow>
  );
}

function TweakButton({ label, onClick, secondary = false }) {
  return (
    <button type="button" className={secondary ? 'twk-btn secondary' : 'twk-btn'}
            onClick={onClick}>{label}</button>
  );
}

Object.assign(window, {
  useTweaks, TweaksPanel, TweakSection, TweakRow,
  TweakSlider, TweakToggle, TweakRadio, TweakSelect,
  TweakText, TweakNumber, TweakColor, TweakButton,
});

/* ===== brand.jsx ===== */
// brand.jsx — Kobar Labs brand marks recreated as crisp SVG
// Exports to window: KMonogram, KobarWordmark, OliveLoader, OliveMark,
//   TatreezTexture, CircuitTexture, ConnectorGlyph

// ---- K monogram (charcoal K + copper square + copper "speed lines") ----
function KMonogram({ size = 34, mono = false, style = {} }) {
  const ink = mono ? "currentColor" : "var(--charcoal)";
  const cu = mono ? "currentColor" : "var(--copper)";
  return (
    <svg viewBox="0 0 122 100" width={size * 1.22} height={size}
         style={style} aria-label="Kobar" role="img">
      {/* speed dashes */}
      <rect x="2"  y="40" width="15" height="5" rx="1.5" fill={cu} opacity={mono ? 0.85 : 1} />
      <rect x="9"  y="50" width="22" height="5" rx="1.5" fill={cu} opacity={mono ? 0.85 : 1} />
      <rect x="5"  y="60" width="13" height="5" rx="1.5" fill={cu} opacity={mono ? 0.85 : 1} />
      {/* copper chip square */}
      <rect x="33" y="45.5" width="14" height="14" rx="1.5" fill={cu} />
      {/* stem */}
      <rect x="46" y="6" width="15" height="88" rx="1" fill={ink} />
      {/* upper arm */}
      <polygon points="60,52 95,6 113,6 71,52" fill={ink} />
      {/* lower arm */}
      <polygon points="60,52 71,52 113,94 95,94" fill={ink} />
    </svg>
  );
}

function KobarWordmark({ size = 22, tagline = false, mono = false, style = {} }) {
  const ink = mono ? "currentColor" : "var(--charcoal)";
  return (
    <div style={{ display: "flex", alignItems: "center", gap: size * 0.45, ...style }}>
      <KMonogram size={size * 1.7} mono={mono} />
      <div style={{ display: "flex", flexDirection: "column", lineHeight: 1 }}>
        <span style={{
          fontFamily: "var(--font-display)", fontWeight: 600, fontSize: size,
          letterSpacing: "-0.01em", color: ink,
        }}>Kobar Labs</span>
        {tagline && (
          <span style={{
            fontFamily: "var(--font-ui)", fontWeight: 500, fontSize: size * 0.36,
            letterSpacing: "0.26em", marginTop: size * 0.28,
            color: "color-mix(in oklab, var(--copper), var(--charcoal) 8%)",
          }}>AI · AUTOMATION · INTELLIGENCE</span>
        )}
      </div>
    </div>
  );
}

// ---- Olive Grove mark / loader ----
// A pinwheel ring of olive leaves with copper berries, swirling.
const LEAF = "M0 0 C 6.4 -3.6, 6.4 -16.5, 0 -22 C -6.4 -16.5, -6.4 -3.6, 0 0 Z";

function OliveMark({ size = 88, spinning = false, speed = 1, style = {} }) {
  const leaves = 11;
  const olives = ["var(--olive)", "color-mix(in oklab, var(--olive), black 16%)",
                  "color-mix(in oklab, var(--olive), var(--warm-stone) 22%)"];
  const dur = spinning ? (3.4 / speed) : 26;
  return (
    <svg viewBox="-60 -60 120 120" width={size} height={size}
         className={spinning ? "olive-spin" : ""}
         style={{ "--olive-dur": dur + "s", overflow: "visible", ...style }}
         aria-hidden="true">
      <g className="olive-rot">
        {Array.from({ length: leaves }).map((_, i) => {
          const a = (i / leaves) * 360;
          const r = 30;
          // swirl: tangential offset + scale variance
          const scale = 0.78 + (i % 3) * 0.12;
          return (
            <g key={i} transform={`rotate(${a}) translate(0 -${r}) rotate(28) scale(${scale})`}>
              <path d={LEAF} fill={olives[i % 3]} />
            </g>
          );
        })}
        {/* copper berries */}
        {[0, 3, 6, 8].map((i) => {
          const a = (i / leaves) * 360;
          return (
            <g key={"b" + i} transform={`rotate(${a}) translate(0 -16)`}>
              <circle r="3.4" fill="var(--copper)" />
            </g>
          );
        })}
        {/* center copper dot */}
        <circle r="3" fill="color-mix(in oklab, var(--copper), white 12%)" />
      </g>
    </svg>
  );
}

// Convenience loader (spinning, with optional pulse glow)
function OliveLoader({ size = 30, speed = 1, style = {} }) {
  return <OliveMark size={size} spinning speed={speed} style={style} />;
}

// ---- Texture: Tatreez (data pattern) — subtle pixel cross-stitch motif ----
function TatreezTexture({ id = "tatreez", color = "var(--olive)", accent = "var(--copper)", opacity = 0.5 }) {
  const cells = [];
  const motif = [
    [2, 0], [1, 1], [2, 1], [3, 1], [0, 2], [2, 2], [4, 2],
    [1, 3], [3, 3], [2, 4],
  ];
  motif.forEach(([x, y], k) => {
    cells.push(
      <rect key={k} x={x * 8} y={y * 8} width="6" height="6"
            fill={k % 4 === 0 ? accent : color} />
    );
  });
  return (
    <svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true">
      <defs>
        <pattern id={id} width="40" height="40" patternUnits="userSpaceOnUse"
                 patternTransform="scale(1)">
          <g opacity={opacity}>{cells}</g>
        </pattern>
      </defs>
    </svg>
  );
}

// ---- Texture: Circuit pathways (line + node flow) for canvas / panels ----
function CircuitTexture({ id = "circuit", color = "var(--copper)", opacity = 0.18 }) {
  return (
    <svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true">
      <defs>
        <pattern id={id} width="120" height="120" patternUnits="userSpaceOnUse">
          <g fill="none" stroke={color} strokeWidth="1" opacity={opacity}>
            <path d="M10 10 V60 H60 M60 10 V40 H100 M10 100 H50 V70" />
            <circle cx="10" cy="10" r="3" fill={color} stroke="none" />
            <circle cx="60" cy="60" r="3" fill={color} stroke="none" />
            <circle cx="100" cy="40" r="3" fill={color} stroke="none" />
            <rect x="47" y="67" width="6" height="6" fill={color} stroke="none" />
            <rect x="7" y="97" width="6" height="6" fill={color} stroke="none" />
          </g>
        </pattern>
      </defs>
    </svg>
  );
}

Object.assign(window, {
  KMonogram, KobarWordmark, OliveLoader, OliveMark, TatreezTexture, CircuitTexture,
});

/* ===== icons.jsx ===== */
// icons.jsx — line icons (UI), connector glyphs, provider marks
// Exports: Icon, Connector, ProviderMark, CONNECTORS, PROVIDERS

const ICONS = {
  plus: "M12 5v14M5 12h14",
  search: "M11 19a8 8 0 100-16 8 8 0 000 16zM21 21l-4.3-4.3",
  send: "M7 11l5-5 5 5M12 6v13",
  chat: "M21 12a8 8 0 01-11.5 7.2L4 20l1.1-4.3A8 8 0 1121 12z",
  flows: "M5 6h6M5 12h14M5 18h9",
  connect: "M8 8L3 13l4 4 5-5M16 16l5-5-4-4-5 5M9 15l6-6",
  runs: "M6 4l14 8-14 8V4z",
  models: "M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3zM12 12l8-4.5M12 12v9M12 12L4 7.5",
  chart: "M4 19V5M4 19h17M8 16v-5M12 16V8M16 16v-9M20 16v-3",
  skills: "M12 3l2.4 5.9L21 9.3l-4.8 4.1L17.7 21 12 17.3 6.3 21l1.5-7.6L3 9.3l6.6-.4L12 3z",
  agents: "M9 11a3 3 0 116 0M5 20a7 7 0 0114 0M12 2v3",
  history: "M3 12a9 9 0 109-9 9 9 0 00-7 3.4M3 4v4h4M12 8v4l3 2",
  settings: "M12 9a3 3 0 100 6 3 3 0 000-6zM19.4 13a7.8 7.8 0 000-2l2-1.5-2-3.4-2.4 1a7.6 7.6 0 00-1.7-1l-.4-2.6H10.7l-.4 2.6a7.6 7.6 0 00-1.7 1l-2.4-1-2 3.4L4.6 11a7.8 7.8 0 000 2l-2 1.5 2 3.4 2.4-1a7.6 7.6 0 001.7 1l.4 2.6h3.5l.4-2.6a7.6 7.6 0 001.7-1l2.4 1 2-3.4z",
  chevDown: "M6 9l6 6 6-6",
  chevRight: "M9 6l6 6-6 6",
  chevLeft: "M15 6l-6 6 6 6",
  x: "M6 6l12 12M18 6L6 18",
  panel: "M4 5h16v14H4zM10 5v14",
  edit: "M4 20h4L19 9l-4-4L4 16v4zM14 6l4 4",
  trash: "M5 7h14M9 7V5h6v2M6 7l1 13h10l1-13",
  copy: "M9 9h10v10H9zM5 15V5h10",
  paperclip: "M20 11l-8 8a5 5 0 01-7-7l8-8a3.3 3.3 0 014.7 4.7l-8 8a1.6 1.6 0 01-2.3-2.3L15 8",
  image: "M4 5h16v14H4zM4 16l4.5-4.5 4 4 3-3L20 16M9 10a1.2 1.2 0 100-2.4 1.2 1.2 0 000 2.4z",
  fileDoc: "M7 3h7l5 5v13H7zM14 3v5h5M10 13h6M10 16h6M10 10h2",
  audioFile: "M9 18V7l10-2v9M9 18a2.5 2.5 0 11-2-2.45M19 14a2.5 2.5 0 11-2-2.45",
  check: "M5 12l5 5 9-11",
  tag: "M3 12V4h8l9 9-8 8-9-9zM7.5 7.5h.01",
  globe: "M12 3a9 9 0 100 18 9 9 0 000-18zM3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18",
  bolt: "M13 3L5 13h6l-1 8 8-10h-6l1-8z",
  brackets: "M8 4C5 4 5 8 5 10s-2 2-2 2 2 0 2 2 0 6 3 6M16 4c3 0 3 4 3 6s2 2 2 2-2 0-2 2 0 6-3 6",
  brain: "M9 4a3 3 0 00-3 3v1a3 3 0 000 6v1a3 3 0 003 3M15 4a3 3 0 013 3v1a3 3 0 010 6v1a3 3 0 01-3 3M9 4c1.5 0 3 1.2 3 3v11M15 4c-1.5 0-3 1.2-3 3M6 8h3M6 14h3M15 8h3M15 14h3",
  play: "M7 5l11 7-11 7V5z",
  sun: "M12 7a5 5 0 100 10 5 5 0 000-10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4",
  moon: "M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z",
  more: "M12 6h.01M12 12h.01M12 18h.01",
  share: "M4 12v7a1 1 0 001 1h14a1 1 0 001-1v-7M16 6l-4-4-4 4M12 2v13",
  pin: "M9 3h6l-1 7 4 3v2H7v-2l4-3-1-7zM12 15v6",
  arrowUp: "M12 19V5M5 12l7-7 7 7",
  refresh: "M3 12a9 9 0 0115-6.7L21 8M21 4v4h-4M21 12a9 9 0 01-15 6.7L3 16M3 20v-4h4",
  layers: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5M3 17l9 5 9-5",
  spark: "M12 4v6M12 14v6M4 12h6M14 12h6",
  clock: "M12 3a9 9 0 100 18 9 9 0 000-18zM12 8v4l3 2",
  filter: "M3 5h18l-7 8v6l-4-2v-4z",
  expand: "M8 3H3v5M16 3h5v5M3 16v5h5M21 16v5h-5",
  drag: "M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01",
  mail: "M4 6h16v12H4zM4 7l8 6 8-6",
  lock: "M6 11V8a6 6 0 0112 0v3M5 11h14v10H5zM12 15v3",
  user: "M12 12a4 4 0 100-8 4 4 0 000 8zM5 20a7 7 0 0114 0",
  eye: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7zM12 15a3 3 0 100-6 3 3 0 000 6z",
  arrowRight: "M5 12h14M13 6l6 6-6 6",
  shield: "M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3zM9 12l2 2 4-4",
  minus: "M5 12h14",
  maximize: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5",
  minimize: "M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5",
  power: "M12 4v8M7.5 7a7 7 0 109 0",
  mic: "M12 3a3 3 0 013 3v6a3 3 0 01-6 0V6a3 3 0 013-3zM5 11a7 7 0 0014 0M12 18v3M8 21h8",
  key: "M14 7a4 4 0 11-3.5 6L4 20H2v-2l1-1h2v-2h2l1.5-1.5A4 4 0 0114 7zM15.5 8.5h.01",
  gear: "M12 9a3 3 0 100 6 3 3 0 000-6zM19.4 13a7.8 7.8 0 000-2l2-1.5-2-3.4-2.4 1a7.6 7.6 0 00-1.7-1l-.4-2.6H10.7l-.4 2.6a7.6 7.6 0 00-1.7 1l-2.4-1-2 3.4L4.6 11a7.8 7.8 0 000 2l-2 1.5 2 3.4 2.4-1a7.6 7.6 0 001.7 1l.4 2.6h3.5l.4-2.6a7.6 7.6 0 001.7-1l2.4 1 2-3.4z",
};

function Icon({ name, size = 18, stroke = 2, style = {}, className = "" }) {
  const d = ICONS[name];
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} fill="none"
         stroke="currentColor" strokeWidth={stroke} strokeLinecap="round"
         strokeLinejoin="round" style={style} className={className} aria-hidden="true">
      {d.split("M").filter(Boolean).map((seg, i) => <path key={i} d={"M" + seg} />)}
    </svg>
  );
}

// ---- Connectors ----
const CONNECTORS = {
  gworkspace: { label: "Google Workspace", color: "#1A73E8", short: "Workspace" },
  m365:       { label: "Microsoft 365", color: "#D8503C", short: "M365" },
  slack:      { label: "Slack", color: "#5A2D6E", short: "Slack" },
  github:     { label: "GitHub", color: "#24292E", short: "GitHub" },
  notion:     { label: "Notion", color: "#1F1F1F", short: "Notion" },
  discord:    { label: "Discord", color: "#5865F2", short: "Discord" },
  sheets:     { label: "Google Sheets", color: "#1E8E5A", short: "Sheets" },
  gmail:      { label: "Gmail", color: "#D8503C", short: "Gmail" },
  http:       { label: "HTTP Request", color: "var(--copper)", short: "HTTP" },
  webhook:    { label: "Webhook", color: "var(--olive)", short: "Webhook" },
  schedule:   { label: "Agendamento", color: "var(--olive)", short: "Schedule" },
  openai_c:   { label: "OpenAI", color: "#0E8C6E", short: "OpenAI" },
  gdrive:     { label: "Google Drive", color: "#1FA463", short: "Drive" },
  gcal:       { label: "Google Calendar", color: "#4285F4", short: "Calendar" },
  gforms:     { label: "Google Forms", color: "#7248B9", short: "Forms" },
  teams:      { label: "Microsoft Teams", color: "#5059C9", short: "Teams" },
  outlook:    { label: "Outlook", color: "#0A66C2", short: "Outlook" },
  gitlab:     { label: "GitLab", color: "#FC6D26", short: "GitLab" },
  jira:       { label: "Jira", color: "#2684FF", short: "Jira" },
  trello:     { label: "Trello", color: "#0079BF", short: "Trello" },
  asana:      { label: "Asana", color: "#F06A6A", short: "Asana" },
  hubspot:    { label: "HubSpot", color: "#FF7A59", short: "HubSpot" },
  salesforce: { label: "Salesforce", color: "#00A1E0", short: "Salesforce" },
  stripe:     { label: "Stripe", color: "#635BFF", short: "Stripe" },
  shopify:    { label: "Shopify", color: "#5E8E3E", short: "Shopify" },
  telegram:   { label: "Telegram", color: "#2AABEE", short: "Telegram" },
  whatsapp:   { label: "WhatsApp", color: "#25D366", short: "WhatsApp" },
  twilio:     { label: "Twilio", color: "#F22F46", short: "Twilio" },
  airtable:   { label: "Airtable", color: "#E5862B", short: "Airtable" },
  postgres:   { label: "PostgreSQL", color: "#336791", short: "Postgres" },
  mysql:      { label: "MySQL", color: "#00758F", short: "MySQL" },
  mongodb:    { label: "MongoDB", color: "#3FA037", short: "MongoDB" },
  dropbox:    { label: "Dropbox", color: "#0061FF", short: "Dropbox" },
  aws:        { label: "AWS S3", color: "#E8912D", short: "AWS" },
  hf:         { label: "Hugging Face", color: "#F4A300", short: "HF" },
};

Object.assign(CONNECTORS, {
  http_request: CONNECTORS.http,
  slack_webhook: CONNECTORS.slack,
  google_sheets_append: CONNECTORS.sheets,
  google_drive: CONNECTORS.gdrive,
  google_calendar: CONNECTORS.gcal,
  microsoft_outlook: CONNECTORS.outlook,
  microsoft_teams: CONNECTORS.teams,
  microsoft_excel: CONNECTORS.m365,
});

const CGLYPH = {
  gworkspace: <><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" strokeWidth="2.4"/><path d="M12 12h7" stroke="currentColor" strokeWidth="2.4"/></>,
  m365: <><rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/></>,
  slack: <><rect x="5" y="9" width="6" height="2.4" rx="1.2"/><rect x="13" y="9" width="6" height="2.4" rx="1.2" transform="rotate(90 16 12)"/><rect x="5" y="12.6" width="6" height="2.4" rx="1.2"/><rect x="9" y="6" width="2.4" height="6" rx="1.2"/></>,
  github: <path d="M12 3a9 9 0 00-2.8 17.5c.4.1.6-.2.6-.4v-1.5c-2.5.5-3-1.2-3-1.2-.4-1-1-1.3-1-1.3-.8-.5 0-.5 0-.5.9.1 1.4.9 1.4.9.8 1.4 2.1 1 2.6.8.1-.6.3-1 .6-1.2-2-.2-4.1-1-4.1-4.4 0-1 .3-1.8.9-2.4-.1-.2-.4-1.1.1-2.3 0 0 .7-.2 2.4.9a8.3 8.3 0 014.4 0c1.7-1.1 2.4-.9 2.4-.9.5 1.2.2 2.1.1 2.3.6.6.9 1.4.9 2.4 0 3.4-2.1 4.2-4.1 4.4.3.3.6.8.6 1.7v2.5c0 .2.2.5.6.4A9 9 0 0012 3z" fill="currentColor" stroke="none"/>,
  notion: <><rect x="5" y="4" width="14" height="16" rx="1.5" fill="none" stroke="currentColor" strokeWidth="2"/><path d="M9 16V9l6 7V9" stroke="currentColor" strokeWidth="2"/></>,
  discord: <path d="M8 7.5a14 14 0 018 0M7 8C5.5 11 5 14 5.2 17c1.4 1 2.8 1.6 4 1.8l.8-1.4M17 8c1.5 3 2 6 1.8 9-1.4 1-2.8 1.6-4 1.8l-.8-1.4M9.5 13.5h.01M14.5 13.5h.01" fill="none" stroke="currentColor" strokeWidth="2"/>,
  sheets: <><rect x="5" y="4" width="14" height="16" rx="1.5" fill="none" stroke="currentColor" strokeWidth="2"/><path d="M5 10h14M5 15h14M12 10v10" stroke="currentColor" strokeWidth="2"/></>,
  gmail: <path d="M4 6h16v12H4zM4 7l8 6 8-6" fill="none" stroke="currentColor" strokeWidth="2"/>,
  http: <path d="M12 3a9 9 0 100 18 9 9 0 000-18zM3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18" fill="none" stroke="currentColor" strokeWidth="1.8"/>,
  webhook: <path d="M9 8a3 3 0 114 2.6L15 15M7 13a3 3 0 103.5 4.9M14.5 17H10M16 11a3 3 0 11-1.5 5.6" fill="none" stroke="currentColor" strokeWidth="2"/>,
  schedule: <path d="M12 3a9 9 0 100 18 9 9 0 000-18zM12 8v4l3 2" fill="none" stroke="currentColor" strokeWidth="2"/>,
  openai_c: <path d="M12 4a4 4 0 014 4 4 4 0 010 8 4 4 0 01-8 0 4 4 0 010-8 4 4 0 014-4zM12 12v4M12 12L8.5 10M12 12l3.5-2" fill="none" stroke="currentColor" strokeWidth="1.8"/>,
  postgres: <><ellipse cx="12" cy="6" rx="7" ry="2.6" fill="none" stroke="currentColor" strokeWidth="2"/><path d="M5 6v12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6V6" fill="none" stroke="currentColor" strokeWidth="2"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6" fill="none" stroke="currentColor" strokeWidth="2"/></>,
};

Object.assign(CGLYPH, {
  http_request: CGLYPH.http,
  slack_webhook: CGLYPH.slack,
  google_sheets_append: CGLYPH.sheets,
  google_drive: CGLYPH.gdrive,
  google_calendar: CGLYPH.gcal,
  microsoft_outlook: CGLYPH.outlook,
  microsoft_teams: CGLYPH.teams,
  microsoft_excel: CGLYPH.m365,
});

function Connector({ id, size = 22, tile = true, style = {} }) {
  const c = CONNECTORS[id] || CONNECTORS.http;
  const glyph = CGLYPH[id];
  const inner = glyph ? (
    <svg viewBox="0 0 24 24" width={tile ? size * 0.62 : size} height={tile ? size * 0.62 : size}
         style={{ color: tile ? "#fff" : c.color }} aria-label={c.label} role="img">
      {glyph}
    </svg>
  ) : (
    <span style={{ fontFamily: "var(--font-display)", fontWeight: 700,
      fontSize: size * (tile ? 0.44 : 0.72), color: tile ? "#fff" : c.color, lineHeight: 1 }}>
      {(c.short || c.label)[0].toUpperCase()}</span>
  );
  if (!tile) return inner;
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      width: size, height: size, borderRadius: size * 0.28,
      background: c.color, flexShrink: 0,
      boxShadow: "inset 0 1px 0 rgba(255,255,255,.18)", ...style,
    }}>{inner}</span>
  );
}

// ---- AI providers ----
const PROVIDERS = {
  ollama:    { label: "Ollama", sub: "Local/Cloud", model: "gpt-oss:120b",    kind: "local",  tools: ["web_search"] },
  openai:    { label: "OpenAI", sub: "Cloud API",  model: "gpt-4.1-mini",    kind: "cloud",  tools: ["web_search"] },
  anthropic: { label: "Claude", sub: "Cloud API",  model: "claude-sonnet-4-5",kind: "cloud", tools: ["web_search"] },
  deepseek:  { label: "DeepSeek", sub: "Cloud API", model: "deepseek-chat",   kind: "cloud",  tools: ["web_search"] },
  gemini:    { label: "Gemini", sub: "Cloud API",  model: "gemini-2.5-flash",kind: "cloud",   tools: ["google_search"] },
};

const PGLYPH = {
  ollama: <path d="M7 14c0-4 2-7 5-7s5 3 5 7M7 14c-1 0-1.5 1-1.5 2.5M17 14c1 0 1.5 1 1.5 2.5M9 11.5h.01M15 11.5h.01M7 16.5h10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>,
  openai: <path d="M12 4a4 4 0 014 4 4 4 0 010 8 4 4 0 01-8 0 4 4 0 010-8 4 4 0 014-4z" fill="none" stroke="currentColor" strokeWidth="1.7"/>,
  anthropic: <path d="M7.5 17l4-11h1l4 11M9.2 13h5.6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>,
  deepseek: <path d="M5 12c2.4-4.6 7.8-6.1 12-3.5-1.4 0-2.7.5-3.6 1.4 1.7.1 3.2.9 4.6 2.1-2.4 4.6-7.8 6.1-12 3.5 1.4 0 2.7-.5 3.6-1.4A8.4 8.4 0 015 12z" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/>,
  gemini: <path d="M12 3c.4 4.5 1.5 6.6 6 9-4.5 2.4-5.6 4.5-6 9-.4-4.5-1.5-6.6-6-9 4.5-2.4 5.6-4.5 6-9z" fill="currentColor" stroke="none"/>,
};

function ProviderMark({ id, size = 18, style = {} }) {
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} style={{ color: "currentColor", ...style }}
         aria-hidden="true">{PGLYPH[id] || PGLYPH.ollama}</svg>
  );
}

Object.assign(window, { Icon, Connector, ProviderMark, CONNECTORS, PROVIDERS });

/* ===== data.jsx ===== */
// data.jsx — seed content
const NAV = [
  { id: "chats", label: "Chats", icon: "chat" },
  { id: "flows", label: "Flows", icon: "flows" },
  { id: "connect", label: "Conectores", icon: "connect" },
  { id: "runs", label: "Execuções", icon: "runs" },
  { id: "models", label: "Modelos", icon: "models" },
  { id: "agents", label: "Agentes", icon: "agents" },
  { id: "history", label: "Histórico", icon: "history" },
];

// connectors shown in the suggestion filter row
const AVAILABLE_CONNECTORS = [
  "webhook", "http", "github", "slack", "gmail", "sheets", "notion",
  "gdrive", "gcal", "outlook", "teams", "m365", "hubspot", "trello", "airtable",
];

// chats live in app state (pin / rename / delete)
const SEED_CHATS = [];
const WHEN_ORDER = ["Hoje", "Ontem", "7 dias anteriores", "30 dias anteriores"];

const STARTERS = [
  { icon: "bolt", text: "Monitorar uma API e me avisar quando algo mudar" },
  { icon: "clock", text: "Agendar uma tarefa que roda todo dia de manhã" },
  { icon: "layers", text: "Conectar duas ferramentas e sincronizar dados" },
  { icon: "spark", text: "Resumir novidades e mandar pro meu canal" },
  { icon: "refresh", text: "Sincronizar leads do formulário com o CRM" },
  { icon: "filter", text: "Filtrar e-mails e encaminhar pro responsável" },
];

const CONNECTOR_SUGGESTIONS = {
  webhook: [
    { title: "Novo lead → Sheets", desc: "Quando chegar um lead via webhook, adicione uma linha na planilha.", connectors: ["webhook", "sheets"] },
    { title: "Webhook → Notion", desc: "Crie um card no Notion sempre que um formulário for enviado.", connectors: ["webhook", "notion"] },
    { title: "Webhook → WhatsApp", desc: "Dispara uma mensagem no WhatsApp a cada novo evento recebido.", connectors: ["webhook", "whatsapp"] },
  ],
  http: [
    { title: "Monitorar API e alertar no Discord", desc: "A cada 5 min, faz GET em uma URL e avisa no Discord se o status mudar.", connectors: ["schedule", "http", "discord"] },
    { title: "Cotação diária em planilha", desc: "Todo dia às 9h, busca a cotação e registra no Google Sheets.", connectors: ["schedule", "http", "sheets"] },
  ],
  github: [
    { title: "Resumo de PRs abertos no Slack", desc: "Toda manhã, lista os PRs aguardando revisão no canal do time.", connectors: ["schedule", "github", "slack"] },
    { title: "Issue nova → Notion", desc: "Cada issue criada vira uma tarefa priorizada no board.", connectors: ["github", "notion"] },
    { title: "Deploy → aviso no Teams", desc: "Quando um release for publicado, notifica o time no Teams.", connectors: ["github", "teams"] },
  ],
  slack: [
    { title: "Resumo diário no Slack", desc: "Compila as métricas do dia e posta um digest no canal.", connectors: ["schedule", "slack"] },
    { title: "Menção → tarefa", desc: "Quando te marcarem, cria uma tarefa de acompanhamento no Trello.", connectors: ["slack", "trello"] },
  ],
  gmail: [
    { title: "Triagem de e-mails de suporte", desc: "Classifica e-mails recebidos e encaminha pro responsável.", connectors: ["gmail", "notion"] },
    { title: "Anexos → Drive", desc: "Salva automaticamente anexos de faturas no Google Drive.", connectors: ["gmail", "gdrive"] },
  ],
  sheets: [
    { title: "Backup semanal de planilhas", desc: "Toda segunda, duplica e arquiva as planilhas do mês.", connectors: ["schedule", "sheets"] },
    { title: "Linha nova → Slack", desc: "Avisa o time quando uma nova linha for adicionada.", connectors: ["sheets", "slack"] },
  ],
  notion: [
    { title: "Notion → lembrete", desc: "Tarefas que vencem hoje viram lembretes no seu canal.", connectors: ["notion", "slack"] },
    { title: "Página nova → Telegram", desc: "Notifica no Telegram quando uma página for criada no banco.", connectors: ["notion", "telegram"] },
  ],
  discord: [
    { title: "Alerta de incidente", desc: "Quando um serviço cair, posta um alerta com detalhes no Discord.", connectors: ["http", "discord"] },
  ],
  gworkspace: [
    { title: "Evento de calendário → resumo", desc: "Antes de cada reunião, envia uma pauta resumida por e-mail.", connectors: ["gcal", "gmail"] },
  ],
  m365: [
    { title: "Teams → registro", desc: "Arquiva mensagens importantes do Teams numa planilha.", connectors: ["teams", "sheets"] },
  ],
  telegram: [
    { title: "Bot de status diário", desc: "Manda um resumo do sistema no Telegram toda manhã.", connectors: ["schedule", "telegram"] },
  ],
  stripe: [
    { title: "Pagamento → recibo no e-mail", desc: "Cada cobrança aprovada dispara um recibo automático.", connectors: ["stripe", "gmail"] },
    { title: "Reembolso → alerta no Slack", desc: "Avisa o financeiro sempre que houver um reembolso.", connectors: ["stripe", "slack"] },
  ],
  hubspot: [
    { title: "Lead qualificado → tarefa", desc: "Quando um lead esquenta, cria tarefa pro vendedor responsável.", connectors: ["hubspot", "asana"] },
  ],
  gcal: [
    { title: "Reunião nova → Slack", desc: "Posta no canal quando um evento for criado na agenda.", connectors: ["gcal", "slack"] },
  ],
  trello: [
    { title: "Card movido → notificação", desc: "Avisa quando um card chega na coluna 'Concluído'.", connectors: ["trello", "discord"] },
  ],
  airtable: [
    { title: "Registro novo → e-mail", desc: "Dispara um e-mail formatado a cada registro adicionado.", connectors: ["airtable", "gmail"] },
  ],
  salesforce: [
    { title: "Oportunidade ganha → Slack", desc: "Comemora no canal quando um negócio é fechado no Salesforce.", connectors: ["salesforce", "slack"] },
  ],
  shopify: [
    { title: "Pedido novo → planilha", desc: "Registra cada pedido da loja numa planilha de controle.", connectors: ["shopify", "sheets"] },
  ],
};

// flat pool used for the 4 random suggestions
const SUGGESTION_POOL = Object.values(CONNECTOR_SUGGESTIONS).flat();
function pickRandom(arr, n) {
  const copy = [...arr];
  for (let i = copy.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [copy[i], copy[j]] = [copy[j], copy[i]]; }
  return copy.slice(0, n);
}

// blueprint generated by the AI
const BLUEPRINT = {
  name: "Cotação USD/BRL diária", tag: "finanças",
  nodes: [
    { id: "n1", kind: "TRIGGER", title: "Todo dia às 9h", sub: "schedule", connector: "schedule" },
    { id: "n2", kind: "ACTION", title: "Buscar cotação USD/BRL", sub: "http.request", connector: "http" },
    { id: "n3", kind: "ACTION", title: "Registrar no Google Sheets", sub: "sheets.append_row", connector: "sheets" },
  ],
};

// blank workflow (Flows → Novo workflow)
const BLANK_BLUEPRINT = { name: "Novo workflow", tag: "rascunho", blank: true, nodes: [] };

// credential fields requested per connector type (node settings)
const CONN_CREDENTIALS = {
  schedule: [
    { k: "freq", label: "Frequência", type: "select", options: ["A cada 5 min", "A cada hora", "Diária", "Semanal", "Mensal"] },
    { k: "time", label: "Horário", ph: "09:00", type: "time" },
    { k: "tz", label: "Fuso horário", ph: "America/Sao_Paulo" },
  ],
  http: [
    { k: "url", label: "URL", ph: "https://api.exemplo.com/cotacao" },
    { k: "method", label: "Método", type: "select", options: ["GET", "POST", "PUT", "DELETE"] },
  ],
  webhook: [
    { k: "path", label: "Caminho do webhook", ph: "/hooks/novo-lead" },
  ],
  sheets: [
    { k: "account", label: "Conta Google", ph: "voce@gmail.com" },
    { k: "sheet", label: "ID da planilha", ph: "1AbC…xyz" },
    { k: "range", label: "Intervalo", ph: "Página1!A:D" },
  ],
  gmail: [{ k: "account", label: "Conta Gmail", ph: "voce@gmail.com" }, { k: "label", label: "Rótulo (opcional)", ph: "Suporte" }],
  slack: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Slack em Conectores" }, { k: "channel", label: "Canal", ph: "#geral" }],
  discord: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Discord em Conectores" }, { k: "channel", label: "Canal", ph: "#avisos" }],
  telegram: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Telegram em Conectores" }, { k: "chat", label: "Chat ID", ph: "@meu_canal" }],
  notion: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Notion em Conectores" }, { k: "db", label: "ID do banco de dados", ph: "a1b2c3…" }],
  github: [{ k: "account", label: "Conta OAuth", ph: "Conecte o GitHub em Conectores" }, { k: "repo", label: "Repositório", ph: "org/projeto" }],
  stripe: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Stripe em Conectores" }],
  gdrive: [{ k: "account", label: "Conta Google", ph: "Conecte o Google Drive em Conectores" }, { k: "folder", label: "Pasta", ph: "ID ou caminho da pasta" }],
  gcal: [{ k: "account", label: "Conta Google", ph: "Conecte o Google Calendar em Conectores" }, { k: "calendar", label: "Agenda", ph: "primary" }],
  outlook: [{ k: "account", label: "Conta Microsoft", ph: "Conecte o Outlook em Conectores" }, { k: "folder", label: "Pasta", ph: "Inbox" }],
  teams: [{ k: "account", label: "Conta Microsoft", ph: "Conecte o Teams em Conectores" }, { k: "channel", label: "Canal", ph: "Time / Canal" }],
  m365: [{ k: "account", label: "Conta Microsoft", ph: "Conecte o Microsoft 365 em Conectores" }, { k: "workbook", label: "Workbook", ph: "Arquivo Excel" }],
  hubspot: [{ k: "account", label: "Conta OAuth", ph: "Conecte o HubSpot em Conectores" }, { k: "pipeline", label: "Pipeline", ph: "default" }],
  airtable: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Airtable em Conectores" }, { k: "base", label: "Base", ph: "app..." }],
  trello: [{ k: "account", label: "Conta OAuth", ph: "Conecte o Trello em Conectores" }, { k: "board", label: "Board", ph: "Board ID" }],
};
const DEFAULT_CREDENTIALS = [{ k: "account", label: "Conta OAuth", ph: "Conecte uma conta em Conectores" }];

// ---- Flows list ----
const FLOWS = [
  { id: "f1", name: "Cotação USD/BRL diária", tag: "finanças", status: "ativo", nodes: 3, connectors: ["schedule", "http", "sheets"], lastRun: "há 2 h", runs: 142 },
  { id: "f2", name: "Resumo de PRs no Slack", tag: "dev", status: "ativo", nodes: 4, connectors: ["schedule", "github", "slack"], lastRun: "há 5 h", runs: 88 },
  { id: "f3", name: "Novo lead para CRM no Notion", tag: "vendas", status: "rascunho", nodes: 3, connectors: ["webhook", "notion"], lastRun: "Nunca", runs: 0 },
  { id: "f4", name: "Alerta de API fora do ar", tag: "infra", status: "pausado", nodes: 3, connectors: ["http", "discord"], lastRun: "há 3 d", runs: 311 },
  { id: "f5", name: "Triagem de e-mails de suporte", tag: "suporte", status: "ativo", nodes: 5, connectors: ["gmail", "notion"], lastRun: "há 12 min", runs: 506 },
  { id: "f6", name: "Cobrança Stripe → planilha", tag: "finanças", status: "ativo", nodes: 4, connectors: ["stripe", "sheets"], lastRun: "há 1 h", runs: 73 },
];

// ---- Runs / executions ----
const RUNS = [
  { id: "r1", flow: "Triagem de e-mails de suporte", status: "sucesso", when: "há 12 min", dur: "1,2 s", trigger: "gmail" },
  { id: "r2", flow: "Cotação USD/BRL diária", status: "sucesso", when: "há 2 h", dur: "0,8 s", trigger: "schedule" },
  { id: "r3", flow: "Cobrança Stripe → planilha", status: "erro", when: "há 1 h", dur: "2,4 s", trigger: "stripe" },
  { id: "r4", flow: "Resumo de PRs no Slack", status: "sucesso", when: "há 5 h", dur: "1,0 s", trigger: "schedule" },
  { id: "r5", flow: "Alerta de API fora do ar", status: "em execução", when: "agora", dur: "em curso", trigger: "http" },
  { id: "r6", flow: "Triagem de e-mails de suporte", status: "sucesso", when: "há 6 h", dur: "1,1 s", trigger: "gmail" },
];

// ---- Skills catalog (agents) ----
const SKILLS_CATALOG = [
  { id: "build_flow", label: "Gerar workflow", icon: "flows", desc: "Cria fluxos de automação a partir de linguagem natural." },
  { id: "web_search", label: "Buscar na web", icon: "globe", desc: "Pesquisa informações atualizadas na internet." },
  { id: "classify", label: "Classificar texto", icon: "tag", desc: "Rotula e categoriza mensagens e documentos." },
  { id: "summarize", label: "Resumir conteúdo", icon: "layers", desc: "Condensa textos longos em resumos objetivos." },
  { id: "extract", label: "Extrair dados", icon: "brackets", desc: "Identifica e estrutura campos de textos e arquivos." },
  { id: "route", label: "Rotear & encaminhar", icon: "share", desc: "Direciona itens ao responsável ou canal certo." },
  { id: "sql", label: "Consultar banco", icon: "models", desc: "Lê e analisa dados em bancos conectados." },
  { id: "notify", label: "Notificar", icon: "bolt", desc: "Envia avisos em canais como Slack, e-mail e Telegram." },
  { id: "schedule", label: "Agendar tarefas", icon: "clock", desc: "Dispara ações em horários ou intervalos definidos." },
  { id: "reply", label: "Responder mensagens", icon: "chat", desc: "Redige e envia respostas em nome do usuário." },
];

// ---- Agents ----
const AGENTS = [
  { id: "a1", name: "Construtor de Workflows", desc: "Transforma pedidos em linguagem natural em fluxos prontos.", model: "claude-sonnet-4.5", status: "ativo",
    skillIds: ["build_flow", "extract", "schedule", "notify", "summarize", "route"],
    instructions: "Interprete o pedido do usuário, identifique gatilhos e ações, e monte o workflow conectando as ferramentas disponíveis." },
  { id: "a2", name: "Triador de Suporte", desc: "Lê, classifica e roteia tickets recebidos por e-mail.", model: "gpt-4o", status: "ativo",
    skillIds: ["classify", "route", "reply"],
    instructions: "Classifique cada e-mail por urgência e tema, e encaminhe ao responsável certo com um resumo." },
  { id: "a3", name: "Analista de Dados", desc: "Resume planilhas e responde perguntas sobre os números.", model: "gemma3:4b", status: "pausado",
    skillIds: ["sql", "summarize", "extract", "notify"],
    instructions: "Consulte os dados conectados, gere análises e responda perguntas de negócio de forma objetiva." },
];

// ---- Models (providers) ----
const MODEL_CATALOG = [
  { provider: "ollama", connected: true, models: ["gpt-oss:120b", "gpt-oss:20b", "gemma3:27b", "qwen3-next:80b"],
    webSearchModels: ["gpt-oss:120b", "gpt-oss:20b", "gemma3:27b", "qwen3-next:80b"],
    thinkingModels: ["gpt-oss:120b", "gpt-oss:20b", "gpt-oss:120b-cloud", "gpt-oss:20b-cloud", "qwen3-next:80b"] },
  { provider: "openai", connected: true, models: ["gpt-4.1-mini", "gpt-4.1", "gpt-4o-mini", "gpt-4o"],
    webSearchModels: ["gpt-4.1-mini", "gpt-4.1", "gpt-4o-mini", "gpt-4o"],
    thinkingModels: ["o3-mini"] },
  { provider: "anthropic", connected: false, models: ["claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5"],
    webSearchModels: ["claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5"],
    thinkingModels: ["claude-sonnet-4-5", "claude-opus-4-1"] },
  { provider: "deepseek", connected: false, models: ["deepseek-chat", "deepseek-reasoner"],
    webSearchModels: ["deepseek-chat", "deepseek-reasoner"],
    thinkingModels: ["deepseek-reasoner"] },
  { provider: "gemini", connected: false, models: ["gemini-2.5-flash", "gemini-2.5-pro"],
    webSearchModels: ["gemini-2.5-flash", "gemini-2.5-pro"],
    thinkingModels: ["gemini-2.5-flash", "gemini-2.5-pro"] },
];

// ---- History feed ----
const HISTORY = [
  { id: "h1", icon: "flows", text: "Workflow “Cotação USD/BRL diária” foi publicado", when: "há 2 h" },
  { id: "h2", icon: "runs", text: "Execução com erro em “Cobrança Stripe → planilha”", when: "há 1 h", bad: true },
  { id: "h3", icon: "connect", text: "Conector Stripe conectado", when: "ontem" },
  { id: "h4", icon: "chat", text: "Nova conversa: “Triagem de e-mails de suporte”", when: "ontem" },
  { id: "h5", icon: "agents", text: "Agente “Triador de Suporte” ativado", when: "há 3 d" },
  { id: "h6", icon: "models", text: "Modelo local llama3.1:8b adicionado", when: "há 4 d" },
];

// ---- Connector categories (Connect page) ----
const CONNECTOR_CATEGORIES = [
  { label: "Comunicação", ids: ["slack", "discord", "telegram", "whatsapp", "teams", "gmail", "outlook"] },
  { label: "Produtividade", ids: ["notion", "sheets", "gdrive", "gcal", "gforms", "airtable", "trello", "asana"] },
  { label: "Dev & DevOps", ids: ["github", "gitlab", "jira", "http", "webhook", "aws"] },
  { label: "CRM & Vendas", ids: ["hubspot", "salesforce", "stripe", "shopify"] },
  { label: "Dados", ids: ["postgres", "mysql", "mongodb", "dropbox"] },
];
const CONNECTED_IDS = [];

// ---- Plans ----
const PLANS = [
  { id: "free", name: "Free", price: "R$ 0", per: "/mês", tagline: "Para experimentar", current: true,
    features: ["1 workflow ativo", "100 execuções/mês", "Somente modelos locais (Ollama)", "3 conectores", "15 buscas online/mês", "Histórico de 7 dias"] },
  { id: "basic", name: "Basic", price: "R$ 49", per: "/mês", tagline: "Para uso pessoal", current: false,
    features: ["10 workflows ativos", "5 mil execuções/mês", "500 mensagens de IA na nuvem/mês", "OpenAI, Claude, Gemini e DeepSeek inclusos", "Conectores ilimitados", "Histórico de 30 dias"] },
  { id: "pro", name: "Pro", price: "R$ 199", per: "/mês", tagline: "Para times pequenos", current: false, popular: true,
    features: ["100 workflows ativos", "50 mil execuções/mês", "2.000 mensagens de IA na nuvem/mês", "Modelos premium via sua chave (BYOK)", "Agentes personalizados", "1.000 buscas online/mês", "Suporte prioritário"] },
  { id: "proplus", name: "Pro+", price: "R$ 599", per: "/mês", tagline: "Para empresas", current: false,
    features: ["Tudo do Pro, limites ampliados", "5.000 mensagens de IA na nuvem/mês", "250 mil execuções/mês", "SSO & controle de acesso", "Logs de auditoria", "Gerente de conta dedicado"] },
];

// mapeia ids de icone do front <-> ids de conector do backend (os conectaveis)
const CONNECTOR_BACKEND = {
  schedule: "schedule", webhook: "webhook", http: "http_request",
  slack: "slack_webhook", sheets: "google_sheets_append", gmail: "gmail",
  gdrive: "google_drive", gcal: "google_calendar",
  outlook: "microsoft_outlook", teams: "microsoft_teams", m365: "microsoft_excel",
  notion: "notion", github: "github", hubspot: "hubspot", airtable: "airtable", trello: "trello",
  stripe: "stripe", discord: "discord", telegram: "telegram",
  whatsapp: "whatsapp", twilio: "twilio",
  // Conector de banco de dados (PostgreSQL, somente-leitura): card "PostgreSQL"
  // da categoria Dados passa a ser conectavel de verdade.
  postgres: "database",
};
const CONNECTOR_FRONT = Object.fromEntries(Object.entries(CONNECTOR_BACKEND).map(([f, b]) => [b, f]));
function frontConnectorId(id) { return CONNECTOR_FRONT[id] || id; }
function backendConnectorId(id) { return CONNECTOR_BACKEND[id] || id; }

Object.assign(window, {
  NAV, AVAILABLE_CONNECTORS, SEED_CHATS, WHEN_ORDER, STARTERS,
  CONNECTOR_SUGGESTIONS, SUGGESTION_POOL, pickRandom, BLUEPRINT, BLANK_BLUEPRINT,
  CONN_CREDENTIALS, DEFAULT_CREDENTIALS,
  FLOWS, RUNS, AGENTS, SKILLS_CATALOG, MODEL_CATALOG, HISTORY, CONNECTOR_CATEGORIES, CONNECTED_IDS, PLANS,
  CONNECTOR_BACKEND, CONNECTOR_FRONT, frontConnectorId, backendConnectorId,
});

/* ===== ui.jsx ===== */
// ui.jsx — shared UI: IconRail, ChatListPanel, ProviderDropdown, SuggestionCard,
// StarterChip, MessageBubble, Composer, Avatar
const { useState, useRef, useEffect } = React;

function Avatar({ name = "Kobar", size = 28 }) {
  const initials = (name || "K").split(" ").map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "K";
  return (
    <span className="kb-avatar" style={{ width: size, height: size, fontSize: size * 0.4 }}>
      {initials}
    </span>
  );
}

function IconRail({ active, onNav, onHome, dark, expanded, onToggleExpand, onUpgrade, user, onLogout }) {
  const [acct, setAcct] = useState(false);
  const uName = (user && user.name) || "Minha conta";
  const uEmail = (user && user.email) || "";
  const uPlan = (user && user.org ? user.org + " · " : "") + ((user && user.plan) || "Free");
  const acctRef = useRef(null);
  useEffect(() => {
    if (!acct) return;
    const h = (e) => { if (acctRef.current && !acctRef.current.contains(e.target)) setAcct(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [acct]);
  return (
    <nav className={"kb-rail" + (dark ? " dark" : "") + (expanded ? " expanded" : "")}>
      <div className="kb-rail-top">
        <button className="kb-rail-logo" onClick={onHome} title="Kobar Flows">
          <KMonogram size={26} mono={dark} />
          {expanded && <span className="kb-rail-wordmark">Kobar Flows</span>}
        </button>
        <button className="kb-rail-toggle" onClick={onToggleExpand} data-tip={expanded ? "Recolher menu" : "Expandir menu"}>
          <Icon name={expanded ? "chevLeft" : "chevRight"} size={17} />
        </button>
      </div>
      <div className="kb-rail-items">
        {NAV.map((n) => (
          <button key={n.id}
            className={"kb-rail-btn" + (active === n.id ? " active" : "")}
            onClick={() => onNav(n.id)} data-tip={expanded ? "" : n.label}>
            <Icon name={n.icon} size={20} stroke={1.9} />
            {expanded && <span className="kb-rail-label">{n.label}</span>}
          </button>
        ))}
      </div>
      <div className="kb-rail-foot" ref={acctRef}>
        <button className={"kb-rail-account" + (acct ? " open" : "")} onClick={() => setAcct((a) => !a)} data-tip={expanded ? "" : uName}>
          <Avatar size={30} name={uName} />
          {expanded && <span className="kb-rail-acct"><span className="kb-rail-acct-name">{uName}</span><span className="kb-rail-acct-plan">{uPlan}</span></span>}
          {expanded && <Icon name="chevDown" size={16} className="kb-rail-acct-chev" />}
        </button>
        {acct && (
          <div className="kb-acctmenu">
            <div className="kb-acctmenu-id">
              <Avatar size={34} name={uName} />
              <div className="kb-acctmenu-idtxt"><span>{uName}</span><em>{uEmail}</em></div>
            </div>
            <div className="kb-acct-sep" />
            <button className="kb-acct-upgrade" onClick={() => { onUpgrade(); setAcct(false); }}>
              <Icon name="spark" size={15} /> Fazer upgrade de plano
            </button>
            <button onClick={() => { onNav("settings"); setAcct(false); }}><Icon name="settings" size={15} /> Configurações</button>
            <button onClick={() => { onNav("connect"); setAcct(false); }}><Icon name="connect" size={15} /> Conectores</button>
            <div className="kb-acct-sep" />
            <button onClick={() => { setAcct(false); onLogout && onLogout(); }}><Icon name="power" size={15} /> Sair</button>
          </div>
        )}
      </div>
    </nav>
  );
}

function ConnectorDots({ ids, size = 16 }) {
  return (
    <span className="kb-cdots">
      {ids.slice(0, 3).map((id, i) => (
        <Connector key={i} id={id} size={size} style={{ marginLeft: i ? -5 : 0, borderRadius: 5 }} />
      ))}
    </span>
  );
}

function ChatRow({ c, active, onSelect, onPin, onRename, onDelete }) {
  const [menu, setMenu] = useState(false);
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState(c.title);
  const ref = useRef(null);
  useEffect(() => {
    if (!menu) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setMenu(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [menu]);
  const submit = () => { const v = val.trim(); if (v) onRename(c.id, v); setEditing(false); };
  return (
    <div className={"kb-chatrow" + (active ? " active" : "") + (menu ? " menuopen" : "")} ref={ref}>
      <button className="kb-chatrow-main" onClick={() => { if (!editing) onSelect(c.id); }}>
        <ConnectorDots ids={c.connectors} />
        {editing ? (
          <input className="kb-chatrow-edit" value={val} autoFocus
            onChange={(e) => setVal(e.target.value)} onClick={(e) => e.stopPropagation()}
            onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") setEditing(false); }}
            onBlur={submit} />
        ) : (
          <span className="kb-chatrow-title">{c.title}</span>
        )}
      </button>
      {c.pinned && !editing && <Icon name="pin" size={12} className="kb-chatrow-pin" />}
      <button className="kb-chatrow-more" onClick={(e) => { e.stopPropagation(); setMenu((m) => !m); }} data-tip2="Opções">
        <Icon name="more" size={16} />
      </button>
      {menu && (
        <div className="kb-ctxmenu" onClick={(e) => e.stopPropagation()}>
          <button onClick={() => { onPin(c.id); setMenu(false); }}><Icon name="pin" size={15} /> {c.pinned ? "Desafixar" : "Fixar"}</button>
          <button onClick={() => { setEditing(true); setMenu(false); }}><Icon name="edit" size={15} /> Renomear</button>
          <button className="danger" onClick={() => { onDelete(c.id); setMenu(false); }}><Icon name="trash" size={15} /> Excluir</button>
        </div>
      )}
    </div>
  );
}

function ChatListPanel({ chats, activeChat, onSelectChat, onNew, onPin, onRename, onDelete, onDeleteAll, onNav, onUpgrade, collapsed, onToggle, planName = "Pro" }) {
  const [q, setQ] = useState("");
  const [confirmAll, setConfirmAll] = useState(false);
  if (collapsed) return null;
  const filtered = chats.filter((c) => c.title.toLowerCase().includes(q.toLowerCase()));
  const pinned = filtered.filter((c) => c.pinned);
  const groups = WHEN_ORDER.map((w) => ({ label: w, chats: filtered.filter((c) => !c.pinned && c.when === w) })).filter((g) => g.chats.length);
  const rowProps = { active: activeChat, onSelect: onSelectChat, onPin, onRename, onDelete };
  return (
    <aside className="kb-chatlist">
      <div className="kb-chatlist-top">
        <span className="kb-product-name">Conversas</span>
        <div style={{ display: "flex", gap: 2 }}>
          {chats.length > 0 && (
            <button className="kb-icbtn" onClick={() => setConfirmAll(true)} data-tip="Apagar todas as conversas" aria-label="Apagar todas as conversas">
              <Icon name="trash" size={17} />
            </button>
          )}
          <button className="kb-icbtn" onClick={onToggle} data-tip="Recolher"><Icon name="panel" size={18} /></button>
        </div>
      </div>
      {confirmAll && (
        <div className="kb-modal-scrim" onClick={() => setConfirmAll(false)}>
          <div className="kb-modal" style={{ maxWidth: 380 }} onClick={(e) => e.stopPropagation()}>
            <div className="kb-modal-head">
              <span className="kb-build-cta-ic" style={{ background: "#FBEFEB", color: "#9B3D27" }}><Icon name="trash" size={18} /></span>
              <div><h3 className="kb-modal-title">Apagar todas as conversas?</h3>
                <span className="kb-modal-subtle">Esta ação não pode ser desfeita.</span></div>
            </div>
            <p className="kb-modal-desc">As {chats.length} conversas e suas mensagens serão removidas permanentemente do seu workspace.</p>
            <div className="kb-modal-actions">
              <button className="kb-btn ghost" onClick={() => setConfirmAll(false)}>Cancelar</button>
              <button className="kb-btn danger-out" onClick={() => { setConfirmAll(false); onDeleteAll && onDeleteAll(); }}>
                <Icon name="trash" size={15} /> Apagar tudo
              </button>
            </div>
          </div>
        </div>
      )}
      <button className="kb-newchat" onClick={onNew}>
        <Icon name="plus" size={17} stroke={2.3} /> Novo chat
      </button>
      <div className="kb-search">
        <Icon name="search" size={15} />
        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Buscar conversas" />
      </div>
      <div className="kb-chatscroll">
        {pinned.length > 0 && (
          <div className="kb-chatgroup">
            <div className="kb-chatgroup-label"><Icon name="pin" size={11} /> Fixados</div>
            {pinned.map((c) => <ChatRow key={c.id} c={c} active={activeChat === c.id} onSelect={onSelectChat} onPin={onPin} onRename={onRename} onDelete={onDelete} />)}
          </div>
        )}
        {groups.map((g) => (
          <div key={g.label} className="kb-chatgroup">
            <div className="kb-chatgroup-label">{g.label}</div>
            {g.chats.map((c) => <ChatRow key={c.id} c={c} active={activeChat === c.id} onSelect={onSelectChat} onPin={onPin} onRename={onRename} onDelete={onDelete} />)}
          </div>
        ))}
        {filtered.length === 0 && <div className="kb-empty-chats">Nenhuma conversa encontrada.</div>}
      </div>
    </aside>
  );
}

function providerModel(id, modelChoice) {
  return (modelChoice && modelChoice[id]) || (PROVIDERS[id] && PROVIDERS[id].model) || id;
}

function ProviderDropdown({ provider, onChange, dark, modelChoice, onManage }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);
  const p = PROVIDERS[provider];
  // So mostra provedores com conexao ativa (status connected na /api/models).
  // window.MODEL_CATALOG ja vem filtrado: non-admin = so connected; admin/demo = todos.
  const catalog = (typeof window !== "undefined" && Array.isArray(window.MODEL_CATALOG)) ? window.MODEL_CATALOG : [];
  const connectedIds = catalog
    .filter((c) => c.connected && PROVIDERS[c.provider] && (c.models ? c.models.length > 0 : true))
    .map((c) => c.provider);
  const fallbackIds = ["ollama", "openai", "anthropic", "deepseek", "gemini"];
  const ids = connectedIds.length ? connectedIds : fallbackIds;
  const localIds = ids.filter((id) => PROVIDERS[id] && PROVIDERS[id].kind === "local");
  const cloudIds = ids.filter((id) => PROVIDERS[id] && PROVIDERS[id].kind === "cloud");
  return (
    <div className={"kb-prov" + (dark ? " dark" : "")} ref={ref}>
      <button className="kb-prov-trigger" onClick={() => setOpen((o) => !o)}>
        <ProviderMark id={provider} size={16} />
        <span className="kb-prov-label">{p.label}</span>
        <span className="kb-prov-model">{providerModel(provider, modelChoice)}</span>
        <Icon name="chevDown" size={14} style={{ opacity: 0.6 }} />
      </button>
      {open && (
        <div className="kb-prov-menu">
          {localIds.length > 0 && <div className="kb-prov-sec">Local</div>}
          {localIds.map((id) => (
            <ProviderItem key={id} id={id} active={id === provider} modelChoice={modelChoice}
              onPick={() => { onChange(id); setOpen(false); }} />
          ))}
          {cloudIds.length > 0 && <div className="kb-prov-sec">Cloud API</div>}
          {cloudIds.map((id) => (
            <ProviderItem key={id} id={id} active={id === provider} modelChoice={modelChoice}
              onPick={() => { onChange(id); setOpen(false); }} />
          ))}
          <button className="kb-prov-manage" onClick={() => { setOpen(false); onManage && onManage(); }}><Icon name="settings" size={14} /> Gerenciar provedores e modelos</button>
        </div>
      )}
    </div>
  );
}

function ProviderItem({ id, active, onPick, modelChoice }) {
  const p = PROVIDERS[id];
  const model = providerModel(id, modelChoice);
  return (
    <button className={"kb-prov-item" + (active ? " active" : "")} onClick={onPick}>
      <span className="kb-prov-ic"><ProviderMark id={id} size={17} /></span>
      <span className="kb-prov-itxt">
        <span className="kb-prov-iname">{p.label}</span>
        <span className="kb-prov-imodel">{model}</span>
      </span>
      <span className={"kb-prov-tag " + p.kind}>
        {model || (p.kind === "local" ? "local" : "Cloud API")}
      </span>
      {active && <Icon name="check" size={15} className="kb-prov-check" />}
    </button>
  );
}

function StarterChip({ icon, text, onClick }) {
  return (
    <button className="kb-chip" onClick={onClick}>
      <Icon name={icon} size={16} stroke={1.9} />
      <span>{text}</span>
    </button>
  );
}

function SuggestionCard({ s, onClick }) {
  return (
    <button className="kb-sugg" onClick={onClick}>
      <span className="kb-sugg-cons">
        {s.connectors.map((id, i) => <Connector key={i} id={id} size={26} />)}
      </span>
      <span className="kb-sugg-title">{s.title}</span>
      <span className="kb-sugg-desc">{s.desc}</span>
      <span className="kb-sugg-go"><Icon name="arrowUp" size={15} style={{ transform: "rotate(45deg)" }} /></span>
    </button>
  );
}

// Bloco de conectores necessarios numa resposta da IA (clicaveis -> abrem a conexao).
function MessageConnections({ msg, connectedIds, onConnect, onOpenFlow }) {
  const conns = msg.connections || [];
  const approvals = (msg.actions || []).filter((action) => action.type === "confirm_tool_approval" && action.approvalId);
  if (!conns.length && !approvals.length) return null;
  const isConnected = (id) => (msg.connectedOverride && msg.connectedOverride[id]) || (connectedIds && connectedIds.has(id));
  const needsReconnect = (rc) => rc.status === "missing" && isConnected(rc.connectorId);
  const isOn = (rc) => isConnected(rc.connectorId) && !needsReconnect(rc);
  const allOn = conns.every((rc) => isOn(rc));
  const anyReconnect = conns.some((rc) => needsReconnect(rc));
  return (
    <div style={{ marginTop: 10, border: "1px solid var(--line, #E4DED5)", borderRadius: 12, padding: 12, background: "var(--surface, #FBF9F6)" }}>
      {!!conns.length && (
        <>
          <div style={{ fontSize: 12.5, fontWeight: 600, color: "#6B7568", marginBottom: 8 }}>
            {allOn ? "Tudo conectado — pronto para abrir o fluxo." : anyReconnect ? "Reconecte as ferramentas para atualizar permissões:" : "Conecte as ferramentas para concluir:"}
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {conns.map((rc) => {
              const on = isOn(rc);
              const reconnect = needsReconnect(rc);
              return (
                <div key={rc.connectorId} style={{ display: "flex", alignItems: "center", gap: 10 }}>
                  <Connector id={frontConnectorId(rc.connectorId)} size={26} />
                  <span style={{ flex: 1, fontSize: 13.5, color: "#384636" }}>{rc.label}</span>
                  {on ? (
                    <span className="kb-conncard-state on" style={{ fontSize: 12 }}><Icon name="check" size={12} /> conectado</span>
                  ) : (
                    <button className="kb-btn tiny primary" onClick={() => onConnect && onConnect(rc.connectorId, { forceReconnect: reconnect })}>
                      {reconnect ? "Reconectar" : "Conectar"}
                    </button>
                  )}
                </div>
              );
            })}
          </div>
        </>
      )}
      <MessageApprovals approvals={approvals} />
      {allOn && msg.workflow && (
        <button className="kb-build-cta" style={{ marginTop: 10 }} onClick={() => onOpenFlow && onOpenFlow(msg.workflow)}>
          <span className="kb-build-cta-ic"><Icon name="flows" size={18} /></span>
          <span className="kb-build-cta-txt"><strong>Abrir no editor de workflows</strong><em>seu fluxo está pronto para revisar</em></span>
          <Icon name="chevRight" size={18} />
        </button>
      )}
    </div>
  );
}

function MessageApprovals({ approvals }) {
  const [state, setState] = useState({});
  if (!approvals.length) return null;
  const confirm = async (approval) => {
    setState((s) => ({ ...s, [approval.approvalId]: "running" }));
    try {
      await kbApprovals.confirm(approval.approvalId);
      setState((s) => ({ ...s, [approval.approvalId]: "done" }));
    } catch (err) {
      setState((s) => ({ ...s, [approval.approvalId]: (err && err.message) || "erro" }));
    }
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: approvals.length ? 8 : 0 }}>
      {approvals.map((approval) => {
        const status = state[approval.approvalId] || "";
        const done = status === "done";
        const running = status === "running";
        return (
          <div key={approval.approvalId} style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <Connector id={frontConnectorId(approval.connectorId)} size={26} />
            <span style={{ flex: 1, fontSize: 13.5, color: "#384636" }}>{approval.label || "Confirmar ação"}</span>
            {done ? (
              <span className="kb-conncard-state on" style={{ fontSize: 12 }}><Icon name="check" size={12} /> executado</span>
            ) : (
              <button className="kb-btn tiny primary" disabled={running} onClick={() => confirm(approval)}>
                {running ? "Executando..." : "Confirmar"}
              </button>
            )}
          </div>
        );
      })}
    </div>
  );
}

function renderInlineMarkdown(text) {
  const parts = [];
  const re = /(\*\*([^*]+)\*\*)|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;
  let last = 0, m;
  while ((m = re.exec(text))) {
    if (m.index > last) parts.push(text.slice(last, m.index));
    if (m[2]) parts.push(<strong key={parts.length}>{m[2]}</strong>);
    else if (m[3] && m[4]) parts.push(<a key={parts.length} href={m[4]} target="_blank" rel="noreferrer">{m[3]}</a>);
    last = re.lastIndex;
  }
  if (last < text.length) parts.push(text.slice(last));
  return parts;
}

function MarkdownTable({ lines }) {
  const rows = lines
    .filter((line) => !/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line))
    .map((line) => line.replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim()));
  if (!rows.length) return null;
  const [head, ...body] = rows;
  return (
    <div className="kb-md-table-wrap">
      <table className="kb-md-table">
        <thead><tr>{head.map((cell, i) => <th key={i}>{renderInlineMarkdown(cell)}</th>)}</tr></thead>
        <tbody>{body.map((row, i) => <tr key={i}>{row.map((cell, j) => <td key={j}>{renderInlineMarkdown(cell)}</td>)}</tr>)}</tbody>
      </table>
    </div>
  );
}

function decodeChartPayload(payload) {
  try {
    const json = decodeURIComponent(escape(window.atob(payload)));
    const chart = JSON.parse(json);
    if (!chart || !Array.isArray(chart.data)) return null;
    const data = chart.data
      .map((item) => ({ label: String(item.label || ""), value: Number(item.value) }))
      .filter((item) => item.label && Number.isFinite(item.value));
    if (!data.length) return null;
    return { ...chart, data };
  } catch (e) {
    try {
      const chart = JSON.parse(window.atob(payload));
      return chart && Array.isArray(chart.data) ? chart : null;
    } catch (_) {
      return null;
    }
  }
}

function splitMessageCharts(text) {
  const charts = [];
  const clean = String(text || "").replace(/\n?\[\[KOBAR_CHART:([A-Za-z0-9+/=]+)\]\]\s*/g, (_, payload) => {
    const chart = decodeChartPayload(payload);
    if (chart) charts.push(chart);
    return "";
  }).trim();
  return { clean, charts };
}

function formatChartValue(value) {
  return new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(value);
}

const CHART_PALETTE = ["#6B7B3A", "#C56B3E", "#3E7C8C", "#B0893A", "#8A5A9E", "#4C8C5A", "#C0533F", "#7A6FB0"];

// Detecta rótulos temporais/sequenciais (datas, meses, semanas, anos) — nesses casos
// a ORDEM importa e NAO deve ser reordenada por valor.
function looksTemporal(data) {
  const labels = data.map((d) => String(d.label || "").toLowerCase().trim());
  const monthRe = /^(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez|q[1-4]|s\d|sem|semana|dia|seg|ter|qua|qui|sex|sab|dom)/;
  const dateRe = /\d{1,4}[/\-.]\d{1,2}([/\-.]\d{1,4})?|\b(19|20)\d{2}\b/;
  const hits = labels.filter((l) => monthRe.test(l) || dateRe.test(l)).length;
  return hits >= Math.max(2, Math.ceil(labels.length * 0.6));
}

// Gráfico de LINHA/ÁREA em SVG (escala Y de min..max para mostrar variação real, ex.: câmbio).
// Mostra o rótulo de valor em cada ponto.
function ChartLineSVG({ data, area, color }) {
  const W = 600, H = 240, padL = 52, padR = 18, padT = 30, padB = 30;
  const vals = data.map((d) => d.value);
  let min = Math.min(...vals), max = Math.max(...vals);
  if (!Number.isFinite(min) || !Number.isFinite(max)) { min = 0; max = 1; }
  if (min === max) { min -= Math.abs(min || 1) * 0.1 || 1; max += Math.abs(max || 1) * 0.1 || 1; }
  const span = max - min || 1;
  const innerW = W - padL - padR, innerH = H - padT - padB;
  const x = (i) => padL + (data.length <= 1 ? innerW / 2 : (i / (data.length - 1)) * innerW);
  const y = (v) => padT + (1 - (v - min) / span) * innerH;
  const linePts = data.map((d, i) => `${x(i)},${y(d.value)}`).join(" ");
  const areaPts = `${padL},${padT + innerH} ${linePts} ${x(data.length - 1)},${padT + innerH}`;
  const ticks = [min, min + span / 2, max];
  const stroke = color || CHART_PALETTE[0];
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }} preserveAspectRatio="xMidYMid meet">
      {ticks.map((t, i) => {
        const yy = y(t);
        return (
          <g key={i}>
            <line x1={padL} y1={yy} x2={W - padR} y2={yy} stroke="var(--line,#E4DED5)" strokeWidth="1" />
            <text x={padL - 8} y={yy + 3} textAnchor="end" fontSize="11" fill="var(--ink-soft,#8A847C)">{formatChartValue(t)}</text>
          </g>
        );
      })}
      {area && <polygon points={areaPts} fill={stroke} opacity="0.12" />}
      <polyline points={linePts} fill="none" stroke={stroke} strokeWidth="2.4" strokeLinejoin="round" strokeLinecap="round" />
      {data.map((d, i) => {
        const px = x(i), py = y(d.value);
        const anchor = i === 0 ? "start" : i === data.length - 1 ? "end" : "middle";
        const near = py - min < (max - min) * 0.18 ? py + 16 : py - 9; // se perto do topo, rótulo abaixo
        return (
          <g key={i}>
            <circle cx={px} cy={py} r="3.4" fill="#fff" stroke={stroke} strokeWidth="2" />
            <text x={px} y={near} textAnchor={anchor} fontSize="11.5" fontWeight="600" fill="var(--ink,#2B2722)">{formatChartValue(d.value)}</text>
            <text x={px} y={H - 9} textAnchor="middle" fontSize="11" fill="var(--ink-soft,#8A847C)">{String(d.label).slice(0, 8)}</text>
          </g>
        );
      })}
    </svg>
  );
}

// Gráfico de PIZZA/DONUT em SVG real. Fatias ordenadas de MAIOR para menor; rótulo de
// porcentagem na fatia (quando cabe) e valor+% na legenda.
function ChartPieSVG({ data, donut }) {
  const sorted = [...data].sort((a, b) => Math.abs(b.value) - Math.abs(a.value));
  const total = sorted.reduce((s, d) => s + Math.abs(d.value), 0) || 1;
  const cx = 95, cy = 95, r = 86, rl = donut ? 66 : r * 0.62;
  let a0 = -Math.PI / 2;
  const slices = sorted.map((d, i) => {
    const frac = Math.abs(d.value) / total;
    const a1 = a0 + frac * 2 * Math.PI;
    const mid = (a0 + a1) / 2;
    const large = frac > 0.5 ? 1 : 0;
    const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0);
    const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
    const path = frac >= 0.9999
      ? `M${cx - r},${cy} A${r},${r} 0 1 1 ${cx + r},${cy} A${r},${r} 0 1 1 ${cx - r},${cy} Z`
      : `M${cx},${cy} L${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1} Z`;
    a0 = a1;
    return { path, color: CHART_PALETTE[i % CHART_PALETTE.length], frac, label: d.label, value: d.value, lx: cx + rl * Math.cos(mid), ly: cy + rl * Math.sin(mid) };
  });
  return (
    <div style={{ display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
      <svg viewBox="0 0 190 190" width="190" height="190" style={{ flex: "0 0 auto" }}>
        {slices.map((s, i) => <path key={i} d={s.path} fill={s.color} stroke="#fff" strokeWidth="1.5" />)}
        {donut && <circle cx={cx} cy={cy} r="48" fill="var(--paper,#FBF9F5)" />}
        {slices.filter((s) => s.frac >= 0.08).map((s, i) => (
          <text key={i} x={s.lx} y={s.ly + 4} textAnchor="middle" fontSize="12" fontWeight="700" fill="#fff" style={{ paintOrder: "stroke" }} stroke="rgba(0,0,0,0.18)" strokeWidth="0.5">{Math.round(s.frac * 100)}%</text>
        ))}
      </svg>
      <div style={{ flex: "1 1 150px", display: "flex", flexDirection: "column", gap: 6 }}>
        {slices.map((s, i) => (
          <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
            <span style={{ width: 11, height: 11, borderRadius: 3, background: s.color, flex: "0 0 auto" }} />
            <span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={s.label}>{s.label}</span>
            <strong style={{ whiteSpace: "nowrap" }}>{formatChartValue(s.value)} · {Math.round(s.frac * 100)}%</strong>
          </div>
        ))}
      </div>
    </div>
  );
}

function ChatChart({ chart }) {
  const data = (chart.data || []).slice(0, 24);
  const max = Math.max(...data.map((item) => Math.abs(item.value)), 1);
  const type = chart.type;
  return (
    <div className="kb-chat-chart">
      <div className="kb-chart-head">
        <div>
          <strong>{chart.title || "Gráfico"}</strong>
          {(chart.xLabel || chart.yLabel) && <em>{[chart.xLabel, chart.yLabel].filter(Boolean).join(" · ")}</em>}
        </div>
        <Icon name="chart" size={18} />
      </div>
      {type === "kpi" ? (
        <div className="kb-chart-bars" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(120px,1fr))", gap: 8 }}>
          {data.map((item, index) => (
            <div key={index} style={{ padding: 10, border: "1px solid #E4DED5", borderRadius: 8, background: "#fff" }}>
              <span className="kb-chart-label" title={item.label}>{item.label}</span>
              <strong className="kb-chart-value" style={{ display: "block", marginTop: 4 }}>{formatChartValue(item.value)}</strong>
            </div>
          ))}
        </div>
      ) : type === "table" ? (
        <div className="kb-md-table-wrap">
          <table className="kb-md-table"><thead><tr><th>Item</th><th>Valor</th></tr></thead><tbody>
            {data.map((item, index) => <tr key={index}><td>{item.label}</td><td>{formatChartValue(item.value)}</td></tr>)}
          </tbody></table>
        </div>
      ) : type === "line" || type === "area" ? (
        <div className="kb-chart-bars"><ChartLineSVG data={data} area={type === "area"} /></div>
      ) : type === "pie" || type === "donut" ? (
        <div className="kb-chart-bars"><ChartPieSVG data={data} donut={type === "donut"} /></div>
      ) : (
        (() => {
          // Barras de categorias: ordena MAIOR->menor para leitura. Series temporais
          // (datas/meses) preservam a ordem cronologica.
          const barData = looksTemporal(data) ? data : [...data].sort((a, b) => Math.abs(b.value) - Math.abs(a.value));
          return (
            <div className="kb-chart-bars">
              {barData.map((item, index) => {
                const pct = Math.max(4, Math.round((Math.abs(item.value) / max) * 100));
                return (
                  <div className="kb-chart-row" key={index}>
                    <span className="kb-chart-label" title={item.label}>{item.label}</span>
                    <span className="kb-chart-track"><span className="kb-chart-bar" style={{ width: pct + "%" }} /></span>
                    <strong className="kb-chart-value">{formatChartValue(item.value)}</strong>
                  </div>
                );
              })}
            </div>
          );
        })()
      )}
    </div>
  );
}

// ---- Dashboards no chat (KPIs, graficos, tabelas, texto, botoes) ----
const DASH_WIDTH = { quarter: "1 1 160px", third: "1 1 220px", half: "1 1 300px", full: "1 1 100%" };

function dashChartToChatChart(w) {
  const series = Array.isArray(w.series) ? w.series : [];
  const first = series[0] || { points: [] };
  const points = (first.points || []).map((p) => ({ label: p.label, value: p.value }));
  // ChatChart agora desenha bar/line/area/pie/donut; scatter/heatmap caem em bar.
  const map = { scatter: "bar", heatmap: "bar" };
  const type = map[w.chartType] || (["bar", "line", "area", "pie", "donut"].includes(w.chartType) ? w.chartType : "bar");
  return { type, title: w.title || "Gráfico", xLabel: w.xLabel, yLabel: w.yLabel, data: points };
}

function DashboardWidget({ w, onConnect, onOpenFlow }) {
  const flex = DASH_WIDTH[w.width] || DASH_WIDTH.full;
  const wrap = (child) => <div className="kb-dash-cell" style={{ flex, minWidth: 0 }}>{child}</div>;
  if (w.type === "kpi") {
    const up = typeof w.delta === "number" ? w.delta >= 0 : null;
    return wrap(
      <div className="kb-dash-kpi" style={{ padding: 14, border: "1px solid var(--line,#E4DED5)", borderRadius: 12, background: "#fff", height: "100%" }}>
        <span className="kb-chart-label" style={{ display: "block" }}>{w.title}</span>
        <strong style={{ display: "block", fontSize: 24, marginTop: 6 }}>{w.value}{w.unit ? <em style={{ fontSize: 14, fontStyle: "normal", opacity: 0.6 }}> {w.unit}</em> : null}</strong>
        {typeof w.delta === "number" && (
          <span style={{ display: "inline-block", marginTop: 6, fontSize: 12.5, fontWeight: 600, color: up ? "#0B8457" : "#C0392B" }}>
            {up ? "▲" : "▼"} {Math.abs(w.delta)}{w.deltaLabel ? ` ${w.deltaLabel}` : "%"}
          </span>
        )}
        {w.hint && <span style={{ display: "block", marginTop: 4, fontSize: 12, opacity: 0.6 }}>{w.hint}</span>}
      </div>
    );
  }
  if (w.type === "chart") return wrap(<ChatChart chart={dashChartToChatChart(w)} />);
  if (w.type === "table") {
    return wrap(
      <div className="kb-md-table-wrap">
        {w.title && <strong style={{ display: "block", marginBottom: 6 }}>{w.title}</strong>}
        <table className="kb-md-table">
          <thead><tr>{(w.headers || []).map((h, i) => <th key={i}>{h}</th>)}</tr></thead>
          <tbody>{(w.rows || []).map((row, i) => <tr key={i}>{(row || []).map((c, j) => <td key={j}>{String(c)}</td>)}</tr>)}</tbody>
        </table>
      </div>
    );
  }
  if (w.type === "text") {
    return wrap(
      <div className="kb-dash-text">
        {w.title && <strong style={{ display: "block", marginBottom: 4 }}>{w.title}</strong>}
        {w.body && <RichMessageText text={w.body} />}
        {Array.isArray(w.bullets) && w.bullets.map((b, i) => <p key={i} className="kb-md-bullet">{renderInlineMarkdown(b)}</p>)}
      </div>
    );
  }
  if (w.type === "buttons") {
    const run = (a) => {
      if (!a) return;
      if (a.kind === "connect_connector" && onConnect) onConnect(a.connectorId);
      else if (a.kind === "open_workflow" && onOpenFlow) onOpenFlow(a.workflowId);
      else if (a.kind === "open_link" && a.url) window.open(a.url, "_blank", "noopener");
      else if (a.kind === "send_message" && window.__kbSend) window.__kbSend(a.message);
    };
    return wrap(
      <div className="kb-dash-buttons" style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        {(w.buttons || []).map((b, i) => (
          <button key={i} className={"kb-btn tiny " + (b.style === "ghost" ? "ghost" : b.style === "secondary" ? "ghost" : "primary")} onClick={() => run(b.action)}>{b.label}</button>
        ))}
      </div>
    );
  }
  if (w.type === "divider") return wrap(<div className="kb-dash-divider" style={{ borderTop: "1px solid var(--line,#E4DED5)", margin: "4px 0", paddingTop: w.label ? 8 : 0, fontSize: 12.5, opacity: 0.7 }}>{w.label || ""}</div>);
  return null;
}

function DashboardBlock({ dashboard, onConnect, onOpenFlow }) {
  const widgets = (dashboard && Array.isArray(dashboard.widgets)) ? dashboard.widgets : [];
  if (!widgets.length) return null;
  return (
    <div className="kb-dash" style={{ border: "1px solid var(--line,#E4DED5)", borderRadius: 14, padding: 14, marginTop: 10, background: "var(--paper,#FBF9F5)" }}>
      <div className="kb-dash-head" style={{ marginBottom: 10 }}>
        <strong style={{ fontSize: 16 }}>{dashboard.title || "Dashboard"}</strong>
        {dashboard.subtitle && <span style={{ display: "block", fontSize: 13, opacity: 0.65, marginTop: 2 }}>{dashboard.subtitle}</span>}
      </div>
      <div className="kb-dash-grid" style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
        {widgets.map((w, i) => <DashboardWidget key={w.id || i} w={w} onConnect={onConnect} onOpenFlow={onOpenFlow} />)}
      </div>
    </div>
  );
}

function MessageDashboards({ dashboards, onConnect, onOpenFlow }) {
  const all = Array.isArray(dashboards) ? dashboards : [];
  if (!all.length) return null;
  return <>{all.map((dash, i) => <DashboardBlock key={"dash-" + i} dashboard={dash} onConnect={onConnect} onOpenFlow={onOpenFlow} />)}</>;
}

function RichMessageText({ text }) {
  const parsed = splitMessageCharts(text);
  const lines = String(parsed.clean || "").split("\n");
  const out = [];
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (!line.trim()) { out.push(<div key={i} className="kb-md-gap" />); continue; }
    if (/^\s*\|.+\|\s*$/.test(line)) {
      const table = [];
      while (i < lines.length && /^\s*\|.+\|\s*$/.test(lines[i])) table.push(lines[i++]);
      i--;
      out.push(<MarkdownTable key={i} lines={table} />);
      continue;
    }
    const heading = line.match(/^(#{2,3})\s+(.+)$/);
    if (heading) {
      const Tag = heading[1].length === 2 ? "h3" : "h4";
      out.push(<Tag key={i} className="kb-md-heading">{renderInlineMarkdown(heading[2])}</Tag>);
      continue;
    }
    const bullet = line.match(/^\s*[-•]\s+(.+)$/);
    if (bullet) { out.push(<p key={i} className="kb-md-bullet">{renderInlineMarkdown(bullet[1])}</p>); continue; }
    const numbered = line.match(/^\s*(\d+)\.\s+(.+)$/);
    if (numbered) { out.push(<p key={i} className="kb-md-number"><span>{numbered[1]}</span>{renderInlineMarkdown(numbered[2])}</p>); continue; }
    out.push(<p key={i}>{renderInlineMarkdown(line)}</p>);
  }
  return (
    <>
      {out}
    </>
  );
}

function MessageCharts({ text, charts }) {
  const parsed = splitMessageCharts(text);
  const explicit = Array.isArray(charts) ? charts : [];
  const all = [...explicit, ...parsed.charts];
  if (!all.length) return null;
  return <>{all.map((chart, i) => <ChatChart key={"chart-" + i} chart={chart} />)}</>;
}

function MessageBubble({ msg, connectedIds, onConnect, onOpenFlow }) {
  if (msg.role === "user") {
    const atts = msg.attachments || [];
    return (
      <div className="kb-msg user">
        {atts.length > 0 && (
          <div className="kb-msg-atts">
            {atts.map((a, i) => (
              a.kind === "image" && a.url ? (
                <a key={i} className="kb-msg-att img" href={a.url} target="_blank" rel="noreferrer" title={a.name}>
                  <img src={a.url} alt={a.name} />
                </a>
              ) : a.kind === "audio" && a.url ? (
                <div key={i} className="kb-msg-att audio">
                  <span className="kb-msg-att-name"><Icon name="audioFile" size={14} /> {a.name}</span>
                  <audio controls src={a.url} />
                </div>
              ) : (
                <div key={i} className="kb-msg-att file" title={a.name}>
                  <Icon name="fileDoc" size={16} />
                  <span className="kb-msg-att-name">{a.name}</span>
                </div>
              )
            ))}
          </div>
        )}
        {msg.text && <div className="kb-bubble user">{msg.text}</div>}
      </div>
    );
  }
  return (
    <div className="kb-msg ai">
      <span className="kb-ai-ava"><OliveMark size={30} /></span>
      <div className="kb-ai-body">
        {msg.thinking ? (
          <div className="kb-thinking">
            <OliveLoader size={26} speed={1.4} />
            <span className="kb-thinking-txt">{msg.thinkingText || "Pensando…"}</span>
          </div>
        ) : (
          <div className="kb-bubble ai">
            <RichMessageText text={msg.text} />
            <MessageCharts text={msg.text} charts={msg.charts} />
            <MessageDashboards dashboards={msg.dashboards} onConnect={onConnect} onOpenFlow={onOpenFlow} />
            {msg.cta && (
              <button className="kb-build-cta" onClick={msg.cta.onClick}>
                <span className="kb-build-cta-ic"><Icon name="flows" size={18} /></span>
                <span className="kb-build-cta-txt">
                  <strong>{msg.cta.title}</strong>
                  <em>{msg.cta.sub}</em>
                </span>
                <Icon name="chevRight" size={18} />
              </button>
            )}
            <MessageConnections msg={msg} connectedIds={connectedIds} onConnect={onConnect} onOpenFlow={onOpenFlow} />
          </div>
        )}
        {msg.via && <span className="kb-via">via {msg.via}</span>}
      </div>
    </div>
  );
}

Object.assign(window, {
  Avatar, IconRail, ChatListPanel, ProviderDropdown, ConnectorDots,
  StarterChip, SuggestionCard, MessageBubble, MessageConnections, RichMessageText,
});

/* ===== api.jsx ===== */
// api.jsx — cliente de sessao + auth para o protótipo (sem bundler).
// Fala com os endpoints /api/auth/* e guarda a sessao no localStorage.
// Exporta para window: kbApi, kbAuth.

const KB_SESSION_KEY = "kb_session";

const kbApi = {
  getSession() {
    try {
      const raw = window.localStorage.getItem(KB_SESSION_KEY);
      return raw ? JSON.parse(raw) : null;
    } catch (e) {
      return null;
    }
  },
  setSession(session, { persist = true } = {}) {
    if (!session) return;
    try {
      // "Manter conectado" desligado: nao persiste o refresh_token entre sessoes.
      const toStore = persist ? session : { ...session, refreshToken: null };
      window.localStorage.setItem(KB_SESSION_KEY, JSON.stringify(toStore));
    } catch (e) {
      /* storage indisponivel — segue em memoria */
    }
  },
  clearSession() {
    try { window.localStorage.removeItem(KB_SESSION_KEY); } catch (e) {}
  },
  token() {
    const s = kbApi.getSession();
    return s && s.accessToken ? s.accessToken : null;
  },

  // fetch autenticado com auto-refresh unico em 401.
  async authFetch(path, opts = {}, _retried = false) {
    const session = kbApi.getSession();
    const headers = Object.assign({}, opts.headers || {});
    if (session && session.accessToken) headers["Authorization"] = "Bearer " + session.accessToken;
    if (opts.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";

    const res = await fetch(path, Object.assign({}, opts, { headers }));

    if (res.status === 401 && !_retried && session && session.refreshToken) {
      const refreshed = await kbApi.refresh(session.refreshToken);
      if (refreshed) return kbApi.authFetch(path, opts, true);
    }
    return res;
  },

  async refresh(refreshToken) {
    try {
      const res = await fetch("/api/auth/refresh", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ refresh_token: refreshToken })
      });
      if (!res.ok) { kbApi.clearSession(); return null; }
      const data = await res.json();
      if (data && data.session) { kbApi.setSession(data.session); return data.session; }
      return null;
    } catch (e) {
      return null;
    }
  }
};

async function postJson(path, body) {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body || {})
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error((data && data.error) || "Falha na requisicao.");
  return data;
}

const kbAuth = {
  // email + senha (login | signup)
  async password({ mode, email, password, name }) {
    return postJson("/api/auth/password", { mode, email, password, name });
  },
  // codigo por e-mail (OTP)
  async otpSend({ email, mode, name }) {
    return postJson("/api/auth/otp/send", { email, mode, name });
  },
  async otpVerify({ email, token }) {
    return postJson("/api/auth/otp/verify", { email, token });
  },
  // recuperacao de senha
  async recover(email) {
    return postJson("/api/auth/recover", { email });
  },
  // Google OAuth — retorna { url } para redirecionar a janela de topo
  async oauth(provider = "google") {
    return postJson("/api/auth/oauth", { provider });
  },
  // valida a sessao atual; retorna { user } ou lanca
  async session() {
    const res = await kbApi.authFetch("/api/auth/session", { method: "GET" });
    if (!res.ok) throw new Error("unauthenticated");
    return res.json();
  },
  async signout() {
    try { await kbApi.authFetch("/api/auth/signout", { method: "POST" }); } catch (e) {}
    kbApi.clearSession();
  }
};

const kbConnectors = {
  // lista catalogo + status de conexao do usuario
  async list() {
    const res = await kbApi.authFetch("/api/connectors", { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar conectores.");
    return res.json();
  },
  async connect(connectorId, credentials) {
    const res = await kbApi.authFetch("/api/connectors/connect", {
      method: "POST",
      body: JSON.stringify({ connectorId, credentials: credentials || {} })
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error((data && data.error) || "Falha ao conectar.");
    return data;
  },
  async test(connectorId, credentials) {
    const res = await kbApi.authFetch("/api/connectors/test", {
      method: "POST",
      body: JSON.stringify({ connectorId, credentials })
    });
    return res.json().catch(() => ({ ok: false }));
  },
  async disconnect(connectorId) {
    const res = await kbApi.authFetch("/api/connectors/disconnect", {
      method: "POST",
      body: JSON.stringify({ connectorId })
    });
    return res.ok;
  },
  async oauthStart(connectorId, returnTo) {
    const res = await kbApi.authFetch("/api/connectors/oauth/start", {
      method: "POST",
      body: JSON.stringify({ connectorId, returnTo })
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error((data && data.error) || "OAuth indisponivel.");
    return data; // { url }
  },
  async health(connectorId) {
    const res = await kbApi.authFetch("/api/connectors/health", {
      method: "POST",
      body: JSON.stringify(connectorId ? { connectorId } : {})
    });
    return res.json().catch(() => ({ ok: false }));
  }
};

// reavalia um workflow apos conexoes (auto-continuar)
const kbChat = {
  async continueWorkflow(workflow) {
    const res = await kbApi.authFetch("/api/chat/continue", {
      method: "POST",
      body: JSON.stringify(workflow && workflow.id ? { workflowId: workflow.id, workflow } : { workflow })
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error((data && data.error) || "Falha ao continuar.");
    return data;
  }
};

const kbApprovals = {
  async confirm(approvalId) {
    const res = await kbApi.authFetch("/api/chat/approvals/" + encodeURIComponent(approvalId) + "/confirm", {
      method: "POST",
      body: "{}"
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error(data.error || "Falha ao confirmar acao.");
    return data;
  }
};

const kbAgents = {
  async list() {
    const res = await kbApi.authFetch("/api/agents", { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar agents.");
    return res.json();
  },
  async create(agent) {
    const res = await kbApi.authFetch("/api/agents", { method: "POST", body: JSON.stringify(agent) });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error(data.error || "Falha ao criar agent.");
    return data;
  },
  async update(id, patch) {
    const res = await kbApi.authFetch("/api/agents/" + encodeURIComponent(id), { method: "PATCH", body: JSON.stringify(patch) });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error(data.error || "Falha ao editar agent.");
    return data;
  },
  async remove(id) {
    const res = await kbApi.authFetch("/api/agents/" + encodeURIComponent(id), { method: "DELETE" });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error(data.error || "Falha ao excluir agent.");
    return data;
  }
};

const kbWorkflows = {
  // executa o workflow de verdade (motor real + credenciais dos conectores conectados)
  async run(id, body) {
    const res = await kbApi.authFetch("/api/workflows/" + encodeURIComponent(id) + "/run", {
      method: "POST",
      body: JSON.stringify(body || {})
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) throw new Error((data && data.error) || "Falha ao executar o workflow.");
    return data; // { run }
  },
  // publica o workflow (versao imutavel) — passa a ser elegivel ao agendador (cron)
  async publish(id) {
    const res = await kbApi.authFetch("/api/workflows/" + encodeURIComponent(id) + "/publish", {
      method: "POST",
      body: "{}"
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      const error = new Error((data && data.error) || "Falha ao publicar o workflow.");
      error.data = data;
      throw error;
    }
    return data; // { ok, version, versionId, webhookUrl, issues }
  },
  async remove(id, force) {
    const qs = force ? "?force=true" : "";
    const res = await kbApi.authFetch("/api/workflows/" + encodeURIComponent(id) + qs, { method: "DELETE" });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      const error = new Error(data.error || "Falha ao excluir flow.");
      error.data = data;
      throw error;
    }
    return data;
  }
};

const kbBilling = {
  async summary() {
    const res = await kbApi.authFetch("/api/billing", { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar billing.");
    return res.json();
  },
  // Assinatura: inicia checkout do plano e devolve a URL do Stripe (ou modo demo).
  async checkout(planId) {
    const res = await kbApi.authFetch("/api/billing/checkout", { method: "POST", body: JSON.stringify({ planId }) });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || "Falha ao iniciar o checkout.");
    return data; // { mode, url }
  },
  // Portal do cliente (gerir assinatura/cartao/cancelamento).
  async portal() {
    const res = await kbApi.authFetch("/api/billing/portal", { method: "POST", body: "{}" });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || "Falha ao abrir o portal.");
    return data;
  },
  // Creditos de IA: saldo atual + pacotes disponiveis.
  async credits() {
    const res = await kbApi.authFetch("/api/billing/credits", { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar creditos.");
    return res.json(); // { balance, packs }
  },
  // Recarga de creditos: inicia o checkout do pacote e devolve a URL do Stripe.
  async buyCredits(packId) {
    const res = await kbApi.authFetch("/api/billing/credits", { method: "POST", body: JSON.stringify({ packId }) });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || "Falha ao comprar creditos.");
    return data; // { mode, url }
  }
};

const kbMetrics = {
  async org(days = 30) {
    const res = await kbApi.authFetch("/api/org/metrics?days=" + days, { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar metricas.");
    return res.json();
  }
};

const kbRuns = {
  async detail(runId) {
    const res = await kbApi.authFetch("/api/runs/" + encodeURIComponent(runId), { method: "GET" });
    if (!res.ok) throw new Error("Falha ao carregar execucao.");
    return res.json();
  },
  async cancel(runId) {
    const res = await kbApi.authFetch("/api/workflows/runs/" + encodeURIComponent(runId) + "/cancel", { method: "POST", body: "{}" });
    return res.ok;
  }
};

Object.assign(window, { kbApi, kbAuth, kbConnectors, kbChat, kbApprovals, kbAgents, kbWorkflows, kbBilling, kbMetrics, kbRuns });

/* ===== auth.jsx ===== */
// auth.jsx — Kobar Flows login / signup screen
const { useState: useStateAu, useRef: useRefAu, useEffect: useEffectAu } = React;

function GoogleG({ size = 18 }) {
  return (
    <svg viewBox="0 0 48 48" width={size} height={size} aria-hidden="true">
      <path fill="#EA4335" d="M24 9.5c3.5 0 6.6 1.2 9.1 3.6l6.8-6.8C35.6 2.4 30.1 0 24 0 14.6 0 6.4 5.4 2.5 13.3l7.9 6.1C12.3 13.3 17.6 9.5 24 9.5z"/>
      <path fill="#4285F4" d="M46.1 24.6c0-1.6-.1-3.1-.4-4.6H24v9.1h12.4c-.5 2.9-2.1 5.3-4.6 7l7.1 5.5c4.2-3.9 6.2-9.6 6.2-17z"/>
      <path fill="#FBBC05" d="M10.4 28.4c-.5-1.4-.8-2.9-.8-4.4s.3-3 .8-4.4l-7.9-6.1C.9 16.5 0 20.1 0 24s.9 7.5 2.5 10.6l7.9-6.2z"/>
      <path fill="#34A853" d="M24 48c6.1 0 11.3-2 15-5.5l-7.1-5.5c-2 1.3-4.6 2.1-7.9 2.1-6.4 0-11.7-3.8-13.6-9.4l-7.9 6.2C6.4 42.6 14.6 48 24 48z"/>
    </svg>
  );
}

function Field({ icon, type = "text", value, onChange, placeholder, label, autoFocus, rightSlot, disabled }) {
  return (
    <label className="kb-field">
      <span className="kb-field-label">{label}</span>
      <span className="kb-field-box">
        <Icon name={icon} size={17} className="kb-field-ic" />
        <input type={type} value={value} autoFocus={autoFocus} disabled={disabled}
          onChange={(e) => onChange(e.target.value)} placeholder={placeholder} />
        {rightSlot}
      </span>
    </label>
  );
}

function AuthScreen({ onAuth }) {
  const [mode, setMode] = useStateAu("login"); // login | signup
  const [flow, setFlow] = useStateAu("form");  // form | code
  const [show, setShow] = useStateAu(false);
  const [name, setName] = useStateAu("");
  const [email, setEmail] = useStateAu("");
  const [pass, setPass] = useStateAu("");
  const [code, setCode] = useStateAu(["", "", "", "", "", ""]);
  const [remember, setRemember] = useStateAu(true);
  const [cooldown, setCooldown] = useStateAu(0);
  const [busy, setBusy] = useStateAu("");     // "" | "password" | "google" | "otp" | "verify" | "recover"
  const [error, setError] = useStateAu("");
  const [info, setInfo] = useStateAu("");
  const codeRefs = useRefAu([]);

  const clearMsgs = () => { setError(""); setInfo(""); };

  useEffectAu(() => {
    if (cooldown <= 0) return;
    const id = setInterval(() => setCooldown((value) => Math.max(0, value - 1)), 1000);
    return () => clearInterval(id);
  }, [cooldown]);

  // sucesso de auth: persiste sessao e sobe para o App
  const handleSession = (data) => {
    if (data && data.session) {
      kbApi.setSession(data.session, { persist: remember });
      onAuth(data.session);
      return true;
    }
    return false;
  };

  const submit = async (e) => {
    e && e.preventDefault();
    if (busy || cooldown > 0) return;
    clearMsgs();
    if (!email || !pass) { setError("Informe e-mail e senha."); return; }
    setBusy("password");
    try {
      const data = await kbAuth.password({ mode, email, password: pass, name });
      if (!handleSession(data)) {
        // signup sem sessao = precisa confirmar e-mail
        setInfo("Conta criada! Confirme seu e-mail para entrar.");
      }
    } catch (err) {
      setError(err.message || "Falha ao autenticar.");
    } finally {
      setBusy("");
    }
  };

  const google = async () => {
    if (busy) return;
    clearMsgs();
    setBusy("google");
    try {
      const data = await kbAuth.oauth("google");
      const top = window.top || window;
      top.location.href = data.url;
    } catch (err) {
      setError(err.message || "Nao foi possivel iniciar o login com Google.");
      setBusy("");
    }
  };

  const sendCode = async (e) => {
    e && e.preventDefault();
    if (busy) return;
    clearMsgs();
    if (!email) { setError("Informe seu e-mail para receber o codigo."); return; }
    setBusy("otp");
    try {
      await kbAuth.otpSend({ email, mode, name });
      setFlow("code");
      setCooldown(45);
      setInfo("Enviamos um codigo de 6 digitos para o seu e-mail.");
    } catch (err) {
      setError(err.message || "Nao foi possivel enviar o codigo.");
    } finally {
      setBusy("");
    }
  };

  const verifyCode = async (e) => {
    e && e.preventDefault();
    if (busy) return;
    clearMsgs();
    const token = code.join("");
    if (token.length < 6) { setError("Digite os 6 digitos do codigo."); return; }
    setBusy("verify");
    try {
      const data = await kbAuth.otpVerify({ email, token });
      if (!handleSession(data)) setError("Codigo invalido ou expirado.");
    } catch (err) {
      setError(err.message || "Codigo invalido ou expirado.");
    } finally {
      setBusy("");
    }
  };

  const recover = async () => {
    if (busy) return;
    clearMsgs();
    if (!email) { setError("Informe seu e-mail para recuperar a senha."); return; }
    setBusy("recover");
    try {
      await kbAuth.recover(email);
      setInfo("Se existir uma conta com esse e-mail, enviamos um link de recuperacao.");
    } catch (err) {
      setError(err.message || "Nao foi possivel enviar o link.");
    } finally {
      setBusy("");
    }
  };

  const setDigit = (i, v) => {
    v = v.replace(/\D/g, "").slice(-1);
    setCode((c) => { const n = [...c]; n[i] = v; return n; });
    if (v && i < 5) codeRefs.current[i + 1]?.focus();
  };

  const pasteCode = (e) => {
    const raw = (e.clipboardData && e.clipboardData.getData("text")) || "";
    const digits = raw.replace(/\D/g, "").slice(0, 6);
    if (digits.length < 2) return;
    e.preventDefault();
    setCode(Array.from({ length: 6 }, (_, i) => digits[i] || ""));
    setTimeout(() => codeRefs.current[Math.min(digits.length, 6) - 1]?.focus(), 0);
  };

  const anyBusy = !!busy;

  return (
    <div className="kb-auth">
      {/* Left brand panel */}
      <div className="kb-auth-brand">
        <div className="kb-auth-brand-texture" />
        <div className="kb-auth-brand-top">
          <KobarWordmark size={22} mono />
        </div>
        <div className="kb-auth-brand-mid">
          <OliveMark size={92} />
          <h2 className="kb-auth-tagline">Automação conversacional para o seu negócio.</h2>
          <p className="kb-auth-lede">Descreva o que precisa em linguagem natural. A Kobar identifica, conecta suas ferramentas e constrói o workflow para você.</p>
          <ul className="kb-auth-points">
            <li><Icon name="check" size={16} /> Mais de 200 conectores prontos</li>
            <li><Icon name="check" size={16} /> Modelos de IA locais ou na nuvem</li>
            <li><Icon name="check" size={16} /> Do chat ao fluxo publicado em minutos</li>
          </ul>
        </div>
        <div className="kb-auth-brand-foot">
          <span>© 2026 Kobar Labs</span>
          <span className="kb-auth-brand-dot" />
          <span>Privacidade</span>
          <span className="kb-auth-brand-dot" />
          <span>Termos</span>
        </div>
      </div>

      {/* Right form panel */}
      <div className="kb-auth-form">
        <div className="kb-auth-card">
          <div className="kb-auth-head">
            <h1 className="kb-auth-title">{mode === "login" ? "Acesse sua conta" : "Crie sua conta"}</h1>
            <p className="kb-auth-sub">
              {mode === "login" ? "Bem-vindo de volta ao Kobar Flows." : "Comece a automatizar em poucos minutos."}
            </p>
          </div>

          {(error || info) && (
            <div role="status" style={{
              display: "flex", alignItems: "center", gap: 8,
              padding: "10px 12px", borderRadius: 10, marginBottom: 14,
              fontSize: 13.5, lineHeight: 1.35,
              border: "1px solid " + (error ? "#E7C3B8" : "#CFE0CB"),
              background: error ? "#FBEFEB" : "#EFF5ED",
              color: error ? "#9B3D27" : "#3C5A39"
            }}>
              <Icon name={error ? "shield" : "check"} size={15} />
              <span>{error || info}</span>
            </div>
          )}

          {flow === "form" ? (
            <>
              <button className="kb-auth-google" onClick={google} disabled={anyBusy}>
                <GoogleG size={18} /> {busy === "google" ? "Conectando…" : "Continuar com o Google"}
              </button>

              <div className="kb-auth-divider"><span>ou continue com e-mail</span></div>

              <form onSubmit={submit} className="kb-auth-fields">
                {mode === "signup" && (
                  <Field icon="user" label="Nome completo" value={name} onChange={setName} placeholder="Nome e sobrenome" autoFocus disabled={anyBusy} />
                )}
                <Field icon="mail" type="email" label="E-mail" value={email} onChange={setEmail}
                  placeholder="voce@empresa.com" autoFocus={mode === "login"} disabled={anyBusy} />
                <button type="button" className="kb-auth-altbtn" onClick={sendCode} disabled={anyBusy || !email.trim() || cooldown > 0}>
                  <Icon name="mail" size={17} /> {busy === "otp" ? "Enviando…" : cooldown > 0 ? `Reenviar em ${cooldown}s` : "Receber código por e-mail"}
                </button>
                <Field icon="lock" type={show ? "text" : "password"} label="Senha" value={pass} onChange={setPass}
                  placeholder="••••••••" disabled={anyBusy}
                  rightSlot={<button type="button" className="kb-field-eye" onClick={() => setShow((s) => !s)} tabIndex={-1}><Icon name="eye" size={16} /></button>} />
                {mode === "login" && (
                  <div className="kb-auth-row">
                    <label className="kb-checkbox"><input type="checkbox" checked={remember} onChange={(e) => setRemember(e.target.checked)} /> <span>Manter conectado</span></label>
                    <button type="button" className="kb-auth-link" onClick={recover} disabled={anyBusy}>{busy === "recover" ? "Enviando…" : "Esqueci a senha"}</button>
                  </div>
                )}
                <button type="submit" className="kb-auth-submit" disabled={anyBusy}>
                  {busy === "password" ? "Aguarde…" : (mode === "login" ? "Entrar" : "Criar conta")} <Icon name="arrowRight" size={17} />
                </button>
              </form>
            </>
          ) : (
            <form onSubmit={verifyCode} className="kb-auth-fields">
              <div className="kb-auth-codeinfo">
                <span className="kb-auth-codeic"><Icon name="mail" size={20} /></span>
                Enviamos um código de 6 dígitos para<br /><strong>{email || "voce@empresa.com"}</strong>
                <em style={{ display: "block", marginTop: 6, fontSize: 12, color: "#7A8177" }}>O código expira em alguns minutos. Verifique spam ou lixo eletrônico.</em>
              </div>
              <div className="kb-codegrid" onPaste={pasteCode}>
                {code.map((d, i) => (
                  <input key={i} ref={(el) => (codeRefs.current[i] = el)} className="kb-codecell"
                    inputMode="numeric" autoComplete={i === 0 ? "one-time-code" : "off"} maxLength={1} value={d} autoFocus={i === 0} disabled={anyBusy}
                    onChange={(e) => setDigit(i, e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Backspace" && !d && i > 0) codeRefs.current[i - 1]?.focus(); }} />
                ))}
              </div>
              <button type="submit" className="kb-auth-submit" disabled={anyBusy}>{busy === "verify" ? "Verificando…" : "Confirmar código"} <Icon name="arrowRight" size={17} /></button>
              <button type="button" className="kb-auth-altbtn" onClick={sendCode} disabled={anyBusy || cooldown > 0}>
                <Icon name="mail" size={17} /> {cooldown > 0 ? `Reenviar em ${cooldown}s` : "Reenviar código"}
              </button>
              <button type="button" className="kb-auth-back2" onClick={() => { setFlow("form"); setCode(["", "", "", "", "", ""]); clearMsgs(); }}><Icon name="chevLeft" size={15} /> Trocar e-mail ou método</button>
            </form>
          )}

          <p className="kb-auth-switch">
            {mode === "login" ? "Ainda não tem uma conta? " : "Já tem uma conta? "}
            <button onClick={() => { setMode(mode === "login" ? "signup" : "login"); setFlow("form"); clearMsgs(); }}>
              {mode === "login" ? "Criar conta" : "Entrar"}
            </button>
          </p>
        </div>
        <div className="kb-auth-secure"><Icon name="shield" size={14} /> Conexão segura · seus dados ficam no seu ambiente</div>
      </div>
    </div>
  );
}

Object.assign(window, { AuthScreen });

/* ===== home.jsx ===== */
// home.jsx — Composer + ChatHome (variants A & B) + conversation view
const { useState: useStateH, useRef: useRefH, useEffect: useEffectH, useMemo: useMemoH } = React;

// Heurística de visão: cloud (Claude/Gemini/OpenAI moderno) interpreta imagens;
// no Ollama depende do modelo (llava, *-vision, *vl, gemma3, minicpm-v, etc.).
function modelSupportsVision(provider, model) {
  const m = String(model || "").toLowerCase();
  if (provider === "anthropic" || provider === "gemini") return true;
  if (provider === "openai") return !/gpt-3\.5|davinci|babbage/.test(m);
  if (provider === "deepseek") return false;
  // Ollama e demais: casa por padrões conhecidos de modelos multimodais.
  return /llava|vision|[-_]?vl\b|qwen2.*vl|gemma3|minicpm-v|moondream|bakllava|llama3\.2-vision|pixtral|cogvlm|internvl|granite.*vision/.test(m);
}

function ComposerConnectors({ up, connectedIds }) {
  const ids = Array.from(connectedIds || []).map((id) => frontConnectorId ? frontConnectorId(id) : id);
  const [open, setOpen] = useStateH(false);
  const [on, setOn] = useStateH({});
  const ref = useRefH(null);
  useEffectH(() => {
    setOn((current) => {
      const next = {};
      ids.forEach((id) => { next[id] = current[id] !== undefined ? current[id] : true; });
      return next;
    });
  }, [ids.join("|")]);
  useEffectH(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [open]);
  const count = ids.filter((id) => on[id]).length;
  return (
    <div className="kb-cconn" ref={ref}>
      <button className={"kb-tool" + (count ? " has-active" : "") + (open ? " active" : "")}
        data-tip="Conectores do sistema" onClick={() => setOpen((o) => !o)}>
        <Icon name="connect" size={17} />
        {count > 0 && <span className="kb-tool-badge">{count}</span>}
      </button>
      {open && (
        <div className={"kb-connpop" + (up ? " up" : "")}>
          <div className="kb-connpop-head"><Icon name="connect" size={15} /> Conectores do sistema<span className="kb-connpop-count">{count}/{ids.length}</span></div>
          <div className="kb-connpop-list">
            {ids.length === 0 && (
              <div className="kb-empty-chats" style={{ padding: 10 }}>Nenhum conector conectado ainda.</div>
            )}
            {ids.map((id) => (
              <div key={id} className="kb-connpop-item">
                <Connector id={id} size={28} />
                <div className="kb-connpop-txt">
                  <span className="kb-connpop-name">{(CONNECTORS[id] && CONNECTORS[id].label) || id}</span>
                  <span className={"kb-connpop-state" + (on[id] ? " on" : "")}>{on[id] ? "ligado" : "desligado"}</span>
                </div>
                <Switch on={on[id]} onClick={() => setOn((s) => ({ ...s, [id]: !s[id] }))} />
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function Composer({ provider, onProvider, onSend, placeholder, dark, autoFocus, voice, up,
                    webSearchOn, onWebSearch, thinkingOn, onThinking, modelChoice, connectedIds }) {
  const [val, setVal] = useStateH("");
  const [recording, setRecording] = useStateH(false);
  const [voiceMsg, setVoiceMsg] = useStateH("");
  const [atts, setAtts] = useStateH([]);
  const [attMenu, setAttMenu] = useStateH(false);
  const ta = useRefH(null);
  const recog = useRefH(null);
  const baseVal = useRefH("");
  const fileRef = useRefH(null);
  const attRef = useRefH(null);
  const grow = () => {
    const el = ta.current; if (!el) return;
    el.style.height = "0px";
    el.style.height = Math.min(el.scrollHeight, 200) + "px";
  };
  useEffectH(() => { const id = requestAnimationFrame(grow); return () => cancelAnimationFrame(id); }, [val]);
  useEffectH(() => () => { try { recog.current && recog.current.stop(); } catch (e) {} }, []);
  useEffectH(() => {
    if (!attMenu) return;
    const h = (e) => { if (attRef.current && !attRef.current.contains(e.target)) setAttMenu(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [attMenu]);
  const submit = () => {
    const t = val.trim();
    if (!t && !atts.length) return;
    const sending = atts;
    setVal(""); setAtts([]);
    onSend(t, sending);
  };

  // ---- Anexos: arquivos, imagens e áudios ----
  const fmtSize = (n) => n < 1024 ? n + " B" : n < 1048576 ? (n / 1024).toFixed(0) + " KB" : (n / 1048576).toFixed(1) + " MB";
  const pickFiles = (accept) => {
    if (fileRef.current) { fileRef.current.accept = accept || ""; fileRef.current.click(); }
    setAttMenu(false);
  };
  const onFiles = (e) => {
    const files = Array.from(e.target.files || []);
    // Limite de leitura inline: arquivos maiores nao sao enviados como conteudo.
    const MAX_READ_BYTES = 8 * 1024 * 1024;
    files.forEach((f) => {
      const kind = f.type.startsWith("image/") ? "image" : f.type.startsWith("audio/") ? "audio" : "file";
      const entry = { id: Math.random().toString(36).slice(2), name: f.name, size: f.size, type: f.type || "", kind, url: null };
      setAtts((a) => [...a, entry]);
      // Le o conteudo de TODOS os anexos (imagem, audio e arquivos de dados como
      // .xlsx/.csv) como data URL, para o servidor usar o conteudo real do anexo
      // como fonte — em vez de puxar dados de um conector conectado.
      if (f.size <= MAX_READ_BYTES) {
        const reader = new FileReader();
        reader.onload = () => setAtts((a) => a.map((x) => x.id === entry.id ? { ...x, url: reader.result } : x));
        reader.readAsDataURL(f);
      }
    });
    e.target.value = "";
  };
  const removeAtt = (id) => setAtts((a) => a.filter((x) => x.id !== id));

  const speechSupported = typeof window !== "undefined" && (window.SpeechRecognition || window.webkitSpeechRecognition);
  const startVoice = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { setVoiceMsg("Voz indisponível neste navegador."); return; }
    const r = new SR();
    r.lang = "pt-BR"; r.continuous = true; r.interimResults = true;
    baseVal.current = val ? val.replace(/\s+$/, "") + " " : "";
    setVoiceMsg("Solicitando microfone...");
    r.onstart = () => { setRecording(true); setVoiceMsg("Ouvindo em pt-BR..."); };
    r.onaudiostart = () => setVoiceMsg("Pode falar.");
    r.onresult = (e) => {
      let txt = "";
      for (let i = e.resultIndex; i < e.results.length; i++) txt += e.results[i][0].transcript;
      setVal((baseVal.current + txt).replace(/^\s+/, ""));
      setVoiceMsg("Transcrevendo...");
    };
    r.onnomatch = () => setVoiceMsg("Não entendi. Tente falar mais perto do microfone.");
    r.onend = () => { setRecording(false); recog.current = null; setVoiceMsg((m) => m === "Transcrevendo..." ? "" : m); };
    r.onerror = (event) => {
      const err = event && event.error;
      const msg = err === "not-allowed" ? "Permissão do microfone negada."
        : err === "no-speech" ? "Não detectei fala."
        : err === "audio-capture" ? "Microfone não encontrado."
        : "Falha ao ouvir. Tente novamente.";
      setVoiceMsg(msg);
      setRecording(false);
      recog.current = null;
    };
    recog.current = r;
    try { r.start(); } catch (e) { setRecording(false); setVoiceMsg("Microfone já está em uso."); }
  };
  const stopVoice = () => { try { recog.current && recog.current.stop(); } catch (e) {} setRecording(false); setVoiceMsg(""); };
  const toggleVoice = () => {
    if (!speechSupported) { setVoiceMsg("Reconhecimento de voz não é suportado neste navegador."); return; }
    if (recording) stopVoice(); else startVoice();
  };

  const activeModel = (modelChoice && modelChoice[provider]) || (PROVIDERS[provider] && PROVIDERS[provider].model) || provider;
  // Aviso de visão: alerta quando há imagem anexada mas o modelo não interpreta imagens.
  const hasImageAtt = atts.some((a) => a.kind === "image");
  const modelHasVision = modelSupportsVision(provider, activeModel);
  const providerTools = (PROVIDERS[provider] && PROVIDERS[provider].tools) || [];
  const catalogEntry = (typeof window !== "undefined" && window.MODEL_CATALOG
    ? window.MODEL_CATALOG.find((c) => c.provider === provider)
    : null);
  const allowedModels = catalogEntry && Array.isArray(catalogEntry.webSearchModels) ? catalogEntry.webSearchModels : null;
  const thinkingModels = catalogEntry && Array.isArray(catalogEntry.thinkingModels) ? catalogEntry.thinkingModels : [];
  const hasSearchTool = providerTools.includes("web_search") || providerTools.includes("google_search");
  const modelAllowed = !allowedModels || allowedModels.length === 0 || allowedModels.includes(activeModel);
  const webSearchAvailable = Boolean(onWebSearch) && hasSearchTool && modelAllowed;
  const thinkingAvailable = Boolean(onThinking) && thinkingModels.includes(activeModel);
  useEffectH(() => {
    if (!thinkingAvailable && thinkingOn && onThinking) onThinking(false);
  }, [thinkingAvailable, thinkingOn, activeModel, provider]);

  return (
    <div className={"kb-composer" + (dark ? " dark" : "") + (recording ? " recording" : "")}>
      {atts.length > 0 && (
        <div className="kb-att-row">
          {atts.map((a) => (
            <div key={a.id} className={"kb-att-chip " + a.kind} title={a.name}>
              {a.kind === "image" && a.url
                ? <span className="kb-att-thumb" style={{ backgroundImage: `url(${a.url})` }} />
                : <span className="kb-att-ic"><Icon name={a.kind === "audio" ? "audioFile" : a.kind === "image" ? "image" : "fileDoc"} size={15} /></span>}
              <span className="kb-att-meta">
                <span className="kb-att-name">{a.name}</span>
                <span className="kb-att-sub">{a.kind === "image" ? "Imagem" : a.kind === "audio" ? "Áudio" : "Arquivo"} · {fmtSize(a.size)}</span>
              </span>
              <button className="kb-att-x" onClick={() => removeAtt(a.id)} aria-label="Remover anexo"><Icon name="x" size={13} /></button>
            </div>
          ))}
        </div>
      )}
      {hasImageAtt && !modelHasVision && (
        <div className="kb-att-warn">
          <Icon name="eye" size={15} />
          <span>O modelo <strong>{activeModel}</strong> não interpreta imagens. Escolha um modelo com visão (ex.: gemma3, llava, llama3.2-vision) para analisar o anexo.</span>
        </div>
      )}
      <textarea ref={ta} value={val} autoFocus={autoFocus}
        onChange={(e) => setVal(e.target.value)}
        onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); } }}
        placeholder={recording ? "Ouvindo… fale agora" : (placeholder || "Descreva o workflow que você quer criar…")} rows={1} />
      <div className="kb-composer-bar">
        <div className="kb-composer-left">
          <ProviderDropdown provider={provider} onChange={onProvider} dark={dark} modelChoice={modelChoice} />
          {webSearchAvailable && (
            <button className={"kb-tool" + (webSearchOn ? " on" : "")}
              data-tip={webSearchOn ? "Busca online ativada — clique para desligar" : "Permitir busca online (desligado)"}
              onClick={() => onWebSearch && onWebSearch(!webSearchOn)}
              aria-label="Busca online" aria-pressed={webSearchOn ? "true" : "false"}>
              <Icon name="globe" size={17} />
            </button>
          )}
          {thinkingAvailable && (
            <button className={"kb-tool" + (thinkingOn ? " on" : "")}
              data-tip={thinkingOn ? "Modo thinking ativado — clique para desligar" : "Ativar modo thinking deste modelo"}
              onClick={() => onThinking && onThinking(!thinkingOn)}
              aria-label="Modo thinking" aria-pressed={thinkingOn ? "true" : "false"}>
              <Icon name="brain" size={17} />
            </button>
          )}
          <div className="kb-att-wrap" ref={attRef}>
            <button className={"kb-tool" + (atts.length ? " has-active" : "") + (attMenu ? " active" : "")}
              data-tip="Anexar arquivo, imagem ou áudio" aria-label="Anexar"
              onClick={() => setAttMenu((o) => !o)}>
              <Icon name="paperclip" size={17} />
              {atts.length > 0 && <span className="kb-tool-badge">{atts.length}</span>}
            </button>
            {attMenu && (
              <div className={"kb-attpop" + (up ? " up" : "")}>
                <div className="kb-attpop-head"><Icon name="paperclip" size={15} /> Anexar ao chat</div>
                <div className="kb-attpop-list">
                  <button className="kb-attpop-item" onClick={() => pickFiles("image/*")}>
                    <span className="kb-attpop-ic img"><Icon name="image" size={17} /></span>
                    <span className="kb-attpop-txt"><span className="kb-attpop-name">Imagem</span><span className="kb-attpop-sub">Analisar fotos e prints</span></span>
                  </button>
                  <button className="kb-attpop-item" onClick={() => pickFiles(".pdf,.doc,.docx,.txt,.csv,.xlsx,.xls,.json,.md")}>
                    <span className="kb-attpop-ic file"><Icon name="fileDoc" size={17} /></span>
                    <span className="kb-attpop-txt"><span className="kb-attpop-name">Arquivo</span><span className="kb-attpop-sub">Relatórios e documentos</span></span>
                  </button>
                </div>
              </div>
            )}
            <input ref={fileRef} type="file" multiple onChange={onFiles} style={{ display: "none" }} />
          </div>
            <ComposerConnectors up={up} connectedIds={connectedIds} />
        </div>
        <div className="kb-composer-right">
          {voiceMsg && <span className="kb-voice-status">{voiceMsg}</span>}
          {voice !== false && (
            <button className={"kb-mic" + (recording ? " on" : "")} onClick={toggleVoice}
              data-tip={recording ? "Parar gravação" : "Mensagem por voz"} aria-label="Mensagem por voz">
              {recording ? <span className="kb-mic-wave"><i /><i /><i /><i /></span> : <Icon name="mic" size={18} />}
            </button>
          )}
          <button className={"kb-send" + (val.trim() ? " on" : "")} onClick={submit} aria-label="Enviar">
            <Icon name="send" size={18} stroke={2.2} />
          </button>
        </div>
      </div>
    </div>
  );
}

function categorySuggestions(label) {
  if (!label) return SUGGESTION_POOL;
  const cat = CONNECTOR_CATEGORIES.find((c) => c.label === label);
  if (!cat) return SUGGESTION_POOL;
  return SUGGESTION_POOL.filter((s) => s.connectors.some((id) => cat.ids.includes(id)));
}
function buildSuggestions(label) {
  return pickRandom(categorySuggestions(label), 4);
}
// only categories that actually have suggestion ideas
const FILTER_CATS = CONNECTOR_CATEGORIES.filter((c) => SUGGESTION_POOL.some((s) => s.connectors.some((id) => c.ids.includes(id))));

function dynamicGreeting(name) {
  const hour = new Date().getHours();
  const greeting = hour < 12 ? "Bom dia" : hour < 18 ? "Boa tarde" : "Boa noite";
  const complements = [
    "O que vamos automatizar hoje?",
    "Quer analisar dados, criar um relatório ou montar um workflow?",
    "Como posso ajudar sua operação agora?"
  ];
  const index = Math.floor(Date.now() / 86400000) % complements.length;
  return `${greeting}${name ? `, ${name}` : ""}. ${complements[index]}`;
}

function CategoryTabs({ selected, onSelect }) {
  return (
    <div className="kb-cattabs">
      <button className={"kb-cattab" + (!selected ? " active" : "")} onClick={() => onSelect(null)}>Todos</button>
      {FILTER_CATS.map((cat) => (
        <button key={cat.label} className={"kb-cattab" + (selected === cat.label ? " active" : "")}
          onClick={() => onSelect(cat.label)}>{cat.label}</button>
      ))}
    </div>
  );
}

// ---------- EMPTY STATE — Variant A (minimal, centered) ----------
function EmptyA({ provider, onProvider, onSend, userName, webSearchOn, onWebSearch, thinkingOn, onThinking, modelChoice, connectedIds }) {
  const [sel, setSel] = useStateH(null);
  const suggestions = useMemoH(() => buildSuggestions(sel), [sel]);
  return (
    <div className="kb-empty-a">
      <div className="kb-empty-a-inner">
        <div className="kb-hero">
          <OliveMark size={64} />
          <h1 className="kb-hero-title">{dynamicGreeting(userName)}</h1>
          <p className="kb-hero-sub">Descreva em linguagem natural e eu transformo em um workflow pronto para rodar.</p>
        </div>
        <div className="kb-hero-composer">
          <Composer provider={provider} onProvider={onProvider} onSend={onSend} autoFocus
            webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking}
            modelChoice={modelChoice} connectedIds={connectedIds} />
        </div>
        <div className="kb-starters">
          {STARTERS.map((s, i) => <StarterChip key={i} {...s} onClick={() => onSend(s.text)} />)}
        </div>
        <div className="kb-sugg-section">
          <div className="kb-sugg-head">
            <span className="kb-sugg-head-label">Sugestões por conector</span>
            <CategoryTabs selected={sel} onSelect={setSel} />
          </div>
          <div className="kb-sugg-grid">
            {suggestions.map((s, i) => <SuggestionCard key={i} s={s} onClick={() => onSend(s.desc)} />)}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- EMPTY STATE — Variant B (connector-forward discovery) ----------
function EmptyB({ provider, onProvider, onSend, webSearchOn, onWebSearch, thinkingOn, onThinking, modelChoice, connectedIds }) {
  const [sel, setSel] = useStateH(null);
  const suggestions = useMemoH(() => buildSuggestions(sel), [sel]);
  return (
    <div className="kb-empty-b">
      <div className="kb-empty-b-texture" />
      <div className="kb-empty-b-scroll">
        <div className="kb-empty-b-inner">
          <div className="kb-hero-b">
            <span className="kb-eyebrow"><OliveMark size={26} /> KOBAR FLOWS</span>
            <h1 className="kb-hero-b-title">Comece pelo conector.<br />Eu construo o resto.</h1>
            <p className="kb-hero-b-sub">Escolha uma categoria para ver ideias de automação ou descreva a sua no campo abaixo.</p>
          </div>
          <CategoryTabs selected={sel} onSelect={setSel} />
          <div className="kb-sugg-grid wide">
            {suggestions.map((s, i) => <SuggestionCard key={i} s={s} onClick={() => onSend(s.desc)} />)}
          </div>
          <div className="kb-starters muted">
            {STARTERS.slice(0, 3).map((s, i) => <StarterChip key={i} {...s} onClick={() => onSend(s.text)} />)}
          </div>
        </div>
      </div>
      <div className="kb-empty-b-dock">
        <Composer provider={provider} onProvider={onProvider} onSend={onSend} up
          webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking}
          modelChoice={modelChoice} connectedIds={connectedIds} />
      </div>
    </div>
  );
}

// ---------- CONVERSATION VIEW ----------
function Conversation({ messages, provider, onProvider, onSend, chatTitle, webSearchOn, onWebSearch, thinkingOn, onThinking, modelChoice, connectedIds, onConnect, onOpenFlow }) {
  const endRef = useRefH(null);
  useEffectH(() => { endRef.current?.scrollTo({ top: endRef.current.scrollHeight, behavior: "smooth" }); }, [messages]);
  return (
    <div className="kb-convo">
      <div className="kb-convo-head">
        <span className="kb-convo-title">{chatTitle}</span>
        <div className="kb-convo-actions">
          <button className="kb-icbtn" data-tip="Renomear"><Icon name="edit" size={17} /></button>
          <button className="kb-icbtn" data-tip="Compartilhar"><Icon name="share" size={17} /></button>
        </div>
      </div>
      <div className="kb-convo-scroll" ref={endRef}>
        <div className="kb-convo-inner">
          {messages.map((m) => <MessageBubble key={m.id} msg={m} connectedIds={connectedIds} onConnect={onConnect} onOpenFlow={onOpenFlow} />)}
        </div>
      </div>
      <div className="kb-convo-dock">
        <div className="kb-convo-dock-inner">
          <Composer provider={provider} onProvider={onProvider} onSend={onSend} placeholder="Continuar a conversa…" up
            webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking}
            modelChoice={modelChoice} connectedIds={connectedIds} />
          <span className="kb-disclaimer">Kobar pode cometer erros. Revise os workflows antes de publicar.</span>
        </div>
      </div>
    </div>
  );
}

function ChatHome({ variant, provider, onProvider, onSend, messages, chatTitle, userName, webSearchOn, onWebSearch, thinkingOn, onThinking, modelChoice, connectedIds, onConnect, onOpenFlow }) {
  if (messages && messages.length) {
    return <Conversation messages={messages} provider={provider} onProvider={onProvider} onSend={onSend} chatTitle={chatTitle}
      webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking} modelChoice={modelChoice}
      connectedIds={connectedIds} onConnect={onConnect} onOpenFlow={onOpenFlow} />;
  }
  return variant === "B"
    ? <EmptyB provider={provider} onProvider={onProvider} onSend={onSend}
        webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking}
        modelChoice={modelChoice} connectedIds={connectedIds} />
    : <EmptyA provider={provider} onProvider={onProvider} onSend={onSend} userName={userName}
        webSearchOn={webSearchOn} onWebSearch={onWebSearch} thinkingOn={thinkingOn} onThinking={onThinking}
        modelChoice={modelChoice} connectedIds={connectedIds} />;
}

Object.assign(window, { Composer, ChatHome });

/* ===== workflow.jsx ===== */
// workflow.jsx — interactive node canvas (drag nodes, zoom/pan/drop, settings), runs tab
const { useState: useStateW, useRef: useRefW, useEffect: useEffectW } = React;

const NODE_W = 248, NODE_H = 96, GAP_X = 312, ORIGIN_X = 64, ORIGIN_Y = 200;
const nodePos = (i) => ({ x: ORIGIN_X + i * GAP_X, y: ORIGIN_Y + (i % 2 === 0 ? 0 : 70) });
const TRIGGER_CONN = { schedule: 1, webhook: 1 };
let DRAGGED_CONNECTOR = null;

function WorkflowCanvas({ nodes, revealed, building, runStep, positions, setPositions,
  onDropConnector, onOpenSettings, nodeCreds, fitNonce, fullscreen, onToggleFullscreen }) {
  const kindColor = (k) => k === "TRIGGER" ? "var(--copper)" : "var(--olive)";
  const wrapRef = useRefW(null);
  const [base, setBase] = useStateW({ scale: 1, x: 0, y: 0 });
  const [zoom, setZoom] = useStateW(1);
  const [pan, setPan] = useStateW({ x: 0, y: 0 });
  const [dragOver, setDragOver] = useStateW(false);
  const [draggingNode, setDraggingNode] = useStateW(null);
  const panState = useRefW(null);
  const nodeDrag = useRefW(null);
  const userZoomed = useRefW(false);
  const scaleRef = useRefW(1);
  const visible = Math.min(revealed, nodes.length);
  const posOf = (n, i) => positions[n.id] || nodePos(i);

  const fit = React.useCallback(() => {
    const el = wrapRef.current; if (!el || visible === 0) { setBase({ scale: 1, x: 0, y: 0 }); setZoom(1); setPan({ x: 0, y: 0 }); return; }
    const W = el.clientWidth, H = el.clientHeight, margin = 90;
    const pts = nodes.slice(0, visible).map((n, i) => posOf(n, i));
    const minX = Math.min(...pts.map((p) => p.x)), maxX = Math.max(...pts.map((p) => p.x)) + NODE_W;
    const minY = Math.min(...pts.map((p) => p.y)), maxY = Math.max(...pts.map((p) => p.y)) + NODE_H;
    const bbW = maxX - minX, bbH = maxY - minY;
    const scale = Math.min(1, (W - 2 * margin) / bbW, (H - 2 * margin) / bbH);
    setBase({ scale, x: (W - bbW * scale) / 2 - minX * scale, y: (H - bbH * scale) / 2 - minY * scale });
    setZoom(1); setPan({ x: 0, y: 0 }); userZoomed.current = false;
  }, [visible, nodes, positions]);
  const fitRef = useRefW(fit); fitRef.current = fit;

  // fit on fullscreen toggle, on node-count change (unless the user manually zoomed), and on explicit refit
  React.useLayoutEffect(() => { fitRef.current(); }, [fullscreen]);
  useEffectW(() => { if (!userZoomed.current) fitRef.current(); }, [visible]);
  useEffectW(() => { if (fitNonce) { const t = setTimeout(() => fitRef.current(), 110); return () => clearTimeout(t); } }, [fitNonce]);
  useEffectW(() => {
    const ro = new ResizeObserver(() => { if (!userZoomed.current) fitRef.current(); });
    if (wrapRef.current) ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  scaleRef.current = base.scale * zoom;
  const zoomAt = (factor) => { userZoomed.current = true; setZoom((z) => Math.max(0.4, Math.min(2.6, z * factor))); };
  const onWheel = (e) => { if (!e.ctrlKey && !e.metaKey) return; e.preventDefault(); zoomAt(e.deltaY < 0 ? 1.1 : 0.9); };

  // background pan
  const onPointerDown = (e) => {
    if (e.target.closest(".kb-node")) return;
    userZoomed.current = true;
    panState.current = { sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y };
    wrapRef.current.setPointerCapture(e.pointerId);
    wrapRef.current.classList.add("grabbing");
  };
  const onPointerMove = (e) => {
    if (!panState.current) return;
    setPan({ x: panState.current.px + (e.clientX - panState.current.sx), y: panState.current.py + (e.clientY - panState.current.sy) });
  };
  const endPan = () => { panState.current = null; wrapRef.current?.classList.remove("grabbing"); };

  // node drag
  const startNodeDrag = (e, n, i) => {
    e.stopPropagation();
    const start = posOf(n, i);
    nodeDrag.current = { id: n.id, sx: e.clientX, sy: e.clientY, ox: start.x, oy: start.y };
    setDraggingNode(n.id);
    const move = (ev) => {
      const d = nodeDrag.current; if (!d) return;
      const s = scaleRef.current || 1;
      setPositions((prev) => ({ ...prev, [d.id]: { x: d.ox + (ev.clientX - d.sx) / s, y: d.oy + (ev.clientY - d.sy) / s } }));
    };
    const up = () => { nodeDrag.current = null; setDraggingNode(null); window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  const tx = base.x + pan.x, ty = base.y + pan.y, scale = base.scale * zoom;

  return (
    <div className={"kb-canvas" + (dragOver ? " dropping" : "")} ref={wrapRef}
      onWheel={onWheel} onPointerDown={onPointerDown} onPointerMove={onPointerMove}
      onPointerUp={endPan} onPointerLeave={endPan}
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={(e) => { if (e.target === wrapRef.current) setDragOver(false); }}
      onDrop={(e) => { e.preventDefault(); setDragOver(false); const id = (e.dataTransfer && e.dataTransfer.getData("text/connector")) || DRAGGED_CONNECTOR; DRAGGED_CONNECTOR = null; if (id) onDropConnector(id); }}>
      <div className="kb-canvas-grid" />
      <div className="kb-canvas-texture" />
      <div className="kb-canvas-scene" style={{ transform: `translate(${tx}px,${ty}px) scale(${scale})`, transition: (building || panState.current || draggingNode) ? "none" : undefined }}>
        <svg className="kb-canvas-wires">
          {nodes.map((n, i) => {
            if (i === 0 || i >= revealed) return null;
            const a = posOf(nodes[i - 1], i - 1), b = posOf(n, i);
            const x1 = a.x + NODE_W, y1 = a.y + NODE_H / 2, x2 = b.x, y2 = b.y + NODE_H / 2, mx = (x1 + x2) / 2;
            const d = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;
            const lit = runStep > i;
            return (
              <g key={i}>
                <path d={d} className="kb-wire" />
                <path d={d} className={"kb-wire-flow" + (runStep >= 0 && !lit ? " idle" : "")} />
                {lit && <circle cx={x2} cy={y2} r="4" className="kb-wire-dot" />}
              </g>
            );
          })}
        </svg>
        {nodes.map((n, i) => {
          if (i >= revealed) return null;
          const p = posOf(n, i);
          const state = runStep < 0 ? "" : (runStep === i ? " running" : (runStep > i ? " done" : ""));
          // Configurado = creds locais OU conector ja conectado na plataforma
          // (credentialRef real, nao "pending:") — evita re-pedir credenciais.
          const configured = (nodeCreds && nodeCreds[n.id]) || (n.credentialRef && !String(n.credentialRef).startsWith("pending:"));
          return (
            <div key={n.id} className={"kb-node" + state + (draggingNode === n.id ? " dragging" : "")}
              style={{ left: p.x, top: p.y, width: NODE_W }}
              onPointerDown={(e) => { if (e.target.closest(".kb-node-gear,.kb-node-port")) return; startNodeDrag(e, n, i); }}>
              <div className="kb-node-top">
                <span className="kb-node-conn"><Connector id={n.connector} size={30} /></span>
                <span className="kb-node-kind" style={{ color: kindColor(n.kind), borderColor: "color-mix(in oklab," + kindColor(n.kind) + ", transparent 70%)" }}>
                  {n.kind === "TRIGGER" ? "GATILHO" : "AÇÃO"}
                </span>
                <button className="kb-node-gear" onClick={(e) => { e.stopPropagation(); onOpenSettings(n); }} data-tip2="Configurar credenciais"><Icon name="gear" size={15} /></button>
              </div>
              <div className="kb-node-title">{n.title}</div>
              <div className="kb-node-sub">{n.sub}</div>
              {configured && <span className="kb-node-cred" data-tip2="Credenciais configuradas"><Icon name="key" size={11} /></span>}
              {state === " running" && <span className="kb-node-runbadge"><OliveLoader size={16} speed={1.6} /></span>}
              {state === " done" && <span className="kb-node-okbadge"><Icon name="check" size={13} /></span>}
              <span className="kb-node-port in" />
              <span className="kb-node-port out" />
            </div>
          );
        })}
      </div>

      <div className="kb-canvas-tools">
        <button data-tip2="Afastar" onClick={() => zoomAt(0.83)}><Icon name="minus" size={16} /></button>
        <span className="kb-zoom-val">{Math.round(scale * 100)}%</span>
        <button data-tip2="Aproximar" onClick={() => zoomAt(1.2)}><Icon name="plus" size={16} /></button>
        <span className="kb-tools-sep" />
        <button data-tip2="Ajustar à tela" onClick={() => fit()}><Icon name="expand" size={16} /></button>
        <button data-tip2={fullscreen ? "Sair da tela cheia" : "Tela cheia"} onClick={onToggleFullscreen}>
          <Icon name={fullscreen ? "minimize" : "maximize"} size={16} />
        </button>
      </div>
      {dragOver && <div className="kb-drop-hint"><Icon name="plus" size={20} /> Solte para adicionar ao fluxo</div>}
      {visible === 0 && !dragOver && (
        <div className="kb-canvas-empty">
          <span className="kb-canvas-empty-ic"><Icon name="flows" size={26} /></span>
          <h3>Workflow em branco</h3>
          <p>Arraste um conector do painel à esquerda para começar, ou peça ao agente no chat à direita.</p>
        </div>
      )}
    </div>
  );
}

function Switch({ on, onClick }) {
  return <button className={"kb-switch" + (on ? " on" : "")} onClick={onClick} role="switch" aria-checked={on}><span className="kb-switch-knob" /></button>;
}

// ---- Node credential settings modal ----
function NodeSettingsModal({ node, saved, onSave, onClose }) {
  const fields = CONN_CREDENTIALS[node.connector] || DEFAULT_CREDENTIALS;
  const [vals, setVals] = useStateW(saved || {});
  const set = (k, v) => setVals((s) => ({ ...s, [k]: v }));
  const c = CONNECTORS[node.connector];
  return (
    <div className="kb-modal-scrim" onClick={onClose}>
      <div className="kb-modal" onClick={(e) => e.stopPropagation()}>
        <button className="kb-modal-x" onClick={onClose}><Icon name="x" size={18} /></button>
        <div className="kb-modal-head">
          <Connector id={node.connector} size={48} />
          <div>
            <h3 className="kb-modal-title">{node.title}</h3>
            <span className="kb-modal-subtle">{c.label} · {node.kind === "TRIGGER" ? "Gatilho" : "Ação"}</span>
          </div>
        </div>
        <div className="kb-modal-fields">
          {fields.map((f) => (
            <label key={f.k} className="kb-field">
              <span className="kb-field-label">{f.label}{f.secret && <Icon name="lock" size={11} className="kb-field-lock" />}</span>
              {f.type === "select" ? (
                <span className="kb-field-box">
                  <Icon name="chevDown" size={15} className="kb-field-ic" />
                  <select value={vals[f.k] || f.options[0]} onChange={(e) => set(f.k, e.target.value)}>
                    {f.options.map((o) => <option key={o} value={o}>{o}</option>)}
                  </select>
                </span>
              ) : (
                <span className="kb-field-box">
                  <Icon name={f.secret ? "key" : (f.type === "time" ? "clock" : "edit")} size={15} className="kb-field-ic" />
                  <input type={f.secret ? "password" : (f.type === "time" ? "time" : "text")}
                    value={vals[f.k] || ""} placeholder={f.ph} onChange={(e) => set(f.k, e.target.value)} />
                </span>
              )}
            </label>
          ))}
        </div>
        <div className="kb-modal-note"><Icon name="shield" size={13} /> As credenciais ficam no seu ambiente e não são compartilhadas.</div>
        <div className="kb-modal-actions">
          <button className="kb-btn ghost" onClick={onClose}>Cancelar</button>
          <button className="kb-btn primary" onClick={() => onSave(node.id, vals)}><Icon name="check" size={15} /> Salvar credenciais</button>
        </div>
      </div>
    </div>
  );
}

function ConnectorsStatusPopover({ active, states, onToggle, onClose }) {
  const ref = useRefW(null);
  useEffectW(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);
  return (
    <div className="kb-connpop" ref={ref}>
      <div className="kb-connpop-head"><Icon name="connect" size={15} /> Conectores ativos<span className="kb-connpop-count">{active.filter((id) => states[id]).length}/{active.length}</span></div>
      <div className="kb-connpop-list">
        {active.length === 0 && <div className="kb-connpop-empty">Nenhum conector no fluxo ainda.</div>}
        {active.map((id) => (
          <div key={id} className="kb-connpop-item">
            <Connector id={id} size={28} />
            <div className="kb-connpop-txt">
              <span className="kb-connpop-name">{CONNECTORS[id].label}</span>
              <span className={"kb-connpop-state" + (states[id] ? " on" : "")}>{states[id] ? "ligado" : "desligado"}</span>
            </div>
            <Switch on={states[id]} onClick={() => onToggle(id)} />
          </div>
        ))}
      </div>
    </div>
  );
}

function WorkflowAIPanel({ provider, onProvider, messages, onSend }) {
  const scroll = useRefW(null);
  useEffectW(() => { scroll.current?.scrollTo({ top: scroll.current.scrollHeight, behavior: "smooth" }); }, [messages]);
  return (
    <aside className="kb-wpanel">
      <div className="kb-wpanel-head">
        <span className="kb-wpanel-title"><OliveMark size={22} /> Chat com os agentes</span>
      </div>
      <div className="kb-wpanel-scroll" ref={scroll}>
        {messages.map((m) => <MessageBubble key={m.id} msg={m} />)}
      </div>
      <div className="kb-wpanel-dock">
        <Composer provider={provider} onProvider={onProvider} onSend={onSend} voice up
          placeholder="Peça um ajuste ao fluxo…" />
      </div>
    </aside>
  );
}

function WorkflowConnectorsPanel({ open, onToggle, activeConn, connStates }) {
  const list = ["schedule", "webhook", "http", "gworkspace", "m365", "slack", "github", "notion", "discord", "sheets", "gmail", "stripe", "telegram"];
  return (
    <aside className={"kb-wconn" + (open ? "" : " closed")} style={{ width: open ? 252 : 0, minWidth: 0 }}>
      <div className="kb-wconn-head">
        <Icon name="connect" size={15} />
        <span className="kb-wconn-title">Conectores</span>
        <span className="kb-wconn-count">{list.length}</span>
        <button className="kb-wconn-collapse" onClick={onToggle} data-tip2="Ocultar painel"><Icon name="chevLeft" size={16} /></button>
      </div>
      <div className="kb-wconn-list">
        <div className="kb-wconn-hint">Arraste um conector para o canvas. O de <strong>Agendamento</strong> roda o fluxo automaticamente.</div>
        {list.map((id) => {
          const inUse = activeConn.includes(id);
          const on = inUse && connStates[id];
          return (
            <div key={id} className={"kb-wconn-item" + (inUse ? " inuse" : "")} draggable
              onDragStart={(e) => { DRAGGED_CONNECTOR = id; e.dataTransfer.setData("text/connector", id); e.dataTransfer.effectAllowed = "copy"; }}
              onDragEnd={() => { DRAGGED_CONNECTOR = null; }}>
              <span className="kb-wconn-ic">
                <Connector id={id} size={30} />
                {inUse && <span className={"kb-wconn-dot" + (on ? " on" : " off")} />}
              </span>
              <div className="kb-wconn-itxt">
                <span className="kb-wconn-iname">{CONNECTORS[id].label}</span>
                <span className="kb-wconn-isub">{inUse ? (on ? "Em uso · ligado" : "Em uso · desligado") : (TRIGGER_CONN[id] ? "Gatilho" : "Arraste para usar")}</span>
              </div>
              {inUse ? <span className="kb-wconn-badge">em uso</span> : <Icon name="drag" size={15} className="kb-wconn-drag" />}
            </div>
          );
        })}
      </div>
    </aside>
  );
}

function WorkflowContextPanel({ nodeCount, connectors }) {
  const [open, setOpen] = useStateW(true);
  const icons = ["schedule", "http", "sheets", "slack", "github", "notion", "stripe", "gmail"];
  return (
    <div className={"kb-wctx" + (open ? "" : " min")}>
      <button className="kb-wctx-head" onClick={() => setOpen((o) => !o)}>
        <div className="kb-wctx-headtxt"><strong>Contexto do workflow</strong><em>Grafo atual e gates de publicação</em></div>
        <Icon name="chevDown" size={16} className="kb-wctx-chev" />
      </button>
      {open && (
        <div className="kb-wctx-body">
          <div className="kb-wctx-row"><span><Icon name="check" size={14} /> Conectores ativos</span><b>{connectors}</b></div>
          <div className="kb-wctx-row"><span><Icon name="flows" size={14} /> Nós</span><b>{nodeCount}</b></div>
          <div className="kb-wctx-row"><span><Icon name="skills" size={14} /> Skill principal</span><b className="kb-cu">Geração de workflow</b></div>
          <div className="kb-wctx-icons">{icons.map((id) => <Connector key={id} id={id} size={26} />)}</div>
          <div className="kb-wctx-note">A publicação usa o grafo atual e o estado das conexões.</div>
        </div>
      )}
    </div>
  );
}

function RunsTab({ runs, nodes }) {
  const [sel, setSel] = useStateW(runs[0] ? runs[0].id : null);
  if (!runs.length) {
    return (
      <div className="kb-runs-empty">
        <span className="kb-runs-empty-ic"><Icon name="runs" size={26} /></span>
        <h3>Nenhuma execução ainda</h3>
        <p>Clique em <strong>Executar</strong> para rodar o workflow. O histórico aparece aqui.</p>
      </div>
    );
  }
  const run = runs.find((r) => r.id === sel) || runs[0];
  return (
    <div className="kb-runs">
      <div className="kb-runs-list">
        {runs.map((r) => (
          <button key={r.id} className={"kb-runrow" + (r.id === run.id ? " active" : "")} onClick={() => setSel(r.id)}>
            <StatusPill s={r.status} />
            <span className="kb-runrow-when">{r.when}</span>
            <span className="kb-runrow-dur">{r.dur}</span>
          </button>
        ))}
      </div>
      <div className="kb-runs-detail">
        <div className="kb-runs-detail-head">
          <div><span className="kb-runs-detail-id">Execução {run.id}</span><span className="kb-runs-detail-meta">{run.trigger} · {run.when} · {run.dur}</span></div>
          <StatusPill s={run.status} />
        </div>
        <div className="kb-runsteps">
          {nodes.map((n, i) => (
            <div key={n.id} className="kb-runstep">
              <span className="kb-runstep-ic"><Connector id={n.connector} size={26} /></span>
              <div className="kb-runstep-txt"><span className="kb-runstep-title">{n.title}</span><span className="kb-runstep-sub">{n.sub}</span></div>
              <span className="kb-runstep-time">{(0.2 + i * 0.3).toFixed(1)}s</span>
              <span className="kb-runstep-ok"><Icon name="check" size={14} /></span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function WorkflowPage({ blueprint, provider, onProvider, onBack }) {
  const blank = !!blueprint.blank;
  const [nodes, setNodes] = useStateW(blueprint.nodes);
  const [positions, setPositions] = useStateW({});
  const [revealed, setRevealed] = useStateW(blank ? 0 : 0);
  const [building, setBuilding] = useStateW(!blank);
  const [msgs, setMsgs] = useStateW([]);
  const [connOpen, setConnOpen] = useStateW(true);
  const [tab, setTab] = useStateW("editor");
  const [fullscreen, setFullscreen] = useStateW(false);
  const [runStep, setRunStep] = useStateW(-1);
  const [running, setRunning] = useStateW(false);
  const [runs, setRuns] = useStateW([]);
  const [toast, setToast] = useStateW(null);
  const [connStates, setConnStates] = useStateW({});
  const [nodeCreds, setNodeCreds] = useStateW({});
  const [settingsNode, setSettingsNode] = useStateW(null);
  const [fitNonce, setFitNonce] = useStateW(0);
  const [publishing, setPublishing] = useStateW(false);
  const [published, setPublished] = useStateW(false);
  const idc = useRefW(100);
  const rc = useRefW(0);
  const nid = () => "wm" + (++idc.current);

  const activeConn = Array.from(new Set(nodes.slice(0, Math.max(revealed, 0)).map((n) => n.connector)));
  useEffectW(() => {
    setConnStates((s) => { const n = { ...s }; activeConn.forEach((id) => { if (!(id in n)) n[id] = true; }); return n; });
  }, [revealed, nodes.length]);

  const showToast = (text, kind) => { setToast({ text, kind }); setTimeout(() => setToast(null), 2600); };

  useEffectW(() => {
    if (blank) {
      setMsgs([{ id: nid(), role: "ai", via: PROVIDERS[provider].model,
        text: "Workflow em branco criado. Arraste conectores do painel à esquerda para montar o fluxo, ou me diga o que você quer automatizar que eu monto para você." }]);
      return;
    }
    let i = 0;
    const tick = () => {
      i += 1; setRevealed(i);
      if (i < blueprint.nodes.length) setTimeout(tick, 620);
      else {
        setBuilding(false);
        setMsgs([{ id: nid(), role: "ai", via: PROVIDERS[provider].model,
          text: `Montei o workflow “${blueprint.name}” com ${blueprint.nodes.length} etapas:\n1 · Gatilho de agendamento, todo dia às 9h\n2 · Buscar a cotação via requisição HTTP\n3 · Registrar a linha no Google Sheets\n\nClique no ícone de engrenagem de cada nó para configurar as credenciais.` }]);
      }
    };
    const t = setTimeout(tick, 350);
    return () => clearTimeout(t);
  }, []);

  const addNode = (node, asTrigger) => {
    setNodes((ns) => {
      const insertTrigger = asTrigger && ns.some((x) => x.kind === "TRIGGER");
      const next = insertTrigger ? ns : [...ns, node];
      const idx = next.length - 1;
      setPositions((p) => ({ ...p, [node.id]: nodePos(idx) }));
      setTimeout(() => { setRevealed(next.length); setFitNonce((n) => n + 1); }, 40);
      return next;
    });
  };

  const send = (text) => {
    setMsgs((m) => [...m, { id: nid(), role: "user", text }]);
    const thinkId = nid();
    setMsgs((m) => [...m, { id: thinkId, role: "ai", thinking: true, thinkingText: "Ajustando o workflow…" }]);
    setTimeout(() => {
      const newNode = { id: nid(), kind: "ACTION", title: "Notificar no Slack", sub: "slack.post_message", connector: "slack" };
      addNode(newNode);
      setMsgs((m) => m.map((x) => x.id === thinkId ? { id: thinkId, role: "ai", via: PROVIDERS[provider].model,
        text: "Adicionei uma etapa de aviso no Slack ao final do fluxo. O nó já aparece conectado no canvas." } : x));
    }, 1700);
  };

  const dropConnector = (id) => {
    const c = CONNECTORS[id];
    const isTrigger = !!TRIGGER_CONN[id];
    const title = id === "schedule" ? "Agendamento" : (id === "webhook" ? "Webhook recebido" : c.label);
    const sub = id === "schedule" ? "schedule.cron" : (isTrigger ? id + ".trigger" : id + ".action");
    const node = { id: nid(), kind: isTrigger ? "TRIGGER" : "ACTION", title, sub, connector: id };
    if (tab !== "editor") setTab("editor");
    if (isTrigger && nodes.some((x) => x.kind === "TRIGGER")) { showToast("O fluxo já tem um gatilho", "warn"); return; }
    addNode(node, isTrigger);
    showToast(`${isTrigger ? "Gatilho" : "Ação"} “${title}” adicionado`, "ok");
  };

  const runTimer = useRefW(null);
  const runWorkflow = async () => {
    if (running || building) return;
    if (revealed === 0) { showToast("Adicione ao menos um passo para executar", "warn"); return; }
    setTab("editor"); setRunning(true); setRunStep(0);
    const total = Math.min(nodes.length, revealed) || nodes.length;
    const firstConn = nodes[0] ? nodes[0].connector : "schedule";

    // animacao visual dos passos enquanto roda
    let step = 0;
    clearInterval(runTimer.current);
    const startAnim = (intervalMs, onDone) => {
      runTimer.current = setInterval(() => {
        step += 1;
        if (step < total) setRunStep(step);
        else { clearInterval(runTimer.current); setRunStep(total); if (onDone) onDone(); }
      }, intervalMs);
    };

    // EXECUCAO REAL: quando o flow esta salvo (tem id), chama o motor de verdade.
    // O backend resolve as credenciais dos conectores ja conectados (o cliente NAO
    // precisa reinserir) e envia para os destinos reais (ex.: Discord).
    if (blueprint.id) {
      startAnim(600);
      try {
        const { run } = await kbWorkflows.run(blueprint.id);
        clearInterval(runTimer.current); setRunStep(total);
        const ok = run.status === "success";
        const statusLabel = ok ? "sucesso" : (run.status === "cancelled" ? "cancelado" : "erro");
        setTimeout(() => {
          setRunning(false); setRunStep(-1);
          const id = run.id || ("#" + (1042 + (++rc.current)));
          setRuns((r) => [{
            id, status: statusLabel, when: "agora",
            dur: ((run.durationMs || 0) / 1000).toFixed(1) + " s",
            trigger: firstConn, steps: run.steps || []
          }, ...r]);
          showToast(ok ? "Execução real concluída — enviado aos conectores" : ("Falha: " + ((run.error && run.error.message) || "erro na execução")), ok ? "ok" : "warn");
        }, 350);
      } catch (e) {
        clearInterval(runTimer.current);
        setRunning(false); setRunStep(-1);
        showToast(e.message || "Falha ao executar o workflow", "warn");
      }
      return;
    }

    // FALLBACK (flow ainda nao salvo, sem id): apenas simulacao visual.
    startAnim(760, () => {
      setTimeout(() => {
        setRunning(false); setRunStep(-1);
        const id = "#" + (1042 + (++rc.current));
        setRuns((r) => [{ id, status: "sucesso", when: "agora", dur: (0.6 + total * 0.3).toFixed(1) + " s", trigger: firstConn }, ...r]);
        showToast("Simulação concluída (salve/publique o flow para executar de verdade)", "ok");
      }, 700);
    });
  };
  useEffectW(() => () => clearInterval(runTimer.current), []);

  const toggleConn = (id) => setConnStates((s) => ({ ...s, [id]: !s[id] }));
  const saveCreds = (nodeId, vals) => { setNodeCreds((c) => ({ ...c, [nodeId]: vals })); setSettingsNode(null); showToast("Credenciais salvas", "ok"); };

  // Publica de verdade: cria versao imutavel e habilita o agendador (cron) a rodar
  // o flow automaticamente. Sem id (flow nao salvo), nao ha o que publicar.
  const publishFlow = async () => {
    if (publishing) return;
    if (!blueprint.id) { showToast("Salve o flow (gere pelo chat) antes de publicar", "warn"); return; }
    setPublishing(true);
    try {
      const r = await kbWorkflows.publish(blueprint.id);
      setPublished(true);
      showToast(`Publicado (v${r.version}). O agendador roda no horário configurado.`, "ok");
    } catch (e) {
      const hasErr = e.data && Array.isArray(e.data.issues) && e.data.issues.some((i) => i.severity === "error");
      showToast(hasErr ? "Corrija os erros de validação antes de publicar." : (e.message || "Falha ao publicar"), "warn");
    } finally { setPublishing(false); }
  };

  return (
    <div className={"kb-workflow" + (fullscreen ? " fullscreen" : "")}>
      <header className="kb-wtopbar">
        <div className="kb-wtop-left">
          <button className="kb-back" onClick={onBack} data-tip="Voltar"><Icon name="chevLeft" size={18} /></button>
          <span className="kb-wname">{blueprint.name}</span>
          <button className="kb-wtag"><Icon name="tag" size={13} /> {blueprint.tag}</button>
          <button className="kb-wtag add"><Icon name="plus" size={13} /> tag</button>
        </div>
        <div className="kb-wtop-center">
          <button className={"kb-wtab" + (tab === "editor" ? " active" : "")} onClick={() => setTab("editor")}>Editor</button>
          <button className={"kb-wtab" + (tab === "runs" ? " active" : "")} onClick={() => setTab("runs")}>Execuções{runs.length ? ` · ${runs.length}` : ""}</button>
        </div>
        <div className="kb-wtop-right">
          <span className="kb-wstatus">{building ? "gerando…" : running ? "executando…" : published ? "publicado · agendador ativo" : "rascunho · salvo localmente"}</span>
          <button className="kb-langpill"><Icon name="globe" size={13} /> PT</button>
          <button className={"kb-wbtn ghost" + (running || building ? " disabled" : "")} onClick={runWorkflow}>
            {running ? <OliveLoader size={15} speed={1.6} /> : <Icon name="play" size={15} />} Executar
          </button>
          <button className={"kb-wbtn primary" + (publishing ? " disabled" : "")} onClick={publishFlow}>
            {publishing ? <OliveLoader size={15} speed={1.6} /> : <Icon name="share" size={15} />} {published ? "Publicado" : "Publicar"}
          </button>
        </div>
      </header>
      <div className="kb-wbody">
        {tab === "editor" ? (
          <>
            <WorkflowConnectorsPanel open={connOpen} onToggle={() => setConnOpen((o) => !o)} activeConn={activeConn} connStates={connStates} />
            <div className="kb-wcanvas-wrap">
              <WorkflowCanvas nodes={nodes} revealed={revealed} building={building} runStep={runStep}
                positions={positions} setPositions={setPositions} fitNonce={fitNonce}
                onDropConnector={dropConnector} onOpenSettings={setSettingsNode} nodeCreds={nodeCreds}
                fullscreen={fullscreen} onToggleFullscreen={() => setFullscreen((f) => !f)} />
              <WorkflowContextPanel nodeCount={revealed} connectors={activeConn.filter((id) => connStates[id]).length} />
              <div className="kb-canvas-topleft">
                {!connOpen && <button className="kb-wconn-reopen" onClick={() => setConnOpen(true)}><Icon name="connect" size={15} /> Conectores</button>}
                <div className="kb-canvas-badge"><ProviderMark id={provider} size={13} /> {PROVIDERS[provider].model}</div>
              </div>
            </div>
            <WorkflowAIPanel provider={provider} onProvider={onProvider} messages={msgs} onSend={send} />
          </>
        ) : (
          <RunsTab runs={runs} nodes={nodes.slice(0, revealed)} />
        )}
      </div>
      {settingsNode && <NodeSettingsModal node={settingsNode} saved={nodeCreds[settingsNode.id]} onSave={saveCreds} onClose={() => setSettingsNode(null)} />}
      {toast && <div className={"kb-toast " + (toast.kind || "")}><Icon name={toast.kind === "warn" ? "bolt" : "check"} size={16} /> {toast.text}</div>}
    </div>
  );
}

Object.assign(window, { WorkflowPage });

/* ===== pages.jsx ===== */
// pages.jsx — section screens (light app shell): Flows, Connect, Runs, Models,
// Agents, History, Settings, Upgrade
const { useState: useStateP, useEffect: useEffectP } = React;

const STATUS = {
  ativo:          { label: "ativo", cls: "ok" },
  rascunho:       { label: "rascunho", cls: "draft" },
  pausado:        { label: "pausado", cls: "paused" },
  sucesso:        { label: "sucesso", cls: "ok" },
  erro:           { label: "erro", cls: "err" },
  "em execução":  { label: "em execução", cls: "run" },
};
function StatusPill({ s }) {
  const d = STATUS[s] || { label: s, cls: "draft" };
  return <span className={"kb-status " + d.cls}>{d.cls === "run" && <span className="kb-status-dot" />}{d.label}</span>;
}

function SectionShell({ title, subtitle, actions, children, eyebrow }) {
  return (
    <main className="kb-section">
      <header className="kb-sec-head">
        <div className="kb-sec-headtxt">
          {eyebrow && <span className="kb-sec-eyebrow">{eyebrow}</span>}
          <h1 className="kb-sec-title">{title}</h1>
          {subtitle && <p className="kb-sec-sub">{subtitle}</p>}
        </div>
        {actions && <div className="kb-sec-actions">{actions}</div>}
      </header>
      <div className="kb-sec-scroll"><div className="kb-sec-body">{children}</div></div>
    </main>
  );
}

// ---------------- Flows ----------------
function FlowsPage({ onOpen, onNew, onDelete, flows = [] }) {
  const [q, setQ] = useStateP("");
  const [busyDelete, setBusyDelete] = useStateP("");
  const list = flows.filter((f) => f.name.toLowerCase().includes(q.toLowerCase()));
  const remove = async (e, flow) => {
    e.stopPropagation();
    if (busyDelete) return;
    const ok = window.confirm(`Excluir o flow "${flow.name}"? Workflows publicados serão arquivados.`);
    if (!ok) return;
    setBusyDelete(flow.id);
    try {
      await onDelete(flow);
    } finally {
      setBusyDelete("");
    }
  };
  return (
    <SectionShell title="Flows" subtitle="Seus workflows de automação."
      actions={<button className="kb-btn primary" onClick={onNew}><Icon name="plus" size={16} stroke={2.2} /> Novo workflow</button>}>
      <div className="kb-toolbar">
        <div className="kb-search inline"><Icon name="search" size={15} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Buscar workflows" /></div>
        <span className="kb-count">{flows.filter((f) => f.status === "ativo").length} ativos · {flows.length} no total</span>
      </div>
      {flows.length === 0 && (
        <div className="kb-empty-chats">Nenhum workflow ainda. Crie o primeiro a partir de um chat ou clique em “Novo workflow”.</div>
      )}
      <div className="kb-flowgrid">
        {list.map((f) => (
          <div key={f.id} className="kb-flowcard" role="button" tabIndex={0} onClick={() => onOpen(f)}
            onKeyDown={(e) => { if (e.key === "Enter") onOpen(f); }}>
            <div className="kb-flowcard-top">
              <span className="kb-flow-cons">{f.connectors.map((id, i) => <Connector key={i} id={id} size={28} />)}</span>
              <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
                <StatusPill s={f.status} />
                <button className="kb-btn tiny ghost" title="Excluir flow" onClick={(e) => remove(e, f)} disabled={busyDelete === f.id}>
                  <Icon name="trash" size={14} />
                </button>
              </span>
            </div>
            <div className="kb-flow-name">{f.name}</div>
            <div className="kb-flow-meta">
              <span><Icon name="flows" size={13} /> {f.nodes} nós</span>
              <span><Icon name="runs" size={13} /> {f.runs} exec.</span>
              <span><Icon name="clock" size={13} /> {f.lastRun}</span>
            </div>
            <span className="kb-flow-tag">#{f.tag}</span>
          </div>
        ))}
      </div>
    </SectionShell>
  );
}

// ---------------- Connect ----------------
// Modal de conexao real, dirigido pela config do conector (/api/connectors).
// `connectorId` aqui e o id do BACKEND (ex.: slack_webhook).
function ConnectorModal({ connectorId, catalog, onClose, onConnected, returnTo, forceReconnect }) {
  const cfg = (catalog || []).find((c) => c.id === connectorId) || null;
  const name = (cfg && cfg.name) || connectorId;
  const method = (cfg && cfg.effectiveMethod) || "credentials";
  const fields = (cfg && cfg.fields) || [];
  const [vals, setVals] = useStateP({});
  const [busy, setBusy] = useStateP(false);
  const [err, setErr] = useStateP("");
  const [done, setDone] = useStateP(!!(cfg && cfg.connected) && !forceReconnect);
  const [displayName, setDisplayName] = useStateP((cfg && cfg.displayName) || "");

  const setVal = (k, v) => setVals((s) => ({ ...s, [k]: v }));

  const submit = async () => {
    if (busy) return;
    setErr("");
    const missing = fields.filter((f) => !((vals[f.k] || "").trim()));
    if (missing.length) { setErr("Preencha: " + missing.map((f) => f.label).join(", ")); return; }
    setBusy(true);
    try {
      const data = await kbConnectors.connect(connectorId, vals);
      setDone(true);
      setDisplayName((data.connection && data.connection.displayName) || "");
      onConnected && onConnected(connectorId, true);
    } catch (e) {
      setErr(e.message || "Falha ao conectar.");
    } finally {
      setBusy(false);
    }
  };

  const authorize = async () => {
    if (busy) return;
    setErr(""); setBusy(true);
    try {
      const { url } = await kbConnectors.oauthStart(connectorId, returnTo || (window.location.pathname + window.location.search));
      const width = 520, height = 640;
      const left = window.screenX + Math.max(0, (window.outerWidth - width) / 2);
      const top = window.screenY + Math.max(0, (window.outerHeight - height) / 2);
      const popup = window.open(url, "kobar_oauth", `width=${width},height=${height},left=${left},top=${top}`);
      if (!popup) (window.top || window).location.href = url;
    } catch (e) {
      setErr(e.message || "OAuth indisponivel."); setBusy(false);
    }
  };

  const disconnect = async () => {
    if (busy) return;
    setBusy(true);
    try { await kbConnectors.disconnect(connectorId); onConnected && onConnected(connectorId, false); onClose(); }
    catch (e) { setErr("Falha ao desconectar."); setBusy(false); }
  };

  const errBox = err ? (
    <div style={{ color: "#9B3D27", background: "#FBEFEB", border: "1px solid #E7C3B8", borderRadius: 10, padding: "9px 11px", fontSize: 13, margin: "4px 0 12px" }}>{err}</div>
  ) : null;

  return (
    <div className="kb-modal-scrim" onClick={onClose}>
      <div className="kb-modal" onClick={(e) => e.stopPropagation()}>
        <button className="kb-modal-x" onClick={onClose}><Icon name="x" size={18} /></button>
        <div className="kb-modal-head">
          <Connector id={frontConnectorId(connectorId)} size={52} />
          <div><h3 className="kb-modal-title">{name}</h3>
            <span className={"kb-conncard-state" + (done ? " on" : "")}>
              {done ? <><Icon name="check" size={12} /> conectado</> : forceReconnect ? "reautorizar" : "não conectado"}</span>
          </div>
        </div>

        {done ? (
          <>
            <p className="kb-modal-desc">{displayName ? `Conectado como ${displayName}.` : "Conector conectado e pronto para uso nos seus workflows."}</p>
            <div className="kb-modal-actions">
              <button className="kb-btn ghost" onClick={onClose}>Fechar</button>
              <button className="kb-btn danger-out" onClick={disconnect} disabled={busy}><Icon name="power" size={15} /> Desconectar</button>
            </div>
          </>
        ) : method === "oauth2" ? (
          (cfg && cfg.oauthAvailable) ? (
            <>
              <p className="kb-modal-desc">{forceReconnect ? `Reautorize o Kobar Flows para renovar os tokens e permissoes de ${name}.` : `Autorize o Kobar Flows a acessar sua conta ${name}. Você poderá revogar o acesso quando quiser.`}</p>
              {errBox}
              <button className="kb-btn primary full" onClick={authorize} disabled={busy}>
                <Icon name="connect" size={16} /> {busy ? "Abrindo…" : forceReconnect ? `Reautorizar ${name}` : `Autorizar com ${name}`}
              </button>
            </>
          ) : (
            <p className="kb-modal-desc">
              A conexão OAuth de {name} ainda não está configurada no servidor.
              Configure {(cfg && cfg.missingEnv && cfg.missingEnv.length) ? cfg.missingEnv.join(", ") : "as credenciais OAuth do provedor"} para habilitar.
            </p>
          )
        ) : (
          <>
            <p className="kb-modal-desc">Informe as credenciais para conectar {name}. Elas são validadas e guardadas criptografadas no seu ambiente.</p>
            <div className="kb-modal-fields">
              {fields.map((f) => (
                <label className="kb-field" key={f.k}>
                  <span className="kb-field-label">{f.label}</span>
                  <span className="kb-field-box">
                    <Icon name={f.secret ? "lock" : "edit"} size={16} className="kb-field-ic" />
                    <input type={f.secret ? "password" : (f.type === "url" ? "url" : "text")} placeholder={f.placeholder || ""}
                      value={vals[f.k] || ""} onChange={(e) => setVal(f.k, e.target.value)} autoFocus={f === fields[0]} />
                  </span>
                  {f.help && <span style={{ fontSize: 11.5, color: "#8A9384", marginTop: 4, display: "block" }}>{f.help}</span>}
                </label>
              ))}
            </div>
            {errBox}
            <button className="kb-btn primary full" onClick={submit} disabled={busy}>
              <Icon name="connect" size={16} /> {busy ? "Conectando…" : `Conectar ${name}`}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

function ConnectPage({ connectors = [], initialConnector = null, onConnected }) {
  const [q, setQ] = useStateP("");
  const [modal, setModal] = useStateP(null);
  const backendId = (id) => backendConnectorId ? backendConnectorId(id) : id;
  const uiId = (id) => frontConnectorId ? frontConnectorId(id) : id;
  const byUi = Object.fromEntries(connectors.map((item) => [uiId(item.id || item.connectorId), item]));
  const connectedIds = connectors.filter((item) => item.connected).map((item) => uiId(item.id || item.connectorId));
  const isConn = (id) => connectedIds.includes(id);
  const needsReconnect = (id) => !!(byUi[id] && byUi[id].needsReconnect);
  const supported = new Set(connectors.map((item) => uiId(item.id || item.connectorId)));
  const isSupported = (id) => supported.has(id);
  useEffectP(() => {
    if (initialConnector) {
      const id = uiId(initialConnector);
      setModal({ id, connected: isConn(id) });
    }
  }, [initialConnector]);
  return (
    <SectionShell title="Conectores" subtitle="Conecte as ferramentas que seus workflows vão usar."
      actions={<span className="kb-count big">{connectedIds.length} conectados</span>}>
      <div className="kb-toolbar">
        <div className="kb-search inline"><Icon name="search" size={15} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Buscar conectores" /></div>
      </div>
      {CONNECTOR_CATEGORIES.map((cat) => {
        const ids = cat.ids.filter((id) => CONNECTORS[id] && CONNECTORS[id].label.toLowerCase().includes(q.toLowerCase()));
        if (!ids.length) return null;
        return (
          <div key={cat.label} className="kb-conn-cat">
            <h2 className="kb-conn-catlabel">{cat.label}</h2>
            <div className="kb-conngrid">
              {ids.map((id) => (
                <div key={id} className="kb-conncard">
                  <Connector id={id} size={42} />
                  <div className="kb-conncard-txt">
                    <span className="kb-conncard-name">{CONNECTORS[id].label}</span>
                    <span className={"kb-conncard-state" + (isConn(id) ? " on" : "")}>
                      {isConn(id) ? <><Icon name="check" size={12} /> conectado</> : needsReconnect(id) ? "reconectar" : "não conectado"}
                    </span>
                  </div>
                  {isSupported(id) ? (
                    <button className={"kb-btn tiny " + (isConn(id) ? "ghost" : "primary")}
                      onClick={() => setModal({ id, forceReconnect: needsReconnect(id) })}>{isConn(id) ? "Gerenciar" : needsReconnect(id) ? "Reconectar" : "Conectar"}</button>
                  ) : (
                    <button className="kb-btn tiny ghost" disabled title="Em breve">Em breve</button>
                  )}
                </div>
              ))}
            </div>
          </div>
        );
      })}
      {modal && <ConnectorModal connectorId={backendId(modal.id)} catalog={connectors}
        returnTo="/?v=exact&view=connect" forceReconnect={modal.forceReconnect}
        onConnected={onConnected} onClose={() => setModal(null)} />}
    </SectionShell>
  );
}

// ---------------- Runs ----------------
const STEP_STATUS = { success: "ok", failed: "err", error: "err", running: "run", pending: "draft", skipped: "draft" };

function RunDetailModal({ runId, onClose }) {
  const [data, setData] = useStateP(null);
  const [err, setErr] = useStateP("");
  useEffectP(() => {
    let active = true;
    kbRuns.detail(runId).then((d) => active && setData(d)).catch((e) => active && setErr(e.message || "Falha"));
    return () => { active = false; };
  }, [runId]);
  const run = data && data.run;
  const steps = (data && data.steps) || [];
  return (
    <div className="kb-modal-scrim" onClick={onClose}>
      <div className="kb-modal kb-modal-lg" onClick={(e) => e.stopPropagation()}>
        <button className="kb-modal-x" onClick={onClose}><Icon name="x" size={18} /></button>
        <div className="kb-modal-head">
          <span className="kb-build-cta-ic"><Icon name="runs" size={18} /></span>
          <div><h3 className="kb-modal-title">Execução</h3>
            <span className="kb-modal-subtle">{runId.slice(0, 8)} · {run ? run.status : (err || "carregando…")}</span></div>
        </div>
        {!data && !err && <div className="kb-modal-connecting"><OliveLoader size={36} speed={1.5} /><p>Carregando passos…</p></div>}
        {err && <p className="kb-modal-desc" style={{ color: "#9B3D27" }}>{err}</p>}
        {run && (
          <div className="kb-modal-scroll">
            <div className="kb-set-row"><span>Status</span><StatusPill s={run.status === "failed" ? "erro" : run.status === "success" ? "sucesso" : run.status} /></div>
            <div className="kb-set-row"><span>Gatilho</span><strong>{run.trigger_type || "—"}</strong></div>
            <div className="kb-set-row"><span>Duração</span><strong>{run.duration_ms != null ? (run.duration_ms / 1000).toFixed(2) + " s" : "—"}</strong></div>
            {run.error && <div className="kb-set-row"><span>Erro</span><strong style={{ color: "#9B3D27" }}>{run.error.message || JSON.stringify(run.error)}</strong></div>}
            <h3 className="kb-card-title" style={{ marginTop: 14 }}>Passos ({steps.length})</h3>
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {steps.map((s, i) => (
                <div key={i} style={{ border: "1px solid var(--line,#E4DED5)", borderRadius: 10, padding: 10, background: "var(--surface,#FBF9F6)" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 8, justifyContent: "space-between" }}>
                    <span style={{ fontWeight: 600, fontSize: 13.5 }}><Connector id={frontConnectorId(s.connector_id)} size={20} /> {s.operation}</span>
                    <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <StatusPill s={s.status === "failed" ? "erro" : "sucesso"} />
                      <span className="kb-td-mono" style={{ fontSize: 12, color: "#8A9384" }}>{s.duration_ms != null ? s.duration_ms + "ms" : ""}</span>
                    </span>
                  </div>
                  {s.error && <div style={{ fontSize: 12.5, color: "#9B3D27", marginTop: 6 }}>{s.error.message || JSON.stringify(s.error)}</div>}
                  {s.output && <pre style={{ fontSize: 11.5, color: "#6B7568", marginTop: 6, maxHeight: 90, overflow: "auto", background: "#F2EFEA", padding: 8, borderRadius: 8 }}>{JSON.stringify(s.output, null, 1).slice(0, 600)}</pre>}
                </div>
              ))}
              {steps.length === 0 && <p className="kb-set-muted">Sem passos registrados.</p>}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function RunsPage({ runs = [] }) {
  const [f, setF] = useStateP("todos");
  const [openRun, setOpenRun] = useStateP(null);
  const list = runs.filter((r) => f === "todos" || (STATUS[r.status] && STATUS[r.status].cls === f));
  const isUuid = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(String(id || ""));
  return (
    <SectionShell title="Execuções" subtitle="Histórico de execuções dos seus workflows. Clique para ver os passos.">
      <div className="kb-toolbar">
        <div className="kb-filterpills">
          {[["todos", "Todos"], ["ok", "Sucesso"], ["err", "Erro"], ["run", "Em execução"]].map(([k, l]) => (
            <button key={k} className={"kb-pill" + (f === k ? " active" : "")} onClick={() => setF(k)}>{l}</button>
          ))}
        </div>
      </div>
      {runs.length === 0 && (
        <div className="kb-empty-chats">Nenhuma execução ainda. Publique e rode um workflow para ver o histórico aqui.</div>
      )}
      <div className="kb-table">
        <div className="kb-tr kb-th"><span>Workflow</span><span>Gatilho</span><span>Status</span><span>Duração</span><span>Quando</span></div>
        {list.map((r) => (
          <div key={r.id} className={"kb-tr" + (isUuid(r.id) ? " clickable" : "")}
            style={isUuid(r.id) ? { cursor: "pointer" } : null}
            onClick={() => isUuid(r.id) && setOpenRun(r.id)}>
            <span className="kb-td-flow"><Icon name="flows" size={15} /> {r.flow}</span>
            <span><Connector id={r.trigger} size={22} /></span>
            <span><StatusPill s={r.status} /></span>
            <span className="kb-td-mono">{r.dur}</span>
            <span className="kb-td-muted">{r.when}</span>
          </div>
        ))}
      </div>
      {openRun && <RunDetailModal runId={openRun} onClose={() => setOpenRun(null)} />}
    </SectionShell>
  );
}

// ---------------- Agents ----------------
const ALL_MODELS = MODEL_CATALOG.flatMap((m) => m.models);

function AgentSkillsPicker({ selected, onToggle }) {
  return (
    <div className="kb-skillpick">
      {SKILLS_CATALOG.map((s) => {
        const on = selected.includes(s.id);
        return (
          <button key={s.id} className={"kb-skillopt" + (on ? " on" : "")} onClick={() => onToggle(s.id)}>
            <span className="kb-skillopt-ic"><Icon name={s.icon} size={17} /></span>
            <span className="kb-skillopt-txt"><span className="kb-skillopt-label">{s.label}</span><span className="kb-skillopt-desc">{s.desc}</span></span>
            <span className="kb-skillopt-check">{on ? <Icon name="check" size={14} /> : <Icon name="plus" size={14} />}</span>
          </button>
        );
      })}
    </div>
  );
}

function AgentEditorModal({ agent, onClose, onSubmit }) {
  const editing = !!agent;
  const [name, setName] = useStateP(agent ? agent.name : "");
  const [desc, setDesc] = useStateP(agent ? agent.desc : "");
  const [model, setModel] = useStateP(agent ? agent.model : ALL_MODELS[0]);
  const [skills, setSkills] = useStateP(agent ? [...(agent.skillIds || agent.skills || [])] : ["build_flow"]);
  const [instr, setInstr] = useStateP(agent ? (agent.instructions || "") : "");
  const toggle = (id) => setSkills((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]);
  const valid = name.trim() && skills.length;
  return (
    <div className="kb-modal-scrim" onClick={onClose}>
      <div className="kb-modal kb-modal-lg" onClick={(e) => e.stopPropagation()}>
        <button className="kb-modal-x" onClick={onClose}><Icon name="x" size={18} /></button>
        <div className="kb-modal-head">
          <span className="kb-agentcard-ava"><OliveMark size={34} /></span>
          <div><h3 className="kb-modal-title">{editing ? "Editar agente" : "Novo agente"}</h3><span className="kb-modal-subtle">Defina o comportamento e as habilidades</span></div>
        </div>
        <div className="kb-modal-scroll">
          <div className="kb-modal-fields">
            <label className="kb-field"><span className="kb-field-label">Nome do agente</span>
              <span className="kb-field-box"><Icon name="agents" size={16} className="kb-field-ic" /><input value={name} onChange={(e) => setName(e.target.value)} placeholder="Ex.: Assistente de vendas" autoFocus /></span></label>
            <label className="kb-field"><span className="kb-field-label">Descrição</span>
              <span className="kb-field-box"><Icon name="edit" size={16} className="kb-field-ic" /><input value={desc} onChange={(e) => setDesc(e.target.value)} placeholder="O que esse agente faz" /></span></label>
            <div className="kb-field"><span className="kb-field-label">Modelo de IA</span>
              <ModelSelect models={ALL_MODELS} value={model} onChange={setModel} /></div>
          </div>
          <div className="kb-modal-sec">
            <div className="kb-modal-sec-head"><span className="kb-field-label">Habilidades (skills)</span><span className="kb-skillcount">{skills.length} selecionadas</span></div>
            <AgentSkillsPicker selected={skills} onToggle={toggle} />
          </div>
          <div className="kb-modal-sec">
            <span className="kb-field-label">Instruções</span>
            <textarea className="kb-textarea" value={instr} onChange={(e) => setInstr(e.target.value)} rows={3}
              placeholder="Descreva como o agente deve se comportar e responder." />
          </div>
        </div>
        <div className="kb-modal-actions">
          <button className="kb-btn ghost" onClick={onClose}>Cancelar</button>
          <button className={"kb-btn primary" + (valid ? "" : " disabled")}
            onClick={() => valid && onSubmit({ name: name.trim(), desc: desc.trim() || "Agente personalizado.", model, skillIds: skills, instructions: instr.trim() })}>
            <Icon name="check" size={15} /> {editing ? "Salvar alterações" : "Criar agente"}
          </button>
        </div>
      </div>
    </div>
  );
}

function AgentDetail({ agent, onBack, onToggleStatus, onEdit, onDelete }) {
  // Skills podem vir como ids (catalogo) ou como labels (dados reais do banco).
  const rawSkills = agent.skillIds || agent.skills || [];
  const skills = rawSkills.map((label, i) => {
        const found = SKILLS_CATALOG.find((s) => s.id === label || s.label === label);
        return found || { id: "sk" + i, label, desc: "", icon: "skills" };
      });
  return (
    <SectionShell eyebrow="AGENTE"
      title={agent.name}
      subtitle={agent.desc}
      actions={<>
        <button className="kb-btn ghost" onClick={onBack}><Icon name="chevLeft" size={15} /> Voltar</button>
        <button className="kb-btn ghost" onClick={() => onToggleStatus(agent.id)}>
          <Icon name="power" size={15} /> {agent.status === "ativo" ? "Pausar" : "Ativar"}
        </button>
        <button className="kb-btn ghost" onClick={() => onDelete(agent)}><Icon name="trash" size={15} /> Excluir</button>
        <button className="kb-btn primary" onClick={() => onEdit(agent)}><Icon name="edit" size={15} /> Editar</button>
      </>}>
      <div className="kb-agentdetail">
        <div className="kb-card">
          <h3 className="kb-card-title">Visão geral</h3>
          <div className="kb-set-row"><span>Status</span><StatusPill s={agent.status} /></div>
          <div className="kb-set-row"><span>Modelo</span><strong>{agent.model}</strong></div>
          <div className="kb-set-row"><span>Habilidades</span><strong>{skills.length}</strong></div>
        </div>
        <div className="kb-card">
          <h3 className="kb-card-title">Instruções</h3>
          <p className="kb-agent-instr">{agent.instructions || "Sem instruções definidas."}</p>
        </div>
        <div className="kb-card kb-card-wide">
          <h3 className="kb-card-title">Habilidades</h3>
          <div className="kb-skilllist">
            {skills.map((s) => (
              <div key={s.id} className="kb-skillchip">
                <span className="kb-skillchip-ic"><Icon name={s.icon} size={16} /></span>
                <div className="kb-skillchip-txt"><span>{s.label}</span><em>{s.desc}</em></div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </SectionShell>
  );
}

function AgentsPage({ initialAgents = [] }) {
  const [agents, setAgents] = useStateP(initialAgents);
  const [creating, setCreating] = useStateP(false);
  const [editId, setEditId] = useStateP(null);
  const [openId, setOpenId] = useStateP(null);
  const [busy, setBusy] = useStateP("");
  useEffectP(() => setAgents(initialAgents), [JSON.stringify(initialAgents)]);
  useEffectP(() => {
    let active = true;
    kbAgents.list().then((data) => { if (active) setAgents(data.agents || []); }).catch(() => {});
    return () => { active = false; };
  }, []);

  const toPayload = (data) => ({
    name: data.name,
    desc: data.desc,
    model: data.model,
    status: data.status || "ativo",
    skills: data.skillIds || data.skills || [],
    instructions: data.instructions || ""
  });

  const create = async (data) => {
    setBusy("create");
    try {
      const res = await kbAgents.create(toPayload(data));
      setAgents((a) => [res.agent, ...a]);
      setCreating(false);
    } catch (err) {
      window.alert((err && err.message) || "Falha ao criar agent.");
    } finally {
      setBusy("");
    }
  };
  const saveEdit = async (data) => {
    if (!editId) return;
    setBusy(editId);
    try {
      const res = await kbAgents.update(editId, toPayload(data));
      setAgents((a) => a.map((x) => x.id === editId ? res.agent : x));
      setEditId(null);
    } catch (err) {
      window.alert((err && err.message) || "Falha ao salvar agent.");
    } finally {
      setBusy("");
    }
  };
  const toggleStatus = async (id) => {
    const agent = agents.find((a) => a.id === id);
    if (!agent) return;
    const status = agent.status === "ativo" ? "pausado" : "ativo";
    setBusy(id);
    try {
      const res = await kbAgents.update(id, { status });
      setAgents((a) => a.map((x) => x.id === id ? res.agent : x));
    } catch (err) {
      window.alert((err && err.message) || "Falha ao alterar status.");
    } finally {
      setBusy("");
    }
  };
  const deleteAgent = async (agent) => {
    if (!window.confirm(`Excluir o agent "${agent.name}"?`)) return;
    setBusy(agent.id);
    try {
      await kbAgents.remove(agent.id);
      setAgents((a) => a.filter((x) => x.id !== agent.id));
      setOpenId(null);
    } catch (err) {
      window.alert((err && err.message) || "Falha ao excluir agent.");
    } finally {
      setBusy("");
    }
  };
  const editingAgent = editId ? agents.find((a) => a.id === editId) : null;

  if (openId) {
    const agent = agents.find((a) => a.id === openId);
    if (agent) return (
      <>
        <AgentDetail agent={agent} onBack={() => setOpenId(null)} onToggleStatus={toggleStatus} onEdit={() => setEditId(agent.id)} onDelete={deleteAgent} />
        {editingAgent && <AgentEditorModal agent={editingAgent} onClose={() => setEditId(null)} onSubmit={saveEdit} />}
      </>
    );
  }

  return (
    <SectionShell title="Agentes" subtitle="Agentes de IA com habilidades e modelos próprios."
      actions={<button className="kb-btn primary" onClick={() => setCreating(true)}><Icon name="plus" size={16} stroke={2.2} /> Novo agente</button>}>
      <div className="kb-agentgrid">
        {agents.map((a) => (
          <button key={a.id} className="kb-agentcard" onClick={() => setOpenId(a.id)}>
            <span className="kb-agentcard-ava"><OliveMark size={34} /></span>
            <div className="kb-agentcard-body">
              <div className="kb-agentcard-top"><span className="kb-agentcard-name">{a.name}</span><StatusPill s={a.status} /></div>
              <p className="kb-agentcard-desc">{a.desc}</p>
              <div className="kb-agentcard-meta">
                <span><Icon name="models" size={13} /> {a.model}</span>
                <span><Icon name="skills" size={13} /> {((a.skillIds || a.skills) || []).length} skills</span>
              </div>
            </div>
            <Icon name="chevRight" size={18} className="kb-agentcard-go" />
          </button>
        ))}
      </div>
      {creating && <AgentEditorModal onClose={() => setCreating(false)} onSubmit={create} />}
    </SectionShell>
  );
}

// ---------------- History ----------------
function HistoryPage() {
  return (
    <SectionShell title="Histórico" subtitle="Atividade recente na sua conta.">
      <div className="kb-timeline">
        {HISTORY.map((h) => (
          <div key={h.id} className="kb-tl-item">
            <span className={"kb-tl-ic" + (h.bad ? " bad" : "")}><Icon name={h.icon} size={16} /></span>
            <span className="kb-tl-text">{h.text}</span>
            <span className="kb-tl-when">{h.when}</span>
          </div>
        ))}
      </div>
    </SectionShell>
  );
}

// ---------------- Settings ----------------
const USAGE_LABELS = {
  ai_calls: "Chamadas de IA", chat_messages: "Mensagens de chat",
  web_searches: "Buscas online", workflow_runs: "Execuções"
};

function UsageBar({ label, used, limit }) {
  const unlimited = limit == null || limit < 0;
  const pct = unlimited ? 0 : Math.min(100, Math.round((used / Math.max(1, limit)) * 100));
  const color = pct >= 100 ? "#B0462F" : pct >= 80 ? "#C9842B" : "var(--accent, #B8734A)";
  return (
    <div style={{ marginBottom: 12 }}>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13, color: "#6B7568", marginBottom: 5 }}>
        <span>{label}</span>
        <strong style={{ color: "#384636" }}>{used}{unlimited ? "" : " / " + limit}</strong>
      </div>
      <div style={{ height: 7, borderRadius: 4, background: "#EDE7DD", overflow: "hidden" }}>
        <div style={{ width: (unlimited ? 6 : pct) + "%", height: "100%", background: unlimited ? "#CFE0CB" : color, transition: ".3s" }} />
      </div>
    </div>
  );
}

function SettingsPage({ provider, onUpgrade, user, isAdmin, onModels }) {
  const planName = (user && user.plan) || "Free";
  const current = PLANS.find((p) => p.name.toLowerCase() === planName.toLowerCase()) || PLANS[0];
  const [m, setM] = useStateP(null);
  const [loading, setLoading] = useStateP(true);
  useEffectP(() => {
    let active = true;
    kbMetrics.org(30).then((data) => { if (active) { setM(data); setLoading(false); } }).catch(() => active && setLoading(false));
    return () => { active = false; };
  }, []);
  const usageMetrics = ["ai_calls", "chat_messages", "web_searches", "workflow_runs"];
  const alerts = (m && m.alerts) || [];
  return (
    <SectionShell title="Configurações" subtitle="Conta, plano, uso e preferências.">
      {alerts.length > 0 && (
        <div style={{ marginBottom: 16, display: "flex", flexDirection: "column", gap: 8 }}>
          {alerts.map((a, i) => (
            <div key={i} style={{
              display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: 10, fontSize: 13.5,
              border: "1px solid " + (a.level === "critical" ? "#E7C3B8" : "#EAD9B6"),
              background: a.level === "critical" ? "#FBEFEB" : "#FBF6EA",
              color: a.level === "critical" ? "#9B3D27" : "#8A5A12"
            }}>
              <Icon name={a.level === "critical" ? "shield" : "spark"} size={15} /> {a.message}
            </div>
          ))}
        </div>
      )}
      <div className="kb-set-grid">
        <div className="kb-card">
          <h3 className="kb-card-title">Perfil</h3>
          <div className="kb-set-row"><span>Nome</span><strong>{(user && user.name) || "—"}</strong></div>
          <div className="kb-set-row"><span>E-mail</span><strong>{(user && user.email) || "—"}</strong></div>
          <div className="kb-set-row"><span>Organização</span><strong>{(user && user.org) || "—"}</strong></div>
          <div className="kb-set-row"><span>Permissão</span><strong>{isAdmin ? "Administrador" : "Usuário"}</strong></div>
        </div>
        <div className="kb-card kb-plan-card">
          <h3 className="kb-card-title">Plano atual</h3>
          <div className="kb-plan-now">
            <span className="kb-plan-now-name">{current.name}</span>
            <span className="kb-plan-now-price">{current.price}<em>{current.per}</em></span>
          </div>
          <p className="kb-set-muted">{m && m.subscriptionStatus && m.subscriptionStatus !== "none" ? "Assinatura: " + m.subscriptionStatus : "Sem assinatura ativa"}</p>
          <button className="kb-btn primary full" onClick={onUpgrade}><Icon name="spark" size={15} /> Ver planos</button>
        </div>
        <div className="kb-card kb-card-wide">
          <h3 className="kb-card-title">Uso este mês</h3>
          {loading ? <p className="kb-set-muted">Carregando…</p> : !m ? <p className="kb-set-muted">Indisponível.</p> : (
            <>
              {usageMetrics.map((k) => <UsageBar key={k} label={USAGE_LABELS[k] || k} used={(m.usage && m.usage[k]) || 0} limit={m.limits && m.limits[k]} />)}
              {m.runs && (
                <p className="kb-set-muted" style={{ marginTop: 6 }}>
                  Execuções: {m.runs.total} no total · {m.runs.error} com erro · taxa de erro {Math.round((m.runs.errorRate || 0) * 100)}%
                </p>
              )}
            </>
          )}
        </div>
        <div className="kb-card">
          <h3 className="kb-card-title">Preferências</h3>
          <div className="kb-set-row"><span>Idioma</span><strong>Português (BR)</strong></div>
          <div className="kb-set-row"><span>Provedor padrão</span><strong>{PROVIDERS[provider].label} · {providerModel(provider, null)}</strong></div>
          <div className="kb-set-row"><span>Tema</span><strong>Claro</strong></div>
          {isAdmin && <button className="kb-btn primary full" style={{ marginTop: 12 }} onClick={onModels}><Icon name="models" size={15} /> Gerenciar modelos dos usuários</button>}
        </div>
      </div>
    </SectionShell>
  );
}

// ---------------- Upgrade / Plans ----------------
function UpgradePage({ onBack }) {
  const [credits, setCredits] = useStateP(null);
  const [packs, setPacks] = useStateP([]);
  const [busy, setBusy] = useStateP("");
  const [err, setErr] = useStateP("");

  useEffectP(() => {
    let alive = true;
    kbBilling.credits().then((d) => {
      if (!alive) return;
      setCredits(d.balance || null);
      setPacks(d.packs || []);
    }).catch(() => {});
    return () => { alive = false; };
  }, []);

  const go = (urlPromise, key) => {
    setErr(""); setBusy(key);
    urlPromise
      .then((data) => { if (data && data.url) (window.top || window).location.href = data.url; else setBusy(""); })
      .catch((e) => { setErr(e.message || "Falha ao processar."); setBusy(""); });
  };
  const onPlan = (planId) => go(kbBilling.checkout(planId), "plan-" + planId);
  const onBuy = (packId) => go(kbBilling.buyCredits(packId), "pack-" + packId);

  const totalCredits = credits && credits.total != null ? credits.total : null;
  const fmt = (n) => (n < 0 ? "ilimitado" : Number(n).toLocaleString("pt-BR"));

  return (
    <SectionShell eyebrow="PLANOS" title="Escolha o plano ideal" subtitle="Comece de graça e evolua conforme suas automações crescem."
      actions={<button className="kb-btn ghost" onClick={onBack}><Icon name="chevLeft" size={15} /> Voltar</button>}>
      {err && <div className="kb-banner err" style={{ marginBottom: 14 }}>{err}</div>}
      <div className="kb-plans">
        {PLANS.map((p) => (
          <div key={p.id} className={"kb-plan" + (p.popular ? " popular" : "") + (p.current ? " current" : "")}>
            {p.popular && <span className="kb-plan-badge">Mais popular</span>}
            <div className="kb-plan-head">
              <span className="kb-plan-name">{p.name}</span>
              <span className="kb-plan-tagline">{p.tagline}</span>
            </div>
            <div className="kb-plan-price">{p.price}<em>{p.per}</em></div>
            <ul className="kb-plan-feats">
              {p.features.map((f, i) => <li key={i}><Icon name="check" size={15} /> {f}</li>)}
            </ul>
            <button className={"kb-btn full " + (p.current ? "ghost" : p.popular ? "primary" : "outline")}
              disabled={p.current || busy === "plan-" + p.id || p.id === "free"}
              onClick={() => p.id !== "free" && onPlan(p.id)}>
              {p.current ? "Plano atual" : p.id === "free" ? "Grátis" : (busy === "plan-" + p.id ? "Redirecionando…" : "Fazer upgrade")}
            </button>
          </div>
        ))}
      </div>

      {/* Créditos de IA na nuvem */}
      <div className="kb-credits">
        <div className="kb-credits-head">
          <div>
            <h3 className="kb-credits-title"><Icon name="spark" size={17} /> Créditos de IA na nuvem</h3>
            <p className="kb-credits-sub">Modelos locais (Ollama) são ilimitados e não consomem créditos. Modelos na nuvem (OpenAI, Claude, Gemini, DeepSeek) consomem créditos pelo uso real.</p>
          </div>
          <div className="kb-credits-balance">
            <span className="kb-credits-num">{totalCredits == null ? "—" : fmt(totalCredits)}</span>
            <span className="kb-credits-lbl">créditos disponíveis</span>
            {credits && credits.monthly != null && credits.monthly >= 0 && (
              <span className="kb-credits-split">{fmt(credits.monthly)} do plano · {fmt(credits.wallet || 0)} comprados</span>
            )}
          </div>
        </div>
        <div className="kb-credits-packs">
          {packs.map((pk) => (
            <div key={pk.id} className="kb-pack">
              <span className="kb-pack-credits">{Number(pk.credits).toLocaleString("pt-BR")} créditos</span>
              {pk.bonus > 0 && <span className="kb-pack-bonus">+{pk.bonus}% bônus</span>}
              <span className="kb-pack-price">R$ {pk.amountBRL}</span>
              <button className="kb-btn outline full" disabled={busy === "pack-" + pk.id} onClick={() => onBuy(pk.id)}>
                {busy === "pack-" + pk.id ? "Redirecionando…" : "Comprar"}
              </button>
            </div>
          ))}
        </div>
      </div>
    </SectionShell>
  );
}

Object.assign(window, {
  StatusPill, SectionShell, FlowsPage, ConnectPage, RunsPage,
  ModelsPage, AgentsPage, HistoryPage, SettingsPage, UpgradePage,
});

/* ===== models-admin.jsx ===== */
// models-admin.jsx — catalogo dinamico da API + preview por usuario (admin)
function normalizeModelEntries(models) {
  return (models || []).map((m) => {
    if (typeof m === "string") return { id: m, name: m };
    if (!m || !m.id) return null;
    return {
      id: m.id,
      name: m.name || m.id,
      supportsThinking: !!m.supportsThinking,
      supportsWebSearch: !!m.supportsWebSearch,
      supportsVision: !!m.supportsVision
    };
  }).filter(Boolean);
}

function mapProviderFromApi(p) {
  const providerId = p.id || p.provider;
  const entries = normalizeModelEntries(p.models);
  const ids = entries.map((m) => m.id);
  const connected = p.connected != null ? p.connected : (p.status === "connected" || p.hasApiKey === true);
  return {
    provider: providerId,
    label: p.label,
    kind: p.kind,
    short: p.short,
    color: p.color,
    connected,
    status: p.status || (connected ? "connected" : "disconnected"),
    hasApiKey: !!p.hasApiKey,
    apiKeyPreview: p.apiKeyPreview || null,
    defaultModel: p.defaultModel || ids[0] || "",
    enabledModels: Array.isArray(p.enabledModels) ? p.enabledModels : [],
    isDefault: !!p.isDefault,
    models: ids,
    modelEntries: entries,
    webSearchModels: entries.filter((m) => m.supportsWebSearch).map((m) => m.id),
    thinkingModels: entries.filter((m) => m.supportsThinking).map((m) => m.id)
  };
}

function modelDisplayLabel(entries, modelId) {
  const hit = (entries || []).find((m) => m.id === modelId);
  if (!hit) return modelId;
  return hit.name && hit.name !== hit.id ? `${hit.name} (${hit.id})` : hit.id;
}

function chatVisibleModels(row) {
  if (!row || (!row.connected && row.status !== "connected")) return [];
  const entries = row.modelEntries || (row.models || []).map((id) => ({ id, name: id }));
  const enabledIds = row.enabledModels && row.enabledModels.length ? row.enabledModels : row.models || [];
  return entries.filter((m) => enabledIds.includes(m.id));
}

function ModelSelect({ models, modelEntries, value, onChange, disabled }) {
  const [open, setOpen] = useStateP(false);
  const ref = React.useRef(null);
  const options = (models || []).map((mod) => (typeof mod === "string" ? mod : mod.id)).filter(Boolean);
  useEffectP(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [open]);
  return (
    <div className={"kb-mselect" + (disabled ? " disabled" : "")} ref={ref}>
      <button className="kb-mselect-trigger" onClick={() => !disabled && setOpen((o) => !o)}>
        <span className="kb-mselect-val">{modelDisplayLabel(modelEntries, value) || value || "Selecione"}</span>
        <Icon name="chevDown" size={15} className={"kb-mselect-chev" + (open ? " up" : "")} />
      </button>
      {open && (
        <div className="kb-mselect-menu" style={{ maxHeight: 300, overflowY: "auto" }}>
          {options.map((mod) => (
            <button key={mod} className={"kb-mselect-opt" + (mod === value ? " active" : "")}
              onClick={() => { onChange(mod); setOpen(false); }}>
              <span>{modelDisplayLabel(modelEntries, mod)}</span>
              {mod === value && <Icon name="check" size={15} />}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

function ModelsPage({ provider, onProvider, modelChoice, onModelChoice, catalog, isAdmin, onCatalogChange, onRefresh, orgMembers, currentUserId }) {
  const [list, setList] = useStateP([]);
  const [expanded, setExpanded] = useStateP(null);
  const [keys, setKeys] = useStateP({});
  const [busy, setBusy] = useStateP("");
  const [notice, setNotice] = useStateP("");
  const [selectedUserId, setSelectedUserId] = useStateP("");
  const [loading, setLoading] = useStateP(false);
  useEffectP(() => { if (catalog && catalog.length) setList(catalog); }, [catalog]);
  useEffectP(() => {
    if (!catalog || !catalog.length) {
      setLoading(true);
      (onRefresh ? onRefresh() : Promise.resolve([])).finally(() => setLoading(false));
    }
  }, []);

  const setProviderRow = (providerId, patch) => {
    setList((rows) => {
      const next = rows.map((row) => row.provider === providerId ? { ...row, ...patch } : row);
      onCatalogChange && onCatalogChange(next);
      return next;
    });
  };
  const callModels = async (providerId, body, message) => {
    setBusy(providerId + ":" + body.action);
    setNotice("");
    try {
      const res = await kbApi.authFetch("/api/models", { method: "POST", body: JSON.stringify({ provider: providerId, ...body }) });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || "Falha ao salvar configuracao.");
      setNotice(message || "Configuracao salva.");
      return data;
    } catch (e) {
      setNotice(e.message || "Falha ao salvar configuracao.");
      return null;
    } finally {
      setBusy("");
    }
  };
  const saveKey = async (row) => {
    const value = (keys[row.provider] || "").trim();
    if (!value) { setNotice("Informe a chave/API URL antes de salvar."); return; }
    const data = await callModels(row.provider, { action: "save_key", apiKey: value, defaultModel: row.defaultModel || row.models[0] }, "Provedor conectado.");
    if (data) {
      setKeys((s) => ({ ...s, [row.provider]: "" }));
      setProviderRow(row.provider, {
        connected: true,
        status: "connected",
        hasApiKey: true,
        defaultModel: row.defaultModel || row.models[0],
        apiKeyPreview: data.apiKeyPreview || row.apiKeyPreview || null
      });
      if (onRefresh) onRefresh();
    }
  };
  const removeKey = async (row) => {
    const data = await callModels(row.provider, { action: "remove_key" }, "Provedor desconectado.");
    if (data) setProviderRow(row.provider, { connected: false, status: "disconnected", hasApiKey: false, apiKeyPreview: null });
  };
  const testKey = async (row) => {
    setBusy(row.provider + ":test");
    setNotice("");
    try {
      const payload = row.provider === "ollama"
        ? { provider: row.provider, baseUrl: (keys[row.provider] || "").trim() }
        : { provider: row.provider, apiKey: (keys[row.provider] || "").trim() };
      const res = await kbApi.authFetch("/api/models/test", { method: "POST", body: JSON.stringify(payload) });
      const data = await res.json().catch(() => ({}));
      if (data.ok && Array.isArray(data.models) && data.models.length) {
        const entries = normalizeModelEntries(data.models);
        setProviderRow(row.provider, {
          models: data.models,
          modelEntries: entries,
          defaultModel: row.defaultModel && data.models.includes(row.defaultModel) ? row.defaultModel : data.models[0]
        });
      }
      setNotice(data.ok ? `Conexao OK. ${data.models ? data.models.length : 0} modelos encontrados.` : (data.error || "Teste falhou."));
    } catch (e) {
      setNotice(e.message || "Teste falhou.");
    } finally {
      setBusy("");
    }
  };
  const setDefaultModel = async (row, modelId) => {
    const data = await callModels(row.provider, { action: "update_config", defaultModel: modelId }, "Modelo padrao atualizado.");
    if (data) {
      setProviderRow(row.provider, { defaultModel: modelId });
      onModelChoice && onModelChoice(row.provider, modelId);
    }
  };
  const toggleModel = async (row, modelId) => {
    const current = row.enabledModels || [];
    const allEnabled = current.length === 0;
    const base = allEnabled ? (row.models || []) : current;
    let next = base.includes(modelId) ? base.filter((id) => id !== modelId) : [...base, modelId];
    if (!next.length) next = [modelId];
    const data = await callModels(row.provider, { action: "update_config", enabledModels: next }, "Modelos disponiveis atualizados.");
    if (data) setProviderRow(row.provider, { enabledModels: next });
  };

  const previewMember = (orgMembers || []).find((member) => member.id === selectedUserId) || null;
  const previewModels = list
    .filter((row) => row.connected || row.status === "connected")
    .flatMap((row) => chatVisibleModels(row).map((entry) => ({
      id: entry.id,
      label: modelDisplayLabel(row.modelEntries, entry.id),
      provider: row.provider
    })));

  return (
    <SectionShell title={isAdmin ? "Modelos — Administracao" : "Modelos"} subtitle={isAdmin ? "Conecte provedores e escolha quais modelos ficam disponiveis para os usuarios." : "Escolha o modelo de cada provedor — ele aparece no seletor do chat."}
      actions={notice ? <span className="kb-count big">{notice}</span> : <span className="kb-count big">{loading ? "Carregando..." : `${list.length} provedores`}</span>}>
      {isAdmin && (
        <div className="kb-model-admin-bar" style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 16, flexWrap: "wrap" }}>
          <label className="kb-field" style={{ minWidth: 280, flex: "1 1 280px" }}>
            <span className="kb-field-label">Visualizar como usuario</span>
            <span className="kb-field-box">
              <select value={selectedUserId} onChange={(e) => setSelectedUserId(e.target.value)} style={{ width: "100%", border: 0, background: "transparent", padding: "10px 12px" }}>
                <option value="">Configuracao da organizacao (todos)</option>
                {(orgMembers || []).map((member) => (
                  <option key={member.id} value={member.id}>
                    {member.name}{member.id === currentUserId ? " (voce)" : ""} · {member.email}
                  </option>
                ))}
              </select>
            </span>
          </label>
          {previewMember && (
            <div className="kb-count big" style={{ alignSelf: "flex-end" }}>
              {previewModels.length} modelos visiveis para {previewMember.name}
            </div>
          )}
        </div>
      )}
      <div className="kb-modelgrid">
        {list.map((m) => {
          const p = PROVIDERS[m.provider] || { label: m.label || m.provider, kind: m.kind || "cloud", placeholder: "API key" };
          const models = m.models || [];
          const entries = m.modelEntries || models.map((id) => ({ id, name: id }));
          const selected = (modelChoice && modelChoice[m.provider]) || m.defaultModel || models[0];
          const isDefaultProvider = provider === m.provider;
          const open = expanded === m.provider;
          const enabled = m.enabledModels && m.enabledModels.length ? m.enabledModels : models;
          const visibleToUser = chatVisibleModels(m);
          return (
            <div key={m.provider} className={"kb-modelcard" + (isDefaultProvider ? " is-default" : "")}>
              <div className="kb-modelcard-head">
                <span className="kb-modelcard-ic"><ProviderMark id={m.provider} size={22} /></span>
                <div className="kb-modelcard-txt">
                  <span className="kb-modelcard-name">{p.label || m.label || m.provider}</span>
                  <span className={"kb-prov-tag " + (p.kind || m.kind)}>{models.length} modelos · {(p.kind || m.kind) === "local" ? "no dispositivo" : "Cloud API"}</span>
                </div>
                <span className={"kb-conncard-state" + (m.connected ? " on" : "")}>{m.connected ? <><Icon name="check" size={12} /> ativo</> : "configurar"}</span>
              </div>

              <div className="kb-modelcard-field">
                <label className="kb-modelcard-flabel">Modelo padrao do provedor</label>
                <ModelSelect models={models} modelEntries={entries} value={selected} disabled={!m.connected || models.length === 0}
                  onChange={(mod) => isAdmin ? setDefaultModel(m, mod) : onModelChoice(m.provider, mod)} />
              </div>

              {isAdmin && previewMember && m.connected && (
                <div className="kb-modelcard-field">
                  <label className="kb-modelcard-flabel">Visivel para {previewMember.name}</label>
                  <div style={{ fontSize: 12.5, color: "var(--ink-soft,#6B6560)", lineHeight: 1.5 }}>
                    {visibleToUser.length
                      ? visibleToUser.map((entry) => modelDisplayLabel(entries, entry.id)).join(" · ")
                      : "Nenhum modelo liberado neste provedor."}
                  </div>
                </div>
              )}

              {isAdmin && open && (
                <div style={{ borderTop: "1px solid var(--line,#E4DED5)", marginTop: 12, paddingTop: 12, display: "flex", flexDirection: "column", gap: 12 }}>
                  {m.apiKeyPreview && (
                    <div className="kb-set-row"><span>Chave salva</span><strong title="Preview mascarado">{m.apiKeyPreview}</strong></div>
                  )}
                  <label className="kb-field">
                    <span className="kb-field-label">{m.provider === "ollama" ? "URL/API key do Ollama" : "Chave de API"}</span>
                    <span className="kb-field-box">
                      <Icon name="lock" size={16} className="kb-field-ic" />
                      <input type="password" placeholder={p.placeholder || "API key"} value={keys[m.provider] || ""} onChange={(e) => setKeys((s) => ({ ...s, [m.provider]: e.target.value }))} />
                    </span>
                  </label>
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                    <button className="kb-btn tiny ghost" onClick={() => testKey(m)} disabled={busy === m.provider + ":test"}>{busy === m.provider + ":test" ? "Testando..." : "Testar"}</button>
                    <button className="kb-btn tiny primary" onClick={() => saveKey(m)} disabled={busy === m.provider + ":save_key"}>{busy === m.provider + ":save_key" ? "Salvando..." : "Salvar conexao"}</button>
                    {m.connected && <button className="kb-btn tiny danger-out" onClick={() => removeKey(m)} disabled={busy === m.provider + ":remove_key"}>Desconectar</button>}
                  </div>
                  <div>
                    <label className="kb-modelcard-flabel">Modelos liberados para usuarios</label>
                    <div style={{ display: "flex", flexDirection: "column", gap: 6, maxHeight: 260, overflow: "auto", paddingRight: 4 }}>
                      {models.map((modelId) => {
                        const on = enabled.includes(modelId);
                        return (
                          <button key={modelId} className={"kb-mselect-opt" + (on ? " active" : "")}
                            style={{ border: "1px solid var(--line,#E4DED5)", borderRadius: 8 }}
                            onClick={() => toggleModel(m, modelId)}>
                            <span>{modelDisplayLabel(entries, modelId)}</span>
                            {on ? <Icon name="check" size={15} /> : <Icon name="plus" size={15} />}
                          </button>
                        );
                      })}
                    </div>
                  </div>
                </div>
              )}

              <div className="kb-modelcard-foot">
                {isDefaultProvider ? (
                  <span className="kb-modelcard-default"><Icon name="check" size={14} /> Provedor padrao</span>
                ) : m.connected ? (
                  <button className="kb-btn tiny ghost" onClick={() => onProvider(m.provider)}>Definir como padrao do chat</button>
                ) : <span />}
                {isAdmin ? (
                  <button className={"kb-btn tiny " + (open ? "primary" : "ghost")} onClick={() => setExpanded(open ? null : m.provider)}>
                    {open ? "Fechar" : (m.connected ? "Gerenciar" : "Conectar")}
                  </button>
                ) : (
                  <button className={"kb-btn tiny " + (m.connected ? "ghost" : "primary")} disabled={!m.connected}>
                    {m.connected ? "Disponivel" : "Indisponivel"}
                  </button>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </SectionShell>
  );
}

Object.assign(window, { mapProviderFromApi, ModelsPage, ModelSelect });

/* ===== app.jsx ===== */
// app.jsx — top-level state + routing across all sections
const { useState: useStateA, useRef: useRefA, useEffect: useEffectA } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "homeLayout": "A",
  "accent": "#B8734A",
  "loaderSpeed": 1.3,
  "showTexture": true
}/*EDITMODE-END*/;

// ---- mapeamento de dados reais (bootstrap) para o formato das telas ----
const STATUS_FLOW = { published: "ativo", paused: "pausado", draft: "rascunho", archived: "rascunho" };
// conectores do backend -> ids de icone do front
const CONNECTOR_ALIAS = {
  http_request: "http", google_sheets_append: "sheets", slack_webhook: "slack",
  gmail: "gmail", notion: "notion", github: "github", stripe: "stripe",
  discord: "discord", telegram: "telegram", webhook: "webhook", schedule: "schedule",
  google_drive: "gdrive", google_calendar: "gcal",
  microsoft_outlook: "outlook", microsoft_teams: "teams", microsoft_excel: "m365",
  hubspot: "hubspot", airtable: "airtable", trello: "trello"
};
const RUN_STATUS = { success: "sucesso", error: "erro", running: "em execução", queued: "em execução", cancelled: "erro" };

function aliasConnector(id) { return CONNECTOR_ALIAS[id] || id; }

function initialView() {
  try {
    const params = new URLSearchParams(window.location.search);
    const value = params.get("view");
    return ["chats", "connect", "flows", "runs", "models", "agents", "history", "settings", "upgrade"].includes(value) ? value : "chats";
  } catch (e) {
    return "chats";
  }
}

function relTime(iso) {
  if (!iso) return "—";
  const diff = Date.now() - new Date(iso).getTime();
  if (isNaN(diff)) return "—";
  const min = Math.floor(diff / 60000);
  if (min < 1) return "agora";
  if (min < 60) return `há ${min} min`;
  const h = Math.floor(min / 60);
  if (h < 24) return `há ${h} h`;
  const d = Math.floor(h / 24);
  return `há ${d} d`;
}

function fmtDuration(ms) {
  if (ms == null) return "—";
  return (ms / 1000).toFixed(1).replace(".", ",") + " s";
}

function mapFlows(workflows, runs) {
  return (workflows || []).map((wf) => {
    const nodes = wf.nodes || [];
    const connectors = Array.from(new Set(nodes.map((n) => aliasConnector(n.connectorId)).filter(Boolean)));
    const wfRuns = (runs || []).filter((r) => (r.workflow_id || r.workflowId) === wf.id);
    const last = wfRuns[0];
    return {
      id: wf.id,
      name: wf.name,
      tag: wf.tag || "rascunho",
      status: STATUS_FLOW[wf.status] || "rascunho",
      nodes: nodes.length,
      connectors,
      lastRun: last ? relTime(last.started_at || last.startedAt) : "Nunca",
      runs: wfRuns.length
    };
  });
}

function mapRuns(runs, workflows) {
  const nameById = {};
  (workflows || []).forEach((w) => { nameById[w.id] = w.name; });
  return (runs || []).map((r) => ({
    id: r.id,
    flow: nameById[r.workflow_id || r.workflowId] || "Workflow",
    status: RUN_STATUS[r.status] || r.status,
    trigger: aliasConnector(r.trigger_type || r.trigger),
    dur: fmtDuration(r.duration_ms),
    when: relTime(r.started_at)
  }));
}

function mapBlueprint(wf) {
  return {
    id: wf.id,
    name: wf.name,
    tag: wf.tag || "rascunho",
    nodes: (wf.nodes || []).map((n) => ({
      id: n.id, kind: n.kind, title: n.title, sub: n.operationId, connector: aliasConnector(n.connectorId),
      // preserva os dados reais do no para execucao/credenciais (nao re-pedir ao usuario)
      connectorId: n.connectorId, operationId: n.operationId,
      config: n.config || {}, credentialRef: n.credentialRef || null
    })),
    edges: wf.edges || []
  };
}

function messageArtifactsFromMetadata(metadata) {
  const m = metadata && typeof metadata === "object" ? metadata : {};
  const approvals = Array.isArray(m.requiredApprovals) ? m.requiredApprovals : [];
  return {
    charts: Array.isArray(m.charts) ? m.charts : [],
    tables: Array.isArray(m.tables) ? m.tables : [],
    reportSections: Array.isArray(m.reportSections) ? m.reportSections : [],
    dashboards: Array.isArray(m.dashboards) ? m.dashboards : [],
    dashboardWidgets: Array.isArray(m.dashboardWidgets) ? m.dashboardWidgets : [],
    actions: approvals.map((approval) => ({
      type: "confirm_tool_approval",
      label: "Confirmar ação",
      connectorId: approval.connectorId,
      approvalId: approval.id
    }))
  };
}

function firstName(name) { return (name || "").trim().split(/\s+/)[0] || ""; }

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [authReady, setAuthReady] = useStateA(false);
  const [authed, setAuthed] = useStateA(false);
  const [isAdmin, setIsAdmin] = useStateA(false);
  const [user, setUser] = useStateA(null);
  const [view, setView] = useStateA(initialView);
  const [provider, setProvider] = useStateA("ollama");
  const [chats, setChats] = useStateA([]);
  const [flows, setFlows] = useStateA([]);
  const [runs, setRuns] = useStateA([]);
  const [agents, setAgents] = useStateA([]);
  const [activeChat, setActiveChat] = useStateA(null);
  const [chatTitle, setChatTitle] = useStateA("Novo chat");
  const [messages, setMessages] = useStateA([]);
  const [collapsed, setCollapsed] = useStateA(false);
  const [railExpanded, setRailExpanded] = useStateA(true);
  const [modelChoice, setModelChoice] = useStateA({});
  const [webSearchOn, setWebSearchOn] = useStateA(false);
  const [thinkingOn, setThinkingOn] = useStateA(false);
  const [openFlow, setOpenFlow] = useStateA(null);
  const [connectorCatalog, setConnectorCatalog] = useStateA([]);
  const [modelsCatalog, setModelsCatalog] = useStateA([]);
  const [connectedIds, setConnectedIds] = useStateA(() => new Set());
  const [connectModal, setConnectModal] = useStateA(null); // { connectorId }
  const [pendingConn, setPendingConn] = useStateA(null);    // { required:[ids], workflow }
  const lastPromptRef = useRefA("");
  const pendingReconnectPromptRef = useRefA("");
  const sendRef = useRefA(null);
  const idc = useRefA(0);
  const nid = () => "m" + (++idc.current);

  // Carrega os modelos realmente disponiveis na API por provedor.
  const refreshModels = async () => {
    try {
      const res = await kbApi.authFetch("/api/models", { method: "GET" });
      if (!res.ok) return [];
      const data = await res.json();
      const toIds = (arr) => (arr || []).map((m) => (typeof m === "string" ? m : (m && m.id))).filter(Boolean);
      const cat = (data.providers || []).map((p) => ({
        provider: p.id, label: p.label, kind: p.kind, short: p.short, color: p.color,
        // Conectado = status do servidor (que ja exige chave real). Nao confiar em
        // p.connected do meta estatico do provedor, que vinha sempre true.
        connected: p.status === "connected",
        status: p.status || "disconnected",
        hasApiKey: !!p.hasApiKey,
        defaultModel: p.defaultModel,
        enabledModels: p.enabledModels || [],
        models: toIds(p.models),
        webSearchModels: toIds(p.webSearchModels),
        thinkingModels: toIds(p.thinkingModels)
      }));
      cat.forEach((p) => {
        if (PROVIDERS[p.provider]) {
          PROVIDERS[p.provider].model = p.defaultModel || PROVIDERS[p.provider].model;
          PROVIDERS[p.provider].kind = p.kind || PROVIDERS[p.provider].kind;
        }
      });
      setModelsCatalog(cat);
      window.MODEL_CATALOG = cat.length ? cat : window.MODEL_CATALOG;
      // Se o provedor atual nao esta conectado, troca para o primeiro conectado
      // (com modelos), para o chat nunca abrir num provedor sem chave.
      const connected = cat.filter((c) => c.connected && c.models.length > 0);
      if (connected.length) {
        setProvider((cur) => {
          const curOk = connected.some((c) => c.provider === cur);
          const next = curOk ? cur : connected[0].provider;
          const nextEntry = connected.find((c) => c.provider === next);
          if (nextEntry && nextEntry.defaultModel) {
            setModelChoice((mc) => (mc[next] ? mc : { ...mc, [next]: nextEntry.defaultModel }));
          }
          return next;
        });
      }
      return cat;
    } catch (e) { return []; }
  };

  // Carrega catalogo de conectores (config + status) e atualiza o conjunto conectado.
  const refreshConnectors = async () => {
    try {
      const data = await kbConnectors.list();
      const items = data.connectors || [];
      setConnectorCatalog(items);
      setConnectedIds(new Set(items.filter((c) => c.connected).map((c) => c.id)));
      return items;
    } catch (e) { return []; }
  };

  useEffectA(() => {
    document.documentElement.style.setProperty("--accent", t.accent);
    document.documentElement.style.setProperty("--loader-speed", String(t.loaderSpeed));
    document.documentElement.style.setProperty("--texture-on", t.showTexture ? "1" : "0");
  }, [t.accent, t.loaderSpeed, t.showTexture]);

  useEffectA(() => {
    const handler = (event) => {
      if (event.origin !== window.location.origin) return;
      if (!event.data || event.data.type !== "kobar:connector") return;
      refreshConnectors();
      if (event.data.status === "ok" && event.data.connector) {
        onConnectorChanged(event.data.connector, true);
        const pendingPrompt = pendingReconnectPromptRef.current;
        pendingReconnectPromptRef.current = "";
        if (pendingPrompt && sendRef.current) {
          setTimeout(() => sendRef.current(pendingPrompt), 450);
        }
      }
    };
    window.addEventListener("message", handler);
    return () => window.removeEventListener("message", handler);
  }, []);

  // Carrega o estado real da conta (usuario + dados) a partir do backend.
  const loadBootstrap = async () => {
    const res = await kbApi.authFetch("/api/bootstrap", { method: "GET" });
    if (!res.ok) throw new Error("unauthenticated");
    const boot = await res.json();
    const session = kbApi.getSession();
    const sessUser = (session && session.user) || {};
    setUser({
      id: boot.user.id,
      name: boot.user.name || sessUser.name || "",
      email: boot.user.email || sessUser.email || "",
      org: boot.user.org || "Workspace",
      plan: boot.plan || "Free"
    });
    setChats(boot.conversations || []);
    setFlows(mapFlows(boot.workflows, boot.runs));
    setRuns(mapRuns(boot.runs, boot.workflows));
    setAgents(boot.agents || []);
    setIsAdmin(!!boot.isAdmin);
    // status inicial de conexoes a partir do bootstrap (sera refinado por refreshConnectors)
    const connConns = boot.connectorConnections || [];
    setConnectedIds(new Set(connConns.filter((c) => c.status === "connected").map((c) => c.connectorId)));
    setAuthed(true);
    refreshConnectors();
    refreshModels();
    return boot;
  };

  // Verifica a sessao guardada ao iniciar.
  useEffectA(() => {
    let active = true;
    (async () => {
      const session = kbApi.getSession();
      if (!session || !session.accessToken) { if (active) { setAuthed(false); setAuthReady(true); } return; }
      try {
        await loadBootstrap();
      } catch (e) {
        kbApi.clearSession();
        if (active) setAuthed(false);
      } finally {
        if (active) setAuthReady(true);
      }
    })();
    return () => { active = false; };
  }, []);

  const onAuth = async () => {
    setAuthReady(false);
    try { await loadBootstrap(); }
    catch (e) { kbApi.clearSession(); setAuthed(false); }
    finally { setAuthReady(true); }
  };

  const logout = async () => {
    try { await kbAuth.signout(); } catch (e) {}
    kbApi.clearSession();
    setAuthed(false);
    setIsAdmin(false);
    setUser(null);
    setChats([]); setFlows([]); setRuns([]); setAgents([]);
    setConnectorCatalog([]); setConnectedIds(new Set()); setConnectModal(null); setPendingConn(null);
    setMessages([]); setActiveChat(null); setChatTitle("Novo chat"); setView("chats");
    setWebSearchOn(false); setThinkingOn(false);
  };

  const openEditor = (bp) => { setOpenFlow(bp); setView("editor"); };

  // Abre um flow pelo id, buscando a definicao COMPLETA (nos/edges/config/credenciais)
  // no backend. Usado pelo menu de Workflows e pelo CTA do chat (inclusive ao reabrir
  // do historico), garantindo que o canvas sempre receba o grafo montado.
  const openFlowById = async (id) => {
    if (!id) return;
    // Busca a definicao ANTES de abrir o editor: o WorkflowPage inicializa os nos
    // apenas na montagem, entao precisamos do grafo pronto antes de montar.
    try {
      const res = await kbApi.authFetch("/api/workflows/" + encodeURIComponent(id));
      const data = await res.json().catch(() => ({}));
      if (res.ok && data.workflow) openEditor(mapBlueprint(data.workflow));
      else window.alert((data && data.error) || "Não foi possível carregar este workflow.");
    } catch (e) { window.alert("Falha ao carregar o workflow."); }
  };

  const deleteFlow = async (flow) => {
    try {
      await kbWorkflows.remove(flow.id, false);
      setFlows((items) => items.filter((item) => item.id !== flow.id));
    } catch (err) {
      if (err && err.data && err.data.requiresConfirmation) {
        const force = window.confirm("Este flow tem execuções ativas. Arquivar mesmo assim?");
        if (!force) return;
        await kbWorkflows.remove(flow.id, true);
        setFlows((items) => items.filter((item) => item.id !== flow.id));
        return;
      }
      window.alert((err && err.message) || "Falha ao excluir flow.");
    }
  };

  // Abre o modal de conexao para um conector (id do backend).
  const openConnect = (connectorId, opts) => {
    if (opts && opts.forceReconnect && lastPromptRef.current) {
      pendingReconnectPromptRef.current = lastPromptRef.current;
    }
    setConnectModal({ connectorId, forceReconnect: !!(opts && opts.forceReconnect) });
  };

  // Auto-continuar: quando todos os conectores pendentes ficam conectados,
  // reavalia o workflow no backend e injeta a confirmacao "tudo pronto".
  const runContinue = async (workflow) => {
    try {
      const r = await kbChat.continueWorkflow(workflow);
      const finalWf = r.workflow || workflow;
      setMessages((m) => [...m, {
        id: nid(), role: "ai", text: r.reply || "Tudo conectado! Seu workflow está pronto.",
        cta: {
          title: "Abrir no editor de workflows",
          sub: `${(finalWf.nodes || []).length} nós · pronto para revisar`,
          onClick: () => openEditor(mapBlueprint(finalWf))
        }
      }]);
    } catch (e) { /* silencioso */ }
  };

  // Chamado pelo modal ao conectar/desconectar um conector.
  const onConnectorChanged = (connectorId, connected) => {
    setConnectorCatalog((cat) => cat.map((c) => c.id === connectorId ? { ...c, connected } : c));
    setConnectedIds((prev) => {
      const next = new Set(prev);
      if (connected) next.add(connectorId); else next.delete(connectorId);
      // dispara auto-continuar se completou os pendentes
      if (connected && pendingConn && pendingConn.required.every((id) => next.has(id))) {
        const wf = pendingConn.workflow;
        setPendingConn(null);
        setTimeout(() => runContinue(wf), 250);
      }
      return next;
    });
    setConnectModal(null);
  };

  const send = async (text, attachments = []) => {
    lastPromptRef.current = text;
    const title = text || (attachments.length ? attachments[0].name : "");
    if (!messages.length && title) setChatTitle(title.length > 42 ? title.slice(0, 42) + "…" : title);
    const userId = nid(), thinkId = nid();
    const convId = activeChat;
    setMessages((m) => [...m,
      { id: userId, role: "user", text, attachments },
      { id: thinkId, role: "ai", thinking: true, thinkingText: thinkingOn ? "Pensando com raciocínio estendido…" : "Analisando seu pedido…" }
    ]);
    try {
      const model = modelChoice[provider] || (PROVIDERS[provider] && PROVIDERS[provider].model);
      const res = await kbApi.authFetch("/api/chat/messages", {
        method: "POST",
        body: JSON.stringify({ conversationId: convId, text, provider, model, webSearch: webSearchOn, thinkingMode: thinkingOn,
          attachments: (attachments || []).map((a) => ({ name: a.name, type: a.type, kind: a.kind, size: a.size, url: a.url })) })
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || "Falha ao processar a mensagem.");
      const a = data.assistant || {};
      const wf = a.workflow;
      const required = a.requiredConnections || [];
      // CTA direto so quando nao falta conectar nada.
      const cta = wf && required.length === 0 ? {
        title: "Abrir no editor de workflows",
        sub: `${(wf.nodes || []).length} nós · pronto para revisar`,
        onClick: () => openEditor(mapBlueprint(wf))
      } : null;
      setMessages((m) => m.map((x) => x.id === thinkId
        ? {
            id: thinkId, role: "ai", text: a.content || "",
            charts: a.charts || [],
            actions: a.actions || [],
            tables: a.tables || [],
            reportSections: a.reportSections || [],
            dashboards: a.dashboards || [],
            dashboardWidgets: a.dashboardWidgets || [],
            via: model || (PROVIDERS[provider] && PROVIDERS[provider].model),
            cta, connections: required, workflow: wf || null
          }
        : x));
      if (wf && required.length > 0) {
        setPendingConn({ required: required.map((r) => r.connectorId), workflow: wf });
      }
      const newId = data.conversationId;
      setActiveChat(newId);
      setChats((cs) => {
        if (cs.some((c) => c.id === newId)) return cs;
        return [{ id: newId, title: text.length > 48 ? text.slice(0, 48) + "…" : text, when: "Hoje", pinned: false, connectors: wf ? Array.from(new Set((wf.nodes || []).map((n) => aliasConnector(n.connectorId)))) : [] }, ...cs];
      });
    } catch (err) {
      setMessages((m) => m.map((x) => x.id === thinkId
        ? { id: thinkId, role: "ai", text: err.message || "Não foi possível responder agora." }
        : x));
    }
  };
  sendRef.current = send;
  // Permite que botoes de dashboard (action send_message) enviem ao chat.
  if (typeof window !== "undefined") window.__kbSend = send;

  const newChat = () => { setMessages([]); setActiveChat(null); setChatTitle("Novo chat"); setView("chats"); };

  const selectChat = async (id) => {
    const c = chats.find((x) => x.id === id);
    setActiveChat(id); setChatTitle(c ? c.title : "Chat"); setView("chats");
    setMessages([{ id: nid(), role: "ai", thinking: true, thinkingText: "Carregando conversa…" }]);
    try {
      const res = await kbApi.authFetch("/api/chat/messages?conversationId=" + encodeURIComponent(id));
      const data = await res.json();
      const msgs = (data.messages || []).map((m) => {
        const msg = {
          ...messageArtifactsFromMetadata(m.metadata),
          id: m.id,
          role: m.role === "assistant" ? "ai" : "user",
          text: m.content,
          via: m.role === "assistant" ? (PROVIDERS[provider] && PROVIDERS[provider].model) : undefined
        };
        // Reconstroi o botao "Abrir no editor" ao reabrir o chat do historico.
        if (m.role === "assistant" && m.workflowId) {
          msg.cta = {
            title: "Abrir no editor de workflows",
            sub: "pronto para revisar",
            onClick: () => openFlowById(m.workflowId)
          };
        }
        return msg;
      });
      setMessages(msgs);
    } catch (e) {
      setMessages([]);
    }
  };

  const pinChat = (id) => setChats((cs) => cs.map((c) => c.id === id ? { ...c, pinned: !c.pinned } : c));
  const renameChat = (id, title) => { setChats((cs) => cs.map((c) => c.id === id ? { ...c, title } : c)); if (activeChat === id) setChatTitle(title); };
  const deleteChat = (id) => {
    setChats((cs) => cs.filter((c) => c.id !== id));
    if (activeChat === id) newChat();
    kbApi.authFetch("/api/chat/conversations?id=" + encodeURIComponent(id), { method: "DELETE" }).catch(() => {});
  };
  const deleteAllChats = async () => {
    setChats([]); newChat();
    try { await kbApi.authFetch("/api/chat/conversations", { method: "DELETE" }); } catch (e) {}
  };

  const nav = (id) => setView(id);
  const railActive = view === "editor" ? "flows" : view;

  if (!authReady) {
    return (
      <div className="kb-bootsplash" style={{ display: "grid", placeItems: "center", minHeight: "100vh" }}>
        <OliveLoader size={48} speed={1.4} />
      </div>
    );
  }

  if (!authed) return <AuthScreen onAuth={onAuth} />;

  const userName = firstName(user && user.name);

  return (
    <div className={"kb-app" + (view === "editor" ? " on-workflow" : "")}>
      <TatreezTexture id="tatreez" opacity={0.5} />
      <CircuitTexture id="circuit" opacity={0.55} />

      <IconRail active={railActive} onNav={nav} onHome={newChat}
        expanded={railExpanded} onToggleExpand={() => setRailExpanded((e) => !e)}
        onUpgrade={() => setView("upgrade")} user={user} onLogout={logout} />

      {view === "chats" && (
        <>
          <ChatListPanel chats={chats} activeChat={activeChat} onSelectChat={selectChat} onNew={newChat}
            onPin={pinChat} onRename={renameChat} onDelete={deleteChat} onDeleteAll={deleteAllChats} onNav={nav}
            onUpgrade={() => setView("upgrade")} collapsed={collapsed} onToggle={() => setCollapsed((c) => !c)}
            planName={user && user.plan} />
          <main className="kb-main">
            {collapsed && (
              <button className="kb-reopen" onClick={() => setCollapsed(false)} data-tip="Abrir conversas">
                <Icon name="panel" size={18} />
              </button>
            )}
            <ChatHome variant={t.homeLayout} provider={provider} onProvider={setProvider}
              onSend={send} messages={messages} chatTitle={chatTitle} userName={userName}
              webSearchOn={webSearchOn} onWebSearch={setWebSearchOn}
              thinkingOn={thinkingOn} onThinking={setThinkingOn} modelChoice={modelChoice}
              connectedIds={connectedIds} onConnect={openConnect}
              onOpenFlow={(wf) => openFlowById(wf.id)} />
          </main>
        </>
      )}

      {view === "editor" && (
        <WorkflowPage key={(openFlow && openFlow.id) || "blank"} blueprint={openFlow || BLANK_BLUEPRINT} provider={provider} onProvider={setProvider}
          onBack={() => setView("flows")} />
      )}

      {view === "flows" && <FlowsPage flows={flows} onOpen={(f) => openFlowById(f.id)} onNew={() => openEditor(BLANK_BLUEPRINT)} onDelete={deleteFlow} />}
      {view === "connect" && <ConnectPage connectors={connectorCatalog} onConnected={onConnectorChanged} />}
      {view === "runs" && <RunsPage runs={runs} />}
      {view === "models" && <ModelsPage provider={provider} onProvider={setProvider} catalog={modelsCatalog}
        isAdmin={isAdmin} onCatalogChange={setModelsCatalog} onRefresh={refreshModels}
        currentUserId={user && user.id}
        modelChoice={modelChoice} onModelChoice={(p, m) => setModelChoice((s) => ({ ...s, [p]: m }))} />}
      {view === "agents" && <AgentsPage initialAgents={agents} />}
      {view === "history" && <HistoryPage />}
      {view === "settings" && <SettingsPage provider={provider} onUpgrade={() => setView("upgrade")} user={user} isAdmin={isAdmin} onModels={() => setView("models")} />}
      {view === "upgrade" && <UpgradePage onBack={() => setView("settings")} />}

      {connectModal && (
        <ConnectorModal connectorId={connectModal.connectorId} catalog={connectorCatalog}
          returnTo="/?v=exact&view=chats" forceReconnect={connectModal.forceReconnect}
          onConnected={onConnectorChanged} onClose={() => setConnectModal(null)} />
      )}

      <TweaksPanel>
        <TweakSection label="Layout da home" />
        <TweakRadio label="Estilo" value={t.homeLayout}
          options={[{ value: "A", label: "Minimal" }, { value: "B", label: "Conector" }]}
          onChange={(v) => setTweak("homeLayout", v)} />
        <TweakSection label="Marca" />
        <TweakColor label="Cor de destaque" value={t.accent}
          options={["#B8734A", "#384636", "#11161D"]}
          onChange={(v) => setTweak("accent", v)} />
        <TweakToggle label="Textura tatreez/circuito" value={t.showTexture}
          onChange={(v) => setTweak("showTexture", v)} />
        <TweakSection label="Animação" />
        <TweakSlider label="Velocidade do loader Olive" value={t.loaderSpeed} min={0.6} max={2.2} step={0.1}
          onChange={(v) => setTweak("loaderSpeed", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
