667 lines
38 KiB
HTML
667 lines
38 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Documents · NAS Intelligence · Foodlinkk{% endblock %}
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="/static/css/documents-v2.css?v=17" />
|
|
<style>
|
|
.doc-hub > [data-tab-panel] { display: none; min-height: 240px; }
|
|
.doc-hub > [data-tab-panel].tab-active { display: block; }
|
|
</style>
|
|
<script>
|
|
window.__DOC_DATA = {
|
|
topWords: {{ top_words | tojson }},
|
|
summary: {{ summary | tojson }},
|
|
documents: {{ documents | tojson }},
|
|
};
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="doc-hub" x-data="documentsDash" x-init="init()">
|
|
|
|
<header class="page-header herman-hero" style="margin-bottom:1rem">
|
|
<h1>NAS Document Intelligence</h1>
|
|
<p class="hero-sub">Corpus · sentiment · labeling · Docling conversie</p>
|
|
<p class="page-tip" id="doc-hub-status" style="margin-top:0.5rem;color:#fbbf24;display:none"></p>
|
|
</header>
|
|
|
|
<nav class="doc-nav" id="doc-nav">
|
|
<button type="button" data-tab="overview" class="doc-nav-btn active">Overzicht</button>
|
|
<button type="button" data-tab="labels" class="doc-nav-btn">Labels & foto's</button>
|
|
<button type="button" data-tab="docling" class="doc-nav-btn">Workbench</button>
|
|
<button type="button" data-tab="powerpoint" class="doc-nav-btn">PowerPoint</button>
|
|
<button type="button" data-tab="chat" class="doc-nav-btn">Chat</button>
|
|
<button type="button" data-tab="analytics360" class="doc-nav-btn">360° Klant</button>
|
|
</nav>
|
|
|
|
<!-- ═══ OVERVIEW ═══ -->
|
|
<div data-tab-panel="overview" class="doc-workbench doc-panel tab-active">
|
|
<div class="doc-kpi-strip">
|
|
<div class="doc-kpi" @click="filterSentiment=''; applyFilters()">
|
|
<div class="lbl">Documenten</div>
|
|
<div class="val" x-text="filteredDocs.length"></div>
|
|
</div>
|
|
<div class="doc-kpi">
|
|
<div class="lbl">Totaal woorden</div>
|
|
<div class="val">{{ summary.total_words }}</div>
|
|
</div>
|
|
<div class="doc-kpi" @click="switchTab('labels')">
|
|
<div class="lbl">Foto's OCR</div>
|
|
<div class="val" x-text="photos.length"></div>
|
|
</div>
|
|
<div class="doc-kpi">
|
|
<div class="lbl">Gem. sentiment</div>
|
|
<div class="val">{{ summary.avg_sentiment }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="doc-toolbar">
|
|
<input type="search" class="form-input" placeholder="Zoek bestand of label…" x-model="filterQuery" @input="applyFilters()" />
|
|
<select class="form-input" x-model="filterSentiment" @change="applyFilters()">
|
|
<option value="">Alle sentiment</option>
|
|
<option value="positive">Positief</option>
|
|
<option value="neutral">Neutraal</option>
|
|
<option value="negative">Negatief</option>
|
|
</select>
|
|
<select class="form-input" x-model="filterType" @change="applyFilters()">
|
|
<option value="">Alle types</option>
|
|
<template x-for="t in docTypes" :key="t"><option :value="t" x-text="t"></option></template>
|
|
</select>
|
|
<select class="form-input" x-model="filterLabel" @change="applyFilters()">
|
|
<option value="">Alle labels</option>
|
|
<template x-for="lb in labelVocabulary" :key="lb.label">
|
|
<option :value="lb.label" x-text="lb.label + ' (' + lb.usage_count + ')'"></option>
|
|
</template>
|
|
</select>
|
|
<label style="font-size:0.78rem;color:#64748b;display:flex;align-items:center;gap:0.35rem">
|
|
<input type="checkbox" x-model="liveRefresh" @change="toggleLive()" /> Live
|
|
</label>
|
|
<button class="btn btn-secondary btn-sm" @click="openSelectedInWorkbench()" :disabled="!selectedSharePaths.length">Open in Workbench (<span x-text="selectedSharePaths.length"></span>)</button>
|
|
<button class="btn btn-secondary btn-sm" @click="refreshShare()">Share scan</button>
|
|
<button class="btn btn-primary btn-sm" :disabled="nasOcrBusy" @click="batchNasOcr(false)">
|
|
<span x-text="nasOcrBusy ? 'OCR…' : 'NAS OCR'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="doc-split">
|
|
<div class="doc-split-col">
|
|
<div class="doc-section-title" style="font-size:0.65rem;text-transform:uppercase;letter-spacing:0.1em;color:#64748b;margin-bottom:0.5rem">Analytics</div>
|
|
<div class="doc-chart-wrap"><canvas id="wordsChart"></canvas></div>
|
|
<div class="doc-chart-wrap" style="margin-top:0.75rem"><canvas id="sentimentChart"></canvas></div>
|
|
</div>
|
|
<div class="doc-split-col doc-table-wrap">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
|
<strong style="color:#f1f5f9;font-size:0.85rem">NAS documenten</strong>
|
|
<span style="font-size:0.72rem;color:#64748b" x-text="filteredDocs.length + ' resultaten'"></span>
|
|
</div>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th style="width:2rem"></th><th>Bestand</th><th>Gewijzigd</th><th>Labels</th><th>Sentiment</th><th>Woorden</th><th></th></tr>
|
|
</thead>
|
|
<tbody id="doc-table-body">
|
|
{% for doc in documents[:80] %}
|
|
<tr class="clickable-row doc-row-fallback">
|
|
<td></td>
|
|
<td><strong>{{ doc.filename }}</strong><span class="path-cell">{{ doc.storage_path }}</span></td>
|
|
<td class="doc-ts-cell">{{ doc.modified_at or doc.analyzed_at or '—' }}</td>
|
|
<td>{% if doc.user_labels %}{% for lb in doc.user_labels %}<span class="doc-chip">{{ lb }}</span>{% endfor %}{% else %}<span class="doc-chip muted">—</span>{% endif %}</td>
|
|
<td><span class="badge badge-{{ doc.sentiment_label or 'neutral' }}">{{ doc.sentiment_label or '—' }}</span></td>
|
|
<td>{{ doc.word_count or 0 }}</td>
|
|
<td></td>
|
|
</tr>
|
|
{% endfor %}
|
|
<template x-for="doc in filteredDocs" :key="doc.storage_path">
|
|
<tr class="clickable-row" :class="activeDoc && activeDoc.storage_path === doc.storage_path && 'doc-row-selected'">
|
|
<td @click.stop>
|
|
<input type="checkbox" :checked="isShareSelected(doc.storage_path)" @change="toggleShareSelect(doc.storage_path)" />
|
|
</td>
|
|
<td @click="openDocLabel(doc)">
|
|
<strong x-text="doc.filename"></strong>
|
|
<span class="path-cell" x-text="doc.storage_path"></span>
|
|
</td>
|
|
<td class="doc-ts-cell" x-text="formatTs(doc.modified_at || doc.analyzed_at)"></td>
|
|
<td @click="openDocLabel(doc)">
|
|
<template x-for="lb in (doc.user_labels || [])" :key="lb">
|
|
<span class="doc-chip" x-text="lb"></span>
|
|
</template>
|
|
<span class="doc-chip muted" x-show="!(doc.user_labels || []).length">—</span>
|
|
</td>
|
|
<td><span class="badge" :class="'badge-' + (doc.sentiment_label || 'neutral')" x-text="doc.sentiment_label || '—'"></span></td>
|
|
<td x-text="doc.word_count"></td>
|
|
<td @click.stop>
|
|
<button type="button" class="btn btn-sm btn-secondary" @click="openInWorkbench(doc.storage_path)">Bewerk</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="doc-label-drawer" x-show="activeDoc">
|
|
<h3>Label document — <span x-text="activeDoc && activeDoc.filename"></span></h3>
|
|
<div class="label-modal-grid">
|
|
<template x-for="t in documentTaxonomy" :key="t.id">
|
|
<button type="button" class="label-pick" :class="docSelectedLabels.includes(t.id) && 'selected'"
|
|
:style="'--pick-color:' + (t.color || '#00e5ff')" @click="toggleDocLabel(t.id)">
|
|
<span class="label-pick-emoji" x-text="t.emoji"></span>
|
|
<span class="label-pick-name" x-text="t.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<div class="label-modal-extra">
|
|
<div><label>Extra labels</label><input type="text" class="form-input" x-model="docLabelsInput" placeholder="komma-gescheiden" /></div>
|
|
<div><label>Notities</label><textarea class="form-input" rows="2" x-model="docNotesInput"></textarea></div>
|
|
</div>
|
|
<div class="label-modal-actions" style="border:none;padding-top:0.75rem">
|
|
<button class="btn btn-secondary btn-sm" @click="activeDoc=null">Sluiten</button>
|
|
<button class="btn btn-primary btn-sm" @click="saveDocLabels()">Opslaan</button>
|
|
<button class="btn btn-secondary btn-sm" @click="doclingSelected=activeDoc.storage_path; switchTab('docling')">→ Docling</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ LABELS & PHOTOS ═══ -->
|
|
<div data-tab-panel="labels" class="doc-workbench doc-panel">
|
|
<div class="doc-wb-head">
|
|
<div>
|
|
<h2>Foto labeling studio</h2>
|
|
<p>Klik een foto → kies label-type → opslaan</p>
|
|
</div>
|
|
<div style="display:flex;gap:0.45rem;flex-wrap:wrap">
|
|
<input type="file" accept="image/*" @change="uploadPhoto($event)" class="form-input" style="max-width:200px" />
|
|
<button class="btn btn-secondary btn-sm" @click="loadPhotos()">Vernieuwen</button>
|
|
<button class="btn btn-primary btn-sm" :disabled="nasOcrBusy" @click="batchNasOcr(false)">NAS OCR</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="label-filter-row">
|
|
<button type="button" class="label-filter-chip" :class="!photoFilterLabel && 'active'" @click="photoFilterLabel=''">Alle</button>
|
|
<template x-for="t in photoTaxonomy" :key="'f'+t.id">
|
|
<button type="button" class="label-filter-chip" :class="photoFilterLabel===t.id && 'active'"
|
|
@click="photoFilterLabel=t.id" x-text="t.emoji + ' ' + t.label"></button>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="photo-masonry">
|
|
<template x-for="p in filteredPhotos()" :key="p.id">
|
|
<article class="photo-tile" @click="openPhotoModal(p.id)">
|
|
<img :src="'/api/admin/photos/' + p.id + '/image.jpg'" :alt="p.filename || 'foto'" loading="lazy" />
|
|
<div class="photo-tile-body">
|
|
<div class="photo-tile-head">
|
|
<span class="photo-tile-id" x-text="'#' + p.id"></span>
|
|
<span class="photo-tile-src" x-text="p.source || 'upload'"></span>
|
|
</div>
|
|
<div class="photo-tile-labels">
|
|
<template x-for="lb in (p.user_labels || [])" :key="lb">
|
|
<span class="doc-chip" x-text="labelDisplayName(lb)"></span>
|
|
</template>
|
|
<span class="doc-chip muted" x-show="!(p.user_labels || []).length">Nog geen label</span>
|
|
</div>
|
|
<p class="photo-tile-ocr" x-text="p.ocr_preview || 'Geen OCR tekst'"></p>
|
|
<span class="photo-tile-meta" x-text="(p.box_count||0) + ' detecties · ' + (p.item_count||0) + ' items'"></span>
|
|
</div>
|
|
</article>
|
|
</template>
|
|
<p class="empty-state" x-show="!filteredPhotos().length" style="grid-column:1/-1;padding:2rem">Geen foto's — upload, Telegram of NAS OCR.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ DOCLING STUDIO ═══ -->
|
|
<div data-tab-panel="docling" class="doc-workbench dl-workbench doc-panel">
|
|
<div class="doc-wb-head">
|
|
<div>
|
|
<h2>Document Workbench</h2>
|
|
<p x-text="doclingStatus"></p>
|
|
<div class="dl-progress" x-show="doclingBusy"><div class="dl-progress-bar"></div></div>
|
|
</div>
|
|
<div class="dl-head-actions">
|
|
<label class="dl-toggle dl-auto-toggle"><input type="checkbox" x-model="doclingAutoConvert" /> Auto-convert bij klik</label>
|
|
<button class="btn btn-secondary btn-sm" :disabled="nasLoading || shareBrowseLoading" @click="loadAllNasFiles(true); browseShare(shareBrowsePath || '', true)"><span x-text="(nasLoading || shareBrowseLoading) ? 'Scannen…' : '↻ NAS'"></span></button>
|
|
<button class="btn btn-secondary btn-sm" @click="dlOptionsOpen=!dlOptionsOpen" x-text="dlOptionsOpen ? 'Pipeline ▲' : 'Pipeline ▼'"></button>
|
|
<button class="btn btn-primary btn-sm" :disabled="doclingBusy || !doclingSelected" @click="runDoclingConvert()">↻ Convert</button>
|
|
<button class="btn btn-secondary btn-sm" :disabled="doclingBusy" @click="runDoclingBatch()">Batch</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dl-pipeline-panel" x-show="dlOptionsOpen">
|
|
<div class="dl-pipeline-grid">
|
|
<div>
|
|
<p class="dl-section-title">Pipeline</p>
|
|
<div class="dl-toggle-grid">
|
|
<label class="dl-toggle"><input type="checkbox" x-model="doclingOpts.ocr" /> OCR</label>
|
|
<label class="dl-toggle"><input type="checkbox" x-model="doclingOpts.tables" /> Tabellen</label>
|
|
<label class="dl-toggle"><input type="checkbox" x-model="doclingOpts.page_images" /> Pagina's</label>
|
|
<label class="dl-toggle"><input type="checkbox" x-model="doclingOpts.picture_images" /> Figuren</label>
|
|
<label class="dl-toggle"><input type="checkbox" x-model="doclingOpts.force_full_page_ocr" /> Full-page OCR</label>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="dl-section-title">Export</p>
|
|
<div class="dl-format-grid">
|
|
<template x-for="fmt in doclingExportFormats" :key="fmt">
|
|
<label class="dl-toggle" style="padding:0.35rem 0.5rem">
|
|
<input type="checkbox" :value="fmt" x-model="doclingFormats" />
|
|
<span x-text="fmt"></span>
|
|
</label>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="dl-stats-grid" x-show="doclingResult">
|
|
<div class="dl-stat"><div class="n" x-text="doclingMeta.page_count || 0"></div><div class="l">Pagina's</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="doclingTables.length"></div><div class="l">Tabellen</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="doclingPictures.length"></div><div class="l">Figuren</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="doclingAvailableExports().length"></div><div class="l">Exports</div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dl-body-v3">
|
|
<!-- Bestanden links -->
|
|
<aside class="dl-sidebar">
|
|
<p class="dl-section-title">Share <span x-text="'(' + shareBrowsePath + ')'"></span></p>
|
|
<nav class="share-breadcrumb" aria-label="Share pad">
|
|
<button type="button" class="share-crumb" @click="browseShare('')">Share</button>
|
|
<template x-for="(part, idx) in sharePathParts()" :key="'bc'+idx">
|
|
<span class="share-crumb-sep">/</span>
|
|
<button type="button" class="share-crumb" @click="browseShare(sharePathPrefix(idx))" x-text="part"></button>
|
|
</template>
|
|
</nav>
|
|
<div class="dl-ext-chips">
|
|
<template x-for="g in dlExtGroups" :key="g.ext || 'all'">
|
|
<button type="button" class="dl-ext-chip" :class="doclingExtFilter===g.ext && 'active'" @click="doclingExtFilter=g.ext" x-text="(g.icon ? g.icon + ' ' : '') + g.label"></button>
|
|
</template>
|
|
</div>
|
|
<input type="search" class="form-input dl-search" placeholder="Zoek in map…" x-model="doclingFilter" />
|
|
<div class="dl-file-list share-browser-list">
|
|
<div class="dl-file-item share-folder-item" x-show="shareBrowsePath" @click="browseShareParent()">
|
|
<span class="dl-file-icon">📁</span>
|
|
<span class="dl-file-name">.. (omhoog)</span>
|
|
</div>
|
|
<template x-for="entry in shareBrowseFolders()" :key="'d'+entry.path">
|
|
<div class="dl-file-item share-folder-item" @click="browseShare(entry.path)">
|
|
<span class="dl-file-icon">📁</span>
|
|
<div class="dl-file-meta">
|
|
<span class="dl-file-name" x-text="entry.name"></span>
|
|
<span class="dl-file-path" x-text="entry.path"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template x-for="f in doclingFilteredFiles()" :key="f.path">
|
|
<div class="dl-file-item" :class="doclingSelected===f.path && 'selected'" @click.prevent="selectDoclingFile(f.path)">
|
|
<div class="dl-file-row">
|
|
<span class="dl-file-icon" x-text="fileIcon(f.path)"></span>
|
|
<div class="dl-file-meta">
|
|
<span class="dl-file-name" x-text="f.filename || f.path"></span>
|
|
<span class="dl-file-path" x-text="f.path"></span>
|
|
</div>
|
|
<span class="dl-file-ext" x-text="fileExtension(f.path)"></span>
|
|
</div>
|
|
<div class="dl-file-tags">
|
|
<span class="dl-file-ts" x-text="formatTs(f.modified_at)"></span>
|
|
<span class="dl-file-badge" x-show="fileDoclingStatus(f.path)==='done'">✓</span>
|
|
<span class="dl-file-badge pending" x-show="fileDoclingStatus(f.path)==='running'">…</span>
|
|
<span class="dl-file-badge failed" x-show="fileDoclingStatus(f.path)==='failed'">✕</span>
|
|
<span class="dl-file-badge convert" x-show="doclingSelected===f.path && doclingFileLoading">laden</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<p class="empty-state" x-show="!shareBrowseFolders().length && !doclingFilteredFiles().length" style="font-size:0.72rem">Geen items — klik ↻ NAS of controleer Synology rechten.</p>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Workspace rechts: editor + preview -->
|
|
<div class="dl-workspace" :class="!doclingSelected && 'empty'">
|
|
<div class="dl-workspace-inner" x-show="doclingSelected" style="display:flex;flex-direction:column;flex:1">
|
|
<div class="dl-ws-head">
|
|
<div class="dl-ws-title">
|
|
<span class="dl-ws-icon" x-text="fileIcon(doclingSelected)"></span>
|
|
<div>
|
|
<strong x-text="doclingSelected.split('/').pop()"></strong>
|
|
<span class="dl-ws-path" x-text="doclingSelected"></span>
|
|
</div>
|
|
<span class="dl-ws-kind" x-text="fileKindLabel(doclingSelected)"></span>
|
|
<span class="dl-ws-ts" x-show="doclingFileMtime" x-text="'NAS: ' + formatTs(doclingFileMtime)"></span>
|
|
</div>
|
|
<div class="dl-version-bar" x-show="docVersions.length">
|
|
<label style="font-size:0.72rem;color:#94a3b8">Versie</label>
|
|
<select class="form-input" style="max-width:280px;font-size:0.72rem" @change="loadVersion($event.target.value)">
|
|
<option value="">Huidige bewerking</option>
|
|
<template x-for="v in docVersions" :key="v.id">
|
|
<option :value="v.id" x-text="formatTs(v.created_at) + (v.version_label ? ' · ' + v.version_label : '')"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="dl-preview-tools">
|
|
<button class="btn btn-sm btn-primary" :disabled="!doclingEditContent" @click="saveWorkspace()">Opslaan</button>
|
|
<button class="btn btn-sm btn-secondary" :disabled="!doclingEditContent" @click="saveToNas()">→ NAS</button>
|
|
<button class="btn btn-sm btn-secondary" @click="sendToPowerpoint()" x-show="doclingEditContent">→ PPT</button>
|
|
<button class="btn btn-sm btn-secondary" :disabled="!doclingResult" @click="copyDoclingExport()">Kopieer</button>
|
|
<button class="btn btn-sm btn-secondary" :disabled="!doclingResult" @click="downloadDoclingExport()">Download</button>
|
|
<a class="btn btn-sm btn-secondary" :href="'/api/admin/documents/nas-file?path=' + encodeURIComponent(doclingSelected) + '&inline=1'" target="_blank" rel="noopener">Open origineel</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dl-loading-bar" x-show="doclingFileLoading || doclingBusy"><div class="dl-progress-bar"></div></div>
|
|
|
|
<div class="dl-workspace-panes">
|
|
<!-- Editor -->
|
|
<div class="dl-pane dl-pane-editor">
|
|
<div class="dl-pane-label">Editor <span x-text="doclingFileKind === 'text' ? '(raw)' : '(markdown)'"></span></div>
|
|
<textarea class="dl-editor dl-editor-full" x-model="doclingEditContent" :placeholder="doclingFileLoading ? 'Laden…' : 'Klik een bestand links — inhoud verschijnt hier…'" :disabled="doclingFileLoading"></textarea>
|
|
</div>
|
|
<!-- Preview -->
|
|
<div class="dl-pane dl-pane-preview">
|
|
<div class="dl-pane-label">
|
|
Preview
|
|
<div class="dl-export-tabs dl-export-tabs-inline" x-show="doclingResult">
|
|
<template x-for="fmt in doclingAvailableExports()" :key="'t'+fmt">
|
|
<button type="button" class="dl-tab" :class="doclingExportTab===fmt && 'active'" @click="doclingExportTab=fmt" x-text="fmt"></button>
|
|
</template>
|
|
<button type="button" class="dl-tab" :class="doclingExportTab==='rendered' && 'active'" @click="doclingExportTab='rendered'" x-show="doclingExports.markdown">md</button>
|
|
<button type="button" class="dl-tab" :class="doclingExportTab==='original' && 'active'" @click="doclingExportTab='original'">orig</button>
|
|
</div>
|
|
</div>
|
|
<div class="dl-preview-viewport">
|
|
<img x-show="doclingFileKind==='image'" class="dl-preview-img" :src="'/api/admin/documents/nas-file?path=' + encodeURIComponent(doclingSelected) + '&inline=1'" :alt="doclingSelected" />
|
|
<iframe x-show="doclingFileKind==='pdf' && doclingExportTab==='original'" class="dl-preview-iframe" :src="'/api/admin/documents/nas-file?path=' + encodeURIComponent(doclingSelected) + '&inline=1'"></iframe>
|
|
<div x-show="doclingExportTab==='html' && doclingPreviewHtml()" class="dl-preview-box rendered" x-html="doclingPreviewHtml()"></div>
|
|
<div x-show="doclingExportTab==='rendered' && doclingExports.markdown" class="dl-preview-box rendered" x-html="doclingRenderedMarkdown()"></div>
|
|
<pre class="dl-preview-box" x-show="doclingResult && doclingExportTab!=='html' && doclingExportTab!=='rendered' && doclingExportTab!=='original' && doclingExportTab!=='tables' && doclingExportTab!=='pictures'" x-text="doclingPreviewText()"></pre>
|
|
<p class="empty-state" x-show="!doclingResult && !doclingFileLoading && doclingFileKind!=='image' && doclingFileKind!=='pdf'" style="padding:2rem">Converteert… of geen preview beschikbaar.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="dl-workspace-empty" x-show="!doclingSelected">
|
|
<p class="empty-state" style="padding:3rem 1rem;text-align:center">
|
|
<span style="font-size:2rem;display:block;margin-bottom:0.5rem">📂</span>
|
|
Klik een bestand links om direct te bewerken.<br/>
|
|
<span style="font-size:0.78rem;color:#64748b">PDF · Word · Excel · PowerPoint · tekst · afbeeldingen — auto-convert aan</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ POWERPOINT STUDIO ═══ -->
|
|
<div data-tab-panel="powerpoint" class="doc-workbench ppt-workbench doc-panel">
|
|
<div class="doc-wb-head">
|
|
<div>
|
|
<h2>PowerPoint Studio</h2>
|
|
<p>Maak presentaties van NAS PDF's of bewerkte Docling-markdown</p>
|
|
</div>
|
|
<div style="display:flex;gap:0.45rem;flex-wrap:wrap">
|
|
<button class="btn btn-secondary btn-sm" @click="loadAllNasFiles(false)">↻ Bestanden</button>
|
|
<button class="btn btn-primary btn-sm" :disabled="pptBusy || !pptSlides.length" @click="createPptx()">Genereer PPTX</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ppt-grid">
|
|
<aside class="ppt-sidebar">
|
|
<p class="dl-section-title">Bron PDF (NAS)</p>
|
|
<div class="dl-file-list">
|
|
<template x-for="f in pptPdfFiles()" :key="'pdf'+f.path">
|
|
<div class="dl-file-item" :class="pptSourcePdf===f.path && 'selected'" @click="pptSourcePdf=f.path">
|
|
<span class="dl-file-name" x-text="f.filename"></span>
|
|
<span class="dl-file-path" x-text="f.path"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<button class="btn btn-sm btn-secondary" style="width:100%;margin-top:0.5rem" :disabled="!pptSourcePdf || pptBusy" @click="convertPdfToPptx()">PDF → PPTX (smart)</button>
|
|
<p class="dl-section-title" style="margin-top:1rem">Bestaande PPTX</p>
|
|
<div class="dl-file-list">
|
|
<template x-for="f in pptPptxFiles()" :key="'ppt'+f.path">
|
|
<div class="dl-file-item" :class="pptDownloadPath===f.path && 'selected'" @click.prevent="openInWorkbench(f.path)">
|
|
<span class="dl-file-name" x-text="f.filename"></span>
|
|
<span class="dl-file-path" x-text="f.path"></span>
|
|
<button type="button" class="btn btn-sm btn-secondary" style="margin-top:0.25rem" @click.stop="openInWorkbench(f.path)">Open in Workbench</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="ppt-editor">
|
|
<div class="ppt-meta-row">
|
|
<input class="form-input" placeholder="Presentatie titel" x-model="pptTitle" />
|
|
<input class="form-input" placeholder="Subtitel" x-model="pptSubtitle" />
|
|
<input class="form-input" placeholder="Bestandsnaam (optioneel)" x-model="pptFilename" />
|
|
</div>
|
|
<div style="display:flex;gap:0.45rem;margin-bottom:0.75rem;flex-wrap:wrap">
|
|
<button class="btn btn-sm btn-secondary" @click="importMarkdownToSlides()">Importeer Docling markdown</button>
|
|
<button class="btn btn-sm btn-secondary" @click="addPptSlide()">+ Slide</button>
|
|
</div>
|
|
<div class="ppt-slides">
|
|
<template x-for="(slide, idx) in pptSlides" :key="'slide'+idx">
|
|
<div class="ppt-slide-card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
|
<strong style="font-size:0.78rem;color:#94a3b8">Slide <span x-text="idx+1"></span></strong>
|
|
<button class="btn btn-sm btn-secondary" @click="removePptSlide(idx)">✕</button>
|
|
</div>
|
|
<input class="form-input" placeholder="Slide titel" x-model="slide.title" style="margin-bottom:0.35rem" />
|
|
<textarea class="form-input" rows="4" placeholder="Bullets (één per regel)" x-model="slide.bulletsText" @input="syncSlideBullets(slide)"></textarea>
|
|
</div>
|
|
</template>
|
|
<p class="empty-state" x-show="!pptSlides.length">Voeg slides toe of importeer uit Docling.</p>
|
|
</div>
|
|
<p class="page-tip" x-show="pptLastOutput" style="margin-top:0.75rem">Laatste export: <span x-text="pptLastOutput"></span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ DOCUMENT CHAT (RAG) ═══ -->
|
|
<div data-tab-panel="chat" class="doc-workbench doc-chat-workbench doc-panel">
|
|
<div class="doc-wb-head">
|
|
<div>
|
|
<h2>Chat met je data</h2>
|
|
<p>Herman + RAG over NAS-documenten · <span x-text="chatStatus"></span></p>
|
|
<p class="page-tip" x-show="nasDiagHint" x-text="nasDiagHint" style="margin-top:0.35rem;color:#fbbf24"></p>
|
|
</div>
|
|
<div style="display:flex;gap:0.45rem;flex-wrap:wrap;align-items:center">
|
|
<select class="form-input" style="min-width:180px;font-size:0.75rem" x-model.number="chatLlmProviderId" @change="onChatProviderChange()">
|
|
<option value="">Standaard LLM</option>
|
|
<template x-for="p in llmProviders" :key="'llm'+p.id">
|
|
<option :value="p.id" x-text="p.label + ' · ' + p.model + (p.is_default ? ' ★' : '')"></option>
|
|
</template>
|
|
</select>
|
|
<a class="btn btn-sm btn-secondary" href="/settings?tab=ai" target="_blank">+ LLM API</a>
|
|
<button class="btn btn-secondary btn-sm" @click="reindexNas()" :disabled="chatReindexing"><span x-text="chatReindexing ? 'Indexeren…' : '↻ Index NAS'"></span></button>
|
|
<label class="dl-toggle dl-auto-toggle"><input type="checkbox" x-model="chatUseHerman" /> Herman orchestrator (langzamer)</label>
|
|
<button class="btn btn-secondary btn-sm" @click="runAutoSync()" :disabled="autoSyncBusy"><span x-text="autoSyncBusy ? 'Sync…' : '⟳ Second Brain'"></span></button>
|
|
<label class="dl-toggle dl-auto-toggle"><input type="checkbox" x-model="chatScopeFile" /> Scope: bestand</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="doc-chat-layout">
|
|
<div class="doc-chat-messages" id="doc-chat-log">
|
|
<template x-for="(m, i) in chatMessages" :key="'cm'+i">
|
|
<div class="doc-chat-msg" :class="m.role">
|
|
<span class="doc-chat-role" x-text="m.role === 'user' ? 'Jij' : (m.agent || 'Herman')"></span>
|
|
<p x-text="m.content"></p>
|
|
<div class="doc-chat-sources" x-show="m.sources && m.sources.length">
|
|
<span style="font-size:0.62rem;color:#64748b">Bronnen:</span>
|
|
<template x-for="s in m.sources" :key="s">
|
|
<a class="doc-chat-src" :href="'/api/admin/documents/nas-file?path=' + encodeURIComponent(s)" target="_blank" x-text="s.split('/').pop()"></a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<p class="empty-state" x-show="!chatMessages.length" style="padding:2rem;text-align:center">
|
|
Stel een vraag over je documenten.<br/>
|
|
<span style="font-size:0.78rem;color:#64748b">bv. "Wat staat er in de Cucina catalogue?" of "Samenvat halal certificaten"</span>
|
|
</p>
|
|
</div>
|
|
|
|
<aside class="doc-chat-side">
|
|
<p class="dl-section-title">Snel zoeken</p>
|
|
<input class="form-input dl-search" placeholder="Zoek in corpus…" x-model="chatSearchQ" @keydown.enter="runChatSearch()" />
|
|
<button class="btn btn-sm btn-secondary" style="width:100%;margin-bottom:0.75rem" @click="runChatSearch()">Zoek</button>
|
|
<div class="doc-chat-hits">
|
|
<template x-for="h in chatSearchHits" :key="h.id || h.path">
|
|
<div class="doc-chat-hit" @click="chatInput = 'Wat staat er in ' + (h.filename || h.path) + '?'">
|
|
<strong x-text="h.filename || h.path"></strong>
|
|
<p x-text="(h.text || '').slice(0, 120) + '…'"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<p class="dl-section-title" style="margin-top:1rem">Scope</p>
|
|
<p class="page-tip" x-show="chatScopeClient && linkClientId" x-text="linkedClientName()"></p>
|
|
<p class="page-tip" x-show="chatScopeFile && doclingSelected" x-text="'Bestand: ' + doclingSelected"></p>
|
|
</aside>
|
|
</div>
|
|
|
|
<form class="doc-chat-input" @submit.prevent="sendDocChat()">
|
|
<input class="form-input" x-model="chatInput" placeholder="Vraag aan Herman over je documenten…" :disabled="chatBusy" autocomplete="off" />
|
|
<button class="btn btn-primary" type="submit" :disabled="chatBusy || !chatInput.trim()"><span x-text="chatBusy ? '…' : 'Stuur'"></span></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- ═══ 360° KLANT ANALYTICS ═══ -->
|
|
<div data-tab-panel="analytics360" class="doc-workbench analytics360-workbench doc-panel">
|
|
<div class="doc-wb-head">
|
|
<div>
|
|
<h2>360° Klant Intelligence</h2>
|
|
<p>Koppel NAS → klant/project · trends · second brain · <span x-text="autoSyncLabel"></span></p>
|
|
</div>
|
|
<div style="display:flex;gap:0.45rem;flex-wrap:wrap">
|
|
<button class="btn btn-secondary btn-sm" @click="runAutoSync(true)" :disabled="autoSyncBusy">⟳ Sync alles</button>
|
|
<button class="btn btn-primary btn-sm" @click="linkSelectedFile()" :disabled="!doclingSelected || !linkClientId">+ Koppel bestand</button>
|
|
<button class="btn btn-secondary btn-sm" @click="linkSelectedFolder()" :disabled="!doclingSelected || !linkClientId">+ Koppel map</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a360-toolbar">
|
|
<select class="form-input" x-model.number="linkClientId" @change="loadClient360()">
|
|
<option value="">— Klant —</option>
|
|
<template x-for="c in linkClients" :key="'lc'+c.id">
|
|
<option :value="c.id" x-text="c.name"></option>
|
|
</template>
|
|
</select>
|
|
<select class="form-input" x-model.number="linkProjectId">
|
|
<option value="">— Project (optioneel) —</option>
|
|
<template x-for="p in linkProjectsForClient()" :key="'lp'+p.id">
|
|
<option :value="p.id" x-text="p.name"></option>
|
|
</template>
|
|
</select>
|
|
<input class="form-input" placeholder="NAS pad (of selecteer in Workbench)" x-model="linkPathInput" />
|
|
</div>
|
|
|
|
<div class="a360-grid" x-show="linkClientId">
|
|
<div class="a360-kpis">
|
|
<div class="dl-stat"><div class="n" x-text="stat360('documents')"></div><div class="l">Documenten</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="stat360('linked_paths')"></div><div class="l">Koppelingen</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="stat360('projects')"></div><div class="l">Projecten</div></div>
|
|
<div class="dl-stat"><div class="n" x-text="stat360('stores')"></div><div class="l">Winkels</div></div>
|
|
</div>
|
|
|
|
<div class="a360-panel">
|
|
<p class="dl-section-title">Gekoppelde paden</p>
|
|
<div class="a360-links">
|
|
<template x-for="l in client360.links || []" :key="'lnk'+l.id">
|
|
<div class="a360-link-row">
|
|
<span x-text="(l.is_folder ? '📁 ' : '📄 ') + l.storage_path"></span>
|
|
<button class="btn btn-sm btn-secondary" @click="unlinkDocument(l.id)">✕</button>
|
|
</div>
|
|
</template>
|
|
<p class="empty-state" x-show="!(client360.links||[]).length">Nog geen koppelingen — selecteer bestand in Workbench</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a360-panel">
|
|
<p class="dl-section-title">Sentiment & trends</p>
|
|
<div class="a360-sentiment">
|
|
<template x-for="s in client360.sentiment_breakdown || []" :key="s.sentiment_label">
|
|
<span class="a360-pill" x-text="s.sentiment_label + ': ' + s.n"></span>
|
|
</template>
|
|
</div>
|
|
<div class="a360-trends">
|
|
<template x-for="t in client360.monthly_trends || []" :key="t.month">
|
|
<div class="a360-trend-row">
|
|
<span x-text="(t.month||'').slice(0,7)"></span>
|
|
<span x-text="t.docs + ' docs'"></span>
|
|
<span x-text="'sentiment ' + (t.avg_sentiment||0).toFixed(2)"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a360-panel">
|
|
<p class="dl-section-title">Top woorden (klant corpus)</p>
|
|
<div class="a360-words">
|
|
<template x-for="w in client360.top_words || []" :key="w.lemma">
|
|
<span class="a360-word" x-text="w.lemma + ' (' + w.total + ')'"></span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a360-panel">
|
|
<p class="dl-section-title">Retail & deals</p>
|
|
<template x-for="s in client360.stores || []" :key="'st'+s.id">
|
|
<p class="page-tip" x-text="s.chain + ' · ' + s.name + ' · ' + (s.city||'')"></p>
|
|
</template>
|
|
<template x-for="d in client360.deals || []" :key="'dl'+d.id">
|
|
<p class="page-tip" x-text="d.title + ' · €' + d.value + ' · ' + d.stage"></p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="empty-state" x-show="!linkClientId" style="padding:2rem">Selecteer een klant om de 360° view te laden</p>
|
|
</div>
|
|
|
|
<!-- ═══ PHOTO LABEL MODAL ═══ -->
|
|
<div class="label-modal-backdrop" :class="photoModalOpen && 'open'"
|
|
@click.self="closePhotoModal()" @keydown.escape.window="closePhotoModal()">
|
|
<div class="label-modal" @click.stop role="dialog" aria-labelledby="labelModalTitle">
|
|
<div class="label-modal-head" style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
<div>
|
|
<h3 id="labelModalTitle">Wat voor label is deze foto?</h3>
|
|
<p class="modal-sub">Foto #<span x-text="activePhoto"></span> — selecteer één of meer types</p>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-secondary" @click="closePhotoModal()" aria-label="Sluiten">✕</button>
|
|
</div>
|
|
|
|
<div class="label-modal-preview" x-show="activePhoto">
|
|
<img :src="activePhoto ? '/api/admin/photos/' + activePhoto + '/image.jpg' : ''" alt="Preview" />
|
|
<div>
|
|
<div class="modal-ocr-label">OCR tekst</div>
|
|
<pre class="modal-ocr-box" x-text="activeOcr || 'Geen tekst gedetecteerd'"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="label-modal-grid">
|
|
<template x-for="t in photoTaxonomy" :key="t.id">
|
|
<button type="button" class="label-pick" :class="modalPhotoLabels.includes(t.id) && 'selected'"
|
|
:style="'--pick-color:' + (t.color || '#00e5ff')" @click="toggleModalLabel(t.id)">
|
|
<span class="label-pick-emoji" x-text="t.emoji"></span>
|
|
<span class="label-pick-name" x-text="t.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="label-modal-extra">
|
|
<div>
|
|
<label>Extra label (optioneel)</label>
|
|
<input type="text" class="form-input" x-model="photoLabelsInput" placeholder="bv. Q2-2026, Jumbo" />
|
|
</div>
|
|
<div>
|
|
<label>Notities</label>
|
|
<textarea class="form-input" rows="2" x-model="photoNotesInput" placeholder="Toelichting…"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="label-modal-actions">
|
|
<button type="button" class="btn btn-secondary" @click="closePhotoModal()">Annuleren</button>
|
|
<button type="button" class="btn btn-primary" @click="saveModalPhotoLabels()">Label opslaan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
<script src="/static/js/doc-tabs.js?v=17"></script>
|
|
<script src="/static/js/documents-v2.js?v=17"></script>
|
|
{% endblock %}
|