AIdeaText commited on
Commit
df333ac
·
verified ·
1 Parent(s): 8e359f5

Upload 3 files

Browse files
modules/studentact/current_situation_analysis.py CHANGED
@@ -1,1008 +1,1009 @@
1
- #v3/modules/studentact/current_situation_analysis.py
2
-
3
- import streamlit as st
4
- import matplotlib.pyplot as plt
5
- import networkx as nx
6
- import seaborn as sns
7
- from collections import Counter
8
- from itertools import combinations
9
- import numpy as np
10
- import matplotlib.patches as patches
11
- import logging
12
-
13
- from translations.recommendations import RECOMMENDATIONS
14
-
15
- # 2. Configuración básica del logging
16
- logging.basicConfig(
17
- level=logging.INFO,
18
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
- handlers=[
20
- logging.StreamHandler(),
21
- logging.FileHandler('app.log')
22
- ]
23
- )
24
-
25
- # 3. Obtener el logger específico para este módulo
26
- logger = logging.getLogger(__name__)
27
-
28
- #########################################################################
29
-
30
- def correlate_metrics(scores):
31
- """
32
- Ajusta los scores para mantener correlaciones lógicas entre métricas.
33
-
34
- Args:
35
- scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
36
-
37
- Returns:
38
- dict con scores ajustados
39
- """
40
- try:
41
- # 1. Correlación estructura-cohesión
42
- # La cohesión no puede ser menor que estructura * 0.7
43
- min_cohesion = scores['structure']['normalized_score'] * 0.7
44
- if scores['cohesion']['normalized_score'] < min_cohesion:
45
- scores['cohesion']['normalized_score'] = min_cohesion
46
-
47
- # 2. Correlación vocabulario-cohesión
48
- # La cohesión léxica depende del vocabulario
49
- vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
50
- scores['cohesion']['normalized_score'] = max(
51
- scores['cohesion']['normalized_score'],
52
- vocab_influence
53
- )
54
-
55
- # 3. Correlación cohesión-claridad
56
- # La claridad no puede superar cohesión * 1.2
57
- max_clarity = scores['cohesion']['normalized_score'] * 1.2
58
- if scores['clarity']['normalized_score'] > max_clarity:
59
- scores['clarity']['normalized_score'] = max_clarity
60
-
61
- # 4. Correlación estructura-claridad
62
- # La claridad no puede superar estructura * 1.1
63
- struct_max_clarity = scores['structure']['normalized_score'] * 1.1
64
- scores['clarity']['normalized_score'] = min(
65
- scores['clarity']['normalized_score'],
66
- struct_max_clarity
67
- )
68
-
69
- # Normalizar todos los scores entre 0 y 1
70
- for metric in scores:
71
- scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
72
-
73
- return scores
74
-
75
- except Exception as e:
76
- logger.error(f"Error en correlate_metrics: {str(e)}")
77
- return scores
78
-
79
- ##########################################################################
80
-
81
- def analyze_text_dimensions(doc):
82
- """
83
- Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
84
- """
85
- try:
86
- # Obtener scores iniciales
87
- vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
88
- struct_score = analyze_structure(doc)
89
- cohesion_score = analyze_cohesion(doc)
90
- clarity_score, clarity_details = analyze_clarity(doc)
91
-
92
- # Crear diccionario de scores inicial
93
- scores = {
94
- 'vocabulary': {
95
- 'normalized_score': vocab_score,
96
- 'details': vocab_details
97
- },
98
- 'structure': {
99
- 'normalized_score': struct_score,
100
- 'details': None
101
- },
102
- 'cohesion': {
103
- 'normalized_score': cohesion_score,
104
- 'details': None
105
- },
106
- 'clarity': {
107
- 'normalized_score': clarity_score,
108
- 'details': clarity_details
109
- }
110
- }
111
-
112
- # Ajustar correlaciones entre métricas
113
- adjusted_scores = correlate_metrics(scores)
114
-
115
- # Logging para diagnóstico
116
- logger.info(f"""
117
- Scores originales vs ajustados:
118
- Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
119
- Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
120
- Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
121
- Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
122
- """)
123
-
124
- return adjusted_scores
125
-
126
- except Exception as e:
127
- logger.error(f"Error en analyze_text_dimensions: {str(e)}")
128
- return {
129
- 'vocabulary': {'normalized_score': 0.0, 'details': {}},
130
- 'structure': {'normalized_score': 0.0, 'details': {}},
131
- 'cohesion': {'normalized_score': 0.0, 'details': {}},
132
- 'clarity': {'normalized_score': 0.0, 'details': {}}
133
- }
134
-
135
-
136
-
137
- #############################################################################################
138
-
139
- def analyze_clarity(doc):
140
- """
141
- Analiza la claridad del texto considerando múltiples factores.
142
- """
143
- try:
144
- sentences = list(doc.sents)
145
- if not sentences:
146
- return 0.0, {}
147
-
148
- # 1. Longitud de oraciones
149
- sentence_lengths = [len(sent) for sent in sentences]
150
- avg_length = sum(sentence_lengths) / len(sentences)
151
-
152
- # Normalizar usando los umbrales definidos para clarity
153
- length_score = normalize_score(
154
- value=avg_length,
155
- metric_type='clarity',
156
- optimal_length=20, # Una oración ideal tiene ~20 palabras
157
- min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
158
- target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
159
- )
160
-
161
- # 2. Análisis de conectores
162
- connector_count = 0
163
- connector_weights = {
164
- 'CCONJ': 1.0, # Coordinantes
165
- 'SCONJ': 1.2, # Subordinantes
166
- 'ADV': 0.8 # Adverbios conectivos
167
- }
168
-
169
- for token in doc:
170
- if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
171
- connector_count += connector_weights[token.pos_]
172
-
173
- # Normalizar conectores por oración
174
- connectors_per_sentence = connector_count / len(sentences) if sentences else 0
175
- connector_score = normalize_score(
176
- value=connectors_per_sentence,
177
- metric_type='clarity',
178
- optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
179
- min_threshold=0.60,
180
- target_threshold=0.75
181
- )
182
-
183
- # 3. Complejidad estructural
184
- clause_count = 0
185
- for sent in sentences:
186
- verbs = [token for token in sent if token.pos_ == 'VERB']
187
- clause_count += len(verbs)
188
-
189
- complexity_raw = clause_count / len(sentences) if sentences else 0
190
- complexity_score = normalize_score(
191
- value=complexity_raw,
192
- metric_type='clarity',
193
- optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
194
- min_threshold=0.60,
195
- target_threshold=0.75
196
- )
197
-
198
- # 4. Densidad léxica
199
- content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
200
- total_words = len([token for token in doc if token.is_alpha])
201
- density = content_words / total_words if total_words > 0 else 0
202
-
203
- density_score = normalize_score(
204
- value=density,
205
- metric_type='clarity',
206
- optimal_connections=0.6, # 60% de palabras de contenido es óptimo
207
- min_threshold=0.60,
208
- target_threshold=0.75
209
- )
210
-
211
- # Score final ponderado
212
- weights = {
213
- 'length': 0.3,
214
- 'connectors': 0.3,
215
- 'complexity': 0.2,
216
- 'density': 0.2
217
- }
218
-
219
- clarity_score = (
220
- weights['length'] * length_score +
221
- weights['connectors'] * connector_score +
222
- weights['complexity'] * complexity_score +
223
- weights['density'] * density_score
224
- )
225
-
226
- details = {
227
- 'length_score': length_score,
228
- 'connector_score': connector_score,
229
- 'complexity_score': complexity_score,
230
- 'density_score': density_score,
231
- 'avg_sentence_length': avg_length,
232
- 'connectors_per_sentence': connectors_per_sentence,
233
- 'density': density
234
- }
235
-
236
- # Agregar logging para diagnóstico
237
- logger.info(f"""
238
- Scores de Claridad:
239
- - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
240
- - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
241
- - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
242
- - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
243
- - Score Final: {clarity_score:.2f}
244
- """)
245
-
246
- return clarity_score, details
247
-
248
- except Exception as e:
249
- logger.error(f"Error en analyze_clarity: {str(e)}")
250
- return 0.0, {}
251
-
252
- #########################################################################
253
- def analyze_vocabulary_diversity(doc):
254
- """Análisis mejorado de la diversidad y calidad del vocabulario"""
255
- try:
256
- # 1. Análisis básico de diversidad
257
- unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
258
- total_words = len([token for token in doc if token.is_alpha])
259
- basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
260
-
261
- # 2. Análisis de registro
262
- academic_words = 0
263
- narrative_words = 0
264
- technical_terms = 0
265
-
266
- # Clasificar palabras por registro
267
- for token in doc:
268
- if token.is_alpha:
269
- # Detectar términos académicos/técnicos
270
- if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
271
- if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
272
- technical_terms += 1
273
- # Detectar palabras narrativas
274
- if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
275
- narrative_words += 1
276
-
277
- # 3. Análisis de complejidad sintáctica
278
- avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
279
-
280
- # 4. Calcular score ponderado
281
- weights = {
282
- 'diversity': 0.3,
283
- 'technical': 0.3,
284
- 'narrative': 0.2,
285
- 'complexity': 0.2
286
- }
287
-
288
- scores = {
289
- 'diversity': basic_diversity,
290
- 'technical': technical_terms / total_words if total_words > 0 else 0,
291
- 'narrative': narrative_words / total_words if total_words > 0 else 0,
292
- 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
293
- }
294
-
295
- # Score final ponderado
296
- final_score = sum(weights[key] * scores[key] for key in weights)
297
-
298
- # Información adicional para diagnóstico
299
- details = {
300
- 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
301
- 'scores': scores
302
- }
303
-
304
- return final_score, details
305
-
306
- except Exception as e:
307
- logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
308
- return 0.0, {}
309
-
310
- #########################################################################
311
- def analyze_cohesion(doc):
312
- """Analiza la cohesión textual"""
313
- try:
314
- sentences = list(doc.sents)
315
- if len(sentences) < 2:
316
- logger.warning("Texto demasiado corto para análisis de cohesión")
317
- return 0.0
318
-
319
- # 1. Análisis de conexiones léxicas
320
- lexical_connections = 0
321
- total_possible_connections = 0
322
-
323
- for i in range(len(sentences)-1):
324
- # Obtener lemmas significativos (no stopwords)
325
- sent1_words = {token.lemma_ for token in sentences[i]
326
- if token.is_alpha and not token.is_stop}
327
- sent2_words = {token.lemma_ for token in sentences[i+1]
328
- if token.is_alpha and not token.is_stop}
329
-
330
- if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
331
- intersection = len(sent1_words.intersection(sent2_words))
332
- total_possible = min(len(sent1_words), len(sent2_words))
333
-
334
- if total_possible > 0:
335
- lexical_score = intersection / total_possible
336
- lexical_connections += lexical_score
337
- total_possible_connections += 1
338
-
339
- # 2. Análisis de conectores
340
- connector_count = 0
341
- connector_types = {
342
- 'CCONJ': 1.0, # Coordinantes
343
- 'SCONJ': 1.2, # Subordinantes
344
- 'ADV': 0.8 # Adverbios conectivos
345
- }
346
-
347
- for token in doc:
348
- if (token.pos_ in connector_types and
349
- token.dep_ in ['cc', 'mark', 'advmod'] and
350
- not token.is_stop):
351
- connector_count += connector_types[token.pos_]
352
-
353
- # 3. Cálculo de scores normalizados
354
- if total_possible_connections > 0:
355
- lexical_cohesion = lexical_connections / total_possible_connections
356
- else:
357
- lexical_cohesion = 0
358
-
359
- if len(sentences) > 1:
360
- connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
361
- else:
362
- connector_cohesion = 0
363
-
364
- # 4. Score final ponderado
365
- weights = {
366
- 'lexical': 0.7,
367
- 'connectors': 0.3
368
- }
369
-
370
- cohesion_score = (
371
- weights['lexical'] * lexical_cohesion +
372
- weights['connectors'] * connector_cohesion
373
- )
374
-
375
- # 5. Logging para diagnóstico
376
- logger.info(f"""
377
- Análisis de Cohesión:
378
- - Conexiones léxicas encontradas: {lexical_connections}
379
- - Conexiones posibles: {total_possible_connections}
380
- - Lexical cohesion score: {lexical_cohesion}
381
- - Conectores encontrados: {connector_count}
382
- - Connector cohesion score: {connector_cohesion}
383
- - Score final: {cohesion_score}
384
- """)
385
-
386
- return cohesion_score
387
-
388
- except Exception as e:
389
- logger.error(f"Error en analyze_cohesion: {str(e)}")
390
- return 0.0
391
-
392
- #########################################################################
393
- def analyze_structure(doc):
394
- try:
395
- if len(doc) == 0:
396
- return 0.0
397
-
398
- structure_scores = []
399
- for token in doc:
400
- if token.dep_ == 'ROOT':
401
- result = get_dependency_depths(token)
402
- structure_scores.append(result['final_score'])
403
-
404
- if not structure_scores:
405
- return 0.0
406
-
407
- return min(1.0, sum(structure_scores) / len(structure_scores))
408
-
409
- except Exception as e:
410
- logger.error(f"Error en analyze_structure: {str(e)}")
411
- return 0.0
412
-
413
- #########################################################################
414
- # Funciones auxiliares de análisis
415
- def get_dependency_depths(token, depth=0, analyzed_tokens=None):
416
- """
417
- Analiza la profundidad y calidad de las relaciones de dependencia.
418
-
419
- Args:
420
- token: Token a analizar
421
- depth: Profundidad actual en el árbol
422
- analyzed_tokens: Set para evitar ciclos en el análisis
423
-
424
- Returns:
425
- dict: Información detallada sobre las dependencias
426
- - depths: Lista de profundidades
427
- - relations: Diccionario con tipos de relaciones encontradas
428
- - complexity_score: Puntuación de complejidad
429
- """
430
- if analyzed_tokens is None:
431
- analyzed_tokens = set()
432
-
433
- # Evitar ciclos
434
- if token.i in analyzed_tokens:
435
- return {
436
- 'depths': [],
437
- 'relations': {},
438
- 'complexity_score': 0
439
- }
440
-
441
- analyzed_tokens.add(token.i)
442
-
443
- # Pesos para diferentes tipos de dependencias
444
- dependency_weights = {
445
- # Dependencias principales
446
- 'nsubj': 1.2, # Sujeto nominal
447
- 'obj': 1.1, # Objeto directo
448
- 'iobj': 1.1, # Objeto indirecto
449
- 'ROOT': 1.3, # Raíz
450
-
451
- # Modificadores
452
- 'amod': 0.8, # Modificador adjetival
453
- 'advmod': 0.8, # Modificador adverbial
454
- 'nmod': 0.9, # Modificador nominal
455
-
456
- # Estructuras complejas
457
- 'csubj': 1.4, # Cláusula como sujeto
458
- 'ccomp': 1.3, # Complemento clausal
459
- 'xcomp': 1.2, # Complemento clausal abierto
460
- 'advcl': 1.2, # Cláusula adverbial
461
-
462
- # Coordinación y subordinación
463
- 'conj': 1.1, # Conjunción
464
- 'cc': 0.7, # Coordinación
465
- 'mark': 0.8, # Marcador
466
-
467
- # Otros
468
- 'det': 0.5, # Determinante
469
- 'case': 0.5, # Caso
470
- 'punct': 0.1 # Puntuación
471
- }
472
-
473
- # Inicializar resultados
474
- current_result = {
475
- 'depths': [depth],
476
- 'relations': {token.dep_: 1},
477
- 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
478
- }
479
-
480
- # Analizar hijos recursivamente
481
- for child in token.children:
482
- child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
483
-
484
- # Combinar profundidades
485
- current_result['depths'].extend(child_result['depths'])
486
-
487
- # Combinar relaciones
488
- for rel, count in child_result['relations'].items():
489
- current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
490
-
491
- # Acumular score de complejidad
492
- current_result['complexity_score'] += child_result['complexity_score']
493
-
494
- # Calcular métricas adicionales
495
- current_result['max_depth'] = max(current_result['depths'])
496
- current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
497
- current_result['relation_diversity'] = len(current_result['relations'])
498
-
499
- # Calcular score ponderado por tipo de estructura
500
- structure_bonus = 0
501
-
502
- # Bonus por estructuras complejas
503
- if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
504
- structure_bonus += 0.3
505
-
506
- # Bonus por coordinación balanceada
507
- if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
508
- structure_bonus += 0.2
509
-
510
- # Bonus por modificación rica
511
- if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
512
- structure_bonus += 0.2
513
-
514
- current_result['final_score'] = (
515
- current_result['complexity_score'] * (1 + structure_bonus)
516
- )
517
-
518
- return current_result
519
-
520
- #########################################################################
521
- def normalize_score(value, metric_type,
522
- min_threshold=0.0, target_threshold=1.0,
523
- range_factor=2.0, optimal_length=None,
524
- optimal_connections=None, optimal_depth=None):
525
- """
526
- Normaliza un valor considerando umbrales específicos por tipo de métrica.
527
-
528
- Args:
529
- value: Valor a normalizar
530
- metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
531
- min_threshold: Valor mínimo aceptable
532
- target_threshold: Valor objetivo
533
- range_factor: Factor para ajustar el rango
534
- optimal_length: Longitud óptima (opcional)
535
- optimal_connections: Número óptimo de conexiones (opcional)
536
- optimal_depth: Profundidad óptima de estructura (opcional)
537
-
538
- Returns:
539
- float: Valor normalizado entre 0 y 1
540
- """
541
- try:
542
- # Definir umbrales por tipo de métrica
543
- METRIC_THRESHOLDS = {
544
- 'vocabulary': {
545
- 'min': 0.60,
546
- 'target': 0.75,
547
- 'range_factor': 1.5
548
- },
549
- 'structure': {
550
- 'min': 0.65,
551
- 'target': 0.80,
552
- 'range_factor': 1.8
553
- },
554
- 'cohesion': {
555
- 'min': 0.55,
556
- 'target': 0.70,
557
- 'range_factor': 1.6
558
- },
559
- 'clarity': {
560
- 'min': 0.60,
561
- 'target': 0.75,
562
- 'range_factor': 1.7
563
- }
564
- }
565
-
566
- # Validar valores negativos o cero
567
- if value < 0:
568
- logger.warning(f"Valor negativo recibido: {value}")
569
- return 0.0
570
-
571
- # Manejar caso donde el valor es cero
572
- if value == 0:
573
- logger.warning("Valor cero recibido")
574
- return 0.0
575
-
576
- # Obtener umbrales específicos para el tipo de métrica
577
- thresholds = METRIC_THRESHOLDS.get(metric_type, {
578
- 'min': min_threshold,
579
- 'target': target_threshold,
580
- 'range_factor': range_factor
581
- })
582
-
583
- # Identificar el valor de referencia a usar
584
- if optimal_depth is not None:
585
- reference = optimal_depth
586
- elif optimal_connections is not None:
587
- reference = optimal_connections
588
- elif optimal_length is not None:
589
- reference = optimal_length
590
- else:
591
- reference = thresholds['target']
592
-
593
- # Validar valor de referencia
594
- if reference <= 0:
595
- logger.warning(f"Valor de referencia inválido: {reference}")
596
- return 0.0
597
-
598
- # Calcular score basado en umbrales
599
- if value < thresholds['min']:
600
- # Valor por debajo del mínimo
601
- score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
602
- elif value < thresholds['target']:
603
- # Valor entre mínimo y objetivo
604
- range_size = thresholds['target'] - thresholds['min']
605
- progress = (value - thresholds['min']) / range_size
606
- score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
607
- else:
608
- # Valor alcanza o supera el objetivo
609
- score = 1.0
610
-
611
- # Penalizar valores muy por encima del objetivo
612
- if value > (thresholds['target'] * thresholds['range_factor']):
613
- excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
614
- score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
615
-
616
- # Asegurar que el resultado esté entre 0 y 1
617
- return max(0.0, min(1.0, score))
618
-
619
- except Exception as e:
620
- logger.error(f"Error en normalize_score: {str(e)}")
621
- return 0.0
622
-
623
- #########################################################################
624
- #########################################################################
625
- def generate_recommendations(metrics, text_type, lang_code='es'):
626
- """
627
- Genera recomendaciones personalizadas basadas en las métricas del texto y el tipo de texto.
628
-
629
- Args:
630
- metrics: Diccionario con las métricas analizadas
631
- text_type: Tipo de texto ('academic_article', 'student_essay', 'general_communication')
632
- lang_code: Código del idioma para las recomendaciones (es, en, fr, pt)
633
-
634
- Returns:
635
- dict: Recomendaciones organizadas por categoría en el idioma correspondiente
636
- """
637
- try:
638
- # Obtener umbrales según el tipo de texto
639
- thresholds = TEXT_TYPES[text_type]['thresholds']
640
-
641
- # Verificar que el idioma esté soportado, usar español como respaldo
642
- if lang_code not in RECOMMENDATIONS:
643
- logger.warning(f"Idioma {lang_code} no soportado para recomendaciones, usando español")
644
- lang_code = 'es'
645
-
646
- # Obtener traducciones para el idioma seleccionado
647
- translations = RECOMMENDATIONS[lang_code]
648
-
649
- # Inicializar diccionario de recomendaciones
650
- recommendations = {
651
- 'vocabulary': [],
652
- 'structure': [],
653
- 'cohesion': [],
654
- 'clarity': [],
655
- 'specific': [],
656
- 'priority': {
657
- 'area': 'general',
658
- 'tips': []
659
- },
660
- 'text_type_name': translations['text_types'][text_type],
661
- 'dimension_names': translations['dimension_names'],
662
- 'ui_text': {
663
- 'priority_intro': translations['priority_intro'],
664
- 'detailed_recommendations': translations['detailed_recommendations'],
665
- 'save_button': translations['save_button'],
666
- 'save_success': translations['save_success'],
667
- 'save_error': translations['save_error'],
668
- 'area_priority': translations['area_priority']
669
- }
670
- }
671
-
672
- # Determinar nivel para cada dimensión y asignar recomendaciones
673
- dimensions = ['vocabulary', 'structure', 'cohesion', 'clarity']
674
- scores = {}
675
-
676
- for dim in dimensions:
677
- score = metrics[dim]['normalized_score']
678
- scores[dim] = score
679
-
680
- # Determinar nivel (bajo, medio, alto)
681
- if score < thresholds[dim]['min']:
682
- level = 'low'
683
- elif score < thresholds[dim]['target']:
684
- level = 'medium'
685
- else:
686
- level = 'high'
687
-
688
- # Asignar recomendaciones para ese nivel
689
- recommendations[dim] = translations[dim][level]
690
-
691
- # Asignar recomendaciones específicas por tipo de texto
692
- recommendations['specific'] = translations[text_type]
693
-
694
- # Determinar área prioritaria (la que tiene menor puntuación)
695
- priority_dimension = min(scores, key=scores.get)
696
- recommendations['priority']['area'] = priority_dimension
697
- recommendations['priority']['tips'] = recommendations[priority_dimension]
698
-
699
- logger.info(f"Generadas recomendaciones en {lang_code} para texto tipo {text_type}")
700
- return recommendations
701
-
702
- except Exception as e:
703
- logger.error(f"Error en generate_recommendations: {str(e)}")
704
- # Retornar mensajes genéricos en caso de error
705
- if lang_code == 'en':
706
- return {
707
- 'vocabulary': ["Try enriching your vocabulary"],
708
- 'structure': ["Work on the structure of your sentences"],
709
- 'cohesion': ["Improve the connection between your ideas"],
710
- 'clarity': ["Try to express your ideas more clearly"],
711
- 'specific': ["Adapt your text according to its purpose"],
712
- 'priority': {
713
- 'area': 'general',
714
- 'tips': ["Seek specific feedback from a tutor or teacher"]
715
- },
716
- 'dimension_names': {
717
- 'vocabulary': 'Vocabulary',
718
- 'structure': 'Structure',
719
- 'cohesion': 'Cohesion',
720
- 'clarity': 'Clarity',
721
- 'general': 'General'
722
- },
723
- 'ui_text': {
724
- 'priority_intro': "This is where you should focus your efforts.",
725
- 'detailed_recommendations': "Detailed recommendations",
726
- 'save_button': "Save analysis",
727
- 'save_success': "Analysis saved successfully",
728
- 'save_error': "Error saving analysis",
729
- 'area_priority': "Priority area"
730
- }
731
- }
732
- elif lang_code == 'fr':
733
- return {
734
- 'vocabulary': ["Essayez d'enrichir votre vocabulaire"],
735
- 'structure': ["Travaillez sur la structure de vos phrases"],
736
- 'cohesion': ["Améliorez la connexion entre vos idées"],
737
- 'clarity': ["Essayez d'exprimer vos idées plus clairement"],
738
- 'specific': ["Adaptez votre texte en fonction de son objectif"],
739
- 'priority': {
740
- 'area': 'general',
741
- 'tips': ["Demandez des commentaires spécifiques à un tuteur ou un professeur"]
742
- },
743
- 'dimension_names': {
744
- 'vocabulary': 'Vocabulaire',
745
- 'structure': 'Structure',
746
- 'cohesion': 'Cohésion',
747
- 'clarity': 'Clarté',
748
- 'general': 'Général'
749
- },
750
- 'ui_text': {
751
- 'priority_intro': "C'est là que vous devriez concentrer vos efforts.",
752
- 'detailed_recommendations': "Recommandations détaillées",
753
- 'save_button': "Enregistrer l'analyse",
754
- 'save_success': "Analyse enregistrée avec succès",
755
- 'save_error': "Erreur lors de l'enregistrement de l'analyse",
756
- 'area_priority': "Domaine prioritaire"
757
- }
758
- }
759
- elif lang_code == 'pt':
760
- return {
761
- 'vocabulary': ["Tente enriquecer seu vocabulário"],
762
- 'structure': ["Trabalhe na estrutura de suas frases"],
763
- 'cohesion': ["Melhore a conexão entre suas ideias"],
764
- 'clarity': ["Tente expressar suas ideias com mais clareza"],
765
- 'specific': ["Adapte seu texto de acordo com seu propósito"],
766
- 'priority': {
767
- 'area': 'general',
768
- 'tips': ["Busque feedback específico de um tutor ou professor"]
769
- },
770
- 'dimension_names': {
771
- 'vocabulary': 'Vocabulário',
772
- 'structure': 'Estrutura',
773
- 'cohesion': 'Coesão',
774
- 'clarity': 'Clareza',
775
- 'general': 'Geral'
776
- },
777
- 'ui_text': {
778
- 'priority_intro': "É aqui que você deve concentrar seus esforços.",
779
- 'detailed_recommendations': "Recomendações detalhadas",
780
- 'save_button': "Salvar análise",
781
- 'save_success': "Análise salva com sucesso",
782
- 'save_error': "Erro ao salvar análise",
783
- 'area_priority': "Área prioritária"
784
- }
785
- }
786
- else: # Español por defecto
787
- return {
788
- 'vocabulary': ["Intenta enriquecer tu vocabulario"],
789
- 'structure': ["Trabaja en la estructura de tus oraciones"],
790
- 'cohesion': ["Mejora la conexión entre tus ideas"],
791
- 'clarity': ["Busca expresar tus ideas con mayor claridad"],
792
- 'specific': ["Adapta tu texto según su propósito"],
793
- 'priority': {
794
- 'area': 'general',
795
- 'tips': ["Busca retroalimentación específica de un tutor o profesor"]
796
- },
797
- 'dimension_names': {
798
- 'vocabulary': 'Vocabulario',
799
- 'structure': 'Estructura',
800
- 'cohesion': 'Cohesión',
801
- 'clarity': 'Claridad',
802
- 'general': 'General'
803
- },
804
- 'ui_text': {
805
- 'priority_intro': "Esta es el área donde debes concentrar tus esfuerzos.",
806
- 'detailed_recommendations': "Recomendaciones detalladas",
807
- 'save_button': "Guardar análisis",
808
- 'save_success': "Análisis guardado con éxito",
809
- 'save_error': "Error al guardar el análisis",
810
- 'area_priority': "Área prioritaria"
811
- }
812
- }
813
-
814
-
815
- #########################################################################
816
- #########################################################################
817
- # Funciones de generación de gráficos
818
- def generate_sentence_graphs(doc):
819
- """Genera visualizaciones de estructura de oraciones"""
820
- fig, ax = plt.subplots(figsize=(10, 6))
821
- # Implementar visualización
822
- plt.close()
823
- return fig
824
-
825
- def generate_word_connections(doc):
826
- """Genera red de conexiones de palabras"""
827
- fig, ax = plt.subplots(figsize=(10, 6))
828
- # Implementar visualización
829
- plt.close()
830
- return fig
831
-
832
- def generate_connection_paths(doc):
833
- """Genera patrones de conexión"""
834
- fig, ax = plt.subplots(figsize=(10, 6))
835
- # Implementar visualización
836
- plt.close()
837
- return fig
838
-
839
- def create_vocabulary_network(doc):
840
- """
841
- Genera el grafo de red de vocabulario.
842
- """
843
- G = nx.Graph()
844
-
845
- # Crear nodos para palabras significativas
846
- words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
847
- word_freq = Counter(words)
848
-
849
- # Añadir nodos con tamaño basado en frecuencia
850
- for word, freq in word_freq.items():
851
- G.add_node(word, size=freq)
852
-
853
- # Crear conexiones basadas en co-ocurrencia
854
- window_size = 5
855
- for i in range(len(words) - window_size):
856
- window = words[i:i+window_size]
857
- for w1, w2 in combinations(set(window), 2):
858
- if G.has_edge(w1, w2):
859
- G[w1][w2]['weight'] += 1
860
- else:
861
- G.add_edge(w1, w2, weight=1)
862
-
863
- # Crear visualización
864
- fig, ax = plt.subplots(figsize=(12, 8))
865
- pos = nx.spring_layout(G)
866
-
867
- # Dibujar nodos
868
- nx.draw_networkx_nodes(G, pos,
869
- node_size=[G.nodes[node]['size']*100 for node in G.nodes],
870
- node_color='lightblue',
871
- alpha=0.7)
872
-
873
- # Dibujar conexiones
874
- nx.draw_networkx_edges(G, pos,
875
- width=[G[u][v]['weight']*0.5 for u,v in G.edges],
876
- alpha=0.5)
877
-
878
- # Añadir etiquetas
879
- nx.draw_networkx_labels(G, pos)
880
-
881
- plt.title("Red de Vocabulario")
882
- plt.axis('off')
883
- return fig
884
-
885
- def create_syntax_complexity_graph(doc):
886
- """
887
- Genera el diagrama de arco de complejidad sintáctica.
888
- Muestra la estructura de dependencias con colores basados en la complejidad.
889
- """
890
- try:
891
- # Preparar datos para la visualización
892
- sentences = list(doc.sents)
893
- if not sentences:
894
- return None
895
-
896
- # Crear figura para el gráfico
897
- fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
898
-
899
- # Colores para diferentes niveles de profundidad
900
- depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
901
-
902
- y_offset = 0
903
- max_x = 0
904
-
905
- for sent in sentences:
906
- words = [token.text for token in sent]
907
- x_positions = range(len(words))
908
- max_x = max(max_x, len(words))
909
-
910
- # Dibujar palabras
911
- plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
912
- plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
913
-
914
- # Añadir texto
915
- for i, word in enumerate(words):
916
- plt.annotate(word, (i, y_offset), xytext=(0, -10),
917
- textcoords='offset points', ha='center')
918
-
919
- # Dibujar arcos de dependencia
920
- for token in sent:
921
- if token.dep_ != "ROOT":
922
- # Calcular profundidad de dependencia
923
- depth = 0
924
- current = token
925
- while current.head != current:
926
- depth += 1
927
- current = current.head
928
-
929
- # Determinar posiciones para el arco
930
- start = token.i - sent[0].i
931
- end = token.head.i - sent[0].i
932
-
933
- # Altura del arco basada en la distancia entre palabras
934
- height = 0.5 * abs(end - start)
935
-
936
- # Color basado en la profundidad
937
- color = depth_colors[min(depth, len(depth_colors)-1)]
938
-
939
- # Crear arco
940
- arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
941
- width=abs(end - start),
942
- height=height,
943
- angle=0,
944
- theta1=0,
945
- theta2=180,
946
- color=color,
947
- alpha=0.6)
948
- ax.add_patch(arc)
949
-
950
- y_offset -= 2
951
-
952
- # Configurar el gráfico
953
- plt.xlim(-1, max_x)
954
- plt.ylim(y_offset - 1, 1)
955
- plt.axis('off')
956
- plt.title("Complejidad Sintáctica")
957
-
958
- return fig
959
-
960
- except Exception as e:
961
- logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
962
- return None
963
-
964
-
965
- def create_cohesion_heatmap(doc):
966
- """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
967
- try:
968
- sentences = list(doc.sents)
969
- n_sentences = len(sentences)
970
-
971
- if n_sentences < 2:
972
- return None
973
-
974
- similarity_matrix = np.zeros((n_sentences, n_sentences))
975
-
976
- for i in range(n_sentences):
977
- for j in range(n_sentences):
978
- sent1_lemmas = {token.lemma_ for token in sentences[i]
979
- if token.is_alpha and not token.is_stop}
980
- sent2_lemmas = {token.lemma_ for token in sentences[j]
981
- if token.is_alpha and not token.is_stop}
982
-
983
- if sent1_lemmas and sent2_lemmas:
984
- intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
985
- union = len(sent1_lemmas | sent2_lemmas) # Y aquí
986
- similarity_matrix[i, j] = intersection / union if union > 0 else 0
987
-
988
- # Crear visualización
989
- fig, ax = plt.subplots(figsize=(10, 8))
990
-
991
- sns.heatmap(similarity_matrix,
992
- cmap='YlOrRd',
993
- square=True,
994
- xticklabels=False,
995
- yticklabels=False,
996
- cbar_kws={'label': 'Cohesión'},
997
- ax=ax)
998
-
999
- plt.title("Mapa de Cohesión Textual")
1000
- plt.xlabel("Oraciones")
1001
- plt.ylabel("Oraciones")
1002
-
1003
- plt.tight_layout()
1004
- return fig
1005
-
1006
- except Exception as e:
1007
- logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
1008
- return None
 
 
1
+ #v3/modules/studentact/current_situation_analysis.py
2
+
3
+ import streamlit as st
4
+ import matplotlib.pyplot as plt
5
+ import networkx as nx
6
+ import seaborn as sns
7
+ from collections import Counter
8
+ from itertools import combinations
9
+ import numpy as np
10
+ import matplotlib.patches as patches
11
+ import logging
12
+
13
+
14
+ # 2. Configuración básica del logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
18
+ handlers=[
19
+ logging.StreamHandler(),
20
+ logging.FileHandler('app.log')
21
+ ]
22
+ )
23
+
24
+ # 3. Obtener el logger específico para este módulo
25
+ logger = logging.getLogger(__name__)
26
+
27
+ #########################################################################
28
+
29
+ def correlate_metrics(scores):
30
+ """
31
+ Ajusta los scores para mantener correlaciones lógicas entre métricas.
32
+
33
+ Args:
34
+ scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
35
+
36
+ Returns:
37
+ dict con scores ajustados
38
+ """
39
+ try:
40
+ # 1. Correlación estructura-cohesión
41
+ # La cohesión no puede ser menor que estructura * 0.7
42
+ min_cohesion = scores['structure']['normalized_score'] * 0.7
43
+ if scores['cohesion']['normalized_score'] < min_cohesion:
44
+ scores['cohesion']['normalized_score'] = min_cohesion
45
+
46
+ # 2. Correlación vocabulario-cohesión
47
+ # La cohesión léxica depende del vocabulario
48
+ vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
49
+ scores['cohesion']['normalized_score'] = max(
50
+ scores['cohesion']['normalized_score'],
51
+ vocab_influence
52
+ )
53
+
54
+ # 3. Correlación cohesión-claridad
55
+ # La claridad no puede superar cohesión * 1.2
56
+ max_clarity = scores['cohesion']['normalized_score'] * 1.2
57
+ if scores['clarity']['normalized_score'] > max_clarity:
58
+ scores['clarity']['normalized_score'] = max_clarity
59
+
60
+ # 4. Correlación estructura-claridad
61
+ # La claridad no puede superar estructura * 1.1
62
+ struct_max_clarity = scores['structure']['normalized_score'] * 1.1
63
+ scores['clarity']['normalized_score'] = min(
64
+ scores['clarity']['normalized_score'],
65
+ struct_max_clarity
66
+ )
67
+
68
+ # Normalizar todos los scores entre 0 y 1
69
+ for metric in scores:
70
+ scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
71
+
72
+ return scores
73
+
74
+ except Exception as e:
75
+ logger.error(f"Error en correlate_metrics: {str(e)}")
76
+ return scores
77
+
78
+ ##########################################################################
79
+
80
+ def analyze_text_dimensions(doc):
81
+ """
82
+ Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
83
+ """
84
+ try:
85
+ # Obtener scores iniciales
86
+ vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
87
+ struct_score = analyze_structure(doc)
88
+ cohesion_score = analyze_cohesion(doc)
89
+ clarity_score, clarity_details = analyze_clarity(doc)
90
+
91
+ # Crear diccionario de scores inicial
92
+ scores = {
93
+ 'vocabulary': {
94
+ 'normalized_score': vocab_score,
95
+ 'details': vocab_details
96
+ },
97
+ 'structure': {
98
+ 'normalized_score': struct_score,
99
+ 'details': None
100
+ },
101
+ 'cohesion': {
102
+ 'normalized_score': cohesion_score,
103
+ 'details': None
104
+ },
105
+ 'clarity': {
106
+ 'normalized_score': clarity_score,
107
+ 'details': clarity_details
108
+ }
109
+ }
110
+
111
+ # Ajustar correlaciones entre métricas
112
+ adjusted_scores = correlate_metrics(scores)
113
+
114
+ # Logging para diagnóstico
115
+ logger.info(f"""
116
+ Scores originales vs ajustados:
117
+ Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
118
+ Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
119
+ Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
120
+ Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
121
+ """)
122
+
123
+ return adjusted_scores
124
+
125
+ except Exception as e:
126
+ logger.error(f"Error en analyze_text_dimensions: {str(e)}")
127
+ return {
128
+ 'vocabulary': {'normalized_score': 0.0, 'details': {}},
129
+ 'structure': {'normalized_score': 0.0, 'details': {}},
130
+ 'cohesion': {'normalized_score': 0.0, 'details': {}},
131
+ 'clarity': {'normalized_score': 0.0, 'details': {}}
132
+ }
133
+
134
+
135
+
136
+ #############################################################################################
137
+
138
+ def analyze_clarity(doc):
139
+ """
140
+ Analiza la claridad del texto considerando múltiples factores.
141
+ """
142
+ try:
143
+ sentences = list(doc.sents)
144
+ if not sentences:
145
+ return 0.0, {}
146
+
147
+ # 1. Longitud de oraciones
148
+ sentence_lengths = [len(sent) for sent in sentences]
149
+ avg_length = sum(sentence_lengths) / len(sentences)
150
+
151
+ # Normalizar usando los umbrales definidos para clarity
152
+ length_score = normalize_score(
153
+ value=avg_length,
154
+ metric_type='clarity',
155
+ optimal_length=20, # Una oración ideal tiene ~20 palabras
156
+ min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
157
+ target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
158
+ )
159
+
160
+ # 2. Análisis de conectores
161
+ connector_count = 0
162
+ connector_weights = {
163
+ 'CCONJ': 1.0, # Coordinantes
164
+ 'SCONJ': 1.2, # Subordinantes
165
+ 'ADV': 0.8 # Adverbios conectivos
166
+ }
167
+
168
+ for token in doc:
169
+ if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
170
+ connector_count += connector_weights[token.pos_]
171
+
172
+ # Normalizar conectores por oración
173
+ connectors_per_sentence = connector_count / len(sentences) if sentences else 0
174
+ connector_score = normalize_score(
175
+ value=connectors_per_sentence,
176
+ metric_type='clarity',
177
+ optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
178
+ min_threshold=0.60,
179
+ target_threshold=0.75
180
+ )
181
+
182
+ # 3. Complejidad estructural
183
+ clause_count = 0
184
+ for sent in sentences:
185
+ verbs = [token for token in sent if token.pos_ == 'VERB']
186
+ clause_count += len(verbs)
187
+
188
+ complexity_raw = clause_count / len(sentences) if sentences else 0
189
+ complexity_score = normalize_score(
190
+ value=complexity_raw,
191
+ metric_type='clarity',
192
+ optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
193
+ min_threshold=0.60,
194
+ target_threshold=0.75
195
+ )
196
+
197
+ # 4. Densidad léxica
198
+ content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
199
+ total_words = len([token for token in doc if token.is_alpha])
200
+ density = content_words / total_words if total_words > 0 else 0
201
+
202
+ density_score = normalize_score(
203
+ value=density,
204
+ metric_type='clarity',
205
+ optimal_connections=0.6, # 60% de palabras de contenido es óptimo
206
+ min_threshold=0.60,
207
+ target_threshold=0.75
208
+ )
209
+
210
+ # Score final ponderado
211
+ weights = {
212
+ 'length': 0.3,
213
+ 'connectors': 0.3,
214
+ 'complexity': 0.2,
215
+ 'density': 0.2
216
+ }
217
+
218
+ clarity_score = (
219
+ weights['length'] * length_score +
220
+ weights['connectors'] * connector_score +
221
+ weights['complexity'] * complexity_score +
222
+ weights['density'] * density_score
223
+ )
224
+
225
+ details = {
226
+ 'length_score': length_score,
227
+ 'connector_score': connector_score,
228
+ 'complexity_score': complexity_score,
229
+ 'density_score': density_score,
230
+ 'avg_sentence_length': avg_length,
231
+ 'connectors_per_sentence': connectors_per_sentence,
232
+ 'density': density
233
+ }
234
+
235
+ # Agregar logging para diagnóstico
236
+ logger.info(f"""
237
+ Scores de Claridad:
238
+ - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
239
+ - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
240
+ - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
241
+ - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
242
+ - Score Final: {clarity_score:.2f}
243
+ """)
244
+
245
+ return clarity_score, details
246
+
247
+ except Exception as e:
248
+ logger.error(f"Error en analyze_clarity: {str(e)}")
249
+ return 0.0, {}
250
+
251
+ #########################################################################
252
+ def analyze_vocabulary_diversity(doc):
253
+ """Análisis mejorado de la diversidad y calidad del vocabulario"""
254
+ try:
255
+ # 1. Análisis básico de diversidad
256
+ unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
257
+ total_words = len([token for token in doc if token.is_alpha])
258
+ basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
259
+
260
+ # 2. Análisis de registro
261
+ academic_words = 0
262
+ narrative_words = 0
263
+ technical_terms = 0
264
+
265
+ # Clasificar palabras por registro
266
+ for token in doc:
267
+ if token.is_alpha:
268
+ # Detectar términos académicos/técnicos
269
+ if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
270
+ if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
271
+ technical_terms += 1
272
+ # Detectar palabras narrativas
273
+ if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
274
+ narrative_words += 1
275
+
276
+ # 3. Análisis de complejidad sintáctica
277
+ avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
278
+
279
+ # 4. Calcular score ponderado
280
+ weights = {
281
+ 'diversity': 0.3,
282
+ 'technical': 0.3,
283
+ 'narrative': 0.2,
284
+ 'complexity': 0.2
285
+ }
286
+
287
+ scores = {
288
+ 'diversity': basic_diversity,
289
+ 'technical': technical_terms / total_words if total_words > 0 else 0,
290
+ 'narrative': narrative_words / total_words if total_words > 0 else 0,
291
+ 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
292
+ }
293
+
294
+ # Score final ponderado
295
+ final_score = sum(weights[key] * scores[key] for key in weights)
296
+
297
+ # Información adicional para diagnóstico
298
+ details = {
299
+ 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
300
+ 'scores': scores
301
+ }
302
+
303
+ return final_score, details
304
+
305
+ except Exception as e:
306
+ logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
307
+ return 0.0, {}
308
+
309
+ #########################################################################
310
+ def analyze_cohesion(doc):
311
+ """Analiza la cohesión textual"""
312
+ try:
313
+ sentences = list(doc.sents)
314
+ if len(sentences) < 2:
315
+ logger.warning("Texto demasiado corto para análisis de cohesión")
316
+ return 0.0
317
+
318
+ # 1. Análisis de conexiones léxicas
319
+ lexical_connections = 0
320
+ total_possible_connections = 0
321
+
322
+ for i in range(len(sentences)-1):
323
+ # Obtener lemmas significativos (no stopwords)
324
+ sent1_words = {token.lemma_ for token in sentences[i]
325
+ if token.is_alpha and not token.is_stop}
326
+ sent2_words = {token.lemma_ for token in sentences[i+1]
327
+ if token.is_alpha and not token.is_stop}
328
+
329
+ if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
330
+ intersection = len(sent1_words.intersection(sent2_words))
331
+ total_possible = min(len(sent1_words), len(sent2_words))
332
+
333
+ if total_possible > 0:
334
+ lexical_score = intersection / total_possible
335
+ lexical_connections += lexical_score
336
+ total_possible_connections += 1
337
+
338
+ # 2. Análisis de conectores
339
+ connector_count = 0
340
+ connector_types = {
341
+ 'CCONJ': 1.0, # Coordinantes
342
+ 'SCONJ': 1.2, # Subordinantes
343
+ 'ADV': 0.8 # Adverbios conectivos
344
+ }
345
+
346
+ for token in doc:
347
+ if (token.pos_ in connector_types and
348
+ token.dep_ in ['cc', 'mark', 'advmod'] and
349
+ not token.is_stop):
350
+ connector_count += connector_types[token.pos_]
351
+
352
+ # 3. Cálculo de scores normalizados
353
+ if total_possible_connections > 0:
354
+ lexical_cohesion = lexical_connections / total_possible_connections
355
+ else:
356
+ lexical_cohesion = 0
357
+
358
+ if len(sentences) > 1:
359
+ connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
360
+ else:
361
+ connector_cohesion = 0
362
+
363
+ # 4. Score final ponderado
364
+ weights = {
365
+ 'lexical': 0.7,
366
+ 'connectors': 0.3
367
+ }
368
+
369
+ cohesion_score = (
370
+ weights['lexical'] * lexical_cohesion +
371
+ weights['connectors'] * connector_cohesion
372
+ )
373
+
374
+ # 5. Logging para diagnóstico
375
+ logger.info(f"""
376
+ Análisis de Cohesión:
377
+ - Conexiones léxicas encontradas: {lexical_connections}
378
+ - Conexiones posibles: {total_possible_connections}
379
+ - Lexical cohesion score: {lexical_cohesion}
380
+ - Conectores encontrados: {connector_count}
381
+ - Connector cohesion score: {connector_cohesion}
382
+ - Score final: {cohesion_score}
383
+ """)
384
+
385
+ return cohesion_score
386
+
387
+ except Exception as e:
388
+ logger.error(f"Error en analyze_cohesion: {str(e)}")
389
+ return 0.0
390
+
391
+ #########################################################################
392
+ def analyze_structure(doc):
393
+ try:
394
+ if len(doc) == 0:
395
+ return 0.0
396
+
397
+ structure_scores = []
398
+ for token in doc:
399
+ if token.dep_ == 'ROOT':
400
+ result = get_dependency_depths(token)
401
+ structure_scores.append(result['final_score'])
402
+
403
+ if not structure_scores:
404
+ return 0.0
405
+
406
+ return min(1.0, sum(structure_scores) / len(structure_scores))
407
+
408
+ except Exception as e:
409
+ logger.error(f"Error en analyze_structure: {str(e)}")
410
+ return 0.0
411
+
412
+ #########################################################################
413
+ # Funciones auxiliares de análisis
414
+ def get_dependency_depths(token, depth=0, analyzed_tokens=None):
415
+ """
416
+ Analiza la profundidad y calidad de las relaciones de dependencia.
417
+
418
+ Args:
419
+ token: Token a analizar
420
+ depth: Profundidad actual en el árbol
421
+ analyzed_tokens: Set para evitar ciclos en el análisis
422
+
423
+ Returns:
424
+ dict: Información detallada sobre las dependencias
425
+ - depths: Lista de profundidades
426
+ - relations: Diccionario con tipos de relaciones encontradas
427
+ - complexity_score: Puntuación de complejidad
428
+ """
429
+ if analyzed_tokens is None:
430
+ analyzed_tokens = set()
431
+
432
+ # Evitar ciclos
433
+ if token.i in analyzed_tokens:
434
+ return {
435
+ 'depths': [],
436
+ 'relations': {},
437
+ 'complexity_score': 0
438
+ }
439
+
440
+ analyzed_tokens.add(token.i)
441
+
442
+ # Pesos para diferentes tipos de dependencias
443
+ dependency_weights = {
444
+ # Dependencias principales
445
+ 'nsubj': 1.2, # Sujeto nominal
446
+ 'obj': 1.1, # Objeto directo
447
+ 'iobj': 1.1, # Objeto indirecto
448
+ 'ROOT': 1.3, # Raíz
449
+
450
+ # Modificadores
451
+ 'amod': 0.8, # Modificador adjetival
452
+ 'advmod': 0.8, # Modificador adverbial
453
+ 'nmod': 0.9, # Modificador nominal
454
+
455
+ # Estructuras complejas
456
+ 'csubj': 1.4, # Cláusula como sujeto
457
+ 'ccomp': 1.3, # Complemento clausal
458
+ 'xcomp': 1.2, # Complemento clausal abierto
459
+ 'advcl': 1.2, # Cláusula adverbial
460
+
461
+ # Coordinación y subordinación
462
+ 'conj': 1.1, # Conjunción
463
+ 'cc': 0.7, # Coordinación
464
+ 'mark': 0.8, # Marcador
465
+
466
+ # Otros
467
+ 'det': 0.5, # Determinante
468
+ 'case': 0.5, # Caso
469
+ 'punct': 0.1 # Puntuación
470
+ }
471
+
472
+ # Inicializar resultados
473
+ current_result = {
474
+ 'depths': [depth],
475
+ 'relations': {token.dep_: 1},
476
+ 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
477
+ }
478
+
479
+ # Analizar hijos recursivamente
480
+ for child in token.children:
481
+ child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
482
+
483
+ # Combinar profundidades
484
+ current_result['depths'].extend(child_result['depths'])
485
+
486
+ # Combinar relaciones
487
+ for rel, count in child_result['relations'].items():
488
+ current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
489
+
490
+ # Acumular score de complejidad
491
+ current_result['complexity_score'] += child_result['complexity_score']
492
+
493
+ # Calcular métricas adicionales
494
+ current_result['max_depth'] = max(current_result['depths'])
495
+ current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
496
+ current_result['relation_diversity'] = len(current_result['relations'])
497
+
498
+ # Calcular score ponderado por tipo de estructura
499
+ structure_bonus = 0
500
+
501
+ # Bonus por estructuras complejas
502
+ if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
503
+ structure_bonus += 0.3
504
+
505
+ # Bonus por coordinación balanceada
506
+ if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
507
+ structure_bonus += 0.2
508
+
509
+ # Bonus por modificación rica
510
+ if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
511
+ structure_bonus += 0.2
512
+
513
+ current_result['final_score'] = (
514
+ current_result['complexity_score'] * (1 + structure_bonus)
515
+ )
516
+
517
+ return current_result
518
+
519
+ #########################################################################
520
+ def normalize_score(value, metric_type,
521
+ min_threshold=0.0, target_threshold=1.0,
522
+ range_factor=2.0, optimal_length=None,
523
+ optimal_connections=None, optimal_depth=None):
524
+ """
525
+ Normaliza un valor considerando umbrales específicos por tipo de métrica.
526
+
527
+ Args:
528
+ value: Valor a normalizar
529
+ metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
530
+ min_threshold: Valor mínimo aceptable
531
+ target_threshold: Valor objetivo
532
+ range_factor: Factor para ajustar el rango
533
+ optimal_length: Longitud óptima (opcional)
534
+ optimal_connections: Número óptimo de conexiones (opcional)
535
+ optimal_depth: Profundidad óptima de estructura (opcional)
536
+
537
+ Returns:
538
+ float: Valor normalizado entre 0 y 1
539
+ """
540
+ try:
541
+ # Definir umbrales por tipo de métrica
542
+ METRIC_THRESHOLDS = {
543
+ 'vocabulary': {
544
+ 'min': 0.60,
545
+ 'target': 0.75,
546
+ 'range_factor': 1.5
547
+ },
548
+ 'structure': {
549
+ 'min': 0.65,
550
+ 'target': 0.80,
551
+ 'range_factor': 1.8
552
+ },
553
+ 'cohesion': {
554
+ 'min': 0.55,
555
+ 'target': 0.70,
556
+ 'range_factor': 1.6
557
+ },
558
+ 'clarity': {
559
+ 'min': 0.60,
560
+ 'target': 0.75,
561
+ 'range_factor': 1.7
562
+ }
563
+ }
564
+
565
+ # Validar valores negativos o cero
566
+ if value < 0:
567
+ logger.warning(f"Valor negativo recibido: {value}")
568
+ return 0.0
569
+
570
+ # Manejar caso donde el valor es cero
571
+ if value == 0:
572
+ logger.warning("Valor cero recibido")
573
+ return 0.0
574
+
575
+ # Obtener umbrales específicos para el tipo de métrica
576
+ thresholds = METRIC_THRESHOLDS.get(metric_type, {
577
+ 'min': min_threshold,
578
+ 'target': target_threshold,
579
+ 'range_factor': range_factor
580
+ })
581
+
582
+ # Identificar el valor de referencia a usar
583
+ if optimal_depth is not None:
584
+ reference = optimal_depth
585
+ elif optimal_connections is not None:
586
+ reference = optimal_connections
587
+ elif optimal_length is not None:
588
+ reference = optimal_length
589
+ else:
590
+ reference = thresholds['target']
591
+
592
+ # Validar valor de referencia
593
+ if reference <= 0:
594
+ logger.warning(f"Valor de referencia inválido: {reference}")
595
+ return 0.0
596
+
597
+ # Calcular score basado en umbrales
598
+ if value < thresholds['min']:
599
+ # Valor por debajo del mínimo
600
+ score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
601
+ elif value < thresholds['target']:
602
+ # Valor entre mínimo y objetivo
603
+ range_size = thresholds['target'] - thresholds['min']
604
+ progress = (value - thresholds['min']) / range_size
605
+ score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
606
+ else:
607
+ # Valor alcanza o supera el objetivo
608
+ score = 1.0
609
+
610
+ # Penalizar valores muy por encima del objetivo
611
+ if value > (thresholds['target'] * thresholds['range_factor']):
612
+ excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
613
+ score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
614
+
615
+ # Asegurar que el resultado esté entre 0 y 1
616
+ return max(0.0, min(1.0, score))
617
+
618
+ except Exception as e:
619
+ logger.error(f"Error en normalize_score: {str(e)}")
620
+ return 0.0
621
+
622
+ #########################################################################
623
+ #########################################################################
624
+
625
+ def generate_recommendations(metrics, text_type, lang_code='es'):
626
+ """
627
+ Genera recomendaciones personalizadas basadas en las métricas del texto y el tipo de texto.
628
+
629
+ Args:
630
+ metrics: Diccionario con las métricas analizadas
631
+ text_type: Tipo de texto ('academic_article', 'student_essay', 'general_communication')
632
+ lang_code: Código del idioma para las recomendaciones (es, en, uk)
633
+
634
+ Returns:
635
+ dict: Recomendaciones organizadas por categoría en el idioma correspondiente
636
+ """
637
+ try:
638
+ # Añadir debug log para verificar el código de idioma recibido
639
+ logger.info(f"generate_recommendations llamado con idioma: {lang_code}")
640
+
641
+ # Comprobar que importamos RECOMMENDATIONS correctamente
642
+ logger.info(f"Idiomas disponibles en RECOMMENDATIONS: {list(RECOMMENDATIONS.keys())}")
643
+
644
+ # Obtener umbrales según el tipo de texto
645
+ thresholds = TEXT_TYPES[text_type]['thresholds']
646
+
647
+ # Verificar que el idioma esté soportado, usar español como respaldo
648
+ if lang_code not in RECOMMENDATIONS:
649
+ logger.warning(f"Idioma {lang_code} no soportado para recomendaciones, usando español")
650
+ lang_code = 'es'
651
+
652
+ # Obtener traducciones para el idioma seleccionado
653
+ translations = RECOMMENDATIONS[lang_code]
654
+
655
+ # Inicializar diccionario de recomendaciones
656
+ recommendations = {
657
+ 'vocabulary': [],
658
+ 'structure': [],
659
+ 'cohesion': [],
660
+ 'clarity': [],
661
+ 'specific': [],
662
+ 'priority': {
663
+ 'area': 'general',
664
+ 'tips': []
665
+ },
666
+ 'text_type_name': translations['text_types'][text_type],
667
+ 'dimension_names': translations['dimension_names'],
668
+ 'ui_text': {
669
+ 'priority_intro': translations['priority_intro'],
670
+ 'detailed_recommendations': translations['detailed_recommendations'],
671
+ 'save_button': translations['save_button'],
672
+ 'save_success': translations['save_success'],
673
+ 'save_error': translations['save_error'],
674
+ 'area_priority': translations['area_priority']
675
+ }
676
+ }
677
+
678
+ # Determinar nivel para cada dimensión y asignar recomendaciones
679
+ dimensions = ['vocabulary', 'structure', 'cohesion', 'clarity']
680
+ scores = {}
681
+
682
+ for dim in dimensions:
683
+ score = metrics[dim]['normalized_score']
684
+ scores[dim] = score
685
+
686
+ # Determinar nivel (bajo, medio, alto)
687
+ if score < thresholds[dim]['min']:
688
+ level = 'low'
689
+ elif score < thresholds[dim]['target']:
690
+ level = 'medium'
691
+ else:
692
+ level = 'high'
693
+
694
+ # Asignar recomendaciones para ese nivel
695
+ recommendations[dim] = translations[dim][level]
696
+
697
+ # Asignar recomendaciones específicas por tipo de texto
698
+ recommendations['specific'] = translations[text_type]
699
+
700
+ # Determinar área prioritaria (la que tiene menor puntuación)
701
+ priority_dimension = min(scores, key=scores.get)
702
+ recommendations['priority']['area'] = priority_dimension
703
+ recommendations['priority']['tips'] = recommendations[priority_dimension]
704
+
705
+ logger.info(f"Generadas recomendaciones en {lang_code} para texto tipo {text_type}")
706
+ return recommendations
707
+
708
+ except Exception as e:
709
+ logger.error(f"Error en generate_recommendations: {str(e)}")
710
+
711
+ # Utilizar un enfoque basado en el idioma actual en lugar de casos codificados
712
+ # Esto permite manejar ucraniano y cualquier otro idioma futuro
713
+ fallback_translations = {
714
+ 'en': {
715
+ 'basic_recommendations': {
716
+ 'vocabulary': ["Try enriching your vocabulary"],
717
+ 'structure': ["Work on the structure of your sentences"],
718
+ 'cohesion': ["Improve the connection between your ideas"],
719
+ 'clarity': ["Try to express your ideas more clearly"],
720
+ 'specific': ["Adapt your text according to its purpose"],
721
+ },
722
+ 'dimension_names': {
723
+ 'vocabulary': 'Vocabulary',
724
+ 'structure': 'Structure',
725
+ 'cohesion': 'Cohesion',
726
+ 'clarity': 'Clarity',
727
+ 'general': 'General'
728
+ },
729
+ 'ui_text': {
730
+ 'priority_intro': "This is where you should focus your efforts.",
731
+ 'detailed_recommendations': "Detailed recommendations",
732
+ 'save_button': "Save analysis",
733
+ 'save_success': "Analysis saved successfully",
734
+ 'save_error': "Error saving analysis",
735
+ 'area_priority': "Priority area"
736
+ }
737
+ },
738
+ 'uk': {
739
+ 'basic_recommendations': {
740
+ 'vocabulary': ["Розширте свій словниковий запас"],
741
+ 'structure': ["Покращіть структуру ваших речень"],
742
+ 'cohesion': ["Покращіть зв'язок між вашими ідеями"],
743
+ 'clarity': ["Висловлюйте свої ідеї ясніше"],
744
+ 'specific': ["Адаптуйте свій текст відповідно до його мети"],
745
+ },
746
+ 'dimension_names': {
747
+ 'vocabulary': 'Словниковий запас',
748
+ 'structure': 'Структура',
749
+ 'cohesion': 'Зв\'язність',
750
+ 'clarity': 'Ясність',
751
+ 'general': 'Загальне'
752
+ },
753
+ 'ui_text': {
754
+ 'priority_intro': "Це область, де ви повинні зосередити свої зусилля.",
755
+ 'detailed_recommendations': "Детальні рекомендації",
756
+ 'save_button': "Зберегти аналіз",
757
+ 'save_success': "Аналіз успішно збережено",
758
+ 'save_error': "Помилка при збереженні аналізу",
759
+ 'area_priority': "Пріоритетна область"
760
+ }
761
+ },
762
+ 'es': {
763
+ 'basic_recommendations': {
764
+ 'vocabulary': ["Intenta enriquecer tu vocabulario"],
765
+ 'structure': ["Trabaja en la estructura de tus oraciones"],
766
+ 'cohesion': ["Mejora la conexión entre tus ideas"],
767
+ 'clarity': ["Busca expresar tus ideas con mayor claridad"],
768
+ 'specific': ["Adapta tu texto según su propósito"],
769
+ },
770
+ 'dimension_names': {
771
+ 'vocabulary': 'Vocabulario',
772
+ 'structure': 'Estructura',
773
+ 'cohesion': 'Cohesión',
774
+ 'clarity': 'Claridad',
775
+ 'general': 'General'
776
+ },
777
+ 'ui_text': {
778
+ 'priority_intro': "Esta es el área donde debes concentrar tus esfuerzos.",
779
+ 'detailed_recommendations': "Recomendaciones detalladas",
780
+ 'save_button': "Guardar análisis",
781
+ 'save_success': "Análisis guardado con éxito",
782
+ 'save_error': "Error al guardar el análisis",
783
+ 'area_priority': "Área prioritaria"
784
+ }
785
+ }
786
+ }
787
+
788
+ # Usar el idioma actual si está disponible, o inglés, o español como última opción
789
+ current_lang = fallback_translations.get(lang_code,
790
+ fallback_translations.get('en',
791
+ fallback_translations['es']))
792
+
793
+ basic_recommendations = current_lang['basic_recommendations']
794
+
795
+ return {
796
+ 'vocabulary': basic_recommendations['vocabulary'],
797
+ 'structure': basic_recommendations['structure'],
798
+ 'cohesion': basic_recommendations['cohesion'],
799
+ 'clarity': basic_recommendations['clarity'],
800
+ 'specific': basic_recommendations['specific'],
801
+ 'priority': {
802
+ 'area': 'general',
803
+ 'tips': ["Busca retroalimentación específica de un tutor o profesor"]
804
+ },
805
+ 'dimension_names': current_lang['dimension_names'],
806
+ 'ui_text': current_lang['ui_text']
807
+ }
808
+
809
+
810
+
811
+
812
+ #########################################################################
813
+ #########################################################################
814
+ # Funciones de generación de gráficos
815
+ def generate_sentence_graphs(doc):
816
+ """Genera visualizaciones de estructura de oraciones"""
817
+ fig, ax = plt.subplots(figsize=(10, 6))
818
+ # Implementar visualización
819
+ plt.close()
820
+ return fig
821
+
822
+ ############################################################################
823
+ def generate_word_connections(doc):
824
+ """Genera red de conexiones de palabras"""
825
+ fig, ax = plt.subplots(figsize=(10, 6))
826
+ # Implementar visualización
827
+ plt.close()
828
+ return fig
829
+
830
+ ############################################################################
831
+ def generate_connection_paths(doc):
832
+ """Genera patrones de conexión"""
833
+ fig, ax = plt.subplots(figsize=(10, 6))
834
+ # Implementar visualización
835
+ plt.close()
836
+ return fig
837
+
838
+ ############################################################################
839
+ def create_vocabulary_network(doc):
840
+ """
841
+ Genera el grafo de red de vocabulario.
842
+ """
843
+ G = nx.Graph()
844
+
845
+ # Crear nodos para palabras significativas
846
+ words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
847
+ word_freq = Counter(words)
848
+
849
+ # Añadir nodos con tamaño basado en frecuencia
850
+ for word, freq in word_freq.items():
851
+ G.add_node(word, size=freq)
852
+
853
+ # Crear conexiones basadas en co-ocurrencia
854
+ window_size = 5
855
+ for i in range(len(words) - window_size):
856
+ window = words[i:i+window_size]
857
+ for w1, w2 in combinations(set(window), 2):
858
+ if G.has_edge(w1, w2):
859
+ G[w1][w2]['weight'] += 1
860
+ else:
861
+ G.add_edge(w1, w2, weight=1)
862
+
863
+ # Crear visualización
864
+ fig, ax = plt.subplots(figsize=(12, 8))
865
+ pos = nx.spring_layout(G)
866
+
867
+ # Dibujar nodos
868
+ nx.draw_networkx_nodes(G, pos,
869
+ node_size=[G.nodes[node]['size']*100 for node in G.nodes],
870
+ node_color='lightblue',
871
+ alpha=0.7)
872
+
873
+ # Dibujar conexiones
874
+ nx.draw_networkx_edges(G, pos,
875
+ width=[G[u][v]['weight']*0.5 for u,v in G.edges],
876
+ alpha=0.5)
877
+
878
+ # Añadir etiquetas
879
+ nx.draw_networkx_labels(G, pos)
880
+
881
+ plt.title("Red de Vocabulario")
882
+ plt.axis('off')
883
+ return fig
884
+
885
+ ############################################################################
886
+ def create_syntax_complexity_graph(doc):
887
+ """
888
+ Genera el diagrama de arco de complejidad sintáctica.
889
+ Muestra la estructura de dependencias con colores basados en la complejidad.
890
+ """
891
+ try:
892
+ # Preparar datos para la visualización
893
+ sentences = list(doc.sents)
894
+ if not sentences:
895
+ return None
896
+
897
+ # Crear figura para el gráfico
898
+ fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
899
+
900
+ # Colores para diferentes niveles de profundidad
901
+ depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
902
+
903
+ y_offset = 0
904
+ max_x = 0
905
+
906
+ for sent in sentences:
907
+ words = [token.text for token in sent]
908
+ x_positions = range(len(words))
909
+ max_x = max(max_x, len(words))
910
+
911
+ # Dibujar palabras
912
+ plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
913
+ plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
914
+
915
+ # Añadir texto
916
+ for i, word in enumerate(words):
917
+ plt.annotate(word, (i, y_offset), xytext=(0, -10),
918
+ textcoords='offset points', ha='center')
919
+
920
+ # Dibujar arcos de dependencia
921
+ for token in sent:
922
+ if token.dep_ != "ROOT":
923
+ # Calcular profundidad de dependencia
924
+ depth = 0
925
+ current = token
926
+ while current.head != current:
927
+ depth += 1
928
+ current = current.head
929
+
930
+ # Determinar posiciones para el arco
931
+ start = token.i - sent[0].i
932
+ end = token.head.i - sent[0].i
933
+
934
+ # Altura del arco basada en la distancia entre palabras
935
+ height = 0.5 * abs(end - start)
936
+
937
+ # Color basado en la profundidad
938
+ color = depth_colors[min(depth, len(depth_colors)-1)]
939
+
940
+ # Crear arco
941
+ arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
942
+ width=abs(end - start),
943
+ height=height,
944
+ angle=0,
945
+ theta1=0,
946
+ theta2=180,
947
+ color=color,
948
+ alpha=0.6)
949
+ ax.add_patch(arc)
950
+
951
+ y_offset -= 2
952
+
953
+ # Configurar el gráfico
954
+ plt.xlim(-1, max_x)
955
+ plt.ylim(y_offset - 1, 1)
956
+ plt.axis('off')
957
+ plt.title("Complejidad Sintáctica")
958
+
959
+ return fig
960
+
961
+ except Exception as e:
962
+ logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
963
+ return None
964
+
965
+ ############################################################################
966
+ def create_cohesion_heatmap(doc):
967
+ """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
968
+ try:
969
+ sentences = list(doc.sents)
970
+ n_sentences = len(sentences)
971
+
972
+ if n_sentences < 2:
973
+ return None
974
+
975
+ similarity_matrix = np.zeros((n_sentences, n_sentences))
976
+
977
+ for i in range(n_sentences):
978
+ for j in range(n_sentences):
979
+ sent1_lemmas = {token.lemma_ for token in sentences[i]
980
+ if token.is_alpha and not token.is_stop}
981
+ sent2_lemmas = {token.lemma_ for token in sentences[j]
982
+ if token.is_alpha and not token.is_stop}
983
+
984
+ if sent1_lemmas and sent2_lemmas:
985
+ intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
986
+ union = len(sent1_lemmas | sent2_lemmas) # Y aquí
987
+ similarity_matrix[i, j] = intersection / union if union > 0 else 0
988
+
989
+ # Crear visualización
990
+ fig, ax = plt.subplots(figsize=(10, 8))
991
+
992
+ sns.heatmap(similarity_matrix,
993
+ cmap='YlOrRd',
994
+ square=True,
995
+ xticklabels=False,
996
+ yticklabels=False,
997
+ cbar_kws={'label': 'Cohesión'},
998
+ ax=ax)
999
+
1000
+ plt.title("Mapa de Cohesión Textual")
1001
+ plt.xlabel("Oraciones")
1002
+ plt.ylabel("Oraciones")
1003
+
1004
+ plt.tight_layout()
1005
+ return fig
1006
+
1007
+ except Exception as e:
1008
+ logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
1009
+ return None
modules/studentact/current_situation_interface.py CHANGED
@@ -1,321 +1,436 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
- from ..database.current_situation_mongo_db import store_current_situation_result
9
-
10
- # Importaciones locales
11
- from translations import get_translations
12
-
13
- # Importamos la función de recomendaciones personalizadas si existe
14
- try:
15
- from .claude_recommendations import display_personalized_recommendations
16
- except ImportError:
17
- # Si no existe el módulo, definimos una función placeholder
18
- def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
19
- st.warning("Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
20
-
21
- from .current_situation_analysis import (
22
- analyze_text_dimensions,
23
- analyze_clarity,
24
- analyze_vocabulary_diversity,
25
- analyze_cohesion,
26
- analyze_structure,
27
- get_dependency_depths,
28
- normalize_score,
29
- generate_sentence_graphs,
30
- generate_word_connections,
31
- generate_connection_paths,
32
- create_vocabulary_network,
33
- create_syntax_complexity_graph,
34
- create_cohesion_heatmap
35
- )
36
-
37
- # Configuración del estilo de matplotlib para el gráfico de radar
38
- plt.rcParams['font.family'] = 'sans-serif'
39
- plt.rcParams['axes.grid'] = True
40
- plt.rcParams['axes.spines.top'] = False
41
- plt.rcParams['axes.spines.right'] = False
42
-
43
- logger = logging.getLogger(__name__)
44
-
45
- ####################################
46
- # Definición global de los tipos de texto y sus umbrales
47
- TEXT_TYPES = {
48
- 'academic_article': {
49
- 'name': 'Artículo Académico',
50
- 'thresholds': {
51
- 'vocabulary': {'min': 0.70, 'target': 0.85},
52
- 'structure': {'min': 0.75, 'target': 0.90},
53
- 'cohesion': {'min': 0.65, 'target': 0.80},
54
- 'clarity': {'min': 0.70, 'target': 0.85}
55
- }
56
- },
57
- 'student_essay': {
58
- 'name': 'Trabajo Universitario',
59
- 'thresholds': {
60
- 'vocabulary': {'min': 0.60, 'target': 0.75},
61
- 'structure': {'min': 0.65, 'target': 0.80},
62
- 'cohesion': {'min': 0.55, 'target': 0.70},
63
- 'clarity': {'min': 0.60, 'target': 0.75}
64
- }
65
- },
66
- 'general_communication': {
67
- 'name': 'Comunicación General',
68
- 'thresholds': {
69
- 'vocabulary': {'min': 0.50, 'target': 0.65},
70
- 'structure': {'min': 0.55, 'target': 0.70},
71
- 'cohesion': {'min': 0.45, 'target': 0.60},
72
- 'clarity': {'min': 0.50, 'target': 0.65}
73
- }
74
- }
75
- }
76
- ####################################
77
-
78
- def display_current_situation_interface(lang_code, nlp_models, t):
79
- """
80
- Interfaz simplificada con gráfico de radar para visualizar métricas.
81
- """
82
- # Inicializar estados si no existen
83
- if 'text_input' not in st.session_state:
84
- st.session_state.text_input = ""
85
- if 'text_area' not in st.session_state: # Añadir inicialización de text_area
86
- st.session_state.text_area = ""
87
- if 'show_results' not in st.session_state:
88
- st.session_state.show_results = False
89
- if 'current_doc' not in st.session_state:
90
- st.session_state.current_doc = None
91
- if 'current_metrics' not in st.session_state:
92
- st.session_state.current_metrics = None
93
- if 'current_recommendations' not in st.session_state:
94
- st.session_state.current_recommendations = None
95
-
96
- try:
97
- # Container principal con dos columnas
98
- with st.container():
99
- input_col, results_col = st.columns([1,2])
100
-
101
- with input_col:
102
- # Text area con manejo de estado
103
- text_input = st.text_area(
104
- t.get('input_prompt', "Escribe o pega tu texto aquí:"),
105
- height=400,
106
- key="text_area",
107
- value=st.session_state.text_input,
108
- help="Este texto será analizado para darte recomendaciones personalizadas"
109
- )
110
-
111
- # Función para manejar cambios de texto
112
- if text_input != st.session_state.text_input:
113
- st.session_state.text_input = text_input
114
- st.session_state.show_results = False
115
-
116
- if st.button(
117
- t.get('analyze_button', "Analizar mi escritura"),
118
- type="primary",
119
- disabled=not text_input.strip(),
120
- use_container_width=True,
121
- ):
122
- try:
123
- with st.spinner(t.get('processing', "Analizando...")):
124
- doc = nlp_models[lang_code](text_input)
125
- metrics = analyze_text_dimensions(doc)
126
-
127
- storage_success = store_current_situation_result(
128
- username=st.session_state.username,
129
- text=text_input,
130
- metrics=metrics,
131
- feedback=None
132
- )
133
-
134
- if not storage_success:
135
- logger.warning("No se pudo guardar el análisis en la base de datos")
136
-
137
- st.session_state.current_doc = doc
138
- st.session_state.current_metrics = metrics
139
- st.session_state.show_results = True
140
-
141
- except Exception as e:
142
- logger.error(f"Error en análisis: {str(e)}")
143
- st.error(t.get('analysis_error', "Error al analizar el texto"))
144
-
145
- # Mostrar resultados en la columna derecha
146
- with results_col:
147
- if st.session_state.show_results and st.session_state.current_metrics is not None:
148
- # Primero los radio buttons para tipo de texto
149
- st.markdown("### Tipo de texto")
150
- text_type = st.radio(
151
- label="Tipo de texto",
152
- options=list(TEXT_TYPES.keys()),
153
- format_func=lambda x: TEXT_TYPES[x]['name'],
154
- horizontal=True,
155
- key="text_type_radio",
156
- label_visibility="collapsed",
157
- help="Selecciona el tipo de texto para ajustar los criterios de evaluación"
158
- )
159
-
160
- st.session_state.current_text_type = text_type
161
-
162
- # Crear subtabs
163
- subtab1, subtab2 = st.tabs(["Diagnóstico", "Recomendaciones"])
164
-
165
- # Mostrar resultados en el primer subtab
166
- with subtab1:
167
- display_diagnosis(
168
- metrics=st.session_state.current_metrics,
169
- text_type=text_type
170
- )
171
-
172
- # Mostrar recomendaciones en el segundo subtab
173
- with subtab2:
174
- # Llamar directamente a la función de recomendaciones personalizadas
175
- display_personalized_recommendations(
176
- text=text_input,
177
- metrics=st.session_state.current_metrics,
178
- text_type=text_type,
179
- lang_code=lang_code,
180
- t=t
181
- )
182
-
183
- except Exception as e:
184
- logger.error(f"Error en interfaz principal: {str(e)}")
185
- st.error("Ocurrió un error al cargar la interfaz")
186
-
187
- def display_diagnosis(metrics, text_type=None):
188
- """
189
- Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
190
- """
191
- try:
192
- # Usar valor por defecto si no se especifica tipo
193
- text_type = text_type or 'student_essay'
194
-
195
- # Obtener umbrales según el tipo de texto
196
- thresholds = TEXT_TYPES[text_type]['thresholds']
197
-
198
- # Crear dos columnas para las métricas y el gráfico
199
- metrics_col, graph_col = st.columns([1, 1.5])
200
-
201
- # Columna de métricas
202
- with metrics_col:
203
- metrics_config = [
204
- {
205
- 'label': "Vocabulario",
206
- 'key': 'vocabulary',
207
- 'value': metrics['vocabulary']['normalized_score'],
208
- 'help': "Riqueza y variedad del vocabulario",
209
- 'thresholds': thresholds['vocabulary']
210
- },
211
- {
212
- 'label': "Estructura",
213
- 'key': 'structure',
214
- 'value': metrics['structure']['normalized_score'],
215
- 'help': "Organización y complejidad de oraciones",
216
- 'thresholds': thresholds['structure']
217
- },
218
- {
219
- 'label': "Cohesión",
220
- 'key': 'cohesion',
221
- 'value': metrics['cohesion']['normalized_score'],
222
- 'help': "Conexión y fluidez entre ideas",
223
- 'thresholds': thresholds['cohesion']
224
- },
225
- {
226
- 'label': "Claridad",
227
- 'key': 'clarity',
228
- 'value': metrics['clarity']['normalized_score'],
229
- 'help': "Facilidad de comprensión del texto",
230
- 'thresholds': thresholds['clarity']
231
- }
232
- ]
233
-
234
- # Mostrar métricas
235
- for metric in metrics_config:
236
- value = metric['value']
237
- if value < metric['thresholds']['min']:
238
- status = "⚠️ Por mejorar"
239
- color = "inverse"
240
- elif value < metric['thresholds']['target']:
241
- status = "📈 Aceptable"
242
- color = "off"
243
- else:
244
- status = "✅ Óptimo"
245
- color = "normal"
246
-
247
- st.metric(
248
- metric['label'],
249
- f"{value:.2f}",
250
- f"{status} (Meta: {metric['thresholds']['target']:.2f})",
251
- delta_color=color,
252
- help=metric['help']
253
- )
254
- st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
255
-
256
- # Gráfico radar en la columna derecha
257
- with graph_col:
258
- display_radar_chart(metrics_config, thresholds)
259
-
260
- except Exception as e:
261
- logger.error(f"Error mostrando resultados: {str(e)}")
262
- st.error("Error al mostrar los resultados")
263
-
264
- def display_radar_chart(metrics_config, thresholds):
265
- """
266
- Muestra el gráfico radar con los resultados.
267
- """
268
- try:
269
- # Preparar datos para el gráfico
270
- categories = [m['label'] for m in metrics_config]
271
- values_user = [m['value'] for m in metrics_config]
272
- min_values = [m['thresholds']['min'] for m in metrics_config]
273
- target_values = [m['thresholds']['target'] for m in metrics_config]
274
-
275
- # Crear y configurar gráfico
276
- fig = plt.figure(figsize=(8, 8))
277
- ax = fig.add_subplot(111, projection='polar')
278
-
279
- # Configurar radar
280
- angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
281
- angles += angles[:1]
282
- values_user += values_user[:1]
283
- min_values += min_values[:1]
284
- target_values += target_values[:1]
285
-
286
- # Configurar ejes
287
- ax.set_xticks(angles[:-1])
288
- ax.set_xticklabels(categories, fontsize=10)
289
- circle_ticks = np.arange(0, 1.1, 0.2)
290
- ax.set_yticks(circle_ticks)
291
- ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
292
- ax.set_ylim(0, 1)
293
-
294
- # Dibujar áreas de umbrales
295
- ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label='Mínimo', alpha=0.5)
296
- ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label='Meta', alpha=0.5)
297
- ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
298
- ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
299
-
300
- # Dibujar valores del usuario
301
- ax.plot(angles, values_user, '#3498db', linewidth=2, label='Tu escritura')
302
- ax.fill(angles, values_user, '#3498db', alpha=0.2)
303
-
304
- # Ajustar leyenda
305
- ax.legend(
306
- loc='upper right',
307
- bbox_to_anchor=(1.3, 1.1),
308
- fontsize=10,
309
- frameon=True,
310
- facecolor='white',
311
- edgecolor='none',
312
- shadow=True
313
- )
314
-
315
- plt.tight_layout()
316
- st.pyplot(fig)
317
- plt.close()
318
-
319
- except Exception as e:
320
- logger.error(f"Error mostrando gráfico radar: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  st.error("Error al mostrar el gráfico")
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from ..database.current_situation_mongo_db import store_current_situation_result
9
+
10
+ # Importaciones locales
11
+ from translations import get_translations
12
+
13
+ # Importamos la función de recomendaciones personalizadas si existe
14
+ try:
15
+ from .claude_recommendations import display_personalized_recommendations
16
+ except ImportError:
17
+ # Si no existe el módulo, definimos una función placeholder
18
+ def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
19
+ # Obtener el mensaje de advertencia traducido si está disponible
20
+ warning = t.get('module_not_available', "Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
21
+ st.warning(warning)
22
+
23
+ from .current_situation_analysis import (
24
+ analyze_text_dimensions,
25
+ analyze_clarity,
26
+ analyze_vocabulary_diversity,
27
+ analyze_cohesion,
28
+ analyze_structure,
29
+ get_dependency_depths,
30
+ normalize_score,
31
+ generate_sentence_graphs,
32
+ generate_word_connections,
33
+ generate_connection_paths,
34
+ create_vocabulary_network,
35
+ create_syntax_complexity_graph,
36
+ create_cohesion_heatmap
37
+ )
38
+
39
+ # Configuración del estilo de matplotlib para el gráfico de radar
40
+ plt.rcParams['font.family'] = 'sans-serif'
41
+ plt.rcParams['axes.grid'] = True
42
+ plt.rcParams['axes.spines.top'] = False
43
+ plt.rcParams['axes.spines.right'] = False
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Definición de tipos de texto con umbrales
48
+ TEXT_TYPES = {
49
+ 'academic_article': {
50
+ # Los nombres se obtendrán de las traducciones
51
+ 'thresholds': {
52
+ 'vocabulary': {'min': 0.70, 'target': 0.85},
53
+ 'structure': {'min': 0.75, 'target': 0.90},
54
+ 'cohesion': {'min': 0.65, 'target': 0.80},
55
+ 'clarity': {'min': 0.70, 'target': 0.85}
56
+ }
57
+ },
58
+ 'student_essay': {
59
+ 'thresholds': {
60
+ 'vocabulary': {'min': 0.60, 'target': 0.75},
61
+ 'structure': {'min': 0.65, 'target': 0.80},
62
+ 'cohesion': {'min': 0.55, 'target': 0.70},
63
+ 'clarity': {'min': 0.60, 'target': 0.75}
64
+ }
65
+ },
66
+ 'general_communication': {
67
+ 'thresholds': {
68
+ 'vocabulary': {'min': 0.50, 'target': 0.65},
69
+ 'structure': {'min': 0.55, 'target': 0.70},
70
+ 'cohesion': {'min': 0.45, 'target': 0.60},
71
+ 'clarity': {'min': 0.50, 'target': 0.65}
72
+ }
73
+ }
74
+ }
75
+
76
+ ####################################################
77
+ ####################################################
78
+ def display_current_situation_interface(lang_code, nlp_models, t):
79
+ """
80
+ Interfaz simplificada con gráfico de radar para visualizar métricas.
81
+ """
82
+ # Agregar logs para depuración
83
+ logger.info(f"Idioma: {lang_code}")
84
+ logger.info(f"Claves en t: {list(t.keys())}")
85
+
86
+ # Inicializar estados si no existen
87
+ if 'text_input' not in st.session_state:
88
+ st.session_state.text_input = ""
89
+ if 'text_area' not in st.session_state:
90
+ st.session_state.text_area = ""
91
+ if 'show_results' not in st.session_state:
92
+ st.session_state.show_results = False
93
+ if 'current_doc' not in st.session_state:
94
+ st.session_state.current_doc = None
95
+ if 'current_metrics' not in st.session_state:
96
+ st.session_state.current_metrics = None
97
+ if 'current_recommendations' not in st.session_state:
98
+ st.session_state.current_recommendations = None
99
+
100
+ try:
101
+ # Container principal con dos columnas
102
+ with st.container():
103
+ input_col, results_col = st.columns([1,2])
104
+
105
+ ###############################################################################################
106
+ # CSS personalizado para que el formulario ocupe todo el alto disponible
107
+ st.markdown("""
108
+ <style>
109
+ /* Hacer que la columna tenga una altura definida */
110
+ [data-testid="column"] {
111
+ min-height: 900px;
112
+ height: 100vh; /* 100% del alto visible de la ventana */
113
+ }
114
+
115
+ /* Hacer que el formulario ocupe el espacio disponible en la columna */
116
+ .stForm {
117
+ height: calc(100% - 40px); /* Ajuste por márgenes y paddings */
118
+ display: flex;
119
+ flex-direction: column;
120
+ }
121
+
122
+ /* Hacer que el área de texto se expanda dentro del formulario */
123
+ .stForm .stTextArea {
124
+ flex: 1;
125
+ display: flex;
126
+ flex-direction: column;
127
+ }
128
+
129
+ /* El textarea en sí debe expandirse */
130
+ .stForm .stTextArea textarea {
131
+ flex: 1;
132
+ min-height: 750px !important;
133
+ }
134
+ </style>
135
+ """, unsafe_allow_html=True)
136
+
137
+ ###############################################################################################
138
+ with input_col:
139
+ with st.form(key=f"text_input_form_{lang_code}"):
140
+ text_input = st.text_area(
141
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
142
+ height=800,
143
+ key=f"text_area_{lang_code}",
144
+ value=st.session_state.text_input,
145
+ help=t.get('help', "Este texto será analizado para darte recomendaciones personalizadas")
146
+ )
147
+
148
+ submit_button = st.form_submit_button(
149
+ t.get('analyze_button', "Analizar mi escritura"),
150
+ type="primary",
151
+ use_container_width=True
152
+ )
153
+
154
+ if submit_button:
155
+ if text_input.strip():
156
+ st.session_state.text_input = text_input
157
+
158
+ #######################################################################
159
+ # Código para análisis...
160
+ try:
161
+ with st.spinner(t.get('processing', "Analizando...")): # Usando t.get directamente
162
+ doc = nlp_models[lang_code](text_input)
163
+ metrics = analyze_text_dimensions(doc)
164
+
165
+ storage_success = store_current_situation_result(
166
+ username=st.session_state.username,
167
+ text=text_input,
168
+ metrics=metrics,
169
+ feedback=None
170
+ )
171
+
172
+ if not storage_success:
173
+ logger.warning("No se pudo guardar el análisis en la base de datos")
174
+
175
+ st.session_state.current_doc = doc
176
+ st.session_state.current_metrics = metrics
177
+ st.session_state.show_results = True
178
+
179
+ except Exception as e:
180
+ logger.error(f"Error en análisis: {str(e)}")
181
+ st.error(t.get('analysis_error', "Error al analizar el texto")) # Usando t.get directamente
182
+
183
+ # Mostrar resultados en la columna derecha
184
+ with results_col:
185
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
186
+ # Primero los radio buttons para tipo de texto - usando t.get directamente
187
+ st.markdown(f"### {t.get('text_type_header', 'Tipo de texto')}")
188
+
189
+ # Preparar opciones de tipos de texto con nombres traducidos
190
+ text_type_options = {}
191
+ for text_type_key in TEXT_TYPES.keys():
192
+ # Fallback a nombres genéricos si no hay traducción
193
+ default_names = {
194
+ 'academic_article': 'Academic Article' if lang_code == 'en' else 'Артикул академічний' if lang_code == 'uk' else 'Artículo Académico',
195
+ 'student_essay': 'Student Essay' if lang_code == 'en' else 'Студентське есе' if lang_code == 'uk' else 'Trabajo Universitario',
196
+ 'general_communication': 'General Communication' if lang_code == 'en' else 'Загальна комунікація' if lang_code == 'uk' else 'Comunicación General'
197
+ }
198
+ text_type_options[text_type_key] = default_names.get(text_type_key, text_type_key)
199
+
200
+ text_type = st.radio(
201
+ label=t.get('text_type_header', "Tipo de texto"), # Usando t.get directamente
202
+ options=list(TEXT_TYPES.keys()),
203
+ format_func=lambda x: text_type_options.get(x, x),
204
+ horizontal=True,
205
+ key="text_type_radio",
206
+ label_visibility="collapsed",
207
+ help=t.get('text_type_help', "Selecciona el tipo de texto para ajustar los criterios de evaluación") # Usando t.get directamente
208
+ )
209
+
210
+ st.session_state.current_text_type = text_type
211
+
212
+ # Crear subtabs con nombres traducidos
213
+ diagnosis_tab = "Diagnosis" if lang_code == 'en' else "Діагностика" if lang_code == 'uk' else "Diagnóstico"
214
+ recommendations_tab = "Recommendations" if lang_code == 'en' else "Рекомендації" if lang_code == 'uk' else "Recomendaciones"
215
+
216
+ subtab1, subtab2 = st.tabs([diagnosis_tab, recommendations_tab])
217
+
218
+ # Mostrar resultados en el primer subtab
219
+ with subtab1:
220
+ display_diagnosis(
221
+ metrics=st.session_state.current_metrics,
222
+ text_type=text_type,
223
+ lang_code=lang_code,
224
+ t=t # Pasar t directamente, no current_situation_t
225
+ )
226
+
227
+ # Mostrar recomendaciones en el segundo subtab
228
+ with subtab2:
229
+ # Llamar directamente a la función de recomendaciones personalizadas
230
+ display_personalized_recommendations(
231
+ text=text_input,
232
+ metrics=st.session_state.current_metrics,
233
+ text_type=text_type,
234
+ lang_code=lang_code,
235
+ t=t
236
+ )
237
+
238
+ except Exception as e:
239
+ logger.error(f"Error en interfaz principal: {str(e)}")
240
+ st.error(t.get('error_interface', "Ocurrió un error al cargar la interfaz")) # Usando t.get directamente
241
+
242
+ #################################################################
243
+ #################################################################
244
+ def display_diagnosis(metrics, text_type=None, lang_code='es', t=None):
245
+ """
246
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
247
+ """
248
+ try:
249
+ # Asegurar que tenemos traducciones
250
+ if t is None:
251
+ t = {}
252
+
253
+ # Traducciones para títulos y etiquetas
254
+ dimension_labels = {
255
+ 'es': {
256
+ 'title': "Tipo de texto",
257
+ 'vocabulary': "Vocabulario",
258
+ 'structure': "Estructura",
259
+ 'cohesion': "Cohesión",
260
+ 'clarity': "Claridad",
261
+ 'improvement': "⚠️ Por mejorar",
262
+ 'acceptable': "📈 Aceptable",
263
+ 'optimal': "✅ Óptimo",
264
+ 'target': "Meta: {:.2f}"
265
+ },
266
+ 'en': {
267
+ 'title': "Text Type",
268
+ 'vocabulary': "Vocabulary",
269
+ 'structure': "Structure",
270
+ 'cohesion': "Cohesion",
271
+ 'clarity': "Clarity",
272
+ 'improvement': "⚠️ Needs improvement",
273
+ 'acceptable': "📈 Acceptable",
274
+ 'optimal': "✅ Optimal",
275
+ 'target': "Target: {:.2f}"
276
+ },
277
+ 'uk': {
278
+ 'title': "Тип тексту",
279
+ 'vocabulary': "Словниковий запас",
280
+ 'structure': "Структура",
281
+ 'cohesion': "Зв'язність",
282
+ 'clarity': "Ясність",
283
+ 'improvement': "⚠️ Потребує покращення",
284
+ 'acceptable': "📈 Прийнятно",
285
+ 'optimal': "✅ Оптимально",
286
+ 'target': "Ціль: {:.2f}"
287
+ }
288
+ }
289
+
290
+ # Obtener traducciones para el idioma actual, con fallback a español
291
+ labels = dimension_labels.get(lang_code, dimension_labels['es'])
292
+
293
+ # Usar valor por defecto si no se especifica tipo
294
+ text_type = text_type or 'student_essay'
295
+
296
+ # Obtener umbrales según el tipo de texto
297
+ thresholds = TEXT_TYPES[text_type]['thresholds']
298
+
299
+ # Crear dos columnas para las métricas y el gráfico
300
+ metrics_col, graph_col = st.columns([1, 1.5])
301
+
302
+ # Columna de métricas
303
+ with metrics_col:
304
+ metrics_config = [
305
+ {
306
+ 'label': labels['vocabulary'],
307
+ 'key': 'vocabulary',
308
+ 'value': metrics['vocabulary']['normalized_score'],
309
+ 'help': t.get('vocabulary_help', "Riqueza y variedad del vocabulario"),
310
+ 'thresholds': thresholds['vocabulary']
311
+ },
312
+ {
313
+ 'label': labels['structure'],
314
+ 'key': 'structure',
315
+ 'value': metrics['structure']['normalized_score'],
316
+ 'help': t.get('structure_help', "Organización y complejidad de oraciones"),
317
+ 'thresholds': thresholds['structure']
318
+ },
319
+ {
320
+ 'label': labels['cohesion'],
321
+ 'key': 'cohesion',
322
+ 'value': metrics['cohesion']['normalized_score'],
323
+ 'help': t.get('cohesion_help', "Conexión y fluidez entre ideas"),
324
+ 'thresholds': thresholds['cohesion']
325
+ },
326
+ {
327
+ 'label': labels['clarity'],
328
+ 'key': 'clarity',
329
+ 'value': metrics['clarity']['normalized_score'],
330
+ 'help': t.get('clarity_help', "Facilidad de comprensión del texto"),
331
+ 'thresholds': thresholds['clarity']
332
+ }
333
+ ]
334
+
335
+ # Mostrar métricas con textos traducidos
336
+ for metric in metrics_config:
337
+ value = metric['value']
338
+ if value < metric['thresholds']['min']:
339
+ status = labels['improvement']
340
+ color = "inverse"
341
+ elif value < metric['thresholds']['target']:
342
+ status = labels['acceptable']
343
+ color = "off"
344
+ else:
345
+ status = labels['optimal']
346
+ color = "normal"
347
+
348
+ target_text = labels['target'].format(metric['thresholds']['target'])
349
+
350
+ st.metric(
351
+ metric['label'],
352
+ f"{value:.2f}",
353
+ f"{status} ({target_text})",
354
+ delta_color=color,
355
+ help=metric['help']
356
+ )
357
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
358
+
359
+ # Gráfico radar en la columna derecha
360
+ with graph_col:
361
+ display_radar_chart(metrics_config, thresholds, lang_code) # Pasar el parámetro lang_code
362
+
363
+ except Exception as e:
364
+ logger.error(f"Error mostrando resultados: {str(e)}")
365
+ st.error(t.get('error_results', "Error al mostrar los resultados"))
366
+
367
+ ##################################################################
368
+ ##################################################################
369
+ def display_radar_chart(metrics_config, thresholds, lang_code='es'):
370
+ """
371
+ Muestra el gráfico radar con los resultados.
372
+ """
373
+ try:
374
+ # Traducción de las etiquetas de leyenda según el idioma
375
+ legend_translations = {
376
+ 'es': {'min': 'Mínimo', 'target': 'Meta', 'user': 'Tu escritura'},
377
+ 'en': {'min': 'Minimum', 'target': 'Target', 'user': 'Your writing'},
378
+ 'uk': {'min': 'Мінімум', 'target': 'Ціль', 'user': 'Ваш текст'}
379
+ }
380
+
381
+ # Usar español por defecto si el idioma no está soportado
382
+ translations = legend_translations.get(lang_code, legend_translations['es'])
383
+
384
+ # Preparar datos para el gráfico
385
+ categories = [m['label'] for m in metrics_config]
386
+ values_user = [m['value'] for m in metrics_config]
387
+ min_values = [m['thresholds']['min'] for m in metrics_config]
388
+ target_values = [m['thresholds']['target'] for m in metrics_config]
389
+
390
+ # Crear y configurar gráfico
391
+ fig = plt.figure(figsize=(8, 8))
392
+ ax = fig.add_subplot(111, projection='polar')
393
+
394
+ # Configurar radar
395
+ angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
396
+ angles += angles[:1]
397
+ values_user += values_user[:1]
398
+ min_values += min_values[:1]
399
+ target_values += target_values[:1]
400
+
401
+ # Configurar ejes
402
+ ax.set_xticks(angles[:-1])
403
+ ax.set_xticklabels(categories, fontsize=10)
404
+ circle_ticks = np.arange(0, 1.1, 0.2)
405
+ ax.set_yticks(circle_ticks)
406
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
407
+ ax.set_ylim(0, 1)
408
+
409
+ # Dibujar áreas de umbrales con etiquetas traducidas
410
+ ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label=translations['min'], alpha=0.5)
411
+ ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label=translations['target'], alpha=0.5)
412
+ ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
413
+ ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
414
+
415
+ # Dibujar valores del usuario con etiqueta traducida
416
+ ax.plot(angles, values_user, '#3498db', linewidth=2, label=translations['user'])
417
+ ax.fill(angles, values_user, '#3498db', alpha=0.2)
418
+
419
+ # Ajustar leyenda
420
+ ax.legend(
421
+ loc='upper right',
422
+ bbox_to_anchor=(1.3, 1.1),
423
+ fontsize=10,
424
+ frameon=True,
425
+ facecolor='white',
426
+ edgecolor='none',
427
+ shadow=True
428
+ )
429
+
430
+ plt.tight_layout()
431
+ st.pyplot(fig)
432
+ plt.close()
433
+
434
+ except Exception as e:
435
+ logger.error(f"Error mostrando gráfico radar: {str(e)}")
436
  st.error("Error al mostrar el gráfico")
modules/studentact/student_activities_v2.py CHANGED
@@ -1,571 +1,576 @@
1
- ##############
2
- ###modules/studentact/student_activities_v2.py
3
-
4
- import streamlit as st
5
- import re
6
- import io
7
- from io import BytesIO
8
- import pandas as pd
9
- import numpy as np
10
- import time
11
- import matplotlib.pyplot as plt
12
- from datetime import datetime, timedelta
13
- from spacy import displacy
14
- import random
15
- import base64
16
- import seaborn as sns
17
- import logging
18
-
19
- # Importaciones de la base de datos
20
- from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
- from ..database.semantic_mongo_db import get_student_semantic_analysis
22
- from ..database.discourse_mongo_db import get_student_discourse_analysis
23
- from ..database.chat_mongo_db import get_chat_history
24
- from ..database.current_situation_mongo_db import get_current_situation_analysis
25
- from ..database.claude_recommendations_mongo_db import get_claude_recommendations
26
-
27
- # Importar la función generate_unique_key
28
- from ..utils.widget_utils import generate_unique_key
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
- ###################################################################################
33
-
34
- def display_student_activities(username: str, lang_code: str, t: dict):
35
- """
36
- Muestra todas las actividades del estudiante
37
- Args:
38
- username: Nombre del estudiante
39
- lang_code: Código del idioma
40
- t: Diccionario de traducciones
41
- """
42
- try:
43
- st.header(t.get('activities_title', 'Mis Actividades'))
44
-
45
- # Tabs para diferentes tipos de análisis
46
- tabs = st.tabs([
47
- t.get('current_situation_activities', 'Mi Situación Actual'),
48
- t.get('morpho_activities', 'Análisis Morfosintáctico'),
49
- t.get('semantic_activities', 'Análisis Semántico'),
50
- t.get('discourse_activities', 'Análisis del Discurso'),
51
- t.get('chat_activities', 'Conversaciones con el Asistente')
52
- ])
53
-
54
- # Tab de Situación Actual
55
- with tabs[0]:
56
- display_current_situation_activities(username, t)
57
-
58
- # Tab de Análisis Morfosintáctico
59
- with tabs[1]:
60
- display_morphosyntax_activities(username, t)
61
-
62
- # Tab de Análisis Semántico
63
- with tabs[2]:
64
- display_semantic_activities(username, t)
65
-
66
- # Tab de Análisis del Discurso
67
- with tabs[3]:
68
- display_discourse_activities(username, t)
69
-
70
- # Tab de Conversaciones del Chat
71
- with tabs[4]:
72
- display_chat_activities(username, t)
73
-
74
- except Exception as e:
75
- logger.error(f"Error mostrando actividades: {str(e)}")
76
- st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
77
-
78
-
79
- ###############################################################################################
80
-
81
- def display_current_situation_activities(username: str, t: dict):
82
- """
83
- Muestra análisis de situación actual junto con las recomendaciones de Claude
84
- unificando la información de ambas colecciones y emparejándolas por cercanía temporal.
85
- """
86
- try:
87
- # Recuperar datos de ambas colecciones
88
- logger.info(f"Recuperando análisis de situación actual para {username}")
89
- situation_analyses = get_current_situation_analysis(username, limit=10)
90
-
91
- # Verificar si hay datos
92
- if situation_analyses:
93
- logger.info(f"Recuperados {len(situation_analyses)} análisis de situación")
94
- # Depurar para ver la estructura de datos
95
- for i, analysis in enumerate(situation_analyses):
96
- logger.info(f"Análisis #{i+1}: Claves disponibles: {list(analysis.keys())}")
97
- if 'metrics' in analysis:
98
- logger.info(f"Métricas disponibles: {list(analysis['metrics'].keys())}")
99
- else:
100
- logger.warning("No se encontraron análisis de situación actual")
101
-
102
- logger.info(f"Recuperando recomendaciones de Claude para {username}")
103
- claude_recommendations = get_claude_recommendations(username)
104
-
105
- if claude_recommendations:
106
- logger.info(f"Recuperadas {len(claude_recommendations)} recomendaciones de Claude")
107
- else:
108
- logger.warning("No se encontraron recomendaciones de Claude")
109
-
110
- # Verificar si hay algún tipo de análisis disponible
111
- if not situation_analyses and not claude_recommendations:
112
- logger.info("No se encontraron análisis de situación actual ni recomendaciones")
113
- st.info(t.get('no_current_situation', 'No hay análisis de situación actual registrados'))
114
- return
115
-
116
- # Crear pares combinados emparejando diagnósticos y recomendaciones cercanos en tiempo
117
- logger.info("Creando emparejamientos temporales de análisis")
118
-
119
- # Convertir timestamps a objetos datetime para comparación
120
- situation_times = []
121
- for analysis in situation_analyses:
122
- if 'timestamp' in analysis:
123
- try:
124
- timestamp_str = analysis['timestamp']
125
- dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
126
- situation_times.append((dt, analysis))
127
- except Exception as e:
128
- logger.error(f"Error parseando timestamp de situación: {str(e)}")
129
-
130
- recommendation_times = []
131
- for recommendation in claude_recommendations:
132
- if 'timestamp' in recommendation:
133
- try:
134
- timestamp_str = recommendation['timestamp']
135
- dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
136
- recommendation_times.append((dt, recommendation))
137
- except Exception as e:
138
- logger.error(f"Error parseando timestamp de recomendación: {str(e)}")
139
-
140
- # Ordenar por tiempo
141
- situation_times.sort(key=lambda x: x[0], reverse=True)
142
- recommendation_times.sort(key=lambda x: x[0], reverse=True)
143
-
144
- # Crear pares combinados
145
- combined_items = []
146
-
147
- # Primero, procesar todas las situaciones encontrando la recomendación más cercana
148
- for sit_time, situation in situation_times:
149
- # Buscar la recomendación más cercana en tiempo
150
- best_match = None
151
- min_diff = timedelta(minutes=30) # Máxima diferencia de tiempo aceptable (30 minutos)
152
- best_rec_time = None
153
-
154
- for rec_time, recommendation in recommendation_times:
155
- time_diff = abs(sit_time - rec_time)
156
- if time_diff < min_diff:
157
- min_diff = time_diff
158
- best_match = recommendation
159
- best_rec_time = rec_time
160
-
161
- # Crear un elemento combinado
162
- if best_match:
163
- timestamp_key = sit_time.isoformat()
164
- combined_items.append((timestamp_key, {
165
- 'situation': situation,
166
- 'recommendation': best_match,
167
- 'time_diff': min_diff.total_seconds()
168
- }))
169
- # Eliminar la recomendación usada para no reutilizarla
170
- recommendation_times = [(t, r) for t, r in recommendation_times if t != best_rec_time]
171
- logger.info(f"Emparejado: Diagnóstico {sit_time} con Recomendación {best_rec_time} (diferencia: {min_diff})")
172
- else:
173
- # Si no hay recomendación cercana, solo incluir la situación
174
- timestamp_key = sit_time.isoformat()
175
- combined_items.append((timestamp_key, {
176
- 'situation': situation
177
- }))
178
- logger.info(f"Sin emparejar: Diagnóstico {sit_time} sin recomendación cercana")
179
-
180
- # Agregar recomendaciones restantes sin situación
181
- for rec_time, recommendation in recommendation_times:
182
- timestamp_key = rec_time.isoformat()
183
- combined_items.append((timestamp_key, {
184
- 'recommendation': recommendation
185
- }))
186
- logger.info(f"Sin emparejar: Recomendación {rec_time} sin diagnóstico cercano")
187
-
188
- # Ordenar por tiempo (más reciente primero)
189
- combined_items.sort(key=lambda x: x[0], reverse=True)
190
-
191
- logger.info(f"Procesando {len(combined_items)} elementos combinados")
192
-
193
- # Mostrar cada par combinado
194
- for i, (timestamp_key, analysis_pair) in enumerate(combined_items):
195
- try:
196
- # Obtener datos de situación y recomendación
197
- situation_data = analysis_pair.get('situation', {})
198
- recommendation_data = analysis_pair.get('recommendation', {})
199
- time_diff = analysis_pair.get('time_diff')
200
-
201
- # Si no hay ningún dato, continuar al siguiente
202
- if not situation_data and not recommendation_data:
203
- continue
204
-
205
- # Determinar qué texto mostrar (priorizar el de la situación)
206
- text_to_show = situation_data.get('text', recommendation_data.get('text', ''))
207
- text_type = situation_data.get('text_type', recommendation_data.get('text_type', ''))
208
-
209
- # Formatear fecha para mostrar
210
- try:
211
- # Usar timestamp del key que ya es un formato ISO
212
- dt = datetime.fromisoformat(timestamp_key)
213
- formatted_date = dt.strftime("%d/%m/%Y %H:%M:%S")
214
- except Exception as date_error:
215
- logger.error(f"Error formateando fecha: {str(date_error)}")
216
- formatted_date = timestamp_key
217
-
218
- # Determinar el título del expander
219
- title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
220
- if text_type:
221
- text_type_display = {
222
- 'academic_article': t.get('academic_article', 'Artículo académico'),
223
- 'student_essay': t.get('student_essay', 'Trabajo universitario'),
224
- 'general_communication': t.get('general_communication', 'Comunicación general')
225
- }.get(text_type, text_type)
226
- title += f" - {text_type_display}"
227
-
228
- # Añadir indicador de emparejamiento si existe
229
- if time_diff is not None:
230
- if time_diff < 60: # menos de un minuto
231
- title += f" 🔄 (emparejados)"
232
- else:
233
- title += f" 🔄 (emparejados, diferencia: {int(time_diff//60)} min)"
234
-
235
- # Usar un ID único para cada expander
236
- expander_id = f"analysis_{i}_{timestamp_key.replace(':', '_')}"
237
-
238
- # Mostrar el análisis en un expander
239
- with st.expander(title, expanded=False):
240
- # Mostrar texto analizado con key único
241
- st.subheader(t.get('analyzed_text', 'Texto analizado'))
242
- st.text_area(
243
- "Text Content",
244
- value=text_to_show,
245
- height=100,
246
- disabled=True,
247
- label_visibility="collapsed",
248
- key=f"text_area_{expander_id}"
249
- )
250
-
251
- # Crear tabs para separar diagnóstico y recomendaciones
252
- diagnosis_tab, recommendations_tab = st.tabs([
253
- t.get('diagnosis_tab', 'Diagnóstico'),
254
- t.get('recommendations_tab', 'Recomendaciones')
255
- ])
256
-
257
- # Tab de diagnóstico
258
- with diagnosis_tab:
259
- if situation_data and 'metrics' in situation_data:
260
- metrics = situation_data['metrics']
261
-
262
- # Dividir en dos columnas
263
- col1, col2 = st.columns(2)
264
-
265
- # Principales métricas en formato de tarjetas
266
- with col1:
267
- st.subheader(t.get('key_metrics', 'Métricas clave'))
268
-
269
- # Mostrar cada métrica principal
270
- for metric_name, metric_data in metrics.items():
271
- try:
272
- # Determinar la puntuación
273
- score = None
274
- if isinstance(metric_data, dict):
275
- # Intentar diferentes nombres de campo
276
- if 'normalized_score' in metric_data:
277
- score = metric_data['normalized_score']
278
- elif 'score' in metric_data:
279
- score = metric_data['score']
280
- elif 'value' in metric_data:
281
- score = metric_data['value']
282
- elif isinstance(metric_data, (int, float)):
283
- score = metric_data
284
-
285
- if score is not None:
286
- # Asegurarse de que score es numérico
287
- if isinstance(score, (int, float)):
288
- # Determinar color y emoji basado en la puntuación
289
- if score < 0.5:
290
- emoji = "🔴"
291
- color = "#ffcccc" # light red
292
- elif score < 0.75:
293
- emoji = "🟡"
294
- color = "#ffffcc" # light yellow
295
- else:
296
- emoji = "🟢"
297
- color = "#ccffcc" # light green
298
-
299
- # Mostrar la métrica con estilo
300
- st.markdown(f"""
301
- <div style="background-color:{color}; padding:10px; border-radius:5px; margin-bottom:10px;">
302
- <b>{emoji} {metric_name.capitalize()}:</b> {score:.2f}
303
- </div>
304
- """, unsafe_allow_html=True)
305
- else:
306
- # Si no es numérico, mostrar como texto
307
- st.markdown(f"""
308
- <div style="background-color:#f0f0f0; padding:10px; border-radius:5px; margin-bottom:10px;">
309
- <b>ℹ️ {metric_name.capitalize()}:</b> {str(score)}
310
- </div>
311
- """, unsafe_allow_html=True)
312
- except Exception as e:
313
- logger.error(f"Error procesando métrica {metric_name}: {str(e)}")
314
-
315
- # Mostrar detalles adicionales si están disponibles
316
- with col2:
317
- st.subheader(t.get('details', 'Detalles'))
318
-
319
- # Para cada métrica, mostrar sus detalles si existen
320
- for metric_name, metric_data in metrics.items():
321
- try:
322
- if isinstance(metric_data, dict):
323
- # Mostrar detalles directamente o buscar en subcampos
324
- details = None
325
- if 'details' in metric_data and metric_data['details']:
326
- details = metric_data['details']
327
- else:
328
- # Crear un diccionario con los detalles excluyendo 'normalized_score' y similares
329
- details = {k: v for k, v in metric_data.items()
330
- if k not in ['normalized_score', 'score', 'value']}
331
-
332
- if details:
333
- st.write(f"**{metric_name.capitalize()}**")
334
- st.json(details, expanded=False)
335
- except Exception as e:
336
- logger.error(f"Error mostrando detalles de {metric_name}: {str(e)}")
337
- else:
338
- st.info(t.get('no_diagnosis', 'No hay datos de diagnóstico disponibles'))
339
-
340
- # Tab de recomendaciones
341
- with recommendations_tab:
342
- if recommendation_data and 'recommendations' in recommendation_data:
343
- st.markdown(f"""
344
- <div style="padding: 20px; border-radius: 10px;
345
- background-color: #f8f9fa; margin-bottom: 20px;">
346
- {recommendation_data['recommendations']}
347
- </div>
348
- """, unsafe_allow_html=True)
349
- elif recommendation_data and 'feedback' in recommendation_data:
350
- st.markdown(f"""
351
- <div style="padding: 20px; border-radius: 10px;
352
- background-color: #f8f9fa; margin-bottom: 20px;">
353
- {recommendation_data['feedback']}
354
- </div>
355
- """, unsafe_allow_html=True)
356
- else:
357
- st.info(t.get('no_recommendations', 'No hay recomendaciones disponibles'))
358
-
359
- except Exception as e:
360
- logger.error(f"Error procesando par de análisis: {str(e)}")
361
- continue
362
-
363
- except Exception as e:
364
- logger.error(f"Error mostrando actividades de situación actual: {str(e)}")
365
- st.error(t.get('error_current_situation', 'Error al mostrar análisis de situación actual'))
366
-
367
- ###############################################################################################
368
-
369
- def display_morphosyntax_activities(username: str, t: dict):
370
- """Muestra actividades de análisis morfosintáctico"""
371
- try:
372
- analyses = get_student_morphosyntax_analysis(username)
373
- if not analyses:
374
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
375
- return
376
-
377
- for analysis in analyses:
378
- with st.expander(
379
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
380
- expanded=False
381
- ):
382
- st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
383
- st.write(analysis['text'])
384
-
385
- if 'arc_diagrams' in analysis:
386
- st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
387
- for diagram in analysis['arc_diagrams']:
388
- st.write(diagram, unsafe_allow_html=True)
389
-
390
- except Exception as e:
391
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
392
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
393
-
394
-
395
- ###############################################################################################
396
-
397
- def display_semantic_activities(username: str, t: dict):
398
- """Muestra actividades de análisis semántico"""
399
- try:
400
- logger.info(f"Recuperando análisis semántico para {username}")
401
- analyses = get_student_semantic_analysis(username)
402
-
403
- if not analyses:
404
- logger.info("No se encontraron análisis semánticos")
405
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
406
- return
407
-
408
- logger.info(f"Procesando {len(analyses)} análisis semánticos")
409
-
410
- for analysis in analyses:
411
- try:
412
- # Verificar campos necesarios
413
- if not all(key in analysis for key in ['timestamp', 'concept_graph']):
414
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
415
- continue
416
-
417
- # Formatear fecha
418
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
419
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
420
-
421
- # Crear expander
422
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
423
- # Procesar y mostrar gráfico
424
- if analysis.get('concept_graph'):
425
- try:
426
- # Convertir de base64 a bytes
427
- logger.debug("Decodificando gráfico de conceptos")
428
- image_data = analysis['concept_graph']
429
-
430
- # Si el gráfico ya es bytes, usarlo directamente
431
- if isinstance(image_data, bytes):
432
- image_bytes = image_data
433
- else:
434
- # Si es string base64, decodificar
435
- image_bytes = base64.b64decode(image_data)
436
-
437
- logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
438
-
439
- # Mostrar imagen
440
- st.image(
441
- image_bytes,
442
- caption=t.get('concept_network', 'Red de Conceptos'),
443
- use_column_width=True
444
- )
445
- logger.debug("Gráfico mostrado exitosamente")
446
-
447
- except Exception as img_error:
448
- logger.error(f"Error procesando gráfico: {str(img_error)}")
449
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
450
- else:
451
- st.info(t.get('no_graph', 'No hay visualización disponible'))
452
-
453
- except Exception as e:
454
- logger.error(f"Error procesando análisis individual: {str(e)}")
455
- continue
456
-
457
- except Exception as e:
458
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
459
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
460
-
461
-
462
- ###################################################################################################
463
- def display_discourse_activities(username: str, t: dict):
464
- """Muestra actividades de análisis del discurso"""
465
- try:
466
- logger.info(f"Recuperando análisis del discurso para {username}")
467
- analyses = get_student_discourse_analysis(username)
468
-
469
- if not analyses:
470
- logger.info("No se encontraron análisis del discurso")
471
- st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
472
- return
473
-
474
- logger.info(f"Procesando {len(analyses)} análisis del discurso")
475
- for analysis in analyses:
476
- try:
477
- # Verificar campos mínimos necesarios
478
- if not all(key in analysis for key in ['timestamp', 'combined_graph']):
479
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
480
- continue
481
-
482
- # Formatear fecha
483
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
484
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
485
-
486
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
487
- if analysis['combined_graph']:
488
- logger.debug("Decodificando gráfico combinado")
489
- try:
490
- image_bytes = base64.b64decode(analysis['combined_graph'])
491
- st.image(image_bytes, use_column_width=True)
492
- logger.debug("Gráfico mostrado exitosamente")
493
- except Exception as img_error:
494
- logger.error(f"Error decodificando imagen: {str(img_error)}")
495
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
496
- else:
497
- st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
498
-
499
- except Exception as e:
500
- logger.error(f"Error procesando análisis individual: {str(e)}")
501
- continue
502
-
503
- except Exception as e:
504
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
505
- st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
506
-
507
- #################################################################################
508
- def display_chat_activities(username: str, t: dict):
509
- """
510
- Muestra historial de conversaciones del chat
511
- """
512
- try:
513
- # Obtener historial del chat
514
- chat_history = get_chat_history(
515
- username=username,
516
- analysis_type='sidebar',
517
- limit=50
518
- )
519
-
520
- if not chat_history:
521
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
522
- return
523
-
524
- for chat in reversed(chat_history): # Mostrar las más recientes primero
525
- try:
526
- # Convertir timestamp a datetime para formato
527
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
528
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
529
-
530
- with st.expander(
531
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
532
- expanded=False
533
- ):
534
- if 'messages' in chat and chat['messages']:
535
- # Mostrar cada mensaje en la conversación
536
- for message in chat['messages']:
537
- role = message.get('role', 'unknown')
538
- content = message.get('content', '')
539
-
540
- # Usar el componente de chat de Streamlit
541
- with st.chat_message(role):
542
- st.markdown(content)
543
-
544
- # Agregar separador entre mensajes
545
- st.divider()
546
- else:
547
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
548
-
549
- except Exception as e:
550
- logger.error(f"Error mostrando conversación: {str(e)}")
551
- continue
552
-
553
- except Exception as e:
554
- logger.error(f"Error mostrando historial del chat: {str(e)}")
555
- st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
556
-
557
- #################################################################################
558
- def display_discourse_comparison(analysis: dict, t: dict):
559
- """Muestra la comparación de análisis del discurso"""
560
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
561
-
562
- col1, col2 = st.columns(2)
563
- with col1:
564
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
565
- df1 = pd.DataFrame(analysis['key_concepts1'])
566
- st.dataframe(df1)
567
-
568
- with col2:
569
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
570
- df2 = pd.DataFrame(analysis['key_concepts2'])
571
- st.dataframe(df2)
 
 
 
 
 
 
1
+ ##############
2
+ ###modules/studentact/student_activities_v2.py
3
+
4
+ import streamlit as st
5
+ import re
6
+ import io
7
+ from io import BytesIO
8
+ import pandas as pd
9
+ import numpy as np
10
+ import time
11
+ import matplotlib.pyplot as plt
12
+ from datetime import datetime, timedelta
13
+ from spacy import displacy
14
+ import random
15
+ import base64
16
+ import seaborn as sns
17
+ import logging
18
+
19
+ # Importaciones de la base de datos
20
+ from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
+ from ..database.semantic_mongo_db import get_student_semantic_analysis
22
+ from ..database.discourse_mongo_db import get_student_discourse_analysis
23
+ from ..database.chat_mongo_db import get_chat_history
24
+ from ..database.current_situation_mongo_db import get_current_situation_analysis
25
+ from ..database.claude_recommendations_mongo_db import get_claude_recommendations
26
+
27
+ # Importar la función generate_unique_key
28
+ from ..utils.widget_utils import generate_unique_key
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ ###################################################################################
33
+
34
+ def display_student_activities(username: str, lang_code: str, t: dict):
35
+ """
36
+ Muestra todas las actividades del estudiante
37
+ Args:
38
+ username: Nombre del estudiante
39
+ lang_code: Código del idioma
40
+ t: Diccionario de traducciones
41
+ """
42
+ try:
43
+ # Cambiado de "Mis Actividades" a "Registro de mis actividades"
44
+ #st.header(t.get('activities_title', 'Registro de mis actividades'))
45
+
46
+ # Tabs para diferentes tipos de análisis
47
+ # Cambiado "Análisis del Discurso" a "Análisis comparado de textos"
48
+ tabs = st.tabs([
49
+ t.get('current_situation_activities', 'Registros de la función: Mi Situación Actual'),
50
+ t.get('morpho_activities', 'Registros de mis análisis morfosintácticos'),
51
+ t.get('semantic_activities', 'Registros de mis análisis semánticos'),
52
+ t.get('discourse_activities', 'Registros de mis análisis comparado de textos'),
53
+ t.get('chat_activities', 'Registros de mis conversaciones con el tutor virtual')
54
+ ])
55
+
56
+ # Tab de Situación Actual
57
+ with tabs[0]:
58
+ display_current_situation_activities(username, t)
59
+
60
+ # Tab de Análisis Morfosintáctico
61
+ with tabs[1]:
62
+ display_morphosyntax_activities(username, t)
63
+
64
+ # Tab de Análisis Semántico
65
+ with tabs[2]:
66
+ display_semantic_activities(username, t)
67
+
68
+ # Tab de Análisis del Discurso (mantiene nombre interno pero UI muestra "Análisis comparado de textos")
69
+ with tabs[3]:
70
+ display_discourse_activities(username, t)
71
+
72
+ # Tab de Conversaciones del Chat
73
+ with tabs[4]:
74
+ display_chat_activities(username, t)
75
+
76
+ except Exception as e:
77
+ logger.error(f"Error mostrando actividades: {str(e)}")
78
+ st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
79
+
80
+
81
+ ###############################################################################################
82
+
83
+ def display_current_situation_activities(username: str, t: dict):
84
+ """
85
+ Muestra análisis de situación actual junto con las recomendaciones de Claude
86
+ unificando la información de ambas colecciones y emparejándolas por cercanía temporal.
87
+ """
88
+ try:
89
+ # Recuperar datos de ambas colecciones
90
+ logger.info(f"Recuperando análisis de situación actual para {username}")
91
+ situation_analyses = get_current_situation_analysis(username, limit=10)
92
+
93
+ # Verificar si hay datos
94
+ if situation_analyses:
95
+ logger.info(f"Recuperados {len(situation_analyses)} análisis de situación")
96
+ # Depurar para ver la estructura de datos
97
+ for i, analysis in enumerate(situation_analyses):
98
+ logger.info(f"Análisis #{i+1}: Claves disponibles: {list(analysis.keys())}")
99
+ if 'metrics' in analysis:
100
+ logger.info(f"Métricas disponibles: {list(analysis['metrics'].keys())}")
101
+ else:
102
+ logger.warning("No se encontraron análisis de situación actual")
103
+
104
+ logger.info(f"Recuperando recomendaciones de Claude para {username}")
105
+ claude_recommendations = get_claude_recommendations(username)
106
+
107
+ if claude_recommendations:
108
+ logger.info(f"Recuperadas {len(claude_recommendations)} recomendaciones de Claude")
109
+ else:
110
+ logger.warning("No se encontraron recomendaciones de Claude")
111
+
112
+ # Verificar si hay algún tipo de análisis disponible
113
+ if not situation_analyses and not claude_recommendations:
114
+ logger.info("No se encontraron análisis de situación actual ni recomendaciones")
115
+ st.info(t.get('no_current_situation', 'No hay análisis de situación actual registrados'))
116
+ return
117
+
118
+ # Crear pares combinados emparejando diagnósticos y recomendaciones cercanos en tiempo
119
+ logger.info("Creando emparejamientos temporales de análisis")
120
+
121
+ # Convertir timestamps a objetos datetime para comparación
122
+ situation_times = []
123
+ for analysis in situation_analyses:
124
+ if 'timestamp' in analysis:
125
+ try:
126
+ timestamp_str = analysis['timestamp']
127
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
128
+ situation_times.append((dt, analysis))
129
+ except Exception as e:
130
+ logger.error(f"Error parseando timestamp de situación: {str(e)}")
131
+
132
+ recommendation_times = []
133
+ for recommendation in claude_recommendations:
134
+ if 'timestamp' in recommendation:
135
+ try:
136
+ timestamp_str = recommendation['timestamp']
137
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
138
+ recommendation_times.append((dt, recommendation))
139
+ except Exception as e:
140
+ logger.error(f"Error parseando timestamp de recomendación: {str(e)}")
141
+
142
+ # Ordenar por tiempo
143
+ situation_times.sort(key=lambda x: x[0], reverse=True)
144
+ recommendation_times.sort(key=lambda x: x[0], reverse=True)
145
+
146
+ # Crear pares combinados
147
+ combined_items = []
148
+
149
+ # Primero, procesar todas las situaciones encontrando la recomendación más cercana
150
+ for sit_time, situation in situation_times:
151
+ # Buscar la recomendación más cercana en tiempo
152
+ best_match = None
153
+ min_diff = timedelta(minutes=30) # Máxima diferencia de tiempo aceptable (30 minutos)
154
+ best_rec_time = None
155
+
156
+ for rec_time, recommendation in recommendation_times:
157
+ time_diff = abs(sit_time - rec_time)
158
+ if time_diff < min_diff:
159
+ min_diff = time_diff
160
+ best_match = recommendation
161
+ best_rec_time = rec_time
162
+
163
+ # Crear un elemento combinado
164
+ if best_match:
165
+ timestamp_key = sit_time.isoformat()
166
+ combined_items.append((timestamp_key, {
167
+ 'situation': situation,
168
+ 'recommendation': best_match,
169
+ 'time_diff': min_diff.total_seconds()
170
+ }))
171
+ # Eliminar la recomendación usada para no reutilizarla
172
+ recommendation_times = [(t, r) for t, r in recommendation_times if t != best_rec_time]
173
+ logger.info(f"Emparejado: Diagnóstico {sit_time} con Recomendación {best_rec_time} (diferencia: {min_diff})")
174
+ else:
175
+ # Si no hay recomendación cercana, solo incluir la situación
176
+ timestamp_key = sit_time.isoformat()
177
+ combined_items.append((timestamp_key, {
178
+ 'situation': situation
179
+ }))
180
+ logger.info(f"Sin emparejar: Diagnóstico {sit_time} sin recomendación cercana")
181
+
182
+ # Agregar recomendaciones restantes sin situación
183
+ for rec_time, recommendation in recommendation_times:
184
+ timestamp_key = rec_time.isoformat()
185
+ combined_items.append((timestamp_key, {
186
+ 'recommendation': recommendation
187
+ }))
188
+ logger.info(f"Sin emparejar: Recomendación {rec_time} sin diagnóstico cercano")
189
+
190
+ # Ordenar por tiempo (más reciente primero)
191
+ combined_items.sort(key=lambda x: x[0], reverse=True)
192
+
193
+ logger.info(f"Procesando {len(combined_items)} elementos combinados")
194
+
195
+ # Mostrar cada par combinado
196
+ for i, (timestamp_key, analysis_pair) in enumerate(combined_items):
197
+ try:
198
+ # Obtener datos de situación y recomendación
199
+ situation_data = analysis_pair.get('situation', {})
200
+ recommendation_data = analysis_pair.get('recommendation', {})
201
+ time_diff = analysis_pair.get('time_diff')
202
+
203
+ # Si no hay ningún dato, continuar al siguiente
204
+ if not situation_data and not recommendation_data:
205
+ continue
206
+
207
+ # Determinar qué texto mostrar (priorizar el de la situación)
208
+ text_to_show = situation_data.get('text', recommendation_data.get('text', ''))
209
+ text_type = situation_data.get('text_type', recommendation_data.get('text_type', ''))
210
+
211
+ # Formatear fecha para mostrar
212
+ try:
213
+ # Usar timestamp del key que ya es un formato ISO
214
+ dt = datetime.fromisoformat(timestamp_key)
215
+ formatted_date = dt.strftime("%d/%m/%Y %H:%M:%S")
216
+ except Exception as date_error:
217
+ logger.error(f"Error formateando fecha: {str(date_error)}")
218
+ formatted_date = timestamp_key
219
+
220
+ # Determinar el título del expander
221
+ title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
222
+ if text_type:
223
+ text_type_display = {
224
+ 'academic_article': t.get('academic_article', 'Artículo académico'),
225
+ 'student_essay': t.get('student_essay', 'Trabajo universitario'),
226
+ 'general_communication': t.get('general_communication', 'Comunicación general')
227
+ }.get(text_type, text_type)
228
+ title += f" - {text_type_display}"
229
+
230
+ # Añadir indicador de emparejamiento si existe
231
+ if time_diff is not None:
232
+ if time_diff < 60: # menos de un minuto
233
+ title += f" 🔄 (emparejados)"
234
+ else:
235
+ title += f" 🔄 (emparejados, diferencia: {int(time_diff//60)} min)"
236
+
237
+ # Usar un ID único para cada expander
238
+ expander_id = f"analysis_{i}_{timestamp_key.replace(':', '_')}"
239
+
240
+ # Mostrar el análisis en un expander
241
+ with st.expander(title, expanded=False):
242
+ # Mostrar texto analizado con key único
243
+ st.subheader(t.get('analyzed_text', 'Texto analizado'))
244
+ st.text_area(
245
+ "Text Content",
246
+ value=text_to_show,
247
+ height=100,
248
+ disabled=True,
249
+ label_visibility="collapsed",
250
+ key=f"text_area_{expander_id}"
251
+ )
252
+
253
+ # Crear tabs para separar diagnóstico y recomendaciones
254
+ diagnosis_tab, recommendations_tab = st.tabs([
255
+ t.get('diagnosis_tab', 'Diagnóstico'),
256
+ t.get('recommendations_tab', 'Recomendaciones')
257
+ ])
258
+
259
+ # Tab de diagnóstico
260
+ with diagnosis_tab:
261
+ if situation_data and 'metrics' in situation_data:
262
+ metrics = situation_data['metrics']
263
+
264
+ # Dividir en dos columnas
265
+ col1, col2 = st.columns(2)
266
+
267
+ # Principales métricas en formato de tarjetas
268
+ with col1:
269
+ st.subheader(t.get('key_metrics', 'Métricas clave'))
270
+
271
+ # Mostrar cada métrica principal
272
+ for metric_name, metric_data in metrics.items():
273
+ try:
274
+ # Determinar la puntuación
275
+ score = None
276
+ if isinstance(metric_data, dict):
277
+ # Intentar diferentes nombres de campo
278
+ if 'normalized_score' in metric_data:
279
+ score = metric_data['normalized_score']
280
+ elif 'score' in metric_data:
281
+ score = metric_data['score']
282
+ elif 'value' in metric_data:
283
+ score = metric_data['value']
284
+ elif isinstance(metric_data, (int, float)):
285
+ score = metric_data
286
+
287
+ if score is not None:
288
+ # Asegurarse de que score es numérico
289
+ if isinstance(score, (int, float)):
290
+ # Determinar color y emoji basado en la puntuación
291
+ if score < 0.5:
292
+ emoji = "🔴"
293
+ color = "#ffcccc" # light red
294
+ elif score < 0.75:
295
+ emoji = "🟡"
296
+ color = "#ffffcc" # light yellow
297
+ else:
298
+ emoji = "🟢"
299
+ color = "#ccffcc" # light green
300
+
301
+ # Mostrar la métrica con estilo
302
+ st.markdown(f"""
303
+ <div style="background-color:{color}; padding:10px; border-radius:5px; margin-bottom:10px;">
304
+ <b>{emoji} {metric_name.capitalize()}:</b> {score:.2f}
305
+ </div>
306
+ """, unsafe_allow_html=True)
307
+ else:
308
+ # Si no es numérico, mostrar como texto
309
+ st.markdown(f"""
310
+ <div style="background-color:#f0f0f0; padding:10px; border-radius:5px; margin-bottom:10px;">
311
+ <b>ℹ️ {metric_name.capitalize()}:</b> {str(score)}
312
+ </div>
313
+ """, unsafe_allow_html=True)
314
+ except Exception as e:
315
+ logger.error(f"Error procesando métrica {metric_name}: {str(e)}")
316
+
317
+ # Mostrar detalles adicionales si están disponibles
318
+ with col2:
319
+ st.subheader(t.get('details', 'Detalles'))
320
+
321
+ # Para cada métrica, mostrar sus detalles si existen
322
+ for metric_name, metric_data in metrics.items():
323
+ try:
324
+ if isinstance(metric_data, dict):
325
+ # Mostrar detalles directamente o buscar en subcampos
326
+ details = None
327
+ if 'details' in metric_data and metric_data['details']:
328
+ details = metric_data['details']
329
+ else:
330
+ # Crear un diccionario con los detalles excluyendo 'normalized_score' y similares
331
+ details = {k: v for k, v in metric_data.items()
332
+ if k not in ['normalized_score', 'score', 'value']}
333
+
334
+ if details:
335
+ st.write(f"**{metric_name.capitalize()}**")
336
+ st.json(details, expanded=False)
337
+ except Exception as e:
338
+ logger.error(f"Error mostrando detalles de {metric_name}: {str(e)}")
339
+ else:
340
+ st.info(t.get('no_diagnosis', 'No hay datos de diagnóstico disponibles'))
341
+
342
+ # Tab de recomendaciones
343
+ with recommendations_tab:
344
+ if recommendation_data and 'recommendations' in recommendation_data:
345
+ st.markdown(f"""
346
+ <div style="padding: 20px; border-radius: 10px;
347
+ background-color: #f8f9fa; margin-bottom: 20px;">
348
+ {recommendation_data['recommendations']}
349
+ </div>
350
+ """, unsafe_allow_html=True)
351
+ elif recommendation_data and 'feedback' in recommendation_data:
352
+ st.markdown(f"""
353
+ <div style="padding: 20px; border-radius: 10px;
354
+ background-color: #f8f9fa; margin-bottom: 20px;">
355
+ {recommendation_data['feedback']}
356
+ </div>
357
+ """, unsafe_allow_html=True)
358
+ else:
359
+ st.info(t.get('no_recommendations', 'No hay recomendaciones disponibles'))
360
+
361
+ except Exception as e:
362
+ logger.error(f"Error procesando par de análisis: {str(e)}")
363
+ continue
364
+
365
+ except Exception as e:
366
+ logger.error(f"Error mostrando actividades de situación actual: {str(e)}")
367
+ st.error(t.get('error_current_situation', 'Error al mostrar análisis de situación actual'))
368
+
369
+ ###############################################################################################
370
+
371
+ def display_morphosyntax_activities(username: str, t: dict):
372
+ """Muestra actividades de análisis morfosintáctico"""
373
+ try:
374
+ analyses = get_student_morphosyntax_analysis(username)
375
+ if not analyses:
376
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
377
+ return
378
+
379
+ for analysis in analyses:
380
+ with st.expander(
381
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
382
+ expanded=False
383
+ ):
384
+ st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
385
+ st.write(analysis['text'])
386
+
387
+ if 'arc_diagrams' in analysis:
388
+ st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
389
+ for diagram in analysis['arc_diagrams']:
390
+ st.write(diagram, unsafe_allow_html=True)
391
+
392
+ except Exception as e:
393
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
394
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
395
+
396
+
397
+ ###############################################################################################
398
+
399
+ def display_semantic_activities(username: str, t: dict):
400
+ """Muestra actividades de análisis semántico"""
401
+ try:
402
+ logger.info(f"Recuperando análisis semántico para {username}")
403
+ analyses = get_student_semantic_analysis(username)
404
+
405
+ if not analyses:
406
+ logger.info("No se encontraron análisis semánticos")
407
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
408
+ return
409
+
410
+ logger.info(f"Procesando {len(analyses)} análisis semánticos")
411
+
412
+ for analysis in analyses:
413
+ try:
414
+ # Verificar campos necesarios
415
+ if not all(key in analysis for key in ['timestamp', 'concept_graph']):
416
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
417
+ continue
418
+
419
+ # Formatear fecha
420
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
421
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
422
+
423
+ # Crear expander
424
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
425
+ # Procesar y mostrar gráfico
426
+ if analysis.get('concept_graph'):
427
+ try:
428
+ # Convertir de base64 a bytes
429
+ logger.debug("Decodificando gráfico de conceptos")
430
+ image_data = analysis['concept_graph']
431
+
432
+ # Si el gráfico ya es bytes, usarlo directamente
433
+ if isinstance(image_data, bytes):
434
+ image_bytes = image_data
435
+ else:
436
+ # Si es string base64, decodificar
437
+ image_bytes = base64.b64decode(image_data)
438
+
439
+ logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
440
+
441
+ # Mostrar imagen
442
+ st.image(
443
+ image_bytes,
444
+ caption=t.get('concept_network', 'Red de Conceptos'),
445
+ use_column_width=True
446
+ )
447
+ logger.debug("Gráfico mostrado exitosamente")
448
+
449
+ except Exception as img_error:
450
+ logger.error(f"Error procesando gráfico: {str(img_error)}")
451
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
452
+ else:
453
+ st.info(t.get('no_graph', 'No hay visualización disponible'))
454
+
455
+ except Exception as e:
456
+ logger.error(f"Error procesando análisis individual: {str(e)}")
457
+ continue
458
+
459
+ except Exception as e:
460
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
461
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
462
+
463
+
464
+ ###################################################################################################
465
+ def display_discourse_activities(username: str, t: dict):
466
+ """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
467
+ try:
468
+ logger.info(f"Recuperando análisis del discurso para {username}")
469
+ analyses = get_student_discourse_analysis(username)
470
+
471
+ if not analyses:
472
+ logger.info("No se encontraron análisis del discurso")
473
+ # Usamos el término "análisis comparado de textos" en la UI
474
+ st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
475
+ return
476
+
477
+ logger.info(f"Procesando {len(analyses)} análisis del discurso")
478
+ for analysis in analyses:
479
+ try:
480
+ # Verificar campos mínimos necesarios
481
+ if not all(key in analysis for key in ['timestamp', 'combined_graph']):
482
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
483
+ continue
484
+
485
+ # Formatear fecha
486
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
487
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
488
+
489
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
490
+ if analysis['combined_graph']:
491
+ logger.debug("Decodificando gráfico combinado")
492
+ try:
493
+ image_bytes = base64.b64decode(analysis['combined_graph'])
494
+ st.image(image_bytes, use_column_width=True)
495
+ logger.debug("Gráfico mostrado exitosamente")
496
+ except Exception as img_error:
497
+ logger.error(f"Error decodificando imagen: {str(img_error)}")
498
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
499
+ else:
500
+ st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
501
+
502
+ except Exception as e:
503
+ logger.error(f"Error procesando análisis individual: {str(e)}")
504
+ continue
505
+
506
+ except Exception as e:
507
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
508
+ # Usamos el término "análisis comparado de textos" en la UI
509
+ st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
510
+
511
+ #################################################################################
512
+ def display_chat_activities(username: str, t: dict):
513
+ """
514
+ Muestra historial de conversaciones del chat
515
+ """
516
+ try:
517
+ # Obtener historial del chat
518
+ chat_history = get_chat_history(
519
+ username=username,
520
+ analysis_type='sidebar',
521
+ limit=50
522
+ )
523
+
524
+ if not chat_history:
525
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
526
+ return
527
+
528
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
529
+ try:
530
+ # Convertir timestamp a datetime para formato
531
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
532
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
533
+
534
+ with st.expander(
535
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
536
+ expanded=False
537
+ ):
538
+ if 'messages' in chat and chat['messages']:
539
+ # Mostrar cada mensaje en la conversación
540
+ for message in chat['messages']:
541
+ role = message.get('role', 'unknown')
542
+ content = message.get('content', '')
543
+
544
+ # Usar el componente de chat de Streamlit
545
+ with st.chat_message(role):
546
+ st.markdown(content)
547
+
548
+ # Agregar separador entre mensajes
549
+ st.divider()
550
+ else:
551
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
552
+
553
+ except Exception as e:
554
+ logger.error(f"Error mostrando conversación: {str(e)}")
555
+ continue
556
+
557
+ except Exception as e:
558
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
559
+ st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
560
+
561
+ #################################################################################
562
+ def display_discourse_comparison(analysis: dict, t: dict):
563
+ """Muestra la comparación de análisis del discurso"""
564
+ # Cambiado para usar "textos comparados" en la UI
565
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
566
+
567
+ col1, col2 = st.columns(2)
568
+ with col1:
569
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
570
+ df1 = pd.DataFrame(analysis['key_concepts1'])
571
+ st.dataframe(df1)
572
+
573
+ with col2:
574
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
575
+ df2 = pd.DataFrame(analysis['key_concepts2'])
576
+ st.dataframe(df2)