742 lines
27 KiB
JavaScript
742 lines
27 KiB
JavaScript
/**
|
|
* ThinkStorm Modern Frontend Client
|
|
* Interactive UI, AJAX workflows, tabs, modals, and notifications.
|
|
*/
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initTabs();
|
|
initIntakeBox();
|
|
initMarkdownViews();
|
|
});
|
|
|
|
// ----------------- Toast Notifications -----------------
|
|
function showToast(message, type = 'info') {
|
|
let container = document.getElementById('toast-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'toast-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.innerText = message;
|
|
container.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
toast.style.transform = 'translateY(10px)';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3500);
|
|
}
|
|
|
|
// ----------------- Tab Navigation -----------------
|
|
function initTabs() {
|
|
const tabContainers = document.querySelectorAll('.tab-container');
|
|
tabContainers.forEach(container => {
|
|
const btns = container.querySelectorAll('.tab-btn');
|
|
const panels = container.querySelectorAll('.tab-panel');
|
|
|
|
function activateTab(rawKey) {
|
|
if (!rawKey) return;
|
|
const cleanKey = String(rawKey).replace(/^#+/, '').replace(/^tab-/, '').trim();
|
|
if (!cleanKey) return;
|
|
|
|
const targetBtn = container.querySelector(`.tab-btn[data-tab="tab-${cleanKey}"]`) ||
|
|
container.querySelector(`.tab-btn[data-tab="${cleanKey}"]`);
|
|
const targetPanel = container.querySelector(`#tab-${cleanKey}`) ||
|
|
container.querySelector(`#${cleanKey}`);
|
|
|
|
if (targetBtn && targetPanel) {
|
|
btns.forEach(b => b.classList.remove('active'));
|
|
panels.forEach(p => p.classList.remove('active'));
|
|
targetBtn.classList.add('active');
|
|
targetPanel.classList.add('active');
|
|
|
|
try {
|
|
history.replaceState(null, null, `#${cleanKey}`);
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, `tab-${cleanKey}`);
|
|
localStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, `tab-${cleanKey}`);
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
btns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const target = btn.getAttribute('data-tab');
|
|
activateTab(target);
|
|
});
|
|
});
|
|
|
|
// Check URL hash, sessionStorage, or localStorage on load
|
|
const hash = window.location.hash;
|
|
const sessionSaved = sessionStorage.getItem(`thinkstorm_active_tab_${window.location.pathname}`);
|
|
const localSaved = localStorage.getItem(`thinkstorm_active_tab_${window.location.pathname}`);
|
|
|
|
if (hash && hash.length > 1) {
|
|
activateTab(hash);
|
|
} else if (sessionSaved) {
|
|
activateTab(sessionSaved);
|
|
} else if (localSaved) {
|
|
activateTab(localSaved);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
const hash = window.location.hash;
|
|
if (hash && hash.length > 1) {
|
|
const cleanKey = hash.replace(/^#+/, '').replace(/^tab-/, '').trim();
|
|
const tabContainers = document.querySelectorAll('.tab-container');
|
|
tabContainers.forEach(c => {
|
|
const targetBtn = c.querySelector(`.tab-btn[data-tab="tab-${cleanKey}"]`) || c.querySelector(`.tab-btn[data-tab="${cleanKey}"]`);
|
|
const targetPanel = c.querySelector(`#tab-${cleanKey}`) || c.querySelector(`#${cleanKey}`);
|
|
if (targetBtn && targetPanel) {
|
|
c.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
c.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
targetBtn.classList.add('active');
|
|
targetPanel.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ----------------- Public Intake Box -----------------
|
|
function initIntakeBox() {
|
|
const textarea = document.getElementById('submission-text');
|
|
const counter = document.getElementById('char-count');
|
|
const urlCount = document.getElementById('detected-urls');
|
|
const form = document.getElementById('intake-form');
|
|
const fileInput = document.getElementById('submission-image');
|
|
const chooseBtn = document.getElementById('choose-image-btn');
|
|
const previewChip = document.getElementById('image-preview-chip');
|
|
const fileNameSpan = document.getElementById('image-file-name');
|
|
const removeBtn = document.getElementById('remove-image-btn');
|
|
const imageStatus = document.getElementById('image-status');
|
|
|
|
if (chooseBtn && fileInput) {
|
|
chooseBtn.addEventListener('click', () => fileInput.click());
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files && fileInput.files.length > 0) {
|
|
const file = fileInput.files[0];
|
|
if (fileNameSpan) fileNameSpan.innerText = file.name;
|
|
if (previewChip) previewChip.style.display = 'inline-flex';
|
|
if (chooseBtn) chooseBtn.style.display = 'none';
|
|
if (imageStatus) imageStatus.innerText = `${(file.size / 1024).toFixed(0)} KB attached`;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (removeBtn && fileInput) {
|
|
removeBtn.addEventListener('click', () => {
|
|
fileInput.value = '';
|
|
if (previewChip) previewChip.style.display = 'none';
|
|
if (chooseBtn) chooseBtn.style.display = 'inline-flex';
|
|
if (imageStatus) imageStatus.innerText = 'Max 1 image';
|
|
});
|
|
}
|
|
|
|
if (textarea) {
|
|
textarea.addEventListener('input', () => {
|
|
const len = textarea.value.length;
|
|
if (counter) counter.innerText = `${len} / 10,000`;
|
|
|
|
// Live URL detection
|
|
const urls = textarea.value.match(/https?:\/\/[^\s<>"]+/gi) || [];
|
|
if (urlCount) {
|
|
urlCount.innerText = urls.length > 0 ? `${urls.length} URL(s) detected` : 'No URLs detected';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const text = textarea.value.trim();
|
|
if (!text) {
|
|
showToast('Please enter an idea description.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerText = 'Preserving & Ingesting...';
|
|
|
|
const file = fileInput && fileInput.files && fileInput.files.length > 0 ? fileInput.files[0] : null;
|
|
|
|
try {
|
|
let res;
|
|
if (file) {
|
|
const formData = new FormData();
|
|
formData.append('text', text);
|
|
formData.append('image', file);
|
|
res = await fetch('/api/ideas', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
} else {
|
|
res = await fetch('/api/ideas', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text })
|
|
});
|
|
}
|
|
|
|
let data = {};
|
|
const contentType = res.headers.get('content-type') || '';
|
|
if (contentType.includes('application/json')) {
|
|
try {
|
|
data = await res.json();
|
|
} catch (e) {
|
|
data = { detail: await res.text() };
|
|
}
|
|
} else {
|
|
const rawErr = await res.text();
|
|
data = { detail: rawErr || `Server error (${res.status})` };
|
|
}
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.detail || `Submission failed with status ${res.status}`);
|
|
}
|
|
|
|
showToast(`Idea preserved as ${data.id}! Ingestion started.`, 'success');
|
|
textarea.value = '';
|
|
if (fileInput) fileInput.value = '';
|
|
if (previewChip) previewChip.style.display = 'none';
|
|
if (chooseBtn) chooseBtn.style.display = 'inline-flex';
|
|
if (imageStatus) imageStatus.innerText = 'Max 1 image';
|
|
|
|
const isAuthenticated = Boolean(document.querySelector('.nav-auth-pill'));
|
|
setTimeout(() => {
|
|
if (isAuthenticated) {
|
|
window.location.href = `/ideas/${data.id}`;
|
|
} else {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerText = 'Submit Idea';
|
|
showToast(`Idea ${data.id} submitted! It will appear in the catalog once available.`, 'info');
|
|
}
|
|
}, 1000);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerText = 'Submit Idea';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ----------------- Idea Actions (Claim, Release, Activate) -----------------
|
|
async function claimIdea(ideaId) {
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/claim`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Claim failed');
|
|
showToast(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function releaseIdea(ideaId) {
|
|
if (!confirm('Are you sure you want to release your claim on this idea?')) return;
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/release`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Release failed');
|
|
showToast(data.message, 'info');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function activateIdea(ideaId) {
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/activate`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Activation failed');
|
|
showToast(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function trashIdea(ideaId) {
|
|
console.log('[ThinkStorm] Trashing idea:', ideaId);
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/trash`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Failed to move idea to trash');
|
|
showToast(data.message || `Idea ${ideaId} moved to trash`, 'info');
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 400);
|
|
} catch (err) {
|
|
console.error('[ThinkStorm] Trash error:', err);
|
|
showToast(err.message || 'Trash operation failed', 'danger');
|
|
}
|
|
}
|
|
|
|
async function restoreIdea(ideaId) {
|
|
console.log('[ThinkStorm] Restoring idea:', ideaId);
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/restore`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Failed to restore idea');
|
|
showToast(data.message || `Idea ${ideaId} restored!`, 'success');
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 400);
|
|
} catch (err) {
|
|
console.error('[ThinkStorm] Restore error:', err);
|
|
showToast(err.message || 'Restore operation failed', 'danger');
|
|
}
|
|
}
|
|
|
|
async function deleteIdeaPermanently(ideaId) {
|
|
console.log('[ThinkStorm] Permanently deleting idea:', ideaId);
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/permanent`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Permanent deletion failed');
|
|
showToast(data.message || `Idea ${ideaId} deleted permanently`, 'info');
|
|
setTimeout(() => {
|
|
if (window.location.pathname.includes(`/ideas/${ideaId}`)) {
|
|
window.location.href = '/ideas?state=TRASHED';
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
}, 400);
|
|
} catch (err) {
|
|
console.error('[ThinkStorm] Delete error:', err);
|
|
showToast(err.message || 'Permanent deletion failed', 'danger');
|
|
}
|
|
}
|
|
|
|
async function emptyTrash() {
|
|
console.log('[ThinkStorm] Emptying entire trash bin');
|
|
try {
|
|
const res = await fetch('/api/trash/empty', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Failed to empty trash');
|
|
showToast(data.message || 'Trash bin emptied!', 'success');
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 400);
|
|
} catch (err) {
|
|
console.error('[ThinkStorm] Empty trash error:', err);
|
|
showToast(err.message || 'Empty trash failed', 'danger');
|
|
}
|
|
}
|
|
|
|
async function createWorkTrack(ideaId) {
|
|
const workType = document.getElementById('work-type-select').value;
|
|
const name = document.getElementById('work-track-name').value.trim();
|
|
const modelOverride = document.getElementById('work-model-select')?.value || null;
|
|
if (!name) {
|
|
showToast('Please provide a track title.', 'warning');
|
|
return;
|
|
}
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-worktracks');
|
|
history.replaceState(null, null, '#worktracks');
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/work-tracks`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ work_type_id: workType, name, model_override: modelOverride })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Track creation failed');
|
|
showToast(`Work Track '${data.name}' created!`, 'success');
|
|
setTimeout(() => window.location.reload(), 600);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function activateWorkTrack(trackId) {
|
|
const modelSelect = document.getElementById(`model-select-${trackId}`);
|
|
const modelOverride = modelSelect ? modelSelect.value : null;
|
|
console.log(`[ThinkStorm] Activating work track ${trackId} with model:`, modelOverride || 'default');
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-worktracks');
|
|
history.replaceState(null, null, '#worktracks');
|
|
try {
|
|
const res = await fetch(`/api/ideas/work-tracks/${trackId}/activate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ model_override: modelOverride })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Track activation failed');
|
|
showToast(data.message || `Work track ${trackId} queued for generation!`, 'info');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function deleteWorkTrackOutput(outputId, docName, version) {
|
|
console.log(`[ThinkStorm] Deleting artifact output ID ${outputId} (${docName} v${version})`);
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-worktracks');
|
|
history.replaceState(null, null, '#worktracks');
|
|
try {
|
|
const res = await fetch(`/api/ideas/work-tracks/outputs/${outputId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Artifact deletion failed');
|
|
showToast(data.message || `Artifact ${docName} (v${version}) deleted.`, 'info');
|
|
setTimeout(() => window.location.reload(), 500);
|
|
} catch (err) {
|
|
console.error('[ThinkStorm] Delete artifact error:', err);
|
|
showToast(err.message || 'Failed to delete artifact', 'danger');
|
|
}
|
|
}
|
|
|
|
async function graduateToGitea(trackId) {
|
|
if (!confirm('Graduate this Coding Project to Gitea?')) return;
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-worktracks');
|
|
history.replaceState(null, null, '#worktracks');
|
|
try {
|
|
const res = await fetch(`/api/ideas/work-tracks/${trackId}/graduate`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Graduation failed');
|
|
showToast('Graduated to Gitea successfully!', 'success');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
// ----------------- Admin Helpers -----------------
|
|
async function testService(serviceId) {
|
|
const btn = document.getElementById(`test-btn-${serviceId}`);
|
|
if (btn) btn.innerText = 'Testing...';
|
|
try {
|
|
const res = await fetch(`/api/admin/services/${serviceId}/test`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Service test failed');
|
|
showToast(`[${serviceId.toUpperCase()}] ${data.message}`, data.healthy ? 'success' : 'danger');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
if (btn) btn.innerText = 'Test';
|
|
}
|
|
}
|
|
|
|
async function retryJob(ideaId) {
|
|
try {
|
|
const res = await fetch(`/api/admin/jobs/retry/${ideaId}`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Retry failed');
|
|
showToast(data.message, 'info');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function quarantineDecision(ideaId, decision) {
|
|
try {
|
|
const res = await fetch(`/api/admin/quarantine/${ideaId}/decision`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ decision })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Decision failed');
|
|
showToast(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function syncIdeaToOpenGist(ideaId) {
|
|
const btn = document.getElementById('sync-opengist-btn');
|
|
if (btn) btn.innerText = 'Publishing...';
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-artifacts');
|
|
history.replaceState(null, null, '#artifacts');
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/sync-opengist`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Sync to OpenGist failed');
|
|
showToast(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
if (btn) btn.innerText = '🔄 Publish / Re-sync to OpenGist';
|
|
}
|
|
}
|
|
|
|
async function syncIdeaToGitea(ideaId) {
|
|
const btn = document.getElementById('sync-gitea-btn');
|
|
const originalLabel = btn ? btn.innerText : '';
|
|
if (btn) btn.innerText = 'Publishing to Gitea...';
|
|
sessionStorage.setItem(`thinkstorm_active_tab_${window.location.pathname}`, 'tab-artifacts');
|
|
history.replaceState(null, null, '#artifacts');
|
|
try {
|
|
const res = await fetch(`/api/ideas/${ideaId}/sync-gitea`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Sync to Gitea failed');
|
|
showToast(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
if (btn) btn.innerText = originalLabel;
|
|
}
|
|
}
|
|
|
|
async function reprocessIdea(ideaId) {
|
|
try {
|
|
showToast('Enqueuing idea for complete intake & research processing...', 'info');
|
|
const res = await fetch(`/api/ideas/${ideaId}/reprocess`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ bypass_duplicate_check: true })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Reprocess request failed');
|
|
showToast('Pipeline started! Page will refresh shortly...', 'success');
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
// ----------------- Universal Markdown Renderer & View Toggler -----------------
|
|
|
|
function renderMarkdownContent(rawText) {
|
|
if (!rawText) return '';
|
|
if (typeof marked !== 'undefined') {
|
|
try {
|
|
marked.setOptions({
|
|
gfm: true,
|
|
breaks: true,
|
|
pedantic: false
|
|
});
|
|
const parsed = marked.parse(rawText);
|
|
if (typeof DOMPurify !== 'undefined' && typeof DOMPurify.sanitize === 'function') {
|
|
return DOMPurify.sanitize(parsed);
|
|
}
|
|
return parsed;
|
|
} catch (e) {
|
|
console.warn('[ThinkStorm] marked.parse error, falling back:', e);
|
|
}
|
|
}
|
|
// Fallback simple escape
|
|
const div = document.createElement('div');
|
|
div.textContent = rawText;
|
|
return `<p style="white-space:pre-wrap;">${div.innerHTML}</p>`;
|
|
}
|
|
|
|
function initMarkdownViews(root = document) {
|
|
const containers = root.querySelectorAll('.markdown-view-container, [data-markdown-view]');
|
|
containers.forEach(container => {
|
|
if (container.dataset.initialized === 'true') return;
|
|
container.dataset.initialized = 'true';
|
|
|
|
// Check if this container has multi-version panes
|
|
const versionPanes = container.querySelectorAll('.research-version-pane');
|
|
if (versionPanes.length > 0) {
|
|
versionPanes.forEach(pane => {
|
|
const formattedBox = pane.querySelector('.markdown-formatted');
|
|
const codeBox = pane.querySelector('.markdown-code');
|
|
const rawSourceEl = pane.querySelector('.raw-markdown-source');
|
|
const rawText = rawSourceEl ? (rawSourceEl.value || rawSourceEl.textContent || '') : '';
|
|
|
|
if (formattedBox && rawText) {
|
|
formattedBox.innerHTML = renderMarkdownContent(rawText);
|
|
}
|
|
if (codeBox && (!codeBox.querySelector('code') || !codeBox.querySelector('code').textContent)) {
|
|
codeBox.innerHTML = `<pre><code>${escapeHtml(rawText)}</code></pre>`;
|
|
}
|
|
});
|
|
} else {
|
|
const formattedBox = container.querySelector('.markdown-formatted');
|
|
const codeBox = container.querySelector('.markdown-code');
|
|
const rawSourceEl = container.querySelector('.raw-markdown-source');
|
|
|
|
let rawText = '';
|
|
if (rawSourceEl) {
|
|
rawText = rawSourceEl.value || rawSourceEl.textContent || '';
|
|
} else if (codeBox && codeBox.querySelector('code')) {
|
|
rawText = codeBox.querySelector('code').textContent || '';
|
|
} else if (container.dataset.content) {
|
|
rawText = container.dataset.content;
|
|
}
|
|
|
|
// Render formatted markdown HTML
|
|
if (formattedBox && rawText) {
|
|
formattedBox.innerHTML = renderMarkdownContent(rawText);
|
|
}
|
|
|
|
// Ensure codeBox has raw text
|
|
if (codeBox && (!codeBox.querySelector('code') || !codeBox.querySelector('code').textContent)) {
|
|
codeBox.innerHTML = `<pre><code>${escapeHtml(rawText)}</code></pre>`;
|
|
}
|
|
|
|
// Default to Formatted view
|
|
if (formattedBox) formattedBox.style.display = 'block';
|
|
if (codeBox) codeBox.style.display = 'none';
|
|
}
|
|
|
|
// Setup toggle buttons
|
|
const toggleBtns = container.querySelectorAll('.btn-toggle-view');
|
|
toggleBtns.forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const mode = btn.getAttribute('data-view');
|
|
setMarkdownContainerView(container, mode);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Also auto-render any standalone .markdown-body-auto
|
|
const standaloneBodies = root.querySelectorAll('.markdown-body-auto');
|
|
standaloneBodies.forEach(el => {
|
|
if (el.dataset.initialized === 'true') return;
|
|
el.dataset.initialized = 'true';
|
|
const rawText = el.textContent || '';
|
|
if (rawText.trim()) {
|
|
el.innerHTML = renderMarkdownContent(rawText);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setMarkdownContainerView(container, mode) {
|
|
const toggleBtns = container.querySelectorAll('.btn-toggle-view');
|
|
toggleBtns.forEach(b => {
|
|
if (b.getAttribute('data-view') === mode) {
|
|
b.classList.add('active');
|
|
} else {
|
|
b.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
const versionPanes = container.querySelectorAll('.research-version-pane');
|
|
if (versionPanes.length > 0) {
|
|
versionPanes.forEach(pane => {
|
|
const formattedBox = pane.querySelector('.markdown-formatted');
|
|
const codeBox = pane.querySelector('.markdown-code');
|
|
if (mode === 'code') {
|
|
if (formattedBox) formattedBox.style.display = 'none';
|
|
if (codeBox) codeBox.style.display = 'block';
|
|
} else {
|
|
if (formattedBox) formattedBox.style.display = 'block';
|
|
if (codeBox) codeBox.style.display = 'none';
|
|
}
|
|
});
|
|
} else {
|
|
const formattedBox = container.querySelector('.markdown-formatted');
|
|
const codeBox = container.querySelector('.markdown-code');
|
|
if (mode === 'code') {
|
|
if (formattedBox) formattedBox.style.display = 'none';
|
|
if (codeBox) codeBox.style.display = 'block';
|
|
} else {
|
|
if (formattedBox) formattedBox.style.display = 'block';
|
|
if (codeBox) codeBox.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
function switchResearchVersion(selectEl, stage) {
|
|
const selectedVersion = String(selectEl.value);
|
|
const container = selectEl.closest('.tab-panel') || document.getElementById(`tab-${stage.toLowerCase().replace('_', '-')}`);
|
|
if (!container) return;
|
|
|
|
// Toggle panes for this stage
|
|
const panes = container.querySelectorAll(`.research-version-pane[data-stage="${stage}"]`);
|
|
let activePane = null;
|
|
panes.forEach(pane => {
|
|
if (String(pane.dataset.version) === selectedVersion) {
|
|
pane.style.display = 'block';
|
|
activePane = pane;
|
|
} else {
|
|
pane.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Update header meta badges
|
|
if (activePane) {
|
|
const metaContainer = container.querySelector('.markdown-view-meta');
|
|
if (metaContainer) {
|
|
const modelBadge = metaContainer.querySelector('.research-meta-model');
|
|
if (modelBadge && activePane.dataset.model) {
|
|
modelBadge.innerText = `🤖 ${activePane.dataset.model}`;
|
|
}
|
|
const tokenBadge = metaContainer.querySelector('.research-meta-tokens');
|
|
if (tokenBadge && activePane.dataset.tokens) {
|
|
tokenBadge.innerText = `⚡ ${activePane.dataset.tokens} tokens`;
|
|
}
|
|
const timeBadge = metaContainer.querySelector('.research-meta-time');
|
|
if (timeBadge && activePane.dataset.time) {
|
|
timeBadge.innerText = `🕒 ${activePane.dataset.time}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function copyMarkdownFromContainer(btn) {
|
|
const container = btn.closest('.markdown-view-container');
|
|
if (!container) return;
|
|
|
|
// Find visible pane if multi-version
|
|
let targetRoot = container;
|
|
const visiblePane = container.querySelector('.research-version-pane:not([style*="display:none"]):not([style*="display: none"])');
|
|
if (visiblePane) {
|
|
targetRoot = visiblePane;
|
|
}
|
|
|
|
const rawSourceEl = targetRoot.querySelector('.raw-markdown-source');
|
|
const codeEl = targetRoot.querySelector('.markdown-code code');
|
|
const text = rawSourceEl ? (rawSourceEl.value || rawSourceEl.textContent) : (codeEl ? codeEl.textContent : '');
|
|
|
|
if (!text) {
|
|
showToast('No markdown content to copy', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(text);
|
|
} else {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.opacity = '0';
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
}
|
|
const origHtml = btn.innerHTML;
|
|
btn.innerHTML = '<span>✅ Copied!</span>';
|
|
showToast('Markdown copied to clipboard!', 'success');
|
|
setTimeout(() => {
|
|
btn.innerHTML = origHtml;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Copy failed:', err);
|
|
showToast('Failed to copy to clipboard', 'danger');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
window.renderMarkdownContent = renderMarkdownContent;
|
|
window.initMarkdownViews = initMarkdownViews;
|
|
window.setMarkdownContainerView = setMarkdownContainerView;
|
|
window.switchResearchVersion = switchResearchVersion;
|
|
window.copyMarkdownFromContainer = copyMarkdownFromContainer;
|