|
|
|
|
| import streamlit as st
|
| import logging
|
| from ..utils.widget_utils import generate_unique_key
|
| import matplotlib.pyplot as plt
|
| import numpy as np
|
| from ..database.current_situation_mongo_db import store_current_situation_result
|
|
|
|
|
| from translations import get_translations
|
|
|
|
|
| try:
|
| from .claude_recommendations import display_personalized_recommendations
|
| except ImportError:
|
|
|
| def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
|
|
|
| warning = t.get('module_not_available', "Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
|
| st.warning(warning)
|
|
|
| from .current_situation_analysis import (
|
| analyze_text_dimensions,
|
| analyze_clarity,
|
| analyze_vocabulary_diversity,
|
| analyze_cohesion,
|
| analyze_structure,
|
| get_dependency_depths,
|
| normalize_score,
|
| generate_sentence_graphs,
|
| generate_word_connections,
|
| generate_connection_paths,
|
| create_vocabulary_network,
|
| create_syntax_complexity_graph,
|
| create_cohesion_heatmap
|
| )
|
|
|
|
|
| plt.rcParams['font.family'] = 'sans-serif'
|
| plt.rcParams['axes.grid'] = True
|
| plt.rcParams['axes.spines.top'] = False
|
| plt.rcParams['axes.spines.right'] = False
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| TEXT_TYPES = {
|
| 'academic_article': {
|
|
|
| 'thresholds': {
|
| 'vocabulary': {'min': 0.70, 'target': 0.85},
|
| 'structure': {'min': 0.75, 'target': 0.90},
|
| 'cohesion': {'min': 0.65, 'target': 0.80},
|
| 'clarity': {'min': 0.70, 'target': 0.85}
|
| }
|
| },
|
| 'student_essay': {
|
| 'thresholds': {
|
| 'vocabulary': {'min': 0.60, 'target': 0.75},
|
| 'structure': {'min': 0.65, 'target': 0.80},
|
| 'cohesion': {'min': 0.55, 'target': 0.70},
|
| 'clarity': {'min': 0.60, 'target': 0.75}
|
| }
|
| },
|
| 'general_communication': {
|
| 'thresholds': {
|
| 'vocabulary': {'min': 0.50, 'target': 0.65},
|
| 'structure': {'min': 0.55, 'target': 0.70},
|
| 'cohesion': {'min': 0.45, 'target': 0.60},
|
| 'clarity': {'min': 0.50, 'target': 0.65}
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
| def display_current_situation_interface(lang_code, nlp_models, t):
|
| """
|
| Interfaz simplificada con gráfico de radar para visualizar métricas.
|
| """
|
|
|
| logger.info(f"Idioma: {lang_code}")
|
| logger.info(f"Claves en t: {list(t.keys())}")
|
|
|
|
|
| if 'text_input' not in st.session_state:
|
| st.session_state.text_input = ""
|
| if 'text_area' not in st.session_state:
|
| st.session_state.text_area = ""
|
| if 'show_results' not in st.session_state:
|
| st.session_state.show_results = False
|
| if 'current_doc' not in st.session_state:
|
| st.session_state.current_doc = None
|
| if 'current_metrics' not in st.session_state:
|
| st.session_state.current_metrics = None
|
| if 'current_recommendations' not in st.session_state:
|
| st.session_state.current_recommendations = None
|
|
|
| try:
|
|
|
| with st.container():
|
| input_col, results_col = st.columns([1,2])
|
|
|
|
|
|
|
| st.markdown("""
|
| <style>
|
| /* Hacer que la columna tenga una altura definida */
|
| [data-testid="column"] {
|
| min-height: 900px;
|
| height: 100vh; /* 100% del alto visible de la ventana */
|
| }
|
|
|
| /* Hacer que el formulario ocupe el espacio disponible en la columna */
|
| .stForm {
|
| height: calc(100% - 40px); /* Ajuste por márgenes y paddings */
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| /* Hacer que el área de texto se expanda dentro del formulario */
|
| .stForm .stTextArea {
|
| flex: 1;
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| /* El textarea en sí debe expandirse */
|
| .stForm .stTextArea textarea {
|
| flex: 1;
|
| min-height: 750px !important;
|
| }
|
| </style>
|
| """, unsafe_allow_html=True)
|
|
|
|
|
| with input_col:
|
| with st.form(key=f"text_input_form_{lang_code}"):
|
| text_input = st.text_area(
|
| t.get('input_prompt', "Escribe o pega tu texto aquí:"),
|
| height=800,
|
| key=f"text_area_{lang_code}",
|
| value=st.session_state.text_input,
|
| help=t.get('help', "Este texto será analizado para darte recomendaciones personalizadas")
|
| )
|
|
|
| submit_button = st.form_submit_button(
|
| t.get('analyze_button', "Analizar mi escritura"),
|
| type="primary",
|
| use_container_width=True
|
| )
|
|
|
| if submit_button:
|
| if text_input.strip():
|
| st.session_state.text_input = text_input
|
|
|
|
|
|
|
| try:
|
| with st.spinner(t.get('processing', "Analizando...")):
|
| doc = nlp_models[lang_code](text_input)
|
| metrics = analyze_text_dimensions(doc)
|
|
|
| storage_success = store_current_situation_result(
|
| username=st.session_state.username,
|
| text=text_input,
|
| metrics=metrics,
|
| feedback=None
|
| )
|
|
|
| if not storage_success:
|
| logger.warning("No se pudo guardar el análisis en la base de datos")
|
|
|
| st.session_state.current_doc = doc
|
| st.session_state.current_metrics = metrics
|
| st.session_state.show_results = True
|
|
|
| except Exception as e:
|
| logger.error(f"Error en análisis: {str(e)}")
|
| st.error(t.get('analysis_error', "Error al analizar el texto"))
|
|
|
|
|
| with results_col:
|
| if st.session_state.show_results and st.session_state.current_metrics is not None:
|
|
|
| st.markdown(f"### {t.get('text_type_header', 'Tipo de texto')}")
|
|
|
|
|
| text_type_options = {}
|
| for text_type_key in TEXT_TYPES.keys():
|
|
|
| default_names = {
|
| 'academic_article': 'Academic Article' if lang_code == 'en' else 'Article Académique' if lang_code == 'fr' else 'Artigo Acadêmico' if lang_code == 'pt' else 'Artículo Académico',
|
| 'student_essay': 'Student Essay' if lang_code == 'en' else 'Devoir Universitaire' if lang_code == 'fr' else 'Trabalho Universitário' if lang_code == 'pt' else 'Trabajo Universitario',
|
| 'general_communication': 'General Communication' if lang_code == 'en' else 'Communication Générale' if lang_code == 'fr' else 'Comunicação Geral' if lang_code == 'pt' else 'Comunicación General'
|
| }
|
| text_type_options[text_type_key] = default_names.get(text_type_key, text_type_key)
|
|
|
| text_type = st.radio(
|
| label=t.get('text_type_header', "Tipo de texto"),
|
| options=list(TEXT_TYPES.keys()),
|
| format_func=lambda x: text_type_options.get(x, x),
|
| horizontal=True,
|
| key="text_type_radio",
|
| label_visibility="collapsed",
|
| help=t.get('text_type_help', "Selecciona el tipo de texto para ajustar los criterios de evaluación")
|
| )
|
|
|
| st.session_state.current_text_type = text_type
|
|
|
|
|
| diagnosis_tab = "Diagnosis" if lang_code == 'en' else "Diagnostic" if lang_code == 'fr' else "Diagnóstico" if lang_code == 'pt' else "Diagnóstico"
|
| recommendations_tab = "Recommendations" if lang_code == 'en' else "Recommandations" if lang_code == 'fr' else "Recomendações" if lang_code == 'pt' else "Recomendaciones"
|
|
|
| subtab1, subtab2 = st.tabs([diagnosis_tab, recommendations_tab])
|
|
|
|
|
| with subtab1:
|
| display_diagnosis(
|
| metrics=st.session_state.current_metrics,
|
| text_type=text_type,
|
| lang_code=lang_code,
|
| t=t
|
| )
|
|
|
|
|
| with subtab2:
|
|
|
| display_personalized_recommendations(
|
| text=text_input,
|
| metrics=st.session_state.current_metrics,
|
| text_type=text_type,
|
| lang_code=lang_code,
|
| t=t
|
| )
|
|
|
| except Exception as e:
|
| logger.error(f"Error en interfaz principal: {str(e)}")
|
| st.error(t.get('error_interface', "Ocurrió un error al cargar la interfaz"))
|
|
|
|
|
|
|
| def display_diagnosis(metrics, text_type=None, lang_code='es', t=None):
|
| """
|
| Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
|
| """
|
| try:
|
|
|
| if t is None:
|
| t = {}
|
|
|
|
|
| dimension_labels = {
|
| 'es': {
|
| 'title': "Tipo de texto",
|
| 'vocabulary': "Vocabulario",
|
| 'structure': "Estructura",
|
| 'cohesion': "Cohesión",
|
| 'clarity': "Claridad",
|
| 'improvement': "⚠️ Por mejorar",
|
| 'acceptable': "📈 Aceptable",
|
| 'optimal': "✅ Óptimo",
|
| 'target': "Meta: {:.2f}"
|
| },
|
| 'en': {
|
| 'title': "Text Type",
|
| 'vocabulary': "Vocabulary",
|
| 'structure': "Structure",
|
| 'cohesion': "Cohesion",
|
| 'clarity': "Clarity",
|
| 'improvement': "⚠️ Needs improvement",
|
| 'acceptable': "📈 Acceptable",
|
| 'optimal': "✅ Optimal",
|
| 'target': "Target: {:.2f}"
|
| },
|
| 'fr': {
|
| 'title': "Type de texte",
|
| 'vocabulary': "Vocabulaire",
|
| 'structure': "Structure",
|
| 'cohesion': "Cohésion",
|
| 'clarity': "Clarté",
|
| 'improvement': "⚠️ À améliorer",
|
| 'acceptable': "📈 Acceptable",
|
| 'optimal': "✅ Optimal",
|
| 'target': "Objectif: {:.2f}"
|
| },
|
| 'pt': {
|
| 'title': "Tipo de texto",
|
| 'vocabulary': "Vocabulário",
|
| 'structure': "Estrutura",
|
| 'cohesion': "Coesão",
|
| 'clarity': "Clareza",
|
| 'improvement': "⚠️ Precisa melhorar",
|
| 'acceptable': "📈 Aceitável",
|
| 'optimal': "✅ Ótimo",
|
| 'target': "Meta: {:.2f}"
|
| }
|
| }
|
|
|
|
|
| labels = dimension_labels.get(lang_code, dimension_labels['es'])
|
|
|
|
|
| text_type = text_type or 'student_essay'
|
|
|
|
|
| thresholds = TEXT_TYPES[text_type]['thresholds']
|
|
|
|
|
| metrics_col, graph_col = st.columns([1, 1.5])
|
|
|
|
|
| with metrics_col:
|
| metrics_config = [
|
| {
|
| 'label': labels['vocabulary'],
|
| 'key': 'vocabulary',
|
| 'value': metrics['vocabulary']['normalized_score'],
|
| 'help': t.get('vocabulary_help', "Riqueza y variedad del vocabulario"),
|
| 'thresholds': thresholds['vocabulary']
|
| },
|
| {
|
| 'label': labels['structure'],
|
| 'key': 'structure',
|
| 'value': metrics['structure']['normalized_score'],
|
| 'help': t.get('structure_help', "Organización y complejidad de oraciones"),
|
| 'thresholds': thresholds['structure']
|
| },
|
| {
|
| 'label': labels['cohesion'],
|
| 'key': 'cohesion',
|
| 'value': metrics['cohesion']['normalized_score'],
|
| 'help': t.get('cohesion_help', "Conexión y fluidez entre ideas"),
|
| 'thresholds': thresholds['cohesion']
|
| },
|
| {
|
| 'label': labels['clarity'],
|
| 'key': 'clarity',
|
| 'value': metrics['clarity']['normalized_score'],
|
| 'help': t.get('clarity_help', "Facilidad de comprensión del texto"),
|
| 'thresholds': thresholds['clarity']
|
| }
|
| ]
|
|
|
|
|
| for metric in metrics_config:
|
| value = metric['value']
|
| if value < metric['thresholds']['min']:
|
| status = labels['improvement']
|
| color = "inverse"
|
| elif value < metric['thresholds']['target']:
|
| status = labels['acceptable']
|
| color = "off"
|
| else:
|
| status = labels['optimal']
|
| color = "normal"
|
|
|
| target_text = labels['target'].format(metric['thresholds']['target'])
|
|
|
| st.metric(
|
| metric['label'],
|
| f"{value:.2f}",
|
| f"{status} ({target_text})",
|
| delta_color=color,
|
| help=metric['help']
|
| )
|
| st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
|
|
|
|
|
| with graph_col:
|
| display_radar_chart(metrics_config, thresholds, lang_code)
|
|
|
| except Exception as e:
|
| logger.error(f"Error mostrando resultados: {str(e)}")
|
| st.error(t.get('error_results', "Error al mostrar los resultados"))
|
|
|
|
|
|
|
| def display_radar_chart(metrics_config, thresholds, lang_code='es'):
|
| """
|
| Muestra el gráfico radar con los resultados.
|
| """
|
| try:
|
|
|
| legend_translations = {
|
| 'es': {'min': 'Mínimo', 'target': 'Meta', 'user': 'Tu escritura'},
|
| 'en': {'min': 'Minimum', 'target': 'Target', 'user': 'Your writing'},
|
| 'fr': {'min': 'Minimum', 'target': 'Objectif', 'user': 'Votre écriture'},
|
| 'pt': {'min': 'Mínimo', 'target': 'Meta', 'user': 'Sua escrita'}
|
| }
|
|
|
|
|
| translations = legend_translations.get(lang_code, legend_translations['es'])
|
|
|
|
|
| categories = [m['label'] for m in metrics_config]
|
| values_user = [m['value'] for m in metrics_config]
|
| min_values = [m['thresholds']['min'] for m in metrics_config]
|
| target_values = [m['thresholds']['target'] for m in metrics_config]
|
|
|
|
|
| fig = plt.figure(figsize=(8, 8))
|
| ax = fig.add_subplot(111, projection='polar')
|
|
|
|
|
| angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
|
| angles += angles[:1]
|
| values_user += values_user[:1]
|
| min_values += min_values[:1]
|
| target_values += target_values[:1]
|
|
|
|
|
| ax.set_xticks(angles[:-1])
|
| ax.set_xticklabels(categories, fontsize=10)
|
| circle_ticks = np.arange(0, 1.1, 0.2)
|
| ax.set_yticks(circle_ticks)
|
| ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
|
| ax.set_ylim(0, 1)
|
|
|
|
|
| ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label=translations['min'], alpha=0.5)
|
| ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label=translations['target'], alpha=0.5)
|
| ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
|
| ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
|
|
|
|
|
| ax.plot(angles, values_user, '#3498db', linewidth=2, label=translations['user'])
|
| ax.fill(angles, values_user, '#3498db', alpha=0.2)
|
|
|
|
|
| ax.legend(
|
| loc='upper right',
|
| bbox_to_anchor=(1.3, 1.1),
|
| fontsize=10,
|
| frameon=True,
|
| facecolor='white',
|
| edgecolor='none',
|
| shadow=True
|
| )
|
|
|
| plt.tight_layout()
|
| st.pyplot(fig)
|
| plt.close()
|
|
|
| except Exception as e:
|
| logger.error(f"Error mostrando gráfico radar: {str(e)}")
|
| st.error("Error al mostrar el gráfico") |