426 lines
16 KiB
JavaScript
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';
|
|
}
|
|
}
|