File size: 6,817 Bytes
b60973d b04fa88 e8f61c8 2ad2aff e8f61c8 b60973d 45ebf29 b60973d 99a44ac 704a62b 99a44ac 93abf36 99a44ac 3387889 b60973d 3387889 99a44ac b60973d 99a44ac b60973d 99a44ac 53148eb e8f61c8 53148eb b60973d 99a44ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | // Shared client helpers: auth bootstrap, fetch wrapper, DOM utils, SVG icons.
const ICONS = {
tree: '<path d="M4 5h14M9 11h9M9 17h9M4 5v9.5a2.5 2.5 0 0 0 2.5 2.5H6"/>',
key: '<circle cx="7.5" cy="14.5" r="3.5"/><path d="M10 12l8-8M14.5 4.5l3 3M12 7l2.5 2.5"/>',
todo: '<rect x="4" y="4" width="6" height="6" rx="1.5"/><path d="M5.2 7l1.3 1.3L8.8 6"/><path d="M13 6.5h6M13 11h6M13 15.5h6"/><rect x="4" y="12.5" width="6" height="6" rx="1.5"/>',
table: '<rect x="3.5" y="4.5" width="15" height="13" rx="1.5"/><path d="M3.5 9h15M3.5 13.5h15M8.5 4.5v13M13.5 4.5v13"/>',
eye: '<path d="M2.5 11s3.2-5.5 8.5-5.5 8.5 5.5 8.5 5.5-3.2 5.5-8.5 5.5S2.5 11 2.5 11z"/><circle cx="11" cy="11" r="2.4"/>',
eyeoff: '<path d="M4 4l14 14M9 5.8A8.7 8.7 0 0 1 11 5.5c5.3 0 8.5 5.5 8.5 5.5a15 15 0 0 1-2.7 3.2M6 7.5A14 14 0 0 0 2.5 11S5.7 16.5 11 16.5a8.6 8.6 0 0 0 3-.5"/>',
back: '<path d="M15 4l-7 7 7 7"/>',
comment: '<path d="M19 12.5a6.5 6.5 0 0 1-6.5 6.5H5l-1.8 1.8V12.5a7.3 7.3 0 0 1 7.3-7.3h1.5A6.5 6.5 0 0 1 19 12.5z"/>',
edit: '<path d="M14.5 4.5l3 3L8 17l-4 1 1-4 9.5-9.5z"/>',
image: '<rect x="3.5" y="4.5" width="15" height="13" rx="2"/><circle cx="8" cy="9" r="1.4"/><path d="M18 14l-4-4-7 7"/>',
link: '<path d="M9 12.5a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 0 0-5-5l-1.2 1.2"/><path d="M13 9.5a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 0 0 5 5l1.2-1.2"/>',
copy: '<rect x="8" y="8" width="10" height="11" rx="2"/><path d="M5.5 14.5H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 2 2v.5"/>',
check: '<path d="M4.5 11.5l4.5 4.5 8.5-9.5"/>',
x: '<path d="M6 6l10 10M16 6L6 16"/>',
reply: '<path d="M18 5v4a4 4 0 0 1-4 4H5"/><path d="M8.5 9.5L5 13l3.5 3.5"/>',
send: '<path d="M4 11l14-6-4.5 14-3-6L4 11z"/>',
trash: '<path d="M4.5 6.5h13M9 6V4.5h4V6M6.5 6.5l.8 12h7.4l.8-12"/>',
quote: '<path d="M8.5 6.5c-2.5 1-4 3-4 6.5v3h4.5v-5h-2.7c.2-2 1-3.3 2.2-4.5zM18 6.5c-2.5 1-4 3-4 6.5v3h4.5v-5h-2.7c.2-2 1-3.3 2.2-4.5z"/>',
resolve: '<circle cx="11" cy="11" r="8"/><path d="M7.5 11.5l2.5 2.5 4.5-5"/>',
undo: '<path d="M8.5 5.5L4 10l4.5 4.5"/><path d="M4 10h9a5 4.5 0 0 1 0 9h-3"/>',
redo: '<path d="M13.5 5.5L18 10l-4.5 4.5"/><path d="M18 10H9a5 4.5 0 0 0 0 9h3"/>',
zoomIn: '<circle cx="10" cy="10" r="6.5"/><path d="M15 15l4 4M10 7.5v5M7.5 10h5"/>',
zoomOut: '<circle cx="10" cy="10" r="6.5"/><path d="M15 15l4 4M7.5 10h5"/>',
}
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, '<span class="mention">@$1</span>')
}
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('<the key>', 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
}
|