/* 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, nasDiagHint: '', autoSyncBusy: false, autoSyncLabel: 'Second brain: —', autoSyncTimer: null, linkClients: [], linkClientId: '', linkProjectId: '', linkPathInput: '', linkProjects: [], client360: { stats: {}, links: [], sentiment_breakdown: [], monthly_trends: [], top_words: [], stores: [], deals: [] }, init() { this.applyFilters(); this.renderCharts(window.__vizMode || 'neo-bars'); this.loadPhotos(); this.loadLabelVocabulary(); this.loadTaxonomy(); this.refreshShare(false); this.loadLinkClients(); this.startAutoSyncLoop(); document.addEventListener('foodlinkk:vizmode', (ev) => this.renderCharts(ev.detail.mode)); }, 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 (!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 refreshShare(showToast) { try { if (showToast !== false) Cockpit.toast('Share scannen…', 'info'); await fetch('/api/admin/documents/trigger-scan', { method: 'POST' }); const r = await fetch('/api/admin/documents/share-files?limit=300'); const data = await r.json(); if (data.ok) { this.mergeShareItems(data.items || []); if (showToast !== false) Cockpit.toast((data.total || 0) + ' bestanden op share', 'success'); } } catch (e) { if (showToast !== false) Cockpit.toast('Share scan mislukt', 'error'); } }, 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) {} }, openDocLabel(doc) { this.activeDoc = doc; this.docSelectedLabels = [...(doc.user_labels || [])]; this.docLabelsInput = ''; this.docNotesInput = doc.label_notes || ''; }, 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 (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(); return this.doclingFiles.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… (kan ~1 min duren)', 'info'); const r = await fetch('/api/admin/documents/share-files?limit=1000'); 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; 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.openDoclingFile(path); }, async openDoclingFile(path) { if (!path || (this.doclingFileLoading && this.doclingSelected === path)) return; this.doclingSelected = path; this.doclingFileKind = this.fileKind(path); this.doclingEditMode = true; this.doclingFileLoading = true; this.doclingResult = null; this.doclingExports = {}; this.doclingTables = []; this.doclingPictures = []; this.doclingEditContent = ''; 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; 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.loadAllNasFiles(false); } catch (e) { Cockpit.toast(e.message, 'error'); } }, sendToPowerpoint() { this.activeTab = '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() { await this.loadAllNasFiles(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, '

$1

') .replace(/^## (.*)$/gm, '

$1

') .replace(/^# (.*)$/gm, '

$1

') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\n\n/g, '

') .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); }, async initDocChat() { this.chatStatus = 'Ollama RAG (snel)'; 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.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; }); try { const history = this.chatMessages.slice(-8, -1).map((m) => ({ role: m.role, content: m.content })); const payload = { message: msg, use_herman: !!this.chatUseHerman, 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), }); 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 || [], }); } catch (e) { this.chatMessages.push({ role: 'assistant', content: 'Fout: ' + (e.message || e), agent: 'Systeem', sources: [] }); this.chatInput = savedInput; } finally { 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'); this.client360 = data; this.linkProjects = data.projects || []; } catch (e) { Cockpit.toast('360 laden mislukt', '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), 5 * 60 * 1000); setTimeout(() => this.runAutoSync(false), 15000); }, }; }; 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(); }, }; }; document.addEventListener('alpine:init', () => { if (window.Alpine && window.documentsDash) { Alpine.data('documentsDash', window.documentsDash); } }); })();