Files

1180 lines
43 KiB
JavaScript
Raw Permalink Normal View History

/* 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, '<h3>$1</h3>')
.replace(/^## (.*)$/gm, '<h2>$1</h2>')
.replace(/^# (.*)$/gm, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n\n/g, '</p><p>')
.replace(/^/, '<p>')
.replace(/$/, '</p>');
},
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);
}
});
})();