/** * 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 `
${div.innerHTML}
`; } 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 = `${escapeHtml(rawText)}`;
}
});
} 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 = `${escapeHtml(rawText)}`;
}
// 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 = '✅ Copied!';
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, ''');
}
window.renderMarkdownContent = renderMarkdownContent;
window.initMarkdownViews = initMarkdownViews;
window.setMarkdownContainerView = setMarkdownContainerView;
window.switchResearchVersion = switchResearchVersion;
window.copyMarkdownFromContainer = copyMarkdownFromContainer;