SysOps: deploy-all — 2026-06-09 10:41 UTC
This commit is contained in:
@@ -2,12 +2,24 @@
|
||||
{% block title %}Documents & Sentiment · Foodlinkk Command Center{% 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" />
|
||||
<style>
|
||||
.doc-neo-shell { --hm-cyan: #00e5ff; --hm-border: rgba(0, 229, 255, 0.12); --hm-panel: #0d1219; }
|
||||
.doc-neo-shell .page-header h1 { font-size: 1.55rem; }
|
||||
.doc-dash-filters { display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center; margin-bottom:1rem; }
|
||||
.doc-chart-panel { height: 140px; }
|
||||
.doc-chart-panel canvas { max-height: 110px !important; }
|
||||
.doc-neo-kpi-row { display:grid; grid-template-columns: repeat(4, 1fr); gap:1rem; margin-bottom:1rem; }
|
||||
@media (max-width: 900px) { .doc-neo-kpi-row { grid-template-columns: 1fr 1fr; } }
|
||||
.doc-neo-kpi {
|
||||
background: linear-gradient(160deg, rgba(13, 18, 25, 0.95), rgba(8, 12, 18, 0.98));
|
||||
border: 1px solid var(--hm-border); border-radius: 14px; padding: 1rem 1.1rem 0.65rem;
|
||||
}
|
||||
.doc-neo-kpi-label { font-size: 0.65rem; letter-spacing: 0.12em; text-transform: uppercase; color: #94a3b8; }
|
||||
.doc-neo-kpi-value { font-size: 1.65rem; font-weight: 700; color: #f1f5f9; margin: 0.25rem 0; }
|
||||
.doc-chart-grid { display:grid; grid-template-columns: 1fr 1fr; gap:1rem; margin-top:1rem; }
|
||||
@media (max-width: 800px) { .doc-chart-grid { grid-template-columns: 1fr; } }
|
||||
.doc-chart-panel .hm-chart-wrap { height: 130px; }
|
||||
.photo-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:1rem; }
|
||||
.photo-card { border:1px solid rgba(148,163,184,.15); border-radius:10px; overflow:hidden; background:rgba(0,0,0,.2); }
|
||||
.photo-card { border:1px solid rgba(0,229,255,0.12); border-radius:12px; overflow:hidden; background:rgba(13,18,25,0.95); }
|
||||
.photo-card img { width:100%; height:140px; object-fit:cover; background:#0a0f1a; }
|
||||
.photo-card-body { padding:0.6rem; font-size:0.78rem; }
|
||||
.detection-wrap { position:relative; display:inline-block; max-width:100%; }
|
||||
@@ -15,17 +27,33 @@
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div x-data="documentsDash()" x-init="init()">
|
||||
<header class="page-header">
|
||||
<h1>Documents & Foto's — interactief dashboard</h1>
|
||||
<p class="subtitle page-tip">Filter NAS documenten, bekijk compacte charts, analyseer foto's uit share/Telegram/browser met OCR + detectie.</p>
|
||||
<div class="doc-neo-shell" x-data="documentsDash()" x-init="init()">
|
||||
<header class="page-header herman-hero" style="margin-bottom:1rem">
|
||||
<h1>Documents & Foto's <span class="live-badge">Neo</span></h1>
|
||||
<p class="hero-sub page-tip">NAS corpus · sentiment · woorden · foto OCR — interactief dashboard</p>
|
||||
</header>
|
||||
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card clickable-kpi" @click="filterSentiment=''"><div class="kpi-label">Documenten</div><div class="kpi-value" x-text="filteredDocs.length"></div></div>
|
||||
<div class="kpi-card purple clickable-kpi"><div class="kpi-label">Totaal woorden</div><div class="kpi-value">{{ summary.total_words }}</div></div>
|
||||
<div class="kpi-card green clickable-kpi"><div class="kpi-label">Foto's in DB</div><div class="kpi-value" x-text="photos.length"></div></div>
|
||||
<div class="kpi-card orange clickable-kpi"><div class="kpi-label">Gem. sentiment</div><div class="kpi-value">{{ summary.avg_sentiment }}</div></div>
|
||||
<div class="doc-neo-kpi-row">
|
||||
<div class="doc-neo-kpi clickable-kpi" @click="filterSentiment=''">
|
||||
<div class="doc-neo-kpi-label">Documenten</div>
|
||||
<div class="doc-neo-kpi-value" x-text="filteredDocs.length"></div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="doc-neo-kpi clickable-kpi">
|
||||
<div class="doc-neo-kpi-label">Totaal woorden</div>
|
||||
<div class="doc-neo-kpi-value">{{ summary.total_words }}</div>
|
||||
<div class="hm-eq hm-eq-purple"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="doc-neo-kpi clickable-kpi">
|
||||
<div class="doc-neo-kpi-label">Foto's in DB</div>
|
||||
<div class="doc-neo-kpi-value" x-text="photos.length"></div>
|
||||
<div class="hm-eq hm-eq-cyan"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="doc-neo-kpi clickable-kpi">
|
||||
<div class="doc-neo-kpi-label">Gem. sentiment</div>
|
||||
<div class="doc-neo-kpi-value">{{ summary.avg_sentiment }}</div>
|
||||
<div class="hm-eq hm-eq-lime"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="doc-dash-filters panel">
|
||||
@@ -46,14 +74,18 @@
|
||||
<button class="btn btn-secondary btn-sm" @click="refreshShare()">↻ Share scan</button>
|
||||
</div>
|
||||
|
||||
<div class="grid-2" style="margin-top:1rem">
|
||||
<section class="panel doc-chart-panel">
|
||||
<h2>Top woorden <small class="muted">klik = filter</small></h2>
|
||||
<canvas id="wordsChart" height="110"></canvas>
|
||||
<div class="doc-chart-grid">
|
||||
<section class="hm-chart-panel doc-chart-panel">
|
||||
<div class="hm-chart-head"><h4>Top woorden</h4><small>klik = filter</small></div>
|
||||
<div class="hm-chart-wrap"><canvas id="wordsChart"></canvas></div>
|
||||
<div id="viz-words-alt" class="viz-alt"></div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</section>
|
||||
<section class="panel doc-chart-panel">
|
||||
<h2>Sentiment <small class="muted">klik = filter</small></h2>
|
||||
<canvas id="sentimentChart" height="110"></canvas>
|
||||
<section class="hm-chart-panel doc-chart-panel">
|
||||
<div class="hm-chart-head"><h4>Sentiment</h4><small>klik = filter</small></div>
|
||||
<div class="hm-chart-wrap"><canvas id="sentimentChart"></canvas></div>
|
||||
<div id="viz-sentiment-alt" class="viz-alt"></div>
|
||||
<div class="hm-eq hm-eq-purple"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -132,8 +164,18 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" x-show="activePhoto" style="margin-top:1rem">
|
||||
<h2>Detectie overlay — foto #<span x-text="activePhoto"></span></h2>
|
||||
<section class="panel" x-show="activePhoto" style="margin-top:1rem">
|
||||
<h2>Detectie overlay — foto #<span x-text="activePhoto"></span></h2>
|
||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.75rem" x-show="activePhoto">
|
||||
<select class="form-input" x-model="linkProjectId" style="max-width:200px">
|
||||
<option value="">Koppel aan project…</option>
|
||||
<template x-for="p in cockpitProjects" :key="p.id">
|
||||
<option :value="p.id" x-text="p.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-primary" @click="savePhotoToProject()">Opslaan in project</button>
|
||||
<a href="/projects" class="btn btn-sm">Alle projecten →</a>
|
||||
</div>
|
||||
<div class="detection-wrap">
|
||||
<img id="detectionImg" :src="activePhoto ? '/api/admin/photos/' + activePhoto + '/image.jpg' : ''" style="max-width:100%;border-radius:8px" @load="drawDetections()" />
|
||||
<canvas id="detectionCanvas"></canvas>
|
||||
@@ -172,14 +214,28 @@ function documentsDash() {
|
||||
activeOcr: '',
|
||||
activeItems: [],
|
||||
detections: [],
|
||||
linkProjectId: '',
|
||||
cockpitProjects: [],
|
||||
liveRefresh: false,
|
||||
liveTimer: null,
|
||||
|
||||
init() {
|
||||
this.applyFilters();
|
||||
this.renderCharts();
|
||||
this.renderCharts(window.__vizMode || 'neo-bars');
|
||||
this.loadPhotos();
|
||||
this.refreshShare(false);
|
||||
fetch('/api/projects?limit=50').then(r=>r.json()).then(d=>{ this.cockpitProjects = d.items || []; }).catch(()=>{});
|
||||
document.addEventListener('foodlinkk:vizmode', (ev) => this.renderCharts(ev.detail.mode));
|
||||
},
|
||||
async savePhotoToProject() {
|
||||
if (!this.activePhoto || !this.linkProjectId) return Cockpit.toast('Kies een project', 'error');
|
||||
try {
|
||||
await Cockpit.api('/api/projects/photos/' + this.activePhoto + '/link', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ project_id: parseInt(this.linkProjectId, 10) }),
|
||||
});
|
||||
Cockpit.toast('Foto gekoppeld aan project', 'success');
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
},
|
||||
|
||||
mergeShareItems(items) {
|
||||
@@ -235,16 +291,44 @@ function documentsDash() {
|
||||
});
|
||||
},
|
||||
|
||||
renderCharts() {
|
||||
if (wordsChartInst) wordsChartInst.destroy();
|
||||
if (sentimentChartInst) sentimentChartInst.destroy();
|
||||
renderCharts(mode) {
|
||||
mode = mode || window.__vizMode || 'neo-bars';
|
||||
if (wordsChartInst) { wordsChartInst.destroy(); wordsChartInst = null; }
|
||||
if (sentimentChartInst) { sentimentChartInst.destroy(); sentimentChartInst = null; }
|
||||
const dash = this;
|
||||
const wordLabels = topWords.slice(0, 10).map(w => w.lemma);
|
||||
const wordValues = topWords.slice(0, 10).map(w => w.total_count);
|
||||
const sentLabels = ['Positief', 'Neutraal', 'Negatief'];
|
||||
const sentValues = [summary.positive, summary.neutral, summary.negative];
|
||||
if (window.VizEngine && (mode === 'neo-equalizer' || mode === 'neo-cards' || mode === 'neo-table')) {
|
||||
VizEngine.renderWidget(mode, {
|
||||
canvas: document.getElementById('wordsChart'),
|
||||
altContainer: document.getElementById('viz-words-alt'),
|
||||
key: 'doc-words',
|
||||
labels: wordLabels, values: wordValues,
|
||||
rows: wordLabels.map((l, i) => ({ label: l, value: wordValues[i], raw: wordValues[i] })),
|
||||
eqClass: 'hm-eq-cyan',
|
||||
});
|
||||
VizEngine.renderWidget(mode, {
|
||||
canvas: document.getElementById('sentimentChart'),
|
||||
altContainer: document.getElementById('viz-sentiment-alt'),
|
||||
key: 'doc-sentiment',
|
||||
labels: sentLabels, values: sentValues,
|
||||
rows: sentLabels.map((l, i) => ({ label: l, value: sentValues[i], raw: sentValues[i] })),
|
||||
eqClass: 'hm-eq-purple',
|
||||
});
|
||||
return;
|
||||
}
|
||||
document.getElementById('viz-words-alt').innerHTML = '';
|
||||
document.getElementById('viz-sentiment-alt').innerHTML = '';
|
||||
document.getElementById('wordsChart').style.display = '';
|
||||
document.getElementById('sentimentChart').style.display = '';
|
||||
wordsChartInst = new Chart(document.getElementById('wordsChart'), {
|
||||
type: 'bar',
|
||||
type: mode === 'neo-rings' ? 'polarArea' : 'bar',
|
||||
data: {
|
||||
labels: topWords.slice(0, 10).map(w => w.lemma),
|
||||
datasets: [{ label: 'Aantal', data: topWords.slice(0, 10).map(w => w.total_count),
|
||||
backgroundColor: 'rgba(232,168,56,.75)', borderWidth: 0 }]
|
||||
backgroundColor: 'rgba(0,229,255,0.75)', borderWidth: 0, borderRadius: 6 }]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
@@ -255,11 +339,11 @@ function documentsDash() {
|
||||
}
|
||||
});
|
||||
sentimentChartInst = new Chart(document.getElementById('sentimentChart'), {
|
||||
type: 'doughnut',
|
||||
type: mode === 'neo-bars' ? 'doughnut' : 'doughnut',
|
||||
data: {
|
||||
labels: ['Positief', 'Neutraal', 'Negatief'],
|
||||
datasets: [{ data: [summary.positive, summary.neutral, summary.negative],
|
||||
backgroundColor: ['#3ecf8e', '#6b7280', '#ef4444'] }]
|
||||
backgroundColor: ['#4ade80', '#64748b', '#fb7185'], borderWidth: 0 }]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
|
||||
Reference in New Issue
Block a user