/* node:coverage disable */ (function (root, factory) { 'use strict'; const contract = typeof module === 'object' && module.exports ? require('./wave_b_contract.js') : root.HermesHuxWaveBContract; const api = factory(contract); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxWaveBArtifactsResearch = api; }(typeof globalThis === 'object' ? globalThis : this, function (contract) { /* node:coverage enable */ 'use strict'; function el(doc, tag, attrs, value) { const node = doc.createElement(tag); Object.entries(attrs || {}).forEach(([key, item]) => node.setAttribute(key, item)); if (value !== undefined) node.textContent = String(value); return node; } function status(doc, message, alert) { return el(doc, 'p', {class: 'hux-wave-b__status', role: alert ? 'alert' : 'status', 'aria-live': 'polite'}, message); } function heading(doc, title, description) { const head = el(doc, 'header', {class: 'hux-wave-b__heading'}); head.appendChild(el(doc, 'h3', {}, title)); head.appendChild(el(doc, 'p', {}, description)); return head; } function etag(result, revision) { if (String(result.etag || '').replaceAll('"', '') !== String(revision)) { throw new contract.WaveBContractError('ETag does not match the record revision'); } } function key(action, target) { return `webui:${action}:${target}`.slice(0, 120); } function createArtifactsExtension(options) { const settings = options || {}; const doc = settings.document; const projectId = settings.projectId; const conversationId = settings.conversationId; if (!doc || !contract.isId(projectId, 'prj') || !contract.isId(conversationId, 'conv')) { throw new TypeError('Artifact runtime context is invalid'); } function render(context) { const root = el(doc, 'div', {class: 'hux-wave-b hux-wave-b--artifacts'}); context.container.replaceChildren(root); let artifacts = []; let generation = 0; function fail() { root.replaceChildren(heading(doc, 'Artifact workspace', 'Preview durable outputs and their history.'), status(doc, 'This artifact workspace is temporarily unavailable.', true)); } async function reload() { const current = ++generation; root.replaceChildren(heading(doc, 'Artifact workspace', 'Preview durable outputs and their history.'), status(doc, 'Loading artifacts…')); try { const result = await context.client.request(`/artifacts?conversation_id=${conversationId}&cursor=0`); if (current !== generation) return; artifacts = contract.normalizeArtifactPage(result.body, context.client.identity, conversationId, projectId).items; draw(); } catch (_) { if (current === generation) fail(); } } async function showPreview(artifact, number, area) { area.replaceChildren(status(doc, `Loading version ${number}…`)); try { const result = await context.client.request(`/artifacts/${artifact.id}/versions/${number}`); etag(result, artifact.revision); const content = contract.normalizeArtifactContent(result.body, artifact, number); area.replaceChildren(); if (content.preview === null) area.appendChild(status(doc, content.reason)); else { area.appendChild(el(doc, 'pre', {class: 'hux-wave-b__preview', tabindex: '0'}, content.preview)); if (content.reason) area.appendChild(status(doc, content.reason)); } } catch (_) { area.replaceChildren(status(doc, 'This version could not be previewed.', true)); } } async function showDiff(artifact, area) { const from = artifact.currentVersion - 1; const to = artifact.currentVersion; area.replaceChildren(status(doc, 'Loading version comparison…')); try { const result = await context.client.request(`/artifacts/${artifact.id}/versions/${to}/diff?from=${from}`); const diff = contract.normalizeDiff(result.body, from, to); area.replaceChildren(el(doc, 'pre', {class: 'hux-wave-b__preview', tabindex: '0'}, diff.text)); } catch (_) { area.replaceChildren(status(doc, 'This comparison could not be loaded.', true)); } } async function promote(artifact) { await context.client.request(`/artifacts/${artifact.id}/promote`, {method: 'POST', headers: {'If-Match': String(artifact.revision)}, body: {project_id: projectId, version: artifact.currentVersion}}); await reload(); } async function addVersion(artifact, content, button) { if (!content.trim()) return; button.disabled = true; await context.client.request(`/artifacts/${artifact.id}/versions`, {method: 'POST', headers: { 'If-Match': String(artifact.revision), 'Idempotency-Key': key('artifact-version', `${artifact.id}:${artifact.revision + 1}`), }, body: {content, mime: artifact.versions.at(-1).mime, diff_from: artifact.currentVersion}}); await reload(); } function artifactCard(artifact) { const card = el(doc, 'article', {class: 'hux-wave-b__card'}); card.appendChild(el(doc, 'h4', {}, artifact.title)); card.appendChild(el(doc, 'p', {}, `${artifact.type} · ${artifact.sensitivity} · ${artifact.versions.length} versions`)); const history = el(doc, 'ol', {class: 'hux-wave-b__history', 'aria-label': `${artifact.title} versions`}); const preview = el(doc, 'div', {class: 'hux-wave-b__preview-area', 'aria-live': 'polite'}); artifact.versions.forEach((version) => { const row = el(doc, 'li'); const open = el(doc, 'button', {type: 'button'}, `Version ${version.version}`); open.addEventListener('click', () => showPreview(artifact, version.version, preview)); row.appendChild(open); row.appendChild(el(doc, 'span', {}, ` ${version.bytes} bytes · ${version.mime}`)); history.appendChild(row); }); card.appendChild(history); const download = el(doc, 'a', {href: context.client.endpoint(`/artifacts/${artifact.id}/versions/${artifact.currentVersion}`), download: '', rel: 'nofollow'}, 'Download current version'); card.appendChild(download); if (artifact.currentVersion > 1) { const diff = el(doc, 'button', {type: 'button'}, 'Compare latest versions'); diff.addEventListener('click', () => showDiff(artifact, preview)); card.appendChild(diff); } if (!artifact.promoted) { const button = el(doc, 'button', {type: 'button'}, 'Promote to current project'); button.addEventListener('click', () => promote(artifact).catch(fail)); card.appendChild(button); } if (contract.isTextType(artifact.type)) { const form = el(doc, 'form', {class: 'hux-wave-b__form'}); const label = el(doc, 'label', {}, 'New version content'); const input = el(doc, 'textarea', {maxlength: String(25 * 1024 * 1024), rows: '5'}); const save = el(doc, 'button', {type: 'submit'}, 'Add version'); label.appendChild(input); form.appendChild(label); form.appendChild(save); form.addEventListener('submit', (event) => { event.preventDefault(); addVersion(artifact, input.value, save).catch(fail); }); card.appendChild(form); } card.appendChild(preview); return card; } function draw() { root.replaceChildren(heading(doc, 'Artifact workspace', 'Preview, compare, download, version, and promote outputs from this conversation.')); root.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Sharing is not exposed by the canonical backend; artifacts remain owner-only.')); if (!artifacts.length) root.appendChild(status(doc, 'No artifacts belong to this conversation.')); artifacts.forEach((artifact) => root.appendChild(artifactCard(artifact))); } reload(); } return Object.freeze({id: 'artifacts', flag: 'hux.artifacts', label: 'Artifacts', order: 40, render}); } function createResearchExtension(options) { const settings = options || {}; const doc = settings.document; const conversationId = settings.conversationId; const messageId = settings.messageId; let notebookId = settings.notebookId; if (!doc || !contract.isId(conversationId, 'conv') || (messageId !== undefined && !contract.MESSAGE.test(messageId)) || (notebookId !== undefined && !contract.isId(notebookId, 'nb'))) { throw new TypeError('Research runtime context is invalid'); } function render(context) { const root = el(doc, 'div', {class: 'hux-wave-b hux-wave-b--research'}); context.container.replaceChildren(root); let citations = []; let notebook = null; let generation = 0; function fail() { root.replaceChildren(heading(doc, 'Research notebook', 'Inspect bounded evidence and unresolved questions.'), status(doc, 'This research workspace is temporarily unavailable.', true)); } function sourceItem(source, passage) { const item = el(doc, 'li'); const title = source.url ? el(doc, 'a', {href: source.url, target: '_blank', rel: 'noopener noreferrer'}, source.title) : el(doc, 'span', {}, source.title); item.appendChild(title); item.appendChild(el(doc, 'span', {}, ` · ${source.classification}${source.publisher ? ` · ${source.publisher}` : ''}`)); if (!source.url) item.appendChild(el(doc, 'span', {class: 'hux-wave-b__gap'}, ' · link withheld by URL policy')); item.appendChild(el(doc, 'blockquote', {}, passage.text)); return item; } function citationCard(item) { const details = el(doc, 'details', {class: 'hux-wave-b__card'}); details.appendChild(el(doc, 'summary', {}, `${item.support.replaceAll('_', ' ')}: ${item.claim}`)); if (item.note) details.appendChild(el(doc, 'p', {}, item.note)); const list = el(doc, 'ol', {'aria-label': 'Supporting passages'}); item.passages.forEach((passage) => { const source = item.sources.find((candidate) => candidate.id === passage.sourceId); list.appendChild(sourceItem(source, passage)); }); details.appendChild(list); return details; } async function patchNotebook(body) { const result = await context.client.request(`/notebooks/${notebook.id}`, {method: 'PATCH', headers: {'If-Match': String(notebook.revision)}, body}); notebook = contract.normalizeNotebook(result.body, conversationId, result.etag); draw('Notebook updated.'); } function notebookView() { const section = el(doc, 'section', {class: 'hux-wave-b__notebook', 'aria-label': 'Research notebook'}); section.appendChild(el(doc, 'h4', {}, notebook.question)); section.appendChild(el(doc, 'p', {}, `Status: ${notebook.status}`)); const groups = [ ['Assumptions', notebook.assumptions], ['Unresolved questions', notebook.unresolvedQuestions], ['Notes', notebook.notes.map((note) => note.text)], ]; groups.forEach(([label, items]) => { section.appendChild(el(doc, 'h5', {}, label)); const list = el(doc, 'ul'); items.forEach((item) => list.appendChild(el(doc, 'li', {}, item))); if (!items.length) list.appendChild(el(doc, 'li', {}, 'None recorded.')); section.appendChild(list); }); if (notebook.passageIds.length) section.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'The backend has no passage-by-id endpoint; passage text is shown only when attached to the selected message.')); if (notebook.status === 'open') { const actions = el(doc, 'div', {class: 'hux-wave-b__actions'}); ['answered', 'abandoned'].forEach((next) => { const button = el(doc, 'button', {type: 'button'}, `Mark ${next}`); button.addEventListener('click', () => patchNotebook({status: next}).catch(fail)); actions.appendChild(button); }); section.appendChild(actions); } const form = el(doc, 'form', {class: 'hux-wave-b__form'}); const label = el(doc, 'label', {}, 'Add research note'); const input = el(doc, 'textarea', {maxlength: '2000', rows: '3'}); const save = el(doc, 'button', {type: 'submit'}, 'Add note'); label.appendChild(input); form.appendChild(label); form.appendChild(save); form.addEventListener('submit', (event) => { event.preventDefault(); const value = input.value.trim(); if (value) patchNotebook({add_notes: [{text: value}]}).catch(fail); }); section.appendChild(form); return section; } async function createNotebook(question) { const result = await context.client.request('/notebooks', {method: 'POST', headers: { 'Idempotency-Key': key('notebook', conversationId), }, body: {conversation_id: conversationId, question}}); notebook = contract.normalizeNotebook(result.body, conversationId, result.etag); notebookId = notebook.id; draw('Notebook created.'); } function createNotebookForm() { const form = el(doc, 'form', {class: 'hux-wave-b__form'}); const label = el(doc, 'label', {}, 'Research question'); const input = el(doc, 'textarea', {maxlength: '1000', rows: '3'}); const save = el(doc, 'button', {type: 'submit'}, 'Create notebook'); label.appendChild(input); form.appendChild(label); form.appendChild(save); form.addEventListener('submit', (event) => { event.preventDefault(); const value = input.value.trim(); if (value) createNotebook(value).catch(fail); }); return form; } function draw(message) { root.replaceChildren(heading(doc, 'Research notebook', 'Inspect sources, supporting passages, assumptions, notes, and unresolved questions.')); if (message) root.appendChild(status(doc, message)); root.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Research records carry no owner identity field; isolation relies on the verified tenant-scoped HUX session.')); const strip = el(doc, 'section', {'aria-label': 'Citations for selected message'}); strip.appendChild(el(doc, 'h4', {}, 'Citations')); if (!messageId) strip.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Select a message to load citations; the backend exposes citations per message, not per conversation.')); else if (!citations.length) strip.appendChild(status(doc, 'No citations are attached to the selected message.')); citations.forEach((item) => strip.appendChild(citationCard(item))); root.appendChild(strip); root.appendChild(notebook ? notebookView() : createNotebookForm()); if (!notebookId) root.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Existing notebooks cannot be discovered because the backend has no conversation notebook-list endpoint.')); } async function load() { const current = ++generation; root.replaceChildren(heading(doc, 'Research notebook', 'Inspect bounded evidence and unresolved questions.'), status(doc, 'Loading research…')); try { const citationRequest = messageId ? context.client.request(`/messages/${messageId}/citations`) : Promise.resolve({body: {items: [], next: null}}); const notebookRequest = notebookId ? context.client.request(`/notebooks/${notebookId}`) : Promise.resolve(null); const [citationResult, notebookResult] = await Promise.all([citationRequest, notebookRequest]); if (current !== generation) return; citations = contract.normalizeCitationPage(citationResult.body, messageId || 'message-default', conversationId); notebook = notebookResult ? contract.normalizeNotebook(notebookResult.body, conversationId, notebookResult.etag) : null; draw(); } catch (_) { if (current === generation) fail(); } } load(); } return Object.freeze({id: 'research', flag: 'hux.research', label: 'Research', order: 80, render}); } return Object.freeze({createArtifactsExtension, createResearchExtension, etag, key}); }));