optimize ui

add new ki endpoint params
This commit is contained in:
team2
2026-02-17 20:36:47 +01:00
parent 5b2a633a99
commit 6822c8f3f8
23 changed files with 1915 additions and 608 deletions

View File

@@ -8,63 +8,113 @@
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="/assets/styles/base.css">
</head>
<body class="bg-dark text-light">
<nav class="navbar navbar-dark bg-black text-info px-3">
<span class="navbar-brand">mitho Admin</span>
{# ============================= #}
{# Top Navigation #}
{# ============================= #}
<nav class="navbar navbar-dark bg-black border-bottom border-secondary px-3">
<span class="navbar-brand fw-semibold text-info">
mitho Admin
</span>
<div class="ms-auto d-flex align-items-center gap-3">
{% if app.user %}
<span class="small text-secondary">{{ app.user.userIdentifier }}</span>
<a class="btn btn-sm btn-outline-light" href="{{ path('admin_logout') }}">Logout</a>
<span class="small text-light">
{{ app.user.userIdentifier }}
</span>
<a class="btn btn-sm btn-outline-light"
href="{{ path('admin_logout') }}">
Logout
</a>
{% endif %}
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-2 bg-black text-info border-end border-secondary min-vh-100 pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_dashboard') }}">Dashboard</a>
</li>
</ul>
<hr>
<h3>Dokumente und Wissen</h3>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_documents') }}">Dokumente</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_jobs') }}">
Indexierung (Ingest Jobs)
</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_system_agent') }}">
Wissensdaten (Chunk-Index)
</a>
</li>
</ul>
<hr>
<h3>System-Profile</h3>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_system_prompt') }}">
System-Prompt-Profil
</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="{{ path('admin_ingest_profile_list') }}">
Indexierungs-Profil (Ingest Profiles)
</a>
</li>
</ul>
</div>
{# ============================= #}
{# Layout Container #}
{# ============================= #}
<div class="col-10 pt-3">
<div class="container-fluid">
<div class="row flex-nowrap">
{# ============================= #}
{# Sidebar #}
{# ============================= #}
{% if app.user %}
<aside class="col-auto col-md-3 col-lg-2 bg-black border-end border-secondary min-vh-100 pt-3">
{% set route = app.request.attributes.get('_route') %}
<nav class="nav flex-column small">
<a class="nav-link text-light {% if route starts with 'admin_dashboard' %}active fw-bold{% endif %}"
href="{{ path('admin_dashboard') }}">
Dashboard
</a>
<hr class="border-secondary">
<div class="text-info text-uppercase small mb-2">
Dokumente & Wissen
</div>
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
href="{{ path('admin_documents') }}">
Dokumente
</a>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_jobs') }}">
Ingest Jobs
</a>
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
href="{{ path('admin_system_agent') }}">
Chunk-Index
</a>
<hr class="border-secondary">
<div class="text-info text-uppercase small mb-2">
System-Profile
</div>
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
href="{{ path('admin_system_prompt') }}">
System Prompt
</a>
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
href="{{ path('admin_ingest_profile_list') }}">
Ingest Profiles
</a>
<hr class="border-secondary">
<div class="text-info text-uppercase small mb-2">
KI-Endpunkte
</div>
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}">
Modell-Generierung
</a>
</nav>
</aside>
{% endif %}
{# ============================= #}
{# Main Content #}
{# ============================= #}
<main class="col py-4 px-4">
{% block body %}{% endblock %}
</div>
</main>
</div>
</div>

View File

@@ -3,52 +3,92 @@
{% block title %}Admin Dashboard{% endblock %}
{% block body %}
<h1 class="h4 mb-3">Dashboard</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Dashboard</h1>
</div>
{# ============================= #}
{# USER + RESET CARD #}
{# USER INFO CARD #}
{# ============================= #}
<div class="card bg-black text-info border-secondary mb-4">
<div class="card bg-black border-secondary mb-4 text-light">
<div class="card-body">
<h5 class="text-info mb-3">System Benutzer</h5>
<div class="mb-2">
<strong>User:</strong> {{ app.user.userIdentifier }}
</div>
<div class="mb-2">
<strong>Rollen:</strong> {{ app.user.roles|join(', ') }}
<strong>User:</strong>
{{ app.user.userIdentifier }}
</div>
<hr class="border-secondary">
<div class="mb-2">
<strong>Rollen:</strong>
{{ app.user.roles|join(', ') }}
</div>
<div class="text-light">
<p class="fw-bold">Reset des Systems</p>
<p>Unwiderruflicher Reset des gesamten Systems</p>
</div>
</div>
{# ============================= #}
{# SYSTEM RESET CARD #}
{# ============================= #}
{% if is_granted('ROLE_SUPER_ADMIN') %}
<div class="card bg-black border-danger mb-4 text-light">
<div class="card-body">
<h5 class="text-danger mb-3">System Reset</h5>
<div class="small text-light mb-3">
Der Reset entfernt:
<ul class="mb-2">
<li>Alle Dokumente und Versionen</li>
<li>Den gesamten NDJSON-Index</li>
<li>Den FAISS-Vektorindex</li>
<li>Alle Ingest-Jobs</li>
</ul>
Diese Aktion ist <strong>irreversibel</strong>.
</div>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} fade show" role="alert">
<div class="alert alert-{{ label }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
<form method="post" action="/admin/documents/reset" onsubmit="return resetSystem()">
<button type="submit" class="btn btn-outline-danger">
Reset System
<form method="post"
action="{{ path('admin_document_reset') }}"
onsubmit="return confirm('Wirklich das gesamte System zurücksetzen? Diese Aktion ist endgültig.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('system_reset') }}">
<button type="submit"
class="btn btn-outline-danger">
System vollständig zurücksetzen
</button>
</form>
</div>
</div>
</div>
{% endif %}
{# ============================= #}
{# KNOWLEDGE INDEX STATUS CARD #}
{# KNOWLEDGE INDEX STATUS #}
{# ============================= #}
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(1) : 0 %}
<div class="card bg-black text-light border-secondary">
<div class="card bg-black border-secondary text-light">
<div class="card-body">
<h5 class="text-info mb-3">Knowledge Index</h5>
<h5 class="text-info mb-3">Knowledge Index Status</h5>
<div class="mb-2">
<strong>Chunks:</strong>
@@ -60,31 +100,31 @@
<div class="progress bg-dark" style="height: 18px;">
<div
class="progress-bar
{% if chunkCount > 115000 %}
bg-danger
{% elseif chunkCount > 100000 %}
bg-warning text-dark
{% else %}
bg-success
{% endif %}
"
{% if percent >= 95 %}
bg-danger
{% elseif percent >= 85 %}
bg-warning text-dark
{% else %}
bg-success
{% endif %}"
role="progressbar"
aria-valuenow="{{ percent }}"
aria-valuemin="0"
aria-valuemax="100"
style="width: {{ percent }}%;"
>
{{ percent }}%
</div>
</div>
<div class="mt-2 small text-light">
System ist für maximal 120.000 Chunks optimiert.
<div class="mt-3 small text-secondary">
System ist für maximal {{ chunkLimit|number_format(0, ',', '.') }} Chunks optimiert.
{% if percent >= 95 %}
<br><strong class="text-danger">Kapazitätsgrenze nahezu erreicht.</strong>
{% endif %}
</div>
</div>
</div>
<script>
function resetSystem() {
return confirm('Sind Sie sicher, dass Sie das gesamte System zurücksetzen möchten?');
}
</script>
{% endblock %}

View File

@@ -3,53 +3,73 @@
{% block title %}Dokumente{% endblock %}
{% block body %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h4 mb-0">Dokumente</h1>
<a href="{{ path('admin_document_new') }}" class="btn btn-sm btn-light">
+ Neues Dokument
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Dokumente</h1>
<a href="{{ path('admin_document_new') }}"
class="btn btn-sm btn-outline-info">
Neues Dokument
</a>
</div>
{% if documents is empty %}
<div class="alert alert-secondary">
Keine Dokumente vorhanden.
</div>
{% else %}
<div class="card bg-black text-info border-secondary">
<div class="card bg-black border-secondary">
<div class="card-body p-0">
<table class="table table-dark table-hover mb-0 align-middle">
<thead>
<table class="table table-dark table-striped table-hover align-middle mb-0">
<thead class="table-secondary text-dark">
<tr>
<th>Titel</th>
<th>ID</th>
<th>Typ</th>
<th>Status</th>
<th>Indexiert</th>
<th>Indexierung</th>
<th>Versionen</th>
<th>Aktive Version</th>
<th>Erstellt am</th>
<th>Aktionen</th>
<th>Erstellt</th>
<th class="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{% for document in documents %}
<tr>
{# Titel #}
<td>
<a href="{{ path('admin_document_show', {id: document.id}) }}"
class="text-decoration-none text-light">
class="text-light text-decoration-none">
{{ document.title }}
</a>
</td>
<td>{{ document.id }}</td>
{# ID #}
<td class="small text-secondary">
{{ document.id }}
</td>
{# Typ #}
<td>
{% if document.currentVersion %}
<span class="badge bg-secondary">
{{ document.currentVersion.fileTypeLabel }}
</span>
{{ document.currentVersion.fileTypeLabel }}
</span>
{% else %}
<span class="badge bg-dark">-</span>
<span class="badge bg-dark border border-secondary">
-
</span>
{% endif %}
</td>
{# Dokument Status #}
<td>
{% if document.status == 'ACTIVE' %}
<span class="badge bg-success">Aktiv</span>
@@ -57,18 +77,32 @@
<span class="badge bg-secondary">Archiviert</span>
{% endif %}
</td>
{# Ingest Status #}
<td>
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
<span class="badge bg-success">
{{ document.currentVersion.ingestStatus }}
</span>
{% if document.currentVersion %}
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
<span class="badge bg-success">INDEXED</span>
{% elseif document.currentVersion.ingestStatus == 'PENDING' %}
<span class="badge bg-warning text-dark">PENDING</span>
{% elseif document.currentVersion.ingestStatus == 'FAILED' %}
<span class="badge bg-danger">FAILED</span>
{% else %}
<span class="badge bg-dark border border-secondary">
{{ document.currentVersion.ingestStatus }}
</span>
{% endif %}
{% else %}
<span class="badge bg-danger">
{{ document.currentVersion.ingestStatus }}
</span>
<span class="badge bg-dark border border-secondary">-</span>
{% endif %}
</td>
<td>{{ document.versions|length }}</td>
{# Version Count #}
<td>
{{ document.versions|length }}
</td>
{# Aktive Version #}
<td>
{% if document.currentVersion %}
v{{ document.currentVersion.versionNumber }}
@@ -76,33 +110,52 @@
-
{% endif %}
</td>
<td>{{ document.createdAt|date('d.m.Y H:i') }}</td>
<td class="d-flex gap-2">
<a class="btn btn-sm btn-outline-light"
{# Created At #}
<td class="small">
{{ document.createdAt|date('d.m.Y H:i') }}
</td>
{# Aktionen #}
<td class="text-end">
<a class="btn btn-sm btn-outline-light me-2"
href="{{ path('admin_document_show', {id: document.id}) }}">
Details
</a>
<form method="post"
action="{{ path('admin_document_delete', {id: document.id}) }}"
onsubmit="return confirm('Dokument wirklich endgültig löschen? Diese Aktion entfernt das Dokument aus Datenbank und Index.');">
{% if is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_document_delete', {id: document.id}) }}"
class="d-inline"
onsubmit="return confirm('Dokument wirklich endgültig löschen? Diese Aktion entfernt Dokument, Versionen und Index-Daten.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_document') }}">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_document_' ~ document.id) }}">
<button class="btn btn-sm btn-outline-danger">
Löschen
</button>
</form>
<button class="btn btn-sm btn-outline-danger">
Löschen
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="mt-4 small text-secondary">
Hinweis: Das Löschen eines Dokuments entfernt alle Versionen und
erfordert eine Aktualisierung des NDJSON-Indexes.
</div>
{% endblock %}

View File

@@ -3,24 +3,84 @@
{% block title %}Neues Dokument{% endblock %}
{% block body %}
<h1 class="h4 mb-4">Neues Dokument</h1>
<form method="post" enctype="multipart/form-data">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Neues Dokument</h1>
<a href="{{ path('admin_documents') }}"
class="btn btn-sm btn-outline-secondary">
Zurück zur Übersicht
</a>
</div>
<div class="card bg-black border-secondary text-light">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<input type="hidden"
name="_token"
value="{{ csrf_token('create_document') }}">
{# ============================= #}
{# Titel #}
{# ============================= #}
<div class="mb-4">
<label class="form-label">Titel</label>
<div class="alert alert-secondary small">
<strong>Hinweis zur Qualität:</strong><br>
Der Titel ist entscheidend für die semantische Einordnung
der erzeugten Chunks. Jeder Chunk erhält den Titel als Kontext,
wodurch Retrieval und Antwortqualität signifikant verbessert werden.<br><br>
Wird kein Titel angegeben, wird automatisch der Dateiname
verwendet (nicht empfohlen).
</div>
<input class="form-control bg-dark text-light border-secondary"
name="title"
placeholder="z. B. Sicherheitsdatenblatt Produkt XY">
</div>
{# ============================= #}
{# Datei Upload #}
{# ============================= #}
<div class="mb-4">
<label class="form-label">Datei</label>
<input type="file"
class="form-control bg-dark text-light border-secondary"
name="file"
required>
<div class="form-text text-secondary">
Unterstützte Formate: PDF, DOCX, TXT, MD.
Das Dokument wird versioniert gespeichert und anschließend
indexiert.
</div>
</div>
{# ============================= #}
{# Submit #}
{# ============================= #}
<div class="d-flex justify-content-end">
<button class="btn btn-outline-info">
Dokument speichern
</button>
</div>
</form>
<div class="mb-3">
<label class="form-label">Titel:</label>
<div class="mb-2"><b>Bitte geben Sie einen aussagekräftigen Titel ein.</b><br>
Der Titel ist entscheidend, damit in jedem Chunk ein sinnvoller thematischer Bezug hergestellt und eine saubere semantische Zuordnung ermöglicht werden kann.<br>
Wenn kein Titel angegeben wird, wird automatisch der Dateiname als Titel verwendet (nicht empfohlen).</div>
<input class="form-control" name="title">
</div>
</div>
<div class="mb-3">
<label class="form-label">Datei:</label>
<input type="file" class="form-control" name="file" required>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Nach dem Upload wird automatisch eine neue Dokumentversion erstellt.
Die Indexierung erfolgt asynchron über einen Ingest-Job.
</div>
<button class="btn btn-light">Speichern</button>
</form>
{% endblock %}

View File

@@ -1,29 +1,86 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Neue Version{% endblock %}
{% block title %}Neue Dokumentversion{% endblock %}
{% block body %}
<a href="{{ path('admin_document_show', {id: document.id}) }}"
class="btn btn-sm btn-outline-light mb-3">
← Zurück
</a>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
Neue Version
</h1>
<h1 class="h4 mb-4">
Neue Version für: {{ document.title }}
</h1>
<a href="{{ path('admin_document_show', {id: document.id}) }}"
class="btn btn-sm btn-outline-secondary">
Zurück zum Dokument
</a>
</div>
<form method="post" enctype="multipart/form-data">
<div class="card bg-black border-secondary mb-4 text-light">
<div class="card-body">
<div class="mb-3">
<strong>Dokument:</strong>
<span class="text-light">{{ document.title }}</span>
</div>
<div class="small text-secondary">
Das Hochladen einer neuen Version erzeugt eine zusätzliche
unveränderliche Dokumentversion. Die Aktivierung erfolgt separat
und löst einen deterministischen Re-Ingest aus.
</div>
<div class="mb-3">
<label class="form-label">Datei auswählen</label>
<input type="file" class="form-control" name="file" required>
</div>
</div>
<button class="btn btn-light">
Version hochladen
</button>
<div class="card bg-black border-secondary text-light">
<div class="card-body">
</form>
<form method="post" enctype="multipart/form-data">
<input type="hidden"
name="_token"
value="{{ csrf_token('create_document_version_' ~ document.id) }}">
{# ============================= #}
{# Datei Upload #}
{# ============================= #}
<div class="mb-4">
<label class="form-label">Datei auswählen</label>
<input type="file"
class="form-control bg-dark text-light border-secondary"
name="file"
required>
<div class="form-text text-secondary">
Unterstützte Formate: PDF, DOCX, TXT, MD.<br>
Die Datei wird versioniert gespeichert und mit einer
eindeutigen Checksum versehen.
</div>
</div>
{# ============================= #}
{# Submit #}
{# ============================= #}
{% if is_granted('ROLE_SUPER_ADMIN') %}
<div class="d-flex justify-content-end">
<button class="btn btn-outline-info">
Version hochladen
</button>
</div>
{% endif %}
</form>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Eine neue Version ersetzt nicht automatisch die aktive Version.
Erst nach Aktivierung wird ein Re-Ingest durchgeführt und der Index
neu aufgebaut.
</div>
{% endblock %}

View File

@@ -4,14 +4,22 @@
{% block body %}
<a href="{{ path('admin_documents') }}" class="btn btn-sm btn-outline-light mb-3">
← Zurück
</a>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">{{ document.title }}</h1>
<a href="{{ path('admin_documents') }}"
class="btn btn-sm btn-outline-secondary">
Zurück zur Übersicht
</a>
</div>
{% if document %}
<h1 class="h4 mb-3">{{ document.title }}</h1>
<div class="card bg-black text-info border-secondary mb-4">
{# ============================= #}
{# Dokument-Meta #}
{# ============================= #}
<div class="card bg-black border-secondary mb-5 text-light">
<div class="card-body">
<div class="mb-2">
@@ -25,7 +33,7 @@
<div class="mb-2">
<strong>Erstellt von:</strong>
{{ document.createdBy.email }}
{{ document.createdBy ? document.createdBy.email : '-' }}
</div>
<div class="mb-2">
@@ -36,7 +44,9 @@
<div class="mb-2">
<strong>Aktive Version:</strong>
{% if document.currentVersion %}
v{{ document.currentVersion.versionNumber }}
<span class="badge bg-info text-dark">
v{{ document.currentVersion.versionNumber }}
</span>
{% else %}
-
{% endif %}
@@ -45,43 +55,65 @@
</div>
</div>
<h2 class="h5 mb-3">Versionen</h2>
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
class="btn btn-sm btn-light mb-3">
+ Neue Version
</a>
{# ============================= #}
{# Versionen #}
{# ============================= #}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Versionen</h2>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Neue Version
</a>
{% endif %}
</div>
{% if document.versions is empty %}
<div class="alert alert-secondary">
Keine Versionen vorhanden.
</div>
{% else %}
<div class="card bg-black text-info border-secondary">
<div class="card bg-black border-secondary">
<div class="card-body p-0">
<table class="table table-dark table-hover mb-0">
<thead>
<table class="table table-dark table-striped table-hover align-middle mb-0">
<thead class="table-secondary text-dark">
<tr>
<th>Version</th>
<th>Aktiv</th>
<th>Status</th>
<th>Ingest</th>
<th>Checksum</th>
<th>Erstellt von</th>
<th>Datum</th>
<th>Aktion</th>
<th class="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{% for version in document.versions %}
<tr>
<td>v{{ version.versionNumber }}</td>
{% for version in document.versions %}
<tr>
<td>
<strong>v{{ version.versionNumber }}</strong>
</td>
{# Aktivstatus #}
<td>
{% if version.isActive %}
<span class="badge bg-success">Ja</span>
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Nein</span>
<span class="badge bg-dark border border-secondary">
Inaktiv
</span>
{% endif %}
</td>
{# Ingest Status #}
<td>
{% if version.ingestStatus == 'INDEXED' %}
<span class="badge bg-success">INDEXED</span>
@@ -89,69 +121,103 @@
<span class="badge bg-warning text-dark">RUNNING</span>
{% elseif version.ingestStatus == 'FAILED' %}
<span class="badge bg-danger">FAILED</span>
{% else %}
{% elseif version.ingestStatus == 'PENDING' %}
<span class="badge bg-secondary">PENDING</span>
{% else %}
<span class="badge bg-dark border border-secondary">
{{ version.ingestStatus }}
</span>
{% endif %}
</td>
<td>
{{ version.checksum[:10] }}...
{# Checksum #}
<td class="small text-secondary">
{{ version.checksum ? version.checksum[:10] ~ '…' : '-' }}
</td>
{# Created by #}
<td>
{{ version.createdBy.email }}
{{ version.createdBy ? version.createdBy.email : '-' }}
</td>
<td>
{# Date #}
<td class="small">
{{ version.createdAt|date('d.m.Y H:i') }}
</td>
<td>
{# Aktionen #}
<td class="text-end">
{% if version.isActive %}
{# Optional: manuelles Re-Ingest nur bei PENDING/FAILED #}
{% if version.ingestStatus in ['PENDING', 'FAILED'] %}
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
style="display:inline;">
<input type="hidden" name="_token"
value="{{ csrf_token('ingest_version') }}">
class="d-inline"
onsubmit="return confirm('Ingest erneut starten?');">
<input type="hidden"
name="_token"
value="{{ csrf_token('ingest_version_' ~ version.id) }}">
<button class="btn btn-sm btn-outline-info">
Ingest starten
</button>
</form>
{% else %}
<span class="text-success">Ingested</span>
<span class="text-success small">
Bereits indexiert
</span>
{% endif %}
{% else %}
<form method="post"
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
style="display:inline;">
<input type="hidden" name="_token"
value="{{ csrf_token('activate_version') }}">
<button class="btn btn-sm btn-outline-light">
Aktivieren
</button>
</form>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
class="d-inline"
onsubmit="return confirm('Diese Version aktivieren? Es wird ein Re-Ingest ausgelöst.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('activate_version_' ~ version.id) }}">
<button class="btn btn-sm btn-outline-light">
Aktivieren
</button>
</form>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="mt-4 small text-secondary">
Hinweis: Beim Aktivieren einer Version wird automatisch ein Re-Ingest
durchgeführt. Der NDJSON-Index und der FAISS-Index werden deterministisch
neu aufgebaut.
</div>
{% else %}
<h1 class="h4 mb-3">Ein Fehler trat auf</h1>
<h2 class="h5 mb-3">Fehler:</h2>
{% for message in app.flashes('danger') %}
<div class="alert alert-danger">
{{ message }}
</div>
{% endfor %}
<div class="alert alert-danger">
Dokument nicht gefunden.
</div>
{% endif %}
{% endblock %}
{% endblock %}

View File

@@ -1,76 +1,145 @@
{% extends 'admin/base.html.twig' %}
{% block title %}System Prompt{% endblock %}
{% block title %}Neues Indexierungsprofil{% endblock %}
{% block body %}
<h1>Create Ingest Profile</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Neues Indexierungsprofil</h1>
<form method="post">
<table class="table table-sm table-dark align-middle">
<tbody>
<tr>
<th scope="row" class="w-25">Chunk Size (500-2500)</th>
<td>
<label>
<select name="chunk_size" class="form-select">
<a href="{{ path('admin_ingest_profile_list') }}"
class="btn btn-sm btn-outline-secondary">
Zurück
</a>
</div>
<div class="card bg-black border-secondary text-light">
<div class="card-body">
<form method="post">
<input type="hidden"
name="_token"
value="{{ csrf_token('create_ingest_profile') }}">
<div class="row g-4">
<!-- ===================== -->
<!-- Chunking Section -->
<!-- ===================== -->
<div class="col-12">
<h5 class="text-info">Chunking</h5>
<hr class="border-secondary">
</div>
<div class="col-md-6">
<label class="form-label">
Chunk Size
</label>
<select name="chunk_size"
class="form-select bg-dark text-light border-secondary"
required>
{% for i in range(250, 2500, 50) %}
<option value="{{ i }}" {{ selectedValue is defined and selectedValue == i ? 'selected' : '' }}>
<option value="{{ i }}">
{{ i }}
</option>
{% endfor %}
</select>
</label>
</td>
</tr>
<tr>
<th scope="row">Chunk Overlap (50-150)</th>
<td>
<label>
<select name="chunk_overlap" class="form-select">
{% for i in range(50, 150, 25) %}
<option value="{{ i }}" {{ selectedValue is defined and selectedValue == i ? 'selected' : '' }}>
{{ i }}
</option>
{% endfor %}
</select>
</label>
</td>
</tr>
<tr>
<th scope="row">Embedding Model (default)</th>
<td>
<label>
<select name="embedding_model" class="form-control" required>
<option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option>
</select>
</label>
</td>
</tr>
<tr>
<th scope="row">Embedding Dimension (default)</th>
<td>
<label>
<select name="embedding_dimension" class="form-control" required>
<option value="768">768</option>
</select>
</label>
</td>
</tr>
<tr>
<th scope="row">Scoring Version (default)</th>
<td>
<label>
<input type="number" name="scoring_version" class="form-control" value="1" placeholder="1" readonly required>
</label>
</td>
</tr>
<tr>
<td colspan="2" class="text-start">
<button type="submit" class="btn btn-primary">Create</button>
</td>
</tr>
</tbody>
</table>
</form>
<div class="form-text text-secondary">
Größere Werte = weniger Chunks, mehr Kontext pro Chunk.
</div>
</div>
<div class="col-md-6">
<label class="form-label">
Chunk Overlap
</label>
<select name="chunk_overlap"
class="form-select bg-dark text-light border-secondary"
required>
{% for i in range(50, 200, 25) %}
<option value="{{ i }}">
{{ i }}
</option>
{% endfor %}
</select>
<div class="form-text text-secondary">
Überlappung zwischen Chunks zur Kontextstabilisierung.
</div>
</div>
<!-- ===================== -->
<!-- Embedding Section -->
<!-- ===================== -->
<div class="col-12 mt-4">
<h5 class="text-info">Embedding</h5>
<hr class="border-secondary">
</div>
<div class="col-md-6">
<label class="form-label">
Embedding Model
</label>
<select name="embedding_model"
class="form-select bg-dark text-light border-secondary"
required>
<option value="all-MiniLM-L6-v2">
all-MiniLM-L6-v2 (768d)
</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">
Embedding Dimension
</label>
<input type="number"
name="embedding_dimension"
value="768"
class="form-control bg-dark text-light border-secondary"
readonly>
<div class="form-text text-secondary">
Muss mit dem Embedding-Modell übereinstimmen.
</div>
</div>
<!-- ===================== -->
<!-- Scoring Section -->
<!-- ===================== -->
<div class="col-12 mt-4">
<h5 class="text-info">Scoring</h5>
<hr class="border-secondary">
</div>
<div class="col-md-6">
<label class="form-label">
Scoring Version
</label>
<input type="number"
name="scoring_version"
value="1"
class="form-control bg-dark text-light border-secondary"
readonly>
<div class="form-text text-secondary">
Erhöhung erzwingt Global Reindex.
</div>
</div>
</div>
<hr class="border-secondary my-4">
<div class="d-flex justify-content-end">
<button type="submit"
class="btn btn-outline-info">
Profil erstellen
</button>
</div>
</form>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Änderungen am Indexierungsprofil wirken sich auf die Struktur des
Vektor-Indexes aus. Nach Aktivierung ist ein vollständiger Reindex erforderlich.
</div>
{% endblock %}

View File

@@ -1,94 +1,202 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Ingest Profiles{% endblock %}
{% block title %}Indexierungsprofile{% endblock %}
{% block body %}
<h1>Ingest Profiles</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Indexierungsprofile</h1>
<a class="btn btn-sm btn-outline-info"
href="{{ path('admin_ingest_profile_create') }}">
Neues Profil anlegen
</a>
</div>
{# ============================= #}
{# Strukturstatus Alert #}
{# ============================= #}
{% if structureMismatch %}
<div class="alert alert-danger">
⚠ Strukturabweichung festgestellt Globale Neuindizierung erforderlich | <a href="{{ path('admin_jobs') }}">Global Reindex aufrufen</a>
<div class="alert alert-danger d-flex justify-content-between align-items-center">
<div>
<strong>Strukturabweichung erkannt.</strong>
Die aktuelle Indexstruktur entspricht nicht dem aktiven Profil.
Eine globale Neuindizierung ist erforderlich.
</div>
<a href="{{ path('admin_jobs') }}"
class="btn btn-sm btn-outline-light">
Global Reindex starten
</a>
</div>
{% else %}
<div class="alert alert-success">
Die Indexstruktur entspricht dem aktiven Profil
Die Indexstruktur entspricht dem aktiven Profil.
</div>
{% endif %}
<p><a class="btn btn-outline-light" href="{{ path('admin_ingest_profile_create') }}">+ Neues Profil anlegen</a></p>
{# ============================= #}
{# Profile Tabelle #}
{# ============================= #}
<h2>Profiles</h2>
<div class="card bg-black border-secondary mb-5">
<div class="card-body p-0">
<table border="1" cellpadding="6" class="table table-sm table-dark align-middle">
<tr>
<th>Version</th>
<th>Chunk Size</th>
<th>Overlap</th>
<th>Model</th>
<th>Dimension</th>
<th>Scoring</th>
<th>Active</th>
<th>Reindex Required</th>
<th>Actions</th>
</tr>
<table class="table table-dark table-striped table-hover align-middle mb-0">
<thead class="table-secondary text-dark">
<tr>
<th>Version</th>
<th>Chunk Size</th>
<th>Overlap</th>
<th>Embedding</th>
<th>Dim</th>
<th>Scoring</th>
<th>Status</th>
<th>Reindex</th>
<th class="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{% for p in profiles %}
<tr>
<td>v{{ p.version }}</td>
<td>{{ p.chunkSize }}</td>
<td>{{ p.chunkOverlap }}</td>
<td>{{ p.embeddingModel }}</td>
<td>{{ p.embeddingDimension }}</td>
<td>{{ p.scoringVersion }}</td>
{% for p in profiles %}
<tr>
<td {% if p.active %}class="text-success"{% endif %}>{{ p.version }}</td>
<td>{{ p.chunkSize }}</td>
<td>{{ p.chunkOverlap }}</td>
<td>{{ p.embeddingModel }}</td>
<td>{{ p.embeddingDimension }}</td>
<td>{{ p.scoringVersion }}</td>
<td {% if p.active %}class="text-success"{% endif %}>{{ p.active ? 'Yes' : 'No' }}</td>
<td>{{ p.reindexRequired ? 'Yes' : 'No' }}</td>
<td>
{% if not p.active %}
<a class="btn btn-outline-success btn-sm" href="{{ path('admin_ingest_profile_activate', {id: p.id}) }}">
Aktivieren
</a>
{% endif %}
{% if not p.active %}
<a class="btn btn-outline-danger btn-sm" href="{{ path('admin_ingest_profile_remove', {id: p.id}) }}">
Löschen
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<td>
{% if p.active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-dark border border-secondary">
Inaktiv
</span>
{% endif %}
</td>
<hr>
<h2>Index-Struktur-Profil Diff</h2>
<td>
{% if p.reindexRequired %}
<span class="badge bg-warning text-dark">
Erforderlich
</span>
{% else %}
<span class="badge bg-secondary">
Nein
</span>
{% endif %}
</td>
{% if indexMeta %}
<p><strong>Index Version:</strong> {{ indexMeta.index_version }}</p>
{% else %}
<p>No index_meta.json found.</p>
{% endif %}
<td class="text-end">
<table border="1" cellpadding="6" class="table table-sm table-dark align-middle">
<tr>
<th>Parameter</th>
<th>Index Meta</th>
<th>Active Profile</th>
<th>Status</th>
</tr>
{% if not p.active and is_granted('ROLE_SUPER_ADMIN') %}
{% for key, row in diff %}
<tr>
<td>{{ key }}</td>
<td>{{ row.meta }}</td>
<td>{{ row.profile }}</td>
<td>
{% if row.equal %}
<span style="color:green;">✓</span>
{% else %}
<span style="color:red;">✗</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{# Aktivieren via POST #}
<form method="post"
action="{{ path('admin_ingest_profile_activate', {id: p.id}) }}"
class="d-inline"
onsubmit="return confirm('Profil aktivieren? Global Reindex kann erforderlich sein.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('activate_ingest_profile_' ~ p.id) }}">
<button class="btn btn-sm btn-outline-success me-2">
Aktivieren
</button>
</form>
{# Löschen via POST #}
<form method="post"
action="{{ path('admin_ingest_profile_remove', {id: p.id}) }}"
class="d-inline"
onsubmit="return confirm('Profil wirklich löschen?');">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_ingest_profile_' ~ p.id) }}">
<button class="btn btn-sm btn-outline-danger">
Löschen
</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="9" class="text-center text-secondary py-4">
Keine Profile vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# ============================= #}
{# Struktur-Diff #}
{# ============================= #}
<div class="card bg-black border-secondary">
<div class="card-body">
<h5 class="text-info mb-3">Index-Struktur Vergleich</h5>
{% if indexMeta %}
<div class="mb-3 small text-secondary">
Aktuelle Index-Version:
<strong>{{ indexMeta.index_version }}</strong>
</div>
{% else %}
<div class="alert alert-warning">
index_meta.json nicht gefunden.
</div>
{% endif %}
<table class="table table-dark table-striped table-hover align-middle">
<thead class="table-secondary text-dark">
<tr>
<th>Parameter</th>
<th>Index Meta</th>
<th>Aktives Profil</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for key, row in diff %}
<tr>
<td>{{ key }}</td>
<td>{{ row.meta }}</td>
<td>{{ row.profile }}</td>
<td>
{% if row.equal %}
<span class="badge bg-success">Identisch</span>
{% else %}
<span class="badge bg-danger">Abweichung</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-center text-secondary py-4">
Keine Vergleichsdaten verfügbar.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Strukturänderungen (Chunking, Embedding, Scoring) führen zu
inkonsistentem Retrieval, bis eine vollständige Neuindizierung durchgeführt wird.
</div>
{% endblock %}

View File

@@ -1,32 +1,43 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Ingest Jobs{% endblock %}
{% block title %}Indexierung (Ingest Jobs){% endblock %}
{% block body %}
<h1 class="h4 mb-4">Indexierung (Ingest Jobs-Liste)</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Indexierung (Ingest Jobs)</h1>
<form method="post"
action="{{ path('admin_global_reindex') }}"
onsubmit="return confirm('Global Reindex starten? Dies kann einige Zeit dauern.');">
{% if is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_global_reindex') }}"
onsubmit="return confirm('Global Reindex starten? Dies kann je nach Datenmenge mehrere Minuten dauern.');"
class="mb-0">
<button type="submit"
class="btn btn-danger mb-3">
Global Reindex starten
</button>
</form>
<input type="hidden"
name="_token"
value="{{ csrf_token('global_reindex') }}">
<button type="submit"
class="btn btn-sm btn-outline-danger">
Global Reindex starten
</button>
</form>
{% endif %}
</div>
{% if jobs is empty %}
<div class="alert alert-secondary">
Keine Jobs vorhanden.
Keine Ingest Jobs vorhanden.
</div>
{% else %}
<div class="card bg-black text-info border-secondary">
<div class="card bg-black border-secondary">
<div class="card-body p-0">
<table class="table table-dark table-hover mb-0">
<thead>
<table class="table table-dark table-striped table-hover align-middle mb-0">
<thead class="table-secondary text-dark">
<tr>
<th>Job-ID</th>
<th>Typ</th>
@@ -35,20 +46,26 @@
<th>Version</th>
<th>Gestartet</th>
<th>Beendet</th>
<th>User</th>
<th>Benutzer</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>
<td class="small">
<a href="{{ path('admin_job_show', {id: job.id}) }}"
class="text-light">
class="text-light text-decoration-none">
{{ job.id }}
</a>
</td>
<td>{{ job.type }}</td>
<td>
<span class="badge bg-info text-dark">
{{ job.type }}
</span>
</td>
<td>
{% if job.status == 'COMPLETED' %}
@@ -60,52 +77,59 @@
{% elseif job.status == 'FAILED' %}
<span class="badge bg-danger">FAILED</span>
{% else %}
<span class="badge bg-secondary">{{ job.status }}</span>
<span class="badge bg-dark border border-secondary">
{{ job.status }}
</span>
{% endif %}
</td>
<td>
{% if job.documentId %}
<a href="/admin/documents/{{ job.documentId }}" class="text-light">{{ job.documentId }}</a>
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
class="text-light text-decoration-none">
{{ job.documentId }}
</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if job.documentVersionId %}
{{ job.documentVersionId }}
{% else %}
-
{% endif %}
{{ job.documentVersionId ?? '-' }}
</td>
<td>{{ job.startedAt|date('d.m.Y H:i:s') }}</td>
<td>
{% if job.finishedAt %}
{{ job.finishedAt|date('d.m.Y H:i:s') }}
{% else %}
-
{% endif %}
<td class="small">
{{ job.startedAt ? job.startedAt|date('d.m.Y H:i:s') : '-' }}
</td>
<td>
{% if job.startedBy %}
{{ job.startedBy.email }}
{% else %}
-
{% endif %}
<td class="small">
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
</td>
<td class="small">
{{ job.startedBy ? job.startedBy.email : '-' }}
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center text-secondary py-4">
Keine Jobs gefunden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="mt-4 small text-secondary">
Hinweis: Während laufender Jobs (Status RUNNING) sollten keine
parallelen Reindex-Prozesse gestartet werden.
</div>
{% endblock %}

View File

@@ -4,48 +4,38 @@
{% block body %}
<a href="{{ path('admin_jobs') }}"
class="btn btn-sm btn-outline-light mb-3">
← Zurück
</a>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Ingest Job</h1>
<h1 class="h4 mb-4">Ingest Job</h1>
<a href="{{ path('admin_jobs') }}"
class="btn btn-sm btn-outline-secondary">
Zurück zur Übersicht
</a>
</div>
<div class="card bg-black text-info border-secondary">
<div class="card bg-black border-secondary text-light">
<div class="card-body">
<div class="mb-2">
<strong>ID:</strong> {{ job.id }}
<strong>ID:</strong>
<span class="small text-light">{{ job.id }}</span>
</div>
<div class="mb-2">
<strong>Typ:</strong> {{ job.type }}
<strong>Typ:</strong>
<span class="badge bg-info text-dark">{{ job.type }}</span>
</div>
<div class="mb-2">
<strong>Status:</strong>
<span id="job-status-badge">
{% if job.status == 'COMPLETED' %}
<span class="badge bg-success">COMPLETED</span>
{% elseif job.status == 'QUEUED' %}
<span class="badge bg-secondary">QUEUED</span>
{% elseif job.status == 'RUNNING' %}
<span class="badge bg-warning text-dark">RUNNING</span>
{% elseif job.status == 'FAILED' %}
<span class="badge bg-danger">FAILED</span>
{% elseif job.status == 'ABORTED' %}
<span class="badge bg-dark">ABORTED</span>
{% else %}
<span class="badge bg-secondary">{{ job.status }}</span>
{% endif %}
</span>
<span id="job-status-badge"></span>
</div>
<div class="mb-2">
<strong>Dokument:</strong>
{% if job.documentId %}
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
class="text-light">
class="text-light text-decoration-none">
{{ job.documentId }}
</a>
{% else %}
@@ -66,8 +56,8 @@
<div class="mb-2">
<strong>Beendet:</strong>
<span id="job-finished-at">
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
</span>
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
</span>
</div>
<div class="mb-2">
@@ -75,32 +65,33 @@
{{ job.startedBy ? job.startedBy.email : '-' }}
</div>
{# Loader #}
<div id="job-loader"
class="mt-3"
style="{% if job.status in ['QUEUED','RUNNING'] %}{% else %}display:none;{% endif %}">
class="mt-3 d-none">
<div class="d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
<div>
<strong>Prozess läuft…</strong><br>
<small class="text-secondary">
Diese Seite aktualisiert den Status automatisch.
Der Status wird automatisch aktualisiert.
</small>
</div>
</div>
</div>
{# Fehlerbereich #}
<div id="job-error"
class="alert alert-danger mt-3"
style="{% if job.status == 'FAILED' %}{% else %}display:none;{% endif %}">
{% if job.errorMessage %}
<strong>Fehler:</strong><br>
{{ job.errorMessage }}
{% endif %}
class="alert alert-danger mt-3 d-none">
</div>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Bei DOCUMENT_VERSION_ACTIVATE-Jobs wird ein vollständiger
NDJSON-Rebuild und FAISS-Reindex durchgeführt.
</div>
<script>
(function () {
@@ -112,31 +103,27 @@
let timer = null;
function renderBadge(status) {
const map = {
COMPLETED: 'bg-success',
QUEUED: 'bg-secondary',
RUNNING: 'bg-warning text-dark',
FAILED: 'bg-danger',
ABORTED: 'bg-dark'
};
const css = map[status] || 'bg-secondary';
badgeWrap.innerHTML =
`<span class="badge ${css}">${status}</span>`;
}
function stopPolling() {
if (timer !== null) {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function setBadge(status) {
let html = '';
if (status === 'COMPLETED')
html = '<span class="badge bg-success">COMPLETED</span>';
else if (status === 'QUEUED')
html = '<span class="badge bg-secondary">QUEUED</span>';
else if (status === 'RUNNING')
html = '<span class="badge bg-warning text-dark">RUNNING</span>';
else if (status === 'FAILED')
html = '<span class="badge bg-danger">FAILED</span>';
else if (status === 'ABORTED')
html = '<span class="badge bg-dark">ABORTED</span>';
else
html = '<span class="badge bg-secondary">' + status + '</span>';
badgeWrap.innerHTML = html;
}
async function poll() {
try {
const res = await fetch(statusUrl);
@@ -145,23 +132,24 @@
const data = await res.json();
const status = (data.status || '').toUpperCase();
setBadge(status);
renderBadge(status);
finishedAtEl.textContent = data.finishedAt
? new Date(data.finishedAt).toLocaleString('de-DE')
: '-';
finishedAtEl.textContent =
data.finishedAt
? new Date(data.finishedAt).toLocaleString('de-DE')
: '-';
if (status === 'QUEUED' || status === 'RUNNING') {
loaderEl.style.display = '';
loaderEl.classList.remove('d-none');
} else {
loaderEl.style.display = 'none';
loaderEl.classList.add('d-none');
stopPolling();
}
if (status === 'FAILED' && data.errorMessage) {
errorEl.style.display = '';
errorEl.classList.remove('d-none');
errorEl.innerHTML =
'<strong>Fehler:</strong><br>' + data.errorMessage;
`<strong>Fehler:</strong><br>${data.errorMessage}`;
}
} catch (e) {
@@ -169,8 +157,13 @@
}
}
timer = setInterval(poll, 1000);
poll();
// Initial render from server state
renderBadge("{{ job.status|upper }}");
if (["QUEUED","RUNNING"].includes("{{ job.status|upper }}")) {
loaderEl.classList.remove('d-none');
timer = setInterval(poll, 2000);
}
})();
</script>

View File

@@ -0,0 +1,139 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Neue Modell-Generierungskonfiguration{% endblock %}
{% block body %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Neue Modell-Generierungskonfiguration</h1>
<a href="{{ path('admin_model_config_list') }}"
class="btn btn-sm btn-outline-secondary">
Zurück
</a>
</div>
<div class="card bg-black border-secondary text-light">
<div class="card-body">
<form method="post">
<input type="hidden" name="_token"
value="{{ csrf_token('create_model_config') }}">
<div class="row g-4">
<!-- Modell -->
<div class="col-md-6">
<label class="form-label">Modellname</label>
<input type="text"
name="model_name"
class="form-control bg-dark text-light border-secondary"
placeholder="z. B. qwen3:latest"
required>
<div class="form-text text-secondary">
Exakter Modellname wie im Endpunkt konfiguriert.
</div>
</div>
<!-- Stream -->
<div class="col-md-6 d-flex align-items-center">
<div class="form-check form-switch mt-4">
<input class="form-check-input"
type="checkbox"
name="stream"
value="1"
id="streamSwitch">
<label class="form-check-label" for="streamSwitch">
Streaming aktivieren
</label>
</div>
</div>
<!-- Temperature -->
<div class="col-md-4">
<label class="form-label">Temperature</label>
<input type="number"
step="0.1"
min="0"
max="2"
name="temperature"
value="0.1"
class="form-control bg-dark text-light border-secondary"
required>
<div class="form-text text-secondary">
Niedrige Werte = deterministisch (empfohlen für RAG).
</div>
</div>
<!-- Top K -->
<div class="col-md-4">
<label class="form-label">Top K</label>
<input type="number"
min="1"
name="top_k"
value="20"
class="form-control bg-dark text-light border-secondary"
required>
</div>
<!-- Top P -->
<div class="col-md-4">
<label class="form-label">Top P</label>
<input type="number"
step="0.05"
min="0"
max="1"
name="top_p"
value="0.8"
class="form-control bg-dark text-light border-secondary"
required>
</div>
<!-- Repeat Penalty -->
<div class="col-md-6">
<label class="form-label">Repeat Penalty</label>
<input type="number"
step="0.05"
min="0"
max="5"
name="repeat_penalty"
value="1.05"
class="form-control bg-dark text-light border-secondary"
required>
</div>
<!-- Num Ctx -->
<div class="col-md-6">
<label class="form-label">Context Window (num_ctx)</label>
<input type="number"
min="512"
max="32768"
name="num_ctx"
value="4096"
class="form-control bg-dark text-light border-secondary"
required>
<div class="form-text text-secondary">
Muss zum Modell passen. Zu hohe Werte können Performance beeinflussen.
</div>
</div>
</div>
<hr class="border-secondary my-4">
<div class="d-flex justify-content-end">
<button type="submit"
class="btn btn-outline-info">
Konfiguration speichern
</button>
</div>
</form>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Neue Konfigurationen werden zunächst inaktiv gespeichert und
müssen separat aktiviert werden. Pro Modell kann nur eine Version aktiv sein.
</div>
{% endblock %}

View File

@@ -0,0 +1,102 @@
{% extends 'admin/base.html.twig' %}
{% block title %}KI Modell-Generierung Config{% endblock %}
{% block body %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">KI Modell-Generierung Config</h1>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a href="{{ path('admin_model_config_create') }}"
class="btn btn-sm btn-outline-info">
Neue Konfiguration
</a>
{% endif %}
</div>
<div class="card bg-black border-secondary">
<div class="card-body p-0">
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<th>Modell</th>
<th>Version</th>
<th>Stream</th>
<th>Temp</th>
<th>Top K</th>
<th>Top P</th>
<th>Repeat</th>
<th>Ctx</th>
<th>Status</th>
<th class="text-end">Aktion</th>
</tr>
</thead>
<tbody>
{% for config in configs %}
<tr>
<td>{{ config.modelName }}</td>
<td>v{{ config.version }}</td>
<td>
{% if config.stream %}
<span class="badge bg-info text-dark">Streaming</span>
{% else %}
<span class="badge bg-secondary">Blocking</span>
{% endif %}
</td>
<td>{{ config.temperature }}</td>
<td>{{ config.topK }}</td>
<td>{{ config.topP }}</td>
<td>{{ config.repeatPenalty }}</td>
<td>{{ config.numCtx }}</td>
<td>
{% if config.active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-dark border border-secondary">
Inaktiv
</span>
{% endif %}
</td>
<td class="text-end">
{% if not config.active and is_granted('ROLE_SUPER_ADMIN') %}
<a href="{{ path('admin_model_config_activate', {id: config.id}) }}"
class="btn btn-sm btn-outline-success me-2">
Aktivieren
</a>
<form method="post"
action="{{ path('admin_model_config_delete', {id: config.id}) }}"
style="display:inline-block"
onsubmit="return confirm('Wirklich löschen?');">
<input type="hidden" name="_token"
value="{{ csrf_token('delete_model_config_' ~ config.id) }}">
<button class="btn btn-sm btn-outline-danger">
Löschen
</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="10" class="text-center text-secondary py-4">
Keine Konfiguration vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Änderungen wirken sich unmittelbar auf die Generierungsparameter
des aktiven Modells aus. Nur eine Konfiguration pro Modell kann aktiv sein.
</div>
{% endblock %}

View File

@@ -3,34 +3,71 @@
{% block title %}Admin Login{% endblock %}
{% block body %}
<div class="row justify-content-center">
<div class="row justify-content-center align-items-center" style="min-height:70vh;">
<div class="col-12 col-md-5 col-lg-4">
<h1 class="h4 mb-3">Admin Login</h1>
<div class="card bg-black border-secondary text-info">
<div class="card-body">
<h1 class="h4 mb-4 text-center text-info">
mitho® KI RAG Login
</h1>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
<form method="post" action="{{ path('admin_login') }}">
{# ============================= #}
{# Email #}
{# ============================= #}
<div class="mb-3">
<label class="form-label">E-Mail</label>
<input class="form-control bg-dark text-light border-secondary"
name="_username"
value="{{ last_username }}"
autocomplete="email"
required>
</div>
{# ============================= #}
{# Passwort #}
{# ============================= #}
<div class="mb-4">
<label class="form-label">Passwort</label>
<input class="form-control bg-dark text-light border-secondary"
type="password"
name="_password"
autocomplete="current-password"
required>
</div>
{# CSRF #}
<input type="hidden"
name="_csrf_token"
value="{{ csrf_token('authenticate') }}">
<button class="btn btn-outline-info w-100"
type="submit">
Einloggen
</button>
</form>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
</div>
<form method="post" action="{{ path('admin_login') }}">
<div class="mb-3">
<label class="form-label">E-Mail</label>
<input class="form-control" name="_username" value="{{ last_username }}" autocomplete="email" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input class="form-control" type="password" name="_password" autocomplete="current-password" required>
</div>
{# CSRF #}
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<button class="btn btn-light w-100" type="submit">Einloggen</button>
</form>
<div class="text-center mt-3 small text-secondary">
Zugriff nur für autorisierte Administratoren.
</div>
</div>
</div>
{% endblock %}

View File

@@ -4,51 +4,64 @@
{% block body %}
<a href="{{ path('admin_dashboard') }}"
class="btn btn-sm btn-outline-light mb-3">
← Zurück
</a>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Wissensdaten (Chunk-Index)</h1>
<h1 class="h4 mb-4">Wissensdaten (Chunk-Index)</h1>
<a href="{{ path('admin_dashboard') }}"
class="btn btn-sm btn-outline-secondary">
Zurück zum Dashboard
</a>
</div>
{# ============================= #}
{# Index Meta Section #}
{# ============================= #}
<div class="card bg-black text-info border-secondary mb-4"{#>
<div class="card bg-black border-secondary mb-5">
<div class="card-body">
<h5 class="mb-3">Index Meta (index_meta.json)</h5>
<h5 class="text-info mb-3">Index Meta (index_meta.json)</h5>
{% if meta.error is defined %}
<div class="alert alert-danger">
<strong>Fehler:</strong><br>
{{ meta.error }}<br>
<small>{{ meta.path }}</small>
<small class="text-muted">{{ meta.path }}</small>
</div>
{% else %}
<table class="table table-dark table-sm table-bordered align-middle mb-0">
<tbody>
{% for key, value in meta %}
<tr>
<th style="width:280px;">{{ key }}</th>
<td>
{% if value is iterable %}
<pre class="mb-0 text-info">
<div class="table-responsive">
<table class="table table-dark table-striped align-middle mb-0">
<tbody>
{% for key, value in meta %}
<tr>
<th style="width:260px;" class="text-secondary">
{{ key }}
</th>
<td>
{% if value is iterable %}
<pre class="mb-0 small text-info">
{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
</pre>
{% else %}
{{ value }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% else %}
{{ value }}
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="text-secondary">
Keine Meta-Daten vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>#}
</div>
{# ============================= #}
{# NDJSON Section #}
@@ -57,19 +70,27 @@
{% set currentPage = ndjson.page|default(1) %}
{% set currentLimit = ndjson.limit|default(50) %}
<div class="card bg-black text-info border-secondary mb-4">
<div class="card bg-black border-secondary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">NdJson Index Übersicht Chunks (index.ndjson)</h5>
<h5 class="text-info mb-0">
NDJSON-Index Übersicht (index.ndjson)
</h5>
<div>
<a href="{{ path('admin_system_agent', {page: (currentPage - 1 < 1 ? 1 : currentPage - 1), limit: currentLimit}) }}"
<div class="btn-group">
<a href="{{ path('admin_system_agent', {
page: currentPage > 1 ? currentPage - 1 : 1,
limit: currentLimit
}) }}"
class="btn btn-sm btn-outline-light">
← Zurück
</a>
<a href="{{ path('admin_system_agent', {page: currentPage + 1, limit: currentLimit}) }}"
<a href="{{ path('admin_system_agent', {
page: currentPage + 1,
limit: currentLimit
}) }}"
class="btn btn-sm btn-outline-light">
Weiter →
</a>
@@ -84,50 +105,74 @@
</div>
{% endif %}
<div class="mb-2 text-secondary">
Datei vorhanden: {{ ndjson.path ? 'JA' : 'NEIN' }} |
Geladene Einträge: {{ debugCount|default(0) }} |
Seite {{ currentPage }} • Limit {{ currentLimit }}
<div class="mb-3 small text-secondary">
Datei:
{% if ndjson.path %}
<span class="badge bg-success">Vorhanden</span>
{% else %}
<span class="badge bg-danger">Nicht gefunden</span>
{% endif %}
• Geladene Einträge: {{ debugCount|default(0) }}
• Seite {{ currentPage }}
• Limit {{ currentLimit }}
</div>
<div class="table-responsive">
<table class="table table-dark table-sm table-bordered align-middle">
<thead>
<table class="table table-dark table-striped table-hover align-middle">
<thead class="table-secondary text-dark">
<tr>
<th style="width:220px;">chunk_id</th>
<th style="width:200px;">chunk_id</th>
<th style="width:180px;">document_id</th>
<th>Text (gekürzt)</th>
</tr>
</thead>
<tbody>
{% for item in ndjson.items|default([]) %}
<tr>
<td>{{ item.chunk_id ?? '-' }}</td>
<td> <a href="{{ path('admin_document_show', {id: item.document_id}) }}"
class="text-decoration-none text-light">
{{ item.document_id ?? '-' }}
</a></td>
<td class="small">{{ item.chunk_id ?? '-' }}</td>
<td>
{% if item.document_id %}
<a href="{{ path('admin_document_show', {id: item.document_id}) }}"
class="text-decoration-none text-light">
{{ item.document_id }}
</a>
{% else %}
-
{% endif %}
</td>
<td>
{% set text = item.text ?? '' %}
{{ text|slice(0, 240) }}{% if text|length > 240 %}{% endif %}
<div class="small">
{{ text|slice(0, 240) }}
{% if text|length > 240 %}{% endif %}
</div>
<details class="mt-2">
<summary class="text-secondary" style="cursor:pointer;">
<summary class="text-secondary small">
JSON anzeigen
</summary>
<pre class="bg-dark text-info p-2 border border-secondary rounded mt-2">
<pre class="bg-dark text-info p-2 border border-secondary rounded mt-2 small">
{{ item|json_encode(constant('JSON_PRETTY_PRINT')) }}
</pre>
</pre>
</details>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="text-secondary">
<td colspan="3" class="text-center text-secondary py-4">
Keine Einträge gefunden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@@ -135,4 +180,10 @@
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Änderungen am NDJSON-Index oder an der Indexstruktur können
inkonsistente Retrieval-Ergebnisse verursachen, bis ein vollständiger
Reindex durchgeführt wurde.
</div>
{% endblock %}

View File

@@ -4,7 +4,13 @@
{% block body %}
<h1 class="h4 mb-4">System Prompt</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">System Prompt</h1>
</div>
{# ============================= #}
{# Flash Messages #}
{# ============================= #}
{% for message in app.flashes('success') %}
<div class="alert alert-success">{{ message }}</div>
@@ -13,92 +19,154 @@
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
<div class="card bg-black text-info border-secondary mb-4">
{# ============================= #}
{# Neue Version erstellen #}
{# ============================= #}
<div class="card bg-black border-secondary mb-5 text-light">
<div class="card-body">
<h5 class="text-info mb-3">Neue Version erstellen</h5>
<form method="post">
<input type="hidden"
name="_token"
value="{{ csrf_token('create_system_prompt') }}">
<div class="mb-3">
<label class="form-label">Kommentar (optional)</label>
<input type="text"
name="comment"
class="form-control bg-dark text-info border-secondary"
class="form-control bg-dark text-light border-secondary"
placeholder="Warum wurde der Prompt geändert?">
<div class="form-text text-secondary">
Dokumentation der Änderung für spätere Nachvollziehbarkeit.
</div>
</div>
<div class="mb-3">
<label class="form-label">Prompt Inhalt | Variablen: {% verbatim %}{% now %}{% endverbatim %} = Datum/Zeit</label>
<label class="form-label">
Prompt-Inhalt
</label>
<div class="form-text text-secondary mb-2">
Verfügbare Variable:
<code>{% verbatim %}{% now %}{% endverbatim %}</code>
(aktuelles Datum/Zeit)
</div>
<textarea name="content"
rows="16"
class="form-control bg-dark text-info border-secondary"
>{{ active ? active.content : '' }}</textarea>
class="form-control bg-dark text-light border-secondary"
required>{{ active ? active.content : '' }}</textarea>
</div>
<button class="btn btn-outline-light">
Neue Version speichern
</button>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<div class="d-flex justify-content-end">
<button class="btn btn-outline-info">
Neue Version speichern
</button>
</div>
{% endif %}
</form>
</div>
</div>
<div class="card bg-black text-info border-secondary">
{# ============================= #}
{# Versionen Übersicht #}
{# ============================= #}
<div class="card bg-black border-secondary">
<div class="card-body">
<h5>Versionen</h5>
<h5 class="text-info mb-3">Versionen</h5>
<table class="table table-dark table-sm table-bordered align-middle">
<thead>
<table class="table table-dark table-striped table-hover align-middle">
<thead class="table-secondary text-dark">
<tr>
<th>Version</th>
<th>Aktiv</th>
<th>Status</th>
<th>Kommentar</th>
<th>Erstellt</th>
<th>Aktionen</th>
<th class="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{% for p in all %}
<tr>
<td>{{ p.version }}</td>
<td>v{{ p.version }}</td>
<td>
{% if p.active %}
<span class="badge bg-success">ACTIVE</span>
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-dark border border-secondary">
Inaktiv
</span>
{% endif %}
</td>
<td>{{ p.comment ?? '-' }}</td>
<td>{{ p.createdAt|date('d.m.Y H:i:s') }}</td>
<td>
{% if not p.active %}
<td>{{ p.comment ?? '-' }}</td>
<td>{{ p.createdAt|date('d.m.Y H:i:s') }}</td>
<td class="text-end">
{% if not p.active and is_granted('ROLE_SUPER_ADMIN') %}
{# Aktivieren #}
<form method="post"
action="{{ path('admin_system_prompt_activate', {id: p.id}) }}"
style="display:inline-block;">
<button class="btn btn-sm btn-outline-light">
class="d-inline"
onsubmit="return confirm('Diese Version aktivieren?');">
<input type="hidden"
name="_token"
value="{{ csrf_token('activate_system_prompt_' ~ p.id) }}">
<button class="btn btn-sm btn-outline-success me-2">
Aktivieren
</button>
</form>
{# Löschen #}
<form method="post"
action="{{ path('admin_system_prompt_delete', {id: p.id}) }}"
style="display:inline-block;"
class="d-inline"
onsubmit="return confirm('Version wirklich löschen?');">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_system_prompt_' ~ p.id) }}">
<button class="btn btn-sm btn-outline-danger">
Löschen
</button>
</form>
{% else %}
-
<span class="text-secondary">—</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center text-secondary py-4">
Keine Versionen vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="mt-4 small text-secondary">
Hinweis: Der aktive System Prompt beeinflusst das Antwortverhalten
des LLM unmittelbar. Änderungen sollten dokumentiert und versioniert erfolgen.
</div>
{% endblock %}