feat: Add .env.example configuration file and integrate Redis service into Docker Compose for improved task management. Update desktop application to support document indexing and extraction workflows, enhancing user experience with a new extraction wizard and stepper components. Revise README and documentation for clarity on setup and usage.

This commit is contained in:
erik
2026-07-05 22:39:07 -05:00
parent 9aca926988
commit d95f8f6ad8
70 changed files with 9807 additions and 499 deletions
@@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Services Count — Demo</title>
<style>
:root {
--bg: #f4f6f8;
--surface: #ffffff;
--text: #1a2332;
--muted: #5c6b7a;
--border: #d8dee6;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--error-bg: #fef2f2;
--error-text: #b91c1c;
--ok: #15803d;
--warn: #b45309;
--radius: 8px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
}
header {
margin-bottom: 1.5rem;
}
header h1 {
margin: 0 0 0.25rem;
font-size: 1.75rem;
font-weight: 700;
}
header p {
margin: 0;
color: var(--muted);
}
header a {
color: var(--primary);
text-decoration: none;
}
header a:hover { text-decoration: underline; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem;
margin-bottom: 1rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
font-weight: 600;
margin-bottom: 0.35rem;
font-size: 0.9rem;
}
input[type="text"],
input[type="file"] {
width: 100%;
padding: 0.55rem 0.65rem;
border: 1px solid var(--border);
border-radius: var(--radius);
font: inherit;
}
.checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
}
.checkbox input { width: auto; }
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
flex: 1;
padding: 0.65rem 1rem;
border: 1px solid var(--border);
background: var(--surface);
border-radius: var(--radius);
cursor: pointer;
font: inherit;
font-weight: 600;
color: var(--muted);
}
.tab.active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.panel { display: none; }
.panel.active { display: block; }
.btn {
display: inline-block;
padding: 0.6rem 1.25rem;
background: var(--primary);
color: #fff;
border: none;
border-radius: var(--radius);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.btn:hover:not(:disabled) { background: var(--primary-hover); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.error {
background: var(--error-bg);
color: var(--error-text);
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
font-size: 0.9rem;
}
.hint {
font-size: 0.85rem;
color: var(--muted);
margin-top: 0.35rem;
}
progress {
width: 100%;
height: 0.65rem;
margin: 0.75rem 0 0.25rem;
}
.progress-label {
font-size: 0.85rem;
color: var(--muted);
}
.results { margin-top: 1.25rem; }
.result-card {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 0.75rem;
background: #fafbfc;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.result-header h3 {
margin: 0;
font-size: 1rem;
word-break: break-all;
}
.badge {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: #e2e8f0;
flex-shrink: 0;
}
.badge-ok { background: #dcfce7; color: var(--ok); }
.badge-error { background: #fee2e2; color: var(--error-text); }
.badge-pending { background: #fef3c7; color: var(--warn); }
.result-meta {
margin: 0 0 0.75rem;
font-weight: 600;
}
.result-error {
margin: 0;
color: var(--error-text);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th, td {
text-align: left;
padding: 0.45rem 0.5rem;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
th {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted);
}
.empty {
color: var(--muted);
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Services Count — Demo</h1>
<p>
Cliente de ejemplo para la API de conteo de servicios en facturas médicas.
<a href="/openapi.json" target="_blank" rel="noopener">OpenAPI</a>
</p>
</header>
<div class="card">
<div class="field">
<label for="baseUrl">URL base de la API</label>
<input type="text" id="baseUrl" placeholder="http://localhost:8080">
<p class="hint">Por defecto usa el mismo origen desde el que se sirve esta página.</p>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" id="includeRejected">
Incluir filas rechazadas (<code>include_rejected</code>)
</label>
</div>
</div>
<div class="tabs">
<button type="button" class="tab active" data-panel="syncPanel">Síncrono</button>
<button type="button" class="tab" data-panel="asyncPanel">Asíncrono</button>
</div>
<div id="syncPanel" class="panel active">
<div class="card">
<form id="syncForm">
<div class="field">
<label for="syncFiles">Archivos PDF (máx. 50)</label>
<input type="file" id="syncFiles" accept=".pdf,application/pdf" multiple required>
<p class="hint">Envía una petición <code>POST /v1/count</code> y muestra la respuesta al instante.</p>
</div>
<div id="syncError" class="error" hidden></div>
<button type="submit" class="btn" id="syncSubmit">Contar servicios</button>
</form>
<div id="syncResults" class="results"></div>
</div>
</div>
<div id="asyncPanel" class="panel">
<div class="card">
<form id="asyncForm">
<div class="field">
<label for="asyncFiles">Archivos PDF (hasta 500)</label>
<input type="file" id="asyncFiles" accept=".pdf,application/pdf" multiple required>
<p class="hint">
Flujo producción: MinIO → cola NATS → workers (autoscaler).
Sube por lotes de hasta 500; el progreso usa long-polling.
</p>
</div>
<div id="asyncError" class="error" hidden></div>
<progress id="asyncProgress" value="0" max="100" hidden></progress>
<p id="asyncProgressLabel" class="progress-label" hidden></p>
<button type="submit" class="btn" id="asyncSubmit">Iniciar job</button>
</form>
<div id="asyncResults" class="results"></div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>