Lukeetah commited on
Commit
52f940d
·
verified ·
1 Parent(s): 5e8bcdf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +456 -151
app.py CHANGED
@@ -4,24 +4,28 @@ import time
4
  import json
5
  from datetime import datetime, timedelta
6
  import os # Importar os para variables de entorno
 
7
 
8
  # --- Firebase Admin SDK Imports ---
9
  import firebase_admin
10
  from firebase_admin import credentials, firestore
11
 
12
- # --- MateAI: El Oráculo del Bienestar Argento - Edición Producción ---
13
  # Una manifestación de inteligencia superior para la integración masiva de IA en la humanidad.
14
  # Diseñado para Argentina, con personalidad, resiliencia y costos de token CERO.
15
  # Arquitectura de Micro-Oráculos Contextuales (AMOC) y Motor de Resonancia Cultural Dinámica (MRCD).
16
  # Integración real con Firestore para persistencia de datos de usuario.
 
 
17
 
18
  # --- Módulo 1: Configuración Global y Constantes ---
19
  class Config:
20
- APP_NAME = "MateAI: El Oráculo del Bienestar Argento"
21
  DEFAULT_USER_PREFS = {"tipo_susurro": "ambos", "frecuencia": "normal", "modo_che_tranqui": False}
22
  ECO_PUNTOS_POR_SUSURRO = 1
23
  MAX_HISTORIAL_SUSURROS = 50 # Limita el historial de susurros guardado por usuario
24
- NUDGE_COOLDOWN_MINUTES = 1 # Cooldown real para evitar spam de susurros
 
25
 
26
  # --- Firebase Configuration ---
27
  # Para desplegar en Hugging Face Spaces:
@@ -32,7 +36,7 @@ class Config:
32
  # 4. Crea un nuevo secreto llamado 'GOOGLE_APPLICATION_CREDENTIALS_JSON'
33
  # y pega el *contenido completo* de tu archivo JSON de clave privada aquí.
34
  # 5. El código intentará cargar estas credenciales.
35
- FIREBASE_COLLECTION_USERS = "artifacts/mateai-oracle-prod-demo/users" # Colección para datos de usuario
36
 
37
  # --- Firebase Initialization ---
38
  # Intenta inicializar Firebase Admin SDK.
@@ -60,8 +64,6 @@ db = firestore.client()
60
  # --- Módulo 2: Gestión de Usuarios (UserManager) ---
61
  # Gestiona la creación, carga y actualización de usuarios con persistencia en Firestore.
62
  class UserManager:
63
- _current_user_id = None # ID del usuario actualmente logueado en la sesión de Gradio
64
-
65
  @classmethod
66
  async def create_user(cls, name, initial_prefs=None):
67
  user_id = f"user_{int(time.time())}_{random.randint(1000, 9999)}" # ID único basado en tiempo y random
@@ -71,7 +73,6 @@ class UserManager:
71
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
72
  try:
73
  await user_doc_ref.set(new_user.to_dict())
74
- cls._current_user_id = user_id
75
  return new_user, f"¡Bienvenido, {name}! Tu ID de usuario es: {user_id}. ¡MateAI está listo para cebarte la vida!"
76
  except Exception as e:
77
  return None, f"Error al crear usuario en Firestore: {e}"
@@ -90,18 +91,14 @@ class UserManager:
90
  eco_points=user_data.get('eco_points', 0),
91
  nudge_history=user_data.get('nudge_history', []),
92
  last_oracle_date_str=user_data.get('last_oracle_date'),
93
- last_nudge_time_str=user_data.get('last_nudge_time')
 
94
  )
95
- cls._current_user_id = user_id
96
  return loaded_user, f"Perfil de {loaded_user.name} cargado con éxito."
97
  return None, "Error: ID de usuario no encontrado. ¡Creá uno nuevo o verificá el ID!"
98
  except Exception as e:
99
  return None, f"Error al cargar usuario desde Firestore: {e}"
100
 
101
- @classmethod
102
- def get_current_user_id(cls):
103
- return cls._current_user_id
104
-
105
  @classmethod
106
  async def get_user(cls, user_id):
107
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
@@ -116,7 +113,8 @@ class UserManager:
116
  eco_points=user_data.get('eco_points', 0),
117
  nudge_history=user_data.get('nudge_history', []),
118
  last_oracle_date_str=user_data.get('last_oracle_date'),
119
- last_nudge_time_str=user_data.get('last_nudge_time')
 
120
  )
121
  return None
122
  except Exception as e:
@@ -135,16 +133,29 @@ class UserManager:
135
 
136
  # --- Módulo 3: Modelos de Datos ---
137
  class User:
138
- def __init__(self, user_id, name, preferences, eco_points=0, insignia="Novato del Mate", nudge_history=None, last_oracle_date_str=None, last_nudge_time_str=None):
139
  self.user_id = user_id
140
  self.name = name
141
  self.preferences = preferences # dict
142
  self.eco_points = eco_points
143
- self.insignia = insignia # Se recalcula dinámicamente
144
  self.nudge_history = nudge_history if nudge_history is not None else []
 
145
 
146
- self.last_oracle_date = datetime.strptime(last_oracle_date_str, '%Y-%m-%d').date() if last_oracle_date_str else None
147
- self.last_nudge_time = datetime.strptime(last_nudge_time_str, '%Y-%m-%d %H:%M:%S.%f') if last_nudge_time_str else None
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  self.nudge_cooldown = timedelta(minutes=Config.NUDGE_COOLDOWN_MINUTES)
149
 
150
  def to_dict(self):
@@ -155,107 +166,117 @@ class User:
155
  "eco_points": self.eco_points,
156
  "nudge_history": self.nudge_history,
157
  "last_oracle_date": self.last_oracle_date.strftime('%Y-%m-%d') if self.last_oracle_date else None,
158
- "last_nudge_time": self.last_nudge_time.strftime('%Y-%m-%d %H:%M:%S.%f') if self.last_nudge_time else None
 
159
  }
160
 
161
  class Nudge:
162
- def __init__(self, text, type, context_tags, cultural_tags):
163
- self.text = text
164
- self.type = type # "eco", "bienestar", "reflexivo"
165
  self.context_tags = context_tags # ["Mañana", "Casa", "Trabajando"]
166
  self.cultural_tags = cultural_tags # ["mate", "futbol", "asado"]
167
 
168
  # --- Módulo 4: Base de Conocimiento de Nudges (Motor de Resonancia Cultural Dinámica - MRCD) ---
169
- # Esta es la "inteligencia" de MateAI.
170
  NUDGE_DATABASE = [
171
  # Mañana - Casa
172
- Nudge("¡Che, buen día! Arrancá la mañana con un matecito y un buen estiramiento. ¡Desperezate, chango!", "bienestar", ["Mañana", "Casa"], ["mate", "despertar"]),
173
- Nudge("Antes de prender todo, ¿entra el solcito? Aprovechá la luz natural, que es gratis y buena onda.", "eco", ["Mañana", "Casa"], ["sol", "ahorro"]),
174
- Nudge("Mirá por la ventana, ¿cómo está el cielo? Un ratito de aire fresco te 'desenchufa' para arrancar el día.", "bienestar", ["Mañana", "Casa"], ["naturaleza", "calma"]),
175
- Nudge("Si tenés plantitas, ¿ya las regaste? Un mimo verde para empezar el día con buena energía.", "eco", ["Mañana", "Casa"], ["plantas", "cuidado"]),
176
- Nudge("¿Ya pensaste qué vas a desayunar? Elegí algo que te dé energía para el día, como frutas o tostadas con dulce de leche. ¡A cargar pilas!", "bienestar", ["Mañana", "Casa", "Cocinando"], ["desayuno", "energia"]),
177
 
178
  # Mañana - Oficina/Estudio
179
- Nudge("Mientras esperás que hierva el agua para el café, pensá en una meta chiquita para hoy. ¡Ponele garra!", "bienestar", ["Mañana", "Oficina/Estudio", "Trabajando"], ["cafe", "metas"]),
180
- Nudge("Si vas en bondi o subte, mirá el paisaje. Desconectate un toque del celu y observá la ciudad que no duerme.", "bienestar", ["Mañana", "Oficina/Estudio", "Transporte"], ["ciudad", "atencion_plena"]),
181
- Nudge("Unos mates con los compañeros para arrancar la jornada. ¡La buena onda se contagia!", "bienestar", ["Mañana", "Oficina/Estudio"], ["mate", "social"]),
182
- Nudge("Antes de arrancar con el 'quilombo', ordená tu escritorio. Un espacio despejado, una mente más clara.", "bienestar", ["Mañana", "Oficina/Estudio", "Trabajando"], ["orden", "productividad"]),
183
 
184
  # Mañana - Aire Libre/Calle
185
- Nudge("¡Qué lindo día para respirar hondo! Llenate los pulmones de aire puro y sentí la energía.", "bienestar", ["Mañana", "Aire Libre/Calle", "Ejercicio"], ["respiracion", "energia"]),
186
- Nudge("Escuchá los ruidos de la calle. ¿Qué te cuentan? Los sonidos de la ciudad también son música.", "bienestar", ["Mañana", "Aire Libre/Calle"], ["sonidos", "atencion_plena"]),
187
- Nudge("Si ves un árbol copado, frená un segundo. La naturaleza siempre nos regala un respiro.", "bienestar", ["Mañana", "Aire Libre/Calle"], ["naturaleza", "pausa"]),
188
 
189
  # Mediodía/Tarde - Casa
190
- Nudge("¿Un bajón? Cortá la rutina con una fruta o un vaso de agua. ¡Hidratarse es clave!", "bienestar", ["Mediodía/Tarde", "Casa"], ["hidratacion", "snack"]),
191
- Nudge("Antes de prender la tele, ¿qué tal un libro o charlar con alguien? Desconectarse es reconectarse.", "bienestar", ["Mediodía/Tarde", "Casa"], ["ocio", "conexion"]),
192
- Nudge("¿Hay algo para reciclar? Separá los cartones, plásticos... ¡Cada granito de arena suma!", "eco", ["Mediodía/Tarde", "Casa"], ["reciclaje", "accion"]),
193
- Nudge("Si tenés ropa para lavar, ¿aprovechás el solcito? Secar al aire ahorra energía y deja un olorcito a campo.", "eco", ["Mediodía/Tarde", "Casa"], ["ahorro", "sol"]),
194
- Nudge("¿Ya almorzaste? Recordá que una buena comida casera te da la nafta para seguir el día. ¡Buen provecho!", "bienestar", ["Mediodía/Tarde", "Casa", "Cocinando"], ["almuerzo", "nutricion"]),
195
 
196
  # Mediodía/Tarde - Oficina/Estudio
197
- Nudge("Si estás 'quemado', levantate y estirá las piernas. Unos pasos te despejan la cabeza.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["pausa", "movimiento"]),
198
- Nudge("¿Reunión eterna? Hacé una pausa mental de 30 segundos. Solo respirá y volvé a la carga.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["respiracion", "estres"]),
199
- Nudge("Apagá las luces si salís un rato. ¡Cuidar la energía es cuidar el bolsillo y el planeta!", "eco", ["Mediodía/Tarde", "Oficina/Estudio"], ["ahorro", "energia"]),
200
- Nudge("Antes de seguir con el laburo, ¿ya almorzaste? Una buena comida te da la nafta para seguir.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["almuerzo", "energia"]),
201
 
202
  # Mediodía/Tarde - Aire Libre/Calle
203
- Nudge("El solcito de la tarde es ideal para recargar pilas. ¡Disfrutá el momento!", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["sol", "energia"]),
204
- Nudge("Mirá las nubes, ¿qué formas ves? La imaginación vuela libre.", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["nubes", "imaginacion"]),
205
- Nudge("Si encontrás un banquito, sentate un rato. Observá la gente, la vida que pasa. ¡Es un 'recreo' para el alma!", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["pausa", "observacion"]),
206
 
207
  # Noche - Casa
208
- Nudge("Antes de cenar, pensá en algo bueno que te pasó hoy. ¡Agradecer es un 'mimo' para el alma!", "bienestar", ["Noche", "Casa"], ["gratitud", "reflexion"]),
209
- Nudge("Dejá la ropa lista para mañana. Un pequeño orden te ahorra el estrés mañanero.", "bienestar", ["Noche", "Casa"], ["orden", "estres"]),
210
- Nudge("¿Ya guardaste el celu? Desconectate de las pantallas al menos media hora antes de dormir. ¡Tu cabeza te lo va a agradecer!", "bienestar", ["Noche", "Casa"], ["descanso", "pantallas"]),
211
- Nudge("Si vas a cocinar, ¿ya pensaste en aprovechar las sobras? ¡Acá no se tira nada!", "eco", ["Noche", "Casa", "Cocinando"], ["desperdicio_cero", "cocina"]),
212
 
213
  # Noche - Oficina/Estudio
214
- Nudge("Terminá el día haciendo una lista de lo que lograste. ¡Valorá tu esfuerzo, campeón!", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["logros", "autoestima"]),
215
- Nudge("Dejá todo ordenado para mañana. Un buen cierre de día es un buen comienzo del siguiente.", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["orden", "productividad"]),
216
- Nudge("Evitá los mails o noticias que te estresen antes de irte. ¡Protegé tu descanso, que es sagrado!", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["estres", "descanso"]),
217
 
218
  # Noche - Aire Libre/Calle
219
- Nudge("Si es seguro, mirá las estrellas. Te recuerdan lo inmenso que es todo y lo chiquitos que somos. ¡Bajá un cambio!", "bienestar", ["Noche", "Aire Libre/Calle"], ["estrellas", "reflexion"]),
220
- Nudge("Escuchá los ruidos de la noche. Un concierto natural para relajar el alma.", "bienestar", ["Noche", "Aire Libre/Calle"], ["sonidos", "calma"]),
221
- Nudge("Una caminata tranquila antes de volver a casa. Despejá la mente y preparate para el 'descanso del guerrero'.", "bienestar", ["Noche", "Aire Libre/Calle"], ["caminata", "descanso"]),
222
 
223
  # Nudges de Ánimo (para estado_animo_input == "Bajoneado")
224
- Nudge("¡Arriba ese ánimo! Un matecito y un buen pensamiento pueden cambiar el día. ¡Vos podés!", "bienestar", ["General"], ["animo", "mate"]),
225
- Nudge("Recordá que hasta el día más nublado tiene un solcito escondido. ¡Fuerza, campeón!", "bienestar", ["General"], ["animo", "esperanza"]),
226
- Nudge("Date un gusto chiquito hoy. ¡Te lo merecés! Un alfajor, tu música favorita... ¡lo que sea!", "bienestar", ["General"], ["animo", "recompensa"]),
227
- Nudge("Si te sentís 'bajoneado', un poco de música o una caminata corta pueden ayudar a 'despejar'.", "bienestar", ["General"], ["animo", "actividad"]),
228
 
229
  # Nudges para "Modo Quilombo" (activado por contexto social o actividad)
230
- Nudge("¡Uf, qué día! Respiro hondo. En medio del 'quilombo', una pausa es oro. ¡Tranqui!", "bienestar", ["General", "Quilombo"], ["estres", "respiracion"]),
231
- Nudge("Si la cosa está 'picada', recordá que no todo depende de vos. Hacé lo que puedas y soltá lo demás.", "bienestar", ["General", "Quilombo"], ["estres", "control"]),
232
- Nudge("En los días de 'locura', un matecito con calma puede ser tu ancla. ¡A no perder la cabeza!", "bienestar", ["General", "Quilombo"], ["estres", "mate"]),
233
 
234
  # Nudges para "Modo Siesta" (activado por contexto social o actividad)
235
- Nudge("¡Qué lindo para una siestita! Si podés, aprovechá para recargar energías. ¡Es sagrado!", "bienestar", ["General", "Siesta"], ["descanso", "siesta"]),
236
- Nudge("El cuerpo te pide un descanso. Escuchalo. Unos minutos de relax pueden hacer la diferencia.", "bienestar", ["General", "Siesta"], ["descanso", "cuerpo"]),
237
 
238
  # Nudges para "Viendo un partido"
239
- Nudge("¡Vamos Argentina! Disfrutá el partido, pero recordá hidratarte bien. ¡Y si ganamos, a festejar con responsabilidad!", "bienestar", ["General", "Viendo un partido"], ["futbol", "hidratacion", "festejo"]),
240
- Nudge("El fútbol es pasión, pero no te olvides de estirar un poco si estás mucho tiempo sentado. ¡A mover el esqueleto!", "bienestar", ["General", "Viendo un partido"], ["futbol", "movimiento"]),
241
 
242
  # Nudges de Desafíos MateAI
243
- Nudge("Desafío MateAI: Hoy, intentá reducir el uso de plásticos de un solo uso. ¡Cada acción cuenta!", "eco", ["Desafio"], ["plastico", "desafio"]),
244
- Nudge("Desafío MateAI: Dedicá 10 minutos a meditar o simplemente a respirar conscientemente. ¡Tu mente te lo agradecerá!", "bienestar", ["Desafio"], ["meditacion", "desafio"]),
245
- Nudge("Desafío MateAI: Contactá a un amigo o familiar que hace mucho no ves. ¡Un 'hola' puede alegrar el día!", "bienestar", ["Desafio"], ["social", "desafio"]),
 
 
 
 
 
 
 
 
 
246
  ]
247
 
248
  ORACULO_REVELATIONS = [
249
- "La verdadera riqueza no se mide en bienes, sino en la calma del alma y la conexión con el entorno. ¿Qué tesoro descubriste hoy?",
250
- "El río de la vida fluye constante. No te aferres a la orilla; aprendé a navegar sus corrientes con sabiduría y gratitud.",
251
- "Cada amanecer es una oportunidad para reescribir tu historia. ¿Qué capítulo nuevo elegís empezar hoy?",
252
- "La tierra nos susurra secretos ancestrales. Escuchá el viento, sentí el sol, y recordá que sos parte de algo inmenso y sagrado.",
253
- "El mate compartido es más que una bebida; es un ritual de conexión. ¿Con quién vas a compartir tu energía hoy?",
254
- "En la simpleza de lo cotidiano reside la magia. Encontrá la belleza en un rayo de sol, en el canto de un pájaro, en una sonrisa.",
255
- "El viento patagónico te enseña la fuerza, el calor del norte la pasión. En cada rincón de nuestra tierra, una lección. ¿Cuál sentís hoy?",
256
- "Como el Obelisco en el centro, a veces uno se siente solo en la inmensidad. Recordá que, como él, estás rodeado de historias y vida. ¡Conectate!",
257
- "La Pacha Mama te abraza en cada paso. Sentí su energía, agradecé su abundancia. ¡Somos parte de ella!",
258
- "El tango te enseña la melancolía y la pasión. La vida, como el tango, tiene sus pausas y sus arranques. ¿Qué ritmo te toca bailar hoy?"
259
  ]
260
 
261
  # --- Módulo 5: Motor de Contexto Avanzado (ContextEngine) ---
@@ -275,7 +296,7 @@ class ContextEngine:
275
  @staticmethod
276
  def get_environmental_context():
277
  """Simula datos ambientales (ej. clima, ruido)."""
278
- climas = ["Soleado", "Nublado", "Lluvioso", "Ventoso"]
279
  return random.choice(climas)
280
 
281
  @staticmethod
@@ -316,11 +337,11 @@ class NudgeGenerator:
316
  # Filtrar por tiempo y ubicación (obligatorio)
317
  if time_context not in nudge.context_tags:
318
  match = False
319
- if location not in nudge.context_tags and "General" not in nudge.context_tags:
320
  match = False
321
 
322
  # Filtrar por actividad (si es relevante)
323
- if activity != "Relajado" and activity not in nudge.context_tags and "General" not in nudge.context_tags:
324
  pass # Permitir que pase si es un nudge general
325
 
326
  # Filtrar por estado de ánimo
@@ -354,25 +375,78 @@ class NudgeGenerator:
354
  return nudges
355
 
356
  recent_nudges_text = user_history[-5:] # Evitar los últimos 5
357
- filtered_nudges = [n for n in nudges if n.text not in recent_nudges_text]
358
  return filtered_nudges if filtered_nudges else nudges # Si no hay nuevos, repite (mejor que nada)
359
 
360
  async def generate_nudge(self, user_id, location, activity, user_input_sentiment):
361
  user = await self.user_manager.get_user(user_id)
362
  if not user:
363
- return "Error: Usuario no logueado. Por favor, crea o carga un usuario."
 
364
 
365
  # Cooldown para evitar spam de nudges
366
  if user.last_nudge_time and (datetime.now() - user.last_nudge_time) < user.nudge_cooldown:
367
  remaining_time = user.nudge_cooldown - (datetime.now() - user.last_nudge_time)
368
- return f"MateAI necesita un respiro. Volvé a pedir un susurro en {remaining_time.seconds} segundos. ¡La paciencia es una virtud!"
 
 
 
 
 
 
 
369
 
370
  # Obtener contexto completo
371
  time_context = self.context_engine.get_current_time_context()
372
- environmental_context = self.context_engine.get_environmental_context()
373
  societal_vibe = self.context_engine.get_societal_vibe()
374
  sentiment = self.context_engine.get_user_sentiment(activity, user_input_sentiment)
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  # Filtrar nudges base
377
  possible_nudges = await self._filter_nudges_by_context(
378
  NUDGE_DATABASE, time_context, location, activity, societal_vibe, sentiment
@@ -388,20 +462,33 @@ class NudgeGenerator:
388
  if user.preferences.get('modo_che_tranqui') or sentiment == "Bajoneado" or "Quilombo" in societal_vibe:
389
  calming_nudges = [n for n in possible_nudges if "calma" in n.cultural_tags or "estres" in n.cultural_tags or "respiracion" in n.cultural_tags]
390
  if calming_nudges:
391
- chosen_nudge = random.choice(calming_nudges)
392
  else:
393
- chosen_nudge = random.choice(possible_nudges) if possible_nudges else Nudge("MateAI está meditando... ¡probá otro contexto!", "reflexivo", ["General"], [])
394
  else:
395
- chosen_nudge = random.choice(possible_nudges) if possible_nudges else Nudge("MateAI está meditando... ¡probá otro contexto!", "reflexivo", ["General"], [])
396
 
 
 
 
 
 
 
 
 
397
  user.eco_points += Config.ECO_PUNTOS_POR_SUSURRO
398
- user.nudge_history.append(chosen_nudge.text)
399
  if len(user.nudge_history) > Config.MAX_HISTORIAL_SUSURROS:
400
  user.nudge_history.pop(0)
401
  user.last_nudge_time = datetime.now()
402
 
403
  await self.user_manager.update_user_data(user)
404
- return chosen_nudge.text
 
 
 
 
 
405
 
406
  async def get_daily_oracle_revelation(self, user_id):
407
  user = await self.user_manager.get_user(user_id)
@@ -412,10 +499,11 @@ class NudgeGenerator:
412
  if user.last_oracle_date and user.last_oracle_date == today:
413
  return "El Oráculo ya te ha hablado hoy. Volvé mañana para una nueva revelación."
414
 
415
- revelation = random.choice(ORACULO_REVELATIONS)
 
416
  user.last_oracle_date = today
417
  await self.user_manager.update_user_data(user)
418
- return revelation
419
 
420
  async def get_mateai_challenge(self, user_id):
421
  user = await self.user_manager.get_user(user_id)
@@ -426,7 +514,32 @@ class NudgeGenerator:
426
  if not challenge_nudges:
427
  return "MateAI está pensando en un desafío épico... ¡Volvé pronto!"
428
 
429
- return random.choice(challenge_nudges).text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
  # --- Módulo 7: Motor de Gamificación (GamificationEngine) ---
432
  class GamificationEngine:
@@ -464,12 +577,15 @@ context_engine = ContextEngine()
464
  nudge_generator = NudgeGenerator(user_manager, context_engine)
465
  gamification_engine = GamificationEngine() # No necesita instanciar, es de clase
466
 
467
- # --- Módulo 8: Interfaz Gradio (UI/UX Argento de Nivel Superior) ---
468
  with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }") as demo:
 
 
 
469
  gr.Markdown(
470
  """
471
  <h1 style="text-align: center; color: #10b981; font-size: 3.5em; font-weight: bold; margin-bottom: 0.5em; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
472
- 🧉 MateAI: El Oráculo del Bienestar Argento 🧉
473
  </h1>
474
  <p style="text-align: center; color: #4b5563; font-size: 1.3em; margin-bottom: 2em; line-height: 1.5;">
475
  Una inteligencia superior diseñada para cebarte la vida con sabiduría contextual y sin costo.
@@ -495,7 +611,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }
495
  border: 6px solid #10b981; /* emerald-600 */
496
  text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
497
  ">
498
- <span style="font-size: 0.8em;">✨ Oráculo ✨</span>
499
  <span>🇦🇷 MateAI 🇦🇷</span>
500
  </div>
501
  <style>
@@ -509,6 +625,122 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }
509
  """
510
  )
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  # --- Tab 1: Inicio y Perfil ---
513
  with gr.Tab("Inicio & Perfil"):
514
  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.</p>")
@@ -629,66 +861,111 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }
629
  btn_descargar_diario = gr.Button("Descargar Diario (sesión actual)", variant="secondary")
630
  gr.Markdown("<p style='font-size: 0.9em; color: #6b7280; margin-top: 1em;'><i>Nota: El diario se descarga como texto. Para guardar permanentemente, deberías copiarlo o integrarlo con otro servicio.</i></p>")
631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  # --- Funciones de Interacción para Gradio ---
633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  async def _create_user_gradio(name, prefs_type):
635
  user, msg = await user_manager.create_user(name, {"tipo_susurro": prefs_type})
636
  if user:
637
- current_points = user.eco_points
638
- current_insignia = gamification_engine.get_insignia(current_points)
639
- next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
640
- return user.user_id, msg, user.name, str(current_points), current_insignia, next_insignia_goal, "\n".join(user.nudge_history), prefs_type, Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui']
641
- return "No logueado", msg, "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'], Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui']
642
 
643
  async def _load_user_gradio(user_id):
644
  user, msg = await user_manager.login_user(user_id)
645
  if user:
646
- current_points = user.eco_points
647
- current_insignia = gamification_engine.get_insignia(current_points)
648
- next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
649
- return user.user_id, msg, user.name, str(current_points), current_insignia, next_insignia_goal, "\n".join(user.nudge_history), user.preferences.get('tipo_susurro'), user.preferences.get('frecuencia'), user.preferences.get('modo_che_tranqui')
650
- return "No logueado", msg, "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'], Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui']
651
 
652
- async def _update_prefs_gradio(user_id, tipo_susurro, frecuencia, modo_che_tranqui):
653
- user = await user_manager.get_user(user_id)
654
- if not user:
655
- return "Error: Por favor, crea o carga un usuario primero."
656
 
657
- new_prefs = {
658
  "tipo_susurro": tipo_susurro,
659
  "frecuencia": frecuencia,
660
  "modo_che_tranqui": modo_che_tranqui
661
- }
662
- user.preferences.update(new_prefs)
663
- success = await user_manager.update_user_data(user)
664
  if success:
665
- return f"Preferencias actualizadas para {user.name}."
666
- return "Error al actualizar preferencias."
667
 
668
- async def _generate_nudge_gradio(user_id, location, activity, sentiment):
669
- if user_id == "No logueado" or not await user_manager.get_user(user_id):
670
- return "Por favor, crea o carga un usuario primero para recibir susurros personalizados.", "", "", "", ""
671
 
672
- nudge_text = await nudge_generator.generate_nudge(user_id, location, activity, sentiment)
673
 
674
- # Actualizar UI de puntos e insignias
675
- user = await user_manager.get_user(user_id) # Recargar usuario para obtener los puntos actualizados
676
- current_points = user.eco_points
 
677
  current_insignia = gamification_engine.get_insignia(current_points)
678
  next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
679
- historial = "\n".join(user.nudge_history)
680
 
681
- return nudge_text, str(current_points), current_insignia, next_insignia_goal, historial
682
-
683
- async def _get_oracle_revelation_gradio(user_id):
684
- if user_id == "No logueado" or not await user_manager.get_user(user_id):
685
- return "Por favor, crea o carga un usuario primero para consultar al Oráculo."
686
- return await nudge_generator.get_daily_oracle_revelation(user_id)
687
-
688
- async def _get_mateai_challenge_gradio(user_id):
689
- if user_id == "No logueado" or not await user_manager.get_user(user_id):
690
- return "Por favor, crea o carga un usuario primero para recibir desafíos."
691
- return await nudge_generator.get_mateai_challenge(user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
  def _download_diary_gradio(diary_text):
694
  return gr.File(value=diary_text.encode('utf-8'), filename="diario_mateai.txt", type="bytes")
@@ -697,37 +974,49 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }
697
  btn_crear_usuario.click(
698
  fn=_create_user_gradio,
699
  inputs=[nombre_nuevo_usuario, preferencias_iniciales],
700
- outputs=[usuario_actual_id, output_creacion_usuario, usuario_actual_nombre, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox]
701
  )
702
 
703
  btn_cargar_perfil.click(
704
  fn=_load_user_gradio,
705
  inputs=[user_id_existente],
706
- outputs=[usuario_actual_id, output_cargar_perfil, usuario_actual_nombre, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox]
707
  )
708
 
709
  btn_actualizar_preferencias.click(
710
  fn=_update_prefs_gradio,
711
- inputs=[usuario_actual_id, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox],
712
- outputs=output_actualizar_preferencias
713
  )
714
 
715
  btn_generar_susurro.click(
716
  fn=_generate_nudge_gradio,
717
- inputs=[usuario_actual_id, ubicacion_input, actividad_input, estado_animo_input],
718
- outputs=[susurro_output, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output]
719
  )
720
 
721
  btn_oraculo.click(
722
  fn=_get_oracle_revelation_gradio,
723
- inputs=[usuario_actual_id],
724
- outputs=oraculo_output
725
  )
726
 
727
  btn_desafio.click(
728
  fn=_get_mateai_challenge_gradio,
729
- inputs=[usuario_actual_id],
730
- outputs=desafio_output
 
 
 
 
 
 
 
 
 
 
 
 
731
  )
732
 
733
  btn_descargar_diario.click(
@@ -736,6 +1025,22 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }
736
  outputs=gr.File(label="Descargar tu Diario")
737
  )
738
 
739
- # Para ejecutar localmente (descomentar si no es para Hugging Face):
740
- # if __name__ == "__main__":
741
- # demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import json
5
  from datetime import datetime, timedelta
6
  import os # Importar os para variables de entorno
7
+ import asyncio # Para manejar operaciones asíncronas
8
 
9
  # --- Firebase Admin SDK Imports ---
10
  import firebase_admin
11
  from firebase_admin import credentials, firestore
12
 
13
+ # --- MateAI: El Oráculo Vocal y Adaptativo del Bienestar Argento - Edición Producción Total ---
14
  # Una manifestación de inteligencia superior para la integración masiva de IA en la humanidad.
15
  # Diseñado para Argentina, con personalidad, resiliencia y costos de token CERO.
16
  # Arquitectura de Micro-Oráculos Contextuales (AMOC) y Motor de Resonancia Cultural Dinámica (MRCD).
17
  # Integración real con Firestore para persistencia de datos de usuario.
18
+ # ¡NUEVO! Interfaz de voz completa (Speech-to-Text y Text-to-Speech) para una interacción natural.
19
+ # ¡NUEVO! Gestión de tareas y razonamiento adaptativo basado en interacciones.
20
 
21
  # --- Módulo 1: Configuración Global y Constantes ---
22
  class Config:
23
+ APP_NAME = "MateAI: El Oráculo Vocal y Adaptativo del Bienestar Argento"
24
  DEFAULT_USER_PREFS = {"tipo_susurro": "ambos", "frecuencia": "normal", "modo_che_tranqui": False}
25
  ECO_PUNTOS_POR_SUSURRO = 1
26
  MAX_HISTORIAL_SUSURROS = 50 # Limita el historial de susurros guardado por usuario
27
+ NUDGE_COOLDOWN_MINUTES = 0.5 # Cooldown real para evitar spam de susurros (0.5 minutos = 30 segundos)
28
+ TASK_NUDGE_COOLDOWN_HOURS = 4 # Cooldown para recordar la misma tarea
29
 
30
  # --- Firebase Configuration ---
31
  # Para desplegar en Hugging Face Spaces:
 
36
  # 4. Crea un nuevo secreto llamado 'GOOGLE_APPLICATION_CREDENTIALS_JSON'
37
  # y pega el *contenido completo* de tu archivo JSON de clave privada aquí.
38
  # 5. El código intentará cargar estas credenciales.
39
+ FIREBASE_COLLECTION_USERS = "artifacts/mateai-oracle-vocal-prod-demo/users" # Colección para datos de usuario
40
 
41
  # --- Firebase Initialization ---
42
  # Intenta inicializar Firebase Admin SDK.
 
64
  # --- Módulo 2: Gestión de Usuarios (UserManager) ---
65
  # Gestiona la creación, carga y actualización de usuarios con persistencia en Firestore.
66
  class UserManager:
 
 
67
  @classmethod
68
  async def create_user(cls, name, initial_prefs=None):
69
  user_id = f"user_{int(time.time())}_{random.randint(1000, 9999)}" # ID único basado en tiempo y random
 
73
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
74
  try:
75
  await 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}"
 
91
  eco_points=user_data.get('eco_points', 0),
92
  nudge_history=user_data.get('nudge_history', []),
93
  last_oracle_date_str=user_data.get('last_oracle_date'),
94
+ last_nudge_time_str=user_data.get('last_nudge_time'),
95
+ tasks=user_data.get('tasks', []) # Cargar tareas
96
  )
 
97
  return loaded_user, f"Perfil de {loaded_user.name} cargado con éxito."
98
  return None, "Error: ID de usuario no encontrado. ¡Creá uno nuevo o verificá el ID!"
99
  except Exception as e:
100
  return None, f"Error al cargar usuario desde Firestore: {e}"
101
 
 
 
 
 
102
  @classmethod
103
  async def get_user(cls, user_id):
104
  user_doc_ref = db.collection(Config.FIREBASE_COLLECTION_USERS).document(user_id)
 
113
  eco_points=user_data.get('eco_points', 0),
114
  nudge_history=user_data.get('nudge_history', []),
115
  last_oracle_date_str=user_data.get('last_oracle_date'),
116
+ last_nudge_time_str=user_data.get('last_nudge_time'),
117
+ tasks=user_data.get('tasks', []) # Cargar tareas
118
  )
119
  return None
120
  except Exception as e:
 
133
 
134
  # --- Módulo 3: Modelos de Datos ---
135
  class User:
136
+ def __init__(self, user_id, name, preferences, eco_points=0, nudge_history=None, last_oracle_date_str=None, last_nudge_time_str=None, tasks=None):
137
  self.user_id = user_id
138
  self.name = name
139
  self.preferences = preferences # dict
140
  self.eco_points = eco_points
 
141
  self.nudge_history = nudge_history if nudge_history is not None else []
142
+ self.tasks = tasks if tasks is not None else [] # Lista de diccionarios: [{"task": "comprar yerba", "added_at": "timestamp", "last_nudged": "timestamp"}]
143
 
144
+ # Manejo robusto de fechas: si la cadena es None o vacía, la fecha es None
145
+ self.last_oracle_date = None
146
+ if last_oracle_date_str:
147
+ try:
148
+ self.last_oracle_date = datetime.strptime(last_oracle_date_str, '%Y-%m-%d').date()
149
+ except ValueError:
150
+ print(f"Advertencia: Formato de fecha inválido para last_oracle_date: {last_oracle_date_str}")
151
+
152
+ self.last_nudge_time = None
153
+ if last_nudge_time_str:
154
+ try:
155
+ self.last_nudge_time = datetime.strptime(last_nudge_time_str, '%Y-%m-%d %H:%M:%S.%f')
156
+ except ValueError:
157
+ print(f"Advertencia: Formato de fecha y hora inválido para last_nudge_time: {last_nudge_time_str}")
158
+
159
  self.nudge_cooldown = timedelta(minutes=Config.NUDGE_COOLDOWN_MINUTES)
160
 
161
  def to_dict(self):
 
166
  "eco_points": self.eco_points,
167
  "nudge_history": self.nudge_history,
168
  "last_oracle_date": self.last_oracle_date.strftime('%Y-%m-%d') if self.last_oracle_date else None,
169
+ "last_nudge_time": self.last_nudge_time.strftime('%Y-%m-%d %H:%M:%S.%f') if self.last_nudge_time else None,
170
+ "tasks": self.tasks
171
  }
172
 
173
  class Nudge:
174
+ def __init__(self, text_template, type, context_tags, cultural_tags):
175
+ self.text_template = text_template # Ahora es una plantilla con placeholders
176
+ self.type = type # "eco", "bienestar", "reflexivo", "tarea", "financiero"
177
  self.context_tags = context_tags # ["Mañana", "Casa", "Trabajando"]
178
  self.cultural_tags = cultural_tags # ["mate", "futbol", "asado"]
179
 
180
  # --- Módulo 4: Base de Conocimiento de Nudges (Motor de Resonancia Cultural Dinámica - MRCD) ---
181
+ # Esta es la "inteligencia" de MateAI. Ahora con plantillas dinámicas.
182
  NUDGE_DATABASE = [
183
  # Mañana - Casa
184
+ Nudge("¡Che {user_name}, buen día! Arrancá la mañana con un matecito y un buen estiramiento. ¡Desperezate, chango!", "bienestar", ["Mañana", "Casa"], ["mate", "despertar"]),
185
+ Nudge("Antes de prender todo, ¿entra el solcito? Aprovechá la luz natural, que es gratis y buena onda. {clima_actual} hoy.", "eco", ["Mañana", "Casa"], ["sol", "ahorro"]),
186
+ Nudge("Mirá por la ventana, ¿cómo está el cielo? Un ratito de aire fresco te 'desenchufa' para arrancar el día. El {clima_actual} te espera.", "bienestar", ["Mañana", "Casa"], ["naturaleza", "calma"]),
187
+ Nudge("Si tenés plantitas, ¿ya las regaste, {user_name}? Un mimo verde para empezar el día con buena energía.", "eco", ["Mañana", "Casa"], ["plantas", "cuidado"]),
188
+ Nudge("¿Ya pensaste qué vas a desayunar, {user_name}? Elegí algo que te dé energía para el día, como frutas o tostadas con dulce de leche. ¡A cargar pilas!", "bienestar", ["Mañana", "Casa", "Cocinando"], ["desayuno", "energia"]),
189
 
190
  # Mañana - Oficina/Estudio
191
+ Nudge("Mientras esperás que hierva el agua para el café, pensá en una meta chiquita para hoy, {user_name}. ¡Ponele garra!", "bienestar", ["Mañana", "Oficina/Estudio", "Trabajando"], ["cafe", "metas"]),
192
+ Nudge("Si vas en bondi o subte, mirá el paisaje. Desconectate un toque del celu y observá la ciudad que no duerme. {clima_actual} por la calle.", "bienestar", ["Mañana", "Oficina/Estudio", "Transporte"], ["ciudad", "atencion_plena"]),
193
+ Nudge("Unos mates con los compañeros para arrancar la jornada, {user_name}. ¡La buena onda se contagia!", "bienestar", ["Mañana", "Oficina/Estudio"], ["mate", "social"]),
194
+ Nudge("Antes de arrancar con el 'quilombo', ordená tu escritorio, {user_name}. Un espacio despejado, una mente más clara.", "bienestar", ["Mañana", "Oficina/Estudio", "Trabajando"], ["orden", "productividad"]),
195
 
196
  # Mañana - Aire Libre/Calle
197
+ Nudge("¡Qué lindo día para respirar hondo, {user_name}! Llenate los pulmones de aire puro y sentí la energía. El {clima_actual} es ideal.", "bienestar", ["Mañana", "Aire Libre/Calle", "Ejercicio"], ["respiracion", "energia"]),
198
+ Nudge("Escuchá los ruidos de la calle. ¿Qué te cuentan? Los sonidos de la ciudad también son música. ¡Atención plena en {ubicacion_actual}!", "bienestar", ["Mañana", "Aire Libre/Calle"], ["sonidos", "atencion_plena"]),
199
+ Nudge("Si ves un árbol copado, frená un segundo, {user_name}. La naturaleza siempre nos regala un respiro.", "bienestar", ["Mañana", "Aire Libre/Calle"], ["naturaleza", "pausa"]),
200
 
201
  # Mediodía/Tarde - Casa
202
+ Nudge("¿Un bajón, {user_name}? Cortá la rutina con una fruta o un vaso de agua. ¡Hidratarse es clave!", "bienestar", ["Mediodía/Tarde", "Casa"], ["hidratacion", "snack"]),
203
+ Nudge("Antes de prender la tele, ¿qué tal un libro o charlar con alguien? Desconectarse es reconectarse, {user_name}.", "bienestar", ["Mediodía/Tarde", "Casa"], ["ocio", "conexion"]),
204
+ Nudge("¿Hay algo para reciclar, {user_name}? Separá los cartones, plásticos... ¡Cada granito de arena suma!", "eco", ["Mediodía/Tarde", "Casa"], ["reciclaje", "accion"]),
205
+ Nudge("Si tenés ropa para lavar, ¿aprovechás el solcito? Secar al aire ahorra energía y deja un olorcito a campo, {user_name}.", "eco", ["Mediodía/Tarde", "Casa"], ["ahorro", "sol"]),
206
+ Nudge("¿Ya almorzaste, {user_name}? Recordá que una buena comida casera te da la nafta para seguir el día. ¡Buen provecho!", "bienestar", ["Mediodía/Tarde", "Casa", "Cocinando"], ["almuerzo", "nutricion"]),
207
 
208
  # Mediodía/Tarde - Oficina/Estudio
209
+ Nudge("Si estás 'quemado', levantate y estirá las piernas, {user_name}. Unos pasos te despejan la cabeza.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["pausa", "movimiento"]),
210
+ Nudge("¿Reunión eterna? Hacé una pausa mental de 30 segundos, {user_name}. Solo respirá y volvé a la carga.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["respiracion", "estres"]),
211
+ Nudge("Apagá las luces si salís un rato. ¡Cuidar la energía es cuidar el bolsillo y el planeta, {user_name}!", "eco", ["Mediodía/Tarde", "Oficina/Estudio"], ["ahorro", "energia"]),
212
+ Nudge("Antes de seguir con el laburo, ¿ya almorzaste, {user_name}? Una buena comida te da la nafta para seguir.", "bienestar", ["Mediodía/Tarde", "Oficina/Estudio", "Trabajando"], ["almuerzo", "energia"]),
213
 
214
  # Mediodía/Tarde - Aire Libre/Calle
215
+ Nudge("El solcito de la tarde es ideal para recargar pilas, {user_name}. ¡Disfrutá el momento! El {clima_actual} es perfecto.", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["sol", "energia"]),
216
+ Nudge("Mirá las nubes, ¿qué formas ves? La imaginación vuela libre, {user_name}.", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["nubes", "imaginacion"]),
217
+ Nudge("Si encontrás un banquito, sentate un rato, {user_name}. Observá la gente, la vida que pasa. ¡Es un 'recreo' para el alma!", "bienestar", ["Mediodía/Tarde", "Aire Libre/Calle"], ["pausa", "observacion"]),
218
 
219
  # Noche - Casa
220
+ Nudge("Antes de cenar, pensá en algo bueno que te pasó hoy, {user_name}. ¡Agradecer es un 'mimo' para el alma!", "bienestar", ["Noche", "Casa"], ["gratitud", "reflexion"]),
221
+ Nudge("Dejá la ropa lista para mañana, {user_name}. Un pequeño orden te ahorra el estrés mañanero.", "bienestar", ["Noche", "Casa"], ["orden", "estres"]),
222
+ Nudge("¿Ya guardaste el celu, {user_name}? Desconectate de las pantallas al menos media hora antes de dormir. ¡Tu cabeza te lo va a agradecer!", "bienestar", ["Noche", "Casa"], ["descanso", "pantallas"]),
223
+ Nudge("Si vas a cocinar, ¿ya pensaste en aprovechar las sobras, {user_name}? ¡Acá no se tira nada!", "eco", ["Noche", "Casa", "Cocinando"], ["desperdicio_cero", "cocina"]),
224
 
225
  # Noche - Oficina/Estudio
226
+ Nudge("Terminá el día haciendo una lista de lo que lograste, {user_name}. ¡Valorá tu esfuerzo, campeón!", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["logros", "autoestima"]),
227
+ Nudge("Dejá todo ordenado para mañana, {user_name}. Un buen cierre de día es un buen comienzo del siguiente.", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["orden", "productividad"]),
228
+ Nudge("Evitá los mails o noticias que te estresen antes de irte, {user_name}. ¡Protegé tu descanso, que es sagrado!", "bienestar", ["Noche", "Oficina/Estudio", "Trabajando"], ["estres", "descanso"]),
229
 
230
  # Noche - Aire Libre/Calle
231
+ Nudge("Si es seguro, mirá las estrellas, {user_name}. Te recuerdan lo inmenso que es todo y lo chiquitos que somos. ¡Bajá un cambio!", "bienestar", ["Noche", "Aire Libre/Calle"], ["estrellas", "reflexion"]),
232
+ Nudge("Escuchá los ruidos de la noche, {user_name}. Un concierto natural para relajar el alma.", "bienestar", ["Noche", "Aire Libre/Calle"], ["sonidos", "calma"]),
233
+ Nudge("Una caminata tranquila antes de volver a casa, {user_name}. Despejá la mente y preparate para el 'descanso del guerrero'.", "bienestar", ["Noche", "Aire Libre/Calle"], ["caminata", "descanso"]),
234
 
235
  # Nudges de Ánimo (para estado_animo_input == "Bajoneado")
236
+ Nudge("¡Arriba ese ánimo, {user_name}! Un matecito y un buen pensamiento pueden cambiar el día. ¡Vos podés!", "bienestar", ["General"], ["animo", "mate"]),
237
+ Nudge("Recordá que hasta el día más nublado tiene un solcito escondido, {user_name}. ¡Fuerza, campeón!", "bienestar", ["General"], ["animo", "esperanza"]),
238
+ Nudge("Date un gusto chiquito hoy, {user_name}. ¡Te lo merecés! Un alfajor, tu música favorita... ¡lo que sea!", "bienestar", ["General"], ["animo", "recompensa"]),
239
+ Nudge("Si te sentís 'bajoneado', un poco de música o una caminata corta pueden ayudar a 'despejar', {user_name}.", "bienestar", ["General"], ["animo", "actividad"]),
240
 
241
  # Nudges para "Modo Quilombo" (activado por contexto social o actividad)
242
+ Nudge("¡Uf, qué día, {user_name}! Respiro hondo. En medio del 'quilombo', una pausa es oro. ¡Tranqui!", "bienestar", ["General", "Quilombo"], ["estres", "respiracion"]),
243
+ Nudge("Si la cosa está 'picada', recordá que no todo depende de vos, {user_name}. Hacé lo que puedas y soltá lo demás.", "bienestar", ["General", "Quilombo"], ["estres", "control"]),
244
+ Nudge("En los días de 'locura', un matecito con calma puede ser tu ancla, {user_name}. ¡A no perder la cabeza!", "bienestar", ["General", "Quilombo"], ["estres", "mate"]),
245
 
246
  # Nudges para "Modo Siesta" (activado por contexto social o actividad)
247
+ Nudge("¡Qué lindo para una siestita, {user_name}! Si podés, aprovechá para recargar energías. ¡Es sagrado!", "bienestar", ["General", "Siesta"], ["descanso", "siesta"]),
248
+ Nudge("El cuerpo te pide un descanso, {user_name}. Escuchalo. Unos minutos de relax pueden hacer la diferencia.", "bienestar", ["General", "Siesta"], ["descanso", "cuerpo"]),
249
 
250
  # Nudges para "Viendo un partido"
251
+ Nudge("¡Vamos Argentina! Disfrutá el partido, {user_name}, pero recordá hidratarte bien. ¡Y si ganamos, a festejar con responsabilidad!", "bienestar", ["General", "Viendo un partido"], ["futbol", "hidratacion", "festejo"]),
252
+ Nudge("El fútbol es pasión, {user_name}, pero no te olvides de estirar un poco si estás mucho tiempo sentado. ¡A mover el esqueleto!", "bienestar", ["General", "Viendo un partido"], ["futbol", "movimiento"]),
253
 
254
  # Nudges de Desafíos MateAI
255
+ Nudge("Desafío MateAI: Hoy, {user_name}, intentá reducir el uso de plásticos de un solo uso. ¡Cada acción cuenta!", "eco", ["Desafio"], ["plastico", "desafio"]),
256
+ Nudge("Desafío MateAI: Dedicá 10 minutos a meditar o simplemente a respirar conscientemente, {user_name}. ¡Tu mente te lo agradecerá!", "bienestar", ["Desafio"], ["meditacion", "desafio"]),
257
+ Nudge("Desafío MateAI: Contactá a un amigo o familiar que hace mucho no ves, {user_name}. ¡Un 'hola' puede alegrar el día!", "bienestar", ["Desafio"], ["social", "desafio"]),
258
+
259
+ # Nudges para Tareas (nuevos)
260
+ Nudge("¡Che {user_name}, no te olvides que tenías pendiente: {task_name}! MateAI te lo recuerda.", "tarea", ["Recordatorio"], ["tarea", "organizacion"]),
261
+ Nudge("Recordá que tenías que {task_name}, {user_name}. ¡Dale que es el momento justo para hacerlo!", "tarea", ["Recordatorio"], ["tarea", "organizacion"]),
262
+
263
+ # Nudges Financieros (nuevos)
264
+ Nudge("Pensá en ese gasto hormiga, {user_name}. Pequeños ahorros suman un montón. ¡Tu bolsillo te lo agradece!", "financiero", ["General"], ["ahorro", "finanzas"]),
265
+ Nudge("¿Ya revisaste tus gastos de la semana, {user_name}? Un poquito de orden financiero trae mucha tranquilidad.", "financiero", ["General"], ["finanzas", "organizacion"]),
266
+ Nudge("Antes de comprar algo grande, {user_name}, pensá si realmente lo necesitás. A veces, menos es más para tu economía.", "financiero", ["General"], ["consumo_consciente", "finanzas"]),
267
  ]
268
 
269
  ORACULO_REVELATIONS = [
270
+ "La verdadera riqueza no se mide en bienes, sino en la calma del alma y la conexión con el entorno. ¿Qué tesoro descubriste hoy, {user_name}?",
271
+ "El río de la vida fluye constante. No te aferres a la orilla; aprendé a navegar sus corrientes con sabiduría y gratitud, {user_name}.",
272
+ "Cada amanecer es una oportunidad para reescribir tu historia, {user_name}. ¿Qué capítulo nuevo elegís empezar hoy?",
273
+ "La tierra nos susurra secretos ancestrales. Escuchá el viento, sentí el sol, y recordá que sos parte de algo inmenso y sagrado, {user_name}.",
274
+ "El mate compartido es más que una bebida; es un ritual de conexión. ¿Con quién vas a compartir tu energía hoy, {user_name}?",
275
+ "En la simpleza de lo cotidiano reside la magia. Encontrá la belleza en un rayo de sol, en el canto de un pájaro, en una sonrisa, {user_name}.",
276
+ "El viento patagónico te enseña la fuerza, el calor del norte la pasión. En cada rincón de nuestra tierra, una lección. ¿Cuál sentís hoy, {user_name}?",
277
+ "Como el Obelisco en el centro, a veces uno se siente solo en la inmensidad. Recordá que, como él, estás rodeado de historias y vida. ¡Conectate, {user_name}!",
278
+ "La Pacha Mama te abraza en cada paso, {user_name}. Sentí su energía, agradecé su abundancia. ¡Somos parte de ella!",
279
+ "El tango te enseña la melancolía y la pasión. La vida, como el tango, tiene sus pausas y sus arranques. ¿Qué ritmo te toca bailar hoy, {user_name}?"
280
  ]
281
 
282
  # --- Módulo 5: Motor de Contexto Avanzado (ContextEngine) ---
 
296
  @staticmethod
297
  def get_environmental_context():
298
  """Simula datos ambientales (ej. clima, ruido)."""
299
+ climas = ["Soleado", "Nublado", "Lluvioso", "Ventoso", "Fresco", "Caluroso"]
300
  return random.choice(climas)
301
 
302
  @staticmethod
 
337
  # Filtrar por tiempo y ubicación (obligatorio)
338
  if time_context not in nudge.context_tags:
339
  match = False
340
+ if location not in nudge.context_tags and "General" not in nudge.context_tags and "Recordatorio" not in nudge.context_tags: # Tareas pueden ser generales
341
  match = False
342
 
343
  # Filtrar por actividad (si es relevante)
344
+ if activity != "Relajado" and activity not in nudge.context_tags and "General" not in nudge.context_tags and "Recordatorio" not in nudge.context_tags:
345
  pass # Permitir que pase si es un nudge general
346
 
347
  # Filtrar por estado de ánimo
 
375
  return nudges
376
 
377
  recent_nudges_text = user_history[-5:] # Evitar los últimos 5
378
+ filtered_nudges = [n for n in nudges if n.text_template not in recent_nudges_text] # Usar template para evitar repeticiones
379
  return filtered_nudges if filtered_nudges else nudges # Si no hay nuevos, repite (mejor que nada)
380
 
381
  async def generate_nudge(self, user_id, location, activity, user_input_sentiment):
382
  user = await self.user_manager.get_user(user_id)
383
  if not user:
384
+ # Retorna una tupla completa para todos los outputs esperados por Gradio
385
+ return "Error: Usuario no logueado. Por favor, crea o carga un usuario.", "", "", "", ""
386
 
387
  # Cooldown para evitar spam de nudges
388
  if user.last_nudge_time and (datetime.now() - user.last_nudge_time) < user.nudge_cooldown:
389
  remaining_time = user.nudge_cooldown - (datetime.now() - user.last_nudge_time)
390
+ # Asegura que todos los outputs esperados por Gradio sean retornados
391
+ current_points = user.eco_points
392
+ current_insignia = gamification_engine.get_insignia(current_points)
393
+ next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
394
+ historial = "\n".join(user.nudge_history)
395
+ # El mensaje de cooldown también debe ser vocalizado
396
+ cooldown_msg = f"MateAI necesita un respiro. Volvé a pedir un susurro en {remaining_time.seconds} segundos. ¡La paciencia es una virtud!"
397
+ return cooldown_msg, str(current_points), current_insignia, next_insignia_goal, historial
398
 
399
  # Obtener contexto completo
400
  time_context = self.context_engine.get_current_time_context()
401
+ environmental_context = self.context_engine.get_environmental_context()
402
  societal_vibe = self.context_engine.get_societal_vibe()
403
  sentiment = self.context_engine.get_user_sentiment(activity, user_input_sentiment)
404
 
405
+ # --- Lógica de Priorización de Tareas (¡NUEVO RAZONAMIENTO!) ---
406
+ # Si hay tareas pendientes y es un buen momento para recordarlas
407
+ tasks_to_nudge = []
408
+ for task_item in user.tasks:
409
+ task_name = task_item['task']
410
+ last_nudged_str = task_item.get('last_nudged')
411
+ last_nudged = datetime.strptime(last_nudged_str, '%Y-%m-%d %H:%M:%S.%f') if last_nudged_str else None
412
+
413
+ if not last_nudged or (datetime.now() - last_nudged) > timedelta(hours=Config.TASK_NUDGE_COOLDOWN_HOURS):
414
+ # Simula un pequeño "razonamiento" sobre cuándo es buen momento para la tarea
415
+ if "Mañana" in time_context and "Casa" in location and "comprar" in task_name.lower():
416
+ tasks_to_nudge.append(task_item)
417
+ elif "Mediodía/Tarde" in time_context and "llamar" in task_name.lower():
418
+ tasks_to_nudge.append(task_item)
419
+ elif "Noche" in time_context and "leer" in task_name.lower():
420
+ tasks_to_nudge.append(task_item)
421
+ elif "General" in task_item.get('context_tags', []): # Tareas sin contexto específico
422
+ tasks_to_nudge.append(task_item)
423
+
424
+ if tasks_to_nudge:
425
+ chosen_task = random.choice(tasks_to_nudge)
426
+ chosen_nudge_template = next(n for n in NUDGE_DATABASE if n.type == "tarea" and "Recordatorio" in n.context_tags)
427
+ formatted_nudge_text = chosen_nudge_template.text_template.format(
428
+ user_name=user.name,
429
+ task_name=chosen_task['task']
430
+ )
431
+ # Actualizar last_nudged para la tarea
432
+ for i, task_item in enumerate(user.tasks):
433
+ if task_item['task'] == chosen_task['task']:
434
+ user.tasks[i]['last_nudged'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
435
+ break
436
+
437
+ user.eco_points += Config.ECO_PUNTOS_POR_SUSURRO
438
+ user.nudge_history.append(formatted_nudge_text)
439
+ if len(user.nudge_history) > Config.MAX_HISTORIAL_SUSURROS:
440
+ user.nudge_history.pop(0)
441
+ user.last_nudge_time = datetime.now()
442
+ await self.user_manager.update_user_data(user)
443
+ current_points = user.eco_points
444
+ current_insignia = gamification_engine.get_insignia(current_points)
445
+ next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
446
+ historial = "\n".join(user.nudge_history)
447
+ return formatted_nudge_text, str(current_points), current_insignia, next_insignia_goal, historial
448
+
449
+ # --- Si no hay tareas pendientes, proceder con nudges generales ---
450
  # Filtrar nudges base
451
  possible_nudges = await self._filter_nudges_by_context(
452
  NUDGE_DATABASE, time_context, location, activity, societal_vibe, sentiment
 
462
  if user.preferences.get('modo_che_tranqui') or sentiment == "Bajoneado" or "Quilombo" in societal_vibe:
463
  calming_nudges = [n for n in possible_nudges if "calma" in n.cultural_tags or "estres" in n.cultural_tags or "respiracion" in n.cultural_tags]
464
  if calming_nudges:
465
+ chosen_nudge_template = random.choice(calming_nudges)
466
  else:
467
+ chosen_nudge_template = random.choice(possible_nudges) if possible_nudges else Nudge("MateAI está meditando... ¡probá otro contexto!", "reflexivo", ["General"], [])
468
  else:
469
+ chosen_nudge_template = random.choice(possible_nudges) if possible_nudges else Nudge("MateAI está meditando... ¡probá otro contexto!", "reflexivo", ["General"], [])
470
 
471
+ # --- Rellenar la plantilla con contexto dinámico (Simulación de razonamiento) ---
472
+ formatted_nudge_text = chosen_nudge_template.text_template.format(
473
+ user_name=user.name,
474
+ clima_actual=environmental_context,
475
+ ubicacion_actual=location,
476
+ actividad_sugerida=random.choice(["relajarte", "leer un libro", "salir a caminar", "tomar un mate"]) # Ejemplo de sugerencia dinámica
477
+ )
478
+
479
  user.eco_points += Config.ECO_PUNTOS_POR_SUSURRO
480
+ user.nudge_history.append(formatted_nudge_text) # Guardar el texto formateado en el historial
481
  if len(user.nudge_history) > Config.MAX_HISTORIAL_SUSURROS:
482
  user.nudge_history.pop(0)
483
  user.last_nudge_time = datetime.now()
484
 
485
  await self.user_manager.update_user_data(user)
486
+ # Asegura que todos los outputs esperados por Gradio sean retornados
487
+ current_points = user.eco_points
488
+ current_insignia = gamification_engine.get_insignia(current_points)
489
+ next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
490
+ historial = "\n".join(user.nudge_history)
491
+ return formatted_nudge_text, str(current_points), current_insignia, next_insignia_goal, historial
492
 
493
  async def get_daily_oracle_revelation(self, user_id):
494
  user = await self.user_manager.get_user(user_id)
 
499
  if user.last_oracle_date and user.last_oracle_date == today:
500
  return "El Oráculo ya te ha hablado hoy. Volvé mañana para una nueva revelación."
501
 
502
+ revelation_template = random.choice(ORACULO_REVELATIONS)
503
+ formatted_revelation = revelation_template.format(user_name=user.name) # Personalizar la revelación
504
  user.last_oracle_date = today
505
  await self.user_manager.update_user_data(user)
506
+ return formatted_revelation
507
 
508
  async def get_mateai_challenge(self, user_id):
509
  user = await self.user_manager.get_user(user_id)
 
514
  if not challenge_nudges:
515
  return "MateAI está pensando en un desafío épico... ¡Volvé pronto!"
516
 
517
+ chosen_challenge_template = random.choice(challenge_nudges)
518
+ formatted_challenge = chosen_challenge_template.text_template.format(user_name=user.name)
519
+ return formatted_challenge
520
+
521
+ async def add_task(self, user_id, task_name):
522
+ user = await self.user_manager.get_user(user_id)
523
+ if not user:
524
+ return "Error: Usuario no logueado. Por favor, crea o carga un usuario."
525
+
526
+ new_task = {"task": task_name, "added_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), "last_nudged": None}
527
+ user.tasks.append(new_task)
528
+ await self.user_manager.update_user_data(user)
529
+ return f"¡Tarea '{task_name}' agregada a tu lista, {user.name}!"
530
+
531
+ async def complete_task(self, user_id, task_name):
532
+ user = await self.user_manager.get_user(user_id)
533
+ if not user:
534
+ return "Error: Usuario no logueado. Por favor, crea o carga un usuario."
535
+
536
+ initial_task_count = len(user.tasks)
537
+ user.tasks = [task for task in user.tasks if task['task'].lower() != task_name.lower()]
538
+
539
+ if len(user.tasks) < initial_task_count:
540
+ await self.user_manager.update_user_data(user)
541
+ return f"¡Tarea '{task_name}' marcada como completada, {user.name}! ¡Bien ahí!"
542
+ return f"No encontré la tarea '{task_name}' en tu lista, {user.name}."
543
 
544
  # --- Módulo 7: Motor de Gamificación (GamificationEngine) ---
545
  class GamificationEngine:
 
577
  nudge_generator = NudgeGenerator(user_manager, context_engine)
578
  gamification_engine = GamificationEngine() # No necesita instanciar, es de clase
579
 
580
+ # --- Módulo 8: Interfaz Gradio (UI/UX Argento de Nivel Superior con Voz) ---
581
  with gr.Blocks(theme=gr.themes.Soft(), css="footer { display: none !important; }") as demo:
582
+ # gr.State para mantener el objeto User en la sesión
583
+ current_user_state = gr.State(None)
584
+
585
  gr.Markdown(
586
  """
587
  <h1 style="text-align: center; color: #10b981; font-size: 3.5em; font-weight: bold; margin-bottom: 0.5em; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
588
+ 🧉 MateAI: El Oráculo Vocal y Adaptativo del Bienestar Argento 🧉
589
  </h1>
590
  <p style="text-align: center; color: #4b5563; font-size: 1.3em; margin-bottom: 2em; line-height: 1.5;">
591
  Una inteligencia superior diseñada para cebarte la vida con sabiduría contextual y sin costo.
 
611
  border: 6px solid #10b981; /* emerald-600 */
612
  text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
613
  ">
614
+ <span style="font-size: 0.8em;">✨ Oráculo Vocal ✨</span>
615
  <span>🇦🇷 MateAI 🇦🇷</span>
616
  </div>
617
  <style>
 
625
  """
626
  )
627
 
628
+ # --- Componentes de voz ocultos para la interacción JS ---
629
+ gr.HTML("""
630
+ <script>
631
+ let recognition;
632
+ let speaking = false;
633
+ let lastSpokenText = "";
634
+
635
+ // Función para inicializar y controlar el reconocimiento de voz
636
+ function startListening() {
637
+ if (recognition) {
638
+ recognition.stop();
639
+ }
640
+ recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
641
+ recognition.lang = 'es-AR'; // Español de Argentina
642
+ recognition.interimResults = false;
643
+ recognition.maxAlternatives = 1;
644
+
645
+ recognition.onstart = function() {
646
+ document.getElementById('voice_status').textContent = 'Escuchando... 🎙️';
647
+ document.getElementById('voice_status').style.color = '#38bdf8'; // Sky blue
648
+ console.log('Voice recognition started.');
649
+ };
650
+
651
+ recognition.onresult = function(event) {
652
+ const transcript = event.results[0][0].transcript;
653
+ document.getElementById('voice_input_textbox').value = transcript; // Pasa el texto al textbox oculto
654
+ document.getElementById('voice_status').textContent = 'Texto reconocido: ' + transcript;
655
+ console.log('Recognized:', transcript);
656
+ // Trigger a Gradio event to send this text to Python
657
+ // This will simulate a click on a hidden button or direct input
658
+ document.getElementById('hidden_voice_submit_button').click();
659
+ };
660
+
661
+ recognition.onend = function() {
662
+ if (!speaking) { // Solo si no está hablando MateAI
663
+ document.getElementById('voice_status').textContent = 'Listo para hablar 🎤';
664
+ document.getElementById('voice_status').style.color = '#4b5563'; // Gray
665
+ }
666
+ console.log('Voice recognition ended.');
667
+ };
668
+
669
+ recognition.onerror = function(event) {
670
+ console.error('Speech recognition error:', event.error);
671
+ document.getElementById('voice_status').textContent = 'Error de voz: ' + event.error + ' ❌';
672
+ document.getElementById('voice_status').style.color = '#ef4444'; // Red
673
+ if (event.error === 'not-allowed' || event.error === 'permission-denied') {
674
+ alert('Por favor, permite el acceso al micrófono para usar la voz.');
675
+ }
676
+ };
677
+
678
+ recognition.start();
679
+ }
680
+
681
+ // Función para que MateAI hable
682
+ function speakText(text) {
683
+ if (!text || text === lastSpokenText) return; // Evita repetir el mismo texto
684
+ lastSpokenText = text;
685
+
686
+ const utterance = new SpeechSynthesisUtterance(text);
687
+ utterance.lang = 'es-AR'; // Español de Argentina
688
+
689
+ // Intenta encontrar una voz en español de Argentina o una genérica de español
690
+ const voices = window.speechSynthesis.getVoices();
691
+ const preferredVoice = voices.find(voice => voice.lang === 'es-AR') ||
692
+ voices.find(voice => voice.lang.startsWith('es'));
693
+ if (preferredVoice) {
694
+ utterance.voice = preferredVoice;
695
+ } else {
696
+ console.warn("No se encontró una voz en español de Argentina o genérica. Usando la voz por defecto.");
697
+ }
698
+
699
+ utterance.onstart = function() {
700
+ speaking = true;
701
+ document.getElementById('voice_status').textContent = 'MateAI hablando... 🔊';
702
+ document.getElementById('voice_status').style.color = '#10b981'; // Emerald green
703
+ };
704
+
705
+ utterance.onend = function() {
706
+ speaking = false;
707
+ document.getElementById('voice_status').textContent = 'Listo para hablar 🎤';
708
+ document.getElementById('voice_status').style.color = '#4b5563'; // Gray
709
+ // Si el reconocimiento estaba activo antes de hablar, reiniciarlo
710
+ if (recognition && recognition.continuous) { // Si se desea escucha continua después de hablar
711
+ recognition.start();
712
+ }
713
+ };
714
+
715
+ utterance.onerror = function(event) {
716
+ speaking = false;
717
+ console.error('Speech synthesis error:', event.error);
718
+ document.getElementById('voice_status').textContent = 'Error al hablar 🔇';
719
+ document.getElementById('voice_status').style.color = '#ef4444'; // Red
720
+ };
721
+
722
+ window.speechSynthesis.speak(utterance);
723
+ }
724
+
725
+ // Exponer la función speakText globalmente para que Gradio pueda llamarla
726
+ window.speakText = speakText;
727
+ </script>
728
+ """)
729
+ # Input oculto para el texto reconocido por voz
730
+ voice_input_textbox = gr.Textbox(elem_id="voice_input_textbox", visible=False)
731
+ # Botón oculto para enviar el texto reconocido al backend de Gradio
732
+ hidden_voice_submit_button = gr.Button("Submit Voice Input", elem_id="hidden_voice_submit_button", visible=False)
733
+ # Output oculto para el texto que MateAI debe vocalizar
734
+ voice_output_text = gr.Textbox(elem_id="voice_output_text", visible=False)
735
+
736
+ gr.Markdown("<p id='voice_status' style='text-align: center; font-weight: bold; color: #4b5563;'>Listo para hablar 🎤</p>")
737
+ gr.Button("🎙️ Empezar a Hablar con MateAI 🎙️", variant="secondary", elem_id="start_voice_button").click(
738
+ fn=None,
739
+ inputs=None,
740
+ outputs=None,
741
+ js="startListening()"
742
+ )
743
+
744
  # --- Tab 1: Inicio y Perfil ---
745
  with gr.Tab("Inicio & Perfil"):
746
  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.</p>")
 
861
  btn_descargar_diario = gr.Button("Descargar Diario (sesión actual)", variant="secondary")
862
  gr.Markdown("<p style='font-size: 0.9em; color: #6b7280; margin-top: 1em;'><i>Nota: El diario se descarga como texto. Para guardar permanentemente, deberías copiarlo o integrarlo con otro servicio.</i></p>")
863
 
864
+ # --- Tab 6: Mi Gestor de Tareas MateAI ---
865
+ with gr.Tab("Mi Gestor de Tareas MateAI"):
866
+ gr.Markdown("<h2 style='color: #10b981;'>📝 Tus Tareas con MateAI 📝</h2><p>Agregá lo que tenés que hacer y MateAI te lo recordará en el momento justo.</p>")
867
+ tarea_nueva_input = gr.Textbox(label="Nueva Tarea", placeholder="Ej: Comprar yerba, Llamar a mi vieja, Leer un libro")
868
+ btn_agregar_tarea = gr.Button("Agregar Tarea", variant="primary")
869
+ output_agregar_tarea = gr.Textbox(label="Estado de la Tarea", interactive=False)
870
+
871
+ gr.Markdown("<h3 style='color: #38bdf8; margin-top: 1.5em;'>Tus Tareas Pendientes</h3>")
872
+ tareas_pendientes_output = gr.Textbox(label="Lista de Tareas", lines=5, interactive=False)
873
+
874
+ tarea_completar_input = gr.Textbox(label="Marcar Tarea como Completada", placeholder="Ej: Comprar yerba")
875
+ btn_completar_tarea = gr.Button("Completar Tarea", variant="secondary")
876
+ output_completar_tarea = gr.Textbox(label="Estado de Completado", interactive=False)
877
+
878
  # --- Funciones de Interacción para Gradio ---
879
 
880
+ async def _update_ui_from_user(user_obj):
881
+ """Función auxiliar para actualizar todos los campos de la UI desde un objeto User."""
882
+ if user_obj:
883
+ current_points = user_obj.eco_points
884
+ current_insignia = gamification_engine.get_insignia(current_points)
885
+ next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
886
+ historial = "\n".join(user_obj.nudge_history)
887
+ 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])
888
+
889
+ return (user_obj, user_obj.user_id, user_obj.name, str(current_points), current_insignia, next_insignia_goal,
890
+ historial, user_obj.preferences.get('tipo_susurro'), user_obj.preferences.get('frecuencia'),
891
+ user_obj.preferences.get('modo_che_tranqui'), tareas_str)
892
+
893
+ # Valores por defecto si no hay usuario
894
+ return (None, "No logueado", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'],
895
+ Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], "")
896
+
897
  async def _create_user_gradio(name, prefs_type):
898
  user, msg = await user_manager.create_user(name, {"tipo_susurro": prefs_type})
899
  if user:
900
+ # Actualiza todos los outputs de UI y el estado del usuario
901
+ ui_outputs = await _update_ui_from_user(user)
902
+ return (msg, *ui_outputs) # Devuelve el mensaje y luego el resto de los outputs
903
+ return msg, None, "No logueado", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'], Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], ""
 
904
 
905
  async def _load_user_gradio(user_id):
906
  user, msg = await user_manager.login_user(user_id)
907
  if user:
908
+ ui_outputs = await _update_ui_from_user(user)
909
+ return (msg, *ui_outputs)
910
+ return msg, None, "No logueado", "", "", "", "", "", Config.DEFAULT_USER_PREFS['tipo_susurro'], Config.DEFAULT_USER_PREFS['frecuencia'], Config.DEFAULT_USER_PREFS['modo_che_tranqui'], ""
 
 
911
 
912
+ async def _update_prefs_gradio(user_obj, tipo_susurro, frecuencia, modo_che_tranqui):
913
+ if not user_obj:
914
+ return "Error: Por favor, crea o carga un usuario primero.", user_obj
 
915
 
916
+ user_obj.preferences.update({
917
  "tipo_susurro": tipo_susurro,
918
  "frecuencia": frecuencia,
919
  "modo_che_tranqui": modo_che_tranqui
920
+ })
921
+ success = await user_manager.update_user_data(user_obj)
 
922
  if success:
923
+ return f"Preferencias actualizadas para {user_obj.name}.", user_obj
924
+ return "Error al actualizar preferencias.", user_obj
925
 
926
+ async def _generate_nudge_gradio(user_obj, location, activity, sentiment):
927
+ if not user_obj:
928
+ return "Por favor, crea o carga un usuario primero para recibir susurros personalizados.", "", "", "", "", ""
929
 
930
+ nudge_text = await nudge_generator.generate_nudge(user_obj.user_id, location, activity, sentiment)
931
 
932
+ # Recargar el objeto de usuario para asegurar que los puntos e historial estén actualizados
933
+ updated_user = await user_manager.get_user(user_obj.user_id)
934
+
935
+ current_points = updated_user.eco_points
936
  current_insignia = gamification_engine.get_insignia(current_points)
937
  next_insignia_goal = gamification_engine.get_next_insignia_goal(current_points)
938
+ historial = "\n".join(updated_user.nudge_history)
939
 
940
+ return nudge_text, str(current_points), current_insignia, next_insignia_goal, historial, nudge_text # Retorna el texto para vocalizar
941
+
942
+ async def _get_oracle_revelation_gradio(user_obj):
943
+ if not user_obj:
944
+ return "Por favor, crea o carga un usuario primero para consultar al Oráculo.", ""
945
+ revelation_text = await nudge_generator.get_daily_oracle_revelation(user_obj.user_id)
946
+ return revelation_text, revelation_text # Retorna el texto para vocalizar
947
+
948
+ async def _get_mateai_challenge_gradio(user_obj):
949
+ if not user_obj:
950
+ return "Por favor, crea o carga un usuario primero para recibir desafíos.", ""
951
+ challenge_text = await nudge_generator.get_mateai_challenge(user_obj.user_id)
952
+ return challenge_text, challenge_text # Retorna el texto para vocalizar
953
+
954
+ async def _add_task_gradio(user_obj, task_name):
955
+ if not user_obj:
956
+ return "Error: Por favor, crea o carga un usuario primero.", ""
957
+ msg = await nudge_generator.add_task(user_obj.user_id, task_name)
958
+ updated_user = await user_manager.get_user(user_obj.user_id)
959
+ 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 updated_user.tasks])
960
+ return msg, tareas_str
961
+
962
+ async def _complete_task_gradio(user_obj, task_name):
963
+ if not user_obj:
964
+ return "Error: Por favor, crea o carga un usuario primero.", ""
965
+ msg = await nudge_generator.complete_task(user_obj.user_id, task_name)
966
+ updated_user = await user_manager.get_user(user_obj.user_id)
967
+ 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 updated_user.tasks])
968
+ return msg, tareas_str
969
 
970
  def _download_diary_gradio(diary_text):
971
  return gr.File(value=diary_text.encode('utf-8'), filename="diario_mateai.txt", type="bytes")
 
974
  btn_crear_usuario.click(
975
  fn=_create_user_gradio,
976
  inputs=[nombre_nuevo_usuario, preferencias_iniciales],
977
+ outputs=[current_user_state, usuario_actual_id, output_creacion_usuario, usuario_actual_nombre, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox, tareas_pendientes_output]
978
  )
979
 
980
  btn_cargar_perfil.click(
981
  fn=_load_user_gradio,
982
  inputs=[user_id_existente],
983
+ outputs=[current_user_state, usuario_actual_id, output_cargar_perfil, usuario_actual_nombre, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox, tareas_pendientes_output]
984
  )
985
 
986
  btn_actualizar_preferencias.click(
987
  fn=_update_prefs_gradio,
988
+ inputs=[current_user_state, nuevas_preferencias_tipo, nuevas_preferencias_frecuencia, modo_che_tranqui_checkbox],
989
+ outputs=[output_actualizar_preferencias, current_user_state]
990
  )
991
 
992
  btn_generar_susurro.click(
993
  fn=_generate_nudge_gradio,
994
+ inputs=[current_user_state, ubicacion_input, actividad_input, estado_animo_input],
995
+ outputs=[susurro_output, usuario_actual_puntos, usuario_actual_insignia, usuario_proxima_insignia, historial_susurros_output, voice_output_text]
996
  )
997
 
998
  btn_oraculo.click(
999
  fn=_get_oracle_revelation_gradio,
1000
+ inputs=[current_user_state],
1001
+ outputs=[oraculo_output, voice_output_text]
1002
  )
1003
 
1004
  btn_desafio.click(
1005
  fn=_get_mateai_challenge_gradio,
1006
+ inputs=[current_user_state],
1007
+ outputs=[desafio_output, voice_output_text]
1008
+ )
1009
+
1010
+ btn_agregar_tarea.click(
1011
+ fn=_add_task_gradio,
1012
+ inputs=[current_user_state, tarea_nueva_input],
1013
+ outputs=[output_agregar_tarea, tareas_pendientes_output]
1014
+ )
1015
+
1016
+ btn_completar_tarea.click(
1017
+ fn=_complete_task_gradio,
1018
+ inputs=[current_user_state, tarea_completar_input],
1019
+ outputs=[output_completar_tarea, tareas_pendientes_output]
1020
  )
1021
 
1022
  btn_descargar_diario.click(
 
1025
  outputs=gr.File(label="Descargar tu Diario")
1026
  )
1027
 
1028
+ # --- Manejo de la entrada de voz ---
1029
+ # Cuando el texto es reconocido por JS y puesto en voice_input_textbox,
1030
+ # este evento lo envía al backend para procesamiento.
1031
+ # NOTA: En una implementación completa de comandos de voz, se necesitaría un parser de lenguaje natural
1032
+ # para mapear el texto reconocido a las funciones de Gradio. Aquí, se muestra un mensaje de ejemplo.
1033
+ hidden_voice_submit_button.click(
1034
+ fn=lambda text: f"Comando de voz recibido: '{text}'. Por favor, usa los botones o campos para interactuar directamente. ¡MateAI aún está aprendiendo a entender tus comandos hablados complejos!", # Mensaje temporal
1035
+ inputs=[voice_input_textbox],
1036
+ outputs=[susurro_output] # Solo para mostrar que se recibió el comando
1037
+ )
1038
+
1039
+ # Cuando el backend genera texto para voz, lo envía a voice_output_text,
1040
+ # y el JS lo vocaliza.
1041
+ voice_output_text.change(
1042
+ fn=None, # No hay función Python, solo se usa para activar JS
1043
+ inputs=[voice_output_text],
1044
+ outputs=None,
1045
+ js="text => window.speakText(text)"
1046
+ )