Spaces:
Configuration error
Configuration error
| // TrendAI Rebuilt Common JavaScript Engine | |
| // Manages shared UI states, alerts, loader, active project selection, and Settings Modal. | |
| window.TrendAI = { | |
| activeProjectId: null, | |
| projects: [], | |
| logPollingInterval: null, | |
| statusPollingInterval: null, | |
| // --- Loading Overlay --- | |
| showLoader: function(message = "Memuat...") { | |
| let loader = document.getElementById('global-loader-overlay'); | |
| if (!loader) { | |
| loader = document.createElement('div'); | |
| loader.id = 'global-loader-overlay'; | |
| loader.className = 'global-loader'; | |
| const spinner = document.createElement('div'); | |
| spinner.className = 'loader-spinner'; | |
| const text = document.createElement('p'); | |
| text.id = 'global-loader-text'; | |
| text.className = 'font-label-md text-slate-700'; | |
| text.textContent = message; | |
| loader.appendChild(spinner); | |
| loader.appendChild(text); | |
| document.body.appendChild(loader); | |
| } else { | |
| document.getElementById('global-loader-text').textContent = message; | |
| } | |
| // Force reflow | |
| loader.offsetHeight; | |
| loader.classList.add('active'); | |
| }, | |
| hideLoader: function() { | |
| const loader = document.getElementById('global-loader-overlay'); | |
| if (loader) { | |
| loader.classList.remove('active'); | |
| } | |
| }, | |
| // --- Toast Notifications --- | |
| showToast: function(message, type = 'success') { | |
| let container = document.getElementById('toast-notification-container'); | |
| if (!container) { | |
| container = document.createElement('div'); | |
| container.id = 'toast-notification-container'; | |
| container.className = 'toast-container'; | |
| document.body.appendChild(container); | |
| } | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| const textSpan = document.createElement('span'); | |
| textSpan.className = 'font-body-sm font-medium text-slate-800'; | |
| textSpan.textContent = message; | |
| toast.appendChild(textSpan); | |
| const closeBtn = document.createElement('span'); | |
| closeBtn.className = 'toast-close'; | |
| closeBtn.textContent = '×'; | |
| closeBtn.onclick = function() { | |
| toast.classList.remove('active'); | |
| setTimeout(() => toast.remove(), 300); | |
| }; | |
| toast.appendChild(closeBtn); | |
| container.appendChild(toast); | |
| // Force reflow and animate in | |
| toast.offsetHeight; | |
| toast.classList.add('active'); | |
| // Auto-remove after 4 seconds | |
| setTimeout(() => { | |
| if (toast.parentNode) { | |
| toast.classList.remove('active'); | |
| setTimeout(() => toast.remove(), 300); | |
| } | |
| }, 4000); | |
| }, | |
| // --- Initialize Projects list --- | |
| initProjects: async function() { | |
| try { | |
| const response = await fetch('/api/projects'); | |
| if (response.ok) { | |
| this.projects = await response.json(); | |
| this.updateProjectSelectors(); | |
| } else { | |
| console.error("Failed to fetch projects list"); | |
| } | |
| } catch (e) { | |
| console.error("Error fetching projects:", e); | |
| } | |
| }, | |
| // --- Update page project selector dropdowns --- | |
| updateProjectSelectors: function() { | |
| const selectors = document.querySelectorAll('.project-selector'); | |
| selectors.forEach(select => { | |
| select.replaceChildren(); // clear old options securely | |
| if (this.projects.length === 0) { | |
| const opt = document.createElement('option'); | |
| opt.value = ""; | |
| opt.textContent = "Belum Ada Proyek"; | |
| select.appendChild(opt); | |
| return; | |
| } | |
| this.projects.forEach(proj => { | |
| const opt = document.createElement('option'); | |
| opt.value = proj.id; | |
| opt.textContent = proj.name; | |
| select.appendChild(opt); | |
| }); | |
| // Set value to currently active project | |
| if (this.activeProjectId) { | |
| select.value = this.activeProjectId; | |
| } | |
| }); | |
| }, | |
| // --- Switch Active Project --- | |
| switchProject: function(projectId) { | |
| if (!projectId) return; | |
| this.activeProjectId = parseInt(projectId); | |
| localStorage.setItem('trendai_active_project_id', this.activeProjectId); | |
| // Update all selectors on the page | |
| const selectors = document.querySelectorAll('.project-selector'); | |
| selectors.forEach(s => s.value = this.activeProjectId); | |
| this.showToast(`Proyek aktif dialihkan ke: ${this.getProjectName(this.activeProjectId)}`); | |
| // Trigger page-specific data loading | |
| if (typeof window.loadPageData === 'function') { | |
| window.loadPageData(this.activeProjectId); | |
| } | |
| }, | |
| getProjectName: function(id) { | |
| const p = this.projects.find(proj => proj.id === id); | |
| return p ? p.name : "Proyek #" + id; | |
| }, | |
| // --- Modal Initialization --- | |
| initSettingsModal: function() { | |
| // Find Settings link in sidebar | |
| const settingsLinks = document.querySelectorAll('.nav-link-settings, #nav-settings, aside nav a[href*="settings"]'); | |
| settingsLinks.forEach(link => { | |
| // Prevent default navigation | |
| link.setAttribute('href', 'javascript:void(0)'); | |
| link.addEventListener('click', () => this.openSettingsModal()); | |
| }); | |
| // Add dynamic settings modal HTML structure if missing | |
| if (!document.getElementById('settings-modal-overlay')) { | |
| this.createSettingsModalDOM(); | |
| } | |
| }, | |
| createSettingsModalDOM: function() { | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'settings-modal-overlay'; | |
| overlay.className = 'modal-overlay'; | |
| const content = document.createElement('div'); | |
| content.className = 'modal-content'; | |
| // Header | |
| const header = document.createElement('div'); | |
| header.className = 'modal-header'; | |
| const title = document.createElement('h3'); | |
| title.className = 'font-headline-md text-slate-900 font-bold'; | |
| title.textContent = 'TrendAI Settings & Workspace'; | |
| header.appendChild(title); | |
| const closeBtn = document.createElement('span'); | |
| closeBtn.className = 'text-2xl text-slate-400 hover:text-slate-600 cursor-pointer font-bold'; | |
| closeBtn.textContent = '×'; | |
| closeBtn.onclick = () => this.closeSettingsModal(); | |
| header.appendChild(closeBtn); | |
| content.appendChild(header); | |
| // Body | |
| const body = document.createElement('div'); | |
| body.className = 'modal-body'; | |
| // Subtitle | |
| const sub1 = document.createElement('p'); | |
| sub1.className = 'font-body-sm text-slate-500 mb-4'; | |
| sub1.textContent = 'Kelola topik riset pasar, scraping berita industri, dan peringkasan otomatis AI.'; | |
| body.appendChild(sub1); | |
| // Table list wrapper | |
| const tableWrapper = document.createElement('div'); | |
| tableWrapper.className = 'mb-6 max-h-48 overflow-y-auto border border-slate-200 rounded-lg'; | |
| const table = document.createElement('table'); | |
| table.className = 'custom-table'; | |
| const thead = document.createElement('thead'); | |
| const trHeader = document.createElement('tr'); | |
| ['Nama Proyek', 'Kata Kunci', 'Aksi'].forEach(txt => { | |
| const th = document.createElement('th'); | |
| th.textContent = txt; | |
| trHeader.appendChild(th); | |
| }); | |
| thead.appendChild(trHeader); | |
| table.appendChild(thead); | |
| const tbody = document.createElement('tbody'); | |
| tbody.id = 'modal-projects-tbody'; | |
| table.appendChild(tbody); | |
| tableWrapper.appendChild(table); | |
| body.appendChild(tableWrapper); | |
| // Add project Form | |
| const formTitle = document.createElement('h4'); | |
| formTitle.className = 'font-label-md text-slate-800 font-bold mb-3'; | |
| formTitle.textContent = 'Buat Proyek Baru'; | |
| body.appendChild(formTitle); | |
| const form = document.createElement('form'); | |
| form.onsubmit = (e) => this.handleCreateProject(e); | |
| const grid = document.createElement('div'); | |
| grid.className = 'grid grid-cols-2 gap-4 mb-4'; | |
| const col1 = document.createElement('div'); | |
| col1.className = 'flex flex-col gap-1'; | |
| const lblName = document.createElement('label'); | |
| lblName.className = 'font-label-sm text-slate-600 font-bold'; | |
| lblName.textContent = 'Nama Proyek:'; | |
| col1.appendChild(lblName); | |
| const inputName = document.createElement('input'); | |
| inputName.id = 'new-project-name'; | |
| inputName.required = true; | |
| inputName.className = 'form-input'; | |
| inputName.placeholder = 'Misal: FMCG Sustainability'; | |
| col1.appendChild(inputName); | |
| grid.appendChild(col1); | |
| const col2 = document.createElement('div'); | |
| col2.className = 'flex flex-col gap-1'; | |
| const lblKws = document.createElement('label'); | |
| lblKws.className = 'font-label-sm text-slate-600 font-bold'; | |
| lblKws.textContent = 'Kata Kunci (Koma-separated, maks 5):'; | |
| col2.appendChild(lblKws); | |
| const inputKws = document.createElement('input'); | |
| inputKws.id = 'new-project-keywords'; | |
| inputKws.required = true; | |
| inputKws.className = 'form-input'; | |
| inputKws.placeholder = 'Misal: ramah lingkungan, ESG ritel'; | |
| col2.appendChild(inputKws); | |
| grid.appendChild(col2); | |
| form.appendChild(grid); | |
| const col3 = document.createElement('div'); | |
| col3.className = 'flex flex-col gap-1 mb-4'; | |
| const lblDesc = document.createElement('label'); | |
| lblDesc.className = 'font-label-sm text-slate-600 font-bold'; | |
| lblDesc.textContent = 'Deskripsi Proyek (Opsional):'; | |
| col3.appendChild(lblDesc); | |
| const inputDesc = document.createElement('textarea'); | |
| inputDesc.id = 'new-project-desc'; | |
| inputDesc.rows = 2; | |
| inputDesc.className = 'form-input'; | |
| inputDesc.placeholder = 'Tulis deskripsi singkat...'; | |
| col3.appendChild(inputDesc); | |
| form.appendChild(col3); | |
| const submitBtn = document.createElement('button'); | |
| submitBtn.type = 'submit'; | |
| submitBtn.className = 'px-4 py-2 bg-secondary text-white font-label-md rounded-lg hover:opacity-90 transition-opacity'; | |
| submitBtn.textContent = 'Simpan Proyek'; | |
| form.appendChild(submitBtn); | |
| body.appendChild(form); | |
| content.appendChild(body); | |
| // Footer | |
| const footer = document.createElement('div'); | |
| footer.className = 'modal-footer'; | |
| const closeFooterBtn = document.createElement('button'); | |
| closeFooterBtn.className = 'px-4 py-2 bg-slate-200 text-slate-700 font-label-md rounded-lg hover:bg-slate-300 transition-colors'; | |
| closeFooterBtn.textContent = 'Tutup'; | |
| closeFooterBtn.onclick = () => this.closeSettingsModal(); | |
| footer.appendChild(closeFooterBtn); | |
| content.appendChild(footer); | |
| overlay.appendChild(content); | |
| document.body.appendChild(overlay); | |
| }, | |
| openSettingsModal: function() { | |
| this.initProjects().then(() => { | |
| this.renderProjectsTable(); | |
| const overlay = document.getElementById('settings-modal-overlay'); | |
| if (overlay) { | |
| overlay.classList.add('active'); | |
| } | |
| }); | |
| }, | |
| closeSettingsModal: function() { | |
| const overlay = document.getElementById('settings-modal-overlay'); | |
| if (overlay) { | |
| overlay.classList.remove('active'); | |
| } | |
| }, | |
| // --- Tab 1: Render projects table --- | |
| renderProjectsTable: function() { | |
| const tbody = document.getElementById('modal-projects-tbody'); | |
| if (!tbody) return; | |
| tbody.replaceChildren(); | |
| if (this.projects.length === 0) { | |
| const tr = document.createElement('tr'); | |
| const td = document.createElement('td'); | |
| td.setAttribute('colspan', '3'); | |
| td.className = 'text-center text-slate-400 py-4 font-body-sm'; | |
| td.textContent = 'Belum ada proyek ditambahkan.'; | |
| tr.appendChild(td); | |
| tbody.appendChild(tr); | |
| return; | |
| } | |
| this.projects.forEach(proj => { | |
| const tr = document.createElement('tr'); | |
| // Name cell | |
| const tdName = document.createElement('td'); | |
| const strong = document.createElement('strong'); | |
| strong.textContent = proj.name; | |
| tdName.appendChild(strong); | |
| // Check active indicator | |
| if (proj.id === this.activeProjectId) { | |
| const badge = document.createElement('span'); | |
| badge.className = 'ml-2 text-[10px] bg-sky-100 text-sky-700 px-1.5 py-0.5 rounded font-bold'; | |
| badge.textContent = 'Aktif'; | |
| tdName.appendChild(badge); | |
| } | |
| tr.appendChild(tdName); | |
| // Keywords cell | |
| const tdKws = document.createElement('td'); | |
| try { | |
| const kwsList = JSON.parse(proj.keywords); | |
| tdKws.textContent = kwsList.join(', '); | |
| } catch (e) { | |
| tdKws.textContent = proj.keywords; | |
| } | |
| tr.appendChild(tdKws); | |
| // Action cell | |
| const tdAction = document.createElement('td'); | |
| // Switch button if not active | |
| if (proj.id !== this.activeProjectId) { | |
| const switchBtn = document.createElement('button'); | |
| switchBtn.className = 'text-xs text-sky-600 hover:text-sky-800 font-bold mr-3'; | |
| switchBtn.textContent = 'Gunakan'; | |
| switchBtn.onclick = () => { | |
| this.switchProject(proj.id); | |
| this.renderProjectsTable(); | |
| }; | |
| tdAction.appendChild(switchBtn); | |
| } | |
| const delBtn = document.createElement('button'); | |
| delBtn.className = 'text-xs text-red-600 hover:text-red-800 font-bold'; | |
| delBtn.textContent = 'Hapus'; | |
| delBtn.onclick = () => this.handleDeleteProject(proj.id); | |
| tdAction.appendChild(delBtn); | |
| tr.appendChild(tdAction); | |
| tbody.appendChild(tr); | |
| }); | |
| }, | |
| handleCreateProject: async function(e) { | |
| e.preventDefault(); | |
| const name = document.getElementById('new-project-name').value.trim(); | |
| const keywordsStr = document.getElementById('new-project-keywords').value.trim(); | |
| const description = document.getElementById('new-project-desc').value.trim(); | |
| if (!name || !keywordsStr) return; | |
| const keywords = keywordsStr.split(',').map(k => k.trim()).filter(k => k.length > 0); | |
| this.showLoader("Membuat proyek baru..."); | |
| try { | |
| const response = await fetch('/api/projects', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, description, keywords }) | |
| }); | |
| this.hideLoader(); | |
| if (response.ok) { | |
| const newProj = await response.json(); | |
| this.showToast("Proyek berhasil dibuat!"); | |
| // Clear fields | |
| document.getElementById('new-project-name').value = ''; | |
| document.getElementById('new-project-keywords').value = ''; | |
| document.getElementById('new-project-desc').value = ''; | |
| // Set as active if it is the first or by default | |
| if (!this.activeProjectId) { | |
| this.activeProjectId = newProj.id; | |
| localStorage.setItem('trendai_active_project_id', this.activeProjectId); | |
| } | |
| await this.initProjects(); | |
| this.renderProjectsTable(); | |
| } else { | |
| const err = await response.json(); | |
| this.showToast(err.detail || "Gagal membuat proyek", "error"); | |
| } | |
| } catch (err) { | |
| this.hideLoader(); | |
| this.showToast("Gagal menyambung ke server", "error"); | |
| } | |
| }, | |
| handleDeleteProject: async function(id) { | |
| if (!confirm("Apakah Anda yakin ingin menghapus proyek ini beserta seluruh datanya?")) return; | |
| this.showLoader("Menghapus proyek..."); | |
| try { | |
| const response = await fetch(`/api/projects/${id}`, { method: 'DELETE' }); | |
| this.hideLoader(); | |
| if (response.ok) { | |
| this.showToast("Proyek berhasil dihapus"); | |
| if (this.activeProjectId === id) { | |
| this.activeProjectId = null; | |
| localStorage.removeItem('trendai_active_project_id'); | |
| } | |
| await this.initProjects(); | |
| // Set new active project if available | |
| if (!this.activeProjectId && this.projects.length > 0) { | |
| this.activeProjectId = this.projects[0].id; | |
| localStorage.setItem('trendai_active_project_id', this.activeProjectId); | |
| } | |
| this.renderProjectsTable(); | |
| // Trigger page load for new active project | |
| if (typeof window.loadPageData === 'function') { | |
| window.loadPageData(this.activeProjectId); | |
| } | |
| } else { | |
| this.showToast("Gagal menghapus proyek", "error"); | |
| } | |
| } catch (e) { | |
| this.hideLoader(); | |
| this.showToast("Gagal menyambung ke server", "error"); | |
| } | |
| }, | |
| }; | |
| // --- Page Bootstrap and Nav logic --- | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| // 1. Parse active project from localStorage | |
| const savedId = localStorage.getItem('trendai_active_project_id'); | |
| if (savedId) { | |
| window.TrendAI.activeProjectId = parseInt(savedId); | |
| } | |
| // 2. Fetch projects and update page selectors | |
| await window.TrendAI.initProjects(); | |
| // 3. If no active project, set to the first one available | |
| if (!window.TrendAI.activeProjectId && window.TrendAI.projects.length > 0) { | |
| window.TrendAI.switchProject(window.TrendAI.projects[0].id); | |
| } | |
| // 4. Initialize Settings Modal | |
| window.TrendAI.initSettingsModal(); | |
| // 5. Update Sidebar Links to work as standard local routes | |
| const links = document.querySelectorAll('aside nav a'); | |
| links.forEach(link => { | |
| const text = link.textContent.trim(); | |
| if (text.includes("Dashboard")) { | |
| link.setAttribute('href', '/dashboard.html'); | |
| if (window.location.pathname === '/' || window.location.pathname.includes('dashboard.html')) { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-secondary dark:text-secondary-fixed font-bold bg-surface-container-low dark:bg-surface-variant transition-colors'; | |
| } else { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-text-muted dark:text-on-surface-variant hover:bg-surface-container-low dark:hover:bg-surface-variant transition-colors'; | |
| } | |
| } else if (text.includes("Trend Discovery")) { | |
| link.setAttribute('href', '/trend_discovery.html'); | |
| if (window.location.pathname.includes('trend_discovery.html')) { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-secondary dark:text-secondary-fixed font-bold bg-surface-container-low dark:bg-surface-variant transition-colors'; | |
| } else { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-text-muted dark:text-on-surface-variant hover:bg-surface-container-low dark:hover:bg-surface-variant transition-colors'; | |
| } | |
| } else if (text.includes("Executive Briefs")) { | |
| link.setAttribute('href', '/executive_brief.html'); | |
| if (window.location.pathname.includes('executive_brief.html')) { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-secondary dark:text-secondary-fixed font-bold bg-surface-container-low dark:bg-surface-variant transition-colors'; | |
| } else { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-text-muted dark:text-on-surface-variant hover:bg-surface-container-low dark:hover:bg-surface-variant transition-colors'; | |
| } | |
| } else if (text.includes("Manajemen Berita")) { | |
| link.setAttribute('href', '/news_management.html'); | |
| if (window.location.pathname.includes('news_management.html')) { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-secondary dark:text-secondary-fixed font-bold bg-surface-container-low dark:bg-surface-variant transition-colors'; | |
| } else { | |
| link.className = 'flex items-center gap-3 px-4 py-3 rounded-lg text-text-muted dark:text-on-surface-variant hover:bg-surface-container-low dark:hover:bg-surface-variant transition-colors'; | |
| } | |
| } | |
| }); | |
| // 6. Bind listener to page-level project selector change | |
| const selectors = document.querySelectorAll('.project-selector'); | |
| selectors.forEach(select => { | |
| select.addEventListener('change', (e) => { | |
| window.TrendAI.switchProject(e.target.value); | |
| }); | |
| }); | |
| // 7. Update copyright year dynamically | |
| const copyrightElements = document.querySelectorAll('#copyright-year'); | |
| const currentYear = new Date().getFullYear(); | |
| copyrightElements.forEach(el => { | |
| el.textContent = currentYear; | |
| }); | |
| // 8. Initial loading call for page content | |
| if (window.TrendAI.activeProjectId && typeof window.loadPageData === 'function') { | |
| window.loadPageData(window.TrendAI.activeProjectId); | |
| } | |
| }); | |