/** * ThinkStorm Modern Frontend Client * Interactive UI, AJAX workflows, tabs, modals, and notifications. */ document.addEventListener('DOMContentLoaded', () => { initTabs(); initIntakeBox(); }); // ----------------- 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'); 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...'; try { const res = await fetch('/api/ideas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Submission failed'); showToast(`Idea preserved as ${data.id}! Ingestion started.`, 'success'); textarea.value = ''; 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'); 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 = '🔄 Publish / Re-sync to Gitea'; } }