605 lines
33 KiB
HTML
605 lines
33 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}Documents · NAS Intelligence · Foodlinkk{% endblock %}
|
||
|
|
{% block extra_head %}
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
|
|
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=9" />
|
||
|
|
<link rel="stylesheet" href="/static/css/documents-v2.css?v=10" />
|
||
|
|
<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()" x-cloak>
|
||
|
|
|
||
|
|
<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>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<nav class="doc-nav">
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='overview' && 'active'" @click="activeTab='overview'">Overzicht</button>
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='labels' && 'active'" @click="activeTab='labels'; loadPhotos()">Labels & foto's</button>
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='docling' && 'active'" @click="activeTab='docling'; initDocling()">Workbench</button>
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='powerpoint' && 'active'" @click="activeTab='powerpoint'; initPowerpoint()">PowerPoint</button>
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='chat' && 'active'" @click="activeTab='chat'; initDocChat()">💬 Chat</button>
|
||
|
|
<button type="button" class="doc-nav-btn" :class="activeTab==='analytics360' && 'active'" @click="activeTab='analytics360'; initAnalytics360()">360° Klant</button>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<!-- ═══ OVERVIEW ═══ -->
|
||
|
|
<div x-show="activeTab==='overview'" class="doc-workbench">
|
||
|
|
<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="activeTab='labels'; loadPhotos()">
|
||
|
|
<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="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>Bestand</th><th>Labels</th><th>Sentiment</th><th>Woorden</th></tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<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'"
|
||
|
|
@click="openDocLabel(doc)">
|
||
|
|
<td>
|
||
|
|
<strong x-text="doc.filename"></strong>
|
||
|
|
<span class="path-cell" x-text="doc.storage_path"></span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<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>
|
||
|
|
</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="activeTab='docling'; doclingSelected=activeDoc.storage_path; initDocling()">→ Docling</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══ LABELS & PHOTOS ═══ -->
|
||
|
|
<div x-show="activeTab==='labels'" class="doc-workbench">
|
||
|
|
<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 x-show="activeTab==='docling'" class="doc-workbench dl-workbench">
|
||
|
|
<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" @click="loadAllNasFiles(true)"><span x-text="nasLoading ? '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">NAS <span x-text="'(' + doclingFilteredFiles().length + '/' + nasFileTotal + ')'"></span></p>
|
||
|
|
<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 bestand…" x-model="doclingFilter" />
|
||
|
|
<div class="dl-file-list">
|
||
|
|
<template x-for="f in doclingFilteredFiles()" :key="f.path">
|
||
|
|
<div class="dl-file-item" :class="doclingSelected===f.path && 'selected'" @click="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-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="!doclingFilteredFiles().length" style="font-size:0.72rem">Geen bestanden — klik NAS</p>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<!-- Workspace rechts: editor + preview -->
|
||
|
|
<div class="dl-workspace" :class="!doclingSelected && 'empty'">
|
||
|
|
<div class="dl-workspace-inner" x-show="doclingSelected">
|
||
|
|
<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>
|
||
|
|
</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)" target="_blank">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)" :alt="doclingSelected" />
|
||
|
|
<iframe x-show="doclingFileKind==='pdf' && doclingExportTab==='original'" class="dl-preview-iframe" :src="'/api/admin/documents/nas-file?path=' + encodeURIComponent(doclingSelected)"></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 x-show="activeTab==='powerpoint'" class="doc-workbench ppt-workbench">
|
||
|
|
<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" @click="pptDownloadPath=f.path">
|
||
|
|
<span class="dl-file-name" x-text="f.filename"></span>
|
||
|
|
<a class="btn btn-sm btn-secondary" style="margin-top:0.25rem;display:inline-block" :href="'/api/admin/pptx/download?path=' + encodeURIComponent(f.path)" @click.stop>Download</a>
|
||
|
|
</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 x-show="activeTab==='chat'" class="doc-workbench doc-chat-workbench">
|
||
|
|
<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">
|
||
|
|
<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="'Klant: ' + (linkClients.find(c=>c.id===linkClientId)||{}).name"></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 x-show="activeTab==='analytics360'" class="doc-workbench analytics360-workbench">
|
||
|
|
<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="client360.stats?.documents || 0"></div><div class="l">Documenten</div></div>
|
||
|
|
<div class="dl-stat"><div class="n" x-text="client360.stats?.linked_paths || 0"></div><div class="l">Koppelingen</div></div>
|
||
|
|
<div class="dl-stat"><div class="n" x-text="client360.stats?.projects || 0"></div><div class="l">Projecten</div></div>
|
||
|
|
<div class="dl-stat"><div class="n" x-text="client360.stats?.stores || 0"></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" x-show="photoModalOpen" x-cloak
|
||
|
|
@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="/static/js/documents-v2.js?v=10"></script>
|
||
|
|
{% endblock %}
|