// 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 , 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 ( //
// Hello // // // setTweak('fontSize', v)} /> // setTweak('density', v)} /> // // setTweak('primaryColor', v)} /> // setTweak('palette', v)} /> // setTweak('dark', v)} /> // //
// ); // } // // 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,"); 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 ( <>
{title}
{children}
); } // ── Layout helpers ────────────────────────────────────────────────────────── function TweakSection({ label, children }) { return ( <>
{label}
{children} ); } function TweakRow({ label, value, children, inline = false }) { return (
{label} {value != null && {value}}
{children}
); } // ── Controls ──────────────────────────────────────────────────────────────── function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { return ( onChange(Number(e.target.value))} /> ); } function TweakToggle({ label, value, onChange }) { return (
{label}
); } 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) { // onChange(e.target.value)}> {options.map((o) => { const v = typeof o === 'object' ? o.value : o; const l = typeof o === 'object' ? o.label : o; return ; })} ); } function TweakText({ label, value, placeholder, onChange }) { return ( onChange(e.target.value)} /> ); } 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 (
{label} onChange(clamp(Number(e.target.value)))} /> {unit && {unit}}
); } // 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 }) => ( ); // 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 (
{label}
onChange(e.target.value)} />
); } // Native 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 (
{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 ( ); })}
); } function TweakButton({ label, onClick, secondary = false }) { return ( ); } 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 ( {/* speed dashes */} {/* copper chip square */} {/* stem */} {/* upper arm */} {/* lower arm */} ); } function KobarWordmark({ size = 22, tagline = false, mono = false, style = {} }) { const ink = mono ? "currentColor" : "var(--charcoal)"; return (
Kobar Labs {tagline && ( AI · AUTOMATION · INTELLIGENCE )}
); } // ---- 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 ( ); } // Convenience loader (spinning, with optional pulse glow) function OliveLoader({ size = 30, speed = 1, style = {} }) { return ; } // ---- 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( ); }); return ( ); } // ---- Texture: Circuit pathways (line + node flow) for canvas / panels ---- function CircuitTexture({ id = "circuit", color = "var(--copper)", opacity = 0.18 }) { return ( ); } 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 ( ); } // ---- 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: <>, m365: <>, slack: <>, github: , notion: <>, discord: , sheets: <>, gmail: , http: , webhook: , schedule: , openai_c: , postgres: <>, }; 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 ? ( {glyph} ) : ( {(c.short || c.label)[0].toUpperCase()} ); if (!tile) return inner; return ( {inner} ); } // ---- 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: , openai: , anthropic: , deepseek: , gemini: , }; function ProviderMark({ id, size = 18, style = {} }) { return ( ); } 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 ( {initials} ); } 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 ( ); } function ConnectorDots({ ids, size = 16 }) { return ( {ids.slice(0, 3).map((id, i) => ( ))} ); } 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 (
{c.pinned && !editing && } {menu && (
e.stopPropagation()}>
)}
); } 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 ( ); } 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 (
{open && (
{localIds.length > 0 &&
Local
} {localIds.map((id) => ( { onChange(id); setOpen(false); }} /> ))} {cloudIds.length > 0 &&
Cloud API
} {cloudIds.map((id) => ( { onChange(id); setOpen(false); }} /> ))}
)}
); } function ProviderItem({ id, active, onPick, modelChoice }) { const p = PROVIDERS[id]; const model = providerModel(id, modelChoice); return ( ); } function StarterChip({ icon, text, onClick }) { return ( ); } function SuggestionCard({ s, onClick }) { return ( ); } // 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 (
{!!conns.length && ( <>
{allOn ? "Tudo conectado — pronto para abrir o fluxo." : anyReconnect ? "Reconecte as ferramentas para atualizar permissões:" : "Conecte as ferramentas para concluir:"}
{conns.map((rc) => { const on = isOn(rc); const reconnect = needsReconnect(rc); return (
{rc.label} {on ? ( conectado ) : ( )}
); })}
)} {allOn && msg.workflow && ( )}
); } 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 (
{approvals.map((approval) => { const status = state[approval.approvalId] || ""; const done = status === "done"; const running = status === "running"; return (
{approval.label || "Confirmar ação"} {done ? ( executado ) : ( )}
); })}
); } 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({m[2]}); else if (m[3] && m[4]) parts.push({m[3]}); 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 (
{head.map((cell, i) => )}{body.map((row, i) => {row.map((cell, j) => )})}
{renderInlineMarkdown(cell)}
{renderInlineMarkdown(cell)}
); } 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 ( {ticks.map((t, i) => { const yy = y(t); return ( {formatChartValue(t)} ); })} {area && } {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 ( {formatChartValue(d.value)} {String(d.label).slice(0, 8)} ); })} ); } // 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 (
{slices.map((s, i) => )} {donut && } {slices.filter((s) => s.frac >= 0.08).map((s, i) => ( {Math.round(s.frac * 100)}% ))}
{slices.map((s, i) => (
{s.label} {formatChartValue(s.value)} · {Math.round(s.frac * 100)}%
))}
); } 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 (
{chart.title || "Gráfico"} {(chart.xLabel || chart.yLabel) && {[chart.xLabel, chart.yLabel].filter(Boolean).join(" · ")}}
{type === "kpi" ? (
{data.map((item, index) => (
{item.label} {formatChartValue(item.value)}
))}
) : type === "table" ? (
{data.map((item, index) => )}
ItemValor
{item.label}{formatChartValue(item.value)}
) : type === "line" || type === "area" ? (
) : type === "pie" || type === "donut" ? (
) : ( (() => { // 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 (
{barData.map((item, index) => { const pct = Math.max(4, Math.round((Math.abs(item.value) / max) * 100)); return (
{item.label} {formatChartValue(item.value)}
); })}
); })() )}
); } // ---- 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) =>
{child}
; if (w.type === "kpi") { const up = typeof w.delta === "number" ? w.delta >= 0 : null; return wrap(
{w.title} {w.value}{w.unit ? {w.unit} : null} {typeof w.delta === "number" && ( {up ? "▲" : "▼"} {Math.abs(w.delta)}{w.deltaLabel ? ` ${w.deltaLabel}` : "%"} )} {w.hint && {w.hint}}
); } if (w.type === "chart") return wrap(); if (w.type === "table") { return wrap(
{w.title && {w.title}} {(w.headers || []).map((h, i) => )}{(w.rows || []).map((row, i) => {(row || []).map((c, j) => )})}
{h}
{String(c)}
); } if (w.type === "text") { return wrap(
{w.title && {w.title}} {w.body && } {Array.isArray(w.bullets) && w.bullets.map((b, i) =>

{renderInlineMarkdown(b)}

)}
); } 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(
{(w.buttons || []).map((b, i) => ( ))}
); } if (w.type === "divider") return wrap(
{w.label || ""}
); return null; } function DashboardBlock({ dashboard, onConnect, onOpenFlow }) { const widgets = (dashboard && Array.isArray(dashboard.widgets)) ? dashboard.widgets : []; if (!widgets.length) return null; return (
{dashboard.title || "Dashboard"} {dashboard.subtitle && {dashboard.subtitle}}
{widgets.map((w, i) => )}
); } function MessageDashboards({ dashboards, onConnect, onOpenFlow }) { const all = Array.isArray(dashboards) ? dashboards : []; if (!all.length) return null; return <>{all.map((dash, i) => )}; } 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(
); continue; } if (/^\s*\|.+\|\s*$/.test(line)) { const table = []; while (i < lines.length && /^\s*\|.+\|\s*$/.test(lines[i])) table.push(lines[i++]); i--; out.push(); continue; } const heading = line.match(/^(#{2,3})\s+(.+)$/); if (heading) { const Tag = heading[1].length === 2 ? "h3" : "h4"; out.push({renderInlineMarkdown(heading[2])}); continue; } const bullet = line.match(/^\s*[-•]\s+(.+)$/); if (bullet) { out.push(

{renderInlineMarkdown(bullet[1])}

); continue; } const numbered = line.match(/^\s*(\d+)\.\s+(.+)$/); if (numbered) { out.push(

{numbered[1]}{renderInlineMarkdown(numbered[2])}

); continue; } out.push(

{renderInlineMarkdown(line)}

); } 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) => )}; } function MessageBubble({ msg, connectedIds, onConnect, onOpenFlow }) { if (msg.role === "user") { const atts = msg.attachments || []; return (
{atts.length > 0 && (
{atts.map((a, i) => ( a.kind === "image" && a.url ? ( {a.name} ) : a.kind === "audio" && a.url ? (
{a.name}
) : (
{a.name}
) ))}
)} {msg.text &&
{msg.text}
}
); } return (
{msg.thinking ? (
{msg.thinkingText || "Pensando…"}
) : (
{msg.cta && ( )}
)} {msg.via && via {msg.via}}
); } 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 ( ); } function Field({ icon, type = "text", value, onChange, placeholder, label, autoFocus, rightSlot, disabled }) { return ( ); } 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 (
{/* Left brand panel */}

Automação conversacional para o seu negócio.

Descreva o que precisa em linguagem natural. A Kobar identifica, conecta suas ferramentas e constrói o workflow para você.

  • Mais de 200 conectores prontos
  • Modelos de IA locais ou na nuvem
  • Do chat ao fluxo publicado em minutos
© 2026 Kobar Labs Privacidade Termos
{/* Right form panel */}

{mode === "login" ? "Acesse sua conta" : "Crie sua conta"}

{mode === "login" ? "Bem-vindo de volta ao Kobar Flows." : "Comece a automatizar em poucos minutos."}

{(error || info) && (
{error || info}
)} {flow === "form" ? ( <>
ou continue com e-mail
{mode === "signup" && ( )} setShow((s) => !s)} tabIndex={-1}>} /> {mode === "login" && (
)} ) : (
Enviamos um código de 6 dígitos para
{email || "voce@empresa.com"} O código expira em alguns minutos. Verifique spam ou lixo eletrônico.
{code.map((d, i) => ( (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(); }} /> ))}
)}

{mode === "login" ? "Ainda não tem uma conta? " : "Já tem uma conta? "}

Conexão segura · seus dados ficam no seu ambiente
); } 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 (
{open && (
Conectores do sistema{count}/{ids.length}
{ids.length === 0 && (
Nenhum conector conectado ainda.
)} {ids.map((id) => (
{(CONNECTORS[id] && CONNECTORS[id].label) || id} {on[id] ? "ligado" : "desligado"}
setOn((s) => ({ ...s, [id]: !s[id] }))} />
))}
)}
); } 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 (
{atts.length > 0 && (
{atts.map((a) => (
{a.kind === "image" && a.url ? : } {a.name} {a.kind === "image" ? "Imagem" : a.kind === "audio" ? "Áudio" : "Arquivo"} · {fmtSize(a.size)}
))}
)} {hasImageAtt && !modelHasVision && (
O modelo {activeModel} não interpreta imagens. Escolha um modelo com visão (ex.: gemma3, llava, llama3.2-vision) para analisar o anexo.
)}