cowrite / client /src /common.js
lvwerra's picture
lvwerra HF Staff
Upload folder using huggingface_hub
2ad2aff verified
Raw
History Blame Contribute Delete
6.82 kB
// 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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
}