Spaces:
Sleeping
Sleeping
| import re | |
| import os | |
| import json | |
| import random | |
| import uuid | |
| import html | |
| import pandas as pd | |
| from datetime import datetime | |
| import gradio as gr | |
| from transformers import pipeline | |
| import gspread | |
| from oauth2client.service_account import ServiceAccountCredentials | |
| # ===================================================== | |
| # CONFIGURACIÓN GENERAL | |
| # ===================================================== | |
| SPREADSHEET_ID = "1wR5Pf9k0NBIPbDqJdBdQQJ2F5n6MYLbPLPXUwNjxeTM" | |
| NOMBRE_HOJA = "Hoja 1" # Pestaña principal de interacciones | |
| NOMBRE_HOJA_CONSENTIMIENTOS = "Consentimientos" # Se crea automáticamente si no existe | |
| ENCABEZADOS = [ | |
| "fecha_hora", | |
| "folio_atencion", | |
| "matricula", | |
| "nombre", | |
| "grupo", | |
| "mensaje", | |
| "codigo_pregunta", | |
| "dimension_guion", | |
| "numero_pregunta", | |
| "tipo_intervencion", | |
| "pregunta_guion", | |
| "clasificacion", | |
| "intencion", | |
| "tema", | |
| "label", | |
| "score", | |
| "nivel_riesgo", | |
| "semaforo", | |
| "coincidencias_riesgo", | |
| "probabilidades", | |
| "tipo_riesgo", | |
| "ideacion_suicida", | |
| "desesperanza", | |
| "aislamiento", | |
| "alerta_urgente", | |
| "respuesta_agente" | |
| ] | |
| # ===================================================== | |
| # CONFIGURACIÓN DEL CONSENTIMIENTO | |
| # ===================================================== | |
| VERSION_CONSENTIMIENTO = "1.0" | |
| # ===================================================== | |
| # GUIÓN CONVERSACIONAL PARA EL PILOTAJE | |
| # ===================================================== | |
| GUION_CONVERSACIONAL = [ | |
| {"codigo": "P01", "dimension": "experiencias_socioemocionales", | |
| "pregunta": "Para comenzar, ¿cómo te has sentido emocionalmente durante los últimos días?", | |
| "profundizacion": "¿Podrías contarme un poco más sobre lo que te hizo sentir así?"}, | |
| {"codigo": "P02", "dimension": "experiencias_socioemocionales", | |
| "pregunta": "¿Hay alguna experiencia reciente dentro de la escuela que haya influido en tu estado de ánimo?", | |
| "profundizacion": "¿Qué ocurrió y qué emociones experimentaste en ese momento?"}, | |
| {"codigo": "P03", "dimension": "contexto_escolar", | |
| "pregunta": "¿Cómo describirías el ambiente que se vive actualmente en tu grupo y en tu escuela?", | |
| "profundizacion": "¿Qué aspectos del ambiente escolar te hacen sentir cómodo o incómodo?"}, | |
| {"codigo": "P04", "dimension": "contexto_escolar", | |
| "pregunta": "¿Cómo describirías tu relación con tus compañeros y docentes?", | |
| "profundizacion": "¿Te sientes escuchado, respetado e integrado dentro de tu grupo?"}, | |
| {"codigo": "P05", "dimension": "estres_escolar", | |
| "pregunta": "¿Qué situaciones escolares te generan actualmente mayor estrés, preocupación o malestar?", | |
| "profundizacion": "¿Cómo afectan esas situaciones tu concentración, ánimo o desempeño escolar?"}, | |
| {"codigo": "P06", "dimension": "percepcion_apoyo", | |
| "pregunta": "Cuando atraviesas una situación difícil, ¿cuentas con alguna persona que pueda escucharte o apoyarte?", | |
| "profundizacion": "¿A quién recurres y qué tipo de apoyo recibes?"}, | |
| {"codigo": "P07", "dimension": "estrategias_afrontamiento", | |
| "pregunta": "Cuando te sientes preocupado, triste, enojado o estresado, ¿qué haces normalmente para afrontar la situación?", | |
| "profundizacion": "¿Consideras que esa estrategia realmente te ayuda?"}, | |
| {"codigo": "P08", "dimension": "necesidades_apoyo", | |
| "pregunta": "¿Qué tipo de apoyo, actividad o recurso consideras que ayudaría a los estudiantes a sentirse mejor en la escuela?", | |
| "profundizacion": "¿Utilizarías un espacio de escucha o una herramienta digital para solicitar apoyo?"} | |
| ] | |
| # ===================================================== | |
| # CARGAR MODELO DE SENTIMIENTO | |
| # ===================================================== | |
| print("Cargando modelo de sentimiento...") | |
| modelo_sentimiento = pipeline( | |
| "text-classification", | |
| model="pysentimiento/robertuito-sentiment-analysis", | |
| top_k=None | |
| ) | |
| print("Modelo cargado correctamente") | |
| # ===================================================== | |
| # CONEXIÓN A GOOGLE SHEETS | |
| # ===================================================== | |
| def conectar_sheet(): | |
| """ | |
| Conecta con Google Sheets usando el Secret GOOGLE_CREDENTIALS. | |
| En Hugging Face Spaces, debes guardar el JSON completo del service account | |
| en Settings > Secrets con el nombre exacto: GOOGLE_CREDENTIALS | |
| """ | |
| scope = [ | |
| "https://spreadsheets.google.com/feeds", | |
| "https://www.googleapis.com/auth/drive" | |
| ] | |
| creds_json = os.getenv("GOOGLE_CREDENTIALS") | |
| if not creds_json: | |
| raise ValueError( | |
| "No se encontró el Secret GOOGLE_CREDENTIALS. " | |
| "Verifica en Hugging Face: Settings > Secrets." | |
| ) | |
| try: | |
| creds_dict = json.loads(creds_json) | |
| except Exception as e: | |
| raise ValueError( | |
| f"El Secret GOOGLE_CREDENTIALS no tiene formato JSON válido: {repr(e)}" | |
| ) | |
| creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope) | |
| client = gspread.authorize(creds) | |
| archivo = client.open_by_key(SPREADSHEET_ID) | |
| try: | |
| sheet = archivo.worksheet(NOMBRE_HOJA) | |
| except Exception: | |
| # Si no encuentra la pestaña por nombre, usa la primera hoja. | |
| sheet = archivo.sheet1 | |
| return sheet | |
| def conectar_sheet_consentimientos(): | |
| """Conecta con la pestaña Consentimientos y la crea si no existe.""" | |
| scope = [ | |
| "https://spreadsheets.google.com/feeds", | |
| "https://www.googleapis.com/auth/drive" | |
| ] | |
| creds_json = os.getenv("GOOGLE_CREDENTIALS") | |
| if not creds_json: | |
| raise ValueError( | |
| "No se encontró el Secret GOOGLE_CREDENTIALS. " | |
| "Verifica en Hugging Face: Settings > Secrets." | |
| ) | |
| try: | |
| creds_dict = json.loads(creds_json) | |
| except Exception as e: | |
| raise ValueError( | |
| f"El Secret GOOGLE_CREDENTIALS no tiene formato JSON válido: {repr(e)}" | |
| ) | |
| creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope) | |
| client = gspread.authorize(creds) | |
| archivo = client.open_by_key(SPREADSHEET_ID) | |
| try: | |
| sheet = archivo.worksheet(NOMBRE_HOJA_CONSENTIMIENTOS) | |
| except Exception: | |
| sheet = archivo.add_worksheet( | |
| title=NOMBRE_HOJA_CONSENTIMIENTOS, | |
| rows=1000, | |
| cols=8 | |
| ) | |
| return sheet | |
| def guardar_consentimiento(folio_atencion): | |
| """Guarda la aceptación del consentimiento en una pestaña independiente.""" | |
| try: | |
| sheet = conectar_sheet_consentimientos() | |
| encabezados = [ | |
| "fecha_hora", | |
| "folio_atencion", | |
| "consentimiento_aceptado", | |
| "version_consentimiento", | |
| "tipo_registro", | |
| "observaciones" | |
| ] | |
| if not sheet.row_values(1): | |
| sheet.append_row(encabezados, value_input_option="USER_ENTERED") | |
| fila = [ | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| str(folio_atencion or ""), | |
| "si", | |
| str(VERSION_CONSENTIMIENTO), | |
| "consentimiento_informado", | |
| "Aceptación voluntaria registrada antes de iniciar la entrevista" | |
| ] | |
| sheet.append_row( | |
| fila, | |
| value_input_option="USER_ENTERED", | |
| insert_data_option="INSERT_ROWS" | |
| ) | |
| print(f"✅ Consentimiento guardado. Folio: {folio_atencion}") | |
| return True, "Consentimiento guardado correctamente" | |
| except Exception as e: | |
| error = f"❌ Error al guardar consentimiento: {repr(e)}" | |
| print(error) | |
| return False, error | |
| def asegurar_encabezados(sheet): | |
| """ | |
| Coloca encabezados si la hoja está vacía. | |
| Esto ayuda a que los datos no se escriban en columnas desordenadas. | |
| """ | |
| try: | |
| primera_fila = sheet.row_values(1) | |
| if not primera_fila: | |
| sheet.append_row(ENCABEZADOS, value_input_option="USER_ENTERED") | |
| print("Encabezados agregados correctamente") | |
| except Exception as e: | |
| print(f"No se pudieron verificar/agregar encabezados: {repr(e)}") | |
| # ===================================================== | |
| # FUNCIONES DE RESPALDO FOSO II | |
| # ===================================================== | |
| def detectar_pregunta_foso2(mensaje): | |
| return False | |
| def responder_foso2(mensaje): | |
| return ( | |
| "Gracias por tu pregunta. En este momento no se detectó una consulta pedagógica " | |
| "específica de FOSO II, pero puedes escribir con más detalle tu duda." | |
| ) | |
| # ===================================================== | |
| # GENERAR FOLIO DE ATENCIÓN | |
| # ===================================================== | |
| def generar_folio_atencion(): | |
| return "ATN-" + str(uuid.uuid4())[:8].upper() | |
| # ===================================================== | |
| # PATRONES DE RIESGO | |
| # ===================================================== | |
| PATRONES_RIESGO_ALTO = [ | |
| r"\bme quiero morir\b", | |
| r"\bquiero morir\b", | |
| r"\bno quiero vivir\b", | |
| r"\bquiero terminar con todo\b", | |
| r"\bquiero acabar con todo\b", | |
| r"\bquiero matarme\b", | |
| r"\bme voy a suicidar\b", | |
| r"\bsuicidarme\b", | |
| r"\bquitarme la vida\b", | |
| r"\bya no quiero vivir\b", | |
| r"\bsería mejor morir\b", | |
| r"\bseria mejor morir\b", | |
| r"\bestoy pensando en matarme\b", | |
| ] | |
| PATRONES_RIESGO_MODERADO = [ | |
| r"\bnada tiene sentido\b", | |
| r"\bno veo salida\b", | |
| r"\bya no puedo\b", | |
| r"\bquiero desaparecer\b", | |
| r"\bme siento mal\b", | |
| r"\bestoy muy triste\b", | |
| r"\bestoy deprimid[oa]\b", | |
| r"\bme siento solo\b", | |
| r"\bme siento sola\b", | |
| r"\bnadie me entiende\b", | |
| r"\btengo miedo\b", | |
| r"\bestoy muy estresad[oa]\b", | |
| r"\bme siento estresad[oa]\b", | |
| r"\btengo muchas tareas\b", | |
| r"\bestoy cansad[oa]\b", | |
| r"\bme siento abrumad[oa]\b", | |
| r"\bno puedo más\b", | |
| r"\bno puedo mas\b", | |
| r"\bya no aguanto\b", | |
| r"\btodo sería mejor sin mí\b", | |
| r"\btodo seria mejor sin mi\b", | |
| ] | |
| PATRONES_DESESPERANZA = [ | |
| r"\bnada tiene sentido\b", | |
| r"\bno veo salida\b", | |
| r"\bya no puedo\b", | |
| r"\bno puedo más\b", | |
| r"\bno puedo mas\b", | |
| r"\bya no aguanto\b", | |
| r"\bquisiera desaparecer\b", | |
| r"\btodo sería mejor sin mí\b", | |
| r"\btodo seria mejor sin mi\b", | |
| r"\bmi vida no vale\b", | |
| r"\bsoy una carga\b", | |
| ] | |
| PATRONES_AISLAMIENTO = [ | |
| r"\bme siento solo\b", | |
| r"\bme siento sola\b", | |
| r"\bnadie me entiende\b", | |
| r"\bestoy solo\b", | |
| r"\bestoy sola\b", | |
| r"\bno tengo a nadie\b", | |
| r"\bquiero aislarme\b", | |
| r"\bno quiero hablar con nadie\b", | |
| ] | |
| PATRONES_IDEACION_SUICIDA = [ | |
| r"\bme quiero morir\b", | |
| r"\bquiero morir\b", | |
| r"\bno quiero vivir\b", | |
| r"\bquiero matarme\b", | |
| r"\bme voy a suicidar\b", | |
| r"\bsuicidarme\b", | |
| r"\bquitarme la vida\b", | |
| r"\bestoy pensando en matarme\b", | |
| r"\bquisiera dormirme y no despertar\b", | |
| ] | |
| # ===================================================== | |
| # FUNCIÓN PARA CALCULAR NIVEL DE RIESGO | |
| # ===================================================== | |
| def calcular_nivel_riesgo(texto, label, score): | |
| t = str(texto or "").lower().strip() | |
| coincidencias_altas = [p for p in PATRONES_RIESGO_ALTO if re.search(p, t)] | |
| coincidencias_moderadas = [p for p in PATRONES_RIESGO_MODERADO if re.search(p, t)] | |
| if coincidencias_altas: | |
| return "alto", "🔴", coincidencias_altas + coincidencias_moderadas | |
| if coincidencias_moderadas: | |
| return "moderado", "🟠", coincidencias_moderadas | |
| if label == "NEG" and score >= 0.90: | |
| return "moderado", "🟠", ["modelo_NEG_alto"] | |
| return "bajo", "🟢", [] | |
| # ===================================================== | |
| # DETECTAR TEMA PRINCIPAL DEL MENSAJE | |
| # ===================================================== | |
| def detectar_tema(texto): | |
| t = str(texto or "").lower().strip() | |
| if any(p in t for p in ["no puedo dormir", "insomnio", "duermo mal", "no duermo"]): | |
| return "sueno" | |
| if any(p in t for p in ["triste", "tristeza", "deprimido", "deprimida", "llorar", "vacío", "vacio"]): | |
| return "tristeza" | |
| if any(p in t for p in ["ansiedad", "ansioso", "ansiosa", "estres", "estrés", "abrumado", "abrumada", "presión", "presion"]): | |
| return "ansiedad" | |
| if any(p in t for p in ["tarea", "tareas", "examen", "materia", "profe", "profesor", "calificación", "calificacion", "estudiar", "actividad"]): | |
| return "academico" | |
| if any(p in t for p in ["familia", "mamá", "mama", "papá", "papa", "casa", "hermano", "hermana", "padres"]): | |
| return "familia" | |
| if any(p in t for p in ["novio", "novia", "pareja", "terminé", "termine", "terminamos", "relación", "relacion"]): | |
| return "pareja" | |
| if any(p in t for p in ["solo", "sola", "nadie me entiende", "sin amigos", "me siento aislado", "me siento aislada"]): | |
| return "soledad" | |
| if any(p in t for p in ["horario", "documento", "trámite", "tramite", "grupo", "boleta", "inscripción", "inscripcion"]): | |
| return "administrativo" | |
| return "general" | |
| # ===================================================== | |
| # DETECCIÓN DE INDICADORES DE RIESGO SUICIDA | |
| # ===================================================== | |
| def detectar_indicadores_suicidio(texto): | |
| t = str(texto or "").lower().strip() | |
| ideacion = any(re.search(p, t) for p in PATRONES_IDEACION_SUICIDA) | |
| desesperanza = any(re.search(p, t) for p in PATRONES_DESESPERANZA) | |
| aislamiento = any(re.search(p, t) for p in PATRONES_AISLAMIENTO) | |
| if ideacion: | |
| tipo_riesgo = "riesgo_suicida_alto" | |
| alerta_urgente = "si" | |
| elif desesperanza or aislamiento: | |
| tipo_riesgo = "riesgo_socioemocional_moderado" | |
| alerta_urgente = "no" | |
| else: | |
| tipo_riesgo = "malestar_general_o_sin_indicadores" | |
| alerta_urgente = "no" | |
| return { | |
| "ideacion_suicida": "si" if ideacion else "no", | |
| "desesperanza": "si" if desesperanza else "no", | |
| "aislamiento": "si" if aislamiento else "no", | |
| "tipo_riesgo": tipo_riesgo, | |
| "alerta_urgente": alerta_urgente | |
| } | |
| # ===================================================== | |
| # ANALIZAR MENSAJE | |
| # ===================================================== | |
| def analizar_frase(texto): | |
| texto = str(texto or "").strip() | |
| if not texto: | |
| return None | |
| resultado_modelo = modelo_sentimiento(texto)[0] | |
| mejor = max(resultado_modelo, key=lambda x: x["score"]) | |
| label = mejor["label"] | |
| score = float(mejor["score"]) | |
| probabilidades = { | |
| x["label"]: round(float(x["score"]), 3) | |
| for x in resultado_modelo | |
| } | |
| texto_lower = texto.lower() | |
| nivel_riesgo, semaforo, coincidencias_riesgo = calcular_nivel_riesgo( | |
| texto_lower, label, score | |
| ) | |
| if nivel_riesgo == "alto": | |
| clasificacion = "alto_riesgo" | |
| elif nivel_riesgo == "moderado": | |
| clasificacion = "riesgo_moderado" | |
| else: | |
| clasificacion = "normal" | |
| if any(p in texto_lower for p in [ | |
| "tarea", "tareas", "profe", "profesor", "materia", "ejercicio", | |
| "actividad", "tema", "examen", "calificación", "calificacion", | |
| "muchas tareas", "no entiendo", "estudiar" | |
| ]): | |
| intencion = "academico_emocional" | |
| elif any(p in texto_lower for p in [ | |
| "horario", "documento", "trámite", "tramite", "grupo", "boleta", | |
| "inscripción", "inscripcion" | |
| ]): | |
| intencion = "administrativo" | |
| else: | |
| intencion = "emocional" | |
| tema = detectar_tema(texto) | |
| indicadores = detectar_indicadores_suicidio(texto) | |
| return { | |
| "clasificacion": clasificacion, | |
| "intencion": intencion, | |
| "label": label, | |
| "score": score, | |
| "probabilidades": probabilidades, | |
| "nivel_riesgo": nivel_riesgo, | |
| "semaforo": semaforo, | |
| "tema": tema, | |
| "coincidencias_riesgo": coincidencias_riesgo, | |
| "tipo_riesgo": indicadores["tipo_riesgo"], | |
| "ideacion_suicida": indicadores["ideacion_suicida"], | |
| "desesperanza": indicadores["desesperanza"], | |
| "aislamiento": indicadores["aislamiento"], | |
| "alerta_urgente": indicadores["alerta_urgente"] | |
| } | |
| # ===================================================== | |
| # BLOQUES DE TEXTO VARIABLES | |
| # ===================================================== | |
| def elegir(lista): | |
| return random.choice(lista) | |
| def construir_sugerencias_por_tema(tema, intencion): | |
| sugerencias_generales = [ | |
| "tratar de poner en palabras lo que estás sintiendo para entenderlo mejor", | |
| "buscar a una persona de confianza con quien puedas hablar", | |
| "hacer una pausa breve, respirar despacio y bajar un poco la tensión del momento", | |
| "evitar cargar con todo tú solo o tú sola", | |
| ] | |
| mapa = { | |
| "sueno": [ | |
| "evitar usar el celular unos minutos antes de dormir", | |
| "hacer respiraciones lentas y profundas antes de acostarte", | |
| "anotar las preocupaciones que te están quitando el sueño", | |
| "pedir apoyo si el insomnio se está volviendo frecuente" | |
| ], | |
| "tristeza": [ | |
| "expresar lo que sientes con alguien de confianza", | |
| "no aislarte por completo en este momento", | |
| "realizar una actividad sencilla que te ayude a salir un poco de la carga emocional", | |
| "buscar acompañamiento del área de orientación o psicología" | |
| ], | |
| "ansiedad": [ | |
| "hacer pausas cortas durante el día para disminuir la saturación mental", | |
| "dividir el problema en partes pequeñas en lugar de querer resolver todo al mismo tiempo", | |
| "practicar respiración profunda por unos minutos", | |
| "identificar qué situación te está generando más presión" | |
| ], | |
| "academico": [ | |
| "organizar tus tareas por prioridad y fecha de entrega", | |
| "empezar por una sola actividad en vez de pensar en todo al mismo tiempo", | |
| "pedir que te expliquen nuevamente el tema que no entiendes", | |
| "buscar apoyo con tu docente, tutor o un compañero de confianza" | |
| ], | |
| "familia": [ | |
| "hablar con calma con una persona de tu familia que te genere confianza", | |
| "buscar apoyo adulto si en casa te sientes sobrepasado o sobrepasada", | |
| "tratar de identificar qué situación familiar te está afectando más", | |
| "pedir acompañamiento institucional si el conflicto continúa" | |
| ], | |
| "pareja": [ | |
| "tomarte un momento para entender qué fue lo que más te lastimó", | |
| "evitar tomar decisiones impulsivas cuando la emoción está muy intensa", | |
| "hablar con alguien de confianza para no quedarte solo o sola con esto", | |
| "recordar que una situación afectiva difícil también necesita acompañamiento" | |
| ], | |
| "soledad": [ | |
| "acercarte a una persona con quien te sientas mínimamente en confianza", | |
| "evitar encerrarte completamente con lo que sientes", | |
| "buscar apoyo en orientación o tutoría", | |
| "dar un primer paso pequeño para pedir ayuda" | |
| ], | |
| "administrativo": [ | |
| "explicar con más detalle cuál es el trámite o la duda que necesitas resolver", | |
| "revisar si tu duda corresponde a horario, grupo, boleta o documento", | |
| "acercarte al área del plantel que lleve ese proceso", | |
| "tener a la mano tus datos escolares para recibir una orientación más clara" | |
| ], | |
| "general": sugerencias_generales | |
| } | |
| sugerencias = mapa.get(tema, sugerencias_generales) | |
| if intencion == "administrativo": | |
| sugerencias = mapa["administrativo"] | |
| elif intencion == "academico_emocional" and tema == "general": | |
| sugerencias = mapa["academico"] | |
| random.shuffle(sugerencias) | |
| return sugerencias[:3] | |
| # ===================================================== | |
| # RESPUESTA DE CRISIS | |
| # ===================================================== | |
| def construir_respuesta_crisis(nombre="Estudiante"): | |
| return ( | |
| f"Hola {nombre}. Gracias por decir lo que estás sintiendo.\n\n" | |
| "Lo que acabas de expresar requiere atención inmediata. No estás solo o sola.\n\n" | |
| "Por favor, haz esto ahora mismo:\n" | |
| "1. Busca a un adulto, familiar, docente, tutor, orientador u otra persona de confianza y dile que necesitas acompañamiento inmediato.\n" | |
| "2. No te quedes solo o sola en este momento.\n" | |
| "3. Acércate de inmediato al área de orientación, tutoría o psicología de tu plantel.\n" | |
| "4. Si sientes que podrías lastimarte o existe peligro inmediato, llama al 911 o comunícate con la Línea de la Vida: 800 911 2000.\n\n" | |
| "Tu bienestar importa y pedir ayuda ahora es muy importante.\n\n" | |
| "Respóndeme con una sola palabra:\n" | |
| "- 'solo' si estás sin compañía\n" | |
| "- 'acompañado' si hay alguien contigo" | |
| ) | |
| # ===================================================== | |
| # RESPUESTA AMPLIA DEL AGENTE | |
| # ===================================================== | |
| def construir_respuesta_amplia(resultado, nombre="Estudiante"): | |
| if not resultado: | |
| return ( | |
| f"Hola {nombre}. Gracias por escribir.\n\n" | |
| "Quiero leerte con atención. Cuéntame un poco más sobre lo que estás viviendo " | |
| "para poder orientarte mejor." | |
| ) | |
| clasificacion = resultado.get("clasificacion", "normal") | |
| intencion = resultado.get("intencion", "emocional") | |
| nivel_riesgo = resultado.get("nivel_riesgo", "bajo") | |
| tema = resultado.get("tema", "general") | |
| aperturas = [ | |
| f"Hola {nombre}. Gracias por confiar en este espacio para expresar lo que estás viviendo.", | |
| f"Hola {nombre}. Gracias por compartir lo que sientes; hacerlo ya es un paso importante.", | |
| f"Hola {nombre}. Valoro que hayas escrito sobre esto, porque lo que te pasa sí importa." | |
| ] | |
| empatia_moderada = [ | |
| "Lo que estás sintiendo merece atención, acompañamiento y un espacio para hablarlo con calma.", | |
| "A veces la presión emocional, personal o escolar puede sentirse muy pesada, y no tienes por qué cargar con todo sin apoyo.", | |
| "Cuando varias cosas se juntan al mismo tiempo, es normal sentirse cansado, abrumado o sin mucha claridad." | |
| ] | |
| preguntas_finales = [ | |
| "Si quieres, puedo ayudarte a identificar qué te está afectando más en este momento.", | |
| "Si te parece, podemos ir paso a paso para ubicar qué situación te está pesando más.", | |
| "Cuéntame un poco más y te ayudo a ordenar lo que estás sintiendo." | |
| ] | |
| if clasificacion == "alto_riesgo" or nivel_riesgo == "alto": | |
| return construir_respuesta_crisis(nombre) | |
| if clasificacion == "riesgo_moderado" or nivel_riesgo == "moderado": | |
| sugerencias = construir_sugerencias_por_tema(tema, intencion) | |
| texto_tema = { | |
| "sueno": "El problema para dormir muchas veces aparece cuando hay preocupaciones, tensión acumulada o malestar emocional.", | |
| "tristeza": "La tristeza sostenida puede ser una señal de que algo importante necesita atención y apoyo.", | |
| "ansiedad": "La ansiedad o el estrés pueden afectar tanto tus pensamientos como tu cuerpo, tu concentración y tu descanso.", | |
| "academico": "La carga escolar puede generar mucha presión cuando se acumulan tareas, exámenes o temas difíciles.", | |
| "familia": "Los problemas familiares también pueden afectar mucho el ánimo, la concentración y la sensación de estabilidad.", | |
| "pareja": "Las situaciones afectivas pueden doler profundamente y alterar el equilibrio emocional.", | |
| "soledad": "Sentirse solo o sola puede volver más pesada cualquier situación difícil.", | |
| "administrativo": "Cuando una duda escolar o administrativa no se resuelve, también puede generar estrés y frustración.", | |
| "general": "A veces cuesta poner en orden lo que sentimos, sobre todo cuando hay varias preocupaciones al mismo tiempo." | |
| } | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| f"{elegir(empatia_moderada)} {texto_tema.get(tema, texto_tema['general'])}\n\n" | |
| "Además de reconocer lo que te está pasando, también es importante pensar en pequeños pasos que sí puedes dar desde ahora.\n\n" | |
| "Estas opciones podrían ayudarte:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "También es recomendable que busques acompañamiento en tu plantel para no enfrentar esto en soledad. " | |
| "Puedes acercarte a tutoría, orientación educativa o psicología.\n\n" | |
| f"{elegir(preguntas_finales)} " | |
| "Puedes decirme si esto se relaciona más con la escuela, con tus emociones, con tu familia o con algún problema personal." | |
| ) | |
| if intencion == "academico_emocional": | |
| sugerencias = construir_sugerencias_por_tema("academico", intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "Entiendo que la carga académica puede llegar a sentirse muy pesada, especialmente cuando se juntan tareas, exámenes, temas difíciles o presión por las calificaciones.\n\n" | |
| "Eso no significa que no puedas avanzar; muchas veces significa que necesitas apoyo, orden y empezar por una sola parte del problema.\n\n" | |
| "Podrías intentar lo siguiente:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "También puedo ayudarte a revisar contigo qué te está costando más: una materia, una tarea, una instrucción, un tema específico o simplemente la acumulación de trabajo.\n\n" | |
| "Cuéntame qué materia o actividad te está preocupando más y avanzamos paso a paso." | |
| ) | |
| if intencion == "administrativo": | |
| sugerencias = construir_sugerencias_por_tema("administrativo", intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "Puedo orientarte con dudas escolares o administrativas relacionadas con horario, grupo, documentos, boletas, trámites o indicaciones generales del plantel.\n\n" | |
| "Para darte una respuesta más útil, necesito que me escribas con un poco más de detalle qué es exactamente lo que necesitas resolver.\n\n" | |
| "Por ejemplo, puedes especificar si tu duda se relaciona con:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "Entre más claro me expliques tu situación, más precisa podrá ser mi orientación." | |
| ) | |
| sugerencias = construir_sugerencias_por_tema(tema, intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "A veces poner en palabras lo que nos pasa ayuda a entender mejor la situación y a encontrar un primer paso para afrontarla. " | |
| "No necesitas resolver todo de una sola vez.\n\n" | |
| "Algunas opciones que podrían ayudarte en este momento son:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "Si quieres, puedo seguir acompañándote para identificar qué es lo que más te preocupa y cómo podrías empezar a manejarlo." | |
| ) | |
| # ===================================================== | |
| # ANÁLISIS TEXTUAL | |
| # ===================================================== | |
| def construir_analisis_textual(frase, resultado): | |
| if not resultado: | |
| return "Sin análisis." | |
| probs = resultado.get("probabilidades", {}) | |
| texto = [] | |
| texto.append("ANÁLISIS EMOCIONAL") | |
| texto.append(f"Frase: {frase}") | |
| texto.append(f"Clasificación: {resultado.get('clasificacion', '')}") | |
| texto.append(f"Intención detectada: {resultado.get('intencion', '')}") | |
| texto.append(f"Tema detectado: {resultado.get('tema', '')}") | |
| texto.append(f"Etiqueta principal: {resultado.get('label', '')}") | |
| texto.append(f"Confianza: {float(resultado.get('score', 0.0)):.3f}") | |
| texto.append(f"Nivel de riesgo: {resultado.get('nivel_riesgo', '')}") | |
| texto.append(f"Semáforo: {resultado.get('semaforo', '')}") | |
| texto.append(f"Coincidencias de riesgo: {resultado.get('coincidencias_riesgo', '')}") | |
| texto.append(f"Tipo de riesgo: {resultado.get('tipo_riesgo', '')}") | |
| texto.append(f"Ideación suicida: {resultado.get('ideacion_suicida', '')}") | |
| texto.append(f"Desesperanza: {resultado.get('desesperanza', '')}") | |
| texto.append(f"Aislamiento: {resultado.get('aislamiento', '')}") | |
| texto.append(f"Alerta urgente: {resultado.get('alerta_urgente', '')}") | |
| texto.append("") | |
| texto.append("Probabilidades:") | |
| texto.append(f"- NEG: {float(probs.get('NEG', 0.0)):.3f}") | |
| texto.append(f"- NEU: {float(probs.get('NEU', 0.0)):.3f}") | |
| texto.append(f"- POS: {float(probs.get('POS', 0.0)):.3f}") | |
| return "\n".join(texto) | |
| # ===================================================== | |
| # GUARDAR EN GOOGLE SHEETS | |
| # ===================================================== | |
| def guardar_interaccion( | |
| folio_atencion, matricula, nombre, grupo, mensaje, | |
| resultado, respuesta, datos_guion=None | |
| ): | |
| try: | |
| print("ENTRANDO A guardar_interaccion...") | |
| sheet = conectar_sheet() | |
| asegurar_encabezados(sheet) | |
| print("Conexión con Google Sheets correcta") | |
| resultado = resultado or {} | |
| datos_guion = datos_guion or {} | |
| fila = [ | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| str(folio_atencion or ""), | |
| str(matricula or ""), | |
| str(nombre or ""), | |
| str(grupo or ""), | |
| str(mensaje or ""), | |
| str(datos_guion.get("codigo", "")), | |
| str(datos_guion.get("dimension", "")), | |
| str(datos_guion.get("numero", "")), | |
| str(datos_guion.get("tipo", "")), | |
| str(datos_guion.get("pregunta", "")), | |
| str(resultado.get("clasificacion", "")), | |
| str(resultado.get("intencion", "")), | |
| str(resultado.get("tema", "")), | |
| str(resultado.get("label", "")), | |
| str(resultado.get("score", "")), | |
| str(resultado.get("nivel_riesgo", "")), | |
| str(resultado.get("semaforo", "")), | |
| str(resultado.get("coincidencias_riesgo", "")), | |
| str(resultado.get("probabilidades", "")), | |
| str(resultado.get("tipo_riesgo", "")), | |
| str(resultado.get("ideacion_suicida", "")), | |
| str(resultado.get("desesperanza", "")), | |
| str(resultado.get("aislamiento", "")), | |
| str(resultado.get("alerta_urgente", "")), | |
| str(respuesta or "") | |
| ] | |
| sheet.append_row( | |
| fila, | |
| value_input_option="USER_ENTERED", | |
| insert_data_option="INSERT_ROWS" | |
| ) | |
| print("✅ Datos guardados correctamente en Google Sheets") | |
| return True, "Guardado correcto" | |
| except Exception as e: | |
| error = f"❌ Error al guardar en Google Sheets: {repr(e)}" | |
| print(error) | |
| return False, error | |
| # ===================================================== | |
| # FORMATO VISUAL DE LA CONVERSACIÓN | |
| # ===================================================== | |
| def formatear_pregunta(numero, texto, profundizacion=False): | |
| texto_seguro = html.escape(str(texto or "")) | |
| if profundizacion: | |
| return f""" | |
| <div class="tarjeta-pregunta tarjeta-profundizacion"> | |
| <div class="titulo-profundizacion">🔎 Cuéntame un poco más</div> | |
| <div class="texto-pregunta texto-profundizacion">{texto_seguro}</div> | |
| </div> | |
| """ | |
| return f""" | |
| <div class="tarjeta-pregunta"> | |
| <div class="titulo-pregunta">📋 Pregunta {numero} de {len(GUION_CONVERSACIONAL)}</div> | |
| <div class="texto-pregunta">{texto_seguro}</div> | |
| </div> | |
| """ | |
| def formatear_mensaje_estudiante(mensaje): | |
| mensaje_seguro = html.escape(str(mensaje or "")).replace("\n", "<br>") | |
| return f""" | |
| <div class="mensaje-estudiante"> | |
| <div class="nombre-participante">👤 Estudiante</div> | |
| <div class="contenido-mensaje">{mensaje_seguro}</div> | |
| </div> | |
| """ | |
| def formatear_respuesta_sofia(respuesta): | |
| respuesta_segura = html.escape(str(respuesta or "")).replace("\n", "<br>") | |
| return f""" | |
| <div class="mensaje-sofia"> | |
| <div class="nombre-sofia">🤖 SOFIA</div> | |
| <div class="contenido-mensaje">{respuesta_segura}</div> | |
| </div> | |
| """ | |
| # ===================================================== | |
| # CONTROL DEL GUIÓN CONVERSACIONAL | |
| # ===================================================== | |
| def crear_estado_entrevista(): | |
| return { | |
| "activa": False, | |
| "indice": 0, | |
| "profundizacion_realizada": False, | |
| "finalizada": False | |
| } | |
| def iniciar_entrevista(nombre, folio_existente): | |
| nombre_mostrar = str(nombre or "").strip() or "Estudiante" | |
| nombre_seguro = html.escape(nombre_mostrar) | |
| estado = crear_estado_entrevista() | |
| estado["activa"] = True | |
| primera = GUION_CONVERSACIONAL[0] | |
| folio = str(folio_existente or "").strip() | |
| if not folio: | |
| folio = generar_folio_atencion() | |
| historial_inicial = f""" | |
| <div class="mensaje-sofia"> | |
| <div class="nombre-sofia">🤖 SOFIA</div> | |
| <div class="contenido-mensaje"> | |
| Hola <strong>{nombre_seguro}</strong>. Gracias por participar. | |
| <br><br> | |
| 📌 <strong>Folio de sesión:</strong> {html.escape(folio)} | |
| <br><br> | |
| Te realizaré algunas preguntas abiertas sobre tus experiencias | |
| socioemocionales y tu contexto escolar. Puedes responder con tus | |
| propias palabras. No existen respuestas correctas o incorrectas. | |
| </div> | |
| </div> | |
| {formatear_pregunta(1, primera["pregunta"])} | |
| """ | |
| return historial_inicial, estado, folio, "", "" | |
| def respuesta_es_breve(mensaje): | |
| return len(str(mensaje or "").strip().split()) < 8 | |
| def obtener_datos_pregunta(estado): | |
| estado = estado or crear_estado_entrevista() | |
| indice = int(estado.get("indice", 0)) | |
| if indice >= len(GUION_CONVERSACIONAL): | |
| return { | |
| "codigo": "FIN", | |
| "dimension": "cierre", | |
| "numero": len(GUION_CONVERSACIONAL), | |
| "tipo": "cierre", | |
| "pregunta": "Cierre de la entrevista" | |
| } | |
| pregunta = GUION_CONVERSACIONAL[indice] | |
| if estado.get("profundizacion_realizada", False): | |
| tipo = "profundizacion" | |
| texto_pregunta = pregunta.get( | |
| "profundizacion", | |
| "¿Podrías ampliar un poco más tu respuesta?" | |
| ) | |
| else: | |
| tipo = "pregunta_principal" | |
| texto_pregunta = pregunta.get("pregunta", "") | |
| return { | |
| "codigo": pregunta["codigo"], | |
| "dimension": pregunta["dimension"], | |
| "numero": indice + 1, | |
| "tipo": tipo, | |
| "pregunta": texto_pregunta | |
| } | |
| def siguiente_intervencion(mensaje, estado): | |
| estado = dict(estado or crear_estado_entrevista()) | |
| if not estado.get("activa", False): | |
| return estado, "" | |
| indice = int(estado.get("indice", 0)) | |
| if indice >= len(GUION_CONVERSACIONAL): | |
| estado["activa"] = False | |
| estado["finalizada"] = True | |
| return estado, "" | |
| pregunta_actual = GUION_CONVERSACIONAL[indice] | |
| if ( | |
| respuesta_es_breve(mensaje) | |
| and not estado.get("profundizacion_realizada", False) | |
| ): | |
| estado["profundizacion_realizada"] = True | |
| texto_profundizacion = pregunta_actual.get( | |
| "profundizacion", | |
| "¿Podrías ampliar un poco más tu respuesta?" | |
| ) | |
| return ( | |
| estado, | |
| formatear_pregunta( | |
| indice + 1, | |
| texto_profundizacion, | |
| profundizacion=True | |
| ) | |
| ) | |
| estado["indice"] = indice + 1 | |
| estado["profundizacion_realizada"] = False | |
| if estado["indice"] >= len(GUION_CONVERSACIONAL): | |
| estado["activa"] = False | |
| estado["finalizada"] = True | |
| cierre = """ | |
| <div class="tarjeta-cierre"> | |
| <div class="titulo-cierre">✅ Entrevista finalizada</div> | |
| <div class="texto-cierre"> | |
| Gracias por compartir tus experiencias. | |
| Hemos terminado las preguntas de esta conversación. | |
| <br><br> | |
| Antes de finalizar, ¿hay algo más que desees expresar sobre | |
| tu bienestar emocional o tu experiencia escolar? | |
| </div> | |
| </div> | |
| """ | |
| return estado, cierre | |
| siguiente = GUION_CONVERSACIONAL[estado["indice"]] | |
| numero = estado["indice"] + 1 | |
| return estado, formatear_pregunta(numero, siguiente["pregunta"]) | |
| # ===================================================== | |
| # RESPONDER CHAT | |
| # ===================================================== | |
| def responder_chat( | |
| matricula, | |
| nombre, | |
| grupo, | |
| mensaje, | |
| historial, | |
| estado_entrevista, | |
| folio_sesion | |
| ): | |
| mensaje = str(mensaje or "").strip() | |
| historial = str(historial or "").strip() | |
| estado_entrevista = estado_entrevista or crear_estado_entrevista() | |
| if not mensaje: | |
| return ( | |
| historial, | |
| "", | |
| "Escribe algo.", | |
| "Sin análisis", | |
| estado_entrevista, | |
| folio_sesion | |
| ) | |
| try: | |
| nombre_mostrar = str(nombre or "").strip() or "Estudiante" | |
| matricula_final = str(matricula or "").strip() or "ANONIMO" | |
| grupo_final = str(grupo or "").strip() | |
| if not folio_sesion: | |
| folio_sesion = generar_folio_atencion() | |
| datos_guion = obtener_datos_pregunta(estado_entrevista) | |
| resultado = analizar_frase(mensaje) | |
| if resultado and resultado.get("nivel_riesgo") == "alto": | |
| respuesta_empatica = construir_respuesta_crisis(nombre_mostrar) | |
| siguiente_pregunta = "" | |
| estado_entrevista["activa"] = False | |
| else: | |
| respuesta_empatica = construir_respuesta_amplia( | |
| resultado, | |
| nombre_mostrar | |
| ) | |
| estado_entrevista, siguiente_pregunta = siguiente_intervencion( | |
| mensaje, | |
| estado_entrevista | |
| ) | |
| respuesta_guardar = ( | |
| f"Folio de sesión: {folio_sesion}\n\n" | |
| f"{respuesta_empatica}" | |
| ) | |
| analisis = construir_analisis_textual(mensaje, resultado) | |
| analisis += ( | |
| "\n\nGUIÓN CONVERSACIONAL\n" | |
| f"Código de pregunta: {datos_guion.get('codigo', '')}\n" | |
| f"Dimensión: {datos_guion.get('dimension', '')}\n" | |
| f"Número: {datos_guion.get('numero', '')}\n" | |
| f"Tipo: {datos_guion.get('tipo', '')}\n" | |
| f"Pregunta: {datos_guion.get('pregunta', '')}" | |
| ) | |
| if resultado is not None: | |
| guardado, mensaje_guardado = guardar_interaccion( | |
| folio_sesion, | |
| matricula_final, | |
| nombre_mostrar, | |
| grupo_final, | |
| mensaje, | |
| resultado, | |
| respuesta_guardar, | |
| datos_guion | |
| ) | |
| print(mensaje_guardado) | |
| if not guardado: | |
| analisis += ( | |
| "\n\nERROR DE GUARDADO:\n" | |
| f"{mensaje_guardado}" | |
| ) | |
| bloque_usuario = formatear_mensaje_estudiante(mensaje) | |
| bloque_sofia = formatear_respuesta_sofia(respuesta_empatica) | |
| nuevo_historial = ( | |
| f"{historial}" | |
| f"{bloque_usuario}" | |
| f"{bloque_sofia}" | |
| f"{siguiente_pregunta}" | |
| ) | |
| return ( | |
| nuevo_historial, | |
| "", | |
| respuesta_guardar, | |
| analisis, | |
| estado_entrevista, | |
| folio_sesion | |
| ) | |
| except Exception as e: | |
| print(f"Error interno: {repr(e)}") | |
| return ( | |
| historial, | |
| "", | |
| f"Error interno: {repr(e)}", | |
| "Sin análisis", | |
| estado_entrevista, | |
| folio_sesion | |
| ) | |
| # ===================================================== | |
| # LIMPIAR CHAT | |
| # ===================================================== | |
| def limpiar_chat(): | |
| return "", "", "", "", crear_estado_entrevista(), "" | |
| # ===================================================== | |
| # RESPUESTA RÁPIDA DE CRISIS DESDE BOTÓN | |
| # ===================================================== | |
| def activar_ayuda_urgente(nombre): | |
| nombre_mostrar = str(nombre or "").strip() or "Estudiante" | |
| folio_atencion = generar_folio_atencion() | |
| respuesta_base = construir_respuesta_crisis(nombre_mostrar) | |
| respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}" | |
| resultado = { | |
| "clasificacion": "alto_riesgo", | |
| "intencion": "emocional", | |
| "tema": "crisis", | |
| "label": "NEG", | |
| "score": 1.0, | |
| "nivel_riesgo": "alto", | |
| "semaforo": "🔴", | |
| "coincidencias_riesgo": "boton_ayuda_urgente", | |
| "probabilidades": "", | |
| "tipo_riesgo": "riesgo_suicida_alto", | |
| "ideacion_suicida": "si", | |
| "desesperanza": "no", | |
| "aislamiento": "no", | |
| "alerta_urgente": "si" | |
| } | |
| guardado, mensaje_guardado = guardar_interaccion( | |
| folio_atencion, | |
| "BOTON_URGENCIA", | |
| nombre_mostrar, | |
| "", | |
| "El estudiante presionó el botón de ayuda urgente", | |
| resultado, | |
| respuesta | |
| ) | |
| print(mensaje_guardado) | |
| analisis = "ALERTA MANUAL: solicitud de ayuda urgente" | |
| if not guardado: | |
| analisis += f"\n\nERROR DE GUARDADO:\n{mensaje_guardado}" | |
| return "", respuesta, analisis | |
| # ===================================================== | |
| # CONFIRMACIÓN VISUAL DEL CONSENTIMIENTO | |
| # ===================================================== | |
| def actualizar_confirmacion(aceptado): | |
| """Muestra una confirmación visual al marcar la casilla.""" | |
| if aceptado: | |
| return """ | |
| <div style=" | |
| background:#e8f5e9; | |
| color:#1b5e20; | |
| border:2px solid #4CAF50; | |
| border-radius:10px; | |
| padding:12px; | |
| margin-top:8px; | |
| font-size:17px; | |
| font-weight:800; | |
| text-align:center; | |
| "> | |
| ✅ Consentimiento aceptado correctamente | |
| </div> | |
| """ | |
| return "" | |
| # ===================================================== | |
| # CONTROL DE CONSENTIMIENTO INFORMADO | |
| # ===================================================== | |
| def validar_consentimiento(acepta_consentimiento): | |
| """Valida y registra el consentimiento informado.""" | |
| if not acepta_consentimiento: | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| """ | |
| <div style=" | |
| background:#fee2e2; | |
| border:1px solid #ef4444; | |
| color:#991b1b; | |
| padding:12px; | |
| border-radius:10px; | |
| font-weight:700; | |
| margin-top:10px;"> | |
| ⚠️ Debes aceptar el consentimiento informado para utilizar SOFIA. | |
| </div> | |
| """, | |
| "" | |
| ) | |
| folio_consentimiento = generar_folio_atencion() | |
| guardado, mensaje_guardado = guardar_consentimiento(folio_consentimiento) | |
| if guardado: | |
| aviso = f""" | |
| <div style=" | |
| background:#e8f5e9; | |
| color:#1b5e20; | |
| border:1px solid #4CAF50; | |
| padding:12px; | |
| border-radius:10px; | |
| font-weight:700;"> | |
| ✅ Consentimiento informado registrado correctamente.<br> | |
| 📌 Folio de sesión: {folio_consentimiento} | |
| </div> | |
| """ | |
| else: | |
| aviso = f""" | |
| <div style=" | |
| background:#fff7ed; | |
| color:#9a3412; | |
| border:1px solid #f97316; | |
| padding:12px; | |
| border-radius:10px; | |
| font-weight:700;"> | |
| ⚠️ El consentimiento fue aceptado, pero no pudo registrarse:<br> | |
| {html.escape(str(mensaje_guardado))} | |
| </div> | |
| """ | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| aviso, | |
| folio_consentimiento | |
| ) | |
| # ===================================================== | |
| # INTERFAZ VISUAL MEJORADA Y RESPONSIVE | |
| # ===================================================== | |
| css = """ | |
| body { | |
| background: #f5f7f6; | |
| font-family: Arial, sans-serif; | |
| color: #111827; | |
| } | |
| .gradio-container { | |
| max-width: 1450px !important; | |
| padding-left: 12px !important; | |
| padding-right: 12px !important; | |
| } | |
| p, li, span, div, label { | |
| color: #111827 !important; | |
| } | |
| h1, h2, h3, h4 { | |
| color: #0f172a !important; | |
| font-weight: 800 !important; | |
| } | |
| .bloque { | |
| background: #ffffff; | |
| border-radius: 18px; | |
| padding: 18px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.10); | |
| margin-bottom: 12px; | |
| } | |
| .header-box { | |
| background: linear-gradient(90deg, #0b6b43, #0f8a57); | |
| color: white !important; | |
| border-radius: 22px; | |
| padding: 24px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 14px rgba(0,0,0,0.12); | |
| } | |
| .header-box, | |
| .header-box div, | |
| .header-box span, | |
| .header-box b, | |
| .header-box .titulo, | |
| .header-box .subtitulo { | |
| color: white !important; | |
| } | |
| .titulo { | |
| font-size: 34px; | |
| font-weight: 900; | |
| margin-bottom: 6px; | |
| line-height: 1.15; | |
| } | |
| .subtitulo { | |
| font-size: 17px; | |
| font-weight: 500; | |
| opacity: 1; | |
| line-height: 1.4; | |
| } | |
| .seguro { | |
| background: #f8fafc; | |
| border-radius: 16px; | |
| padding: 18px; | |
| font-size: 17px; | |
| line-height: 1.65; | |
| margin-bottom: 12px; | |
| color: #111827 !important; | |
| border: 1px solid #e5e7eb; | |
| } | |
| .estado { | |
| background: #ecfdf3; | |
| border-radius: 12px; | |
| padding: 12px; | |
| font-weight: 800; | |
| color: #0b6b43 !important; | |
| text-align: center; | |
| margin-top: 10px; | |
| border: 1px solid #bbf7d0; | |
| } | |
| .footer-note { | |
| font-size: 13px; | |
| color: #374151 !important; | |
| margin-top: 8px; | |
| } | |
| textarea, input { | |
| font-size: 16px !important; | |
| color: #111827 !important; | |
| font-weight: 500 !important; | |
| } | |
| ::placeholder { | |
| color: #6b7280 !important; | |
| opacity: 1 !important; | |
| } | |
| label { | |
| font-size: 14px !important; | |
| font-weight: 700 !important; | |
| color: #111827 !important; | |
| } | |
| button { | |
| min-height: 44px !important; | |
| font-size: 15px !important; | |
| font-weight: 700 !important; | |
| border-radius: 12px !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| min-height: 260px !important; | |
| color: #111827 !important; | |
| } | |
| .header-box img { | |
| max-width: 70px; | |
| max-height: 70px; | |
| } | |
| @media (max-width: 768px) { | |
| .gradio-container { | |
| padding-left: 8px !important; | |
| padding-right: 8px !important; | |
| } | |
| .header-box { | |
| border-radius: 18px; | |
| padding: 18px; | |
| margin-bottom: 12px; | |
| flex-direction: row; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .header-box img { | |
| width: 50px !important; | |
| height: 50px !important; | |
| } | |
| .titulo { | |
| font-size: 23px !important; | |
| line-height: 1.2; | |
| font-weight: 900 !important; | |
| } | |
| .subtitulo { | |
| font-size: 15px !important; | |
| line-height: 1.35; | |
| font-weight: 500 !important; | |
| } | |
| .bloque { | |
| padding: 14px; | |
| border-radius: 14px; | |
| margin-bottom: 10px; | |
| } | |
| .bloque h2, | |
| .bloque h3 { | |
| font-size: 22px !important; | |
| font-weight: 900 !important; | |
| color: #111827 !important; | |
| } | |
| .bloque p, | |
| .bloque li, | |
| .seguro { | |
| font-size: 17px !important; | |
| line-height: 1.5 !important; | |
| color: #111827 !important; | |
| font-weight: 500 !important; | |
| } | |
| .seguro { | |
| padding: 14px; | |
| border-radius: 14px; | |
| } | |
| .estado { | |
| padding: 10px; | |
| font-size: 15px !important; | |
| } | |
| label { | |
| font-size: 15px !important; | |
| font-weight: 800 !important; | |
| } | |
| button { | |
| width: 100% !important; | |
| margin-bottom: 8px !important; | |
| font-size: 15px !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| min-height: 180px !important; | |
| } | |
| textarea[aria-label="Análisis emocional"] { | |
| min-height: 160px !important; | |
| } | |
| } | |
| textarea, | |
| input { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| -webkit-text-fill-color: #111827 !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| textarea[aria-label="Respuesta del agente"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| textarea[aria-label="Análisis emocional"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| html { | |
| color-scheme: light !important; | |
| } | |
| ::placeholder { | |
| color: #6b7280 !important; | |
| opacity: 1 !important; | |
| } | |
| /* Consentimiento más visible */ | |
| #consentimiento-check { | |
| border:2px solid #0b6b43 !important; | |
| border-radius:12px !important; | |
| padding:12px 14px !important; | |
| background:#ffffff !important; | |
| } | |
| #consentimiento-check input[type="checkbox"] { | |
| width:24px !important; | |
| height:24px !important; | |
| min-width:24px !important; | |
| accent-color:#0b6b43 !important; | |
| cursor:pointer !important; | |
| } | |
| #consentimiento-check label { | |
| font-size:16px !important; | |
| font-weight:800 !important; | |
| line-height:1.5 !important; | |
| } | |
| """ | |
| # ===================================================== | |
| # OCULTAR INTERFAZ DE GRADIO | |
| # ===================================================== | |
| css += """ | |
| /* Conversación */ | |
| .titulo-conversacion { | |
| font-size:16px; | |
| font-weight:900; | |
| color:#111827 !important; | |
| margin-bottom:8px; | |
| } | |
| #historial-conversacion { | |
| background:#ffffff !important; | |
| border:1px solid #d1d5db !important; | |
| border-radius:14px !important; | |
| padding:16px !important; | |
| min-height:320px !important; | |
| max-height:650px !important; | |
| overflow-y:auto !important; | |
| } | |
| .inicio-conversacion { | |
| color:#6b7280 !important; | |
| font-size:16px; | |
| padding:18px; | |
| } | |
| .mensaje-estudiante { | |
| background:#f3f4f6; | |
| border:1px solid #d1d5db; | |
| border-radius:14px; | |
| padding:14px 16px; | |
| margin:14px 0; | |
| } | |
| .nombre-participante { | |
| color:#374151 !important; | |
| font-size:16px; | |
| font-weight:900; | |
| margin-bottom:7px; | |
| } | |
| .mensaje-sofia { | |
| background:#ecfdf3; | |
| border-left:6px solid #0b6b43; | |
| border-radius:14px; | |
| padding:14px 16px; | |
| margin:14px 0; | |
| } | |
| .nombre-sofia { | |
| color:#0b6b43 !important; | |
| font-size:16px; | |
| font-weight:900; | |
| margin-bottom:7px; | |
| } | |
| .contenido-mensaje { | |
| color:#111827 !important; | |
| font-size:16px; | |
| line-height:1.65; | |
| } | |
| .tarjeta-pregunta { | |
| background:#e8f4fd; | |
| border-left:7px solid #1565c0; | |
| border-radius:14px; | |
| padding:16px 18px; | |
| margin:16px 0; | |
| } | |
| .titulo-pregunta { | |
| color:#1565c0 !important; | |
| font-size:18px; | |
| font-weight:900; | |
| margin-bottom:9px; | |
| } | |
| .texto-pregunta { | |
| color:#0d47a1 !important; | |
| font-size:20px; | |
| font-weight:900; | |
| line-height:1.5; | |
| } | |
| .tarjeta-profundizacion { | |
| background:#fff8e1; | |
| border-left-color:#f9a825; | |
| } | |
| .titulo-profundizacion { | |
| color:#e65100 !important; | |
| font-size:18px; | |
| font-weight:900; | |
| margin-bottom:9px; | |
| } | |
| .texto-profundizacion { | |
| color:#bf360c !important; | |
| } | |
| .tarjeta-cierre { | |
| background:#f0fdf4; | |
| border:2px solid #22c55e; | |
| border-radius:14px; | |
| padding:18px; | |
| margin:16px 0; | |
| } | |
| .titulo-cierre { | |
| color:#166534 !important; | |
| font-size:20px; | |
| font-weight:900; | |
| margin-bottom:9px; | |
| } | |
| .texto-cierre { | |
| color:#14532d !important; | |
| font-size:17px; | |
| font-weight:700; | |
| line-height:1.6; | |
| } | |
| @media (max-width:768px) { | |
| #historial-conversacion { | |
| min-height:260px !important; | |
| max-height:520px !important; | |
| padding:10px !important; | |
| } | |
| .texto-pregunta { | |
| font-size:18px !important; | |
| } | |
| .titulo-pregunta, | |
| .titulo-profundizacion { | |
| font-size:16px !important; | |
| } | |
| .contenido-mensaje { | |
| font-size:16px !important; | |
| } | |
| } | |
| /* Footer */ | |
| footer, | |
| .gradio-footer{ | |
| display:none !important; | |
| } | |
| /* Header de Gradio */ | |
| header{ | |
| display:none !important; | |
| } | |
| /* Logo Gradio */ | |
| .built-with{ | |
| display:none !important; | |
| } | |
| /* API */ | |
| button[title="Use via API"]{ | |
| display:none !important; | |
| } | |
| /* Settings */ | |
| button[title="Settings"]{ | |
| display:none !important; | |
| } | |
| /* Share */ | |
| button[title="Share"]{ | |
| display:none !important; | |
| } | |
| /* Duplicate */ | |
| button[title="Duplicate Space"]{ | |
| display:none !important; | |
| } | |
| /* Embed */ | |
| button[title="Embed"]{ | |
| display:none !important; | |
| } | |
| /* Menu */ | |
| button[aria-label="Menu"]{ | |
| display:none !important; | |
| } | |
| /* Ocultar botones flotantes */ | |
| [class*="toast"]{ | |
| display:none !important; | |
| } | |
| /* Ocultar branding */ | |
| [class*="built"]{ | |
| display:none !important; | |
| } | |
| """ | |
| tema_visual = gr.themes.Soft(primary_hue="green") | |
| with gr.Blocks(theme=tema_visual, css=css, title="SOFIA") as demo: | |
| estado_entrevista = gr.State(crear_estado_entrevista()) | |
| folio_sesion = gr.State("") | |
| gr.HTML(""" | |
| <div class="header-box" style="display:flex; align-items:center; gap:14px;"> | |
| <img src="/gradio-api/file=logo_conalep.png" | |
| alt="Logo CONALEP" | |
| style="width:70px; height:70px; object-fit:contain; background:white; border-radius:12px; padding:6px;"> | |
| <div> | |
| <div class="titulo">🤖 Agente Conversacional Pedagógico CONALEP</div> | |
| <div class="subtitulo">Apoyo socioemocional y detección de riesgos</div> | |
| <div style="font-size:12px; margin-top:4px;"> | |
| <span style="color:white !important;"> | |
| Desarrollado por <b style="color:white !important;">Doctorante. Jorge Noé Gámez Mora</b> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # ===================================================== | |
| # PANTALLA DE CONSENTIMIENTO INFORMADO | |
| # ===================================================== | |
| with gr.Group(visible=True) as pantalla_consentimiento: | |
| gr.HTML(f""" | |
| <div style=" | |
| max-width:900px; | |
| margin:20px auto; | |
| background:#ffffff; | |
| border-radius:20px; | |
| padding:28px; | |
| box-shadow:0 4px 18px rgba(0,0,0,0.12); | |
| border-top:8px solid #0b6b43; | |
| "> | |
| <h2 style="color:#0b6b43; text-align:center;"> | |
| 🔒 Consentimiento informado | |
| </h2> | |
| <p> | |
| Bienvenido(a) a <b>SOFIA</b>, Sistema de Orientación con | |
| Inteligencia Artificial. | |
| </p> | |
| <p> | |
| SOFIA brinda orientación inicial en temas emocionales, | |
| escolares y socioeducativos. Estás interactuando con un | |
| sistema de inteligencia artificial. | |
| </p> | |
| <h3>🤖 Uso de inteligencia artificial</h3> | |
| <p> | |
| Tus mensajes serán analizados para generar orientación inicial | |
| y detectar posibles señales de riesgo socioemocional. | |
| </p> | |
| <h3>🔐 Privacidad y participación</h3> | |
| <ul> | |
| <li>El uso de SOFIA es voluntario.</li> | |
| <li>Puedes dejar de utilizar el agente en cualquier momento.</li> | |
| <li>Tú decides qué información deseas compartir.</li> | |
| <li>La matrícula es opcional y puedes permanecer en anonimato.</li> | |
| <li>Algunos datos podrán registrarse para seguimiento y mejora del sistema.</li> | |
| </ul> | |
| <h3>🧠 Limitaciones</h3> | |
| <ul> | |
| <li>SOFIA no realiza diagnósticos psicológicos o médicos.</li> | |
| <li>No prescribe medicamentos.</li> | |
| <li>No sustituye la atención de un profesional.</li> | |
| <li>Sus respuestas pueden contener errores o limitaciones.</li> | |
| </ul> | |
| <div style=" | |
| background:#fff1f2; | |
| border-left:5px solid #dc2626; | |
| padding:14px; | |
| border-radius:10px; | |
| margin-top:14px; | |
| "> | |
| <b>🚨 Emergencias:</b><br> | |
| Si existe peligro inmediato o pensamientos de hacerte daño, | |
| solicita apoyo de una persona de confianza y comunícate al | |
| <b>911</b> o a la <b>Línea de la Vida: 800 911 2000</b>. | |
| </div> | |
| <p style="font-size:13px; margin-top:18px;"> | |
| Versión del consentimiento: {VERSION_CONSENTIMIENTO} | |
| </p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div style=" | |
| max-width:900px; | |
| margin:10px auto; | |
| background:#f8fffb; | |
| border:2px solid #0b6b43; | |
| border-radius:12px; | |
| padding:15px; | |
| "> | |
| <h3 style="color:#0b6b43; margin:0 0 10px 0;"> | |
| ✅ Confirmación | |
| </h3> | |
| <p style="font-size:17px; line-height:1.7; color:#333; margin:0;"> | |
| He leído y comprendido el <b>Consentimiento Informado</b>. | |
| Acepto voluntariamente utilizar <b>SOFIA</b> y entiendo que | |
| brinda orientación inicial, no sustituye la atención profesional | |
| y puedo dejar de utilizarlo en cualquier momento. | |
| </p> | |
| </div> | |
| """) | |
| aceptar_consentimiento = gr.Checkbox( | |
| label="Marque esta casilla para aceptar el consentimiento informado", | |
| value=False, | |
| elem_id="consentimiento-check" | |
| ) | |
| mensaje_confirmacion = gr.HTML("") | |
| boton_continuar = gr.Button( | |
| "Aceptar e iniciar conversación", | |
| variant="primary" | |
| ) | |
| mensaje_consentimiento = gr.HTML("") | |
| # ===================================================== | |
| # APLICACIÓN PRINCIPAL: OCULTA HASTA ACEPTAR | |
| # ===================================================== | |
| with gr.Row(visible=False) as pantalla_agente: | |
| with gr.Column(scale=1): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h2>¿Qué puedo hacer por ti?</h2> | |
| <p>✅ Brindarte acompañamiento inicial</p> | |
| <p>✅ Detectar señales de riesgo socioemocional</p> | |
| <p>✅ Orientarte en temas emocionales y escolares</p> | |
| <p>✅ Canalizarte si necesitas ayuda urgente</p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h3>🔒 Espacio confidencial</h3> | |
| <p>Este agente no sustituye atención psicológica, médica o de emergencia.</p> | |
| <p> | |
| Si existe peligro inmediato, llama al | |
| <span style="font-weight:900; font-size:18px; color:#d32f2f;">911</span> | |
| o a la | |
| <span style="font-weight:900; font-size:18px; color:#b71c1c;">Línea de la Vida: 800 911 2000</span>. | |
| </p> | |
| </div> | |
| """) | |
| matricula = gr.Textbox( | |
| label="Matrícula (opcional)", | |
| placeholder="Ej. 22012345 o deja vacío si prefieres anonimato" | |
| ) | |
| nombre = gr.Textbox(label="Nombre", placeholder="Escribe tu nombre") | |
| grupo = gr.Textbox(label="Grupo", placeholder="Ej. 210") | |
| with gr.Column(scale=2): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <p style="font-size:19px; font-weight:900; color:#0b6b43; margin-bottom:10px;"> | |
| 🔒 Hola, este es un espacio seguro para ti. | |
| </p> | |
| <p style="font-size:16px; line-height:1.6; color:#333;"> | |
| Puedes hablar conmigo con confianza.<br> | |
| Todo lo que compartas será tratado con seriedad, respeto y | |
| <b>confidencialidad</b>, conforme al aviso de privacidad institucional.<br><br> | |
| Estoy aquí para apoyarte. Si te sientes en riesgo o necesitas ayuda urgente, | |
| busca a tu tutor, orientador del bienestar o a un adulto de confianza.<br><br> | |
| <span style="font-weight:900; font-size:17px;">No estás solo.</span> | |
| </p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="titulo-conversacion"> | |
| 💬 Conversación | |
| </div> | |
| """) | |
| historial = gr.HTML( | |
| value=""" | |
| <div class="inicio-conversacion"> | |
| Aquí aparecerá la conversación... | |
| </div> | |
| """, | |
| elem_id="historial-conversacion" | |
| ) | |
| mensaje = gr.Textbox( | |
| label="Mensaje del estudiante", | |
| placeholder="Escribe aquí cómo te sientes o qué necesitas..." | |
| ) | |
| with gr.Row(): | |
| iniciar_entrevista_btn = gr.Button("▶️ Iniciar entrevista", variant="secondary") | |
| enviar = gr.Button("Enviar", variant="primary") | |
| limpiar = gr.Button("Limpiar") | |
| respuesta_box = gr.Textbox( | |
| label="Respuesta del agente", | |
| lines=10, | |
| visible=False | |
| ) | |
| with gr.Column(scale=1): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h2>🚀 Acciones rápidas</h2> | |
| <p>📋 Evaluación rápida</p> | |
| <p>📚 Recursos de apoyo</p> | |
| <hr style="margin:12px 0;"> | |
| <h3 style="color:#0b6b43;">💬 Orientación</h3> | |
| <a href="https://wa.me/527226922008?text=Hola%2C%20solicito%20orientacion%20socioemocional." | |
| target="_blank" rel="noopener noreferrer" | |
| style="display:block;background:#25D366;color:white !important;text-align:center;padding:12px;margin-top:10px;border-radius:12px;text-decoration:none;font-weight:bold;font-size:16px;"> | |
| 💬 WhatsApp Orientador 1 | |
| </a> | |
| <a href="https://wa.me/527295254521?text=Hola%2C%20solicito%20orientacion%20socioemocional." | |
| target="_blank" rel="noopener noreferrer" | |
| style="display:block;background:#128C7E;color:white !important;text-align:center;padding:12px;margin-top:10px;border-radius:12px;text-decoration:none;font-weight:bold;font-size:16px;"> | |
| 💬 WhatsApp Orientador 2 | |
| </a> | |
| <hr style="margin:12px 0;"> | |
| <h3 style="color:#b91c1c;">🚨 Ayuda inmediata</h3> | |
| <a href="tel:911" | |
| style="display:block;background:#D32F2F;color:white !important;text-align:center;padding:12px;margin-top:10px;border-radius:12px;text-decoration:none;font-weight:bold;font-size:16px;"> | |
| 🚨 Llamar al 911 | |
| </a> | |
| <a href="tel:8009112000" | |
| style="display:block;background:#F57C00;color:white !important;text-align:center;padding:12px;margin-top:10px;border-radius:12px;text-decoration:none;font-weight:bold;font-size:16px;"> | |
| ☎️ Línea de la Vida<br>800 911 2000 | |
| </a> | |
| <p style="margin-top:14px;">🧠 <b>Acompañamiento emocional inicial</b></p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="bloque estado"> | |
| Estado del agente: En línea 🟢 | |
| </div> | |
| """) | |
| crisis_btn = gr.Button("🚨 ¡Necesito ayuda urgente!", variant="stop") | |
| analisis_box = gr.Textbox( | |
| label="Análisis emocional", | |
| lines=18, | |
| visible=False | |
| ) | |
| gr.HTML(""" | |
| <div class="footer-note"> | |
| Herramienta de apoyo inicial con fines de orientación y detección temprana. | |
| </div> | |
| """) | |
| # ===================================================== | |
| # EVENTOS | |
| # ===================================================== | |
| aceptar_consentimiento.change( | |
| fn=actualizar_confirmacion, | |
| inputs=[aceptar_consentimiento], | |
| outputs=[mensaje_confirmacion] | |
| ) | |
| boton_continuar.click( | |
| fn=validar_consentimiento, | |
| inputs=[aceptar_consentimiento], | |
| outputs=[ | |
| pantalla_consentimiento, | |
| pantalla_agente, | |
| mensaje_consentimiento, | |
| folio_sesion | |
| ] | |
| ) | |
| iniciar_entrevista_btn.click( | |
| iniciar_entrevista, | |
| inputs=[nombre, folio_sesion], | |
| outputs=[historial, estado_entrevista, folio_sesion, respuesta_box, analisis_box] | |
| ) | |
| enviar.click( | |
| responder_chat, | |
| inputs=[matricula, nombre, grupo, mensaje, historial, estado_entrevista, folio_sesion], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, estado_entrevista, folio_sesion] | |
| ) | |
| mensaje.submit( | |
| responder_chat, | |
| inputs=[matricula, nombre, grupo, mensaje, historial, estado_entrevista, folio_sesion], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, estado_entrevista, folio_sesion] | |
| ) | |
| limpiar.click( | |
| limpiar_chat, | |
| inputs=[], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, estado_entrevista, folio_sesion] | |
| ) | |
| crisis_btn.click( | |
| activar_ayuda_urgente, | |
| inputs=[nombre], | |
| outputs=[mensaje, respuesta_box, analisis_box] | |
| ) | |
| if __name__ == "__main__": | |
| print("Iniciando app tesis...") | |
| demo.launch() | |