# 🎯 TODO: Dashboard de Monitoreo y Reprocesamiento

## 📋 Objetivo Principal

Crear un dashboard funcional en localhost que permita:
1. **Monitorear** archivos procesados y pendientes
2. **Visualizar** transcripciones y resúmenes generados
3. **Regenerar resúmenes** cuando no son satisfactorios (usando la transcripción existente)
4. **Reprocesar** archivos completos si es necesario

---

## 📊 Estado Actual del Proyecto

### ✅ Lo que YA existe:

| Componente | Ubicación | Estado |
|------------|-----------|--------|
| UI Dashboard | `templates/index.html` | ✅ Completo (1586 líneas) |
| API Routes | `api/routes.py` | ✅ Parcial (281 líneas) |
| Backend Flask | `api/__init__.py` | ✅ Básico |
| Generador Docs | `document/generators.py` | ✅ Completo |
| Audio Processor | `processors/audio_processor.py` | ✅ Completo |
| PDF Processor | `processors/pdf_processor.py` | ✅ Completo |
| AI Providers | `services/ai/` | ✅ Completo |

### ❌ Lo que FALTA implementar:

| Funcionalidad | Prioridad | Complejidad |
|---------------|-----------|-------------|
| Endpoint regenerar resumen | 🔴 Alta | Media |
| Vista previa de transcripción | 🔴 Alta | Baja |
| Vista previa de resumen | 🔴 Alta | Baja |
| Editor de prompts | 🟡 Media | Media |
| Historial de versiones | 🟢 Baja | Alta |

---

## 🏗️ ARQUITECTURA DEL DASHBOARD

```
┌─────────────────────────────────────────────────────────────────────┐
│                        FRONTEND (index.html)                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │
│  │  Stats      │  │  File List  │  │  Preview    │  │  Actions    │ │
│  │  Cards      │  │  + Filters  │  │  Panel      │  │  Panel      │ │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         API ROUTES (routes.py)                       │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐   │
│  │ GET /api/files   │  │ GET /api/preview │  │ POST /api/regen  │   │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       BACKEND SERVICES                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               │
│  │ Document     │  │ AI Provider  │  │ WebDAV       │               │
│  │ Generator    │  │ (Gemini)     │  │ Service      │               │
│  └──────────────┘  └──────────────┘  └──────────────┘               │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 📁 ARCHIVOS A MODIFICAR/CREAR

### 1. `api/routes.py` - Nuevos Endpoints

#### 1.1 Endpoint: Obtener contenido de transcripción
```
GET /api/transcription/<filename>
```

**Propósito**: Devolver el contenido de la transcripción (.txt) de un archivo de audio.

**Request**:
```
GET /api/transcription/clase_historia_01
```

**Response**:
```json
{
  "success": true,
  "filename": "clase_historia_01",
  "transcription": "Contenido completo de la transcripción...",
  "file_path": "/home/ren/proyectos/cbc/downloads/clase_historia_01.txt",
  "word_count": 3456,
  "char_count": 18234
}
```

**Implementación**:
```python
@app.route('/api/transcription/<filename>')
def get_transcription(filename):
    """Obtener contenido de transcripción"""
    try:
        # Buscar archivo .txt en downloads/
        txt_path = settings.LOCAL_DOWNLOADS_PATH / f"{filename}.txt"
        
        if not txt_path.exists():
            return jsonify({
                'success': False,
                'message': f"Transcripción no encontrada: {filename}.txt"
            }), 404
        
        with open(txt_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        return jsonify({
            'success': True,
            'filename': filename,
            'transcription': content,
            'file_path': str(txt_path),
            'word_count': len(content.split()),
            'char_count': len(content)
        })
    except Exception as e:
        app.logger.error(f"Error getting transcription: {e}")
        return jsonify({
            'success': False,
            'message': f"Error: {str(e)}"
        }), 500
```

---

#### 1.2 Endpoint: Obtener contenido de resumen
```
GET /api/summary/<filename>
```

**Propósito**: Devolver el contenido del resumen (.md) de un archivo procesado.

**Request**:
```
GET /api/summary/clase_historia_01_unificado
```

**Response**:
```json
{
  "success": true,
  "filename": "clase_historia_01_unificado",
  "summary": "# Resumen\n\n## Contenido...",
  "file_path": "/home/ren/proyectos/cbc/downloads/clase_historia_01_unificado.md",
  "formats_available": {
    "md": true,
    "docx": true,
    "pdf": true
  }
}
```

**Implementación**:
```python
@app.route('/api/summary/<filename>')
def get_summary(filename):
    """Obtener contenido de resumen"""
    try:
        # Normalizar nombre (agregar _unificado si no lo tiene)
        base_name = filename.replace('_unificado', '')
        unified_name = f"{base_name}_unificado"
        
        # Buscar archivo .md en downloads/
        md_path = settings.LOCAL_DOWNLOADS_PATH / f"{unified_name}.md"
        
        if not md_path.exists():
            return jsonify({
                'success': False,
                'message': f"Resumen no encontrado: {unified_name}.md"
            }), 404
        
        with open(md_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # Verificar formatos disponibles
        formats = {
            'md': md_path.exists(),
            'docx': (settings.LOCAL_DOCX / f"{unified_name}.docx").exists(),
            'pdf': (settings.LOCAL_DOWNLOADS_PATH / f"{unified_name}.pdf").exists()
        }
        
        return jsonify({
            'success': True,
            'filename': unified_name,
            'summary': content,
            'file_path': str(md_path),
            'formats_available': formats
        })
    except Exception as e:
        app.logger.error(f"Error getting summary: {e}")
        return jsonify({
            'success': False,
            'message': f"Error: {str(e)}"
        }), 500
```

---

#### 1.3 Endpoint: Regenerar resumen (CRÍTICO)
```
POST /api/regenerate-summary
```

**Propósito**: Regenerar el resumen usando la transcripción existente, sin reprocesar el audio.

**Request**:
```json
{
  "filename": "clase_historia_01",
  "custom_prompt": "Opcional: prompt personalizado para el resumen",
  "ai_provider": "gemini"
}
```

**Response**:
```json
{
  "success": true,
  "message": "Resumen regenerado exitosamente",
  "new_summary": "# Nuevo Resumen...",
  "files_updated": {
    "md": "/path/to/clase_historia_01_unificado.md",
    "docx": "/path/to/clase_historia_01_unificado.docx",
    "pdf": "/path/to/clase_historia_01_unificado.pdf"
  },
  "processing_time": 4.5
}
```

**Implementación detallada**:
```python
@app.route('/api/regenerate-summary', methods=['POST'])
def regenerate_summary():
    """Regenerar resumen desde transcripción existente"""
    try:
        data = request.get_json()
        filename = data.get('filename')
        custom_prompt = data.get('custom_prompt', None)
        
        if not filename:
            return jsonify({
                'success': False,
                'message': "Nombre de archivo requerido"
            }), 400
        
        # Paso 1: Obtener transcripción
        base_name = filename.replace('_unificado', '').replace('.txt', '').replace('.md', '')
        txt_path = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.txt"
        
        if not txt_path.exists():
            return jsonify({
                'success': False,
                'message': f"Transcripción no encontrada: {base_name}.txt"
            }), 404
        
        with open(txt_path, 'r', encoding='utf-8') as f:
            transcription_text = f.read()
        
        if not transcription_text.strip():
            return jsonify({
                'success': False,
                'message': "La transcripción está vacía"
            }), 400
        
        # Paso 2: Regenerar resumen con IA
        import time
        start_time = time.time()
        
        from document.generators import DocumentGenerator
        doc_generator = DocumentGenerator()
        
        success, summary, output_files = doc_generator.generate_summary(
            transcription_text, 
            base_name
        )
        
        processing_time = time.time() - start_time
        
        if not success:
            return jsonify({
                'success': False,
                'message': "Error al generar el resumen con IA"
            }), 500
        
        # Paso 3: Subir a Nextcloud si está configurado
        if settings.has_webdav_config:
            from services.webdav_service import webdav_service
            
            for folder in [settings.RESUMENES_FOLDER, settings.DOCX_FOLDER]:
                try:
                    webdav_service.makedirs(folder)
                except Exception:
                    pass
            
            # Subir MD
            md_path = Path(output_files.get('markdown_path', ''))
            if md_path.exists():
                remote_md = f"{settings.RESUMENES_FOLDER}/{md_path.name}"
                webdav_service.upload(md_path, remote_md)
            
            # Subir DOCX
            docx_path = Path(output_files.get('docx_path', ''))
            if docx_path.exists():
                remote_docx = f"{settings.DOCX_FOLDER}/{docx_path.name}"
                webdav_service.upload(docx_path, remote_docx)
            
            # Subir PDF
            pdf_path = Path(output_files.get('pdf_path', ''))
            if pdf_path.exists():
                remote_pdf = f"{settings.DOCX_FOLDER}/{pdf_path.name}"
                webdav_service.upload(pdf_path, remote_pdf)
        
        return jsonify({
            'success': True,
            'message': "Resumen regenerado exitosamente",
            'new_summary': summary[:500] + "..." if len(summary) > 500 else summary,
            'files_updated': output_files,
            'processing_time': round(processing_time, 2)
        })
        
    except Exception as e:
        app.logger.error(f"Error regenerating summary: {e}")
        return jsonify({
            'success': False,
            'message': f"Error: {str(e)}"
        }), 500
```

---

#### 1.4 Endpoint: Listar todos los archivos procesados con detalles
```
GET /api/files-detailed
```

**Propósito**: Obtener lista de archivos con información sobre transcripciones y resúmenes disponibles.

**Response**:
```json
{
  "success": true,
  "files": [
    {
      "filename": "clase_historia_01.mp3",
      "base_name": "clase_historia_01",
      "audio_path": "/path/to/audio.mp3",
      "has_transcription": true,
      "transcription_path": "/path/to/clase_historia_01.txt",
      "transcription_words": 3456,
      "has_summary": true,
      "summary_path": "/path/to/clase_historia_01_unificado.md",
      "formats": {
        "txt": true,
        "md": true,
        "docx": true,
        "pdf": true
      },
      "processed": true,
      "last_modified": "2024-01-10 15:30:00"
    }
  ]
}
```

**Implementación**:
```python
@app.route('/api/files-detailed')
def get_files_detailed():
    """Obtener lista detallada de archivos con transcripciones y resúmenes"""
    try:
        files = []
        downloads_path = settings.LOCAL_DOWNLOADS_PATH
        docx_path = settings.LOCAL_DOCX
        
        # Buscar archivos de audio
        for ext in settings.AUDIO_EXTENSIONS:
            for audio_file in downloads_path.glob(f"*{ext}"):
                base_name = audio_file.stem
                
                # Verificar transcripción
                txt_path = downloads_path / f"{base_name}.txt"
                has_transcription = txt_path.exists()
                transcription_words = 0
                if has_transcription:
                    with open(txt_path, 'r', encoding='utf-8') as f:
                        transcription_words = len(f.read().split())
                
                # Verificar resumen
                md_path = downloads_path / f"{base_name}_unificado.md"
                has_summary = md_path.exists()
                
                # Verificar formatos
                formats = {
                    'txt': has_transcription,
                    'md': has_summary,
                    'docx': (docx_path / f"{base_name}_unificado.docx").exists(),
                    'pdf': (downloads_path / f"{base_name}_unificado.pdf").exists()
                }
                
                files.append({
                    'filename': audio_file.name,
                    'base_name': base_name,
                    'audio_path': str(audio_file),
                    'has_transcription': has_transcription,
                    'transcription_path': str(txt_path) if has_transcription else None,
                    'transcription_words': transcription_words,
                    'has_summary': has_summary,
                    'summary_path': str(md_path) if has_summary else None,
                    'formats': formats,
                    'processed': has_transcription and has_summary,
                    'last_modified': datetime.fromtimestamp(
                        audio_file.stat().st_mtime
                    ).strftime('%Y-%m-%d %H:%M:%S')
                })
        
        return jsonify({
            'success': True,
            'files': sorted(files, key=lambda x: x['last_modified'], reverse=True),
            'total': len(files),
            'with_transcription': sum(1 for f in files if f['has_transcription']),
            'with_summary': sum(1 for f in files if f['has_summary'])
        })
        
    except Exception as e:
        app.logger.error(f"Error getting detailed files: {e}")
        return jsonify({
            'success': False,
            'message': f"Error: {str(e)}"
        }), 500
```

---

### 2. `templates/index.html` - Nuevos Componentes UI

#### 2.1 Panel de Preview (Sidebar Derecho)

**Ubicación**: Después del `files-container` existente

**HTML a agregar** (después de línea 1093):
```html
<!-- Panel de Preview -->
<div class="preview-panel" id="previewPanel">
    <div class="preview-header">
        <h3 id="previewTitle">Selecciona un archivo</h3>
        <button class="close-preview-btn" onclick="closePreview()">✕</button>
    </div>
    
    <div class="preview-tabs">
        <button class="tab-btn active" data-tab="transcription" onclick="switchTab('transcription')">
            📝 Transcripción
        </button>
        <button class="tab-btn" data-tab="summary" onclick="switchTab('summary')">
            📄 Resumen
        </button>
    </div>
    
    <div class="preview-content">
        <div class="tab-content active" id="transcriptionTab">
            <div class="content-stats">
                <span id="wordCount">0 palabras</span>
                <span id="charCount">0 caracteres</span>
            </div>
            <div class="text-preview" id="transcriptionText">
                Haz clic en un archivo para ver su transcripción
            </div>
        </div>
        
        <div class="tab-content" id="summaryTab">
            <div class="markdown-preview" id="summaryText">
                Haz clic en un archivo para ver su resumen
            </div>
        </div>
    </div>
    
    <div class="preview-actions">
        <button class="action-btn regenerate-btn" id="regenerateBtn" onclick="regenerateSummary()" disabled>
            🔄 Regenerar Resumen
        </button>
        <button class="action-btn download-btn" id="downloadBtn" onclick="downloadFiles()" disabled>
            ⬇️ Descargar
        </button>
    </div>
    
    <!-- Modal de progreso -->
    <div class="regenerate-progress" id="regenerateProgress">
        <div class="progress-spinner"></div>
        <p>Regenerando resumen con IA...</p>
        <small>Esto puede tomar 10-30 segundos</small>
    </div>
</div>
```

---

#### 2.2 Estilos CSS para el Panel de Preview

**Agregar al bloque `<style>`** (antes de línea 1009):
```css
/* ================================================
   PANEL DE PREVIEW - SIDEBAR DERECHO
   ================================================ */

.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;
    display: flex;
    flex-direction: column;
    transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.preview-panel.active {
    right: 0;
}

.preview-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 25px 30px;
    border-bottom: 1px solid var(--border-color);
    background: var(--bg-tertiary);
}

.preview-header h3 {
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0;
    max-width: 380px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.close-preview-btn {
    background: none;
    border: none;
    color: var(--text-secondary);
    font-size: 1.5rem;
    cursor: pointer;
    padding: 5px 10px;
    border-radius: 8px;
    transition: all 0.3s ease;
}

.close-preview-btn:hover {
    background: var(--bg-hover);
    color: var(--error-color);
}

/* Tabs */
.preview-tabs {
    display: flex;
    padding: 15px 20px;
    gap: 10px;
    border-bottom: 1px solid var(--border-color);
    background: var(--bg-secondary);
}

.tab-btn {
    flex: 1;
    padding: 12px 20px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    color: var(--text-secondary);
    font-size: 0.95rem;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
    font-family: 'Inter', sans-serif;
}

.tab-btn:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
    border-color: var(--accent-color);
}

.tab-btn.active {
    background: var(--accent-color);
    color: white;
    border-color: var(--accent-color);
}

/* Content */
.preview-content {
    flex: 1;
    overflow: hidden;
    position: relative;
}

.tab-content {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 20px;
    overflow-y: auto;
    display: none;
}

.tab-content.active {
    display: block;
    animation: fadeIn 0.3s ease;
}

.content-stats {
    display: flex;
    gap: 20px;
    padding: 10px 15px;
    background: var(--bg-tertiary);
    border-radius: 10px;
    margin-bottom: 15px;
    font-size: 0.9rem;
    color: var(--text-secondary);
}

.content-stats span {
    display: flex;
    align-items: center;
    gap: 5px;
}

.text-preview, .markdown-preview {
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 12px;
    padding: 20px;
    font-size: 0.95rem;
    line-height: 1.8;
    color: var(--text-primary);
    white-space: pre-wrap;
    word-wrap: break-word;
    max-height: calc(100vh - 350px);
    overflow-y: auto;
}

.markdown-preview h1 { font-size: 1.5rem; margin: 20px 0 10px 0; color: var(--accent-color); }
.markdown-preview h2 { font-size: 1.3rem; margin: 18px 0 8px 0; color: var(--text-primary); }
.markdown-preview h3 { font-size: 1.1rem; margin: 15px 0 6px 0; color: var(--text-secondary); }
.markdown-preview p { margin: 10px 0; }
.markdown-preview ul, .markdown-preview ol { padding-left: 25px; margin: 10px 0; }
.markdown-preview li { margin: 5px 0; }
.markdown-preview hr { border: none; border-top: 1px solid var(--border-color); margin: 20px 0; }

/* Actions */
.preview-actions {
    display: flex;
    gap: 12px;
    padding: 20px;
    border-top: 1px solid var(--border-color);
    background: var(--bg-tertiary);
}

.regenerate-btn {
    flex: 2;
    background: linear-gradient(135deg, var(--warning-color) 0%, #ffb84d 100%) !important;
    color: #000 !important;
}

.regenerate-btn:hover:not(:disabled) {
    box-shadow: 0 8px 25px rgba(249, 168, 38, 0.5) !important;
}

.download-btn {
    flex: 1;
    background: var(--bg-secondary) !important;
    border: 1px solid var(--border-color) !important;
    color: var(--text-primary) !important;
}

.download-btn:hover:not(:disabled) {
    border-color: var(--accent-color) !important;
    background: var(--bg-hover) !important;
}

/* Progress overlay */
.regenerate-progress {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(15, 17, 21, 0.95);
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 20px;
    z-index: 100;
}

.regenerate-progress.active {
    display: flex;
}

.progress-spinner {
    width: 60px;
    height: 60px;
    border: 4px solid var(--border-color);
    border-top-color: var(--accent-color);
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

.regenerate-progress p {
    font-size: 1.2rem;
    font-weight: 600;
    color: var(--text-primary);
}

.regenerate-progress small {
    color: var(--text-tertiary);
}

/* Responsive */
@media (max-width: 768px) {
    .preview-panel {
        width: 100%;
        right: -100%;
    }
}
```

---

#### 2.3 JavaScript para el Panel de Preview

**Agregar al bloque `<script>`** (antes de línea 1584):
```javascript
// ================================================
// PANEL DE PREVIEW - FUNCIONES
// ================================================

let currentPreviewFile = null;

// Abrir panel de preview al hacer clic en un archivo
function openPreview(filename) {
    currentPreviewFile = filename;
    const panel = document.getElementById('previewPanel');
    const title = document.getElementById('previewTitle');
    
    // Actualizar título
    title.textContent = filename;
    
    // Cargar transcripción
    loadTranscription(filename);
    
    // Cargar resumen
    loadSummary(filename);
    
    // Mostrar panel
    panel.classList.add('active');
    
    // Activar botones si hay transcripción
    document.getElementById('regenerateBtn').disabled = false;
    document.getElementById('downloadBtn').disabled = false;
}

// Cerrar panel de preview
function closePreview() {
    const panel = document.getElementById('previewPanel');
    panel.classList.remove('active');
    currentPreviewFile = null;
}

// Cambiar tab
function switchTab(tabName) {
    // Actualizar botones de tab
    document.querySelectorAll('.tab-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.tab === tabName);
    });
    
    // Actualizar contenido
    document.querySelectorAll('.tab-content').forEach(content => {
        content.classList.toggle('active', content.id === `${tabName}Tab`);
    });
}

// Cargar transcripción
async function loadTranscription(filename) {
    const baseName = filename.replace(/\.[^/.]+$/, ''); // Quitar extensión
    
    try {
        const response = await fetch(`/api/transcription/${encodeURIComponent(baseName)}`);
        const data = await response.json();
        
        if (data.success) {
            document.getElementById('transcriptionText').textContent = data.transcription;
            document.getElementById('wordCount').textContent = `${data.word_count} palabras`;
            document.getElementById('charCount').textContent = `${data.char_count} caracteres`;
        } else {
            document.getElementById('transcriptionText').textContent = 
                '⚠️ Transcripción no disponible. Procesa el archivo primero.';
            document.getElementById('wordCount').textContent = '0 palabras';
            document.getElementById('charCount').textContent = '0 caracteres';
        }
    } catch (error) {
        console.error('Error loading transcription:', error);
        document.getElementById('transcriptionText').textContent = 
            '❌ Error al cargar la transcripción';
    }
}

// Cargar resumen
async function loadSummary(filename) {
    const baseName = filename.replace(/\.[^/.]+$/, ''); // Quitar extensión
    
    try {
        const response = await fetch(`/api/summary/${encodeURIComponent(baseName)}`);
        const data = await response.json();
        
        if (data.success) {
            // Renderizar Markdown a HTML básico
            document.getElementById('summaryText').innerHTML = renderMarkdown(data.summary);
        } else {
            document.getElementById('summaryText').innerHTML = 
                '<p style="color: var(--warning-color);">⚠️ Resumen no disponible. Genera un resumen primero.</p>';
        }
    } catch (error) {
        console.error('Error loading summary:', error);
        document.getElementById('summaryText').innerHTML = 
            '<p style="color: var(--error-color);">❌ Error al cargar el resumen</p>';
    }
}

// Renderizar Markdown básico a HTML
function renderMarkdown(text) {
    if (!text) return '';
    
    return text
        // Headers
        .replace(/^### (.*$)/gm, '<h3>$1</h3>')
        .replace(/^## (.*$)/gm, '<h2>$1</h2>')
        .replace(/^# (.*$)/gm, '<h1>$1</h1>')
        // Bold
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        // Italic
        .replace(/\*(.*?)\*/g, '<em>$1</em>')
        // Lists
        .replace(/^\- (.*$)/gm, '<li>$1</li>')
        .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
        // Line breaks
        .replace(/\n\n/g, '</p><p>')
        .replace(/\n/g, '<br>')
        // Horizontal rule
        .replace(/---/g, '<hr>')
        // Wrap in paragraph
        .replace(/^(.+)$/gm, '<p>$1</p>')
        // Clean up empty paragraphs
        .replace(/<p><\/p>/g, '')
        .replace(/<p><h/g, '<h')
        .replace(/<\/h(\d)><\/p>/g, '</h$1>')
        .replace(/<p><ul>/g, '<ul>')
        .replace(/<\/ul><\/p>/g, '</ul>')
        .replace(/<p><hr><\/p>/g, '<hr>');
}

// Regenerar resumen
async function regenerateSummary() {
    if (!currentPreviewFile) return;
    
    const baseName = currentPreviewFile.replace(/\.[^/.]+$/, '');
    
    // Confirmar acción
    const confirmed = await showConfirmDialog(
        '🔄 Regenerar Resumen',
        `¿Estás seguro de que quieres regenerar el resumen de "${baseName}"?\n\n` +
        'Esto sobrescribirá el resumen actual con uno nuevo generado por IA.'
    );
    
    if (!confirmed) return;
    
    // Mostrar progreso
    const progressEl = document.getElementById('regenerateProgress');
    progressEl.classList.add('active');
    
    try {
        const response = await fetch('/api/regenerate-summary', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                filename: baseName
            })
        });
        
        const data = await response.json();
        
        if (data.success) {
            showMessage(`✅ Resumen regenerado en ${data.processing_time}s`, 'success');
            
            // Recargar resumen
            await loadSummary(currentPreviewFile);
            
            // Cambiar a tab de resumen
            switchTab('summary');
            
            // Recargar lista de archivos
            await loadFiles();
        } else {
            showMessage('❌ ' + data.message, 'error');
        }
    } catch (error) {
        console.error('Error regenerating summary:', error);
        showMessage('❌ Error de conexión al regenerar', 'error');
    } finally {
        progressEl.classList.remove('active');
    }
}

// Descargar archivos
function downloadFiles() {
    if (!currentPreviewFile) return;
    
    const baseName = currentPreviewFile.replace(/\.[^/.]+$/, '');
    
    // Abrir menú de descargas
    const formats = ['txt', 'md', 'docx', 'pdf'];
    const menu = document.createElement('div');
    menu.className = 'download-menu';
    menu.innerHTML = `
        <div style="
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: var(--bg-secondary);
            border: 1px solid var(--border-color);
            border-radius: 16px;
            padding: 30px;
            z-index: 10001;
            box-shadow: var(--shadow-xl);
        ">
            <h3 style="margin-bottom: 20px; color: var(--text-primary);">⬇️ Descargar Archivos</h3>
            <div style="display: flex; flex-direction: column; gap: 10px;">
                ${formats.map(fmt => `
                    <a href="/downloads/${baseName}_unificado.${fmt}" 
                       target="_blank" 
                       style="
                           padding: 12px 20px;
                           background: var(--bg-tertiary);
                           border: 1px solid var(--border-color);
                           border-radius: 10px;
                           color: var(--text-primary);
                           text-decoration: none;
                           display: flex;
                           align-items: center;
                           gap: 10px;
                           transition: all 0.3s ease;
                       "
                       onmouseover="this.style.borderColor='var(--accent-color)'"
                       onmouseout="this.style.borderColor='var(--border-color)'"
                    >
                        ${fmt === 'txt' ? '📝' : fmt === 'md' ? '📋' : fmt === 'docx' ? '📄' : '📑'}
                        ${fmt.toUpperCase()}
                    </a>
                `).join('')}
            </div>
            <button onclick="this.parentElement.parentElement.remove()" 
                    style="
                        margin-top: 20px;
                        width: 100%;
                        padding: 12px;
                        background: var(--bg-tertiary);
                        border: 1px solid var(--border-color);
                        border-radius: 10px;
                        color: var(--text-primary);
                        cursor: pointer;
                        font-family: 'Inter', sans-serif;
                    "
            >Cerrar</button>
        </div>
        <div style="
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.7);
            z-index: 10000;
        " onclick="this.parentElement.remove()"></div>
    `;
    document.body.appendChild(menu);
}
```

---

#### 2.4 Modificar renderFiles() para agregar click handler

**Buscar y modificar** en la función `renderFiles()` (alrededor de línea 1171):

**ANTES**:
```javascript
return `
    <div class="file-card" data-filename="${file.filename.toLowerCase()}">
```

**DESPUÉS**:
```javascript
return `
    <div class="file-card" data-filename="${file.filename.toLowerCase()}" onclick="openPreview('${file.filename}')">
```

---

### 3. Iniciar el Dashboard

#### 3.1 Crear script de inicio `run_dashboard.py`

**Ubicación**: `/home/ren/proyectos/cbc/run_dashboard.py`

```python
#!/usr/bin/env python3
"""
Iniciar el dashboard de CBCFacil
"""
import os
import sys

# Agregar directorio raíz al path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

# Cargar variables de entorno
from dotenv import load_dotenv
load_dotenv()

# Importar y crear app Flask
from api.routes import create_app

app = create_app()

if __name__ == '__main__':
    print("=" * 60)
    print("  🚀 CBCFacil Dashboard")
    print("  📍 http://localhost:5000")
    print("=" * 60)
    
    app.run(
        host='0.0.0.0',
        port=5000,
        debug=True,
        threaded=True
    )
```

---

## 🔧 INSTRUCCIONES DE IMPLEMENTACIÓN PASO A PASO

### FASE 1: Backend - Nuevos Endpoints (30 min)

**Archivo**: `api/routes.py`

1. [ ] Agregar import `from pathlib import Path` si no existe
2. [ ] Agregar import `from document.generators import DocumentGenerator`
3. [ ] Agregar endpoint `GET /api/transcription/<filename>`
4. [ ] Agregar endpoint `GET /api/summary/<filename>`
5. [ ] Agregar endpoint `POST /api/regenerate-summary`
6. [ ] Agregar endpoint `GET /api/files-detailed`
7. [ ] Probar cada endpoint con curl:
   ```bash
   curl http://localhost:5000/api/transcription/nombre_archivo
   curl http://localhost:5000/api/summary/nombre_archivo
   curl -X POST http://localhost:5000/api/regenerate-summary \
        -H "Content-Type: application/json" \
        -d '{"filename": "nombre_archivo"}'
   ```

---

### FASE 2: Frontend - Panel de Preview (45 min)

**Archivo**: `templates/index.html`

1. [ ] Agregar HTML del panel de preview después de línea 1093
2. [ ] Agregar estilos CSS antes de línea 1009
3. [ ] Agregar funciones JavaScript antes de línea 1584
4. [ ] Modificar `renderFiles()` para agregar `onclick="openPreview(...)"`
5. [ ] Probar en navegador:
   - Hacer clic en un archivo → debe abrir panel
   - Cambiar tabs → debe mostrar transcripción/resumen
   - Botón regenerar → debe llamar API y actualizar

---

### FASE 3: Script de Inicio (10 min)

1. [ ] Crear archivo `run_dashboard.py`
2. [ ] Hacer ejecutable: `chmod +x run_dashboard.py`
3. [ ] Probar: `python run_dashboard.py`

---

### FASE 4: Testing Manual (15 min)

1. [ ] Iniciar dashboard: `python run_dashboard.py`
2. [ ] Abrir http://localhost:5000
3. [ ] Verificar lista de archivos carga correctamente
4. [ ] Hacer clic en un archivo procesado → verificar que muestra transcripción
5. [ ] Cambiar a tab "Resumen" → verificar que muestra el resumen
6. [ ] Hacer clic en "Regenerar Resumen" → verificar que regenera
7. [ ] Verificar que los archivos .md, .docx, .pdf se actualizan

---

## 📝 NOTAS IMPORTANTES PARA EL DESARROLLADOR

### Rutas de Archivos

```python
# Transcripciones (.txt)
settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.txt"
# Ejemplo: /home/ren/proyectos/cbc/downloads/clase_historia_01.txt

# Resúmenes Markdown (.md)
settings.LOCAL_DOWNLOADS_PATH / f"{base_name}_unificado.md"
# Ejemplo: /home/ren/proyectos/cbc/downloads/clase_historia_01_unificado.md

# DOCX
settings.LOCAL_DOCX / f"{base_name}_unificado.docx"
# Ejemplo: /home/ren/proyectos/cbc/resumenes_docx/clase_historia_01_unificado.docx

# PDF
settings.LOCAL_DOWNLOADS_PATH / f"{base_name}_unificado.pdf"
# Ejemplo: /home/ren/proyectos/cbc/downloads/clase_historia_01_unificado.pdf
```

### Configuración Necesaria

El archivo `.env` debe tener configurado:
- `GEMINI_API_KEY` o `ANTHROPIC_AUTH_TOKEN` para regenerar resúmenes
- `NEXTCLOUD_URL`, `NEXTCLOUD_USER`, `NEXTCLOUD_PASSWORD` (opcional, para sincronizar)

### Dependencias Python

```python
# Ya instaladas en requirements.txt
flask
flask-cors
python-docx
reportlab
python-dotenv
```

---

## ✅ CHECKLIST FINAL

### Backend
- [ ] Endpoint `/api/transcription/<filename>` funciona
- [ ] Endpoint `/api/summary/<filename>` funciona
- [ ] Endpoint `/api/regenerate-summary` funciona
- [ ] Endpoint `/api/files-detailed` funciona
- [ ] Manejo de errores correcto (404, 500)
- [ ] Logs de debug activos

### Frontend
- [ ] Panel de preview se abre al hacer clic
- [ ] Panel de preview se cierra con X
- [ ] Tabs funcionan (transcripción/resumen)
- [ ] Contador de palabras/caracteres funciona
- [ ] Markdown se renderiza correctamente
- [ ] Botón "Regenerar Resumen" funciona
- [ ] Spinner de progreso aparece
- [ ] Mensajes de éxito/error aparecen
- [ ] Responsive en móvil

### Integración
- [ ] Dashboard inicia con `python run_dashboard.py`
- [ ] Archivos regenerados se suben a Nextcloud (si configurado)
- [ ] No hay errores en consola del navegador
- [ ] No hay errores en logs del servidor

---

*Documento creado para implementación por Minimax M2*
*Última actualización: 2026-01-10*
