NoeMartinezSanchez commited on
Commit
e0f016c
·
1 Parent(s): 82f8a5c

Add hierarchical menu navigation from Excel

Browse files
Files changed (5) hide show
  1. .gitignore +0 -0
  2. api/main.py +36 -1
  3. data/build_menu_json.py +206 -0
  4. requirements.txt +6 -0
  5. static/index.html +277 -0
.gitignore CHANGED
Binary files a/.gitignore and b/.gitignore differ
 
api/main.py CHANGED
@@ -20,6 +20,7 @@ if project_root not in sys.path:
20
  from config.settings import settings, print_config_summary
21
  from config.models import ChatRequest, ChatResponse, FeedbackRequest
22
  from rag.core import RAGSystem
 
23
 
24
  # Configurar logging
25
  logging.basicConfig(level=logging.INFO)
@@ -50,6 +51,9 @@ rag_system = RAGSystem()
50
  feedback_store = {}
51
  conversation_store = {}
52
 
 
 
 
53
  # Montar archivos estáticos
54
  app.mount("/static", StaticFiles(directory="static"), name="static")
55
 
@@ -57,14 +61,38 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
57
  async def startup_event():
58
  """Inicializar sistema al arrancar"""
59
  try:
60
- print_config_summary() # <-- MUESTRA CONFIGURACIÓN
 
61
  # Cargar intents
62
  rag_system.load_intents("data/intents.json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  logger.info("Sistema RAG inicializado correctamente")
64
  logger.info("Interfaz web disponible en: http://localhost:8000")
65
  logger.info("API Docs disponible en: http://localhost:8000/api/docs")
66
  except Exception as e:
67
  logger.error(f"Error inicializando RAG: {e}")
 
68
 
69
  @app.get("/")
70
  async def root():
@@ -231,6 +259,13 @@ async def get_rag_stats():
231
  except Exception as e:
232
  raise HTTPException(status_code=500, detail=str(e))
233
 
 
 
 
 
 
 
 
234
  if __name__ == "__main__":
235
  import uvicorn
236
  logger.info(f"🚀 Iniciando servidor en {settings.API_HOST}:{settings.API_PORT}")
 
20
  from config.settings import settings, print_config_summary
21
  from config.models import ChatRequest, ChatResponse, FeedbackRequest
22
  from rag.core import RAGSystem
23
+ from data.build_menu_json import build_menu_json, load_menu_json
24
 
25
  # Configurar logging
26
  logging.basicConfig(level=logging.INFO)
 
51
  feedback_store = {}
52
  conversation_store = {}
53
 
54
+ # Estado del menú jerárquico
55
+ app.state.menu = {}
56
+
57
  # Montar archivos estáticos
58
  app.mount("/static", StaticFiles(directory="static"), name="static")
59
 
 
61
  async def startup_event():
62
  """Inicializar sistema al arrancar"""
63
  try:
64
+ print_config_summary()
65
+
66
  # Cargar intents
67
  rag_system.load_intents("data/intents.json")
68
+
69
+ # Cargar o generar menú jerárquico
70
+ menu_json_path = "data/menu.json"
71
+ excel_path = "data/Navegación Jerárquica_FER.xlsx"
72
+
73
+ if os.path.exists(menu_json_path):
74
+ app.state.menu = load_menu_json(menu_json_path)
75
+ logger.info(f"✅ Menú cargado desde: {menu_json_path}")
76
+ logger.info(f" Categorías: {len(app.state.menu)}")
77
+ elif os.path.exists(excel_path):
78
+ logger.info("📋 Generando menú desde Excel...")
79
+ if build_menu_json(excel_path, menu_json_path):
80
+ app.state.menu = load_menu_json(menu_json_path)
81
+ logger.info(f"✅ Menú generado y cargado: {len(app.state.menu)} categorías")
82
+ else:
83
+ logger.warning("⚠️ No se pudo generar el menú desde Excel")
84
+ app.state.menu = {}
85
+ else:
86
+ logger.warning(f"⚠️ Archivo Excel no encontrado: {excel_path}")
87
+ logger.warning(" El menú jerárquico no estará disponible")
88
+ app.state.menu = {}
89
+
90
  logger.info("Sistema RAG inicializado correctamente")
91
  logger.info("Interfaz web disponible en: http://localhost:8000")
92
  logger.info("API Docs disponible en: http://localhost:8000/api/docs")
93
  except Exception as e:
94
  logger.error(f"Error inicializando RAG: {e}")
95
+ app.state.menu = {}
96
 
97
  @app.get("/")
98
  async def root():
 
259
  except Exception as e:
260
  raise HTTPException(status_code=500, detail=str(e))
261
 
262
+ @app.get("/menu")
263
+ async def get_menu():
264
+ """Endpoint para obtener la estructura del menú jerárquico"""
265
+ if hasattr(app.state, 'menu') and app.state.menu:
266
+ return {"menu": app.state.menu}
267
+ return {"menu": {}}
268
+
269
  if __name__ == "__main__":
270
  import uvicorn
271
  logger.info(f"🚀 Iniciando servidor en {settings.API_HOST}:{settings.API_PORT}")
data/build_menu_json.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script para construir el archivo JSON del menú desde un Excel.
3
+
4
+ Lee todas las hojas del archivo Excel y las convierte en una estructura
5
+ jerárquica JSON con: Categoría → Subcategoría → Pregunta → Respuesta.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import logging
11
+ from typing import Dict, List, Any, Optional
12
+
13
+ import pandas as pd
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def normalize_column_name(col: str) -> str:
20
+ """
21
+ Normaliza nombres de columnas para coincidir con los esperados.
22
+
23
+ Args:
24
+ col: Nombre original de la columna
25
+
26
+ Returns:
27
+ Nombre normalizado en minúsculas sin acentos
28
+ """
29
+ col_lower = col.lower().strip()
30
+ mapping = {
31
+ 'categoría': 'categoria',
32
+ 'subcategoría': 'subcategoria',
33
+ 'asunto': 'asunto',
34
+ 'solución': 'solucion',
35
+ 'pregunta': 'solucion',
36
+ 'respuesta': 'respuesta',
37
+ 'url': 'url',
38
+ 'formato': 'formato',
39
+ 'etiquetas': 'etiquetas',
40
+ }
41
+ return mapping.get(col_lower, col_lower)
42
+
43
+
44
+ def get_column_value(row: pd.Series, possible_names: List[str]) -> Optional[str]:
45
+ """
46
+ Obtiene el valor de una columna probando múltiples nombres posibles.
47
+
48
+ Args:
49
+ row: Fila del DataFrame
50
+ possible_names: Lista de nombres de columna posibles
51
+
52
+ Returns:
53
+ Valor de la columna o None si no se encuentra
54
+ """
55
+ for col_name in possible_names:
56
+ if col_name in row.index and pd.notna(row[col_name]):
57
+ return str(row[col_name]).strip()
58
+ return None
59
+
60
+
61
+ def extract_additional_columns(row: pd.Series) -> Dict[str, Any]:
62
+ """
63
+ Extrae columnas adicionales como metadatos.
64
+
65
+ Args:
66
+ row: Fila del DataFrame
67
+
68
+ Returns:
69
+ Diccionario con columnas adicionales
70
+ """
71
+ main_columns = {'categoria', 'subcategoria', 'asunto', 'solucion', 'respuesta'}
72
+ additional = {}
73
+
74
+ for col in row.index:
75
+ col_normalized = normalize_column_name(col)
76
+ if col_normalized not in main_columns and pd.notna(row[col]):
77
+ additional[col_normalized] = str(row[col]).strip()
78
+
79
+ return additional
80
+
81
+
82
+ def build_menu_json(excel_path: str, output_json_path: str) -> bool:
83
+ """
84
+ Lee el archivo Excel y genera el JSON del menú.
85
+
86
+ Args:
87
+ excel_path: Ruta al archivo Excel
88
+ output_json_path: Ruta donde se guardará el JSON
89
+
90
+ Returns:
91
+ True si se generó correctamente, False si hubo errores
92
+ """
93
+ if not os.path.exists(excel_path):
94
+ logger.warning(f"⚠️ Archivo Excel no encontrado: {excel_path}")
95
+ return False
96
+
97
+ try:
98
+ logger.info(f"📊 Leyendo Excel: {excel_path}")
99
+
100
+ excel_file = pd.ExcelFile(excel_path)
101
+ sheet_names = excel_file.sheet_names
102
+ logger.info(f"📋 Hojas encontradas: {sheet_names}")
103
+
104
+ menu_structure = {}
105
+
106
+ for sheet_name in sheet_names:
107
+ try:
108
+ logger.info(f" → Procesando hoja: {sheet_name}")
109
+
110
+ df = pd.read_excel(excel_file, sheet_name=sheet_name)
111
+
112
+ if df.empty:
113
+ logger.warning(f" ⚠️ Hoja vacía: {sheet_name}")
114
+ continue
115
+
116
+ category_name = sheet_name.strip()
117
+ subcategories: Dict[str, List[Dict]] = {}
118
+
119
+ for idx, row in df.iterrows():
120
+ subcategory = get_column_value(row, ['subcategoria', 'asunto', 'categoría', 'subcategoría'])
121
+ question = get_column_value(row, ['solucion', 'pregunta', 'solución', 'pregunta'])
122
+ answer = get_column_value(row, ['respuesta'])
123
+
124
+ if not question or not answer:
125
+ continue
126
+
127
+ subcat_key = subcategory if subcategory else "General"
128
+
129
+ if subcat_key not in subcategories:
130
+ subcategories[subcat_key] = []
131
+
132
+ question_data = {
133
+ "question": question,
134
+ "answer": answer,
135
+ }
136
+
137
+ additional_cols = extract_additional_columns(row)
138
+ if additional_cols:
139
+ question_data["metadata"] = additional_cols
140
+
141
+ subcategories[subcat_key].append(question_data)
142
+
143
+ if subcategories:
144
+ menu_structure[category_name] = subcategories
145
+ logger.info(f" ✓ Categoría '{category_name}': {len(subcategories)} subcategorías")
146
+ else:
147
+ logger.warning(f" ⚠️ Sin datos válidos: {sheet_name}")
148
+
149
+ except Exception as e:
150
+ logger.error(f" ❌ Error procesando hoja '{sheet_name}': {e}")
151
+ continue
152
+
153
+ if not menu_structure:
154
+ logger.warning("⚠️ No se generó ninguna categoría")
155
+ return False
156
+
157
+ os.makedirs(os.path.dirname(output_json_path), exist_ok=True)
158
+
159
+ with open(output_json_path, 'w', encoding='utf-8') as f:
160
+ json.dump(menu_structure, f, ensure_ascii=False, indent=2)
161
+
162
+ logger.info(f"✅ JSON guardado en: {output_json_path}")
163
+ logger.info(f" Categorías: {len(menu_structure)}")
164
+
165
+ return True
166
+
167
+ except Exception as e:
168
+ logger.error(f"❌ Error general al generar JSON: {e}")
169
+ return False
170
+
171
+
172
+ def load_menu_json(json_path: str) -> Dict[str, Any]:
173
+ """
174
+ Carga el menú desde el archivo JSON.
175
+
176
+ Args:
177
+ json_path: Ruta al archivo JSON
178
+
179
+ Returns:
180
+ Diccionario con la estructura del menú
181
+ """
182
+ if not os.path.exists(json_path):
183
+ return {}
184
+
185
+ try:
186
+ with open(json_path, 'r', encoding='utf-8') as f:
187
+ return json.load(f)
188
+ except Exception as e:
189
+ logger.error(f"❌ Error cargando JSON: {e}")
190
+ return {}
191
+
192
+
193
+ if __name__ == "__main__":
194
+ excel_path = "data/Navegación Jerárquica_FER.xlsx"
195
+ output_path = "data/menu.json"
196
+
197
+ logger.info("🚀 Iniciando generación de menú JSON")
198
+
199
+ success = build_menu_json(excel_path, output_path)
200
+
201
+ if success:
202
+ logger.info("✅ Proceso completado")
203
+ menu = load_menu_json(output_path)
204
+ logger.info(f"Estructura: {json.dumps(menu, ensure_ascii=False, indent=2)[:500]}...")
205
+ else:
206
+ logger.error("❌ Proceso fallido")
requirements.txt CHANGED
@@ -51,6 +51,12 @@ tiktoken==0.5.1
51
  psutil==5.9.8
52
  pynvml==11.5.0 # Para monitorear GPU NVIDIA
53
 
 
 
 
 
 
 
54
  # Serialización (para guardar/recuperar índices)
55
  joblib==1.3.2
56
 
 
51
  psutil==5.9.8
52
  pynvml==11.5.0 # Para monitorear GPU NVIDIA
53
 
54
+ # ============================================
55
+ # Excel and Data Processing
56
+ # ============================================
57
+ openpyxl==3.1.2
58
+ pandas==2.0.3
59
+
60
  # Serialización (para guardar/recuperar índices)
61
  joblib==1.3.2
62
 
static/index.html CHANGED
@@ -123,6 +123,144 @@
123
  transform: translateX(5px);
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  /* Área principal del chat */
127
  .main-content {
128
  flex: 1;
@@ -415,6 +553,13 @@
415
  ¿Cuál es el proceso de reingreso?
416
  </div>
417
  </div>
 
 
 
 
 
 
 
418
  </div>
419
 
420
  <!-- Área principal -->
@@ -474,6 +619,11 @@
474
  let messageCount = 0;
475
  let ragResponseCount = 0;
476
  let totalConfidence = 0;
 
 
 
 
 
477
 
478
  // Inicialización
479
  document.addEventListener('DOMContentLoaded', function() {
@@ -487,6 +637,9 @@
487
  // Cargar estadísticas iniciales
488
  loadStats();
489
 
 
 
 
490
  // Enfocar el input
491
  document.getElementById('messageInput').focus();
492
  });
@@ -759,6 +912,130 @@
759
  }
760
  }
761
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  // Seguridad: escapar HTML
763
  function escapeHtml(text) {
764
  const div = document.createElement('div');
 
123
  transform: translateX(5px);
124
  }
125
 
126
+ /* Menú jerárquico */
127
+ .menu-section {
128
+ margin-top: 20px;
129
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
130
+ padding-top: 20px;
131
+ }
132
+
133
+ .menu-section h3 {
134
+ margin-bottom: 15px;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ }
139
+
140
+ .menu-container {
141
+ max-height: 300px;
142
+ overflow-y: auto;
143
+ }
144
+
145
+ .menu-category {
146
+ margin-bottom: 8px;
147
+ }
148
+
149
+ .menu-btn {
150
+ width: 100%;
151
+ background: rgba(255, 255, 255, 0.15);
152
+ border: none;
153
+ border-radius: 8px;
154
+ padding: 12px;
155
+ color: white;
156
+ cursor: pointer;
157
+ text-align: left;
158
+ font-size: 14px;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ transition: all 0.3s;
163
+ }
164
+
165
+ .menu-btn:hover {
166
+ background: rgba(255, 255, 255, 0.25);
167
+ transform: translateX(5px);
168
+ }
169
+
170
+ .menu-btn.active {
171
+ background: var(--accent-color);
172
+ }
173
+
174
+ .menu-btn i {
175
+ transition: transform 0.3s;
176
+ }
177
+
178
+ .menu-btn.expanded i {
179
+ transform: rotate(90deg);
180
+ }
181
+
182
+ .menu-subcategory {
183
+ display: none;
184
+ margin-left: 20px;
185
+ margin-top: 5px;
186
+ }
187
+
188
+ .menu-subcategory.visible {
189
+ display: block;
190
+ }
191
+
192
+ .menu-question {
193
+ display: none;
194
+ margin-left: 20px;
195
+ margin-top: 5px;
196
+ }
197
+
198
+ .menu-question.visible {
199
+ display: block;
200
+ }
201
+
202
+ .subcategory-btn {
203
+ width: 100%;
204
+ background: rgba(255, 255, 255, 0.1);
205
+ border: none;
206
+ border-radius: 6px;
207
+ padding: 10px;
208
+ color: white;
209
+ cursor: pointer;
210
+ text-align: left;
211
+ font-size: 13px;
212
+ margin-bottom: 5px;
213
+ transition: all 0.3s;
214
+ }
215
+
216
+ .subcategory-btn:hover {
217
+ background: rgba(255, 255, 255, 0.2);
218
+ }
219
+
220
+ .question-btn {
221
+ width: 100%;
222
+ background: rgba(26, 188, 156, 0.2);
223
+ border: 1px solid rgba(26, 188, 156, 0.3);
224
+ border-radius: 6px;
225
+ padding: 8px 10px;
226
+ color: white;
227
+ cursor: pointer;
228
+ text-align: left;
229
+ font-size: 12px;
230
+ margin-bottom: 4px;
231
+ transition: all 0.3s;
232
+ }
233
+
234
+ .question-btn:hover {
235
+ background: rgba(26, 188, 156, 0.4);
236
+ }
237
+
238
+ .menu-back-btn {
239
+ background: rgba(255, 255, 255, 0.1);
240
+ border: none;
241
+ border-radius: 6px;
242
+ padding: 8px 12px;
243
+ color: white;
244
+ cursor: pointer;
245
+ font-size: 13px;
246
+ margin-bottom: 10px;
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 8px;
250
+ transition: all 0.3s;
251
+ }
252
+
253
+ .menu-back-btn:hover {
254
+ background: rgba(255, 255, 255, 0.2);
255
+ }
256
+
257
+ .menu-empty {
258
+ text-align: center;
259
+ padding: 20px;
260
+ color: rgba(255, 255, 255, 0.6);
261
+ font-size: 14px;
262
+ }
263
+
264
  /* Área principal del chat */
265
  .main-content {
266
  flex: 1;
 
553
  ¿Cuál es el proceso de reingreso?
554
  </div>
555
  </div>
556
+
557
+ <div class="menu-section">
558
+ <h3><i class="fas fa-folder-tree"></i> Navegación por Temas</h3>
559
+ <div id="menuContainer" class="menu-container">
560
+ <div class="menu-empty">Cargando menú...</div>
561
+ </div>
562
+ </div>
563
  </div>
564
 
565
  <!-- Área principal -->
 
619
  let messageCount = 0;
620
  let ragResponseCount = 0;
621
  let totalConfidence = 0;
622
+
623
+ // Estado del menú jerárquico
624
+ let menuData = {};
625
+ let currentCategory = null;
626
+ let currentSubcategory = null;
627
 
628
  // Inicialización
629
  document.addEventListener('DOMContentLoaded', function() {
 
637
  // Cargar estadísticas iniciales
638
  loadStats();
639
 
640
+ // Cargar menú jerárquico
641
+ loadMenu();
642
+
643
  // Enfocar el input
644
  document.getElementById('messageInput').focus();
645
  });
 
912
  }
913
  }
914
 
915
+ // Cargar menú jerárquico
916
+ async function loadMenu() {
917
+ try {
918
+ const response = await fetch(`${API_URL}/menu`);
919
+ if (response.ok) {
920
+ const data = await response.json();
921
+ menuData = data.menu || {};
922
+ renderMenuCategories();
923
+ } else {
924
+ document.getElementById('menuContainer').innerHTML = '<div class="menu-empty">Menú no disponible</div>';
925
+ }
926
+ } catch (error) {
927
+ console.error('Error cargando menú:', error);
928
+ document.getElementById('menuContainer').innerHTML = '<div class="menu-empty">Error al cargar menú</div>';
929
+ }
930
+ }
931
+
932
+ // Renderizar categorías
933
+ function renderMenuCategories() {
934
+ const container = document.getElementById('menuContainer');
935
+
936
+ if (Object.keys(menuData).length === 0) {
937
+ container.innerHTML = '<div class="menu-empty">No hay temas disponibles</div>';
938
+ return;
939
+ }
940
+
941
+ let html = '';
942
+ Object.keys(menuData).forEach(category => {
943
+ html += `
944
+ <div class="menu-category">
945
+ <button class="menu-btn" onclick="toggleCategory('${escapeHtml(category)}')">
946
+ <i class="fas fa-chevron-right"></i>
947
+ ${escapeHtml(category)}
948
+ </button>
949
+ <div class="menu-subcategory" id="subcat-${escapeHtml(category)}">
950
+ ${renderSubcategories(category)}
951
+ </div>
952
+ </div>
953
+ `;
954
+ });
955
+ container.innerHTML = html;
956
+ }
957
+
958
+ // Renderizar subcategorías
959
+ function renderSubcategories(category) {
960
+ const subcategories = menuData[category];
961
+ let html = '';
962
+
963
+ Object.keys(subcategories).forEach(subcat => {
964
+ const questions = subcategories[subcat];
965
+ html += `
966
+ <button class="subcategory-btn" onclick="toggleSubcategory('${escapeHtml(category)}', '${escapeHtml(subcat)}')">
967
+ <i class="fas fa-folder"></i> ${escapeHtml(subcat)} (${questions.length})
968
+ </button>
969
+ <div class="menu-question" id="ques-${escapeHtml(category)}-${escapeHtml(subcat)}">
970
+ ${renderQuestions(category, subcat)}
971
+ </div>
972
+ `;
973
+ });
974
+
975
+ return html;
976
+ }
977
+
978
+ // Renderizar preguntas
979
+ function renderQuestions(category, subcategory) {
980
+ const questions = menuData[category][subcategory];
981
+ let html = '';
982
+
983
+ questions.forEach((q, idx) => {
984
+ html += `
985
+ <button class="question-btn" onclick="showAnswer('${escapeHtml(category)}', '${escapeHtml(subcategory)}', ${idx})">
986
+ <i class="fas fa-question-circle"></i> ${escapeHtml(q.question)}
987
+ </button>
988
+ `;
989
+ });
990
+
991
+ return html;
992
+ }
993
+
994
+ // Alternar categoría
995
+ function toggleCategory(category) {
996
+ const subcatDiv = document.getElementById(`subcat-${category}`);
997
+ const btn = subcatDiv.previousElementSibling;
998
+
999
+ subcatDiv.classList.toggle('visible');
1000
+ btn.classList.toggle('expanded');
1001
+ }
1002
+
1003
+ // Alternar subcategoría
1004
+ function toggleSubcategory(category, subcategory) {
1005
+ const quesDiv = document.getElementById(`ques-${category}-${subcategory}`);
1006
+ quesDiv.classList.toggle('visible');
1007
+ }
1008
+
1009
+ // Mostrar respuesta en el chat
1010
+ function showAnswer(category, subcategory, questionIdx) {
1011
+ const question = menuData[category][subcategory][questionIdx];
1012
+
1013
+ const chatMessages = document.getElementById('chatMessages');
1014
+
1015
+ // Crear mensaje del bot
1016
+ const messageDiv = document.createElement('div');
1017
+ messageDiv.className = 'message bot-message';
1018
+
1019
+ const contentDiv = document.createElement('div');
1020
+ contentDiv.className = 'message-content';
1021
+ contentDiv.innerHTML = `
1022
+ <strong><i class="fas fa-folder-tree"></i> Asistente:</strong><br>
1023
+ <span style="font-size: 12px; color: var(--accent-color);">Tema: ${escapeHtml(category)} > ${escapeHtml(subcategory)}</span><br><br>
1024
+ <strong>Pregunta:</strong> ${escapeHtml(question.question)}<br><br>
1025
+ <strong>Respuesta:</strong><br>${escapeHtml(question.answer)}
1026
+ `;
1027
+
1028
+ const timeDiv = document.createElement('div');
1029
+ timeDiv.className = 'message-time';
1030
+ timeDiv.textContent = getCurrentTime();
1031
+
1032
+ messageDiv.appendChild(contentDiv);
1033
+ messageDiv.appendChild(timeDiv);
1034
+ chatMessages.appendChild(messageDiv);
1035
+
1036
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1037
+ }
1038
+
1039
  // Seguridad: escapar HTML
1040
  function escapeHtml(text) {
1041
  const div = document.createElement('div');