';
const cat = job.category || '';
const catIcon = _CAT_ICONS[cat] || '';
const catLabel = _CAT_LABELS[cat] || '';
let html = '';
// Category hero banner — only for completed, known-category results
if (cat && catIcon) {
html += `
${catIcon}
${catLabel}
${_esc(job.query)}
`;
}
if (job.sources?.length) {
html += '
';
for (const s of job.sources.slice(0, 10)) {
const title = _esc(s.title || s.url || '');
const url = _esc(s.url || '');
html += `${title}`;
}
if (job.sources.length > 10) html += `+${job.sources.length - 10} more`;
html += '
';
}
const bodyCls = `research-job-report-body${cat ? ' research-body-' + cat : ''}`;
if (_markdownModule) {
html += `
${_markdownModule.renderContent(job.result)}
`;
} else {
html += `
${_esc(job.result)}
`;
}
return html;
}
async function _ensureResult(job) {
if (job.result) return;
try {
const res = await fetch(`${_apiBase}/api/research/result-peek/${job.id}`, {
method: 'POST', credentials: 'same-origin',
});
if (!res.ok) return;
const d = await res.json();
job.result = d.result;
job.sources = d.sources;
job.findings = d.raw_findings;
} catch {}
}
async function _copyResult(job, btn) {
if (!job.result) return;
let text = `# ${job.query}\n\n${job.result}`;
if (job.findings?.length) {
text += '\n\n---\n## Raw Findings\n';
for (const f of job.findings) {
text += `\n### ${f.title || 'Untitled'}\nSource: ${f.url || ''}\n${f.summary || ''}\n`;
}
}
if (job.sources?.length) {
const srcList = job.sources.map(s => `- [${s.title || s.url}](${s.url})`).join('\n');
text += `\n\n---\n## Sources\n${srcList}`;
}
let ok = false;
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
ok = true;
}
} catch {}
if (!ok) {
// Fallback for non-secure contexts (HTTP self-host) where navigator.clipboard
// is unavailable. The textarea must be in-viewport and focusable for Firefox
// Android / iOS Safari to allow execCommand('copy').
const ta = document.createElement('textarea');
ta.value = text;
ta.readOnly = false;
ta.contentEditable = 'true';
ta.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:0;opacity:0;font-size:16px;';
document.body.appendChild(ta);
ta.focus();
ta.select();
try { ta.setSelectionRange(0, text.length); } catch {}
try {
const sel = window.getSelection();
if (sel && (!sel.rangeCount || sel.isCollapsed)) {
const range = document.createRange();
range.selectNodeContents(ta);
sel.removeAllRanges();
sel.addRange(range);
ta.setSelectionRange(0, text.length);
}
} catch {}
try { ok = document.execCommand('copy'); } catch {}
ta.remove();
}
if (btn) {
const orig = btn.innerHTML;
if (ok) {
btn.innerHTML = ``;
btn.classList.add('research-job-action-copied');
setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('research-job-action-copied'); }, 2000);
} else {
btn.innerHTML = `${_cancelIcon} Failed`;
setTimeout(() => { btn.innerHTML = orig; }, 2000);
}
}
}
// ── Chat about this research (server-side spinoff) ──
async function _chatAboutResearch(researchId, btn) {
if (!researchId) return;
const origLabel = btn ? btn.innerHTML : '';
if (btn) { btn.disabled = true; btn.innerHTML = `${_chatIcon} Creating…`; }
try {
const res = await fetch(`${_apiBase}/api/research/spinoff/${researchId}`, {
method: 'POST', credentials: 'same-origin',
});
if (!res.ok) {
let detail = '';
try { detail = (await res.json()).detail || ''; } catch {}
throw new Error(detail || `HTTP ${res.status}`);
}
const payload = await res.json();
if (_sessionModule && _sessionModule.selectSession && payload.session_id) {
if (_sessionModule.loadSessions) await _sessionModule.loadSessions().catch(() => {});
await _sessionModule.selectSession(payload.session_id);
closePanel();
} else if (payload.session_id) {
window.location.hash = '#' + payload.session_id;
window.location.reload();
} else {
// 200 OK but no session_id — server contract violation. Don't leave
// the button stuck on 'Creating…'; surface the failure instead.
throw new Error('Server returned no session id');
}
} catch (e) {
if (btn) { btn.disabled = false; btn.innerHTML = origLabel; }
alert('Could not start follow-up chat: ' + e.message);
}
}
function _esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}