/* * detail-render.js * HTML-string generators for the right-side detail panel (desktop) and * the selected-entity view (mobile). Covers all four entity types. * * Depends on: S, _bnd (state.js); esc (helpers.js); * pluginsByCategory, pluginsByTarget, vPluginBtn, getBookStats, * vAiProgressBar, candidateSugRows, _STATUS_BADGE (tree-render.js); * parseBounds, parseBndPluginResults (canvas-boundary.js) * Provides: vDetailBody(), vRoomDetail(), vCabinetDetail(), * vShelfDetail(), vBookDetail() */ /* exported vDetailBody, aiBlocksShown */ // ── Room detail ────────────────────────────────────────────────────────────── function vRoomDetail(r) { const stats = getBookStats(r, 'room'); const totalBooks = stats.total; return `
${vAiProgressBar(stats)}

${r.cabinets.length} cabinet${r.cabinets.length !== 1 ? 's' : ''} · ${totalBooks} book${totalBooks !== 1 ? 's' : ''}

`; } // ── Root detail (no selection) ──────────────────────────────────────────────── function vAiLogEntry(entry) { const ts = new Date(entry.ts * 1000).toLocaleTimeString(); const statusColor = entry.status === 'ok' ? '#15803d' : entry.status === 'error' ? '#dc2626' : '#b45309'; const statusLabel = entry.status === 'running' ? '⏳' : entry.status === 'ok' ? '✓' : '✗'; const dur = entry.duration_ms > 0 ? ` ${entry.duration_ms}ms` : ''; const model = entry.model ? `${esc(entry.model)}` : ''; const isBook = entry.entity_type === 'books'; const entityLabel = isBook ? `` : `${esc(entry.entity_id.slice(0, 8))}`; const thumb = isBook ? `` : ''; return `
${statusLabel} ${esc(entry.plugin_id)} · ${entityLabel}${thumb} ${ts}${dur}
${model} ${entry.request ? `
Request: ${esc(entry.request)}
` : ''} ${entry.response ? `
Response: ${esc(entry.response)}
` : ''}
`; } function vRootDetail() { const log = (_aiLog || []).slice().reverse(); // newest first return `
AI Request Log
${ log.length === 0 ? `
No AI requests yet. Use Identify or run a plugin on a book.
` : log.map(vAiLogEntry).join('
') }
`; } // ── Detail body (right panel) ──────────────────────────────────────────────── function vDetailBody() { if (!S.selected) return `
${vRootDetail()}
`; const { type, id } = S.selected; const node = findNode(id); if (!node) return '
Not found
'; if (type === 'room') return vRoomDetail(node); if (type === 'cabinet') return vCabinetDetail(node); if (type === 'shelf') return vShelfDetail(node); if (type === 'book') return vBookDetail(node); return ''; } // ── Cabinet detail ─────────────────────────────────────────────────────────── function vCabinetDetail(cab) { const bounds = parseBounds(cab.shelf_boundaries); const hasPhoto = !!cab.photo_filename; const stats = getBookStats(cab, 'cabinet'); const bndPlugins = pluginsByTarget('boundary_detector', 'shelves'); const pluginResults = parseBndPluginResults(cab.ai_shelf_boundaries); const pluginIds = Object.keys(pluginResults); const sel = _bnd?.nodeId === cab.id ? _bnd.selectedPlugin : cab.shelves.length > 0 ? null : (pluginIds[0] ?? null); const selOpts = [ ``, ...pluginIds.map((pid) => ``), ...(pluginIds.length > 1 ? [``] : []), ].join(''); return `
${vAiProgressBar(stats)} ${ hasPhoto ? `
` : `
📷
Upload a cabinet photo (📷 in header) to get started
` } ${hasPhoto ? `

Drag lines · Ctrl+Alt+Click to add · Snap to AI guides

` : ''} ${ hasPhoto ? `
${bounds.length ? `${cab.shelves.length} shelf${cab.shelves.length !== 1 ? 's' : ''} · ${bounds.length} boundar${bounds.length !== 1 ? 'ies' : 'y'}` : ''}
${bndPlugins.map((p) => vPluginBtn(p, cab.id, 'cabinets')).join('')}
` : '' }
`; } // ── Shelf detail ───────────────────────────────────────────────────────────── function vShelfDetail(shelf) { const bounds = parseBounds(shelf.book_boundaries); const stats = getBookStats(shelf, 'shelf'); const bndPlugins = pluginsByTarget('boundary_detector', 'books'); const pluginResults = parseBndPluginResults(shelf.ai_book_boundaries); const pluginIds = Object.keys(pluginResults); const sel = _bnd?.nodeId === shelf.id ? _bnd.selectedPlugin : shelf.books.length > 0 ? null : (pluginIds[0] ?? null); const selOpts = [ ``, ...pluginIds.map((pid) => ``), ...(pluginIds.length > 1 ? [``] : []), ].join(''); return `
${vAiProgressBar(stats)}

Drag lines · Ctrl+Alt+Click to add · Snap to AI guides

${bounds.length ? `${shelf.books.length} book${shelf.books.length !== 1 ? 's' : ''} · ${bounds.length} boundary${bounds.length !== 1 ? 'ies' : ''}` : ''}
${bndPlugins.map((p) => vPluginBtn(p, shelf.id, 'shelves')).join('')}
`; } // ── AI blocks helpers ───────────────────────────────────────────────────────── function parseAiBlocks(json) { if (!json) return []; try { return JSON.parse(json) || []; } catch { return []; } } function aiBlocksShown(b) { if (b.id in _aiBlocksVisible) return _aiBlocksVisible[b.id]; return b.identification_status !== 'user_approved'; } function vAiBlock(block, bookId) { const score = typeof block.score === 'number' ? (block.score * 100).toFixed(0) + '%' : ''; const sources = (block.sources || []).join(', '); const fields = [ ['title', block.title], ['author', block.author], ['year', block.year], ['isbn', block.isbn], ['publisher', block.publisher], ].filter(([, v]) => v && v.trim()); const rows = fields .map( ([k, v]) => `
${k} ${esc(v)}
`, ) .join(''); const blockData = esc(JSON.stringify(block)); return `
${score ? `${score}` : ''} ${sources ? `${esc(sources)}` : ''}
${rows}
`; } // ── Book detail ────────────────────────────────────────────────────────────── function vBookDetail(b) { const [sc, sl] = _STATUS_BADGE[b.identification_status] ?? _STATUS_BADGE.unidentified; const isLoading_ = isLoading('identify', b.id); const blocks = parseAiBlocks(b.ai_blocks); const shown = aiBlocksShown(b); const spineUrl = `/api/books/${b.id}/spine?t=${Date.now()}`; const titleUrl = b.image_filename ? `/images/${b.image_filename}` : ''; return `
Spine
${ titleUrl ? `
Title page
` : '' }
${sl} ${b.identification_status ?? 'unidentified'} ${b.analyzed_at ? `Identified ${b.analyzed_at.slice(0, 10)}` : ''}
${ blocks.length ? `
AI Results (${blocks.length})
${shown ? blocks.map((bl) => vAiBlock(bl, b.id)).join('') : ''}
` : '' }
`; }