Files
ThinkStorm/thinkstorm/static/js/app.js
T

426 lines
16 KiB
JavaScript

/**
* 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';
}
}