Files
cbc2027/templates/index.html
renato97 75ef0afcb1 feat(dashboard): agregar panel de versiones y corregir carga de transcripciones
- Corregir endpoints /api/transcription y /api/summary para manejar filenames con extensión
- Agregar endpoint /api/versions para listar archivos generados
- Agregar tab 'Versiones' en panel lateral con lista de archivos
- Mejorar modal de progreso con barra animada y estados
- Cambiar archivos para que se abran en pestaña en lugar de descargarse
- Agregar botón 'Regenerar' en lista de archivos procesados
2026-01-10 19:18:14 +00:00

2682 lines
87 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard de Gestión de Audio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
<style>
:root {
/* Dark Mode Colors */
--bg-primary: #0f1115;
--bg-secondary: #1a1d24;
--bg-tertiary: #222734;
--bg-hover: #2a3042;
--text-primary: #e6e9ef;
--text-secondary: #a4a9b6;
--text-tertiary: #7c8293;
--border-color: #2a3042;
--accent-color: #646cff;
--accent-hover: #7c83ff;
--success-color: #3ddc84;
--warning-color: #f9a826;
--error-color: #ff6b6b;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.6);
--glass-bg: rgba(255, 255, 255, 0.03);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
overflow-x: hidden;
}
/* Animated background particles */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 30%, rgba(100, 108, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(61, 220, 132, 0.08) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(249, 168, 38, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
animation: backgroundShift 20s ease-in-out infinite;
}
@keyframes backgroundShift {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 60px 20px;
position: relative;
z-index: 1;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
background: var(--bg-secondary);
border-radius: 24px;
padding: 50px;
margin-bottom: 40px;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
animation: slideInDown 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), var(--success-color), var(--warning-color));
background-size: 200% 100%;
animation: gradientMove 3s linear infinite;
}
@keyframes gradientMove {
0% {
background-position: 0% 0%;
}
100% {
background-position: 200% 0%;
}
}
.header:hover {
box-shadow: var(--shadow-xl);
border-color: var(--accent-color);
transform: translateY(-4px);
}
.header h1 {
color: var(--text-primary);
font-size: 3.2rem;
font-weight: 800;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 20px;
letter-spacing: -0.02em;
animation: fadeInUp 0.8s ease 0.2s backwards;
}
.header .icon {
font-size: 4rem;
animation: float 3s ease-in-out infinite, fadeIn 1s ease;
filter: drop-shadow(0 0 20px rgba(100, 108, 255, 0.5));
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
}
33% {
transform: translateY(-8px) rotate(1deg);
}
66% {
transform: translateY(-4px) rotate(-1deg);
}
}
.header p {
color: var(--text-secondary);
font-size: 1.3rem;
font-weight: 400;
margin-top: 10px;
animation: fadeInUp 0.8s ease 0.4s backwards;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-60px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 40px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 35px;
text-align: center;
box-shadow: var(--shadow-md);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: fadeInUp 0.6s ease backwards;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
transform: translateX(-100%);
transition: transform 0.4s ease;
}
.stat-card:nth-child(2)::before {
background: linear-gradient(90deg, var(--success-color), #5ff3a0);
}
.stat-card:nth-child(3)::before {
background: linear-gradient(90deg, var(--warning-color), #ffb84d);
}
.stat-card:hover::before {
transform: translateX(0);
}
.stat-card:nth-child(1) {
animation-delay: 0.1s;
}
.stat-card:nth-child(2) {
animation-delay: 0.2s;
}
.stat-card:nth-child(3) {
animation-delay: 0.3s;
}
.stat-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-xl);
border-color: var(--accent-color);
}
.stat-number {
font-size: 3.5rem;
font-weight: 900;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: countUp 1s ease 0.5s backwards;
text-shadow: 0 0 30px rgba(100, 108, 255, 0.3);
}
@keyframes countUp {
from {
transform: scale(0.5);
opacity: 0;
filter: blur(10px);
}
to {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
.stat-label {
color: var(--text-secondary);
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.controls {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 35px;
margin-bottom: 40px;
box-shadow: var(--shadow-md);
display: flex;
gap: 15px;
flex-wrap: wrap;
animation: fadeIn 0.8s ease 0.5s backwards;
transition: all 0.3s ease;
}
.controls:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-lg);
}
.controls button {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 14px 30px;
border-radius: 14px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.controls button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s;
}
.controls button:hover::before {
left: 100%;
}
.controls button:hover {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(100, 108, 255, 0.4);
}
.controls button:active {
transform: translateY(-1px);
}
.controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.refresh-btn {
background: linear-gradient(135deg, var(--success-color) 0%, #5ff3a0 100%);
border: none;
color: #000;
font-weight: 700;
}
.refresh-btn:hover {
background: linear-gradient(135deg, #5ff3a0 0%, var(--success-color) 100%);
box-shadow: 0 8px 24px rgba(61, 220, 132, 0.5);
color: #000;
}
#reprocessAllBtn {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
border: none;
color: white;
font-weight: 700;
}
#reprocessAllBtn:hover {
box-shadow: 0 8px 24px rgba(100, 108, 255, 0.5);
color: white;
}
.files-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 45px;
box-shadow: var(--shadow-lg);
position: relative;
animation: fadeIn 1s ease 0.6s backwards;
transition: all 0.3s ease;
}
.files-container:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-xl);
}
.files-header {
margin-bottom: 30px;
padding-bottom: 25px;
border-bottom: 1px solid var(--border-color);
}
.header-title h2 {
color: var(--text-primary);
font-size: 2.2rem;
font-weight: 800;
margin: 0;
letter-spacing: -0.02em;
animation: fadeInLeft 0.6s ease 0.7s backwards;
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-top: 25px;
flex-wrap: wrap;
}
.source-filters {
display: flex;
gap: 15px;
align-items: center;
animation: fadeInLeft 0.6s ease 0.8s backwards;
}
.filter-checkbox {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
font-size: 0.95rem;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s ease;
padding: 10px 20px;
border-radius: 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.filter-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--accent-color);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
.filter-checkbox:hover::before {
opacity: 0.1;
}
.filter-checkbox:hover {
color: var(--text-primary);
border-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.filter-checkbox span {
position: relative;
z-index: 1;
}
.filter-checkbox input[type="checkbox"] {
display: none;
}
.filter-checkbox .checkmark {
width: 22px;
height: 22px;
border: 2px solid var(--border-color);
border-radius: 8px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--bg-secondary);
position: relative;
z-index: 1;
}
.filter-checkbox:hover .checkmark {
border-color: var(--accent-color);
transform: scale(1.1);
box-shadow: 0 0 15px rgba(100, 108, 255, 0.3);
}
.filter-checkbox input[type="checkbox"]:checked+.checkmark {
background: var(--accent-color);
border-color: var(--accent-color);
animation: checkMarkPop 0.3s ease;
}
@keyframes checkMarkPop {
0% {
transform: scale(0);
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
.filter-checkbox input[type="checkbox"]:checked+.checkmark::after {
content: '✓';
color: white;
font-size: 14px;
font-weight: bold;
animation: checkMarkBounce 0.3s ease;
}
@keyframes checkMarkBounce {
0% {
transform: scale(0) rotate(-180deg);
}
50% {
transform: scale(1.2) rotate(-90deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
.sort-checkbox {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
font-size: 0.95rem;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s ease;
padding: 10px 20px;
border-radius: 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.sort-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--success-color);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
.sort-checkbox:hover::before {
opacity: 0.1;
}
.sort-checkbox:hover {
color: var(--text-primary);
border-color: var(--success-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.sort-checkbox span {
position: relative;
z-index: 1;
}
.sort-checkbox input[type="radio"] {
display: none;
}
.sort-checkbox .checkmark {
width: 22px;
height: 22px;
border: 2px solid var(--border-color);
border-radius: 50%;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--bg-secondary);
position: relative;
z-index: 1;
}
.sort-checkbox:hover .checkmark {
border-color: var(--success-color);
transform: scale(1.1);
box-shadow: 0 0 15px rgba(61, 220, 132, 0.3);
}
.sort-checkbox input[type="radio"]:checked+.checkmark {
background: var(--success-color);
border-color: var(--success-color);
animation: checkMarkPop 0.3s ease;
}
.sort-checkbox input[type="radio"]:checked+.checkmark::after {
content: '';
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
animation: checkMarkBounce 0.3s ease;
}
@keyframes checkMarkBounce {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.5);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.search-box {
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
border-radius: 14px;
padding: 14px 22px;
font-size: 1rem;
width: 350px;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
animation: fadeInRight 0.6s ease 0.9s backwards;
}
.search-box::placeholder {
color: var(--text-tertiary);
}
.search-box:focus {
outline: none;
border-color: var(--accent-color);
background: var(--bg-secondary);
box-shadow: 0 0 0 4px rgba(100, 108, 255, 0.15), var(--shadow-md);
transform: translateY(-2px);
}
.files-grid {
display: grid;
gap: 18px;
}
.file-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 25px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: fadeInLeft 0.6s ease backwards;
cursor: pointer;
}
.file-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--accent-color);
transform: scaleY(0);
transition: transform 0.3s ease;
transform-origin: bottom;
}
.file-card:hover::before {
transform: scaleY(1);
}
.file-card:nth-child(odd) {
animation-delay: 0.1s;
}
.file-card:nth-child(even) {
animation-delay: 0.2s;
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.file-card:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-xl);
transform: translateY(-5px) scale(1.01);
background: var(--bg-hover);
}
.file-info {
flex: 1;
}
.file-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.file-meta {
display: flex;
gap: 18px;
flex-wrap: wrap;
font-size: 0.9rem;
color: var(--text-tertiary);
}
.file-formats {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.file-actions {
display: flex;
gap: 12px;
align-items: center;
}
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
}
.status-processed {
background: rgba(61, 220, 132, 0.15);
color: var(--success-color);
border: 1px solid rgba(61, 220, 132, 0.3);
}
.status-pending {
background: rgba(249, 168, 38, 0.15);
color: var(--warning-color);
border: 1px solid rgba(249, 168, 38, 0.3);
}
.action-btn {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 12px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.action-btn:active::after {
width: 300px;
height: 300px;
}
.action-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(100, 108, 255, 0.5);
}
.action-btn:disabled {
background: var(--bg-hover);
border: 1px solid var(--border-color);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.action-btn.reset {
background: linear-gradient(135deg, var(--warning-color) 0%, #ffb84d 100%);
color: #000;
}
.action-btn.reset:hover {
box-shadow: 0 8px 25px rgba(249, 168, 38, 0.5);
}
.action-btn.regen-btn {
background: linear-gradient(135deg, #9333ea 0%, #c084fc 100%);
color: white;
}
.action-btn.regen-btn:hover {
box-shadow: 0 8px 25px rgba(147, 51, 234, 0.5);
}
.source-badge {
padding: 5px 12px;
border-radius: 14px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.source-webdav {
background: rgba(100, 108, 255, 0.15);
color: var(--accent-color);
border: 1px solid rgba(100, 108, 255, 0.3);
}
.source-local {
background: rgba(61, 220, 132, 0.15);
color: var(--success-color);
border: 1px solid rgba(61, 220, 132, 0.3);
}
.loading {
display: none;
text-align: center;
padding: 60px;
color: var(--text-secondary);
animation: fadeIn 0.3s ease;
}
.loading.active {
display: block;
}
.spinner {
display: inline-block;
width: 60px;
height: 60px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite, glow 2s ease-in-out infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes glow {
0%,
100% {
box-shadow: 0 0 20px rgba(100, 108, 255, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(100, 108, 255, 0.6);
}
}
.loading p {
margin-top: 20px;
font-size: 1.2rem;
font-weight: 600;
}
.error,
.success {
padding: 20px 24px;
border-radius: 14px;
margin: 20px 0;
font-weight: 600;
border: 1px solid;
animation: slideInRight 0.5s ease;
backdrop-filter: blur(10px);
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.error {
background: rgba(255, 107, 107, 0.1);
border-color: var(--error-color);
color: var(--error-color);
}
.success {
background: rgba(61, 220, 132, 0.1);
border-color: var(--success-color);
color: var(--success-color);
}
.format-link {
display: inline-block;
padding: 6px 12px;
margin: 3px 6px 3px 0;
background: var(--bg-secondary);
color: var(--text-secondary);
text-decoration: none;
border-radius: 10px;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.format-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: var(--accent-color);
transition: left 0.3s ease;
z-index: 0;
}
.format-link:hover::before {
left: 0;
}
.format-link:hover {
color: white;
border-color: var(--accent-color);
text-decoration: none;
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.format-link span {
position: relative;
z-index: 1;
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Preview Panel Styles */
.preview-panel {
position: fixed;
top: 0;
right: -500px;
width: 500px;
height: 100vh;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
box-shadow: var(--shadow-xl);
z-index: 1000;
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.preview-panel.open {
right: 0;
}
.preview-header {
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.preview-header h3 {
color: var(--text-primary);
font-size: 1.3rem;
font-weight: 700;
margin: 0;
letter-spacing: -0.01em;
}
.preview-close {
background: var(--bg-hover);
border: 1px solid var(--border-color);
color: var(--text-secondary);
width: 36px;
height: 36px;
border-radius: 10px;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-family: 'Inter', sans-serif;
}
.preview-close:hover {
background: var(--error-color);
border-color: var(--error-color);
color: white;
transform: rotate(90deg);
}
.preview-tabs {
display: flex;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.tab-btn {
flex: 1;
background: transparent;
border: none;
color: var(--text-secondary);
padding: 16px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
position: relative;
border-bottom: 2px solid transparent;
}
.tab-btn:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.tab-btn.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
}
.preview-content {
flex: 1;
overflow-y: auto;
padding: 24px;
background: var(--bg-secondary);
}
.preview-content::-webkit-scrollbar {
width: 8px;
}
.preview-content::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
.preview-content::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.preview-content::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
.content-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
color: var(--text-tertiary);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
}
.stat-value {
color: var(--text-primary);
font-size: 1.1rem;
font-weight: 700;
}
.text-preview {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
color: var(--text-primary);
font-size: 0.95rem;
line-height: 1.7;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
}
.text-preview::-webkit-scrollbar {
width: 6px;
}
.text-preview::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
.text-preview::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.text-preview::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
.markdown-preview {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
color: var(--text-primary);
font-size: 0.95rem;
line-height: 1.7;
max-height: 400px;
overflow-y: auto;
}
.markdown-preview::-webkit-scrollbar {
width: 6px;
}
.markdown-preview::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
.markdown-preview::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.markdown-preview::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
.versions-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 400px;
overflow-y: auto;
padding: 10px 0;
}
.version-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
}
.version-item:hover {
border-color: var(--accent-color);
background: var(--bg-hover);
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(100, 108, 255, 0.2);
}
.version-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.version-label {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-primary);
}
.version-meta {
font-size: 0.8rem;
color: var(--text-tertiary);
display: flex;
gap: 12px;
}
.version-open-icon {
color: var(--accent-color);
font-size: 1.2rem;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3 {
color: var(--text-primary);
margin-top: 20px;
margin-bottom: 12px;
font-weight: 700;
}
.markdown-preview h1 {
font-size: 1.5rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
.markdown-preview h2 {
font-size: 1.3rem;
}
.markdown-preview h3 {
font-size: 1.1rem;
}
.markdown-preview p {
margin-bottom: 12px;
}
.markdown-preview ul,
.markdown-preview ol {
margin-left: 24px;
margin-bottom: 12px;
}
.markdown-preview li {
margin-bottom: 6px;
}
.markdown-preview strong {
color: var(--accent-color);
font-weight: 700;
}
.markdown-preview em {
color: var(--text-secondary);
}
.preview-actions {
background: var(--bg-tertiary);
border-top: 1px solid var(--border-color);
padding: 20px 24px;
display: flex;
gap: 12px;
flex-shrink: 0;
}
.regenerate-btn,
.download-btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.regenerate-btn {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
color: white;
}
.regenerate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(100, 108, 255, 0.4);
}
.regenerate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.download-btn {
background: var(--bg-hover);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.download-btn:hover {
background: var(--success-color);
border-color: var(--success-color);
color: #000;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(61, 220, 132, 0.4);
}
.regenerate-progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 10001;
backdrop-filter: blur(5px);
}
.regenerate-progress.active {
display: flex;
}
.progress-spinner {
width: 50px;
height: 50px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.regenerate-progress .progress-text {
position: absolute;
top: 60%;
color: var(--text-primary);
font-size: 1rem;
font-weight: 600;
}
.preview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
backdrop-filter: blur(3px);
}
.preview-overlay.active {
display: block;
animation: fadeIn 0.3s ease;
}
@media (max-width: 768px) {
.container {
padding: 30px 15px;
}
.header {
padding: 30px;
}
.header h1 {
font-size: 2.2rem;
}
.stats {
grid-template-columns: 1fr;
}
.stat-card {
padding: 25px;
}
.stat-number {
font-size: 2.5rem;
}
.header-controls {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.source-filters {
justify-content: center;
flex-wrap: wrap;
}
.search-box {
width: 100%;
}
.file-card {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.file-actions {
justify-content: center;
}
.controls {
flex-direction: column;
}
.controls button {
width: 100%;
}
.preview-panel {
width: 100%;
right: -100%;
}
.preview-panel.open {
right: 0;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>
<span class="icon">🎵</span>
Dashboard de Gestión de Audio
</h1>
<p>Monitorea y reprocesa archivos de audio con un solo click</p>
</div>
<!-- Estadísticas -->
<div class="stats">
<div class="stat-card">
<div class="stat-number total" id="totalFiles">-</div>
<div class="stat-label">Total Archivos</div>
</div>
<div class="stat-card">
<div class="stat-number processed" id="processedFiles">-</div>
<div class="stat-label">Procesados</div>
</div>
<div class="stat-card">
<div class="stat-number pending" id="pendingFiles">-</div>
<div class="stat-label">Pendientes</div>
</div>
</div>
<!-- Controles -->
<div class="controls">
<button onclick="refreshFiles()" class="refresh-btn">
🔄 Refrescar Lista
</button>
<button onclick="reprocessAllPending()" id="reprocessAllBtn">
⚡ Reprocesar Todos los Pendientes
</button>
</div>
<!-- Mensajes -->
<div id="messages"></div>
<!-- Cargando -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Cargando archivos...</p>
</div>
<!-- Lista de Archivos -->
<div class="files-container">
<div class="files-header">
<div class="header-title">
<h2>📁 Archivos de Audio</h2>
</div>
<div class="header-controls">
<div class="source-filters">
<label class="filter-checkbox">
<input type="checkbox" id="filterLocal" checked onchange="filterFiles()">
<span class="checkmark"></span>
<span>Local</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="filterWebDAV" onchange="filterFiles()">
<span class="checkmark"></span>
<span>WebDAV</span>
</label>
<div style="width: 2px; height: 30px; background: var(--border-color); margin: 0 10px;"></div>
<label class="sort-checkbox">
<input type="radio" id="sortDate" name="sortBy" value="date" checked
onchange="filterFiles()">
<span class="checkmark"></span>
<span>📅 Fecha</span>
</label>
<label class="sort-checkbox">
<input type="radio" id="sortName" name="sortBy" value="name" onchange="filterFiles()">
<span class="checkmark"></span>
<span>🔤 Nombre</span>
</label>
</div>
<input type="text" class="search-box" id="searchBox" placeholder="🔍 Buscar archivos..."
onkeyup="filterFiles()">
</div>
</div>
<div class="files-grid" id="filesGrid">
<!-- Los archivos se cargarán aquí dinámicamente -->
</div>
</div>
</div>
<!-- Preview Panel -->
<div class="preview-overlay" id="previewOverlay" onclick="closePreview()"></div>
<div class="preview-panel" id="previewPanel">
<div class="regenerate-progress" id="regenerateProgress">
<div class="progress-spinner"></div>
<div class="progress-text">Regenerando resumen...</div>
</div>
<div class="preview-header">
<h3 id="previewTitle">Vista Previa</h3>
<button class="preview-close" onclick="closePreview()">×</button>
</div>
<div class="preview-tabs">
<button class="tab-btn active" onclick="switchTab('transcription')">Transcripción</button>
<button class="tab-btn" onclick="switchTab('summary')">Resumen</button>
<button class="tab-btn" onclick="switchTab('versions')">📁 Versiones</button>
</div>
<div class="preview-content">
<div class="tab-content active" id="tabTranscription">
<div class="content-stats">
<div class="stat-item">
<div class="stat-label">Palabras</div>
<div class="stat-value" id="transcriptionWords">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Caracteres</div>
<div class="stat-value" id="transcriptionChars">-</div>
</div>
</div>
<div class="text-preview" id="transcriptionPreview">
Cargando transcripción...
</div>
</div>
<div class="tab-content" id="tabSummary">
<div class="content-stats">
<div class="stat-item">
<div class="stat-label">Palabras</div>
<div class="stat-value" id="summaryWords">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Caracteres</div>
<div class="stat-value" id="summaryChars">-</div>
</div>
</div>
<div class="markdown-preview" id="summaryPreview">
Cargando resumen...
</div>
</div>
<div class="tab-content" id="tabVersions">
<div class="versions-header">
<h4 style="color: var(--text-primary); margin-bottom: 10px;">📁 Archivos Generados</h4>
<p style="color: var(--text-tertiary); font-size: 0.85rem;">Haz clic para abrir en nueva pestaña</p>
</div>
<div class="versions-list" id="versionsList">
Cargando versiones...
</div>
</div>
</div>
<div class="preview-actions">
<button class="regenerate-btn" onclick="regenerateSummary()" id="regenerateBtn">
🔄 Regenerar
</button>
<button class="download-btn" onclick="downloadFiles()">
⬇️ Descargar
</button>
</div>
</div>
<script>
let files = [];
let loading = false;
let currentPreviewFile = null;
// Inicialización
document.addEventListener('DOMContentLoaded', function () {
loadFiles();
});
// Cargar archivos desde la API
async function loadFiles() {
setLoading(true);
try {
const response = await fetch('/api/files');
const data = await response.json();
if (data.success) {
files = data.files;
updateStats(data);
renderFiles();
hideMessages();
} else {
showMessage('Error cargando archivos: ' + data.message, 'error');
}
} catch (error) {
showMessage('Error de conexión con el servidor', 'error');
console.error('Error:', error);
} finally {
setLoading(false);
}
}
// Obtener estadísticas actuales de los archivos locales
function getCurrentStats() {
const total = files.length;
const processed = files.filter(f => f.processed).length;
const pending = total - processed;
return { total, processed, pending };
}
// Actualizar estadísticas
function updateStats(data) {
document.getElementById('totalFiles').textContent = data.total;
document.getElementById('processedFiles').textContent = data.processed;
document.getElementById('pendingFiles').textContent = data.pending;
}
// Renderizar archivos
function renderFiles() {
const grid = document.getElementById('filesGrid');
const filteredFiles = filterFilesArray();
if (filteredFiles.length === 0) {
grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-tertiary); font-size: 1.1rem; animation: fadeIn 0.5s ease;">No se encontraron archivos</div>';
return;
}
grid.innerHTML = filteredFiles.map(file => {
const formats = file.available_formats || {};
const availableFormatsList = Object.keys(formats).filter(ext => formats[ext]);
// Determinar si tiene transcripción (txt disponible)
const hasTranscription = formats.txt === true;
const formatLinks = availableFormatsList.length > 0
? availableFormatsList.map(ext => {
const baseName = file.filename.substring(0, file.filename.lastIndexOf('.'));
const possibleNames = [
`${baseName}.${ext}`,
`${baseName}_unificado.${ext}`,
`${baseName.replace(/ /g, '_')}.${ext}`,
`${baseName.replace(/ /g, '_')}_unificado.${ext}`
];
const icon = ext === 'txt' ? '📝' : ext === 'docx' ? '📄' : ext === 'md' ? '📋' : '📑';
return `<a href="/downloads/find-file?filename=${encodeURIComponent(baseName)}&ext=${ext}" target="_blank" class="format-link"><span>${icon} ${ext}</span></a>`;
}).join(' ')
: '<span style="color: var(--text-tertiary); font-size: 0.9rem;">no procesado aún</span>';
return `
<div class="file-card" data-filename="${file.filename.toLowerCase()}" onclick="openPreview('${file.filename}')">
<div class="file-info">
<div class="file-name">${file.filename}</div>
<div class="file-meta">
<span class="source-badge source-${file.source}">${file.source}</span>
<span>📦 ${file.size}</span>
<span>📅 ${file.last_modified}</span>
</div>
<div class="file-formats">
${formatLinks}
</div>
</div>
<div class="file-actions">
<span class="status-badge ${file.processed ? 'status-processed' : 'status-pending'}">
${file.processed ? 'Procesado' : 'Pendiente'}
</span>
${!file.processed ? `
<button class="action-btn" onclick="event.stopPropagation(); reprocessFile('${file.path}', '${file.source}')" id="btn-${file.filename.replace(/[^a-zA-Z0-9]/g, '')}">
🚀 Procesar
</button>
` : `
${hasTranscription ? `
<button class="action-btn regen-btn" onclick="event.stopPropagation(); quickRegenerate('${file.filename}')" id="regen-${file.filename.replace(/[^a-zA-Z0-9]/g, '')}" title="Regenerar resumen (mantiene transcripción)">
✨ Regenerar
</button>
` : ''}
<button class="action-btn reset" onclick="event.stopPropagation(); markUnprocessed('${file.path}')" id="btn-${file.filename.replace(/[^a-zA-Z0-9]/g, '')}">
🔄 Resetear
</button>
`}
</div>
</div>
`;
}).join('');
}
// Filtrar archivos
function filterFiles() {
renderFiles();
}
function filterFilesArray() {
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
const filterLocal = document.getElementById('filterLocal').checked;
const filterWebDAV = document.getElementById('filterWebDAV').checked;
const sortBy = document.querySelector('input[name="sortBy"]:checked')?.value || 'date';
let filteredFiles = files.filter(file => {
const matchesSearch = file.filename.toLowerCase().includes(searchTerm);
const matchesSource =
(filterLocal && file.source === 'local') ||
(filterWebDAV && file.source === 'webdav');
return matchesSearch && matchesSource;
});
// Ordenar archivos
if (sortBy === 'date') {
// Para archivos locales, convertir last_modified a fecha
// Para archivos webdav, usar last_modified como string
filteredFiles.sort((a, b) => {
// Si ambos tienen last_modified
if (a.last_modified !== 'Unknown' && b.last_modified !== 'Unknown') {
// Si uno es local y otro webdav, comparar como fechas
if (a.source === 'local' || b.source === 'local') {
try {
// Intentar convertir a timestamp
const dateA = new Date(a.last_modified).getTime();
const dateB = new Date(b.last_modified).getTime();
return dateB - dateA; // Más reciente primero
} catch (e) {
// Si falla, ordenar alfabéticamente por fecha como string
return b.last_modified.localeCompare(a.last_modified);
}
}
}
// Ordenar alfabéticamente como fallback
return a.filename.localeCompare(b.filename);
});
} else if (sortBy === 'name') {
filteredFiles.sort((a, b) => a.filename.localeCompare(b.filename));
}
return filteredFiles;
}
// Actualizar estado de un archivo específico
function updateFileStatus(filePath, isProcessed) {
const file = files.find(f => f.path === filePath);
if (file) {
file.processed = isProcessed;
renderFiles();
}
}
// Reprocesar un archivo
async function reprocessFile(filePath, source) {
const file = files.find(f => f.path === filePath);
if (!file) return;
const formats = file.available_formats || {};
const existingFormats = Object.keys(formats).filter(ext => formats[ext]);
if (existingFormats.length > 0) {
const confirmed = await showConfirmDialog(
`⚠️ Archivos existentes detectados`,
`El archivo "${file.filename}" ya tiene los siguientes formatos: ${existingFormats.join(', ')}.\n\n` +
`¿Estás seguro de que quieres reprocesarlo? Esto sobrescribirá los archivos existentes.`
);
if (!confirmed) {
return;
}
}
await performReprocess(filePath, source);
}
// Función que ejecuta el reprocesamiento real
async function performReprocess(filePath, source) {
const btnId = 'btn-' + filePath.replace(/[^a-zA-Z0-9]/g, '');
const btn = document.getElementById(btnId);
if (!btn) return;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳ Procesando...';
try {
const response = await fetch('/api/reprocess', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: filePath,
source: source
})
});
const data = await response.json();
if (data.success) {
showMessage('✅ ' + data.message, 'success');
setTimeout(async () => {
await loadFiles();
showMessage('🔄 Actualizando estado de archivos...', 'success');
}, 2000);
} else {
showMessage('❌ ' + data.message, 'error');
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (error) {
showMessage('❌ Error de conexión', 'error');
btn.disabled = false;
btn.innerHTML = originalText;
console.error('Error:', error);
}
}
// Marcar como no procesado
async function markUnprocessed(filePath) {
if (!confirm('¿Estás seguro de que quieres marcar este archivo como no procesado?')) {
return;
}
try {
const response = await fetch('/api/mark-unprocessed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: filePath
})
});
const data = await response.json();
if (data.success) {
showMessage('✅ ' + data.message, 'success');
updateFileStatus(filePath, false);
updateStats(getCurrentStats());
} else {
showMessage('❌ ' + data.message, 'error');
}
} catch (error) {
showMessage('❌ Error de conexión', 'error');
console.error('Error:', error);
}
}
// Refrescar archivos
async function refreshFiles() {
showMessage('🔄 Actualizando lista de archivos...', 'success');
await loadFiles();
}
// Reprocesar todos los pendientes
async function reprocessAllPending() {
const pendingFiles = files.filter(f => !f.processed);
if (pendingFiles.length === 0) {
showMessage(' No hay archivos pendientes para procesar', 'success');
return;
}
if (!confirm(`¿Estás seguro de que quieres procesar ${pendingFiles.length} archivos pendientes?`)) {
return;
}
const btn = document.getElementById('reprocessAllBtn');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳ Procesando...';
showMessage(`🚀 Iniciando procesamiento de ${pendingFiles.length} archivos...`, 'success');
for (let i = 0; i < pendingFiles.length; i++) {
const file = pendingFiles[i];
try {
await reprocessFile(file.path, file.source);
if (i < pendingFiles.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
console.error(`Error procesando ${file.filename}:`, error);
}
}
btn.disabled = false;
btn.innerHTML = originalText;
showMessage('✅ Procesamiento en lote completado', 'success');
}
// Control de estado de carga
function setLoading(isLoading) {
loading = isLoading;
const loadingEl = document.getElementById('loading');
const gridEl = document.getElementById('filesGrid');
if (isLoading) {
loadingEl.classList.add('active');
gridEl.style.display = 'none';
} else {
loadingEl.classList.remove('active');
gridEl.style.display = 'block';
}
}
// Mostrar mensajes
function showMessage(message, type) {
const messagesEl = document.getElementById('messages');
const messageEl = document.createElement('div');
messageEl.className = type;
messageEl.textContent = message;
messagesEl.innerHTML = '';
messagesEl.appendChild(messageEl);
setTimeout(() => {
if (messagesEl.contains(messageEl)) {
messagesEl.removeChild(messageEl);
}
}, 5000);
}
function hideMessages() {
document.getElementById('messages').innerHTML = '';
}
// Función para mostrar diálogo de confirmación
async function showConfirmDialog(title, message) {
return new Promise((resolve) => {
const modalOverlay = document.createElement('div');
modalOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 20px;
padding: 40px;
max-width: 500px;
width: 90%;
box-shadow: var(--shadow-xl);
text-align: center;
animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
`;
modalContent.innerHTML = `
<h2 style="color: var(--text-primary); margin-bottom: 20px; font-size: 1.8rem; font-weight: 700;">${title}</h2>
<p style="color: var(--text-secondary); margin-bottom: 30px; line-height: 1.6; white-space: pre-line; font-size: 1.05rem;">${message}</p>
<div style="display: flex; gap: 15px; justify-content: center;">
<button id="confirm-cancel" style="
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
">Cancelar</button>
<button id="confirm-ok" style="
background: linear-gradient(135deg, var(--error-color) 0%, #ff5252 100%);
color: white;
border: none;
padding: 12px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
">Sí, reprocesar</button>
</div>
`;
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
const cancelBtn = document.getElementById('confirm-cancel');
const okBtn = document.getElementById('confirm-ok');
const cleanup = () => {
modalOverlay.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (document.body.contains(modalOverlay)) {
document.body.removeChild(modalOverlay);
}
}, 300);
};
cancelBtn.addEventListener('click', () => {
cleanup();
resolve(false);
});
okBtn.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelBtn.addEventListener('mouseenter', () => {
cancelBtn.style.transform = 'translateY(-3px)';
cancelBtn.style.boxShadow = 'var(--shadow-md)';
cancelBtn.style.borderColor = 'var(--accent-color)';
});
cancelBtn.addEventListener('mouseleave', () => {
cancelBtn.style.transform = 'translateY(0)';
cancelBtn.style.boxShadow = 'none';
cancelBtn.style.borderColor = 'var(--border-color)';
});
okBtn.addEventListener('mouseenter', () => {
okBtn.style.transform = 'translateY(-3px)';
okBtn.style.boxShadow = '0 8px 25px rgba(255, 107, 107, 0.5)';
});
okBtn.addEventListener('mouseleave', () => {
okBtn.style.transform = 'translateY(0)';
okBtn.style.boxShadow = 'none';
});
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
cleanup();
resolve(false);
}
});
});
}
// Preview Panel Functions
function openPreview(filename) {
currentPreviewFile = filename;
const panel = document.getElementById('previewPanel');
const overlay = document.getElementById('previewOverlay');
const title = document.getElementById('previewTitle');
title.textContent = filename;
panel.classList.add('open');
overlay.classList.add('active');
// Load content
loadTranscription(filename);
loadSummary(filename);
}
function closePreview() {
currentPreviewFile = null;
const panel = document.getElementById('previewPanel');
const overlay = document.getElementById('previewOverlay');
panel.classList.remove('open');
overlay.classList.remove('active');
}
function switchTab(tabName) {
const tabs = document.querySelectorAll('.tab-btn');
const contents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => tab.classList.remove('active'));
contents.forEach(content => content.classList.remove('active'));
if (tabName === 'transcription') {
tabs[0].classList.add('active');
document.getElementById('tabTranscription').classList.add('active');
} else if (tabName === 'summary') {
tabs[1].classList.add('active');
document.getElementById('tabSummary').classList.add('active');
} else if (tabName === 'versions') {
tabs[2].classList.add('active');
document.getElementById('tabVersions').classList.add('active');
// Load versions when tab is clicked
if (currentPreviewFile) {
loadVersions(currentPreviewFile);
}
}
}
async function loadVersions(filename) {
const listEl = document.getElementById('versionsList');
listEl.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-tertiary);">Cargando versiones...</div>';
try {
const response = await fetch(`/api/versions/${encodeURIComponent(filename)}`);
const data = await response.json();
if (data.success && data.versions.length > 0) {
listEl.innerHTML = data.versions.map(v => `
<a href="${v.path}" target="_blank" class="version-item">
<div class="version-info">
<div class="version-label">${v.label}</div>
<div class="version-meta">
<span>📅 ${v.date}</span>
<span>📦 ${v.size}</span>
</div>
</div>
<span class="version-open-icon">↗️</span>
</a>
`).join('');
} else {
listEl.innerHTML = '<div style="text-align: center; padding: 30px; color: var(--text-tertiary);">No hay versiones disponibles aún</div>';
}
} catch (error) {
listEl.innerHTML = '<div style="text-align: center; padding: 30px; color: var(--error-color);">Error al cargar versiones</div>';
console.error('Error loading versions:', error);
}
}
async function loadTranscription(filename) {
const preview = document.getElementById('transcriptionPreview');
const wordsEl = document.getElementById('transcriptionWords');
const charsEl = document.getElementById('transcriptionChars');
preview.innerHTML = 'Cargando transcripción...';
try {
const response = await fetch(`/api/transcription/${encodeURIComponent(filename)}`);
const data = await response.json();
if (data.success && data.transcription) {
preview.textContent = data.transcription;
// Update stats
const words = data.transcription.split(/\s+/).filter(w => w.length > 0).length;
const chars = data.transcription.length;
wordsEl.textContent = words.toLocaleString();
charsEl.textContent = chars.toLocaleString();
} else {
preview.textContent = 'No se encontró transcripción para este archivo.';
wordsEl.textContent = '0';
charsEl.textContent = '0';
}
} catch (error) {
preview.textContent = 'Error al cargar la transcripción.';
wordsEl.textContent = '0';
charsEl.textContent = '0';
console.error('Error loading transcription:', error);
}
}
async function loadSummary(filename) {
const preview = document.getElementById('summaryPreview');
const wordsEl = document.getElementById('summaryWords');
const charsEl = document.getElementById('summaryChars');
preview.innerHTML = 'Cargando resumen...';
try {
const response = await fetch(`/api/summary/${encodeURIComponent(filename)}`);
const data = await response.json();
if (data.success && data.summary) {
preview.innerHTML = renderMarkdown(data.summary);
// Update stats (strip HTML for word count)
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.summary;
const textContent = tempDiv.textContent || tempDiv.innerText || '';
const words = textContent.split(/\s+/).filter(w => w.length > 0).length;
const chars = textContent.length;
wordsEl.textContent = words.toLocaleString();
charsEl.textContent = chars.toLocaleString();
} else {
preview.textContent = 'No se encontró resumen para este archivo.';
wordsEl.textContent = '0';
charsEl.textContent = '0';
}
} catch (error) {
preview.textContent = 'Error al cargar el resumen.';
wordsEl.textContent = '0';
charsEl.textContent = '0';
console.error('Error loading summary:', error);
}
}
function renderMarkdown(text) {
if (!text) return '';
// Basic markdown to HTML conversion
let html = text
// Escape HTML
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Headers
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
// Italic
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
// Unordered lists
.replace(/^\* (.*$)/gim, '<li>$1</li>')
.replace(/^(\s*)- (.*$)/gim, '<li>$2</li>')
// Ordered lists
.replace(/^\d+\. (.*$)/gim, '<li>$1</li>')
// Paragraphs
.replace(/\n\n/g, '</p><p>')
// Line breaks
.replace(/\n/g, '<br>');
// Wrap in paragraph if not starting with header
if (!html.startsWith('<h')) {
html = '<p>' + html + '</p>';
}
// Wrap lists
html = html.replace(/(<li>.*?<\/li>)/gim, '<ul>$1</ul>');
html = html.replace(/<\/ul><ul>/gim, '');
return html;
}
async function regenerateSummary() {
if (!currentPreviewFile) return;
const progress = document.getElementById('regenerateProgress');
const btn = document.getElementById('regenerateBtn');
progress.classList.add('active');
btn.disabled = true;
try {
const response = await fetch('/api/regenerate-summary', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: currentPreviewFile
})
});
const data = await response.json();
if (data.success) {
showMessage('✅ Resumen regenerado exitosamente', 'success');
await loadSummary(currentPreviewFile);
} else {
showMessage('❌ ' + data.message, 'error');
}
} catch (error) {
showMessage('❌ Error al regenerar el resumen', 'error');
console.error('Error regenerating summary:', error);
} finally {
progress.classList.remove('active');
btn.disabled = false;
}
}
// Regeneración rápida desde la lista de archivos
async function quickRegenerate(filename) {
const btnId = 'regen-' + filename.replace(/[^a-zA-Z0-9]/g, '');
const btn = document.getElementById(btnId);
if (!btn) return;
// Crear modal de progreso
const modal = document.createElement('div');
modal.id = 'progressModal';
modal.innerHTML = `
<div style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
">
<div style="
background: var(--bg-secondary);
border: 2px solid var(--accent-color);
border-radius: 20px;
padding: 40px;
min-width: 350px;
text-align: center;
box-shadow: 0 20px 60px rgba(100, 108, 255, 0.3);
">
<div style="
width: 60px;
height: 60px;
border: 4px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 25px;
"></div>
<h3 style="color: var(--text-primary); margin-bottom: 15px; font-size: 1.4rem;">
✨ Regenerando Resumen
</h3>
<p id="progressStatus" style="color: var(--text-secondary); margin-bottom: 20px; font-size: 1rem;">
📖 Leyendo transcripción...
</p>
<div style="
background: var(--bg-tertiary);
border-radius: 10px;
height: 8px;
overflow: hidden;
margin-bottom: 15px;
">
<div id="progressBar" style="
background: linear-gradient(90deg, var(--accent-color), var(--success-color));
height: 100%;
width: 10%;
transition: width 0.5s ease;
border-radius: 10px;
"></div>
</div>
<p style="color: var(--text-tertiary); font-size: 0.85rem;">
Archivo: ${filename}
</p>
</div>
</div>
`;
document.body.appendChild(modal);
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳...';
// Función para actualizar progreso
const updateProgress = (status, percent) => {
const statusEl = document.getElementById('progressStatus');
const barEl = document.getElementById('progressBar');
if (statusEl) statusEl.textContent = status;
if (barEl) barEl.style.width = percent + '%';
};
try {
updateProgress('🤖 Conectando con IA...', 25);
const response = await fetch('/api/regenerate-summary', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename
})
});
updateProgress('📝 Generando resumen...', 60);
const data = await response.json();
if (data.success) {
updateProgress('✅ ¡Completado!', 100);
await new Promise(r => setTimeout(r, 800));
showMessage(`✅ Resumen regenerado en ${data.processing_time}`, 'success');
await loadFiles();
} else {
showMessage('❌ ' + data.message, 'error');
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (error) {
showMessage('❌ Error al regenerar el resumen', 'error');
console.error('Error regenerating summary:', error);
btn.disabled = false;
btn.innerHTML = originalText;
} finally {
const modalEl = document.getElementById('progressModal');
if (modalEl) document.body.removeChild(modalEl);
}
}
async function downloadFiles() {
if (!currentPreviewFile) return;
const baseName = currentPreviewFile.substring(0, currentPreviewFile.lastIndexOf('.'));
// Create download menu
const menu = document.createElement('div');
menu.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 20px;
padding: 30px;
min-width: 300px;
box-shadow: var(--shadow-xl);
z-index: 10002;
animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
`;
menu.innerHTML = `
<h3 style="color: var(--text-primary); margin-bottom: 20px; font-size: 1.5rem; font-weight: 700;">Descargar Archivos</h3>
<div style="display: flex; flex-direction: column; gap: 10px;">
<a href="/downloads/find-file?filename=${encodeURIComponent(baseName)}&ext=txt" target="_blank" style="
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 20px;
border-radius: 12px;
text-decoration: none;
font-weight: 600;
text-align: center;
transition: all 0.3s ease;
display: block;
">📝 Transcripción (TXT)</a>
<a href="/downloads/find-file?filename=${encodeURIComponent(baseName)}&ext=md" target="_blank" style="
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 20px;
border-radius: 12px;
text-decoration: none;
font-weight: 600;
text-align: center;
transition: all 0.3s ease;
display: block;
">📋 Resumen (MD)</a>
<a href="/downloads/find-file?filename=${encodeURIComponent(baseName)}&ext=docx" target="_blank" style="
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 20px;
border-radius: 12px;
text-decoration: none;
font-weight: 600;
text-align: center;
transition: all 0.3s ease;
display: block;
">📄 Documento (DOCX)</a>
<button id="closeDownloadMenu" style="
background: var(--bg-hover);
color: var(--text-secondary);
border: 1px solid var(--border-color);
padding: 12px 20px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
margin-top: 10px;
">Cancelar</button>
</div>
`;
document.body.appendChild(menu);
// Add hover effects
const links = menu.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('mouseenter', () => {
link.style.background = 'var(--accent-color)';
link.style.color = 'white';
link.style.borderColor = 'var(--accent-color)';
link.style.transform = 'translateY(-2px)';
});
link.addEventListener('mouseleave', () => {
link.style.background = 'var(--bg-tertiary)';
link.style.color = 'var(--text-primary)';
link.style.borderColor = 'var(--border-color)';
link.style.transform = 'translateY(0)';
});
});
const closeBtn = document.getElementById('closeDownloadMenu');
closeBtn.addEventListener('click', () => {
document.body.removeChild(menu);
});
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = 'var(--bg-hover)';
closeBtn.style.borderColor = 'var(--error-color)';
closeBtn.style.color = 'var(--error-color)';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = 'var(--bg-hover)';
closeBtn.style.borderColor = 'var(--border-color)';
closeBtn.style.color = 'var(--text-secondary)';
});
// Close on outside click
menu.addEventListener('click', (e) => {
if (e.target === menu) {
document.body.removeChild(menu);
}
});
}
// Añadir estilos de animación dinámicos
const style = document.createElement('style');
style.textContent = `
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>