AIdeaText commited on
Commit
bae92bb
·
verified ·
1 Parent(s): c635dc7

Update modules/discourse/discourse_interface.py

Browse files
Files changed (1) hide show
  1. modules/discourse/discourse_interface.py +318 -318
modules/discourse/discourse_interface.py CHANGED
@@ -1,318 +1,318 @@
1
- # modules/discourse/discourse/discourse_interface.py
2
-
3
- import streamlit as st
4
- import pandas as pd
5
- import matplotlib.pyplot as plt
6
- import plotly.graph_objects as go
7
- import logging
8
- from ..utils.widget_utils import generate_unique_key
9
- from .discourse_process import perform_discourse_analysis
10
- from ..database.chat_mongo_db import store_chat_history
11
- from ..database.discourse_mongo_db import store_student_discourse_result
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- #############################################################################################
16
- def display_discourse_interface(lang_code, nlp_models, discourse_t):
17
- """
18
- Interfaz para el análisis del discurso
19
- Args:
20
- lang_code: Código del idioma actual
21
- nlp_models: Modelos de spaCy cargados
22
- discourse_t: Diccionario de traducciones
23
- """
24
- try:
25
- # 1. Inicializar estado si no existe
26
- if 'discourse_state' not in st.session_state:
27
- st.session_state.discourse_state = {
28
- 'analysis_count': 0,
29
- 'last_analysis': None,
30
- 'current_files': None
31
- }
32
-
33
- # 2. Título y descripción
34
- # st.subheader(discourse_t.get('discourse_title', 'Análisis del Discurso'))
35
- st.info(discourse_t.get('initial_instruction',
36
- 'Cargue dos archivos de texto para realizar un análisis comparativo del discurso.'))
37
-
38
- # 3. Área de carga de archivos
39
- col1, col2 = st.columns(2)
40
- with col1:
41
- st.markdown(discourse_t.get('file1_label', "**Documento 1 (Patrón)**"))
42
- uploaded_file1 = st.file_uploader(
43
- discourse_t.get('file_uploader1', "Cargar archivo 1"),
44
- type=['txt'],
45
- key=f"discourse_file1_{st.session_state.discourse_state['analysis_count']}"
46
- )
47
-
48
- with col2:
49
- st.markdown(discourse_t.get('file2_label', "**Documento 2 (Comparación)**"))
50
- uploaded_file2 = st.file_uploader(
51
- discourse_t.get('file_uploader2', "Cargar archivo 2"),
52
- type=['txt'],
53
- key=f"discourse_file2_{st.session_state.discourse_state['analysis_count']}"
54
- )
55
-
56
- # 4. Botón de análisis
57
- col1, col2, col3 = st.columns([1,2,1])
58
- with col1:
59
- analyze_button = st.button(
60
- discourse_t.get('discourse_analyze_button', 'Comparar textos'),
61
- key=generate_unique_key("discourse", "analyze_button"),
62
- type="primary",
63
- icon="🔍",
64
- disabled=not (uploaded_file1 and uploaded_file2),
65
- use_container_width=True
66
- )
67
-
68
- # 5. Proceso de análisis
69
- if analyze_button and uploaded_file1 and uploaded_file2:
70
- try:
71
- with st.spinner(discourse_t.get('processing', 'Procesando análisis...')):
72
- # Leer contenido de archivos
73
- text1 = uploaded_file1.getvalue().decode('utf-8')
74
- text2 = uploaded_file2.getvalue().decode('utf-8')
75
-
76
- # Realizar análisis
77
- result = perform_discourse_analysis(
78
- text1,
79
- text2,
80
- nlp_models[lang_code],
81
- lang_code
82
- )
83
-
84
- if result['success']:
85
- # Guardar estado
86
- st.session_state.discourse_result = result
87
- st.session_state.discourse_state['analysis_count'] += 1
88
- st.session_state.discourse_state['current_files'] = (
89
- uploaded_file1.name,
90
- uploaded_file2.name
91
- )
92
-
93
- # Guardar en base de datos
94
- if store_student_discourse_result(
95
- st.session_state.username,
96
- text1,
97
- text2,
98
- result
99
- ):
100
- st.success(discourse_t.get('success_message', 'Análisis guardado correctamente'))
101
-
102
- # Mostrar resultados
103
- display_discourse_results(result, lang_code, discourse_t)
104
- else:
105
- st.error(discourse_t.get('error_message', 'Error al guardar el análisis'))
106
- else:
107
- st.error(discourse_t.get('analysis_error', 'Error en el análisis'))
108
-
109
- except Exception as e:
110
- logger.error(f"Error en análisis del discurso: {str(e)}")
111
- st.error(discourse_t.get('error_processing', f'Error procesando archivos: {str(e)}'))
112
-
113
- # 6. Mostrar resultados previos
114
- elif 'discourse_result' in st.session_state and st.session_state.discourse_result is not None:
115
- if st.session_state.discourse_state.get('current_files'):
116
- st.info(
117
- discourse_t.get('current_analysis_message', 'Mostrando análisis de los archivos: {} y {}')
118
- .format(*st.session_state.discourse_state['current_files'])
119
- )
120
- display_discourse_results(
121
- st.session_state.discourse_result,
122
- lang_code,
123
- discourse_t
124
- )
125
-
126
- except Exception as e:
127
- logger.error(f"Error general en interfaz del discurso: {str(e)}")
128
- st.error(discourse_t.get('general_error', 'Se produjo un error. Por favor, intente de nuevo.'))
129
-
130
-
131
-
132
- #####################################################################################################################
133
- def display_discourse_results(result, lang_code, discourse_t):
134
- """
135
- Muestra los resultados del análisis del discurso
136
- """
137
- if not result.get('success'):
138
- st.warning(discourse_t.get('no_results', 'No hay resultados disponibles'))
139
- return
140
-
141
- # Estilo CSS
142
- st.markdown("""
143
- <style>
144
- .concepts-container {
145
- display: flex;
146
- flex-wrap: nowrap;
147
- gap: 8px;
148
- padding: 12px;
149
- background-color: #f8f9fa;
150
- border-radius: 8px;
151
- overflow-x: auto;
152
- margin-bottom: 15px;
153
- white-space: nowrap;
154
- }
155
- .concept-item {
156
- background-color: white;
157
- border-radius: 4px;
158
- padding: 6px 10px;
159
- display: inline-flex;
160
- align-items: center;
161
- gap: 4px;
162
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
163
- flex-shrink: 0;
164
- }
165
- .concept-name {
166
- font-weight: 500;
167
- color: #1f2937;
168
- font-size: 0.85em;
169
- }
170
- .concept-freq {
171
- color: #6b7280;
172
- font-size: 0.75em;
173
- }
174
- .graph-container {
175
- background-color: white;
176
- padding: 15px;
177
- border-radius: 8px;
178
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
179
- margin-top: 10px;
180
- }
181
- </style>
182
- """, unsafe_allow_html=True)
183
-
184
- col1, col2 = st.columns(2)
185
-
186
- # Documento 1
187
- with col1:
188
- st.subheader(discourse_t.get('doc1_title', 'Documento 1'))
189
- st.markdown(discourse_t.get('key_concepts', 'Conceptos Clave'))
190
- if 'key_concepts1' in result:
191
- concepts_html = f"""
192
- <div class="concepts-container">
193
- {''.join([
194
- f'<div class="concept-item"><span class="concept-name">{concept}</span>'
195
- f'<span class="concept-freq">({freq:.2f})</span></div>'
196
- for concept, freq in result['key_concepts1']
197
- ])}
198
- </div>
199
- """
200
- st.markdown(concepts_html, unsafe_allow_html=True)
201
-
202
- # Verificar el tipo de graph1 de manera más robusta
203
- if 'graph1' in result:
204
- st.markdown('<div class="graph-container">', unsafe_allow_html=True)
205
-
206
- # Más información para depuración
207
- graph_type = type(result['graph1']).__name__
208
- graph_size = len(result['graph1']) if isinstance(result['graph1'], bytes) else "N/A"
209
- logger.info(f"Tipo de graph1: {graph_type}, Tamaño: {graph_size}")
210
-
211
- if isinstance(result['graph1'], bytes) and len(result['graph1']) > 0:
212
- # Es bytes válidos
213
- st.image(result['graph1'])
214
- elif isinstance(result['graph1'], plt.Figure):
215
- # Es una figura de matplotlib
216
- st.pyplot(result['graph1'])
217
- elif result['graph1'] is None:
218
- # Es None
219
- st.warning("Gráfico no disponible")
220
- else:
221
- # Otro tipo o bytes vacíos
222
- st.warning(f"Formato de gráfico no reconocido: {graph_type}")
223
-
224
- # Botones y controles
225
- button_col1, spacer_col1 = st.columns([1,4])
226
- with button_col1:
227
- if 'graph1_bytes' in result:
228
- st.download_button(
229
- label="📥 " + discourse_t.get('download_graph', "Download"),
230
- data=result['graph1_bytes'],
231
- file_name="discourse_graph1.png",
232
- mime="image/png",
233
- use_container_width=True
234
- )
235
-
236
- # Interpretación como texto normal sin expander
237
- st.markdown("**📊 Interpretación del grafo:**")
238
- st.markdown("""
239
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
240
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
241
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
242
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
243
- """)
244
-
245
- st.markdown('</div>', unsafe_allow_html=True)
246
- else:
247
- st.warning(discourse_t.get('graph_not_available', 'Gráfico no disponible'))
248
- else:
249
- st.warning(discourse_t.get('concepts_not_available', 'Conceptos no disponibles'))
250
-
251
- # Documento 2
252
- with col2:
253
- st.subheader(discourse_t.get('doc2_title', 'Documento 2'))
254
- st.markdown(discourse_t.get('key_concepts', 'Conceptos Clave'))
255
- if 'key_concepts2' in result:
256
- concepts_html = f"""
257
- <div class="concepts-container">
258
- {''.join([
259
- f'<div class="concept-item"><span class="concept-name">{concept}</span>'
260
- f'<span class="concept-freq">({freq:.2f})</span></div>'
261
- for concept, freq in result['key_concepts2']
262
- ])}
263
- </div>
264
- """
265
- st.markdown(concepts_html, unsafe_allow_html=True)
266
-
267
- # Verificar el tipo de graph1 de manera más robusta
268
- if 'graph1' in result:
269
- st.markdown('<div class="graph-container">', unsafe_allow_html=True)
270
-
271
- # Más información para depuración
272
- graph_type = type(result['graph2']).__name__
273
- graph_size = len(result['graph2']) if isinstance(result['graph2'], bytes) else "N/A"
274
- logger.info(f"Tipo de graph2: {graph_type}, Tamaño: {graph_size}")
275
-
276
- if isinstance(result['graph2'], bytes) and len(result['graph2']) > 0:
277
- # Es bytes válidos
278
- st.image(result['graph2'])
279
- elif isinstance(result['graph2'], plt.Figure):
280
- # Es una figura de matplotlib
281
- st.pyplot(result['graph2'])
282
- elif result['graph2'] is None:
283
- # Es None
284
- st.warning("Gráfico no disponible")
285
- else:
286
- # Otro tipo o bytes vacíos
287
- st.warning(f"Formato de gráfico no reconocido: {graph_type}")
288
-
289
- # Botones y controles
290
- button_col2, spacer_col2 = st.columns([1,4])
291
- with button_col2:
292
- if 'graph2_bytes' in result:
293
- st.download_button(
294
- label="📥 " + discourse_t.get('download_graph', "Download"),
295
- data=result['graph2_bytes'],
296
- file_name="discourse_graph2.png",
297
- mime="image/png",
298
- use_container_width=True
299
- )
300
-
301
- # Interpretación como texto normal sin expander
302
- st.markdown("**📊 Interpretación del grafo:**")
303
- st.markdown("""
304
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
305
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
306
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
307
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
308
- """)
309
-
310
- st.markdown('</div>', unsafe_allow_html=True)
311
- else:
312
- st.warning(discourse_t.get('graph_not_available', 'Gráfico no disponible'))
313
- else:
314
- st.warning(discourse_t.get('concepts_not_available', 'Conceptos no disponibles'))
315
-
316
- # Nota informativa sobre la comparación
317
- st.info(discourse_t.get('comparison_note',
318
- 'La funcionalidad de comparación detallada estará disponible en una próxima actualización.'))
 
1
+ # modules/discourse/discourse/discourse_interface.py
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import matplotlib.pyplot as plt
6
+ import plotly.graph_objects as go
7
+ import logging
8
+ from ..utils.widget_utils import generate_unique_key
9
+ from .discourse_process import perform_discourse_analysis
10
+ from ..database.chat_mongo_db import store_chat_history
11
+ from ..database.discourse_mongo_db import store_student_discourse_result
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ #############################################################################################
16
+ def display_discourse_interface(lang_code, nlp_models, discourse_t):
17
+ """
18
+ Interfaz para el análisis del discurso
19
+ Args:
20
+ lang_code: Código del idioma actual
21
+ nlp_models: Modelos de spaCy cargados
22
+ discourse_t: Diccionario de traducciones
23
+ """
24
+ try:
25
+ # 1. Inicializar estado si no existe
26
+ if 'discourse_state' not in st.session_state:
27
+ st.session_state.discourse_state = {
28
+ 'analysis_count': 0,
29
+ 'last_analysis': None,
30
+ 'current_files': None
31
+ }
32
+
33
+ # 2. Título y descripción
34
+ # st.subheader(discourse_t.get('discourse_title', 'Análisis del Discurso'))
35
+ st.info(discourse_t.get('initial_instruction',
36
+ 'Cargue dos archivos de texto para realizar un análisis comparativo del discurso.'))
37
+
38
+ # 3. Área de carga de archivos
39
+ col1, col2 = st.columns(2)
40
+ with col1:
41
+ st.markdown(discourse_t.get('file1_label', "**Documento 1 (Patrón)**"))
42
+ uploaded_file1 = st.file_uploader(
43
+ discourse_t.get('file_uploader1', "Cargar archivo 1"),
44
+ type=['txt'],
45
+ key=f"discourse_file1_{st.session_state.discourse_state['analysis_count']}"
46
+ )
47
+
48
+ with col2:
49
+ st.markdown(discourse_t.get('file2_label', "**Documento 2 (Comparación)**"))
50
+ uploaded_file2 = st.file_uploader(
51
+ discourse_t.get('file_uploader2', "Cargar archivo 2"),
52
+ type=['txt'],
53
+ key=f"discourse_file2_{st.session_state.discourse_state['analysis_count']}"
54
+ )
55
+
56
+ # 4. Botón de análisis
57
+ col1, col2, col3 = st.columns([1,2,1])
58
+ with col1:
59
+ analyze_button = st.button(
60
+ discourse_t.get('discourse_analyze_button', 'Comparar textos'),
61
+ key=generate_unique_key("discourse", "analyze_button"),
62
+ type="primary",
63
+ icon="🔍",
64
+ disabled=not (uploaded_file1 and uploaded_file2),
65
+ use_container_width=True
66
+ )
67
+
68
+ # 5. Proceso de análisis
69
+ if analyze_button and uploaded_file1 and uploaded_file2:
70
+ try:
71
+ with st.spinner(discourse_t.get('processing', 'Procesando análisis...')):
72
+ # Leer contenido de archivos
73
+ text1 = uploaded_file1.getvalue().decode('utf-8')
74
+ text2 = uploaded_file2.getvalue().decode('utf-8')
75
+
76
+ # Realizar análisis
77
+ result = perform_discourse_analysis(
78
+ text1,
79
+ text2,
80
+ nlp_models[lang_code],
81
+ lang_code
82
+ )
83
+
84
+ if result['success']:
85
+ # Guardar estado
86
+ st.session_state.discourse_result = result
87
+ st.session_state.discourse_state['analysis_count'] += 1
88
+ st.session_state.discourse_state['current_files'] = (
89
+ uploaded_file1.name,
90
+ uploaded_file2.name
91
+ )
92
+
93
+ # Guardar en base de datos
94
+ if store_student_discourse_result(
95
+ st.session_state.username,
96
+ text1,
97
+ text2,
98
+ result
99
+ ):
100
+ st.success(discourse_t.get('success_message', 'Análisis guardado correctamente'))
101
+
102
+ # Mostrar resultados
103
+ display_discourse_results(result, lang_code, discourse_t)
104
+ else:
105
+ st.error(discourse_t.get('error_message', 'Error al guardar el análisis'))
106
+ else:
107
+ st.error(discourse_t.get('analysis_error', 'Error en el análisis'))
108
+
109
+ except Exception as e:
110
+ logger.error(f"Error en análisis del discurso: {str(e)}")
111
+ st.error(discourse_t.get('error_processing', f'Error procesando archivos: {str(e)}'))
112
+
113
+ # 6. Mostrar resultados previos
114
+ elif 'discourse_result' in st.session_state and st.session_state.discourse_result is not None:
115
+ if st.session_state.discourse_state.get('current_files'):
116
+ st.info(
117
+ discourse_t.get('current_analysis_message', 'Mostrando análisis de los archivos: {} y {}')
118
+ .format(*st.session_state.discourse_state['current_files'])
119
+ )
120
+ display_discourse_results(
121
+ st.session_state.discourse_result,
122
+ lang_code,
123
+ discourse_t
124
+ )
125
+
126
+ except Exception as e:
127
+ logger.error(f"Error general en interfaz del discurso: {str(e)}")
128
+ st.error(discourse_t.get('general_error', 'Se produjo un error. Por favor, intente de nuevo.'))
129
+
130
+
131
+
132
+ #####################################################################################################################
133
+ def display_discourse_results(result, lang_code, discourse_t):
134
+ """
135
+ Muestra los resultados del análisis del discurso
136
+ """
137
+ if not result.get('success'):
138
+ st.warning(discourse_t.get('no_results', 'No hay resultados disponibles'))
139
+ return
140
+
141
+ # Estilo CSS
142
+ st.markdown("""
143
+ <style>
144
+ .concepts-container {
145
+ display: flex;
146
+ flex-wrap: nowrap;
147
+ gap: 8px;
148
+ padding: 12px;
149
+ background-color: #f8f9fa;
150
+ border-radius: 8px;
151
+ overflow-x: auto;
152
+ margin-bottom: 15px;
153
+ white-space: nowrap;
154
+ }
155
+ .concept-item {
156
+ background-color: white;
157
+ border-radius: 4px;
158
+ padding: 6px 10px;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 4px;
162
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
163
+ flex-shrink: 0;
164
+ }
165
+ .concept-name {
166
+ font-weight: 500;
167
+ color: #1f2937;
168
+ font-size: 0.85em;
169
+ }
170
+ .concept-freq {
171
+ color: #6b7280;
172
+ font-size: 0.75em;
173
+ }
174
+ .graph-container {
175
+ background-color: white;
176
+ padding: 15px;
177
+ border-radius: 8px;
178
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
179
+ margin-top: 10px;
180
+ }
181
+ </style>
182
+ """, unsafe_allow_html=True)
183
+
184
+ col1, col2 = st.columns(2)
185
+
186
+ # Documento 1
187
+ with col1:
188
+ st.subheader(discourse_t.get('compare_doc1_title', 'Documento 1'))
189
+ st.markdown(discourse_t.get('key_concepts', 'Conceptos Clave'))
190
+ if 'key_concepts1' in result:
191
+ concepts_html = f"""
192
+ <div class="concepts-container">
193
+ {''.join([
194
+ f'<div class="concept-item"><span class="concept-name">{concept}</span>'
195
+ f'<span class="concept-freq">({freq:.2f})</span></div>'
196
+ for concept, freq in result['key_concepts1']
197
+ ])}
198
+ </div>
199
+ """
200
+ st.markdown(concepts_html, unsafe_allow_html=True)
201
+
202
+ # Verificar el tipo de graph1 de manera más robusta
203
+ if 'graph1' in result:
204
+ st.markdown('<div class="graph-container">', unsafe_allow_html=True)
205
+
206
+ # Más información para depuración
207
+ graph_type = type(result['graph1']).__name__
208
+ graph_size = len(result['graph1']) if isinstance(result['graph1'], bytes) else "N/A"
209
+ logger.info(f"Tipo de graph1: {graph_type}, Tamaño: {graph_size}")
210
+
211
+ if isinstance(result['graph1'], bytes) and len(result['graph1']) > 0:
212
+ # Es bytes válidos
213
+ st.image(result['graph1'])
214
+ elif isinstance(result['graph1'], plt.Figure):
215
+ # Es una figura de matplotlib
216
+ st.pyplot(result['graph1'])
217
+ elif result['graph1'] is None:
218
+ # Es None
219
+ st.warning("Gráfico no disponible")
220
+ else:
221
+ # Otro tipo o bytes vacíos
222
+ st.warning(f"Formato de gráfico no reconocido: {graph_type}")
223
+
224
+ # Botones y controles
225
+ button_col1, spacer_col1 = st.columns([1,4])
226
+ with button_col1:
227
+ if 'graph1_bytes' in result:
228
+ st.download_button(
229
+ label="📥 " + discourse_t.get('download_graph', "Download"),
230
+ data=result['graph1_bytes'],
231
+ file_name="discourse_graph1.png",
232
+ mime="image/png",
233
+ use_container_width=True
234
+ )
235
+
236
+ # Interpretación como texto normal sin expander
237
+ st.markdown("**📊 Interpretación del grafo:**")
238
+ st.markdown("""
239
+ - 🔀 Las flechas indican la dirección de la relación entre conceptos
240
+ - 🎨 Los colores más intensos indican conceptos más centrales en el texto
241
+ - ⭕ El tamaño de los nodos representa la frecuencia del concepto
242
+ - ↔️ El grosor de las líneas indica la fuerza de la conexión
243
+ """)
244
+
245
+ st.markdown('</div>', unsafe_allow_html=True)
246
+ else:
247
+ st.warning(discourse_t.get('graph_not_available', 'Gráfico no disponible'))
248
+ else:
249
+ st.warning(discourse_t.get('concepts_not_available', 'Conceptos no disponibles'))
250
+
251
+ # Documento 2
252
+ with col2:
253
+ st.subheader(discourse_t.get('compare_doc2_title', 'Documento 2'))
254
+ st.markdown(discourse_t.get('key_concepts', 'Conceptos Clave'))
255
+ if 'key_concepts2' in result:
256
+ concepts_html = f"""
257
+ <div class="concepts-container">
258
+ {''.join([
259
+ f'<div class="concept-item"><span class="concept-name">{concept}</span>'
260
+ f'<span class="concept-freq">({freq:.2f})</span></div>'
261
+ for concept, freq in result['key_concepts2']
262
+ ])}
263
+ </div>
264
+ """
265
+ st.markdown(concepts_html, unsafe_allow_html=True)
266
+
267
+ # Verificar el tipo de graph1 de manera más robusta
268
+ if 'graph1' in result:
269
+ st.markdown('<div class="graph-container">', unsafe_allow_html=True)
270
+
271
+ # Más información para depuración
272
+ graph_type = type(result['graph2']).__name__
273
+ graph_size = len(result['graph2']) if isinstance(result['graph2'], bytes) else "N/A"
274
+ logger.info(f"Tipo de graph2: {graph_type}, Tamaño: {graph_size}")
275
+
276
+ if isinstance(result['graph2'], bytes) and len(result['graph2']) > 0:
277
+ # Es bytes válidos
278
+ st.image(result['graph2'])
279
+ elif isinstance(result['graph2'], plt.Figure):
280
+ # Es una figura de matplotlib
281
+ st.pyplot(result['graph2'])
282
+ elif result['graph2'] is None:
283
+ # Es None
284
+ st.warning("Gráfico no disponible")
285
+ else:
286
+ # Otro tipo o bytes vacíos
287
+ st.warning(f"Formato de gráfico no reconocido: {graph_type}")
288
+
289
+ # Botones y controles
290
+ button_col2, spacer_col2 = st.columns([1,4])
291
+ with button_col2:
292
+ if 'graph2_bytes' in result:
293
+ st.download_button(
294
+ label="📥 " + discourse_t.get('download_graph', "Download"),
295
+ data=result['graph2_bytes'],
296
+ file_name="discourse_graph2.png",
297
+ mime="image/png",
298
+ use_container_width=True
299
+ )
300
+
301
+ # Interpretación como texto normal sin expander
302
+ st.markdown("**📊 Interpretación del grafo:**")
303
+ st.markdown("""
304
+ - 🔀 Las flechas indican la dirección de la relación entre conceptos
305
+ - 🎨 Los colores más intensos indican conceptos más centrales en el texto
306
+ - ⭕ El tamaño de los nodos representa la frecuencia del concepto
307
+ - ↔️ El grosor de las líneas indica la fuerza de la conexión
308
+ """)
309
+
310
+ st.markdown('</div>', unsafe_allow_html=True)
311
+ else:
312
+ st.warning(discourse_t.get('graph_not_available', 'Gráfico no disponible'))
313
+ else:
314
+ st.warning(discourse_t.get('concepts_not_available', 'Conceptos no disponibles'))
315
+
316
+ # Nota informativa sobre la comparación
317
+ st.info(discourse_t.get('comparison_note',
318
+ 'La funcionalidad de comparación detallada estará disponible en una próxima actualización.'))