Lukeetah commited on
Commit
4b1ba62
·
verified ·
1 Parent(s): cc5cb68

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -151
app.py CHANGED
@@ -28,7 +28,7 @@ class Config:
28
  # o se usará la configuración proporcionada por el usuario si no está definida.
29
  # En un entorno de producción real, se recomienda usar variables de entorno seguras.
30
  # El project_id es mateai-815ca, como se especificó.
31
- FIREBASE_COLLECTION_USERS = "artifacts/mateai-815ca/users" # Colección para datos de usuario
32
 
33
  # --- Firebase Initialization ---
34
  # Intenta inicializar Firebase Admin SDK.
@@ -38,7 +38,11 @@ try:
38
  # Intenta cargar las credenciales desde la variable de entorno GOOGLE_APPLICATION_CREDENTIALS_JSON
39
  firebase_credentials_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON', None)
40
  if firebase_credentials_json:
41
- cred = credentials.Certificate(json.loads(firebase_credentials_json))
 
 
 
 
42
  firebase_admin.initialize_app(cred)
43
  print("Firebase Admin SDK inicializado correctamente desde variable de entorno GOOGLE_APPLICATION_CREDENTIALS_JSON.")
44
  else:
@@ -46,8 +50,13 @@ try:
46
  print("ADVERTENCIA: GOOGLE_APPLICATION_CREDENTIALS_JSON no está configurada y no se pudo inicializar Firebase Admin SDK.")
47
  print("La persistencia de datos de usuario en Firestore NO funcionará.")
48
  # No inicializamos firebase_admin si no tenemos credenciales de servicio válidas.
49
- except ValueError:
50
- pass # Ya inicializado
 
 
 
 
 
51
 
52
  db = firestore.client() if firebase_admin._apps else None # Solo si Firebase se inicializó con éxito
53
 
@@ -63,7 +72,7 @@ class UserManager:
63
 
64
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
65
  try:
66
- user_doc_ref.set(new_user.to_dict())
67
  return new_user, f"¡Bienvenido, {name}! Tu ID de usuario es: {user_id}. ¡MateAI está listo para cebarte la vida!"
68
  except Exception as e:
69
  return None, f"Error al crear usuario en Firestore: {e}"
@@ -73,7 +82,7 @@ class UserManager:
73
  if not db: return None, "Error: La base de datos no está disponible. Por favor, configura Firebase Admin SDK."
74
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
75
  try:
76
- doc = user_doc_ref.get()
77
  if doc.exists:
78
  user_data = doc.to_dict()
79
  loaded_user = User(
@@ -96,7 +105,7 @@ class UserManager:
96
  if not db: return None
97
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
98
  try:
99
- doc = user_doc_ref.get()
100
  if doc.exists:
101
  user_data = doc.to_dict()
102
  return User(
@@ -119,7 +128,7 @@ class UserManager:
119
  if not db: return False
120
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_obj.user_id)
121
  try:
122
- user_doc_ref.update(user_obj.to_dict())
123
  return True
124
  except Exception as e:
125
  print(f"Error al actualizar datos de usuario en Firestore: {e}")
@@ -399,21 +408,16 @@ class NudgeGenerator:
399
  # --- Lógica de Priorización de Tareas (¡NUEVO RAZONAMIENTO!) ---
400
  # Si hay tareas pendientes y es un buen momento para recordarlas
401
  tasks_to_nudge = []
402
- for task_item in user.tasks:
403
- task_name = task_item['task']
404
- last_nudged_str = task_item.get('last_nudged')
405
- last_nudged = datetime.strptime(last_nudged_str, '%Y-%m-%d %H:%M:%S.%f') if last_nudged_str else None
406
-
407
- if not last_nudged or (datetime.now() - last_nudged) > timedelta(hours=Config.TASK_NUDGE_COOLDOWN_HOURS):
408
- # Simula un pequeño "razonamiento" sobre cuándo es buen momento para la tarea
409
- if "Mañana" in time_context and "Casa" in location and "comprar" in task_name.lower():
410
- tasks_to_nudge.append(task_item)
411
- elif "Mediodía/Tarde" in time_context and "llamar" in task_name.lower():
412
- tasks_to_nudge.append(task_item)
413
- elif "Noche" in time_context and "leer" in task_name.lower():
414
- tasks_to_nudge.append(task_item)
415
- elif "General" in task_item.get('context_tags', []): # Tareas sin contexto específico
416
- tasks_to_nudge.append(task_item)
417
 
418
  if tasks_to_nudge:
419
  chosen_task = random.choice(tasks_to_nudge)
@@ -791,7 +795,12 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
791
 
792
  recognition.onresult = function(event) {
793
  const transcript = event.results[0][0].transcript;
794
- document.getElementById('voice_input_textbox').value = transcript; // Pass text to hidden textbox
 
 
 
 
 
795
  voiceStatus.textContent = 'Texto reconocido: ' + transcript;
796
  console.log('Recognized:', transcript);
797
  // Trigger a Gradio event to send this text to Python
@@ -876,10 +885,6 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
876
  matecitoAvatar.style.animation = 'fade-out 0.3s forwards';
877
  setTimeout(() => matecitoAvatar.style.display = 'none', 300); // Hide after animation
878
  startVoiceButton.disabled = false; // Re-enable button
879
- // If recognition was active before speaking, restart it
880
- // if (recognition && recognition.continuous) { // If continuous listening is desired after speaking
881
- // recognition.start();
882
- // }
883
  };
884
 
885
  utterance.onerror = function(event) {
@@ -914,6 +919,16 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
914
  outputs=None,
915
  js="startListening()"
916
  )
 
 
 
 
 
 
 
 
 
 
917
 
918
  # --- Nuevo Menú de Navegación con Gradio Buttons ---
919
  gr.Markdown("<h2 style='text-align: center; color: #38bdf8; margin-top: 2em;'>Navegación MateAI</h2>")
@@ -926,16 +941,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
926
  ).click(
927
  fn=lambda: change_view('home'),
928
  inputs=[],
929
- outputs=[
930
- gr.Column.update(visible=True), # home_section
931
- gr.Column.update(visible=False), # nudges_section
932
- gr.Column.update(visible=False), # oracle_section
933
- gr.Column.update(visible=False), # challenges_section
934
- gr.Column.update(visible=False), # history_section
935
- gr.Column.update(visible=False), # tasks_section
936
- gr.Column.update(visible=False), # browser_agent_section
937
- current_view_state # Update the state
938
- ]
939
  )
940
  gr.Button(
941
  value="""<div class="icon">💬</div>Conversar con MateAI""",
@@ -944,16 +950,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
944
  ).click(
945
  fn=lambda: change_view('nudges'),
946
  inputs=[],
947
- outputs=[
948
- gr.Column.update(visible=False), # home_section
949
- gr.Column.update(visible=True), # nudges_section
950
- gr.Column.update(visible=False), # oracle_section
951
- gr.Column.update(visible=False), # challenges_section
952
- gr.Column.update(visible=False), # history_section
953
- gr.Column.update(visible=False), # tasks_section
954
- gr.Column.update(visible=False), # browser_agent_section
955
- current_view_state
956
- ]
957
  )
958
  gr.Button(
959
  value="""<div class="icon">✨</div>Oráculo del Día""",
@@ -962,16 +959,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
962
  ).click(
963
  fn=lambda: change_view('oracle'),
964
  inputs=[],
965
- outputs=[
966
- gr.Column.update(visible=False), # home_section
967
- gr.Column.update(visible=False), # nudges_section
968
- gr.Column.update(visible=True), # oracle_section
969
- gr.Column.update(visible=False), # challenges_section
970
- gr.Column.update(visible=False), # history_section
971
- gr.Column.update(visible=False), # tasks_section
972
- gr.Column.update(visible=False), # browser_agent_section
973
- current_view_state
974
- ]
975
  )
976
  gr.Button(
977
  value="""<div class="icon">🎯</div>Desafíos MateAI""",
@@ -980,16 +968,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
980
  ).click(
981
  fn=lambda: change_view('challenges'),
982
  inputs=[],
983
- outputs=[
984
- gr.Column.update(visible=False), # home_section
985
- gr.Column.update(visible=False), # nudges_section
986
- gr.Column.update(visible=False), # oracle_section
987
- gr.Column.update(visible=True), # challenges_section
988
- gr.Column.update(visible=False), # history_section
989
- gr.Column.update(visible=False), # tasks_section
990
- gr.Column.update(visible=False), # browser_agent_section
991
- current_view_state
992
- ]
993
  )
994
  gr.Button(
995
  value="""<div class="icon">📝</div>Mi Gestor de Tareas""",
@@ -998,16 +977,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
998
  ).click(
999
  fn=lambda: change_view('tasks'),
1000
  inputs=[],
1001
- outputs=[
1002
- gr.Column.update(visible=False), # home_section
1003
- gr.Column.update(visible=False), # nudges_section
1004
- gr.Column.update(visible=False), # oracle_section
1005
- gr.Column.update(visible=False), # challenges_section
1006
- gr.Column.update(visible=False), # history_section
1007
- gr.Column.update(visible=True), # tasks_section
1008
- gr.Column.update(visible=False), # browser_agent_section
1009
- current_view_state
1010
- ]
1011
  )
1012
  gr.Button(
1013
  value="""<div class="icon">📜</div>Mi Historial & Logros""",
@@ -1016,16 +986,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1016
  ).click(
1017
  fn=lambda: change_view('history'),
1018
  inputs=[],
1019
- outputs=[
1020
- gr.Column.update(visible=False), # home_section
1021
- gr.Column.update(visible=False), # nudges_section
1022
- gr.Column.update(visible=False), # oracle_section
1023
- gr.Column.update(visible=False), # challenges_section
1024
- gr.Column.update(visible=True), # history_section
1025
- gr.Column.update(visible=False), # tasks_section
1026
- gr.Column.update(visible=False), # browser_agent_section
1027
- current_view_state
1028
- ]
1029
  )
1030
  gr.Button(
1031
  value="""<div class="icon">🌐</div>Asistente Web (Concepto)""",
@@ -1034,28 +995,9 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1034
  ).click(
1035
  fn=lambda: change_view('browser_agent'),
1036
  inputs=[],
1037
- outputs=[
1038
- gr.Column.update(visible=False), # home_section
1039
- gr.Column.update(visible=False), # nudges_section
1040
- gr.Column.update(visible=False), # oracle_section
1041
- gr.Column.update(visible=False), # challenges_section
1042
- gr.Column.update(visible=False), # history_section
1043
- gr.Column.update(visible=False), # tasks_section
1044
- gr.Column.update(visible=True), # browser_agent_section
1045
- current_view_state
1046
- ]
1047
  )
1048
 
1049
- # --- Contenido de cada Sección ---
1050
- # Todas las secciones deben ser definidas aquí para poder ser referenciadas en los outputs
1051
- home_section = gr.Column(elem_id="home_section", elem_classes="section-content", visible=True)
1052
- nudges_section = gr.Column(elem_id="nudges_section", elem_classes="section-content", visible=False)
1053
- oracle_section = gr.Column(elem_id="oracle_section", elem_classes="section-content", visible=False)
1054
- challenges_section = gr.Column(elem_id="challenges_section", elem_classes="section-content", visible=False)
1055
- tasks_section = gr.Column(elem_id="tasks_section", elem_classes="section-content", visible=False)
1056
- history_section = gr.Column(elem_id="history_section", elem_classes="section-content", visible=False)
1057
- browser_agent_section = gr.Column(elem_id="browser_agent_section", elem_classes="section-content", visible=False)
1058
-
1059
  with home_section: # Home visible por defecto
1060
  gr.Markdown("<h2 style='color: #10b981;'>¡Bienvenido a tu Espacio MateAI!</h2><p>Acá manejás tu perfil para que MateAI te conozca a fondo y te ofrezca una experiencia bien personalizada.</p>")
1061
  with gr.Row():
@@ -1208,16 +1150,16 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1208
  def change_view(view_name):
1209
  # Create a dictionary to hold updates for each section
1210
  updates = {
1211
- "home": gr.Column.update(visible=False),
1212
- "nudges": gr.Column.update(visible=False),
1213
- "oracle": gr.Column.update(visible=False),
1214
- "challenges": gr.Column.update(visible=False),
1215
- "history": gr.Column.update(visible=False),
1216
- "tasks": gr.Column.update(visible=False),
1217
- "browser_agent": gr.Column.update(visible=False),
1218
  }
1219
  # Set the selected section to visible
1220
- updates[view_name] = gr.Column.update(visible=True)
1221
 
1222
  # Return the updates in a consistent order, followed by the state update
1223
  return (
@@ -1231,24 +1173,11 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1231
  view_name # This is the current_view_state output
1232
  )
1233
 
1234
- async def _update_ui_from_user(user_obj):
1235
- """Función auxiliar para actualizar todos los campos de la UI desde un objeto User."""
1236
- if user_obj:
1237
- current_points = user_obj.eco_points
1238
- current_insignia = gamification_engine.get_insignia(current_points)
1239
- next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
1240
- historial = "\n".join([entry['content'] for entry in user_obj.nudge_history if entry['role'] == 'MateAI'])
1241
- tareas_str = "\n".join([f"- {t['task']} (Agregada el: {datetime.strptime(t['added_at'], '%Y-%m-%d %H:%M:%S.%f').strftime('%d/%m')})" for t in user_obj.tasks])
1242
-
1243
- return (user_obj, user_obj.user_id, user_obj.name, str(current_points), current_insignia, next_insignia_goal,
1244
- historial, user_obj.preferences.get('tipo_susurro'), user_obj.preferences.get('frecuencia'),
1245
- user_obj.preferences.get('modo_che_tranqui'), tareas_str)
1246
-
1247
- # Valores por defecto si no hay usuario
1248
- return (None, "No logueado", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'],
1249
- Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
1250
-
1251
  async def _create_user_gradio(name, prefs_type):
 
 
 
 
1252
  user, msg = await user_manager.create_user(name, {"tipo_susurro": prefs_type})
1253
  if user:
1254
  # Prepare all UI outputs based on the newly created user
@@ -1267,6 +1196,10 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1267
  Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
1268
 
1269
  async def _load_user_gradio(user_id):
 
 
 
 
1270
  user, msg = await user_manager.login_user(user_id)
1271
  if user:
1272
  # Prepare all UI outputs based on the loaded user
@@ -1277,7 +1210,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1277
  tareas_str = "\n".join([f"- {t['task']} (Agregada el: {datetime.strptime(t['added_at'], '%Y-%m-%d %H:%M:%S.%f').strftime('%d/%m')})" for t in user.tasks])
1278
 
1279
  # Retorna todos los outputs en el orden correcto para los componentes de Gradio
1280
- return (user, user.id, msg, user.name, str(current_points), current_insignia, next_insignia_goal,
1281
  historial, user.preferences.get('tipo_susurro'), user.preferences.get('frecuencia'),
1282
  user.preferences.get('modo_che_tranqui'), tareas_str)
1283
  # If user loading failed, return default values and the error message
@@ -1331,35 +1264,36 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1331
 
1332
  # --- THE NEW UNIFIED CONVERSATIONAL ENGINE ---
1333
  async def _process_conversational_input(user_message, chat_history, current_user_state, conversation_context_state):
1334
- if not user_message.strip():
1335
- return chat_history, "", conversation_context_state, "", gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip()
 
1336
 
1337
  user_message_lower = user_message.lower()
1338
  user_obj = current_user_state
1339
 
1340
- # Append user message to chat history immediately
1341
  chat_history.append((user_message, None))
1342
 
1343
  # Check if user is logged in
1344
  if not user_obj:
1345
  mateai_response = "¡Che, para arrancar, necesito que crees o cargues tu perfil en la sección 'Inicio & Perfil'! Después volvé acá y seguimos charlando."
1346
  chat_history[-1] = (user_message, mateai_response)
1347
- return chat_history, "", conversation_context_state, mateai_response, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip()
1348
 
1349
  # --- Intent Router ---
1350
  response = ""
1351
  voice_response = ""
1352
- action_output = gr.skip()
1353
- browser_url = gr.skip()
1354
- browser_content = gr.skip()
1355
- task_list_update = gr.skip()
1356
- oracle_update = gr.skip()
1357
 
1358
  # --- Intent: Oracle/Challenge ---
1359
  if "oráculo" in user_message_lower or "revelación" in user_message_lower:
1360
  response = await nudge_generator.get_daily_oracle_revelation(user_obj.user_id)
1361
  voice_response = response
1362
- oracle_update = response
1363
 
1364
  elif "desafío" in user_message_lower or "reto" in user_message_lower:
1365
  response = await nudge_generator.get_mateai_challenge(user_obj.user_id)
@@ -1369,7 +1303,8 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1369
  elif "agregar tarea" in user_message_lower or "nueva tarea" in user_message_lower:
1370
  task_name = user_message.replace("agregar tarea", "").replace("nueva tarea", "", 1).strip()
1371
  if task_name:
1372
- response, task_list_update = await nudge_generator.add_task(user_obj.user_id, task_name)
 
1373
  voice_response = response
1374
  else:
1375
  response = "¡Dale, che! ¿Qué tarea querés que agregue? Decime, por ejemplo: 'agregar tarea comprar yerba'."
@@ -1378,7 +1313,8 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1378
  elif "completar tarea" in user_message_lower or "tarea lista" in user_message_lower:
1379
  task_name = user_message.replace("completar tarea", "").replace("tarea lista", "", 1).strip()
1380
  if task_name:
1381
- response, task_list_update = await nudge_generator.complete_task(user_obj.user_id, task_name)
 
1382
  voice_response = response
1383
  else:
1384
  response = "¡Decime qué tarea completaste, así la saco de la lista! Por ejemplo: 'completar tarea ir al súper'."
@@ -1394,7 +1330,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1394
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1395
  document.getElementById('simulated_browser_content').innerHTML = '<p>¡Estás en Google! ¿Qué querés buscar?</p><img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google Logo" style="height: 92px;">';
1396
  """
1397
- action_output, browser_url, browser_content = response, new_url, gr.HTML(js_code)
1398
  elif "wikipedia" in user_message_lower:
1399
  response = "¡Perfecto! Abriendo Wikipedia en el navegador simulado."
1400
  new_url = "https://es.wikipedia.org"
@@ -1402,7 +1338,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1402
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1403
  document.getElementById('simulated_browser_content').innerHTML = '<p>¡Estás en Wikipedia! ¿Qué querés buscar?</p><img src="https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg" alt="Wikipedia Logo" style="height: 100px;">';
1404
  """
1405
- action_output, browser_url, browser_content = response, new_url, gr.HTML(js_code)
1406
  else:
1407
  response = "¡Che, a qué página querés ir? Decime un sitio conocido como 'Google' o 'Wikipedia'."
1408
 
@@ -1415,7 +1351,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1415
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1416
  document.getElementById('simulated_browser_content').innerHTML = '<h3>Resultados de búsqueda para: {query}</h3><p><i>(Simulación de resultados)</i></p><ul><li>{query} - Wikipedia</li><li>Noticias sobre {query}</li><li>Imágenes de {query}</li></ul>';
1417
  """
1418
- action_output, browser_url, browser_content = response, new_url, gr.HTML(js_code)
1419
  else:
1420
  response = "¡Decime qué querés buscar, che! Por ejemplo: 'buscar el clima en Google'."
1421
 
@@ -1426,7 +1362,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1426
  document.getElementById('simulated_browser_address').innerText = 'Ninguna página abierta';
1427
  document.getElementById('simulated_browser_content').innerHTML = '<p>Navegador listo para tu próxima instrucción.</p>';
1428
  """
1429
- action_output, browser_url, browser_content = response, new_url, gr.HTML(js_code)
1430
 
1431
  voice_response = response
1432
 
@@ -1492,6 +1428,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1492
 
1493
  return chat_history, "", conversation_context_state, voice_response, action_output, browser_url, browser_content, task_list_update, oracle_update
1494
 
 
1495
  # --- Conexión de Eventos ---
1496
  btn_crear_usuario.click(
1497
  fn=_create_user_gradio,
@@ -1570,7 +1507,7 @@ with demo: # Ahora el bloque 'with' utiliza el objeto demo ya creado
1570
  hidden_voice_submit_button.click(
1571
  fn=_process_conversational_input,
1572
  inputs=[voice_input_textbox, chat_history, current_user_state, conversation_context_state],
1573
- outputs=[chatbot, voice_input_textbox, conversation_context_state, voice_output_text, simulated_action_output, simulated_browser_url, simulated_browser_content, tareas_pendientes_output, oraculo_output]
1574
  )
1575
 
1576
  # When the backend generates text for voice, it sends it to voice_output_text,
 
28
  # o se usará la configuración proporcionada por el usuario si no está definida.
29
  # En un entorno de producción real, se recomienda usar variables de entorno seguras.
30
  # El project_id es mateai-815ca, como se especificó.
31
+ FIREBASE_COLLECTION_USERS = "users" # Colección para datos de usuario
32
 
33
  # --- Firebase Initialization ---
34
  # Intenta inicializar Firebase Admin SDK.
 
38
  # Intenta cargar las credenciales desde la variable de entorno GOOGLE_APPLICATION_CREDENTIALS_JSON
39
  firebase_credentials_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON', None)
40
  if firebase_credentials_json:
41
+ cred_dict = json.loads(firebase_credentials_json)
42
+ # Asegurar que el project_id está presente, es crucial
43
+ if 'project_id' not in cred_dict:
44
+ cred_dict['project_id'] = 'mateai-815ca'
45
+ cred = credentials.Certificate(cred_dict)
46
  firebase_admin.initialize_app(cred)
47
  print("Firebase Admin SDK inicializado correctamente desde variable de entorno GOOGLE_APPLICATION_CREDENTIALS_JSON.")
48
  else:
 
50
  print("ADVERTENCIA: GOOGLE_APPLICATION_CREDENTIALS_JSON no está configurada y no se pudo inicializar Firebase Admin SDK.")
51
  print("La persistencia de datos de usuario en Firestore NO funcionará.")
52
  # No inicializamos firebase_admin si no tenemos credenciales de servicio válidas.
53
+ except Exception as e:
54
+ # Captura errores más específicos si es posible, como ValueError si ya está inicializado.
55
+ if "The default Firebase app already exists" in str(e):
56
+ pass # App ya inicializada, no es un error.
57
+ else:
58
+ print(f"Error al inicializar Firebase: {e}")
59
+
60
 
61
  db = firestore.client() if firebase_admin._apps else None # Solo si Firebase se inicializó con éxito
62
 
 
72
 
73
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
74
  try:
75
+ await asyncio.to_thread(user_doc_ref.set, new_user.to_dict())
76
  return new_user, f"¡Bienvenido, {name}! Tu ID de usuario es: {user_id}. ¡MateAI está listo para cebarte la vida!"
77
  except Exception as e:
78
  return None, f"Error al crear usuario en Firestore: {e}"
 
82
  if not db: return None, "Error: La base de datos no está disponible. Por favor, configura Firebase Admin SDK."
83
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
84
  try:
85
+ doc = await asyncio.to_thread(user_doc_ref.get)
86
  if doc.exists:
87
  user_data = doc.to_dict()
88
  loaded_user = User(
 
105
  if not db: return None
106
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
107
  try:
108
+ doc = await asyncio.to_thread(user_doc_ref.get)
109
  if doc.exists:
110
  user_data = doc.to_dict()
111
  return User(
 
128
  if not db: return False
129
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_obj.user_id)
130
  try:
131
+ await asyncio.to_thread(user_doc_ref.update, user_obj.to_dict())
132
  return True
133
  except Exception as e:
134
  print(f"Error al actualizar datos de usuario en Firestore: {e}")
 
408
  # --- Lógica de Priorización de Tareas (¡NUEVO RAZONAMIENTO!) ---
409
  # Si hay tareas pendientes y es un buen momento para recordarlas
410
  tasks_to_nudge = []
411
+ if user.tasks:
412
+ for task_item in user.tasks:
413
+ task_name = task_item['task']
414
+ last_nudged_str = task_item.get('last_nudged')
415
+ last_nudged = datetime.strptime(last_nudged_str, '%Y-%m-%d %H:%M:%S.%f') if last_nudged_str else None
416
+
417
+ if not last_nudged or (datetime.now() - last_nudged) > timedelta(hours=Config.TASK_NUDGE_COOLDOWN_HOURS):
418
+ # Simula un pequeño "razonamiento" sobre cuándo es buen momento para la tarea
419
+ if random.random() < 0.2: # 20% chance to remind a task
420
+ tasks_to_nudge.append(task_item)
 
 
 
 
 
421
 
422
  if tasks_to_nudge:
423
  chosen_task = random.choice(tasks_to_nudge)
 
795
 
796
  recognition.onresult = function(event) {
797
  const transcript = event.results[0][0].transcript;
798
+ const voiceInputTextbox = document.querySelector('#voice_input_textbox textarea');
799
+ if (voiceInputTextbox) {
800
+ voiceInputTextbox.value = transcript; // Pass text to hidden textbox
801
+ // Manually trigger the 'input' event for Gradio to detect the change
802
+ voiceInputTextbox.dispatchEvent(new Event('input', { bubbles: true }));
803
+ }
804
  voiceStatus.textContent = 'Texto reconocido: ' + transcript;
805
  console.log('Recognized:', transcript);
806
  // Trigger a Gradio event to send this text to Python
 
885
  matecitoAvatar.style.animation = 'fade-out 0.3s forwards';
886
  setTimeout(() => matecitoAvatar.style.display = 'none', 300); // Hide after animation
887
  startVoiceButton.disabled = false; // Re-enable button
 
 
 
 
888
  };
889
 
890
  utterance.onerror = function(event) {
 
919
  outputs=None,
920
  js="startListening()"
921
  )
922
+
923
+ # --- Contenido de cada Sección (DEFINED BEFORE USAGE) ---
924
+ home_section = gr.Column(elem_id="home_section", elem_classes="section-content", visible=True)
925
+ nudges_section = gr.Column(elem_id="nudges_section", elem_classes="section-content", visible=False)
926
+ oracle_section = gr.Column(elem_id="oracle_section", elem_classes="section-content", visible=False)
927
+ challenges_section = gr.Column(elem_id="challenges_section", elem_classes="section-content", visible=False)
928
+ tasks_section = gr.Column(elem_id="tasks_section", elem_classes="section-content", visible=False)
929
+ history_section = gr.Column(elem_id="history_section", elem_classes="section-content", visible=False)
930
+ browser_agent_section = gr.Column(elem_id="browser_agent_section", elem_classes="section-content", visible=False)
931
+
932
 
933
  # --- Nuevo Menú de Navegación con Gradio Buttons ---
934
  gr.Markdown("<h2 style='text-align: center; color: #38bdf8; margin-top: 2em;'>Navegación MateAI</h2>")
 
941
  ).click(
942
  fn=lambda: change_view('home'),
943
  inputs=[],
944
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
945
  )
946
  gr.Button(
947
  value="""<div class="icon">💬</div>Conversar con MateAI""",
 
950
  ).click(
951
  fn=lambda: change_view('nudges'),
952
  inputs=[],
953
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
954
  )
955
  gr.Button(
956
  value="""<div class="icon">✨</div>Oráculo del Día""",
 
959
  ).click(
960
  fn=lambda: change_view('oracle'),
961
  inputs=[],
962
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
963
  )
964
  gr.Button(
965
  value="""<div class="icon">🎯</div>Desafíos MateAI""",
 
968
  ).click(
969
  fn=lambda: change_view('challenges'),
970
  inputs=[],
971
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
972
  )
973
  gr.Button(
974
  value="""<div class="icon">📝</div>Mi Gestor de Tareas""",
 
977
  ).click(
978
  fn=lambda: change_view('tasks'),
979
  inputs=[],
980
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
981
  )
982
  gr.Button(
983
  value="""<div class="icon">📜</div>Mi Historial & Logros""",
 
986
  ).click(
987
  fn=lambda: change_view('history'),
988
  inputs=[],
989
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
990
  )
991
  gr.Button(
992
  value="""<div class="icon">🌐</div>Asistente Web (Concepto)""",
 
995
  ).click(
996
  fn=lambda: change_view('browser_agent'),
997
  inputs=[],
998
+ outputs=[home_section, nudges_section, oracle_section, challenges_section, history_section, tasks_section, browser_agent_section, current_view_state]
 
 
 
 
 
 
 
 
 
999
  )
1000
 
 
 
 
 
 
 
 
 
 
 
1001
  with home_section: # Home visible por defecto
1002
  gr.Markdown("<h2 style='color: #10b981;'>¡Bienvenido a tu Espacio MateAI!</h2><p>Acá manejás tu perfil para que MateAI te conozca a fondo y te ofrezca una experiencia bien personalizada.</p>")
1003
  with gr.Row():
 
1150
  def change_view(view_name):
1151
  # Create a dictionary to hold updates for each section
1152
  updates = {
1153
+ "home": gr.Column(visible=False),
1154
+ "nudges": gr.Column(visible=False),
1155
+ "oracle": gr.Column(visible=False),
1156
+ "challenges": gr.Column(visible=False),
1157
+ "history": gr.Column(visible=False),
1158
+ "tasks": gr.Column(visible=False),
1159
+ "browser_agent": gr.Column(visible=False),
1160
  }
1161
  # Set the selected section to visible
1162
+ updates[view_name] = gr.Column(visible=True)
1163
 
1164
  # Return the updates in a consistent order, followed by the state update
1165
  return (
 
1173
  view_name # This is the current_view_state output
1174
  )
1175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1176
  async def _create_user_gradio(name, prefs_type):
1177
+ if not name:
1178
+ return (None, "No logueado", "Error: El nombre no puede estar vacío.", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'],
1179
+ Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
1180
+
1181
  user, msg = await user_manager.create_user(name, {"tipo_susurro": prefs_type})
1182
  if user:
1183
  # Prepare all UI outputs based on the newly created user
 
1196
  Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
1197
 
1198
  async def _load_user_gradio(user_id):
1199
+ if not user_id:
1200
+ return (None, "No logueado", "Error: El ID de usuario no puede estar vacío.", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'],
1201
+ Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
1202
+
1203
  user, msg = await user_manager.login_user(user_id)
1204
  if user:
1205
  # Prepare all UI outputs based on the loaded user
 
1210
  tareas_str = "\n".join([f"- {t['task']} (Agregada el: {datetime.strptime(t['added_at'], '%Y-%m-%d %H:%M:%S.%f').strftime('%d/%m')})" for t in user.tasks])
1211
 
1212
  # Retorna todos los outputs en el orden correcto para los componentes de Gradio
1213
+ return (user, user.user_id, msg, user.name, str(current_points), current_insignia, next_insignia_goal,
1214
  historial, user.preferences.get('tipo_susurro'), user.preferences.get('frecuencia'),
1215
  user.preferences.get('modo_che_tranqui'), tareas_str)
1216
  # If user loading failed, return default values and the error message
 
1264
 
1265
  # --- THE NEW UNIFIED CONVERSATIONAL ENGINE ---
1266
  async def _process_conversational_input(user_message, chat_history, current_user_state, conversation_context_state):
1267
+ if not user_message or not user_message.strip():
1268
+ # Return current state without change if input is empty
1269
+ return chat_history, "", conversation_context_state, "", gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1270
 
1271
  user_message_lower = user_message.lower()
1272
  user_obj = current_user_state
1273
 
1274
+ # Append user message to chat history immediately for responsiveness
1275
  chat_history.append((user_message, None))
1276
 
1277
  # Check if user is logged in
1278
  if not user_obj:
1279
  mateai_response = "¡Che, para arrancar, necesito que crees o cargues tu perfil en la sección 'Inicio & Perfil'! Después volvé acá y seguimos charlando."
1280
  chat_history[-1] = (user_message, mateai_response)
1281
+ return chat_history, "", conversation_context_state, mateai_response, gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1282
 
1283
  # --- Intent Router ---
1284
  response = ""
1285
  voice_response = ""
1286
+ action_output = gr.update()
1287
+ browser_url = gr.update()
1288
+ browser_content = gr.update()
1289
+ task_list_update = gr.update()
1290
+ oracle_update = gr.update()
1291
 
1292
  # --- Intent: Oracle/Challenge ---
1293
  if "oráculo" in user_message_lower or "revelación" in user_message_lower:
1294
  response = await nudge_generator.get_daily_oracle_revelation(user_obj.user_id)
1295
  voice_response = response
1296
+ oracle_update = gr.Textbox(value=response)
1297
 
1298
  elif "desafío" in user_message_lower or "reto" in user_message_lower:
1299
  response = await nudge_generator.get_mateai_challenge(user_obj.user_id)
 
1303
  elif "agregar tarea" in user_message_lower or "nueva tarea" in user_message_lower:
1304
  task_name = user_message.replace("agregar tarea", "").replace("nueva tarea", "", 1).strip()
1305
  if task_name:
1306
+ response, tasks_str = await nudge_generator.add_task(user_obj.user_id, task_name)
1307
+ task_list_update = gr.Textbox(value=tasks_str)
1308
  voice_response = response
1309
  else:
1310
  response = "¡Dale, che! ¿Qué tarea querés que agregue? Decime, por ejemplo: 'agregar tarea comprar yerba'."
 
1313
  elif "completar tarea" in user_message_lower or "tarea lista" in user_message_lower:
1314
  task_name = user_message.replace("completar tarea", "").replace("tarea lista", "", 1).strip()
1315
  if task_name:
1316
+ response, tasks_str = await nudge_generator.complete_task(user_obj.user_id, task_name)
1317
+ task_list_update = gr.Textbox(value=tasks_str)
1318
  voice_response = response
1319
  else:
1320
  response = "¡Decime qué tarea completaste, así la saco de la lista! Por ejemplo: 'completar tarea ir al súper'."
 
1330
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1331
  document.getElementById('simulated_browser_content').innerHTML = '<p>¡Estás en Google! ¿Qué querés buscar?</p><img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google Logo" style="height: 92px;">';
1332
  """
1333
+ action_output, browser_url, browser_content = gr.Textbox(value=response), gr.Textbox(value=new_url), gr.HTML(js_code)
1334
  elif "wikipedia" in user_message_lower:
1335
  response = "¡Perfecto! Abriendo Wikipedia en el navegador simulado."
1336
  new_url = "https://es.wikipedia.org"
 
1338
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1339
  document.getElementById('simulated_browser_content').innerHTML = '<p>¡Estás en Wikipedia! ¿Qué querés buscar?</p><img src="https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg" alt="Wikipedia Logo" style="height: 100px;">';
1340
  """
1341
+ action_output, browser_url, browser_content = gr.Textbox(value=response), gr.Textbox(value=new_url), gr.HTML(js_code)
1342
  else:
1343
  response = "¡Che, a qué página querés ir? Decime un sitio conocido como 'Google' o 'Wikipedia'."
1344
 
 
1351
  document.getElementById('simulated_browser_address').innerText = '{new_url}';
1352
  document.getElementById('simulated_browser_content').innerHTML = '<h3>Resultados de búsqueda para: {query}</h3><p><i>(Simulación de resultados)</i></p><ul><li>{query} - Wikipedia</li><li>Noticias sobre {query}</li><li>Imágenes de {query}</li></ul>';
1353
  """
1354
+ action_output, browser_url, browser_content = gr.Textbox(value=response), gr.Textbox(value=new_url), gr.HTML(js_code)
1355
  else:
1356
  response = "¡Decime qué querés buscar, che! Por ejemplo: 'buscar el clima en Google'."
1357
 
 
1362
  document.getElementById('simulated_browser_address').innerText = 'Ninguna página abierta';
1363
  document.getElementById('simulated_browser_content').innerHTML = '<p>Navegador listo para tu próxima instrucción.</p>';
1364
  """
1365
+ action_output, browser_url, browser_content = gr.Textbox(value=response), gr.Textbox(value=new_url), gr.HTML(js_code)
1366
 
1367
  voice_response = response
1368
 
 
1428
 
1429
  return chat_history, "", conversation_context_state, voice_response, action_output, browser_url, browser_content, task_list_update, oracle_update
1430
 
1431
+
1432
  # --- Conexión de Eventos ---
1433
  btn_crear_usuario.click(
1434
  fn=_create_user_gradio,
 
1507
  hidden_voice_submit_button.click(
1508
  fn=_process_conversational_input,
1509
  inputs=[voice_input_textbox, chat_history, current_user_state, conversation_context_state],
1510
+ outputs=CONVERSATIONAL_OUTPUTS
1511
  )
1512
 
1513
  # When the backend generates text for voice, it sends it to voice_output_text,