File size: 22,537 Bytes
c951ae3
a955397
 
 
c951ae3
a955397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
import streamlit as st
import json
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch

# Page configuration
st.set_page_config(
    page_title="MiNER - Stage 2: Metadata Extraction",
    page_icon="🏷️",
    layout="wide"
)

# Custom CSS for improved appearance
st.markdown("""
    <style>
    .main-header {
        font-size: 2.5rem;
        color: #4A90E2;
        text-align: center;
        margin-bottom: 0.5rem;
    }
    .sub-header {
        text-align: center;
        color: #666;
        margin-bottom: 2rem;
    }
    .stButton>button {
        width: 100%;
        background-color: #FF6B6B;
        color: white;
        font-size: 1.1rem;
        padding: 0.75rem;
        border-radius: 8px;
        border: none;
    }
    .stButton>button:hover {
        background-color: #FF5252;
    }
    .entity-group {
        padding: 0.5rem 0;
        margin-bottom: 0.5rem;
    }
    .entity-group-title {
        font-weight: 600;
        color: #555;
        margin-bottom: 0.4rem;
        font-size: 0.8rem;
        text-transform: uppercase;
        letter-spacing: 0.5px;
    }
    .entity-badge {
        display: inline-block;
        padding: 0.3rem 0.7rem;
        border-radius: 5px;
        margin: 0.2rem 0.3rem 0.2rem 0;
        font-size: 0.9rem;
        font-weight: 500;
        box-shadow: 0 1px 3px rgba(0,0,0,0.12);
    }
    .example-section {
        background-color: #2d3436;
        color: #dfe6e9;
        padding: 1rem;
        border-radius: 8px;
        margin: 1rem 0;
    }
    </style>
""", unsafe_allow_html=True)

# ==================== MODEL LOADING ====================
@st.cache_resource
def load_model():
    """Loads the model and tokenizer (cached to avoid reloading)"""
    MODEL_NAME = "liaad/Citilink-BERTimbau-large-metadata-pt-baseline"
    
    try:
        tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
        model = AutoModelForTokenClassification.from_pretrained(MODEL_NAME)
        model.eval()
        return tokenizer, model
    except Exception as e:
        st.error(f"Error loading model: {e}")
        return None, None

# ==================== ENTITY EXTRACTION ====================
def extract_entities(text, tokenizer, model):
    """
    Extracts entities from text using BERT model.
    Based on the original extract_entities function.
    """
    if not text or text.strip() == "":
        return {}
    
    # Tokenization + offsets
    encoding = tokenizer(
        text,
        return_tensors="pt",
        return_offsets_mapping=True,
        truncation=True,
        max_length=512
    )
    
    offsets = encoding["offset_mapping"][0].tolist()
    word_ids = encoding.word_ids(batch_index=0)
    
    inputs = {
        "input_ids": encoding["input_ids"],
        "attention_mask": encoding["attention_mask"],
    }
    
    # Prediction
    with torch.no_grad():
        outputs = model(**inputs)
    
    pred_ids = torch.argmax(outputs.logits, dim=2)[0].tolist()
    pred_labels = [model.config.id2label[i] for i in pred_ids]
    
    # Entity reconstruction
    entities = []
    current = None
    prev_word_idx = None
    
    for i, label in enumerate(pred_labels):
        word_idx = word_ids[i]
        start, end = offsets[i]
        
        # Ignore special tokens (CLS, SEP, PAD)
        if word_idx is None:
            continue
        
        # ONLY process the FIRST subtoken of each word
        if word_idx == prev_word_idx:
            # This is a subsequent subtoken, ignore prediction
            # But extend the offset of current entity if it exists
            if current:
                current["end"] = end
            continue
        
        # Update prev_word_idx
        prev_word_idx = word_idx
        
        # Process the label of the first subtoken
        if label.startswith("B-"):
            # Close previous entity if it exists
            if current:
                entities.append(current)
            
            # Start new entity
            current = {"label": label[2:], "start": start, "end": end}
        
        elif label.startswith("I-"):
            if current and current["label"] == label[2:]:
                # Continue entity correctly
                current["end"] = end
            else:
                # Ignore loose I- (don't create entity)
                continue
        
        else:  # label == "O"
            # Close current entity
            if current:
                entities.append(current)
                current = None
    
    # Close last entity if it exists
    if current:
        entities.append(current)
    
    # Add extracted text to each entity
    for ent in entities:
        ent["text"] = text[ent["start"]:ent["end"]]
    
    # Group entities by label
    grouped_entities = {}
    for ent in entities:
        label = ent["label"]
        if label not in grouped_entities:
            grouped_entities[label] = []
        
        grouped_entities[label].append({
            "text": ent["text"],
            "start": ent["start"],
            "end": ent["end"]
        })
    
    # ==================== POST-PROCESSING ====================
    # For certain metadata, keep only the first occurrence
    unique_entities = ["HORARIO", "DATA", "NUMERO-ATA", "LOCAL", "TIPO-REUNIAO"]
    
    for label in list(grouped_entities.keys()):
        # Check if the label starts with any of the unique entities
        for unique_entity in unique_entities:
            if label.startswith(unique_entity):
                # Keep only the first occurrence (smallest start offset)
                grouped_entities[label] = sorted(
                    grouped_entities[label], 
                    key=lambda x: x["start"]
                )[:1]
                break
    
    return grouped_entities

# ==================== UTILITY FUNCTIONS ====================
def translate_entity_label(label):
    """Translates entity labels from Portuguese to English and adds emojis"""
    translations = {
        "NUMERO-ATA": ("📋", "MINUTES NUMBER"),
        "DATA": ("📅", "DATE"),
        "LOCAL": ("📍", "LOCATION"),
        "TIPO-REUNIAO": ("📌", "MEETING TYPE"),
        "HORARIO-INICIO": ("🕐", "BEGIN TIME"),
        "HORARIO-FIM": ("🕐", "END TIME"),
        "PARTICIPANTE-PRESIDENTE-PRESENTE": ("👔", "PRESIDENT - PRESENT"),
        "PARTICIPANTE-PRESIDENTE-AUSENTE": ("👔", "PRESIDENT - ABSENT"),
        "PARTICIPANTE-PRESIDENTE-SUBSTITUIDO": ("👔", "PRESIDENT - SUBSTITUTED"),
        "PARTICIPANTE-VEREADOR-PRESENTE": ("👥", "COUNCILOR - PRESENT"),
        "PARTICIPANTE-VEREADOR-AUSENTE": ("👥", "COUNCILOR - ABSENT"),
        "PARTICIPANTE-VEREADOR-SUBSTITUIDO": ("👥", "COUNCILOR - SUBSTITUTED"),
    }
    
    # Check if exact translation exists
    if label in translations:
        return translations[label]
    
    # Fallback: try to extract base category
    for key, value in translations.items():
        if label.startswith(key.split("-")[0]):
            return value
    
    return ("", label)

def get_entity_style(label):
    """Returns color and border style based on entity type"""
    # Base colors
    colors = {
        "NUMERO-ATA": "#E74C3C",           # Red
        "DATA": "#16A085",                 # Teal
        "LOCAL": "#2980B9",                # Blue
        "TIPO-REUNIAO": "#E67E22",         # Orange
        "HORARIO": "#F39C12",              # Yellow-orange
        "PARTICIPANTE-PRESIDENTE": "#8E44AD",  # Purple for President
        "PARTICIPANTE-VEREADOR": "#27AE60"     # Green for Councilors
    }
    
    # Border styles for different attendance states
    border_styles = {
        "PRESENTE": "solid",      # Solid border
        "AUSENTE": "dashed",      # Dashed border
        "SUBSTITUIDO": "dotted"   # Dotted border
    }
    
    # Determine base color
    color = "#7F8C8D"  # Default color
    border_style = "solid"
    border_width = "2px"
    
    # Check if it's President or Councilor
    if "PARTICIPANTE-PRESIDENTE" in label:
        color = colors["PARTICIPANTE-PRESIDENTE"]
        # Determine border style
        if "PRESENTE" in label:
            border_style = border_styles["PRESENTE"]
        elif "AUSENTE" in label:
            border_style = border_styles["AUSENTE"]
        elif "SUBSTITUIDO" in label:
            border_style = border_styles["SUBSTITUIDO"]
    elif "PARTICIPANTE-VEREADOR" in label:
        color = colors["PARTICIPANTE-VEREADOR"]
        # Determine border style
        if "PRESENTE" in label:
            border_style = border_styles["PRESENTE"]
        elif "AUSENTE" in label:
            border_style = border_styles["AUSENTE"]
        elif "SUBSTITUIDO" in label:
            border_style = border_styles["SUBSTITUIDO"]
    else:
        # For other entities, use specific colors
        base_category = label.split("-")[0]
        color = colors.get(base_category, color)
    
    return color, border_style, border_width

def display_entities_compact(entities_dict):
    """Displays entities in a compact format with emojis and translation"""
    if not entities_dict:
        st.info("No entities detected.")
        return
    
    for label, entities in sorted(entities_dict.items()):
        emoji, translated_label = translate_entity_label(label)
        color, border_style, border_width = get_entity_style(label)
        
        # Group title with emoji
        title_html = f'<div class="entity-group-title">{emoji} {translated_label}</div>'
        st.markdown(title_html, unsafe_allow_html=True)
        
        # Show all entities of this type in compact badges
        badges_html = '<div class="entity-group">'
        for ent in entities:
            badge_style = f"background-color: {color}; color: white; border: {border_width} {border_style} rgba(0,0,0,0.2);"
            badges_html += f'<span class="entity-badge" style="{badge_style}">{ent["text"]}</span>'
        badges_html += '</div>'
        
        st.markdown(badges_html, unsafe_allow_html=True)

# ==================== SIDEBAR ====================
with st.sidebar:
    st.markdown("### ⚙️ Configuration")
    st.markdown("Choose an example or enter your own text:")
    
    selected_example = st.selectbox(
        "Select an example",
        ["Custom Text", "Example 1 - Alandroal", "Example 2 - Campo Maior", "Example 3 - Covilhã", "Example 4 - Fundão", "Example 5 - Guimarães", "Example 6 - Porto"]
    )
    
    st.markdown("---")
    
    # About Section
    st.markdown("### 📋 About")
    st.markdown("""
    **MiNER Stage 2** uses Named Entity Recognition models to automatically extract metadata from meeting minutes.

    - **Model**: BERTimbau fine-tuned
    - **Languages**: Portuguese
    - **Method**: Token Classification (NER) with BIO tagging
    """)
    
    st.markdown("---")
    
    # Resources Section
    st.markdown("### 🔗 Resources")
    st.markdown("""
    - [GitHub Repository](https://github.com/LIAAD/MiNER)
    - [Model](https://huggingface.co/liaad/Citilink-BERTimbau-large-metadata-pt-baseline)
    """)

# ==================== MAIN CONTENT ====================

# Header
st.markdown('<div class="main-header">🏷️ MiNER — Stage 2: Metadata Extraction Demo</div>', unsafe_allow_html=True)
st.markdown('<div class="sub-header">Automatic extraction of structured metadata from municipal meeting minutes</div>', unsafe_allow_html=True)

# ==================== HOW IT WORKS (MOVED TO TOP) ====================
with st.expander("🎯 How It Works", expanded=False):
    st.markdown("""
    The model analyzes the **meeting minutes** to automatically extract **structured metadata** using a *Named Entity Recognition (NER)* approach.
    
    **What information is extracted:**
    
    Each token in the document is classified, identifying information such as:
    - 📅 **Date**
    - 🕐 **Start / End time**
    - 📍 **Location**
    - 📋 **Minute ID**
    - 📌 **Meeting type**
    - 👔 **President** (present / absent / substituted)
    - 👥 **Councilors** (present / absent / substituted)
    
    **Technical approach:**
    
    The model uses the **BIO tagging scheme** (*Begin, Inside, Outside*) to mark entity boundaries, and the final spans are reconstructed from token-level predictions.
    
    ---
    
    ### 📖 Complete Example
    
    **Input Document:**
    """)
    
    # Input example in code block
    st.code("""CÂMARA MUNICIPAL DE ALANDROAL
ATA N.º 21
REUNIÃO ORDINÁRIA 11/09/2024
Presidiu o Senhor João Maria Aranha Grilo, Presidente da Câmara Municipal de Alandroal
Vereadores Paulo Jorge da Silva Gonçalves
Fernanda Manuela Brites Romão
Elisabete de Jesus dos Passos Galhardas
Faltou João Carlos Camões Roma Balsante
Secretariou a Reunião ****************************************
No Edifício Sede do Município de Alandroal, o Senhor Presidente da Câmara Municipal, João Maria Aranha Grilo, declarou aberta a reunião, eram 15 horas e 30 minutos.""", language=None)
    
    st.markdown("""
    **Expected Output (Extracted Entities):**
    
    ```
    📅 DATE
       • 11/09/2024
    
    🕐 TIME
       • 15 horas e 30 minutos
    
    📍 LOCATION
       • No Edifício Sede do Município de Alandroal
    
    📋 MINUTES NUMBER
       • 21
    
    📌 MEETING TYPE
       • ORDINÁRIA
    
    👔 PRESIDENT - PRESENT
       • João Maria Aranha Grilo
    
    👥 COUNCILOR - PRESENT
       • Paulo Jorge da Silva Gonçalves
       • Fernanda Manuela Brites Romão
       • Elisabete de Jesus dos Passos Galhardas
    
    👥 COUNCILOR - ABSENT
       • João Carlos Camões Roma Balsante
    ```
    """)

# Load model
with st.spinner("Loading model..."):
    tokenizer, model = load_model()

if tokenizer is None or model is None:
    st.error("❌ Failed to load model. Please check if the model path is correct.")
    st.stop()

# Main layout with two columns
col1, col2 = st.columns([1, 1])

# ==================== LEFT COLUMN - INPUT ====================
with col1:
    st.markdown("### 📝 Input Document")
    
    if selected_example == "Custom Text":
        example_text = ""
    elif selected_example == "Example 1 - Alandroal":
        example_text = """CÂMARA MUNICIPAL DE ALANDROAL
ATA N.º 21
REUNIÃO ORDINÁRIA 11/09/2024
Presidiu o Senhor João Maria Aranha Grilo, Presidente da Câmara Municipal de Alandroal
Vereadores Paulo Jorge da Silva Gonçalves
Fernanda Manuela Brites Romão
Elisabete de Jesus dos Passos Galhardas
Faltou João Carlos Camões Roma Balsante
Secretariou a Reunião ****************************************
No Edifício Sede do Município de Alandroal, o Senhor Presidente da Câmara Municipal, João Maria Aranha Grilo, declarou aberta a reunião, eram 15 horas e 30 minutos."""    
    elif selected_example == "Example 2 - Campo Maior":
        example_text = """ATA Nº 1 REUNIÃO ORDINÁRIA DA CÂMARA MUNICIPAL DE CAMPO MAIOR, REALIZADA EM 5 DE JANEIRO DE 2022.
Aos cinco dias do mês de janeiro do ano de dois mil e vinte e dois, no Edifício dos Paços do Concelho, nesta Vila, realizou-se, pelas nove horas e trinta minutos, a reunião Ordinária da Câmara Municipal, comparecendo os Excelentíssimos Senhores Luís Fernando Martins Rosinha, Paulo Ivo Sabino Martins de Almeida, Paulo Jorge Furtado Pinheiro, Maria da Encarnação Grifo Silveirinha (videoconferência) e Fátima do Rosário Pingo Vitorino Pereira, respetivamente, Presidente e Vereadores efetivos deste Órgão Autárquico.
-Verificada a presença dos respectivos membros, o Senhor Presidente declarou aberta a reunião:
-Estava presente o Chefe **************************************, Dr. *********************************** e a Assistente Técnico **************************************.
-Depois de todos terem ocupado os seus lugares, o Senhor Presidente declarou aberta a reunião eram nove horas e trinta minutos."""
    elif selected_example == "Example 3 - Covilhã":
        example_text = """-- --

-- --

CÂMARA MUNICIPAL

DA

COVILHÃ

TEXTO DEFINITIVO DA ATA Nº 02/2023

Da reunião ordinária privada realizada no dia 03 de fevereiro de 2023, iniciada às 09:05 horas e concluída às 10:15 horas.

------------------------------------ Sumário:                        01 ------------------------------- ---- Abertura                        02

Período Antes da Ordem do Dia   05

Período da Ordem do Dia         06

Agenda                          06

Aprovação de Atas               06

Balancete                       07

Despachos                       07

DAGCJ                           10

DFMA                            17

DOP                             19

DECAD                           29

DU                              38

Aprovação em minuta             42 Votação das deliberações        42 Montante Global de Encargos     42 Encerramento                    42

------------------------------------

ABERTURA

ATA Nº 02/2023

Aos três dias do mês de fevereiro do ano de dois mil e vinte e três, na Sala de Reuniões dos Paços do Concelho, na Covilhã, realizou-se a reunião ordinária privada da Câmara Municipal da Covilhã sob a presidência do Senhor Presidente da Câmara, Vítor Manuel Pinheiro Pereira, estando presentes o Senhor Vice-Presidente José Armando Serra dos Reis e os Senhores Vereadores Pedro Miguel Santos Farromba, Maria Regina Gomes Gouveia, Jorge Humberto Martins Simões (em substituição de Ricardo Miguel Correia Leitão Ferreira da Silva), José Miguel Ribeiro Oliveira e Marta Maria Tomaz Gomes Morais Alçada Bom Jesus.

A reunião foi secretariada pela Senhora Dr.ª ********************************, Diretora *************************************************************.

E, pelas 09:05 horas, o Senhor Presidente da Câmara deu início aos trabalhos da presente reunião com a seguinte Ordem de Trabalhos:"""
    elif selected_example == "Example 4 - Fundão":
        example_text = """ 14/02/2022 ATA DA REUNIÃO DE 14/02/2022 CÂMARA MUNICIPAL
DO
FUNDÃO
Texto definitivo da ata n.º 2/2022 da reunião ordinária realizada no dia 14 de fevereiro de 2022, iniciada às 17:00 horas e concluída às 19:00.
ATA N.º 2/2022 Aos catorze dias do mês fevereiro do ano dois mil e vinte e dois, realizou-se por videoconferência, a reunião ordinária privada da Câmara Municipal do Fundão, sob a presidência do Senhor Presidente da Câmara, Dr. Paulo Alexandre Bernardo Fernandes, com a participação do Senhor Vice-presidente, Dr. Luís Miguel Roque Tarouca Duarte Gavinhos e dos Senhores Vereadores, Dra. Joana Morgadinho Bento, Dra. Maria Alcina Domingues Cerdeira, Dr. Pedro Manuel Figueiredo Neto, Prof. Sérgio Miguel Cardoso Mendes e Dra. Ana Paula Coelho Duarte.
A reunião foi secretariada pela Dra. ****************************, Diretora *******************************************."""
    elif selected_example == "Example 5 - Guimarães":
        example_text = """Câmara Municipal de Guimarães.
 ATA Nº 1 Fls. __10__ REUNIÃO ORDINÁRIA DE 13 DE JANEIRO DE 2022 
ATA
Aos treze dias do mês de janeiro do ano de dois mil e vinte e dois, no Edifício dos Paços do Concelho, na Sala de Reuniões, compareceram os Excelentíssimos Senhores: Presidente da Câmara – Domingos Bragança Salgado e Vereadores – Adelina Paula Mendes Pinto, Paulo Rui Lopes Pereira da Silva, Paula Cristina dos Santos Oliveira, Nelson José Guimarães Felgueiras, Alice Sofia de Freitas Soares Ferreira Fernandes, Ana Maria Prego de Faria Berkeley Cotter, Bruno Alberto Vieira Fernandes, Ricardo José Machado Pereira da Silva Araújo, Vânia Carvalho Dias da Silva de Antas de Barros e Hugo Miguel Alves Ribeiro. 
Secretariou a Diretora ***************, **************************************. 
Pelas 10.10 horas foi declarada aberta a reunião."""
    elif selected_example == "Example 6 - Porto":
        example_text = """2.ª REUNIÃO PÚBLICA,
DA CÂMARA MUNICIPAL DO PORTO
REALIZADA EM 8 DE NOVEMBRO DE 2021
ÀS 10 HORAS
PRESENTES:
-   Rui de Carvalho de Araújo Moreira
-   Filipe Manuel Ventura Camões de Almeida Araújo
-   Ana Catarina da Rocha Araújo
-   Ricardo Miguel Araújo Cardoso Valente
-   Albino Pedro Pereira Baganha
-   Cristina Mafalda Nieto Guimarães Pimentel
-   Tiago Barbosa Ribeiro
-   Maria do Rosário Gambôa Lopes de Carvalho
-   Catarina Maria da Costa Santos Cunha Pereira de Abreu
-   Vladimiro Mota Cardoso Feliz
-   Alberto Amaro Guedes Machado
-   Maria Ilda da Costa Figueiredo
-   Sérgio Augusto Leite Aires
Secretariou a reunião a Técnica ********, ***************."""
    else:
        example_text = "Add your text here"
    
    st.markdown(f"**Example:** {selected_example}")
    
    text_input = st.text_area(
        "Type or paste the text here:",
        value=example_text,
        height=400,
        placeholder="Enter the meeting minutes or administrative document text..."
    )
    
    # Segmentation button
    process = st.button("🔍 Segment Document")

# ==================== RIGHT COLUMN - RESULTS ====================
with col2:
    st.markdown("### 📊 Segmentation Results")
    
    if process:
        if text_input.strip():
            with st.spinner("Processing text..."):
                # Extract entities using the model
                entities = extract_entities(text_input, tokenizer, model)
                
                if entities:
                    st.markdown("#### Detected Entities:")
                    display_entities_compact(entities)
                    
                    # ==================== JSON EXPORT ====================
                    st.markdown("---")
                    
                    # Expander with JSON visualization
                    with st.expander("📄 View complete JSON"):
                        st.json(entities)
                    
                    # JSON download button
                    json_str = json.dumps(entities, ensure_ascii=False, indent=2)
                    st.download_button(
                        label="⬇️ Download JSON",
                        data=json_str,
                        file_name="extracted_entities.json",
                        mime="application/json"
                    )
                else:
                    st.warning("⚠️ No entities were detected in the text.")
        else:
            st.warning("⚠️ Please enter some text to process.")
    else:
        st.info("👈 Enter text in the input box and click 'Segment Document' to begin.")