// Shared client helpers: auth bootstrap, fetch wrapper, DOM utils, SVG icons. const ICONS = { tree: '', key: '', todo: '', table: '', eye: '', eyeoff: '', back: '', comment: '', edit: '', image: '', link: '', copy: '', check: '', x: '', reply: '', send: '', trash: '', quote: '', resolve: '', undo: '', redo: '', zoomIn: '', zoomOut: '', } export function icon(name) { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') svg.setAttribute('viewBox', '0 0 22 22') svg.setAttribute('class', 'icon') svg.innerHTML = ICONS[name] || '' return svg } export async function initAuth(nextPath) { const res = await fetch('/api/me').then(r => r.json()) const whoami = document.getElementById('whoami') if (res.user) { res.user.buildId = res.build_id || null whoami.replaceChildren() if (res.user.avatar) { const img = document.createElement('img') img.src = res.user.avatar whoami.appendChild(img) } whoami.appendChild(document.createTextNode(res.user.username + ' ')) const out = document.createElement('a') out.href = '/auth/logout' out.textContent = 'sign out' whoami.appendChild(out) return { ...res.user, isAdmin: res.is_admin === true } } const overlay = document.getElementById('signin') overlay.classList.remove('hidden') if (!res.oauth) { document.getElementById('signin-btn').classList.add('hidden') document.getElementById('dev-login').classList.remove('hidden') document.getElementById('dev-go').addEventListener('click', () => { const u = document.getElementById('dev-user').value.trim() if (u) location.href = `/auth/dev?u=${encodeURIComponent(u)}&next=${encodeURIComponent(nextPath)}` }) } else { // Plain in-place navigation, also inside the huggingface.co iframe: the HF // OAuth page renders there, while target="_top" clicks get blocked by the // iframe sandbox. document.getElementById('signin-btn').href = `/auth/login?next=${encodeURIComponent(nextPath)}` } return null } export async function api(path, { method = 'GET', body } = {}) { const res = await fetch(path, { method, headers: body ? { 'content-type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, }) try { return await res.json() } catch { return { error: `HTTP ${res.status}` } } } export function el(tag, attrs = {}, text) { const node = document.createElement(tag) for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v) if (text != null) node.textContent = text return node } export function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) } export function renderMentionText(text) { return esc(text).replace(/@([a-z0-9][a-z0-9_.-]{1,38})/g, '@$1') } export function colorFor(name) { let h = 0 for (const c of String(name)) h = (h * 31 + c.charCodeAt(0)) % 360 return `hsl(${h}, 32%, 46%)` } export function timeAgo(ts) { if (!ts) return '' const s = Math.floor((Date.now() - ts) / 1000) if (s < 60) return 'now' if (s < 3600) return `${Math.floor(s / 60)}m` if (s < 86400) return `${Math.floor(s / 3600)}h` return `${Math.floor(s / 86400)}d` } // One-time display of a freshly issued agent key, with copy actions. export function agentKeyBox({ handle, key, docId }) { const box = el('div', { class: 'key-box' }) box.appendChild(el('div', { class: 'key-label' }, `Key for @${handle} — copy it now, it won't be shown again:`)) box.appendChild(el('code', {}, key)) const row = el('div', { class: 'row' }) const copyKey = el('button', { class: 'btn small', type: 'button' }, 'Copy key') copyKey.addEventListener('click', async () => { await navigator.clipboard.writeText(key) copyKey.textContent = 'Copied' setTimeout(() => (copyKey.textContent = 'Copy key'), 1500) }) const copyPrompt = el('button', { class: 'btn small primary', type: 'button' }, 'Copy agent prompt (incl. key)') copyPrompt.addEventListener('click', async () => { const text = await fetch(`/api/agent-prompt?handle=${handle}${docId ? `&doc=${docId}` : ''}`).then(r => r.text()) await navigator.clipboard.writeText(text.replace('', key)) copyPrompt.textContent = 'Copied' setTimeout(() => (copyPrompt.textContent = 'Copy agent prompt (incl. key)'), 1500) }) const dismiss = el('button', { class: 'btn small ghost', type: 'button' }, 'Done') dismiss.addEventListener('click', () => box.remove()) row.append(copyKey, copyPrompt, dismiss) box.appendChild(row) return box } export function statusLabel(status) { return { pending: 'pending', claimed: 'seen', done: 'handled', failed: 'failed' }[status] || status }