1410 lines
50 KiB
HTML
1410 lines
50 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Belegscanner – PowerOn</title>
|
||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Poweron Design System - Light Theme */
|
||
:root {
|
||
--color-bg: #F8F9FA;
|
||
--color-surface: #ffffff;
|
||
--color-surface-elevated: #f5f5f5;
|
||
--color-text: #1a1a1a;
|
||
--color-text-secondary: #666666;
|
||
--color-text-muted: #888888;
|
||
|
||
--color-primary: #F25843;
|
||
--color-primary-hover: #D94A37;
|
||
--color-primary-disabled: #F5B0A4;
|
||
--color-primary-glow: rgba(242, 88, 67, 0.12);
|
||
|
||
--color-secondary: #F25843;
|
||
--color-secondary-hover: #FF6A55;
|
||
--color-secondary-disabled: #F5B0A4;
|
||
|
||
--color-border: #e0e0e0;
|
||
--color-border-hover: #d0d0d0;
|
||
|
||
--color-success: #22c55e;
|
||
--color-warning: #f59e0b;
|
||
--color-error: #dc2626;
|
||
|
||
--font-family: "DM Sans", "Trebuchet MS", sans-serif;
|
||
--font-mono: 'JetBrains Mono', monospace;
|
||
|
||
--radius-large: 30px;
|
||
--radius-medium: 15px;
|
||
--radius-small: 8px;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-family);
|
||
background: var(--color-bg);
|
||
color: var(--color-text);
|
||
min-height: 100vh;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.app-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 0 2rem 2rem 2rem;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Header - Sticky */
|
||
header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
padding: 1rem 2rem;
|
||
border-bottom: 1px solid var(--color-border);
|
||
backdrop-filter: blur(10px);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
header .app-container {
|
||
padding: 0;
|
||
min-height: auto;
|
||
}
|
||
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.logo-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.logo-icon img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.75rem;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.subtitle {
|
||
color: var(--color-text-secondary);
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
/* Grid Layout - 3 Spalten */
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(280px, 1fr) minmax(350px, 1.2fr) minmax(350px, 1.2fr);
|
||
gap: 1.5rem;
|
||
align-items: start;
|
||
padding-top: 1.5rem;
|
||
height: calc(100vh - 120px);
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.main-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
height: auto;
|
||
}
|
||
.upload-panel {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.main-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Panel */
|
||
.panel {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-medium);
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: calc(100vh - 140px);
|
||
}
|
||
|
||
.panel-header {
|
||
padding: 1rem 1.25rem;
|
||
border-bottom: 1px solid var(--color-border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 1.25rem;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Upload Panel - kompakt */
|
||
.upload-panel {
|
||
min-width: 200px;
|
||
}
|
||
|
||
.upload-panel .upload-zone {
|
||
padding: 1.5rem 1rem;
|
||
min-height: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.upload-panel .preview-image {
|
||
max-height: 350px;
|
||
}
|
||
|
||
/* Settings Panel */
|
||
.settings-panel .prompt-textarea {
|
||
min-height: 180px;
|
||
}
|
||
|
||
/* Settings Row */
|
||
.settings-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
padding-bottom: 0.75rem;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
.settings-row label {
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-secondary);
|
||
min-width: 55px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.settings-row input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 0.5rem 0.75rem;
|
||
background: var(--color-surface-elevated);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-small);
|
||
color: var(--color-text);
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.settings-row input:focus {
|
||
outline: none;
|
||
border-color: var(--color-primary);
|
||
box-shadow: 0 0 0 3px var(--color-primary-glow);
|
||
}
|
||
|
||
/* Upload Zone */
|
||
.upload-zone {
|
||
border: 2px dashed var(--color-border);
|
||
border-radius: var(--radius-medium);
|
||
padding: 3rem 2rem;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
background: var(--color-surface-elevated);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.upload-zone:hover {
|
||
border-color: var(--color-primary);
|
||
background: var(--color-primary-glow);
|
||
}
|
||
|
||
.upload-zone.dragover {
|
||
border-color: var(--color-primary);
|
||
background: var(--color-primary-glow);
|
||
transform: scale(1.01);
|
||
}
|
||
|
||
.upload-zone.has-image {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 3rem;
|
||
margin-bottom: 1rem;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.upload-text {
|
||
color: var(--color-text-secondary);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
#file-input {
|
||
display: none;
|
||
}
|
||
|
||
.preview-image {
|
||
max-width: 100%;
|
||
max-height: 400px;
|
||
border-radius: var(--radius-small);
|
||
object-fit: contain;
|
||
}
|
||
|
||
.image-actions {
|
||
margin-top: 1rem;
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
#pdf-page-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
margin-left: 1rem;
|
||
padding-left: 1rem;
|
||
border-left: 1px solid var(--color-border);
|
||
}
|
||
|
||
/* Prompt Section */
|
||
.prompt-section {
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
.prompt-label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.prompt-label span {
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.prompt-textarea {
|
||
width: 100%;
|
||
min-height: 140px;
|
||
padding: 1rem;
|
||
background: var(--color-surface-elevated);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-small);
|
||
color: var(--color-text);
|
||
font-family: var(--font-mono);
|
||
font-size: 0.85rem;
|
||
line-height: 1.6;
|
||
resize: vertical;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.prompt-textarea:focus {
|
||
outline: none;
|
||
border-color: var(--color-primary);
|
||
box-shadow: 0 0 0 3px var(--color-primary-glow);
|
||
}
|
||
|
||
/* Buttons - Poweron Style */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: var(--radius-large);
|
||
font-family: inherit;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: none;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--color-primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: var(--color-primary-hover);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-primary:disabled {
|
||
background: var(--color-primary-disabled);
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #EFEDE5;
|
||
color: var(--color-text);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #E0DDD3;
|
||
border-color: var(--color-border-hover);
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.action-bar {
|
||
margin-top: 1.5rem;
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Results Panel */
|
||
.results-panel {
|
||
position: sticky;
|
||
top: 2rem;
|
||
}
|
||
|
||
/* Status Badge */
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
padding: 0.35rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-idle {
|
||
background: var(--color-surface-elevated);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.status-processing {
|
||
background: rgba(245, 158, 11, 0.15);
|
||
color: var(--color-warning);
|
||
}
|
||
|
||
.status-success {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.status-error {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
color: var(--color-error);
|
||
}
|
||
|
||
.status-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
}
|
||
|
||
.status-processing .status-dot {
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.3; }
|
||
}
|
||
|
||
/* Form Placeholder */
|
||
.form-placeholder {
|
||
text-align: center;
|
||
padding: 4rem 2rem;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.form-placeholder-icon {
|
||
font-size: 3rem;
|
||
margin-bottom: 1rem;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
/* Form Fields */
|
||
.form-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.form-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.form-field label {
|
||
font-size: 0.8rem;
|
||
font-weight: 500;
|
||
color: var(--color-text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.form-field input,
|
||
.form-field textarea {
|
||
padding: 0.75rem 1rem;
|
||
background: var(--color-surface-elevated);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-small);
|
||
color: var(--color-text);
|
||
font-family: inherit;
|
||
font-size: 0.95rem;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.form-field input:focus,
|
||
.form-field textarea:focus {
|
||
outline: none;
|
||
border-color: var(--color-primary);
|
||
box-shadow: 0 0 0 3px var(--color-primary-glow);
|
||
}
|
||
|
||
.form-field textarea {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: 1.5rem;
|
||
padding-top: 1.5rem;
|
||
border-top: 1px solid var(--color-border);
|
||
display: flex;
|
||
gap: 1rem;
|
||
}
|
||
|
||
/* Spinner */
|
||
.spinner {
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2px solid transparent;
|
||
border-top-color: currentColor;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Error Message */
|
||
.error-message {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
border-radius: var(--radius-small);
|
||
padding: 1rem;
|
||
color: var(--color-error);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
/* Raw JSON Section */
|
||
.raw-json-section {
|
||
margin-top: 1.5rem;
|
||
padding-top: 1.5rem;
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
.raw-json-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-muted);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.raw-json-toggle:hover {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.raw-json-content {
|
||
margin-top: 1rem;
|
||
padding: 1rem;
|
||
background: var(--color-surface-elevated);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-small);
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem;
|
||
overflow-x: auto;
|
||
white-space: pre-wrap;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
/* Ollama Status */
|
||
.ollama-status {
|
||
padding: 0.75rem 1rem;
|
||
border-radius: var(--radius-small);
|
||
font-size: 0.85rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.ollama-status.success {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
color: #16a34a;
|
||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
.ollama-status.error {
|
||
background: rgba(220, 38, 38, 0.1);
|
||
color: var(--color-error);
|
||
border: 1px solid rgba(220, 38, 38, 0.3);
|
||
}
|
||
|
||
.ollama-status.loading {
|
||
background: rgba(242, 88, 67, 0.1);
|
||
color: var(--color-primary);
|
||
border: 1px solid rgba(242, 88, 67, 0.3);
|
||
}
|
||
|
||
/* Footer Branding */
|
||
.footer-brand {
|
||
margin-top: 1.5rem;
|
||
padding: 1rem;
|
||
text-align: center;
|
||
color: var(--color-text-muted);
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.footer-brand a {
|
||
color: var(--color-primary);
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.footer-brand a:hover {
|
||
color: var(--color-primary-hover);
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="app-container" style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<div class="logo">
|
||
<div class="logo-icon">
|
||
<img src="/static/poweron-logo.png" alt="Poweron">
|
||
</div>
|
||
<h1>Belegscanner</h1>
|
||
</div>
|
||
<p class="subtitle">KI-gestützte Dokumentenanalyse</p>
|
||
</div>
|
||
<a href="/logout" class="btn btn-secondary btn-small" style="text-decoration: none;">Abmelden</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="app-container">
|
||
|
||
<div class="main-grid">
|
||
<!-- Panel 1: Upload/Preview -->
|
||
<div class="panel upload-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Dokument</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="upload-zone" id="upload-zone">
|
||
<div class="upload-placeholder" id="upload-placeholder">
|
||
<div class="upload-icon">📤</div>
|
||
<p class="upload-text">Bild hochladen</p>
|
||
<p class="upload-hint">Drag & Drop</p>
|
||
</div>
|
||
<img id="preview-image" class="preview-image" style="display: none;">
|
||
<input type="file" id="file-input" accept="image/*">
|
||
</div>
|
||
<div class="image-actions" id="image-actions" style="display: none;">
|
||
<button class="btn btn-secondary btn-small" id="remove-image">✕</button>
|
||
<div id="pdf-page-selector" style="display: none;">
|
||
<button class="btn btn-secondary btn-small" id="prev-page" disabled>◀</button>
|
||
<span id="page-info" style="font-size: 0.8rem;">1/1</span>
|
||
<button class="btn btn-secondary btn-small" id="next-page" disabled>▶</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel 2: Settings & Prompt -->
|
||
<div class="panel settings-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Einstellungen</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="settings-row">
|
||
<label>Server:</label>
|
||
<input type="text" id="ollama-url" value="http://83.228.200.109:11434" placeholder="http://83.228.200.109:11434">
|
||
<button class="btn btn-secondary btn-small" id="check-ollama">Prüfen</button>
|
||
</div>
|
||
<div class="settings-row">
|
||
<label>Modell:</label>
|
||
<select id="model-name" style="flex:1; padding:0.6rem; background:var(--color-surface-elevated); border:1px solid var(--color-border); border-radius:var(--radius-small); color:var(--color-text); font-family:var(--font-mono); font-size:0.85rem;">
|
||
<option value="">-- Modell wählen --</option>
|
||
</select>
|
||
</div>
|
||
<div id="ollama-status" class="ollama-status" style="display:none;"></div>
|
||
|
||
<div class="prompt-section" style="margin-top: 1rem;">
|
||
<div class="prompt-label">
|
||
<span>Prompt</span>
|
||
<button class="btn btn-secondary btn-small" id="reset-prompt">Reset</button>
|
||
</div>
|
||
<textarea class="prompt-textarea" id="prompt-input">Analysiere diesen Beleg/diese Rechnung und extrahiere alle relevanten Daten.
|
||
|
||
Antworte NUR mit einem validen JSON-Objekt im folgenden Format:
|
||
{
|
||
"haendler": "Name des Geschäfts/Händlers",
|
||
"datum": "Datum im Format TT.MM.JJJJ",
|
||
"betrag_brutto": "Gesamtbetrag inkl. MwSt",
|
||
"betrag_netto": "Nettobetrag ohne MwSt (falls vorhanden)",
|
||
"mwst_betrag": "MwSt-Betrag (falls vorhanden)",
|
||
"mwst_satz": "MwSt-Satz in Prozent (falls vorhanden)",
|
||
"waehrung": "EUR, CHF, USD etc.",
|
||
"kategorie": "z.B. Verpflegung, Transport, Unterkunft, Büromaterial",
|
||
"zahlungsart": "Bar, Karte, Rechnung etc. (falls erkennbar)",
|
||
"rechnungsnummer": "Falls vorhanden",
|
||
"positionen": ["Liste der einzelnen Positionen falls erkennbar"],
|
||
"notizen": "Alle weiteren Informationen aus dem Dokument hier erfassen"
|
||
}
|
||
|
||
Falls ein Feld nicht erkennbar ist, setze den Wert auf null.</textarea>
|
||
</div>
|
||
|
||
<div class="action-bar">
|
||
<button class="btn btn-primary" id="analyze-btn" disabled>
|
||
<span id="btn-text">Analysieren</span>
|
||
<div class="spinner" id="btn-spinner" style="display: none;"></div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Panel: Results -->
|
||
<div class="panel results-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Extrahierte Daten</span>
|
||
<span class="status-badge status-idle" id="status-badge">
|
||
<span class="status-dot"></span>
|
||
<span id="status-text">Warte auf Eingabe</span>
|
||
</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div id="results-container">
|
||
<div class="form-placeholder" id="form-placeholder">
|
||
<div class="form-placeholder-icon">🔍</div>
|
||
<p>Laden Sie ein Dokument hoch und klicken Sie auf "Analysieren"</p>
|
||
</div>
|
||
<div id="error-container" style="display: none;"></div>
|
||
<div id="form-container" style="display: none;">
|
||
<!-- Dynamisch generierte Felder -->
|
||
<div id="dynamic-fields" class="form-grid"></div>
|
||
|
||
<div class="form-actions">
|
||
<button class="btn btn-primary" id="copy-json">📋 JSON kopieren</button>
|
||
<button class="btn btn-secondary" id="download-json">💾 JSON speichern</button>
|
||
</div>
|
||
|
||
<div class="raw-json-section">
|
||
<div class="raw-json-toggle" id="toggle-raw">
|
||
<span>▶</span>
|
||
<span>Rohe JSON-Antwort anzeigen</span>
|
||
</div>
|
||
<div class="raw-json-content" id="raw-json" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer-brand">
|
||
Powered by <a href="#">Poweron</a> • KI-Dokumentenanalyse
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// State
|
||
let currentImage = null;
|
||
let currentImageBase64 = null;
|
||
let extractedData = null;
|
||
|
||
const DEFAULT_PROMPT = document.getElementById('prompt-input').value;
|
||
|
||
// Elements
|
||
const uploadZone = document.getElementById('upload-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
const previewImage = document.getElementById('preview-image');
|
||
const uploadPlaceholder = document.getElementById('upload-placeholder');
|
||
const imageActions = document.getElementById('image-actions');
|
||
const removeImageBtn = document.getElementById('remove-image');
|
||
const promptInput = document.getElementById('prompt-input');
|
||
const resetPromptBtn = document.getElementById('reset-prompt');
|
||
const analyzeBtn = document.getElementById('analyze-btn');
|
||
const btnText = document.getElementById('btn-text');
|
||
const btnSpinner = document.getElementById('btn-spinner');
|
||
const statusBadge = document.getElementById('status-badge');
|
||
const statusText = document.getElementById('status-text');
|
||
const formPlaceholder = document.getElementById('form-placeholder');
|
||
const formContainer = document.getElementById('form-container');
|
||
const errorContainer = document.getElementById('error-container');
|
||
const ollamaUrl = document.getElementById('ollama-url');
|
||
const modelName = document.getElementById('model-name');
|
||
const toggleRaw = document.getElementById('toggle-raw');
|
||
const rawJson = document.getElementById('raw-json');
|
||
const copyJsonBtn = document.getElementById('copy-json');
|
||
const downloadJsonBtn = document.getElementById('download-json');
|
||
const checkOllamaBtn = document.getElementById('check-ollama');
|
||
const ollamaStatusDiv = document.getElementById('ollama-status');
|
||
|
||
// Ollama Status prüfen
|
||
checkOllamaBtn.addEventListener('click', _checkOllamaStatus);
|
||
|
||
// PowerOn Model Definitions (must match app.py MODEL_MAPPING)
|
||
const POWERON_MODELS = [
|
||
{
|
||
name: 'poweron-vision-general',
|
||
displayName: 'PowerOn Vision General',
|
||
description: 'Handschrift & allgemeine Bilder (qwen2.5vl:7b)',
|
||
isVision: true,
|
||
ollamaModel: 'qwen2.5vl:7b'
|
||
},
|
||
{
|
||
name: 'poweron-vision-deep',
|
||
displayName: 'PowerOn Vision Deep',
|
||
description: 'Rechnungen, Belege, Dokumente (granite3.2-vision)',
|
||
isVision: true,
|
||
ollamaModel: 'granite3.2-vision'
|
||
},
|
||
{
|
||
name: 'poweron-text-general',
|
||
displayName: 'PowerOn Text General',
|
||
description: 'Text-Verarbeitung / Analyse (qwen2.5:7b)',
|
||
isVision: false, // Text model - no image required
|
||
ollamaModel: 'qwen2.5:7b'
|
||
}
|
||
];
|
||
|
||
async function _checkOllamaStatus() {
|
||
ollamaStatusDiv.style.display = 'block';
|
||
ollamaStatusDiv.className = 'ollama-status loading';
|
||
ollamaStatusDiv.textContent = 'Prüfe Ollama-Verbindung...';
|
||
|
||
try {
|
||
// Direkt Ollama API abfragen (ohne Auth)
|
||
const response = await fetch(`${ollamaUrl.value}/api/tags`, {
|
||
method: 'GET',
|
||
headers: { 'Accept': 'application/json' }
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Ollama antwortet mit Status ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
const availableModels = (result.models || []).map(m => m.name);
|
||
|
||
console.log('Available Ollama models:', availableModels);
|
||
|
||
ollamaStatusDiv.className = 'ollama-status success';
|
||
|
||
// PowerOn Modelle in Dropdown laden (nur wenn Backend-Modell verfügbar)
|
||
modelName.innerHTML = '';
|
||
|
||
// Flexibler Modell-Match: prüft ob Ollama-Modell mit PowerOn-Modell beginnt oder umgekehrt
|
||
const availablePowerOnModels = POWERON_MODELS.filter(pm => {
|
||
const ollamaBase = pm.ollamaModel.split(':')[0];
|
||
return availableModels.some(m =>
|
||
m.startsWith(ollamaBase) ||
|
||
m.split(':')[0] === ollamaBase ||
|
||
ollamaBase.startsWith(m.split(':')[0])
|
||
);
|
||
});
|
||
|
||
console.log('Matched PowerOn models:', availablePowerOnModels.map(m => m.name));
|
||
|
||
if (availablePowerOnModels.length > 0) {
|
||
const optGroup = document.createElement('optgroup');
|
||
optGroup.label = 'PowerOn Modelle';
|
||
availablePowerOnModels.forEach(model => {
|
||
const opt = document.createElement('option');
|
||
opt.value = model.name;
|
||
opt.textContent = `${model.displayName}`;
|
||
opt.title = model.description;
|
||
optGroup.appendChild(opt);
|
||
});
|
||
modelName.appendChild(optGroup);
|
||
|
||
// Erstes Modell auswählen
|
||
modelName.value = availablePowerOnModels[0].name;
|
||
} else {
|
||
// Fallback: Alle PowerOn Modelle anzeigen (ohne Verfügbarkeitsprüfung)
|
||
console.log('No matches found, showing all PowerOn models');
|
||
const optGroup = document.createElement('optgroup');
|
||
optGroup.label = 'PowerOn Modelle';
|
||
POWERON_MODELS.forEach(model => {
|
||
const opt = document.createElement('option');
|
||
opt.value = model.name;
|
||
opt.textContent = `${model.displayName}`;
|
||
opt.title = model.description;
|
||
optGroup.appendChild(opt);
|
||
});
|
||
modelName.appendChild(optGroup);
|
||
modelName.value = POWERON_MODELS[0].name;
|
||
}
|
||
|
||
ollamaStatusDiv.innerHTML = `✓ Verbunden - ${availablePowerOnModels.length || POWERON_MODELS.length} PowerOn Modelle verfügbar`;
|
||
|
||
// Button-Status nach Modell-Laden aktualisieren
|
||
_updateAnalyzeButtonState();
|
||
} catch (error) {
|
||
ollamaStatusDiv.className = 'ollama-status error';
|
||
ollamaStatusDiv.textContent = `✗ Keine Verbindung zu Ollama: ${error.message}`;
|
||
modelName.innerHTML = '<option value="">-- Ollama nicht verbunden --</option>';
|
||
}
|
||
}
|
||
|
||
// Helper: Prüft ob Modell ein Vision-Modell ist
|
||
function _isVisionModel(model) {
|
||
if (!model) return true; // Default: als Vision behandeln
|
||
// Check PowerOn models first
|
||
const powerOnModel = POWERON_MODELS.find(pm => pm.name === model);
|
||
if (powerOnModel) return powerOnModel.isVision;
|
||
// Fallback for direct Ollama model names
|
||
const modelLower = model.toLowerCase();
|
||
return ['vision', 'vl', 'llava', 'bakllava', 'granite', 'deepseek-ocr'].some(indicator => modelLower.includes(indicator));
|
||
}
|
||
|
||
// Button-Status basierend auf Modell und Bild aktualisieren
|
||
function _updateAnalyzeButtonState() {
|
||
const isVision = _isVisionModel(modelName.value);
|
||
const hasImage = !!currentImageBase64;
|
||
const hasPrompt = promptInput.value.trim().length > 0;
|
||
|
||
// Vision-Modelle brauchen Bild, Non-Vision nicht
|
||
if (isVision) {
|
||
analyzeBtn.disabled = !hasImage;
|
||
} else {
|
||
analyzeBtn.disabled = !hasPrompt;
|
||
}
|
||
}
|
||
|
||
// Bei Modell-Wechsel Button-Status aktualisieren
|
||
modelName.addEventListener('change', _updateAnalyzeButtonState);
|
||
|
||
// Bei Prompt-Änderung Button-Status aktualisieren (für Non-Vision Modelle)
|
||
promptInput.addEventListener('input', _updateAnalyzeButtonState);
|
||
|
||
// Beim Laden automatisch prüfen
|
||
window.addEventListener('load', () => {
|
||
setTimeout(_checkOllamaStatus, 500);
|
||
});
|
||
|
||
// Upload handling
|
||
uploadZone.addEventListener('click', () => fileInput.click());
|
||
|
||
uploadZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadZone.classList.add('dragover');
|
||
});
|
||
|
||
uploadZone.addEventListener('dragleave', () => {
|
||
uploadZone.classList.remove('dragover');
|
||
});
|
||
|
||
uploadZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadZone.classList.remove('dragover');
|
||
const file = e.dataTransfer.files[0];
|
||
if (file && (file.type.startsWith('image/') || file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))) {
|
||
_handleFile(file);
|
||
}
|
||
});
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) _handleFile(file);
|
||
});
|
||
|
||
// PDF State
|
||
let pdfData = null;
|
||
let currentPdfPage = 1;
|
||
let totalPdfPages = 1;
|
||
|
||
const pdfPageSelector = document.getElementById('pdf-page-selector');
|
||
const prevPageBtn = document.getElementById('prev-page');
|
||
const nextPageBtn = document.getElementById('next-page');
|
||
const pageInfo = document.getElementById('page-info');
|
||
|
||
async function _handleFile(file) {
|
||
currentImage = file;
|
||
|
||
// PDF erkennen
|
||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||
await _handlePdfFile(file);
|
||
} else {
|
||
// Normales Bild
|
||
_handleImageFile(file);
|
||
}
|
||
}
|
||
|
||
function _handleImageFile(file) {
|
||
pdfData = null;
|
||
pdfPageSelector.style.display = 'none';
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
currentImageBase64 = e.target.result.split(',')[1];
|
||
previewImage.src = e.target.result;
|
||
previewImage.style.display = 'block';
|
||
uploadPlaceholder.style.display = 'none';
|
||
uploadZone.classList.add('has-image');
|
||
imageActions.style.display = 'flex';
|
||
_updateAnalyzeButtonState();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
async function _handlePdfFile(file) {
|
||
uploadPlaceholder.innerHTML = '<div class="upload-icon">⏳</div><p class="upload-text">PDF wird verarbeitet...</p>';
|
||
|
||
try {
|
||
const reader = new FileReader();
|
||
const pdfBase64 = await new Promise((resolve, reject) => {
|
||
reader.onload = (e) => resolve(e.target.result.split(',')[1]);
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(file);
|
||
});
|
||
|
||
// PDF an Backend senden zur Bildextraktion
|
||
const response = await fetch('/api/pdf/extract', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pdfBase64, page: 1 })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(result.error || 'PDF-Verarbeitung fehlgeschlagen');
|
||
}
|
||
|
||
// PDF-Daten speichern
|
||
pdfData = pdfBase64;
|
||
currentPdfPage = 1;
|
||
totalPdfPages = result.image.totalPages;
|
||
|
||
// Bild anzeigen
|
||
currentImageBase64 = result.image.base64;
|
||
previewImage.src = 'data:image/png;base64,' + result.image.base64;
|
||
previewImage.style.display = 'block';
|
||
uploadPlaceholder.style.display = 'none';
|
||
uploadZone.classList.add('has-image');
|
||
imageActions.style.display = 'flex';
|
||
_updateAnalyzeButtonState();
|
||
|
||
// Page Selector anzeigen wenn mehrere Seiten
|
||
if (totalPdfPages > 1) {
|
||
pdfPageSelector.style.display = 'flex';
|
||
_updatePageSelector();
|
||
} else {
|
||
pdfPageSelector.style.display = 'none';
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('PDF Error:', error);
|
||
uploadPlaceholder.innerHTML = `<div class="upload-icon">❌</div><p class="upload-text">PDF-Fehler: ${error.message}</p><p class="upload-hint">Bitte versuchen Sie es mit einem anderen PDF</p>`;
|
||
uploadPlaceholder.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function _updatePageSelector() {
|
||
pageInfo.textContent = `Seite ${currentPdfPage}/${totalPdfPages}`;
|
||
prevPageBtn.disabled = currentPdfPage <= 1;
|
||
nextPageBtn.disabled = currentPdfPage >= totalPdfPages;
|
||
}
|
||
|
||
prevPageBtn.addEventListener('click', () => _changePdfPage(-1));
|
||
nextPageBtn.addEventListener('click', () => _changePdfPage(1));
|
||
|
||
async function _changePdfPage(delta) {
|
||
const newPage = currentPdfPage + delta;
|
||
if (newPage < 1 || newPage > totalPdfPages || !pdfData) return;
|
||
|
||
prevPageBtn.disabled = true;
|
||
nextPageBtn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch('/api/pdf/extract', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pdfBase64: pdfData, page: newPage })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.success) {
|
||
currentPdfPage = newPage;
|
||
currentImageBase64 = result.image.base64;
|
||
previewImage.src = 'data:image/png;base64,' + result.image.base64;
|
||
}
|
||
} catch (error) {
|
||
console.error('Page change error:', error);
|
||
}
|
||
|
||
_updatePageSelector();
|
||
}
|
||
|
||
removeImageBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
_resetImage();
|
||
});
|
||
|
||
function _resetImage() {
|
||
currentImage = null;
|
||
currentImageBase64 = null;
|
||
pdfData = null;
|
||
currentPdfPage = 1;
|
||
totalPdfPages = 1;
|
||
previewImage.src = '';
|
||
previewImage.style.display = 'none';
|
||
uploadPlaceholder.innerHTML = '<div class="upload-icon">📤</div><p class="upload-text">Bild oder PDF</p><p class="upload-hint">Drag & Drop</p>';
|
||
uploadPlaceholder.style.display = 'block';
|
||
uploadZone.classList.remove('has-image');
|
||
imageActions.style.display = 'none';
|
||
pdfPageSelector.style.display = 'none';
|
||
fileInput.value = '';
|
||
_updateAnalyzeButtonState();
|
||
}
|
||
|
||
resetPromptBtn.addEventListener('click', () => {
|
||
promptInput.value = DEFAULT_PROMPT;
|
||
});
|
||
|
||
// Analysis - direct Ollama API call
|
||
analyzeBtn.addEventListener('click', _analyzeDocument);
|
||
|
||
// Map PowerOn model names to Ollama model names
|
||
function _getOllamaModelName(powerOnName) {
|
||
const model = POWERON_MODELS.find(m => m.name === powerOnName);
|
||
return model ? model.ollamaModel : powerOnName;
|
||
}
|
||
|
||
async function _analyzeDocument() {
|
||
const isVision = _isVisionModel(modelName.value);
|
||
|
||
// Vision-Modelle brauchen ein Bild
|
||
if (isVision && !currentImageBase64) {
|
||
_showError('Bitte laden Sie zuerst ein Bild oder PDF hoch (erforderlich für Vision-Modelle).');
|
||
return;
|
||
}
|
||
|
||
// Prüfen ob Modell gewählt
|
||
if (!modelName.value) {
|
||
_showError('Bitte wählen Sie zuerst ein Modell aus. Klicken Sie auf "Prüfen" um die verfügbaren Modelle zu laden.');
|
||
return;
|
||
}
|
||
|
||
// Prüfen ob Prompt vorhanden
|
||
if (!promptInput.value.trim()) {
|
||
_showError('Bitte geben Sie einen Prompt ein.');
|
||
return;
|
||
}
|
||
|
||
console.log('=== ANALYZE BUTTON CLICKED ===');
|
||
console.log('Selected model:', modelName.value);
|
||
console.log('Has image:', !!currentImageBase64);
|
||
console.log('Prompt length:', promptInput.value.length);
|
||
|
||
_setStatus('processing', 'Analysiere...');
|
||
_setLoading(true);
|
||
_hideError();
|
||
|
||
try {
|
||
// Get Ollama model name from PowerOn name
|
||
const ollamaModelName = _getOllamaModelName(modelName.value);
|
||
console.log('Ollama model name:', ollamaModelName);
|
||
|
||
// Model-specific context lengths
|
||
const modelContextLengths = {
|
||
'qwen2.5:7b': 32768,
|
||
'qwen2.5vl:7b': 32768,
|
||
'granite3.2-vision': 16000
|
||
};
|
||
const numCtx = modelContextLengths[ollamaModelName] || 8192;
|
||
|
||
// Request-Body für Ollama erstellen
|
||
const requestBody = {
|
||
model: ollamaModelName,
|
||
prompt: promptInput.value,
|
||
stream: false,
|
||
options: {
|
||
num_ctx: numCtx
|
||
}
|
||
};
|
||
|
||
console.log('Sending request to:', `${ollamaUrl.value}/api/generate`);
|
||
console.log('Request body (without prompt):', { ...requestBody, prompt: '[TRUNCATED]' });
|
||
|
||
// Bild nur hinzufügen wenn vorhanden
|
||
if (currentImageBase64) {
|
||
requestBody.images = [currentImageBase64];
|
||
}
|
||
|
||
// Call Ollama API directly
|
||
console.log('Fetching from Ollama...');
|
||
const response = await fetch(`${ollamaUrl.value}/api/generate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
console.log('Response status:', response.status);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('Ollama error:', errorText);
|
||
throw new Error(`Ollama Fehler: ${response.status} - ${errorText.substring(0, 200)}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('Ollama response received:', result);
|
||
const responseText = result.response || '';
|
||
|
||
// Try to extract JSON from response
|
||
let extractedData = null;
|
||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||
|
||
if (jsonMatch) {
|
||
try {
|
||
extractedData = JSON.parse(jsonMatch[0]);
|
||
} catch (e) {
|
||
extractedData = null;
|
||
}
|
||
}
|
||
|
||
// Wrap plain text response in JSON object
|
||
if (extractedData === null) {
|
||
extractedData = { response: responseText.trim() };
|
||
}
|
||
|
||
_displayResults(extractedData, responseText);
|
||
_setStatus('success', 'Erfolgreich extrahiert');
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
_showError(error.message);
|
||
_setStatus('error', 'Fehler');
|
||
} finally {
|
||
_setLoading(false);
|
||
}
|
||
}
|
||
|
||
function _setStatus(type, text) {
|
||
statusBadge.className = `status-badge status-${type}`;
|
||
statusText.textContent = text;
|
||
}
|
||
|
||
function _setLoading(loading) {
|
||
analyzeBtn.disabled = loading;
|
||
btnText.textContent = loading ? 'Analysiere...' : 'Dokument analysieren';
|
||
btnSpinner.style.display = loading ? 'block' : 'none';
|
||
}
|
||
|
||
function _showError(message) {
|
||
errorContainer.innerHTML = `<div class="error-message">❌ ${message}</div>`;
|
||
errorContainer.style.display = 'block';
|
||
formPlaceholder.style.display = 'none';
|
||
formContainer.style.display = 'none';
|
||
}
|
||
|
||
function _hideError() {
|
||
errorContainer.style.display = 'none';
|
||
}
|
||
|
||
function _displayResults(data, rawResponse) {
|
||
formPlaceholder.style.display = 'none';
|
||
errorContainer.style.display = 'none';
|
||
formContainer.style.display = 'block';
|
||
|
||
// Dynamische Felder Container
|
||
const dynamicFields = document.getElementById('dynamic-fields');
|
||
dynamicFields.innerHTML = '';
|
||
|
||
// Alle Keys aus dem JSON (Ebene 1) als Felder rendern
|
||
const keys = Object.keys(data);
|
||
|
||
// Felder in 2er-Reihen anordnen (ausser bei langen Werten)
|
||
let currentRow = null;
|
||
let fieldsInRow = 0;
|
||
|
||
keys.forEach((key, index) => {
|
||
const value = data[key];
|
||
const valueStr = _formatValue(value);
|
||
const isLongValue = valueStr.length > 50 || Array.isArray(value) || (typeof value === 'object' && value !== null);
|
||
|
||
// Neue Reihe starten wenn nötig
|
||
if (!currentRow || fieldsInRow >= 2 || isLongValue) {
|
||
currentRow = document.createElement('div');
|
||
currentRow.className = isLongValue ? 'form-field' : 'form-row';
|
||
dynamicFields.appendChild(currentRow);
|
||
fieldsInRow = 0;
|
||
}
|
||
|
||
// Feld-Container erstellen
|
||
const fieldDiv = document.createElement('div');
|
||
fieldDiv.className = 'form-field';
|
||
|
||
// Label erstellen (Key formatieren)
|
||
const label = document.createElement('label');
|
||
label.textContent = _formatLabel(key);
|
||
fieldDiv.appendChild(label);
|
||
|
||
// Input oder Textarea erstellen
|
||
if (isLongValue) {
|
||
const textarea = document.createElement('textarea');
|
||
textarea.id = `field-${key}`;
|
||
textarea.rows = Math.min(Math.max(3, valueStr.split('\n').length), 10);
|
||
textarea.value = valueStr;
|
||
fieldDiv.appendChild(textarea);
|
||
currentRow.appendChild(fieldDiv);
|
||
// Nach Textarea neue Reihe starten
|
||
currentRow = null;
|
||
fieldsInRow = 0;
|
||
} else {
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.id = `field-${key}`;
|
||
input.value = valueStr;
|
||
fieldDiv.appendChild(input);
|
||
|
||
if (currentRow.className === 'form-row') {
|
||
currentRow.appendChild(fieldDiv);
|
||
fieldsInRow++;
|
||
} else {
|
||
currentRow.appendChild(fieldDiv);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Raw JSON
|
||
rawJson.textContent = JSON.stringify(data, null, 2);
|
||
}
|
||
|
||
// Hilfsfunktion: Wert für Anzeige formatieren
|
||
function _formatValue(value) {
|
||
if (value === null || value === undefined) return '';
|
||
if (Array.isArray(value)) return value.join('\n');
|
||
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
||
return String(value);
|
||
}
|
||
|
||
// Hilfsfunktion: Key als Label formatieren (snake_case -> Title Case)
|
||
function _formatLabel(key) {
|
||
return key
|
||
.replace(/_/g, ' ')
|
||
.replace(/([A-Z])/g, ' $1')
|
||
.replace(/^./, str => str.toUpperCase())
|
||
.trim();
|
||
}
|
||
|
||
// Toggle raw JSON
|
||
let rawVisible = false;
|
||
toggleRaw.addEventListener('click', () => {
|
||
rawVisible = !rawVisible;
|
||
rawJson.style.display = rawVisible ? 'block' : 'none';
|
||
toggleRaw.querySelector('span:first-child').textContent = rawVisible ? '▼' : '▶';
|
||
});
|
||
|
||
// Get current form data (dynamisch aus allen Feldern)
|
||
function _getCurrentFormData() {
|
||
const data = {};
|
||
const dynamicFields = document.getElementById('dynamic-fields');
|
||
const inputs = dynamicFields.querySelectorAll('input, textarea');
|
||
|
||
inputs.forEach(input => {
|
||
// Key aus ID extrahieren (field-xxx -> xxx)
|
||
const key = input.id.replace('field-', '');
|
||
const value = input.value.trim();
|
||
|
||
// Mehrzeilige Werte als Array speichern
|
||
if (input.tagName === 'TEXTAREA' && value.includes('\n')) {
|
||
data[key] = value.split('\n').filter(line => line.trim());
|
||
} else {
|
||
data[key] = value || null;
|
||
}
|
||
});
|
||
|
||
return data;
|
||
}
|
||
|
||
// Copy JSON
|
||
copyJsonBtn.addEventListener('click', () => {
|
||
const data = _getCurrentFormData();
|
||
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
||
copyJsonBtn.textContent = '✓ Kopiert!';
|
||
setTimeout(() => copyJsonBtn.textContent = '📋 JSON kopieren', 2000);
|
||
});
|
||
|
||
// Download JSON
|
||
downloadJsonBtn.addEventListener('click', () => {
|
||
const data = _getCurrentFormData();
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
const timestamp = new Date().toISOString().slice(0, 10);
|
||
a.download = `result-${timestamp}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|