/* Documents hub v2 — NAS, labels, Docling */ (function () { const D = window.__DOC_DATA || {}; const topWords = D.topWords || []; const summary = D.summary || {}; const allDocuments = D.documents || []; let wordsChartInst = null; let sentimentChartInst = null; window.documentsDash = function documentsDash() { return { activeTab: 'overview', filterQuery: '', filterSentiment: '', filterType: '', filterLabel: '', filteredDocs: allDocuments, docTypes: [...new Set(allDocuments.map((d) => d.doc_type).filter(Boolean))], labelVocabulary: [], photoTaxonomy: [], documentTaxonomy: [], photoFilterLabel: '', activeDoc: null, docSelectedLabels: [], docLabelsInput: '', docNotesInput: '', photos: [], activePhoto: null, photoModalOpen: false, modalPhotoLabels: [], activeOcr: '', photoLabelsInput: '', photoNotesInput: '', activeItems: [], detections: [], liveRefresh: false, liveTimer: null, nasOcrBusy: false, doclingStatus: 'Docling laden…', doclingFiles: [], doclingHistory: [], doclingFilter: '', doclingExtFilter: '', doclingSelected: '', doclingOpts: { ocr: true, tables: true, page_images: false, picture_images: true, force_full_page_ocr: false }, doclingFormats: ['markdown', 'html', 'json', 'text', 'doctags', 'yaml'], doclingExportFormats: ['markdown', 'html', 'json', 'text', 'doctags', 'yaml'], doclingBusy: false, doclingResult: null, doclingExports: {}, doclingTables: [], doclingPictures: [], doclingMeta: {}, doclingExportTab: 'markdown', doclingPreviewSearch: '', doclingFileStatus: {}, nasFileTotal: 0, nasLoading: false, doclingEditMode: true, doclingEditContent: '', doclingAutoConvert: true, dlOptionsOpen: false, doclingFileLoading: false, doclingFileKind: '', doclingSupportedSuffixes: [], dlExtGroups: [ { ext: '', label: 'Alles', icon: '' }, { ext: '.pdf', label: 'PDF', icon: '📕' }, { ext: '.docx', label: 'Word', icon: '📝' }, { ext: '.doc', label: 'DOC', icon: '📝' }, { ext: '.xlsx', label: 'Excel', icon: '📊' }, { ext: '.xls', label: 'XLS', icon: '📊' }, { ext: '.pptx', label: 'PPT', icon: '📽️' }, { ext: '.ppt', label: 'PPT', icon: '📽️' }, { ext: '.txt', label: 'Tekst', icon: '📄' }, { ext: '.md', label: 'MD', icon: '📄' }, { ext: '.csv', label: 'CSV', icon: '📋' }, { ext: '.html', label: 'HTML', icon: '🌐' }, { ext: '.json', label: 'JSON', icon: '{}' }, { ext: '.png', label: 'IMG', icon: '🖼️' }, ], pptTitle: 'Foodlinkk Presentatie', pptSubtitle: 'Foodlinkk', pptFilename: '', pptSlides: [], pptSourcePdf: '', pptLastOutput: '', pptBusy: false, pptDownloadPath: '', chatMessages: [], chatInput: '', chatBusy: false, chatStatus: 'RAG laden…', chatSearchQ: '', chatSearchHits: [], chatScopeFile: false, chatScopeClient: true, chatUseHerman: false, chatReindexing: false, llmProviders: [], chatLlmProviderId: '', nasDiagHint: '', autoSyncBusy: false, autoSyncLabel: 'Second brain: —', autoSyncTimer: null, linkClients: [], linkClientId: '', linkProjectId: '', linkPathInput: '', linkProjects: [], client360: { stats: {}, links: [], sentiment_breakdown: [], monthly_trends: [], top_words: [], stores: [], deals: [] }, shareBrowsePath: '', shareBrowseEntries: [], shareBrowseLoading: false, shareBrowseRecursive: true, selectedSharePaths: [], docVersions: [], doclingFileMtime: '', init() { try { document.querySelectorAll('.doc-row-fallback').forEach(function (el) { el.remove(); }); var tab = window.__docActiveTab || this.activeTab || 'overview'; this.activeTab = tab; if (window.DocTabs && window.DocTabs.show) window.DocTabs.show(tab); this.$watch('activeTab', function (v) { if (v && window.DocTabs && window.DocTabs.getActive && window.DocTabs.getActive() !== v) { window.DocTabs.show(v); } }); this.applyFilters(); this.renderCharts(window.__vizMode || 'neo-bars'); this.loadPhotos(); this.loadLabelVocabulary(); this.loadTaxonomy(); this.loadShareFiles(false); this.browseShare('', false); this.loadLinkClients(); this.loadLlmProviders(); this.refreshAutoSyncStatus(); document.addEventListener('foodlinkk:vizmode', (ev) => this.renderCharts(ev.detail.mode)); } catch (e) { console.error('documentsDash init:', e); Cockpit.toast('Documenten UI gedeeltelijk geladen — ververs de pagina (Ctrl+F5)', 'error'); } }, switchTab(tab) { this.activeTab = tab; window.__docActiveTab = tab; if (window.DocTabs && window.DocTabs.show) window.DocTabs.show(tab); this.onTabShown(tab); }, onTabShown(tab) { const runners = { labels: () => this.loadPhotos(), docling: () => this.initDocling(), powerpoint: () => this.initPowerpoint(), chat: () => this.initDocChat(), analytics360: () => this.initAnalytics360(), }; const fn = runners[tab]; if (fn) { try { const out = fn(); if (out && typeof out.catch === 'function') out.catch(() => {}); } catch (err) { console.error('switchTab', tab, err); } } }, formatTs(iso) { if (!iso) return '—'; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16); return d.toLocaleString('nl-NL', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch (e) { return String(iso).slice(0, 16); } }, async loadVersions(path) { if (!path) { this.docVersions = []; return; } try { const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path)); this.docVersions = data.versions || []; } catch (e) { this.docVersions = []; } }, async loadVersion(versionId) { if (!versionId || !this.doclingSelected) return; try { const data = await Cockpit.api('/docling/versions/' + versionId); if (data.version && data.version.content != null) { this.doclingEditContent = data.version.content; Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info'); } } catch (e) { Cockpit.toast(e.message || 'Versie laden mislukt', 'error'); } }, async recordVersion(label) { if (!this.doclingSelected || !this.doclingEditContent) return; try { await Cockpit.api('/docling/versions', { method: 'POST', body: JSON.stringify({ storage_path: this.doclingSelected, content: this.doclingEditContent, source_format: 'markdown', version_label: label || 'bewerking', nas_mtime: this.doclingFileMtime || null, }), }); await this.loadVersions(this.doclingSelected); } catch (e) { } }, formatTs(iso) { if (!iso) return '—'; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16); return d.toLocaleString('nl-NL', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch (e) { return String(iso).slice(0, 16); } }, async loadVersions(path) { if (!path) { this.docVersions = []; return; } try { const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path)); this.docVersions = data.versions || []; } catch (e) { this.docVersions = []; } }, async loadVersion(versionId) { if (!versionId || !this.doclingSelected) return; try { const data = await Cockpit.api('/docling/versions/' + versionId); if (data.version && data.version.content != null) { this.doclingEditContent = data.version.content; Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info'); } } catch (e) { Cockpit.toast(e.message || 'Versie laden mislukt', 'error'); } }, async recordVersion(label) { if (!this.doclingSelected || !this.doclingEditContent) return; try { await Cockpit.api('/docling/versions', { method: 'POST', body: JSON.stringify({ storage_path: this.doclingSelected, content: this.doclingEditContent, source_format: 'markdown', version_label: label || 'bewerking', nas_mtime: this.doclingFileMtime || null, }), }); await this.loadVersions(this.doclingSelected); } catch (e) {} }, async loadLlmProviders() { try { const data = await Cockpit.api('/api/settings/llm'); this.llmProviders = data.providers || []; const def = data.default || this.llmProviders.find((p) => p.is_default); if (def && !this.chatLlmProviderId) this.chatLlmProviderId = def.id; const banner = document.getElementById('doc-hub-status'); if (banner && def && def.provider_type === 'ollama') { banner.style.display = 'block'; banner.textContent = 'Tip: Ollama is traag op CPU (~2 min). Voeg DeepSeek/Gemini toe via Instellingen → AI / LLM voor snelle chat & RAG.'; } const cloud = this.llmProviders.find((p) => p.provider_type !== 'ollama' && p.api_key_set); if (cloud && def && def.provider_type === 'ollama') { this.chatLlmProviderId = cloud.id; } } catch (e) {} }, onChatProviderChange() { const p = this.llmProviders.find((row) => Number(row.id) === Number(this.chatLlmProviderId)); this.chatStatus = p ? (p.label + ' · ' + p.model) : 'Standaard LLM'; }, filteredPhotos() { if (!this.photoFilterLabel) return this.photos; return this.photos.filter((p) => (p.user_labels || []).includes(this.photoFilterLabel)); }, mergeShareItems(items) { const byPath = new Map(allDocuments.map((d) => [d.storage_path, d])); (items || []).forEach((f) => { const path = f.path || f.storage_path; if (!path) return; if (byPath.has(path)) { const ex = byPath.get(path); if (f.modified_at) ex.modified_at = f.modified_at; if (!ex.analyzed_at && f.modified_at) ex.modified_at = f.modified_at; return; } const row = { filename: f.filename || path.split('/').pop(), storage_path: path, doc_type: f.doc_type || 'general', word_count: f.word_count || 0, sentiment_label: f.sentiment_label || 'neutral', sentiment_compound: f.sentiment_compound || 0, analyzed_at: f.analyzed_at || '', modified_at: f.modified_at || '', user_labels: f.user_labels || [], label_notes: f.label_notes || '', from_share: !f.analyzed_at, }; allDocuments.push(row); byPath.set(path, row); }); this.docTypes = [...new Set(allDocuments.map((d) => d.doc_type).filter(Boolean))]; this.applyFilters(); }, async loadShareFiles(showToast) { try { const r = await fetch('/api/admin/documents/share-files?limit=5000'); const data = await r.json(); if (data.ok !== false) { this.mergeShareItems(data.items || []); this.nasFileTotal = data.total || (data.items || []).length; if (showToast) Cockpit.toast((this.nasFileTotal || 0) + ' bestanden op NAS', 'success'); } else if (showToast) { Cockpit.toast(data.error || 'NAS laden mislukt', 'error'); } } catch (e) { if (showToast) Cockpit.toast('NAS bestanden laden mislukt', 'error'); } }, async refreshShare(showToast) { try { if (showToast !== false) Cockpit.toast('NAS scan gestart (~30s)…', 'info'); const scanCtrl = new AbortController(); const scanTimer = setTimeout(() => scanCtrl.abort(), 90000); try { await fetch('/api/admin/documents/trigger-scan', { method: 'POST', signal: scanCtrl.signal }); } finally { clearTimeout(scanTimer); } await this.loadShareFiles(showToast !== false); } catch (e) { if (showToast !== false) Cockpit.toast('Share scan mislukt — toon cache', 'error'); await this.loadShareFiles(false); } }, applyFilters() { const q = this.filterQuery.toLowerCase(); this.filteredDocs = allDocuments.filter((d) => { if (this.filterSentiment && d.sentiment_label !== this.filterSentiment) return false; if (this.filterType && d.doc_type !== this.filterType) return false; if (this.filterLabel && !(d.user_labels || []).includes(this.filterLabel)) return false; if (q) { const hay = ((d.filename || '') + ' ' + (d.storage_path || '') + ' ' + (d.user_labels || []).join(' ')).toLowerCase(); if (!hay.includes(q)) return false; } return true; }); }, async loadLabelVocabulary() { try { const r = await fetch('/api/admin/labels/vocabulary?limit=60'); const data = await r.json(); if (data.ok) this.labelVocabulary = data.labels || []; } catch (e) {} }, async loadTaxonomy() { try { const r = await fetch('/api/admin/labels/taxonomy'); const data = await r.json(); if (data.ok) { this.photoTaxonomy = data.photo || []; this.documentTaxonomy = data.document || []; } } catch (e) { } }, formatTs(iso) { if (!iso) return '—'; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16); return d.toLocaleString('nl-NL', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch (e) { return String(iso).slice(0, 16); } }, async loadVersions(path) { if (!path) { this.docVersions = []; return; } try { const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path)); this.docVersions = data.versions || []; } catch (e) { this.docVersions = []; } }, async loadVersion(versionId) { if (!versionId || !this.doclingSelected) return; try { const data = await Cockpit.api('/docling/versions/' + versionId); if (data.version && data.version.content != null) { this.doclingEditContent = data.version.content; Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info'); } } catch (e) { Cockpit.toast(e.message || 'Versie laden mislukt', 'error'); } }, async recordVersion(label) { if (!this.doclingSelected || !this.doclingEditContent) return; try { await Cockpit.api('/docling/versions', { method: 'POST', body: JSON.stringify({ storage_path: this.doclingSelected, content: this.doclingEditContent, source_format: 'markdown', version_label: label || 'bewerking', nas_mtime: this.doclingFileMtime || null, }), }); await this.loadVersions(this.doclingSelected); } catch (e) {} }, openDocLabel(doc) { this.activeDoc = doc; this.docSelectedLabels = [...(doc.user_labels || [])]; this.docLabelsInput = ''; this.docNotesInput = doc.label_notes || ''; }, isShareSelected(path) { return this.selectedSharePaths.includes(path); }, toggleShareSelect(path) { const idx = this.selectedSharePaths.indexOf(path); if (idx >= 0) this.selectedSharePaths.splice(idx, 1); else this.selectedSharePaths.push(path); }, openInWorkbench(path) { if (!path) return; const folder = path.includes('/') ? path.replace(/\/[^/]+$/, '') : ''; const open = async () => { if (folder !== this.shareBrowsePath) { await this.browseShare(folder, false); } this.switchTab('docling'); await this.openDoclingFile(path); }; open().catch((e) => Cockpit.toast(e.message || 'Openen mislukt', 'error')); }, openSelectedInWorkbench() { const paths = [...this.selectedSharePaths]; if (!paths.length) return; this.openInWorkbench(paths[0]); if (paths.length > 1) { Cockpit.toast(paths.length + ' geselecteerd — eerste geopend in Workbench', 'info'); } }, sharePathParts() { return (this.shareBrowsePath || '').split('/').filter(Boolean); }, sharePathPrefix(idx) { return this.sharePathParts().slice(0, idx + 1).join('/'); }, shareBrowseFolders() { return (this.shareBrowseEntries || []).filter((e) => e.type === 'folder'); }, async browseShare(path, showToast) { const rel = (path || '').replace(/^\/+/, '').replace(/\/+$/, ''); this.shareBrowsePath = rel; this.shareBrowseLoading = true; try { const r = await fetch( '/api/admin/documents/share-browse?path=' + encodeURIComponent(rel) + '&limit=3000&recursive=' + (this.shareBrowseRecursive ? 'true' : 'false'), ); const data = await r.json(); if (data.ok !== false && Array.isArray(data.entries)) { this.shareBrowseEntries = data.entries; const files = data.entries.filter((e) => e.type === 'file'); files.forEach((f) => { const ex = this.doclingFiles.find((x) => x.path === f.path); if (!ex) { this.doclingFiles.push({ path: f.path, filename: f.filename || f.name, size: f.size, modified_at: f.modified_at, }); } }); if (showToast) Cockpit.toast((data.entries.length || 0) + ' items in ' + (rel || 'Share'), 'success'); } else { await this.loadAllNasFiles(false); this.buildShareEntriesFromFiles(); if (showToast) Cockpit.toast('Share map geladen (flat)', 'info'); } } catch (e) { await this.loadAllNasFiles(false); this.buildShareEntriesFromFiles(); } finally { this.shareBrowseLoading = false; } }, browseShareParent() { const parts = this.sharePathParts(); parts.pop(); this.browseShare(parts.join('/'), false); }, buildShareEntriesFromFiles() { const prefix = this.shareBrowsePath ? this.shareBrowsePath + '/' : ''; const folders = new Set(); const files = []; this.doclingFiles.forEach((f) => { const path = f.path || ''; if (prefix && !path.startsWith(prefix)) return; if (!prefix && path.includes('/')) { folders.add(path.split('/')[0]); return; } const rest = prefix ? path.slice(prefix.length) : path; if (!rest) return; const slash = rest.indexOf('/'); if (slash >= 0) folders.add(rest.slice(0, slash)); else files.push({ type: 'file', path, filename: f.filename, name: f.filename, size: f.size }); }); this.shareBrowseEntries = [ ...[...folders].sort().map((name) => ({ type: 'folder', name, path: prefix ? prefix.replace(/\/$/, '') + (prefix.endsWith('/') ? '' : '/') + name : name, })), ...files, ]; }, toggleDocLabel(id) { const idx = this.docSelectedLabels.indexOf(id); if (idx >= 0) this.docSelectedLabels.splice(idx, 1); else this.docSelectedLabels.push(id); }, async saveDocLabels() { if (!this.activeDoc) return; const labels = [...this.docSelectedLabels]; this.docLabelsInput.split(',').map((s) => s.trim()).filter(Boolean).forEach((t) => { if (!labels.includes(t)) labels.push(t); }); try { const r = await Cockpit.api('/documents/labels', { method: 'PATCH', body: JSON.stringify({ storage_path: this.activeDoc.storage_path, labels, notes: this.docNotesInput || null, }), }); const saved = r.document || {}; this.activeDoc.user_labels = saved.user_labels || labels; this.activeDoc.label_notes = saved.label_notes || this.docNotesInput; const idx = allDocuments.findIndex((d) => d.storage_path === this.activeDoc.storage_path); if (idx >= 0) { allDocuments[idx].user_labels = this.activeDoc.user_labels; allDocuments[idx].label_notes = this.activeDoc.label_notes; } this.applyFilters(); await this.loadLabelVocabulary(); Cockpit.toast('Document gelabeld', 'success'); } catch (e) { Cockpit.toast(e.message, 'error'); } }, toggleModalLabel(id) { const idx = this.modalPhotoLabels.indexOf(id); if (idx >= 0) this.modalPhotoLabels.splice(idx, 1); else this.modalPhotoLabels.push(id); }, async openPhotoModal(id) { this.activePhoto = id; this.photoModalOpen = true; document.body.style.overflow = 'hidden'; this.modalPhotoLabels = []; const p = this.photos.find((x) => x.id === id); if (p && p.user_labels) this.modalPhotoLabels = [...p.user_labels]; this.photoLabelsInput = ''; this.photoNotesInput = p ? (p.label_notes || '') : ''; this.activeOcr = p ? (p.ocr_preview || '') : ''; try { const r = await Cockpit.api('/photos/' + id + '/detections'); this.detections = r.detections || []; this.activeItems = r.extracted_items || []; this.activeOcr = r.ocr_text || this.activeOcr; this.modalPhotoLabels = [...(r.user_labels || this.modalPhotoLabels)]; this.photoNotesInput = r.label_notes || this.photoNotesInput; } catch (e) { Cockpit.toast(e.message, 'error'); } }, closePhotoModal() { this.photoModalOpen = false; document.body.style.overflow = ''; }, async saveModalPhotoLabels() { if (!this.activePhoto) return; const labels = [...this.modalPhotoLabels]; this.photoLabelsInput.split(',').map((s) => s.trim()).filter(Boolean).forEach((t) => { if (!labels.includes(t)) labels.push(t); }); if (!labels.length) return Cockpit.toast('Kies minimaal één label-type', 'error'); try { const r = await Cockpit.api('/photos/' + this.activePhoto + '/labels', { method: 'PATCH', body: JSON.stringify({ labels, notes: this.photoNotesInput || null }), }); const saved = r.photo || {}; const p = this.photos.find((x) => x.id === this.activePhoto); if (p) { p.user_labels = saved.user_labels || labels; p.label_notes = saved.label_notes || this.photoNotesInput; } await this.loadLabelVocabulary(); this.closePhotoModal(); Cockpit.toast('Foto gelabeld', 'success'); } catch (e) { Cockpit.toast(e.message, 'error'); } }, labelDisplayName(id) { const t = this.photoTaxonomy.find((x) => x.id === id); return t ? t.label : id; }, async batchNasOcr(force) { if (this.nasOcrBusy) return; this.nasOcrBusy = true; Cockpit.toast('NAS OCR gestart…', 'info'); try { const url = '/api/admin/documents/nas-ocr' + (force ? '?force=true' : ''); const r = await fetch(url, { method: 'POST' }); const data = await r.json(); if (!r.ok || !data.ok) throw new Error(data.detail || data.error || 'OCR mislukt'); Cockpit.toast((data.images_extracted || 0) + ' afbeeldingen ge-OCR\'d', 'success'); await this.loadPhotos(); } catch (e) { Cockpit.toast(e.message || 'NAS OCR mislukt', 'error'); } finally { this.nasOcrBusy = false; } }, renderCharts(mode) { mode = mode || window.__vizMode || 'neo-bars'; if (typeof Chart === 'undefined') return; if (wordsChartInst) { wordsChartInst.destroy(); wordsChartInst = null; } if (sentimentChartInst) { sentimentChartInst.destroy(); sentimentChartInst = null; } const dash = this; const wc = document.getElementById('wordsChart'); const sc = document.getElementById('sentimentChart'); if (!wc || !sc) return; wordsChartInst = new Chart(wc, { type: 'bar', data: { labels: topWords.slice(0, 10).map((w) => w.lemma), datasets: [{ data: topWords.slice(0, 10).map((w) => w.total_count), backgroundColor: 'rgba(0,229,255,0.75)', borderWidth: 0, borderRadius: 6, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, onClick: (_, els) => { if (els[0]) { dash.filterQuery = topWords[els[0].index].lemma; dash.applyFilters(); } }, scales: { y: { beginAtZero: true, ticks: { color: '#64748b', font: { size: 9 } } }, x: { ticks: { color: '#64748b', font: { size: 9 } } }, }, }, }); sentimentChartInst = new Chart(sc, { type: 'doughnut', data: { labels: ['Positief', 'Neutraal', 'Negatief'], datasets: [{ data: [summary.positive, summary.neutral, summary.negative], backgroundColor: ['#4ade80', '#64748b', '#fb7185'], borderWidth: 0, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#94a3b8', font: { size: 9 } } } }, onClick: (_, els) => { if (!els[0]) return; dash.filterSentiment = ['positive', 'neutral', 'negative'][els[0].index]; dash.applyFilters(); }, }, }); }, async loadPhotos() { try { const r = await Cockpit.api('/photos?limit=80'); this.photos = r.photos || []; } catch (e) {} }, toggleLive() { if (this.liveTimer) clearInterval(this.liveTimer); if (this.liveRefresh) { this.liveTimer = setInterval(() => { this.loadPhotos(); this.refreshShare(false); }, 15000); } }, async uploadPhoto(ev) { const file = ev.target.files && ev.target.files[0]; if (!file) return; const b64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result.split(',')[1]); r.onerror = rej; r.readAsDataURL(file); }); try { await Cockpit.api('/photos/analyze', { method: 'POST', body: JSON.stringify({ image_b64: b64, source: 'upload', filename: file.name }), }); Cockpit.toast('Foto geanalyseerd', 'success'); await this.loadPhotos(); } catch (e) { Cockpit.toast(e.message, 'error'); } ev.target.value = ''; }, /* ── Docling ── */ doclingFilteredFiles() { const q = (this.doclingFilter || '').toLowerCase(); const fromBrowse = (this.shareBrowseEntries || []) .filter((e) => e.type === 'file') .map((e) => ({ path: e.path, filename: e.filename || e.name, size: e.size, modified_at: e.modified_at, })); const pool = fromBrowse.length ? fromBrowse : this.doclingFiles.filter((f) => { const path = f.path || ''; const prefix = this.shareBrowsePath ? this.shareBrowsePath.replace(/\/?$/, '/') : ''; if (prefix) { if (!path.startsWith(prefix)) return false; if (path.slice(prefix.length).includes('/')) return false; } else if (path.includes('/')) { return false; } return true; }); return pool.filter((f) => { if (this.doclingExtFilter) { const ext = this.doclingExtFilter.startsWith('.') ? this.doclingExtFilter : '.' + this.doclingExtFilter; const pathExt = this.fileExtension(f.path); if (ext === '.png') { if (!['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.tif'].includes(pathExt)) return false; } else if (pathExt !== ext.toLowerCase()) return false; } if (!q) return true; return ((f.path || '') + ' ' + (f.filename || '')).toLowerCase().includes(q); }); }, fileExtension(path) { const p = (path || '').toLowerCase(); const i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; }, fileIcon(path) { const ext = this.fileExtension(path); const map = { '.pdf': '📕', '.docx': '📝', '.doc': '📝', '.xlsx': '📊', '.xls': '📊', '.pptx': '📽️', '.ppt': '📽️', '.txt': '📄', '.md': '📄', '.csv': '📋', '.html': '🌐', '.htm': '🌐', '.json': '{}', '.xml': '📰', '.yaml': '⚙️', '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.webp': '🖼️', '.gif': '🖼️', }; return map[ext] || '📁'; }, fileKind(path) { const ext = this.fileExtension(path); if (this.isPlainTextExt(ext)) return 'text'; if (this.isImageExt(ext)) return 'image'; if (ext === '.pdf') return 'pdf'; if (['.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'].includes(ext)) return 'office'; return 'other'; }, fileKindLabel(path) { const k = this.fileKind(path); return { text: 'Tekst', image: 'Afbeelding', pdf: 'PDF', office: 'Office', other: 'Bestand' }[k] || 'Bestand'; }, isPlainTextExt(ext) { return ['.txt', '.md', '.csv', '.json', '.xml', '.html', '.htm', '.yaml', '.yml', '.log', '.rtf'].includes(ext); }, isImageExt(ext) { return ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.tif'].includes(ext); }, isDoclingSupported(path) { const ext = this.fileExtension(path); if (!ext) return false; const supported = this.doclingSupportedSuffixes.length ? this.doclingSupportedSuffixes : ['.pdf', '.docx', '.pptx', '.xlsx', '.html', '.htm', '.md', '.txt', '.png', '.jpg', '.jpeg', '.webp', '.tiff', '.tif', '.bmp', '.gif']; return supported.includes(ext); }, pptPdfFiles() { return this.doclingFiles.filter((f) => (f.path || '').toLowerCase().endsWith('.pdf')); }, pptPptxFiles() { return this.doclingFiles.filter((f) => (f.path || '').toLowerCase().endsWith('.pptx')); }, async loadAllNasFiles(showToast) { this.nasLoading = true; try { if (showToast) Cockpit.toast('NAS bestanden laden…', 'info'); const r = await fetch('/api/admin/documents/share-files?limit=5000'); const data = await r.json(); const byPath = new Map(); allDocuments.forEach((d) => { byPath.set(d.storage_path, { path: d.storage_path, filename: d.filename, word_count: d.word_count, sentiment_label: d.sentiment_label, user_labels: d.user_labels || [], }); }); (data.items || []).forEach((f) => { const path = f.path || f.storage_path; if (!path) return; const ex = byPath.get(path) || {}; byPath.set(path, { ...ex, path, filename: f.filename || path.split('/').pop(), size: f.size, modified_at: f.modified_at, }); }); this.doclingFiles = Array.from(byPath.values()).sort((a, b) => (a.filename || '').localeCompare(b.filename || ''), ); this.nasFileTotal = data.total || this.doclingFiles.length; this.buildShareEntriesFromFiles(); if (showToast) Cockpit.toast(this.nasFileTotal + ' bestanden op NAS', 'success'); } catch (e) { if (showToast) Cockpit.toast('NAS laden mislukt', 'error'); } finally { this.nasLoading = false; } }, fileDoclingStatus(path) { return this.doclingFileStatus[path] || ''; }, selectDoclingFile(path) { this.openInWorkbench(path); }, async openDoclingFile(path) { if (!path || (this.doclingFileLoading && this.doclingSelected === path)) return; this.doclingSelected = path; const fileMeta = this.doclingFiles.find((f) => f.path === path) || {}; this.doclingFileMtime = fileMeta.modified_at || ''; this.doclingFileKind = this.fileKind(path); this.doclingEditMode = true; this.doclingFileLoading = true; this.doclingResult = null; this.doclingExports = {}; this.doclingTables = []; this.doclingPictures = []; this.doclingEditContent = ''; await this.loadVersions(path); const ext = this.fileExtension(path); if (this.doclingFileKind === 'pdf') this.doclingExportTab = 'original'; else if (this.doclingFileKind === 'image') this.doclingExportTab = 'original'; else this.doclingExportTab = 'rendered'; try { const hasDraft = await this.loadWorkspace(path); if (hasDraft) { this.doclingFileLoading = false; return; } if (this.isPlainTextExt(ext)) { await this.loadNasRawText(path); this.doclingFileLoading = false; return; } if (this.doclingFileKind === 'image') { this.doclingFileLoading = false; if (this.doclingAutoConvert && this.isDoclingSupported(path)) { await this.runDoclingConvert(true); } return; } if (this.doclingFileKind === 'pdf') { const cached = await this.loadDoclingCached(); this.doclingFileLoading = false; if (!cached && this.doclingAutoConvert) await this.runDoclingConvert(true); return; } const cached = await this.loadDoclingCached(); if (cached) { this.doclingFileLoading = false; return; } if (this.doclingAutoConvert && this.isDoclingSupported(path)) { this.doclingFileLoading = false; await this.runDoclingConvert(true); } else if (!this.isDoclingSupported(path)) { this.doclingEditContent = '# ' + path.split('/').pop() + '\n\nDit bestandstype wordt niet ondersteund voor Docling-conversie.\nOpen het origineel via de knop rechtsboven.'; Cockpit.toast('Geen Docling-support voor ' + ext, 'info'); } } catch (e) { Cockpit.toast(e.message || 'Laden mislukt', 'error'); } finally { this.doclingFileLoading = false; } }, async loadNasRawText(path) { const data = await Cockpit.api('/docling/nas-read?path=' + encodeURIComponent(path)); this.doclingEditContent = data.content || ''; this.doclingExports = { markdown: this.doclingEditContent, text: this.doclingEditContent }; this.doclingResult = { status: 'raw' }; this.doclingExportTab = 'rendered'; }, async loadWorkspace(path) { if (!path) return false; try { const data = await Cockpit.api('/docling/workspace?path=' + encodeURIComponent(path)); if (data.workspace && data.workspace.content) { this.doclingEditContent = data.workspace.content; return true; } } catch (e) {} const md = this.doclingExports.markdown; if (typeof md === 'string') this.doclingEditContent = md; return false; }, async saveWorkspace() { if (!this.doclingSelected) return; try { await Cockpit.api('/docling/workspace', { method: 'PUT', body: JSON.stringify({ storage_path: this.doclingSelected, content: this.doclingEditContent, source_format: 'markdown', }), }); if (this.doclingExports) this.doclingExports.markdown = this.doclingEditContent; await this.recordVersion('concept opgeslagen'); Cockpit.toast('Concept opgeslagen', 'success'); } catch (e) { Cockpit.toast(e.message, 'error'); } }, async saveToNas() { if (!this.doclingSelected || !this.doclingEditContent) return; try { const data = await Cockpit.api('/docling/save-nas', { method: 'POST', body: JSON.stringify({ path: this.doclingSelected, content: this.doclingEditContent, subdir: 'Telegram/Exports', }), }); Cockpit.toast('Opgeslagen op NAS: ' + (data.path || data.filename), 'success'); await this.recordVersion('NAS export'); await this.loadAllNasFiles(false); } catch (e) { Cockpit.toast(e.message, 'error'); } }, sendToPowerpoint() { this.switchTab('powerpoint'); this.importMarkdownToSlides(this.doclingEditContent || this.doclingExports.markdown || ''); }, async initPowerpoint() { await this.loadAllNasFiles(false); if (!this.pptSlides.length) { this.pptSlides = [{ title: 'Intro', bullets: [], bulletsText: '' }]; } }, markdownToSlides(md) { const slides = []; let current = { title: '', bullets: [], bulletsText: '' }; (md || '').split('\n').forEach((line) => { const t = line.trim(); if (t.startsWith('## ')) { if (current.title || current.bullets.length) slides.push({ ...current }); current = { title: t.slice(3).trim(), bullets: [], bulletsText: '' }; } else if (t.startsWith('# ') && !current.title) { current.title = t.slice(2).trim(); } else if (/^[-*]\s/.test(t)) { current.bullets.push(t.replace(/^[-*]\s+/, '')); current.bulletsText = current.bullets.join('\n'); } else if (t.length > 20 && !t.startsWith('|')) { current.bullets.push(t); current.bulletsText = current.bullets.join('\n'); } }); if (current.title || current.bullets.length) slides.push(current); return slides.length ? slides : [{ title: 'Inhoud', bullets: [(md || '').slice(0, 500)], bulletsText: (md || '').slice(0, 500) }]; }, importMarkdownToSlides(md) { const text = md || this.doclingEditContent || this.doclingExports.markdown || ''; if (!text) return Cockpit.toast('Geen markdown — eerst Docling convert', 'info'); this.pptSlides = this.markdownToSlides(text); const firstLine = (text.split('\n').find((l) => l.startsWith('# ')) || '').replace(/^#\s+/, ''); if (firstLine) this.pptTitle = firstLine.slice(0, 200); Cockpit.toast(this.pptSlides.length + ' slides gegenereerd', 'success'); }, addPptSlide() { this.pptSlides.push({ title: 'Nieuwe slide', bullets: [], bulletsText: '' }); }, removePptSlide(idx) { this.pptSlides.splice(idx, 1); }, syncSlideBullets(slide) { slide.bullets = (slide.bulletsText || '').split('\n').map((s) => s.trim()).filter(Boolean); }, async createPptx() { if (!this.pptSlides.length) return; this.pptSlides.forEach((s) => this.syncSlideBullets(s)); this.pptBusy = true; try { const data = await Cockpit.api('/pptx/create', { method: 'POST', body: JSON.stringify({ title: this.pptTitle || 'Presentatie', subtitle: this.pptSubtitle || 'Foodlinkk', filename: this.pptFilename || this.pptTitle, slides: this.pptSlides.map((s) => ({ title: s.title, bullets: s.bullets })), }), }); this.pptLastOutput = data.relative_output || data.output || data.path || 'klaar'; Cockpit.toast('PowerPoint aangemaakt', 'success'); await this.loadAllNasFiles(false); } catch (e) { Cockpit.toast(e.message, 'error'); } finally { this.pptBusy = false; } }, async convertPdfToPptx() { if (!this.pptSourcePdf) return; this.pptBusy = true; Cockpit.toast('PDF → PPTX conversie…', 'info'); try { const data = await Cockpit.api('/pptx/pdf-to-pptx?path=' + encodeURIComponent(this.pptSourcePdf) + '&mode=smart'); this.pptLastOutput = data.relative_output || data.output || ''; Cockpit.toast('PPTX gegenereerd', 'success'); await this.loadAllNasFiles(false); } catch (e) { Cockpit.toast(e.message, 'error'); } finally { this.pptBusy = false; } }, async loadDoclingHistory() { try { const data = await Cockpit.api('/docling/history?limit=40'); this.doclingHistory = data.items || []; const statusMap = {}; (this.doclingHistory || []).forEach((h) => { if (!statusMap[h.storage_path]) statusMap[h.storage_path] = h.status; }); this.doclingFileStatus = statusMap; } catch (e) {} }, async initDocling() { if (!this.doclingFiles.length) await this.loadAllNasFiles(false); else if (!this.shareBrowseEntries.length) await this.browseShare(this.shareBrowsePath || '', false); else this.nasLoading = false; try { const caps = await Cockpit.api('/docling/capabilities'); if (caps.installed) { this.doclingStatus = 'Docling actief · ' + (caps.supported_suffixes || []).length + ' formaten · klik = open + convert'; if (caps.export_formats) this.doclingExportFormats = caps.export_formats; if (caps.supported_suffixes) this.doclingSupportedSuffixes = caps.supported_suffixes; } else { this.doclingStatus = 'Docling niet geïnstalleerd'; } } catch (e) { this.doclingStatus = 'Docling offline: ' + e.message; } await this.loadDoclingHistory(); }, doclingPayload() { return { path: this.doclingSelected, ocr: this.doclingOpts.ocr, tables: this.doclingOpts.tables, page_images: this.doclingOpts.page_images, picture_images: this.doclingOpts.picture_images, force_full_page_ocr: this.doclingOpts.force_full_page_ocr, formats: this.doclingFormats.length ? this.doclingFormats : ['markdown', 'json'], }; }, applyDoclingResult(data) { const src = data.result || data; this.doclingResult = src; this.doclingExports = src.exports || data.exports || {}; this.doclingTables = src.tables_data || data.tables || []; this.doclingPictures = src.pictures || data.pictures || []; this.doclingMeta = { page_count: src.page_count || data.page_count || 0, metadata: src.metadata || data.metadata || {}, status: src.status || 'done', }; const keys = this.doclingAvailableExports(); this.doclingExportTab = keys.includes('markdown') ? 'rendered' : (keys[0] || 'rendered'); if (typeof this.doclingExports.markdown === 'string') { this.doclingEditContent = this.doclingExports.markdown; } }, doclingAvailableExports() { return Object.keys(this.doclingExports || {}).filter((k) => this.doclingExports[k] != null); }, doclingPreviewText() { let val = this.doclingExports[this.doclingExportTab]; if (val == null) return ''; if (typeof val === 'string') val = val; else if (typeof val === 'object' && val.error) return 'Error: ' + val.error; else val = JSON.stringify(val, null, 2); const q = (this.doclingPreviewSearch || '').trim().toLowerCase(); if (!q || typeof val !== 'string') return val; const lines = val.split('\n'); const hits = lines.filter((l) => l.toLowerCase().includes(q)); return hits.length ? hits.join('\n') : '(geen matches voor "' + q + '")'; }, doclingPreviewHtml() { const val = this.doclingExports.html; return typeof val === 'string' ? val : ''; }, doclingRenderedMarkdown() { const md = this.doclingExports.markdown; if (typeof md !== 'string') return ''; return md .replace(/^### (.*)$/gm, '
') .replace(/^/, '
') .replace(/$/, '
'); }, async copyDoclingExport() { const text = this.doclingExportTab === 'html' ? this.doclingPreviewHtml() : this.doclingPreviewText(); try { await navigator.clipboard.writeText(text); Cockpit.toast('Gekopieerd', 'success'); } catch (e) { Cockpit.toast('Kopiëren mislukt', 'error'); } }, downloadDoclingExport() { const tab = this.doclingExportTab; let content = tab === 'html' ? this.doclingPreviewHtml() : this.doclingPreviewText(); if (!content) return Cockpit.toast('Geen export', 'info'); const ext = tab === 'json' ? 'json' : tab === 'html' ? 'html' : tab === 'yaml' ? 'yaml' : 'md'; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = (this.doclingSelected || 'export').split('/').pop().replace(/\.[^.]+$/, '') + '.' + ext; a.click(); URL.revokeObjectURL(a.href); }, async runDoclingConvert(silent) { if (!this.doclingSelected || this.doclingBusy) return; this.doclingBusy = true; if (!silent) Cockpit.toast('Docling converteert…', 'info'); try { const data = await Cockpit.api('/docling/convert', { method: 'POST', body: JSON.stringify(this.doclingPayload()), }); this.applyDoclingResult(data); await this.loadDoclingHistory(); if (!silent) Cockpit.toast('Klaar · ' + (data.page_count || 0) + ' pagina\'s · ' + (this.doclingTables.length || 0) + ' tabellen', 'success'); } catch (e) { if (!silent) Cockpit.toast(e.message, 'error'); else Cockpit.toast('Convert mislukt: ' + e.message, 'error'); } finally { this.doclingBusy = false; } }, async runDoclingBatch() { if (this.doclingBusy) return; this.doclingBusy = true; try { const data = await Cockpit.api('/docling/batch', { method: 'POST', body: JSON.stringify({ limit: 10, ext: this.doclingExtFilter || null, formats: this.doclingFormats, ...this.doclingOpts, }), }); await this.loadDoclingHistory(); Cockpit.toast((data.converted || 0) + ' / ' + (data.total || 0) + ' geconverteerd', 'success'); if (this.doclingSelected && data.results) { const hit = (data.results || []).find((r) => r.storage_path === this.doclingSelected && r.ok); if (hit) this.applyDoclingResult(hit); } } catch (e) { Cockpit.toast(e.message, 'error'); } finally { this.doclingBusy = false; } }, async loadDoclingCached() { if (!this.doclingSelected) return false; try { const data = await Cockpit.api('/docling/result?path=' + encodeURIComponent(this.doclingSelected)); if (data.ok && data.result) { this.applyDoclingResult(data.result); return true; } } catch (e) {} return false; }, openHistoryItem(item) { if (!item || !item.storage_path) return; this.openDoclingFile(item.storage_path); }, stat360(key) { const stats = this.client360 && this.client360.stats; if (!stats || stats[key] == null) return 0; return stats[key]; }, linkedClientName() { const id = Number(this.linkClientId); if (!id) return ''; const c = (this.linkClients || []).find((row) => Number(row.id) === id); return 'Klant: ' + (c && c.name ? c.name : '—'); }, async initDocChat() { await this.loadLlmProviders(); const p = this.llmProviders.find((row) => Number(row.id) === Number(this.chatLlmProviderId)); this.chatStatus = p ? (p.label + ' · ' + p.model) : 'LLM laden…'; await this.loadLinkClients(); try { const d = await Cockpit.api('/documents/nas-diagnostics'); const vis = d.visible_files || 0; this.chatStatus = vis + ' bestanden zichtbaar · Chroma RAG actief'; if (vis < 30) { this.nasDiagHint = d.hint || 'Weinig bestanden zichtbaar op NAS — controleer Synology rechten voor map CUCINA/Foodlinkk.'; } else { this.nasDiagHint = ''; } } catch (e) { this.chatStatus = 'Diagnostics offline'; } }, async reindexNas() { this.chatReindexing = true; Cockpit.toast('NAS indexeren voor RAG…', 'info'); try { await Cockpit.api('/documents/trigger-scan', { method: 'POST' }); Cockpit.toast('Index scan gestart — Herman kan zo meer documenten zien', 'success'); } catch (e) { Cockpit.toast(e.message, 'error'); } finally { this.chatReindexing = false; } }, async runChatSearch() { const q = (this.chatSearchQ || '').trim(); if (!q) return; try { const data = await Cockpit.api('/documents/search?q=' + encodeURIComponent(q) + '&limit=8'); this.chatSearchHits = data.results || []; } catch (e) { Cockpit.toast('Zoeken mislukt', 'error'); } }, async sendDocChat() { const msg = (this.chatInput || '').trim(); if (!msg || this.chatBusy) return; this.chatBusy = true; this.chatStatus = 'Bezig met denken… (kan ~1–2 min duren op CPU)'; this.chatMessages.push({ role: 'user', content: msg }); const savedInput = this.chatInput; this.chatInput = ''; this.$nextTick(() => { const log = document.getElementById('doc-chat-log'); if (log) log.scrollTop = log.scrollHeight; }); const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 110000); try { const history = this.chatMessages.slice(-8, -1).map((m) => ({ role: m.role, content: m.content })); const payload = { message: msg, use_herman: !!this.chatUseHerman, llm_provider_id: this.chatLlmProviderId ? Number(this.chatLlmProviderId) : null, history, path_prefix: this.chatScopeFile && this.doclingSelected ? this.doclingSelected : '', client_id: this.chatScopeClient && this.linkClientId ? Number(this.linkClientId) : null, project_id: this.linkProjectId ? Number(this.linkProjectId) : null, }; const data = await Cockpit.api('/documents/chat', { method: 'POST', body: JSON.stringify(payload), signal: ctrl.signal, }); if (!data.ok && !data.reply) throw new Error(data.detail || data.error || 'Chat mislukt'); this.chatMessages.push({ role: 'assistant', content: data.reply || '(geen antwoord)', agent: data.agent_label || 'Herman', sources: data.rag_sources || [], }); this.chatStatus = (data.rag_sources && data.rag_sources.length) ? data.rag_sources.length + ' bronnen gebruikt' : 'Antwoord ontvangen'; } catch (e) { const errMsg = e.name === 'AbortError' ? 'Timeout na 3 min — probeer een kortere vraag of zet Herman orchestrator uit' : (e.message || e); this.chatMessages.push({ role: 'assistant', content: 'Fout: ' + errMsg, agent: 'Systeem', sources: [] }); this.chatInput = savedInput; this.chatStatus = 'Fout bij chat'; } finally { clearTimeout(timer); this.chatBusy = false; this.$nextTick(() => { const log = document.getElementById('doc-chat-log'); if (log) log.scrollTop = log.scrollHeight; }); } }, async loadLinkClients() { try { const data = await Cockpit.api('/clients'); this.linkClients = data.items || []; } catch (e) {} }, linkProjectsForClient() { if (!this.linkClientId) return []; return (this.client360.projects || []).length ? this.client360.projects : this.linkProjects.filter((p) => p.client_id === Number(this.linkClientId)); }, async initAnalytics360() { await this.loadLinkClients(); if (this.doclingSelected) this.linkPathInput = this.doclingSelected; if (this.linkClientId) await this.loadClient360(); await this.refreshAutoSyncStatus(); }, async loadClient360() { if (!this.linkClientId) return; try { const data = await Cockpit.api('/clients/' + this.linkClientId + '/360'); if (!data.ok) throw new Error(data.error || '360 laden mislukt'); this.client360 = data; this.linkProjects = data.projects || []; } catch (e) { Cockpit.toast('360: ' + (e.message || e), 'error'); } }, async linkSelectedFile() { const path = (this.linkPathInput || this.doclingSelected || '').trim(); if (!path || !this.linkClientId) return; try { await Cockpit.api('/documents/links', { method: 'POST', body: JSON.stringify({ storage_path: path, client_id: Number(this.linkClientId), project_id: this.linkProjectId ? Number(this.linkProjectId) : null, is_folder: false, }), }); Cockpit.toast('Bestand gekoppeld', 'success'); await this.loadClient360(); } catch (e) { Cockpit.toast(e.message, 'error'); } }, async linkSelectedFolder() { let path = (this.linkPathInput || this.doclingSelected || '').trim(); if (!path || !this.linkClientId) return; if (!path.endsWith('/')) path = path.replace(/\/[^/]+$/, '') || path; try { await Cockpit.api('/documents/links', { method: 'POST', body: JSON.stringify({ storage_path: path, client_id: Number(this.linkClientId), project_id: this.linkProjectId ? Number(this.linkProjectId) : null, is_folder: true, }), }); Cockpit.toast('Map gekoppeld: ' + path, 'success'); await this.loadClient360(); } catch (e) { Cockpit.toast(e.message, 'error'); } }, async unlinkDocument(linkId) { try { await Cockpit.api('/documents/links/' + linkId, { method: 'DELETE' }); await this.loadClient360(); } catch (e) { Cockpit.toast(e.message, 'error'); } }, async runAutoSync(force) { this.autoSyncBusy = true; try { const data = await Cockpit.api('/brain/auto-sync' + (force ? '?force=true' : ''), { method: 'POST' }); const brain = (data.steps || {}).brain_sync || {}; Cockpit.toast('Sync: ' + (brain.synced || 0) + ' docs → second brain', 'success'); await this.refreshAutoSyncStatus(); } catch (e) { Cockpit.toast(e.message, 'error'); } finally { this.autoSyncBusy = false; } }, async refreshAutoSyncStatus() { try { const data = await Cockpit.api('/brain/auto-sync/status'); const last = data.last; if (last && last.created_at) { this.autoSyncLabel = 'Laatste sync: ' + last.created_at.slice(0, 16) + ' (' + last.status + ')'; } } catch (e) {} }, startAutoSyncLoop() { if (this.autoSyncTimer) clearInterval(this.autoSyncTimer); this.autoSyncTimer = setInterval(() => this.runAutoSync(false), 10 * 60 * 1000); }, }; }; window.wordSearch = function wordSearch() { return { query: '', includeStopwords: false, results: topWords.slice(0, 20), loading: false, async search() { this.loading = true; try { const params = new URLSearchParams({ limit: '40', stopwords: this.includeStopwords ? 'true' : 'false' }); if (this.query.trim()) params.set('q', this.query.trim()); const res = await fetch('/api/admin/documents/words?' + params); this.results = (await res.json()).items || []; } catch (e) { Cockpit.toast('Woord zoeken mislukt', 'error'); } finally { this.loading = false; } }, init() { this.search(); }, }; }; function registerDocumentsDash() { if (window.Alpine && window.documentsDash) { Alpine.data('documentsDash', window.documentsDash); } } document.addEventListener('alpine:init', registerDocumentsDash); if (window.Alpine) registerDocumentsDash(); })();