openvibers/openvibe.shared ov-run.js
ov-run.js
| 1 | /** |
| 2 | * ov-run.js (openvibe-shared 4.224.0): the one run thread. Everything a person asks an AI to do on OpenVibe is a run |
| 3 | * with a thread of steps (a chat with Vibe, a task for an agent, a coding task); this draws every one of them the same |
| 4 | * way, on every site, from the Frame's Run API (Network /api/frame/runs, Contracts `agents.run@1`, `agents.step@1`, |
| 5 | * `agents.event@1`). The owner, 11-12, crossing out the menu's Chat · Code · Agents switch: "one new unified agent … |
| 6 | * a shared UI … a full conversation with rich embedding … agent mode". |
| 7 | * |
| 8 | * const view = OpenVibeRun.mount(host, { |
| 9 | * base: 'https://openvibe.network/api/frame', // the Frame (default) |
| 10 | * token: () => token, // the person's Network token (or a string) |
| 11 | * me: { name, avatar_url }, // the person, for their own bubbles |
| 12 | * sibling: (file) => url, // where ov-composer.js is (the navbar's sibling()) |
| 13 | * examples: ['A goal bar for my stream', …], // what people make, shown in turn in the empty box |
| 14 | * start: { kind, target, context }, // what a new run starts with (the box's defaults) |
| 15 | * }); |
| 16 | * view.list('code'); view.open('chat:c_8f3k2m'); view.start('A goal bar for my stream'); view.prefill('Words, unsent'); |
| 17 | * view.destroy(); |
| 18 | * (o.filter: the list's first filter, all | chat | task | code; o.open: a run's ref to open first) |
| 19 | * |
| 20 | * 4.225.0, what the menu's Vibe needs to be the one place (the Dock's Vibe and Agents panels did all of it): |
| 21 | * o.starters: [[glyph, words], …] things to ask, as chips above the box (All and Chats); a press starts that run |
| 22 | * o.actions: { base } the Action Center's waiting and running work from every product (Network |
| 23 | * GET /api/v1/me/actions), merged into the list (All and Tasks) when a run does not |
| 24 | * already show it; approved or declined in place, and opened on its own page |
| 25 | * o.top(el) → { destroy } something of the host's above the list (All and Tasks): the menu puts the Dock's |
| 26 | * Now, your Actor and the live agent view there |
| 27 | * o.panes: { chat, task, code } a filter the host still draws itself: pane(el) → { destroy }. The menu's Vibe |
| 28 | * lends the Dock's Vibe, Agents and Code panels here until this view does all |
| 29 | * they do; the pane replaces the rows and the box under the filters |
| 30 | * o.onPlace({ filter } | { ref }) where the person is, for the host to remember |
| 31 | * view.prefill(text) the words in the list's box, unsent (Ask your agent) |
| 32 | * |
| 33 | * OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its |
| 34 | * own); an unknown type shows the step's words. |
| 35 | * |
| 36 | * The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and |
| 37 | * hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right, |
| 38 | * the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row, |
| 39 | * an approval as a card with its buttons, child runs as rows, files as chips. It follows the run live (the stream, |
| 40 | * resumed after its last step) and the composer below says the next thing, or starts a new run from the list. |
| 41 | */ |
| 42 | (function (root) { |
| 43 | ; |
| 44 | if (root.OpenVibeRun || typeof document === 'undefined') return; |
| 45 | |
| 46 | // ── glyphs (the registry's drawings, openvibe-shared/glyphs.js; 24-unit strokes) ── |
| 47 | const G = { |
| 48 | back: '<path d="M20.8 12H4M10.3 5.7l-6.3 6.3l6.3 6.3"/>', |
| 49 | open: '<path d="M7 17L17 7M9 7h8v8"/>', |
| 50 | send: '<path d="M12 19.5V5M5.8 11.2L12 5l6.2 6.2"/>', |
| 51 | plus: '<path d="M12 5v14M5 12h14"/>', |
| 52 | spark: '<path d="M10.4 3.6l1.7 4.8L17 10.1l-4.8 1.7L10.4 16.7l-1.7-4.8L3.9 10.1l4.8-1.7ZM17 14.8l0.8 2.1L19.8 17.6l-2.1 0.8L17 20.3l-0.8-2.1L14.3 17.6l2.1-0.8Z"/>', |
| 53 | chat: '<path d="M3.8 4.3h16.8v11.7H8l-4.2 4.2Z"/>', |
| 54 | code: '<path d="M7.8 7.8l-4.2 4.2l4.2 4.2M16.2 7.8l4.2 4.2l-4.2 4.2M14.2 4.6l-4.2 14.8"/>', |
| 55 | robot: '<rect x="5.4" y="8.3" width="13.2" height="10.4" rx="2.8"/><path d="M12 4.6v3.8M9.2 13.1h0M14.8 13.1h0M9.6 16.4h4.7"/>', |
| 56 | tool: '<path d="M14.6 3.6a5.2 5.2 0 0 0-4.6 7.3L3.7 17.3V20.3h3.1l6.4-6.4a5.2 5.2 0 0 0 7.3-4.6l-3.2 1.2l-2.5-2.5l1.2-3.2Z"/>', |
| 57 | think: '<path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9V16h7v-2.1A6 6 0 0 0 12 3Z"/>', |
| 58 | file: '<path d="M7 3h7l5 5v12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM14 3v5h5"/>', |
| 59 | check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>', |
| 60 | x: '<path d="M7 7l10 10M17 7L7 17"/>', |
| 61 | pr: '<circle cx="6" cy="6" r="2.2"/><circle cx="6" cy="18" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M6 8.2v7.6M18 15.8V9a3 3 0 0 0-3-3h-4M13 4l-2 2 2 2"/>', |
| 62 | link: '<path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/>', |
| 63 | child: '<path d="M6 4v8a4 4 0 0 0 4 4h8M14 12l4 4-4 4"/>', |
| 64 | clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>', |
| 65 | search: '<circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/>', |
| 66 | book: '<path d="M12 6.3C10 4.8 7 4.3 3.5 4.9v13.2c3.5-0.6 6.5-0.1 8.5 1.4c2-1.5 5-2 8.5-1.4V4.9C17 4.3 14 4.8 12 6.3Z"/><path d="M12 6.3v13.2"/>', |
| 67 | pencil: '<path d="M4 20h4L19 9l-4-4L4 16v4zM13.5 6.5l4 4"/>', |
| 68 | live: '<circle cx="12" cy="11.8" r="2"/><path d="M8.6 8.4a4.9 4.9 0 0 0 0 7.1M15.4 8.4a4.9 4.9 0 0 1 0 7.1M6.3 6.1a8.1 8.1 0 0 0 0 11.9M17.7 6.1a8.1 8.1 0 0 1 0 11.9"/>', |
| 69 | store: '<path d="M3.7 8.8L5.3 3.7h13.4L20.3 8.8M3.7 8.8h16.5v1.6a2.8 2.8 0 0 1-5.4 0a2.8 2.8 0 0 1-5.6 0A2.8 2.8 0 0 1 3.7 10.4ZM4.8 12.9v7.2h14.5v-7.2"/>', |
| 70 | game: '<rect x="3.7" y="7.2" width="16.7" height="9.7" rx="4.4"/><path d="M7.6 10.7v2.7M6.4 12h2.7M15.1 11.6h0M17.4 12.9h0"/>', |
| 71 | compass: '<circle cx="12" cy="12" r="8.5"/><path d="m15.6 8.4-2.1 5.1-5.1 2.1 2.1-5.1Z"/>', |
| 72 | page: '<path d="M6.5 3h7.5l4 4v14h-11.5Z"/><path d="M14 3v4h4M9.5 12h5M9.5 15.5h5"/>', |
| 73 | timer: '<path d="M12 4.5a8.2 8.2 0 1 1 0 16.4 8.2 8.2 0 0 1 0-16.4zM12 8.6v4.2l2.7 1.7M9.6 2.6h4.8"/>', |
| 74 | bell: '<path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 1.5h-15ZM10 20.5a2.2 2.2 0 0 0 4 0"/>', |
| 75 | calendar: '<rect x="4" y="5.5" width="16" height="14.5" rx="2.5"/><path d="M4 10h16M8.5 3.5v4M15.5 3.5v4"/>', |
| 76 | phone: '<path d="M6.5 3.8h3l1.5 4-2 1.3a10.5 10.5 0 0 0 5.9 5.9l1.3-2 4 1.5v3a1.8 1.8 0 0 1-2 1.8A15.5 15.5 0 0 1 4.7 5.8a1.8 1.8 0 0 1 1.8-2Z"/>', |
| 77 | }; |
| 78 | const KIND_GLYPH = { chat: 'chat', ask: 'search', task: 'robot', code: 'code', machine: 'tool', goal: 'spark' }; |
| 79 | const KIND_WORD = { chat: 'Chat', ask: 'Answer', task: 'Task', code: 'Code', machine: 'Machine', goal: 'Goal' }; |
| 80 | const SERVICE_NAME = { actor: 'Actor', codes: 'Codes', ai: 'AI', network: 'OpenVibe', run: 'Run', domains: 'Domains' }; |
| 81 | const svg = (name, cls = '') => { |
| 82 | const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); |
| 83 | s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); if (cls) s.setAttribute('class', cls); |
| 84 | s.innerHTML = G[name] || G.spark; // the registry's constants above, never page text |
| 85 | return s; |
| 86 | }; |
| 87 | const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; }; |
| 88 | const httpsUrl = (v) => { try { const u = new URL(String(v)); return u.protocol === 'https:' ? u.href : ''; } catch { return ''; } }; |
| 89 | const hueOf = (id) => { let x = 7; for (const c of String(id || 'ov')) x = (x * 31 + c.codePointAt(0)) % 360; return x; }; |
| 90 | const ago = (iso) => { |
| 91 | const t = Date.parse(iso || ''); if (!t) return ''; |
| 92 | const s = Math.max(1, Math.round((Date.now() - t) / 1000)); |
| 93 | if (s < 60) return 'now'; if (s < 3600) return `${Math.round(s / 60)} min ago`; if (s < 86400) return `${Math.round(s / 3600)} h ago`; |
| 94 | const d = Math.round(s / 86400); return d === 1 ? 'yesterday' : `${d} days ago`; |
| 95 | }; |
| 96 | const needsPerson = (r) => !!r && r.status === 'waiting' && !!r.waiting && r.waiting.for !== 'message'; |
| 97 | const isEnd = (r) => !!r && ['done', 'failed', 'stopped'].includes(r.status); |
| 98 | |
| 99 | // ── rich cards ── |
| 100 | const CARDS = new Map(); |
| 101 | function card(type, render) { if (typeof type === 'string' && /^[a-z][a-z0-9-]{0,40}$/.test(type) && typeof render === 'function') CARDS.set(type, render); } |
| 102 | card('results', (data) => { |
| 103 | const row = el('div', 'ovr-chips'); |
| 104 | for (const r of (Array.isArray(data.results) ? data.results : []).slice(0, 12)) { |
| 105 | const c = el('span', 'ovr-chip'); c.append(svg(r.kind === 'timer' ? 'timer' : r.kind === 'reminder' ? 'clock' : 'check'), el('span', '', r.label || r.text || r.kind || 'Done')); |
| 106 | if (r.undone) c.classList.add('is-undone'); |
| 107 | row.append(c); |
| 108 | } |
| 109 | return row; |
| 110 | }); |
| 111 | card('table', (data) => { |
| 112 | const wrap = el('div', 'ovr-table-wrap'); const t = el('table', 'ovr-table'); |
| 113 | const cols = Array.isArray(data.columns) ? data.columns.slice(0, 8) : []; |
| 114 | if (cols.length) { const tr = el('tr'); cols.forEach((c) => tr.append(el('th', '', c))); const th = el('thead'); th.append(tr); t.append(th); } |
| 115 | const tb = el('tbody'); |
| 116 | for (const row of (Array.isArray(data.rows) ? data.rows : []).slice(0, 50)) { const tr = el('tr'); (Array.isArray(row) ? row : []).slice(0, 8).forEach((v) => tr.append(el('td', '', v))); tb.append(tr); } |
| 117 | t.append(tb); wrap.append(t); return wrap; |
| 118 | }); |
| 119 | card('link', (data) => { |
| 120 | const url = httpsUrl(data.url); if (!url) return null; |
| 121 | const a = el('a', 'ovr-linkcard'); a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer'; |
| 122 | const img = httpsUrl(data.image); if (img) { const i = el('img'); i.src = img; i.alt = ''; i.loading = 'lazy'; a.append(i); } |
| 123 | const t = el('span', 'ovr-linkcard-t'); t.append(el('b', '', data.title || url), el('small', '', data.site || new URL(url).hostname)); a.append(t); |
| 124 | return a; |
| 125 | }); |
| 126 | card('code', (data) => { const pre = el('pre', 'ovr-code'); pre.append(el('code', '', String(data.code || data.text || '').slice(0, 20000))); return pre; }); |
| 127 | |
| 128 | // ── words: paragraphs, lists, links, inline code, numbered sources ── |
| 129 | function words(text, sources) { |
| 130 | const box = el('div', 'ovr-words'); |
| 131 | const raw = String(text || '').replace(/\r\n/g, '\n').slice(0, 65536); |
| 132 | const blocks = raw.split(/\n{2,}/); |
| 133 | for (const b of blocks) { |
| 134 | const lines = b.split('\n'); |
| 135 | if (lines.every((l) => /^\s*[-*•]\s+/.test(l) || !l.trim())) { |
| 136 | const ul = el('ul'); lines.filter((l) => l.trim()).forEach((l) => { const li = el('li'); inline(li, l.replace(/^\s*[-*•]\s+/, ''), sources); ul.append(li); }); box.append(ul); |
| 137 | } else if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l) || !l.trim())) { |
| 138 | const ol = el('ol'); lines.filter((l) => l.trim()).forEach((l) => { const li = el('li'); inline(li, l.replace(/^\s*\d+[.)]\s+/, ''), sources); ol.append(li); }); box.append(ol); |
| 139 | } else if (/^```/.test(b.trim())) { |
| 140 | const pre = el('pre', 'ovr-code'); pre.append(el('code', '', b.trim().replace(/^```[a-z0-9]*\n?/i, '').replace(/```$/, ''))); box.append(pre); |
| 141 | } else { const p = el('p'); lines.forEach((l, i) => { if (i) p.append(el('br')); inline(p, l, sources); }); box.append(p); } |
| 142 | } |
| 143 | return box; |
| 144 | } |
| 145 | function inline(node, text, sources) { |
| 146 | const re = /(https?:\/\/[^\s<>"')\]]+)|`([^`]{1,200})`|\*\*([^*]{1,200})\*\*|\[(\d{1,2})\]/g; |
| 147 | let last = 0, m; |
| 148 | while ((m = re.exec(text))) { |
| 149 | if (m.index > last) node.append(document.createTextNode(text.slice(last, m.index))); |
| 150 | if (m[1]) { const a = el('a', 'ovr-a', m[1]); a.href = m[1]; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; node.append(a); } |
| 151 | else if (m[2]) node.append(el('code', 'ovr-ic', m[2])); |
| 152 | else if (m[3]) node.append(el('strong', '', m[3])); |
| 153 | else if (m[4]) { |
| 154 | const src = (sources || []).find((s) => Number(s.n) === Number(m[4])); |
| 155 | const url = src && httpsUrl(src.url); |
| 156 | if (url) { const a = el('a', 'ovr-cite', m[4]); a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; a.title = src.title || src.site || url; node.append(a); } |
| 157 | else node.append(document.createTextNode(m[0])); |
| 158 | } |
| 159 | last = m.index + m[0].length; |
| 160 | } |
| 161 | if (last < text.length) node.append(document.createTextNode(text.slice(last))); |
| 162 | } |
| 163 | |
| 164 | // ── an agent's face: its picture, or its glyph on its hue; what it is doing as a small badge ── |
| 165 | function face(run, size = 'm') { |
| 166 | const a = (run && run.agent) || {}; |
| 167 | const f = el('span', `ovr-face is-${size}`); |
| 168 | f.style.setProperty('--ovr-h', String(Number.isInteger(a.hue) ? a.hue : hueOf(a.id || (run && run.kind)))); |
| 169 | const pic = httpsUrl(a.avatar_url); |
| 170 | if (pic) { const i = el('img'); i.src = pic; i.alt = ''; i.loading = 'lazy'; f.append(i); } |
| 171 | else f.append(svg(G[a.glyph] ? a.glyph : KIND_GLYPH[run && run.kind] || 'spark')); |
| 172 | if (run && run.doing && run.status === 'running') { const d = el('i', 'ovr-doing'); d.append(svg(G[run.doing.glyph] ? run.doing.glyph : 'spark')); f.append(d); f.classList.add('is-working'); } |
| 173 | if (needsPerson(run)) f.classList.add('is-needs'); |
| 174 | return f; |
| 175 | } |
| 176 | function statusLine(run) { |
| 177 | if (!run) return ''; |
| 178 | if (needsPerson(run)) return (run.waiting && run.waiting.what) || 'Needs you'; |
| 179 | if (run.status === 'running') return (run.doing && run.doing.text) || 'Working'; |
| 180 | if (run.status === 'planning') return 'Starting'; |
| 181 | if (run.status === 'waiting') return 'Your turn'; |
| 182 | return { done: 'Done', failed: 'Did not finish', stopped: 'Stopped' }[run.status] || ''; |
| 183 | } |
| 184 | |
| 185 | // ── the stream: a fetch reader over server-sent events (EventSource cannot send a bearer) ── |
| 186 | function follow({ url, headers, after, onEvent, signal }) { |
| 187 | let last = String(after || 0); |
| 188 | let stopped = false; |
| 189 | const once = async () => { |
| 190 | const r = await fetch(url, { headers: { ...headers, Accept: 'text/event-stream', 'Last-Event-ID': last }, signal, credentials: 'omit' }); |
| 191 | if (!r.ok || !r.body) throw new Error(`stream ${r.status}`); |
| 192 | const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = ''; |
| 193 | for (;;) { |
| 194 | const { value, done } = await reader.read(); if (done) return 'closed'; |
| 195 | buf += dec.decode(value, { stream: true }); |
| 196 | let i; |
| 197 | while ((i = buf.indexOf('\n\n')) >= 0) { |
| 198 | const raw = buf.slice(0, i); buf = buf.slice(i + 2); |
| 199 | let id = null, event = 'message', data = ''; |
| 200 | for (const line of raw.split('\n')) { |
| 201 | if (line.startsWith('id:')) id = line.slice(3).trim(); |
| 202 | else if (line.startsWith('event:')) event = line.slice(6).trim(); |
| 203 | else if (line.startsWith('data:')) data += line.slice(5).trim(); |
| 204 | } |
| 205 | let d = null; try { d = JSON.parse(data); } catch { d = null; } |
| 206 | if (event === 'step' && id && /^\d+$/.test(id) && Number(id) > Number(last)) last = id; |
| 207 | if (onEvent(event, d) === 'end') return 'end'; |
| 208 | } |
| 209 | } |
| 210 | }; |
| 211 | (async () => { |
| 212 | let failures = 0; |
| 213 | while (!stopped && !(signal && signal.aborted)) { |
| 214 | try { const how = await once(); failures = 0; if (how === 'end') return; } |
| 215 | catch { if (signal && signal.aborted) return; failures++; } |
| 216 | await new Promise((r) => setTimeout(r, Math.min(30000, 800 * 2 ** Math.min(failures, 5)))); |
| 217 | } |
| 218 | })(); |
| 219 | return () => { stopped = true; }; |
| 220 | } |
| 221 | |
| 222 | const FILTERS = [['all', 'All', 'spark'], ['chat', 'Chats', 'chat'], ['task', 'Tasks', 'robot'], ['code', 'Code', 'code']]; |
| 223 | const FILTER_IDS = FILTERS.map(([id]) => id); |
| 224 | |
| 225 | function mount(host, o = {}) { |
| 226 | styles(); |
| 227 | const base = String(o.base || 'https://openvibe.network/api/frame').replace(/\/+$/, ''); |
| 228 | const tokenOf = () => (typeof o.token === 'function' ? o.token() : o.token) || ''; |
| 229 | const headers = () => { const t = tokenOf(); return t ? { Authorization: `Bearer ${t}` } : {}; }; |
| 230 | const api = async (path, init = {}) => { |
| 231 | const r = await fetch(`${base}/runs${path}`, { ...init, credentials: 'omit', headers: { Accept: 'application/json', ...(init.body ? { 'Content-Type': 'application/json' } : {}), ...headers(), ...(init.headers || {}) } }); |
| 232 | const j = await r.json().catch(() => ({})); |
| 233 | if (!r.ok) { const e = new Error(j.error || `The runs did not answer (${r.status})`); e.status = r.status; e.code = j.code; throw e; } |
| 234 | return j; |
| 235 | }; |
| 236 | const box = el('div', 'ovr'); |
| 237 | host.textContent = ''; host.append(box); |
| 238 | const actionsBase = o.actions ? String(o.actions.base || 'https://openvibe.network').replace(/\/+$/, '') : ''; |
| 239 | const place = (p) => { try { if (typeof o.onPlace === 'function') o.onPlace(p); } catch { /* the host's */ } }; |
| 240 | let topHandle = null, paneHandle = null, paneId = null; |
| 241 | const dropTop = () => { if (topHandle && typeof topHandle.destroy === 'function') { try { topHandle.destroy(); } catch { /* */ } } topHandle = null; }; |
| 242 | const dropPane = () => { if (paneHandle && typeof paneHandle.destroy === 'function') { try { paneHandle.destroy(); } catch { /* */ } } paneHandle = null; paneId = null; }; |
| 243 | const paneFor = (f) => (o.panes && typeof o.panes[f] === 'function' ? o.panes[f] : null); |
| 244 | /** The Action Center's items (common.action-item@1) that need you or are working, as rows beside the runs. */ |
| 245 | async function actionRows(runs) { |
| 246 | if (!actionsBase || !['all', 'task'].includes(state.filter)) return []; |
| 247 | try { |
| 248 | const r = await fetch(`${actionsBase}/api/v1/me/actions`, { headers: { Accept: 'application/json', ...headers() }, credentials: 'omit' }); |
| 249 | if (!r.ok) return []; |
| 250 | const doc = await r.json(); |
| 251 | const shown = new Set(runs.map((x) => x.ref)); |
| 252 | runs.forEach((x) => { if (x.waiting && x.waiting.approval) shown.add(`approval:${x.waiting.approval}`); }); |
| 253 | const out = []; |
| 254 | for (const bucket of ['waiting', 'running']) { |
| 255 | for (const it of (doc && doc.buckets && Array.isArray(doc.buckets[bucket]) ? doc.buckets[bucket] : []).slice(0, 30)) { |
| 256 | if (!it || typeof it.id !== 'string' || shown.has(`${it.service}:${it.id}`) || (it.kind === 'approval' && shown.has(`approval:${it.id}`))) continue; |
| 257 | out.push({ |
| 258 | action: it, id: it.id, kind: it.kind === 'code_task' ? 'code' : 'task', title: it.title, url: it.href, |
| 259 | status: bucket === 'waiting' ? 'waiting' : 'running', updated_at: it.updated_at, |
| 260 | waiting: bucket === 'waiting' ? { for: 'approval', what: it.summary || it.title } : null, |
| 261 | doing: bucket === 'running' ? { text: it.step || it.summary || 'Working', glyph: it.kind === 'code_task' ? 'code' : 'robot' } : null, |
| 262 | preview: it.summary ? { text: it.summary } : null, |
| 263 | agent: { id: `${it.service}`, name: it.agent || SERVICE_NAME[it.service] || 'OpenVibe', glyph: it.kind === 'code_task' ? 'code' : it.kind === 'ai_run' ? 'spark' : 'robot' }, |
| 264 | }); |
| 265 | } |
| 266 | } |
| 267 | return out; |
| 268 | } catch { return []; } |
| 269 | } |
| 270 | const state = { view: 'list', filter: FILTER_IDS.includes(o.filter) ? o.filter : 'all', runs: [], run: null, steps: new Map(), stopStream: null, abort: null, gen: 0 }; |
| 271 | |
| 272 | function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; } |
| 273 | |
| 274 | // ── the composer: the shared one when it is there, else a plain box ── |
| 275 | function composer(where, { placeholder, onSend }) { |
| 276 | const slot = el('div', 'ovr-compose'); |
| 277 | where.append(slot); |
| 278 | const plain = () => { |
| 279 | const f = el('form', 'ovr-plain'); const ta = el('textarea'); ta.rows = 2; ta.placeholder = placeholder; ta.setAttribute('aria-label', placeholder); |
| 280 | const go = el('button', 'ovr-send'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(svg('send')); |
| 281 | f.append(ta, go); |
| 282 | f.addEventListener('submit', async (e) => { |
| 283 | e.preventDefault(); const t = ta.value.trim(); if (!t) return; go.disabled = true; |
| 284 | const old = slot.querySelector('.ovr-note'); if (old) old.remove(); |
| 285 | try { if (await onSend({ text: t }) !== false) ta.value = ''; } |
| 286 | catch (err) { slot.append(el('p', 'ovr-note is-err', (err && err.message) || 'That did not send')); } |
| 287 | finally { go.disabled = false; } |
| 288 | }); |
| 289 | ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); f.requestSubmit ? f.requestSubmit() : f.dispatchEvent(new Event('submit', { cancelable: true })); } }); |
| 290 | slot.append(f); |
| 291 | }; |
| 292 | const C = root.OpenVibeComposer; |
| 293 | const real = () => { |
| 294 | try { |
| 295 | // The composer notes a failed send itself and keeps the words. |
| 296 | root.OpenVibeComposer.mount(slot, { preset: 'agent', placeholder, label: placeholder, examples: o.examples, user: o.me, noDraft: true, |
| 297 | onSubmit: (v) => onSend({ text: v.text, files: v.files, mode: v.mode }) }); |
| 298 | } catch { slot.textContent = ''; plain(); } |
| 299 | }; |
| 300 | if (C && C.mount) real(); |
| 301 | else if (typeof o.sibling === 'function') { |
| 302 | let sc = document.getElementById('ov-composer-script'); |
| 303 | if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = o.sibling('ov-composer.js'); document.head.appendChild(sc); } |
| 304 | plain(); |
| 305 | sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { slot.textContent = ''; real(); } }, { once: true }); |
| 306 | } else plain(); |
| 307 | } |
| 308 | |
| 309 | /** A new run with these words (Vibe answers, or hands the work on), then its thread. */ |
| 310 | async function start(text) { |
| 311 | const run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: String(text || '').slice(0, 16000) }) }); |
| 312 | open(run.ref || run.id, run); |
| 313 | return run; |
| 314 | } |
| 315 | |
| 316 | // ── the list ── |
| 317 | async function list(filter) { |
| 318 | if (filter && FILTER_IDS.includes(filter)) state.filter = filter; |
| 319 | clearStream(); |
| 320 | const gen = ++state.gen; |
| 321 | // The list's frame (filters, the box) is drawn once; a filter only reloads the rows, so the words being typed stay. |
| 322 | if (state.view !== 'list' || !box.querySelector('.ovr-rows')) { |
| 323 | state.view = 'list'; state.run = null; |
| 324 | box.textContent = ''; box.className = 'ovr is-list'; |
| 325 | const head = el('div', 'ovr-head'); |
| 326 | const tabs = el('div', 'ovr-filters'); tabs.setAttribute('role', 'tablist'); tabs.setAttribute('aria-label', 'Show'); |
| 327 | for (const [id, label, ic] of FILTERS) { |
| 328 | const b = el('button', 'ovr-filter'); b.type = 'button'; b.setAttribute('role', 'tab'); b.dataset.filter = id; |
| 329 | b.append(svg(ic), el('span', '', label)); |
| 330 | b.addEventListener('click', () => { if (state.filter !== id || state.view !== 'list') list(id); }); |
| 331 | tabs.append(b); |
| 332 | } |
| 333 | tabs.addEventListener('keydown', (e) => { |
| 334 | if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; |
| 335 | const all = [...tabs.children]; const k = all.indexOf(document.activeElement); if (k < 0) return; |
| 336 | const next = all[(k + (e.key === 'ArrowRight' ? 1 : all.length - 1)) % all.length]; |
| 337 | e.preventDefault(); next.focus(); next.click(); |
| 338 | }); |
| 339 | head.append(tabs); |
| 340 | const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs'); |
| 341 | const foot = el('div', 'ovr-list-note'); |
| 342 | const top = el('div', 'ovr-top'); |
| 343 | const pane = el('div', 'ovr-pane'); pane.hidden = true; |
| 344 | box.append(head, top, rows, foot, pane); |
| 345 | dropTop(); dropPane(); |
| 346 | if (Array.isArray(o.starters) && o.starters.length) { |
| 347 | const st = el('div', 'ovr-starters'); st.setAttribute('role', 'group'); st.setAttribute('aria-label', 'Things to ask'); |
| 348 | o.starters.slice(0, 8).forEach((x) => { |
| 349 | const [ic, words] = Array.isArray(x) ? x : ['spark', String(x)]; |
| 350 | const b = el('button', 'ovr-starter'); b.type = 'button'; b.append(svg(G[ic] ? ic : 'spark'), el('span', '', words)); |
| 351 | b.addEventListener('click', async () => { b.disabled = true; try { await start(words); } catch (err) { b.disabled = false; foot.textContent = ''; foot.append(el('p', 'ovr-note is-err', err.message)); } }); |
| 352 | st.append(b); |
| 353 | }); |
| 354 | box.append(st); |
| 355 | } |
| 356 | composer(box, { placeholder: o.placeholder || 'Ask, or say what to make', onSend: (v) => start(v.text) }); |
| 357 | } |
| 358 | place({ filter: state.filter }); |
| 359 | box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; }); |
| 360 | // A filter the host draws itself (a pane): it takes the place of the rows, the starters and the box. The host's |
| 361 | // pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes. |
| 362 | const pf = paneFor(state.filter); |
| 363 | const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane'); |
| 364 | const wantTop = !pf && ['all', 'task'].includes(state.filter) && typeof o.top === 'function'; |
| 365 | if (!wantTop) { dropTop(); topEl.textContent = ''; } |
| 366 | if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; } |
| 367 | box.classList.toggle('has-pane', !!pf); |
| 368 | paneEl.hidden = !pf; topEl.hidden = !wantTop; |
| 369 | { const st = box.querySelector('.ovr-starters'); if (st) st.hidden = !!pf || !['all', 'chat'].includes(state.filter); } |
| 370 | if (pf) { |
| 371 | if (!paneHandle) { paneId = state.filter; try { paneHandle = pf(paneEl) || {}; } catch { paneHandle = {}; } } |
| 372 | const rows0 = box.querySelector('.ovr-rows'); rows0.textContent = ''; rows0.removeAttribute('aria-busy'); box.querySelector('.ovr-list-note').textContent = ''; |
| 373 | return; |
| 374 | } |
| 375 | if (wantTop && !topHandle) { try { topHandle = o.top(topEl) || null; } catch { topHandle = null; } } |
| 376 | const rows = box.querySelector('.ovr-rows'); const foot = box.querySelector('.ovr-list-note'); |
| 377 | rows.textContent = ''; foot.textContent = ''; rows.setAttribute('aria-busy', 'true'); |
| 378 | for (let i = 0; i < 3; i++) rows.append(el('li', 'ovr-row is-ghost')); |
| 379 | let out; |
| 380 | try { out = await api(`?limit=40${state.filter !== 'all' ? `&kind=${state.filter}` : ''}`); } |
| 381 | catch (err) { |
| 382 | if (gen !== state.gen) return; |
| 383 | rows.textContent = ''; rows.removeAttribute('aria-busy'); |
| 384 | foot.append(empty(err.status === 401 ? 'Sign in to see your runs' : 'Your runs did not load', err.status === 401 ? 'Your chats, tasks and code are here once you sign in.' : 'Check your connection; they come back on their own.')); |
| 385 | return; |
| 386 | } |
| 387 | if (gen !== state.gen) return; |
| 388 | state.runs = Array.isArray(out.runs) ? out.runs : []; |
| 389 | const extra = await actionRows(state.runs); |
| 390 | if (gen !== state.gen) return; |
| 391 | if (extra.length) { |
| 392 | // What needs you first, then newest: the Action Center's rows join in that order. |
| 393 | const needs = (r) => (needsPerson(r) ? 1 : 0); |
| 394 | state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || ''))); |
| 395 | } |
| 396 | rows.textContent = ''; rows.removeAttribute('aria-busy'); |
| 397 | if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream.' : 'Ask anything below. Chats, tasks and code you start show up here.')); |
| 398 | state.runs.forEach((r, i) => { const li = el('li'); li.append(row(r, i)); const acts = answerRow(r); if (acts) { li.classList.add('has-acts'); li.append(acts); } rows.append(li); }); |
| 399 | if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`)); |
| 400 | } |
| 401 | function row(r, i) { |
| 402 | const away = r.action ? httpsUrl(r.url) : ''; |
| 403 | const b = away ? el('a', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`) : el('button', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`); |
| 404 | if (away) { b.href = away; b.target = '_blank'; b.rel = 'noopener'; } else b.type = 'button'; |
| 405 | b.style.setProperty('--i', String(Math.min(i, 12))); |
| 406 | const t = el('span', 'ovr-row-t'); |
| 407 | const top = el('span', 'ovr-row-top'); top.append(el('b', '', r.title || (r.agent && r.agent.name) || KIND_WORD[r.kind] || 'Run'), el('time', '', ago(r.updated_at))); |
| 408 | const line = el('span', 'ovr-row-line', needsPerson(r) || r.status === 'running' ? statusLine(r) : (r.preview && r.preview.text) || statusLine(r)); |
| 409 | const meta = el('span', 'ovr-row-meta'); meta.append(svg(KIND_GLYPH[r.kind] || 'spark'), el('span', '', [KIND_WORD[r.kind] || 'Task', r.agent && r.agent.name && r.agent.name !== r.title ? r.agent.name : ''].filter(Boolean).join(' · '))); |
| 410 | if (needsPerson(r)) meta.append(el('i', 'ovr-pill is-needs', 'Needs you')); |
| 411 | else if (r.status === 'running') meta.append(el('i', 'ovr-pill is-working', 'Working')); |
| 412 | else if (r.status === 'failed') meta.append(el('i', 'ovr-pill is-failed', 'Did not finish')); |
| 413 | t.append(top, line, meta); |
| 414 | b.append(face(r), t); |
| 415 | if (!away) b.addEventListener('click', () => open(r.ref || r.id, r)); |
| 416 | return b; |
| 417 | } |
| 418 | /** A run that asks before it acts is answered from the list (the Action Center's approve in place). */ |
| 419 | function answerRow(r) { |
| 420 | const w = r && r.waiting; |
| 421 | const it = r && r.action; |
| 422 | if (it && !(needsPerson(r) && Array.isArray(it.actions) && it.actions.includes('approve'))) return null; |
| 423 | if (!it && (!needsPerson(r) || w.for !== 'approval' || !w.approval || !/^[A-Za-z0-9._~-]{1,160}$/.test(String(w.approval)))) return null; |
| 424 | const box2 = el('div', 'ovr-row-acts'); |
| 425 | const say = (text, err) => { box2.textContent = ''; box2.append(el('p', `ovr-note${err ? ' is-err' : ''}`, text)); }; |
| 426 | [['approve', 'Approve', true], ['decline', 'Not now', false]].forEach(([id, label, primary]) => { |
| 427 | const b = el('button', primary ? 'ovr-btn is-primary' : 'ovr-btn', label); b.type = 'button'; |
| 428 | b.setAttribute('aria-label', `${label}: ${r.title || 'this run'}`); |
| 429 | b.addEventListener('click', async () => { |
| 430 | [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = true; }); |
| 431 | try { |
| 432 | if (it) { |
| 433 | const res = await fetch(`${actionsBase}/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/${id}`, { method: 'POST', credentials: 'omit', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...headers() }, body: '{}' }); |
| 434 | if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.detail || j.error || j.title || `That did not go through (${res.status})`); } |
| 435 | } else await api(`/${encodeURIComponent(r.ref || r.id)}/approvals/${encodeURIComponent(w.approval)}`, { method: 'POST', body: JSON.stringify({ answer: id }) }); |
| 436 | say(id === 'approve' ? 'Approved' : 'Declined'); |
| 437 | } |
| 438 | catch (err) { [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = false; }); box2.append(el('p', 'ovr-note is-err', err.message)); } |
| 439 | }); |
| 440 | box2.append(b); |
| 441 | }); |
| 442 | return box2; |
| 443 | } |
| 444 | function empty(title, sub) { const e = el('div', 'ovr-empty'); e.append(svg('spark')); const w = el('div'); w.append(el('b', '', title), el('span', '', sub)); e.append(w); return e; } |
| 445 | |
| 446 | // ── the thread ── |
| 447 | async function open(ref, known) { |
| 448 | clearStream(); |
| 449 | dropTop(); dropPane(); |
| 450 | const gen = ++state.gen; |
| 451 | state.view = 'thread'; state.run = known || null; state.steps = new Map(); |
| 452 | place({ ref }); |
| 453 | box.textContent = ''; box.className = 'ovr is-thread'; |
| 454 | const head = el('div', 'ovr-thead'); |
| 455 | const back = el('button', 'ovr-ib'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your runs'); back.title = 'Your runs'; back.append(svg('back')); |
| 456 | back.addEventListener('click', () => list()); |
| 457 | const who = el('div', 'ovr-who'); const title = el('b'); const sub = el('small'); |
| 458 | who.append(title, sub); |
| 459 | const away = el('a', 'ovr-ib'); away.setAttribute('aria-label', 'Open on its own page'); away.title = 'Open on its own page'; away.target = '_blank'; away.rel = 'noopener'; away.append(svg('open')); |
| 460 | const faceSlot = el('span', 'ovr-face-slot'); |
| 461 | head.append(back, faceSlot, who, away); |
| 462 | const thread = el('div', 'ovr-thread'); thread.setAttribute('role', 'log'); thread.setAttribute('aria-live', 'polite'); |
| 463 | box.append(head, thread); |
| 464 | const paintHead = (r) => { |
| 465 | state.run = r; |
| 466 | faceSlot.textContent = ''; faceSlot.append(face(r, 's')); |
| 467 | title.textContent = r.title || (r.agent && r.agent.name) || 'Run'; |
| 468 | sub.textContent = [KIND_WORD[r.kind] || 'Task', statusLine(r)].filter(Boolean).join(' · '); |
| 469 | const url = httpsUrl(r.url); away.hidden = !url; if (url) away.href = url; |
| 470 | box.classList.toggle('is-needs', needsPerson(r)); |
| 471 | }; |
| 472 | if (known) paintHead(known); |
| 473 | composer(box, { placeholder: 'Say the next thing', onSend: async (v) => { const step = await api(`/${encodeURIComponent(ref)}/messages`, { method: 'POST', body: JSON.stringify({ text: v.text }) }); add(step); } }); |
| 474 | const add = (s) => { |
| 475 | if (!s || !Number.isInteger(s.seq)) return; |
| 476 | if (state.steps.has(s.seq)) { const old = state.steps.get(s.seq); const n = stepNode(s); old.replaceWith(n); state.steps.set(s.seq, n); return; } |
| 477 | const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160; |
| 478 | const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove(); |
| 479 | const n = stepNode(s); |
| 480 | state.steps.set(s.seq, n); |
| 481 | const after = [...state.steps.keys()].filter((k) => k > s.seq).sort((a, b) => a - b)[0]; |
| 482 | if (after !== undefined && state.steps.get(after).parentNode === thread) thread.insertBefore(n, state.steps.get(after)); else thread.append(n); |
| 483 | if (wasNear) requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; }); |
| 484 | }; |
| 485 | try { |
| 486 | const [r, page] = await Promise.all([api(`/${encodeURIComponent(ref)}`), api(`/${encodeURIComponent(ref)}/steps?limit=200`)]); |
| 487 | if (gen !== state.gen) return; |
| 488 | paintHead(r); |
| 489 | (page.steps || []).forEach(add); |
| 490 | requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; }); |
| 491 | if (!(page.steps || []).length) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.')); |
| 492 | } catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; } |
| 493 | if (isEnd(state.run)) return; |
| 494 | const last = Math.max(0, ...state.steps.keys()); |
| 495 | state.abort = new AbortController(); |
| 496 | state.stopStream = follow({ url: `${base}/runs/${encodeURIComponent(ref)}/events`, headers: headers(), after: last, signal: state.abort.signal, |
| 497 | onEvent(event, d) { |
| 498 | if (gen !== state.gen) return 'end'; |
| 499 | if (event === 'step' && d && d.step) add(d.step); |
| 500 | else if (event === 'delta' && d && state.steps.has(d.seq)) { const w = state.steps.get(d.seq).querySelector('.ovr-words'); if (w) w.append(document.createTextNode(String(d.text || ''))); } |
| 501 | else if (event === 'run' && d && d.run) paintHead(d.run); |
| 502 | else if (event === 'end') { if (state.run) paintHead({ ...state.run, status: (d && d.status) || 'done' }); return 'end'; } |
| 503 | return null; |
| 504 | } }); |
| 505 | } |
| 506 | |
| 507 | function stepNode(s) { |
| 508 | const from = s.from || (s.kind === 'message' ? 'agent' : 'agent'); |
| 509 | if (s.kind === 'message' && from === 'person') { |
| 510 | const n = el('div', 'ovr-step is-you'); const b = el('div', 'ovr-bubble'); b.append(words(s.text)); n.append(b); |
| 511 | attachments(n, s); return n; |
| 512 | } |
| 513 | if (s.kind === 'message' || (s.kind !== 'tool' && s.kind !== 'think' && s.kind !== 'approval' && s.kind !== 'child' && s.kind !== 'file' && s.text)) { |
| 514 | const n = el('div', `ovr-step is-agent${s.status === 'running' ? ' is-writing' : ''}`); |
| 515 | if (s.author && s.author.name) n.append(el('small', 'ovr-author', s.author.name)); |
| 516 | n.append(words(s.text, s.sources)); |
| 517 | cardFor(n, s); |
| 518 | if (Array.isArray(s.sources) && s.sources.length) { |
| 519 | const src = el('div', 'ovr-sources'); |
| 520 | s.sources.slice(0, 12).forEach((x) => { const url = httpsUrl(x.url); if (!url) return; const a = el('a', 'ovr-source'); a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; a.append(el('i', '', String(x.n)), el('span', '', x.title || x.site || new URL(url).hostname)); src.append(a); }); |
| 521 | n.append(src); |
| 522 | } |
| 523 | attachments(n, s); return n; |
| 524 | } |
| 525 | if (s.kind === 'think') { |
| 526 | const d = el('details', 'ovr-step is-think'); const sm = el('summary'); sm.append(svg('think'), el('span', '', s.title || 'Thinking')); |
| 527 | d.append(sm); if (s.text) d.append(words(s.text)); return d; |
| 528 | } |
| 529 | if (s.kind === 'approval') { |
| 530 | const n = el('div', 'ovr-step is-approval'); const head = el('div', 'ovr-approval-h'); head.append(svg('check'), el('b', '', s.title || 'It asks before it goes on')); |
| 531 | n.append(head); if (s.text) n.append(words(s.text)); |
| 532 | const r = s.result && typeof s.result === 'object' ? s.result : {}; |
| 533 | const opts = Array.isArray(r.options) ? r.options : [{ id: 'approve', label: 'Approve' }, { id: 'decline', label: 'Not now' }]; |
| 534 | const row = el('div', 'ovr-approval-acts'); |
| 535 | opts.slice(0, 4).forEach((op, i) => { |
| 536 | const b = el('button', i === 0 ? 'ovr-btn is-primary' : 'ovr-btn', op.label || op.id); b.type = 'button'; |
| 537 | b.addEventListener('click', async () => { |
| 538 | [...row.children].forEach((x) => { x.disabled = true; }); |
| 539 | try { await api(`/${encodeURIComponent(state.run.ref || state.run.id)}/approvals/${encodeURIComponent(r.approval || r.id || String(s.seq))}`, { method: 'POST', body: JSON.stringify({ answer: op.id }) }); row.replaceWith(el('p', 'ovr-note', `You answered: ${op.label || op.id}`)); } |
| 540 | catch (err) { [...row.children].forEach((x) => { x.disabled = false; }); n.append(el('p', 'ovr-note is-err', err.message)); } |
| 541 | }); |
| 542 | row.append(b); |
| 543 | }); |
| 544 | if (!r.answer) n.append(row); else n.append(el('p', 'ovr-note', `Answered: ${r.answer}`)); |
| 545 | return n; |
| 546 | } |
| 547 | // tool, child, file: one quiet row |
| 548 | const n = el(s.text && s.kind === 'tool' ? 'details' : 'div', `ovr-step is-row is-${s.kind}${s.status === 'running' ? ' is-running' : s.status === 'failed' ? ' is-failed' : ''}`); |
| 549 | const line = el(s.text && s.kind === 'tool' ? 'summary' : 'div', 'ovr-row-step'); |
| 550 | const glyph = G[s.glyph] ? s.glyph : s.kind === 'child' ? 'child' : s.kind === 'file' ? 'file' : 'tool'; |
| 551 | line.append(svg(glyph), el('span', '', s.title || (s.tool_call && s.tool_call.name) || (s.kind === 'file' ? s.text : s.kind === 'child' ? 'A helper took part of the work' : 'A step'))); |
| 552 | if (s.status === 'running') line.append(el('i', 'ovr-pulse')); |
| 553 | if (s.status === 'failed') line.append(svg('x', 'ovr-fail')); |
| 554 | n.append(line); |
| 555 | if (s.text && s.kind === 'tool') n.append(words(s.text)); |
| 556 | if (s.kind === 'child' && s.child_run) { n.classList.add('is-link'); n.addEventListener('click', () => open(s.child_run)); } |
| 557 | return n; |
| 558 | } |
| 559 | function cardFor(n, s) { |
| 560 | if (!s.ui || !s.ui.type) return; |
| 561 | const render = CARDS.get(s.ui.type); if (!render) return; |
| 562 | try { const c = render(s.ui.data || {}, s, { open }); if (c) { c.classList && c.classList.add('ovr-card'); n.append(c); } } catch { /* the words above say it */ } |
| 563 | } |
| 564 | function attachments(n, s) { |
| 565 | const list = Array.isArray(s.attachments) ? s.attachments : []; if (!list.length) return; |
| 566 | const row = el('div', 'ovr-files'); |
| 567 | list.slice(0, 8).forEach((a) => { |
| 568 | const url = httpsUrl(a.url); |
| 569 | if (a.kind === 'image' && url) { const i = el('img', 'ovr-thumb'); i.src = url; i.alt = a.alt || ''; i.loading = 'lazy'; row.append(i); } |
| 570 | else { const c = el('span', 'ovr-chip'); c.append(svg('file'), el('span', '', a.name || a.kind)); row.append(c); } |
| 571 | }); |
| 572 | n.append(row); |
| 573 | } |
| 574 | |
| 575 | /** Words in the list's box, unsent, with the focus there (a page's "Ask your agent"). */ |
| 576 | function prefill(text) { |
| 577 | if (state.view !== 'list' || paneFor(state.filter)) list(paneFor(state.filter) ? 'all' : undefined); |
| 578 | const put = (n = 0) => { |
| 579 | const ta = box.querySelector('.ovr-compose textarea'); |
| 580 | if (!ta) { if (n < 40) setTimeout(() => put(n + 1), 50); return; } |
| 581 | ta.value = String(text || '').slice(0, 16000); |
| 582 | ta.dispatchEvent(new Event('input', { bubbles: true })); |
| 583 | try { ta.focus({ preventScroll: true }); ta.setSelectionRange(ta.value.length, ta.value.length); } catch { /* */ } |
| 584 | }; |
| 585 | put(); |
| 586 | } |
| 587 | const view = { list, open, start, prefill, destroy() { clearStream(); dropTop(); dropPane(); state.gen++; host.textContent = ''; }, get state() { return { view: state.view, run: state.run, filter: state.filter }; } }; |
| 588 | if (o.open) open(o.open); else list(); |
| 589 | return view; |
| 590 | } |
| 591 | |
| 592 | let styled = false; |
| 593 | function styles() { |
| 594 | if (styled || document.getElementById('ovr-styles')) { styled = true; return; } |
| 595 | styled = true; |
| 596 | const st = document.createElement('style'); st.id = 'ovr-styles'; |
| 597 | st.textContent = ` |
| 598 | .ovr { --ovr-gap: 12px; display: flex; flex-direction: column; min-height: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--text-primary, #e6edf7); } |
| 599 | .ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; } |
| 600 | .ovr-head { padding: 4px 4px 8px; } |
| 601 | .ovr-filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); } |
| 602 | .ovr-filter { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border-radius: 12px; cursor: pointer; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-weight: 800; font-size: 14px; color: var(--text-secondary, #a8b3c4); transition: background-color .18s, color .18s; } |
| 603 | .ovr-filter[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); } |
| 604 | .ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); } |
| 605 | .ovr-filter:focus-visible, .ovr-row:focus-visible, .ovr-ib:focus-visible, .ovr-btn:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; } |
| 606 | @media (max-width: 380px) { .ovr-filter { font-size: 13px; gap: 5px; } .ovr-filter svg { width: 16px; height: 16px; } } |
| 607 | .ovr-rows { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; align-content: start; gap: 6px; margin: 0; padding: 2px 4px 10px; list-style: none; } |
| 608 | .ovr-rows > li { display: grid; min-width: 0; } |
| 609 | .ovr-rows > li.has-acts { border-radius: 16px; background: color-mix(in srgb, #f59e0b 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 50%, transparent); } |
| 610 | .ovr-rows > li.has-acts > .ovr-row { background: transparent; box-shadow: none; } |
| 611 | .ovr-row-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 12px 12px 72px; } |
| 612 | .ovr-row-acts .ovr-btn { min-height: 38px; font-size: 13.5px; } |
| 613 | .ovr-row-acts .ovr-note { margin: 0; } |
| 614 | @media (max-width: 380px) { .ovr-row-acts { padding-left: 12px; } } |
| 615 | .ovr-rows:empty { display: none; } .ovr-list-note:empty { display: none; } .ovr-list-note { padding: 0 4px; } |
| 616 | .ovr.is-list > .ovr-compose, .ovr.is-list > .ovr-starters:not([hidden]) { margin-top: auto; } |
| 617 | .ovr.is-list > .ovr-starters:not([hidden]) + .ovr-compose { margin-top: 0; } |
| 618 | /* On a wide screen the list, the thread and the box keep a reading column. */ |
| 619 | .ovr-head, .ovr-top, .ovr-rows, .ovr-list-note, .ovr-starters, .ovr-compose, .ovr-thead { width: 100%; max-width: 780px; margin-inline: auto; box-sizing: border-box; } |
| 620 | .ovr-thread { padding-inline: max(4px, calc((100% - 772px) / 2)); } |
| 621 | .ovr-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; min-height: 72px; padding: 10px 12px; border-radius: 16px; cursor: pointer; background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.07)); transition: background-color .15s, transform .15s; animation: ovr-rise .3s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i, 0) * 18ms); } |
| 622 | .ovr-row.is-needs { box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); } |
| 623 | .ovr-row:active { transform: scale(.985); } |
| 624 | @media (hover: hover) and (pointer: fine) { .ovr-row:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); } } |
| 625 | .ovr-row.is-ghost { min-height: 72px; animation: ovr-shimmer 1.4s ease-in-out infinite; cursor: default; } |
| 626 | .ovr-row-t { flex: 1; min-width: 0; display: grid; gap: 3px; } |
| 627 | .ovr-row-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; } |
| 628 | .ovr-row-top b { flex: 1; min-width: 0; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-size: 15px; font-weight: 800; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow-wrap: anywhere; } |
| 629 | .ovr-row-top time { flex: none; font-size: 12px; color: var(--text-muted, #93a0b4); } |
| 630 | .ovr-row-line { font-size: 13.5px; color: var(--text-secondary, #a8b3c4); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere; line-height: 1.35; } |
| 631 | .ovr-row-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted, #93a0b4); } |
| 632 | .ovr-row-meta svg { width: 14px; height: 14px; } |
| 633 | .ovr-pill { font-style: normal; font-size: 11.5px; font-weight: 750; padding: 1px 8px; border-radius: 999px; margin-left: auto; } |
| 634 | .ovr-pill.is-needs { color: #fbbf24; background: color-mix(in srgb, #f59e0b 16%, transparent); } |
| 635 | .ovr-pill.is-working { color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } |
| 636 | .ovr-pill.is-failed { color: #f87171; background: color-mix(in srgb, #ef4444 14%, transparent); } |
| 637 | .ovr-face { position: relative; flex: none; display: grid; place-items: center; border-radius: 30%; color: hsl(var(--ovr-h, 215) 92% 84%); background: linear-gradient(150deg, hsl(var(--ovr-h, 215) 72% 58% / .4), hsl(var(--ovr-h, 215) 70% 38% / .22)); box-shadow: inset 0 0 0 1px hsl(var(--ovr-h, 215) 70% 64% / .3); } |
| 638 | :root[data-theme="light"] .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); } |
| 639 | @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); } } |
| 640 | .ovr-face.is-m { width: 48px; height: 48px; } .ovr-face.is-m > svg { width: 26px; height: 26px; } |
| 641 | .ovr-face.is-s { width: 38px; height: 38px; } .ovr-face.is-s > svg { width: 21px; height: 21px; } |
| 642 | .ovr-face img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; } |
| 643 | .ovr-face.is-needs { box-shadow: 0 0 0 2px #f59e0b; } |
| 644 | .ovr-face.is-working { animation: ovr-breathe 2.4s ease-in-out infinite; } |
| 645 | .ovr-doing { position: absolute; right: -4px; bottom: -4px; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; color: #fff; background: linear-gradient(140deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #a855f7)); box-shadow: 0 0 0 2px var(--bg-secondary, #121826); } |
| 646 | .ovr-doing svg { width: 12px; height: 12px; stroke-width: 2.3; } |
| 647 | .ovr-empty { display: flex; align-items: center; gap: 14px; margin: 6px 0; padding: 16px; border-radius: 16px; border: 1px dashed color-mix(in srgb, var(--text-muted, #93a0b4) 45%, transparent); } |
| 648 | .ovr-empty > svg { width: 24px; height: 24px; color: var(--accent-light, #60a5fa); } |
| 649 | .ovr-empty > div { display: grid; gap: 2px; } .ovr-empty b { font-size: 14.5px; } .ovr-empty span { font-size: 13px; color: var(--text-muted, #93a0b4); } |
| 650 | .ovr-note { margin: 6px 4px; font-size: 13px; color: var(--text-muted, #93a0b4); } .ovr-note.is-err { color: #f87171; } |
| 651 | .ovr-compose { flex: none; padding: 8px 4px 4px; } |
| 652 | .ovr-top { flex: none; } |
| 653 | .ovr-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .ovr-pane[hidden] { display: none; } |
| 654 | .ovr.has-pane > .ovr-rows, .ovr.has-pane > .ovr-list-note, .ovr.has-pane > .ovr-compose { display: none; } .ovr-top[hidden], .ovr-starters[hidden] { display: none; } .ovr-top:empty { display: none; } |
| 655 | .ovr-starters { flex: none; display: flex; gap: 6px; overflow-x: auto; padding: 8px 4px 0; scrollbar-width: none; -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent); mask-image: linear-gradient(90deg, #000 92%, transparent); } |
| 656 | .ovr-starters::-webkit-scrollbar { display: none; } |
| 657 | .ovr-starter { all: unset; box-sizing: border-box; flex: none; display: inline-flex; align-items: center; gap: 7px; min-height: 38px; padding: 0 13px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13.5px; white-space: nowrap; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.07)); transition: background-color .15s, color .15s; } |
| 658 | .ovr-starter svg { width: 16px; height: 16px; color: var(--accent-light, #60a5fa); } |
| 659 | .ovr-starter:disabled { opacity: .5; cursor: default; } .ovr-starter:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; } |
| 660 | @media (hover: hover) and (pointer: fine) { .ovr-starter:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); } } |
| 661 | a.ovr-row { color: inherit; text-decoration: none; } |
| 662 | .ovr-plain { display: flex; align-items: flex-end; gap: 8px; padding: 8px; border-radius: 18px; border: 1px solid var(--border, rgba(148,163,184,.2)); background: var(--bg-primary, rgba(0,0,0,.2)); } |
| 663 | .ovr-plain textarea { flex: 1; min-height: 40px; max-height: 160px; resize: none; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; font-size: 15px; } |
| 664 | .ovr-send { all: unset; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; color: #fff; background: var(--accent-strong, var(--accent, #2563eb)); } |
| 665 | .ovr-thead { flex: none; display: flex; align-items: center; gap: 10px; padding: 4px 4px 10px; border-bottom: 1px solid var(--border, rgba(255,255,255,.07)); } |
| 666 | .ovr-ib { all: unset; box-sizing: border-box; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); transition: background-color .15s, transform .12s; } |
| 667 | .ovr-ib[hidden] { display: none; } .ovr-ib:active { transform: scale(.92); } |
| 668 | @media (hover: hover) { .ovr-ib:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 13%, transparent); } } |
| 669 | .ovr-who { flex: 1; min-width: 0; display: grid; gap: 1px; } |
| 670 | .ovr-who b { font-family: 'OV Brand', Nunito, Inter, sans-serif; font-size: 15.5px; font-weight: 800; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } |
| 671 | .ovr-who small { font-size: 12.5px; color: var(--text-secondary, #a8b3c4); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } |
| 672 | .ovr.is-needs .ovr-who small { color: #fbbf24; } |
| 673 | .ovr-thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 10px; padding: 12px 4px; } |
| 674 | .ovr-step { max-width: min(640px, 92%); animation: ovr-rise .26s cubic-bezier(.2,.8,.2,1) both; } |
| 675 | .ovr-step.is-you { align-self: flex-end; } |
| 676 | .ovr-bubble { padding: 9px 13px; border-radius: 18px 18px 6px 18px; color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } |
| 677 | .ovr-bubble .ovr-a { color: inherit; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); text-underline-offset: 2px; } |
| 678 | .ovr-step.is-agent { align-self: flex-start; padding: 10px 14px; border-radius: 18px 18px 18px 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); } |
| 679 | .ovr-step.is-writing .ovr-words > :last-child::after { content: ''; display: inline-block; width: .5em; height: 1em; margin-left: 2px; vertical-align: -2px; border-radius: 2px; background: currentColor; opacity: .5; animation: ovr-caret 1s steps(2) infinite; } |
| 680 | .ovr-author { display: block; margin-bottom: 3px; font-size: 12px; font-weight: 750; color: var(--text-muted, #93a0b4); } |
| 681 | .ovr-words { font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; } |
| 682 | .ovr-words p { margin: 0; } .ovr-words p + p, .ovr-words p + ul, .ovr-words ul + p, .ovr-words ol + p, .ovr-words p + ol { margin-top: 8px; } |
| 683 | .ovr-words ul, .ovr-words ol { margin: 0; padding-left: 20px; } .ovr-words li + li { margin-top: 3px; } |
| 684 | .ovr-a { color: var(--accent-light, #60a5fa); text-decoration: underline; text-underline-offset: 2px; } |
| 685 | .ovr-ic { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; padding: 1px 5px; border-radius: 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); } |
| 686 | .ovr-cite { display: inline-grid; place-items: center; min-width: 18px; height: 18px; margin: 0 2px; padding: 0 4px; border-radius: 6px; font-size: 11px; font-weight: 800; text-decoration: none; vertical-align: 2px; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } |
| 687 | .ovr-code { margin: 8px 0 0; padding: 10px 12px; border-radius: 12px; overflow-x: auto; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; background: color-mix(in srgb, #000 30%, transparent); } |
| 688 | .ovr-sources { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } |
| 689 | .ovr-source { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 10px 4px 4px; border-radius: 999px; font-size: 12.5px; color: var(--text-secondary, #a8b3c4); text-decoration: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); } |
| 690 | .ovr-source i { font-style: normal; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 800; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } |
| 691 | .ovr-source span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 220px; } |
| 692 | .ovr-card { margin-top: 10px; } |
| 693 | .ovr-chips { display: flex; flex-wrap: wrap; gap: 6px; } |
| 694 | .ovr-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px; border-radius: 999px; font-size: 13px; background: color-mix(in srgb, var(--accent, #3b82f6) 13%, transparent); } |
| 695 | .ovr-chip svg { width: 15px; height: 15px; color: var(--accent-light, #60a5fa); } .ovr-chip.is-undone { opacity: .5; text-decoration: line-through; } |
| 696 | .ovr-table-wrap { overflow-x: auto; } .ovr-table { border-collapse: collapse; font-size: 13.5px; min-width: 100%; } |
| 697 | .ovr-table th, .ovr-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border, rgba(255,255,255,.08)); } .ovr-table th { font-weight: 750; color: var(--text-secondary, #a8b3c4); } |
| 698 | .ovr-linkcard { display: flex; gap: 10px; align-items: center; padding: 8px; border-radius: 14px; color: inherit; text-decoration: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); } |
| 699 | .ovr-linkcard img { width: 64px; height: 48px; object-fit: cover; border-radius: 10px; } .ovr-linkcard-t { display: grid; min-width: 0; } .ovr-linkcard-t b { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .ovr-linkcard-t small { font-size: 12px; color: var(--text-muted, #93a0b4); } |
| 700 | .ovr-step.is-row, .ovr-step.is-think { align-self: flex-start; max-width: 100%; } |
| 701 | .ovr-row-step, .ovr-step.is-think > summary { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 2px 8px; border-radius: 10px; font-size: 13.5px; color: var(--text-secondary, #a8b3c4); cursor: default; list-style: none; } |
| 702 | .ovr-step.is-think > summary, details.ovr-step > .ovr-row-step { cursor: pointer; } |
| 703 | .ovr-step.is-think > summary::-webkit-details-marker, details.ovr-step > summary::-webkit-details-marker { display: none; } |
| 704 | .ovr-row-step svg, .ovr-step.is-think > summary svg { width: 16px; height: 16px; color: var(--text-muted, #93a0b4); } |
| 705 | .ovr-step.is-row.is-running .ovr-row-step { color: var(--text-primary, #e6edf7); } |
| 706 | .ovr-step.is-row.is-failed .ovr-row-step { color: #f87171; } |
| 707 | .ovr-step.is-row.is-link { cursor: pointer; } |
| 708 | .ovr-step.is-think .ovr-words, details.ovr-step.is-row .ovr-words { margin: 4px 0 2px 32px; font-size: 13.5px; color: var(--text-secondary, #a8b3c4); } |
| 709 | .ovr-pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light, #60a5fa); animation: ovr-pulse 1.2s ease-in-out infinite; } |
| 710 | .ovr-fail { width: 15px; height: 15px; color: #f87171; } |
| 711 | .ovr-step.is-approval { align-self: stretch; max-width: 100%; padding: 12px 14px; border-radius: 16px; background: color-mix(in srgb, #f59e0b 8%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 40%, transparent); } |
| 712 | .ovr-approval-h { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .ovr-approval-h svg { color: #fbbf24; } |
| 713 | .ovr-approval-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; } |
| 714 | .ovr-btn { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 999px; cursor: pointer; font-weight: 750; font-size: 14px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); } |
| 715 | .ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; } |
| 716 | .ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; } |
| 717 | .ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; } |
| 718 | @keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } } |
| 719 | @keyframes ovr-shimmer { 50% { opacity: .55; } } |
| 720 | @keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } } |
| 721 | @keyframes ovr-pulse { 50% { opacity: .35; transform: scale(.7); } } |
| 722 | @keyframes ovr-caret { 50% { opacity: 0; } } |
| 723 | html[data-ov-motion=reduced] .ovr-row, html[data-ov-motion=reduced] .ovr-step { animation-name: ovr-fade; animation-delay: 0s; } |
| 724 | @keyframes ovr-fade { from { opacity: 0; } } |
| 725 | html[data-ov-motion=reduced] .ovr-face.is-working { animation-duration: 5s; } |
| 726 | html[data-ov-motion=off] .ovr-row, html[data-ov-motion=off] .ovr-step, html[data-ov-motion=off] .ovr-face.is-working, html[data-ov-motion=off] .ovr-pulse, html[data-ov-motion=off] .ovr-row.is-ghost { animation: none; } |
| 727 | `; |
| 728 | document.head.appendChild(st); |
| 729 | } |
| 730 | |
| 731 | root.OpenVibeRun = { mount, card, version: 1 }; |
| 732 | if (typeof module !== 'undefined' && module.exports) module.exports = root.OpenVibeRun; |
| 733 | })(typeof window !== 'undefined' ? window : globalThis); |
| 734 |