File size: 26,106 Bytes
8d3a14c
 
 
 
 
 
 
7c2c345
8d3a14c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398a7da
8d3a14c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import streamlit as st
import numpy as np
import re
from joblib import load
from transformers import AutoTokenizer, AutoModel
import torch


# ============================================================
# PAGE CONFIG
# ============================================================
st.set_page_config(
    page_title="Council Topics Classifier",
    page_icon="🏛️",
    layout="wide",
    initial_sidebar_state="expanded"
)

# ============================================================
# CUSTOM CSS
# ============================================================
st.markdown("""
<style>
    /* ==============================
       UNIVERSAL COLOR VARIABLES
       ============================== */
    :root {
        --accent-color: var(--primary-color);
        --bg-color: var(--background-color);
        --text-color: var(--text-color);
        --secondary-bg: var(--secondary-background-color);
        --border-color: var(--border-color);
    }

    /* ==============================
       HEADER
       ============================== */
        div[data-testid="stAppViewContainer"] .main-header {
        font-size: 2.5rem !important; 
        font-weight: 900 !important;
        color: var(--accent-color) !important;
        text-align: center !important;
        margin-bottom: 1rem !important;
        letter-spacing: 0.5px;
    }




    /* ==============================
       RESULT BOXES
       ============================== */
    .result-box {
        padding: 1rem;
        margin: 0.5rem 0;
        border-radius: 0.5rem;
        border-left: 4px solid var(--accent-color);
        background-color: var(--secondary-bg);
        color: var(--text-color);
        transition: background-color 0.2s ease, border-color 0.2s ease;
    }

    /* Add hover highlight */
    .result-box:hover {
        background-color: color-mix(in srgb, var(--secondary-bg) 85%, var(--accent-color) 15%);
    }

    /* ==============================
       METRIC BOXES
       ============================== */
    .metric-box {
        background-color: var(--secondary-bg);
        padding: 1rem;
        border-radius: 0.75rem;
        text-align: center;
        color: var(--text-color);
        border: 1px solid var(--border-color);
    }

    /* ==============================
       LANGUAGE NOTICE
       ============================== */
    .lang-notice {
        background-color: color-mix(in srgb, #ffc107 15%, var(--bg-color) 85%);
        border-left: 4px solid #ffc107;
        padding: 1rem;
        border-radius: 0.5rem;
        margin: 1rem 0;
        color: var(--text-color);
    }

    /* ==============================
       STREAMLIT BASE ELEMENTS
       ============================== */
    .stAlert, .stMarkdown, .stTextArea, .stMetric {
        color: var(--text-color) !important;
    }

    /* Progress bar adaptation */
    div[data-testid="stProgressBar"] > div > div {
        background-color: var(--accent-color) !important;
    }
</style>
""", unsafe_allow_html=True)


# ============================================================
# LOAD MODELS (with caching)
# ============================================================
@st.cache_resource(show_spinner="🔄 Loading models...")
def load_models():
    """Load all model components"""
    models_dir = 'models'
    
    # Load saved components
    tfidf = load(f'{models_dir}/gradient_boosting_tfidf_vectorizer.joblib')
    mlb = load(f'{models_dir}/gradient_boosting_mlb_encoder.joblib')
    optimal_thresholds = np.load(f'{models_dir}/gradient_boosting_optimal_thresholds.npy')
    adaptive_weights = np.load(f'{models_dir}/gradient_boosting_adaptive_weights.npy')
    logistic_model = load(f'{models_dir}/gradient_boosting_logistic_model.joblib')
    gb_models = load(f'{models_dir}/gradient_boosting_gb_models.joblib')

    
    # Load BERT model
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    tokenizer = AutoTokenizer.from_pretrained("neuralmind/bert-base-portuguese-cased")
    bert_model = AutoModel.from_pretrained("neuralmind/bert-base-portuguese-cased").to(device)
    bert_model.eval()
    
    return {
        'tfidf': tfidf,
        'mlb': mlb,
        'optimal_thresholds': optimal_thresholds,
        'adaptive_weights': adaptive_weights,
        'logistic_model': logistic_model,
        'gb_models': gb_models,
        'tokenizer': tokenizer,
        'bert_model': bert_model,
        'device': device
    }

models = load_models()

# ============================================================
# PREPROCESSING
# ============================================================
def smart_preprocess(text):
    """Smart preprocessing with Portuguese municipal context"""
    text = text.lower()
    
    substitutions = {
        r'câmara\s+municipal': 'camara_municipal',
        r'assembleia\s+municipal': 'assembleia_municipal',
        r'junta\s+de\s+freguesia': 'junta_freguesia',
        r'presidente\s+da\s+câmara': 'presidente_camara',
        r'vereador\s+': 'vereador_',
        r'art\.?\s*(\d+)': r'artigo_\1',
        r'n\.?º\s*(\d+)': r'numero_\1',
        r'decreto\s+lei': 'decreto_lei',
        r'código\s+civil': 'codigo_civil',
        r'€\s*(\d+)': r'\1_euros'
    }
    
    for pattern, replacement in substitutions.items():
        text = re.sub(pattern, replacement, text)
    
    text = re.sub(r'[^\w\s_]', ' ', text)
    text = re.sub(r'\s+', ' ', text)
    
    words = [w for w in text.split() if len(w) >= 2]
    return ' '.join(words).strip()

# ============================================================
# FEATURE EXTRACTION
# ============================================================
def get_bert_embeddings(texts):
    """Get BERT embeddings"""
    all_embeddings = []
    with torch.no_grad():
        for text in texts:
            encoded = models['tokenizer']([text], padding=True, truncation=True, 
                                         max_length=512, return_tensors="pt").to(models['device'])
            outputs = models['bert_model'](**encoded)
            
            embeddings = outputs.last_hidden_state
            attention_mask = encoded['attention_mask'].unsqueeze(-1)
            embeddings = (embeddings * attention_mask).sum(1) / attention_mask.sum(1)
            all_embeddings.append(embeddings.cpu().numpy())
    
    return np.vstack(all_embeddings)

# ============================================================
# PREDICTION
# ============================================================
def predict_topics(text):
    """Predict topics using Gradient Boosting ensemble"""
    
    # Preprocess
    cleaned_text = smart_preprocess(text)
    
    # Extract features
    tfidf_features = models['tfidf'].transform([cleaned_text])
    bert_features = get_bert_embeddings([cleaned_text])
    
    # Combine features
    X_dense = np.hstack([tfidf_features.toarray(), bert_features])
    
    # Get predictions from LogisticRegression
    logistic_proba = models['logistic_model'].predict_proba(X_dense)
    
    # Get predictions from GradientBoosting models
    gb_predictions = []
    for gb_models_set in models['gb_models']:
        gb_proba = np.zeros((1, len(models['mlb'].classes_)))
        for label_idx, label_model in enumerate(gb_models_set):
            gb_proba[:, label_idx] = label_model.predict_proba(X_dense)[:, 1]
        gb_predictions.append(gb_proba)
    
    # Weighted ensemble prediction
    ensemble_proba = np.zeros_like(logistic_proba)
    for label_idx in range(len(models['mlb'].classes_)):
        w = models['adaptive_weights'][label_idx]
        ensemble_proba[:, label_idx] = (
            w[0] * logistic_proba[:, label_idx] +
            w[1] * gb_predictions[0][:, label_idx] +
            w[2] * gb_predictions[1][:, label_idx] +
            w[3] * gb_predictions[2][:, label_idx]
        )
    
    # Apply optimal thresholds
    predictions = np.zeros(len(models['mlb'].classes_))
    for i, thresh in enumerate(models['optimal_thresholds']):
        predictions[i] = (ensemble_proba[0, i] >= thresh).astype(int)
    
    # ---  Make sure at least one topic is given ---
    if predictions.sum() == 0:
        # No topic surpassed the threshold
        best_idx = np.argmax(ensemble_proba[0])
        predictions[best_idx] = 1
    
    # Get predicted labels with confidences
    results = []
    for i, label in enumerate(models['mlb'].classes_):
        if predictions[i] == 1:
            confidence = ensemble_proba[0, i]
            results.append((label, confidence))
    
    # Sort by confidence
    results.sort(key=lambda x: x[1], reverse=True)
    
    return results, cleaned_text


# ============================================================
# MAIN APP
# ============================================================
def main():
    # Header
    st.markdown('<p class="main-header">🏛️ Council Topics Classifier</p>', unsafe_allow_html=True)
    st.markdown("""
    <p style="text-align: center; color: #666;">
    Gradient Boosting + Active Learning Topic Classification for Portuguese municipal meeting minutes discussion subjects
    </p>
    """, unsafe_allow_html=True)
    
    # Sidebar
    st.sidebar.header("⚙️ Configuration")
    
    example = st.sidebar.selectbox(
        "Choose an example or enter your own text:",
        [
            "Custom Text",
            "M01_cm_015_2024-06-19_sub05",
            "M01_cm_021_2024-09-11_sub12",
            "M02_cm_010_2024-05-15_sub17",
            "M02_cm_020_2024-11-06_sub05",
            "M03_cm_010_2024-06-17_sub16",
            "M03_cm_021_2024-12-12_sub01",
            "M04_cm_008_2024-13-05_sub012",
            "M04_cm_010_2024-21-06_sub014",
            "M05_cm_016_2024-09-30_sub16",
            "M05_cm_018_2024-10-28_sub15",
            "M06_cm_066_2024-10-14_sub10",
            "M06_cm_070_2024-12-02_sub19"
        ]
    )
    
    # Example texts
    example_texts = {
        "Custom Text": "",
        "M01_cm_015_2024-06-19_sub05": "Pelo Sr. Presidente foi presente a reunião a informação da contabilidade que se anexa à presente ata. \nPonderado e analisado o assunto o Executivo Municipal deliberou por maioria, com os votos a favor dos eleitos pelo PS e a abstenção da eleita pelo Nós, Cidadãos, ratificar a alteração orçamental permutativa.",
        "M01_cm_021_2024-09-11_sub12": "Transporte de aluno para a Escola Associação Sociocultural Terapeuta de Évora.\n Pelo Senhor Presidente da Câmara Municipal foi presente a esta reunião um requerimento com o registo de entrada n.º 12022/24, de 02/09/2024, que se anexa à presente ata.  Relativamente ao requerimento apresentado o Senhor Presidente da Câmara Municipal informou, que este era um transporte feito por parte da Autarquia á vários anos, e que, propunha que o mesmo fosse renovado nos mesmos moldes. \n Ponderado e analisado o assunto o Executivo Municipal deliberou por unanimidade, aprovar o transporte de um aluno para a Escola Associação Sociocultural Terapeuta de Évora. -",
        "M02_cm_010_2024-05-15_sub17": "-ALTERAÇÃO DOS PREÇOS DE VENDA AO PÚBLICO DOS PRODUTOS PARA SEREM VENDIDO NOS ESPAÇOS TURÍSTICOS DE CAMPO MAIOR:\n-Apreciação da informação (registo 8274) do senhor presidente, referente ao assunto em epígrafe, que a seguir se transcreve:-“ Devido ao aumento dos valores dos fornecedores, é necessário proceder á alteração de preços de alguns dos produtos à venda nos espaços turísticos do Município, assim PROPONHO conforme determina o artigo 21º da Lei 73/2013, na sua atual redação, que os preços das peças/produtos para venda, sejam fixados pela Câmara Municipal, com IVA incluído à taxa legal em vigor, constantes do mapa abaixo mencionado:\n\n————————————Artigos para Venda————————————\nProdutos,IVA,Valor\nGarrafa de Azeite Virgem Extra Gralha 2L,6%,20,00€\nGarrafa de Azeite Virgem Extra Gralha 3L,6%,29,00€\nGarrafa de Azeite Virgem Extra GR 0,5L,6%,8,50€\n——————————————————————————————————————————— \n-A CÂMARA DELIBEROU, POR UNANIMIDADE, APROVAR A PROPOSTA DO SENHOR PRESIDENTE, REFERENTE À ALTERAÇÃO DOS PREÇOS DE VENDA AO PÚBLICO DOS PRODUTOS PARA SEREM VENDIDO NOS ESPAÇOS TURÍSTICOS DE CAMPO MAIOR, NOMEADAMENTE:\n\n————————————Artigo para venda————————————\nProdutos,IVA,Valor\nGarrafa de Azeite Virgem Extra Gralha 2L,6%,20,00€\nGarrafa de Azeite Virgem Extra Gralha 3L,6%,29,00€\nGarrafa de Azeite Virgem Extra GR 0,5L,6%,8,50€",
        "M02_cm_020_2024-11-06_sub05": "-LOUVOR - PARA CONHECIMENTO:\n-Atribuição de um louvor ao Município de Campo Maior, na pessoa do Senhor Presidente, por parte da Associação Humanitária de Bombeiros Voluntários de Campo Maior, pelo apoio prestado ao longo do seu mandato, na aquisição de várias ambulâncias, cedência de pessoal e também financeiramente.\n-A CÂMARA TOMOU CONHECIMENTO.",
        "M03_cm_010_2024-06-17_sub16": "11.2. – Empreitada da Obra de Requalificação e Construção de Parques Infantis nas Freguesias do Concelho\n\nPresente à Câmara informação da Divisão de Obras e Planeamento, constante da plataforma de gestão documental Sigmadoc Web/NIPG: 4341/24_Pendente: 100473, e conta final da Empreitada em apreço, onde se conclui poder ser aprovada pela Câmara Municipal.\n\nDocumentos que se dão como inteiramente reproduzidos na presente ata e ficam, para todos os efeitos legais, arquivados em pasta própria existente para o efeito.\n\nA Câmara deliberou, nos termos da informação da Divisão de Obras e do parecer do Senhor Diretor **************************************, aprovar a conta final da Empreitada da Obra de Requalificação e Construção de Parques Infantis nas Freguesias do Concelho.",
        "M03_cm_021_2024-12-12_sub01":  "Os serviços propuseram ao órgão a retirada do assunto contido na alínea a) do 5.2. - DFMA, tendo sido aprovado por unanimidade:\n\nRetirar:\n\n5.2. DEPARTAMENTO DE FINANÇAS E MODERNIZAÇÃO ADMINISTRATIVA\n\n“f) Atualização do Tarifário 2025 – Movicovilhã (Aprovação) ”",
        "M04_cm_008_2024-13-05_sub012": "Foi apresentada à Câmara uma proposta subscrita pelo Senhor Presidente, datada de 2 de maio de 2024, e que se transcreve:\n“Considerando que o procedimento de concurso público “Unidade de Saúde Familiar - Cereja”, foi autorizado em sede de reunião de Câmara Municipal do Fundão, datada de 28/02/2024, e publicado na II Série do Diário da República, n.º 64 de 01/04/2024; Considerando que no âmbito do referido procedimento e devido a circunstâncias imprevistas é necessário alterar aspetos fundamentais das peças que o constituem; Considerando o teor do meu despacho datado de 2 de Maio de 2024, referente ao procedimento administrativo acima referido, e dada a necessidade premente ocorrida, Proponho, que a Câmara Municipal delibere no sentido de aprovar o despacho em anexo à presente proposta (anexo I), nos termos do n.º 3 do art.º 35.º da Lei 75/2013 de 12 de Setembro na sua atual redação.” A Câmara Municipal tomou conhecimento e deliberou, por unanimidade e em minuta, aprovar a proposta apresentada. (Empreitada de:“Unidade de saúde familiar – Cereja” –ratificação de despacho)",
        "M04_cm_010_2024-21-06_sub014": "Foi apresentada à Câmara uma proposta subscrita pelo Senhor Vice-presidente, datada de 13 de maio de 2024, e que se transcreve:\n“Considerando o teor do despacho proferido no dia 29 de Maio de 2024, ora junto em anexo à presente proposta, relativo à aprovação das Normas de Participação e Funcionamento das Tascas e outros Espaços de Comercialização da Festa da Cereja, a realizar nos dias 07, 08, 09 e 10 de Junho de 2024, na freguesia de Alcongosta; Considerando o disposto no nº 3 do artigo 35.º da Lei n.º 75/2013, de 12 de Setembro, na sua atual redação, proponho, face aos factos e com os fundamentos que se deixam acima expostos, que a Câmara Municipal, delibere no sentido de ratificar o Despacho ora junto em anexo à presente proposta e que dela faz parte integrante.” A Câmara Municipal tomou conhecimento e deliberou, por unanimidade e em minuta, aprovar a proposta apresentada (Aprovação das “Normas de Participação e Funcionamento das Tascas e outros Espaços de Comercialização da Festa da Cereja – 2024” – ratificação de despacho)",
        "M05_cm_016_2024-09-30_sub16": "12. TRANSPORTES – GRUPO DESPORTIVO OLIVEIRA DO CASTELO - CEDÊNCIA DE AUTOCARRO – Presente a seguinte proposta: “O Grupo Desportivo Oliveira do Castelo solicitou a colaboração do Município através da cedência de um autocarro, para efetuar o transporte da equipa de veteranos, no dia 19 de outubro, a Bragança. Considerando que se trata de uma deslocação que visa a participação da equipa num jogo/convívio a realizar com a equipa local, não se afigura inconveniente para os serviços a disponibilização do autocarro em causa, pelo que por despacho datado de 11 de setembro de 2024, foi deferido o pedido. Sendo uma competência da Câmara Municipal de Guimarães, a atribuição deste tipo de apoios, submete-se à aprovação do Executivo Camarário o transporte solicitado.” DELIBERADO APROVAR POR UNANIMIDADE.",
        "M05_cm_018_2024-10-28_sub15": "14. TURISMO – PEDIDO DE RECONHECIMENTO DE INTERESSE PÚBLICO PARA A INSTALAÇÃO DE UM EMPREENDIMENTO DE AGROTURISMO – FREGUESIA DE NESPEREIRA – Presente, para aprovação pela Câmara Municipal e ulterior aprovação pela Assembleia Municipal, o reconhecimento de interesse público para a instalação de um empreendimento turístico, na modalidade de agroturismo, na rua da Arrochela de Cima, na freguesia de Nespereira, de acordo com documentos, que se dão aqui por reproduzidos e ficam arquivados em pasta anexa ao livro de atas. DELIBERADO, POR MAIORIA, SUBMETER À APROVAÇÃO DA ASSEMBLEIA MUNICIPAL. Votaram a favor o Presidente da Câmara e os Vereadores Adelina Paula Pinto, Paulo Lopes Silva, Paula Oliveira, Nelson Felgueiras, Sofia Ferreira, Ricardo Araújo, Hugo Ribeiro e Emília Lemos. Abstiveram-se as Vereadoras Ana Cotter e Vânia Dias da Silva.",
        "M06_cm_066_2024-10-14_sub10": "10. Aprovação de apoio à Catorze de Outubro -- Suites & Events, Lda., na realização do FIOMS -- Festival Internacional de Órgão e Música Sacra, Edição 2024.\nAprovada, por unanimidade.",
        "M06_cm_070_2024-12-02_sub19": "19. Aditamentos aos Contratos-Programa (i.) de Gestão de Resíduos Urbanos, (ii.) de Limpeza do Espaço Público, e (iii.) de Neutralidade Carbónica, a celebrar entre o Município do Porto e a Empresa Municipal de Ambiente do Porto, E. M., S. A.\nAprovada, por unanimidade.",

    }
    
    st.sidebar.markdown("---")
    st.sidebar.markdown("### 📊 About")
    st.sidebar.info("""
    **Council Topics PT Classifier** uses Gradient Boosting with Active Learning to identify topics in Portuguese municipal documents.
    
    - **Models**: LogReg + 3x GradientBoosting
    - **Features**: TF-IDF + BERTimbau
    - **Optimization**: Adaptive weighting
    - **Language**: Portuguese
    """)
    
    st.sidebar.markdown("---")
    st.sidebar.markdown("### ⚠️ Language Notice")
    st.sidebar.warning("""
    This model is trained on **Portuguese** texts. 
    Please enter text in Portuguese for accurate classification.
    """)
    
    # ============================================================
    # COLLAPSIBLE SECTIONS
    # ============================================================
    
    # Example of how it works
    with st.expander("💡 **Example: How the Classifier Works**", expanded=False):
        st.markdown("""
        #### Sample Classification Process
        
        **Input Text:**
        > "A Câmara Municipal aprovou o orçamento para 2024, com dotações para obras de reabilitação urbana no valor de 500.000€ e apoio financeiro a associações culturais."
        
        **Expected Output:**
        - 🟢 **Administração Geral, Finanças e Recursos Humanos** (Confidence: 84.2%)
        - 🟡 **Obras Públicas** (Confidence: 67.7%)
        - 🟡 **Cultura** (Confidence: 52.9%)
        """)
    
    # All possible topics
    with st.expander("📋 **All Possible Topics** (Classification Categories)", expanded=False):
        st.markdown("### Available Topics for Classification")
        st.markdown("The model can identify the following categories:")
        
        # Get all labels from the model
        all_labels = sorted(models['mlb'].classes_)
        
        num_cols = 4
        cols = st.columns(num_cols)
        
        for idx, label in enumerate(all_labels):
            col_idx = idx % num_cols
            with cols[col_idx]:
                st.markdown(f"• **{label}**")
        
        st.markdown(f"\n**Total: {len(all_labels)} topics**")
    
    
    # Main content
    col1, col2 = st.columns([1, 1])
    
    with col1:
        st.subheader("📝 Input Document")
        
        if example == "Custom Text":
            input_text = st.text_area(
                "Enter your Portuguese text here:",
                height=400,
                placeholder="Paste your council discussion subject here...\n\nExample: A Câmara Municipal aprovou o orçamento de 2024 com investimentos em infraestruturas."
            )
        else:
            input_text = st.text_area(
                f"Example: {example}",
                value=example_texts[example],
                height=400
            )
        
        classify_button = st.button("🔍 Classify Topics", type="primary", use_container_width=True)
    
    with col2:
        st.subheader("📌 Identified Topics")
        
        if classify_button and input_text:
            if len(input_text.strip()) < 10:
                st.warning("⚠️ Please enter a longer text (minimum 10 characters).")
            else:
                with st.spinner("🔄 Analyzing document..."):
                    # Perform classification
                    results, cleaned_text = predict_topics(input_text)
                    
                    if not results:
                        st.info("ℹ️ No topics identified with sufficient confidence.")
                    else:

                        for label, confidence in results:
                            if confidence >= 0.75:
                                emoji = "🟢"
                                color = "#28a745"
                            elif confidence >= 0.50:
                                emoji = "🟡"
                                color = "#ffc107"
                            else:
                                emoji = "🟠"
                                color = "#fd7e14"

                            st.markdown(f"""
                            <div class="result-box">
                                <strong>{emoji} {label}</strong>
                                <br>
                                <span style="color: {color}; font-weight: bold;">Confidence: {confidence:.1%}</span>
                            </div>
                            """, unsafe_allow_html=True)
                            
                            st.progress(confidence)

                        st.markdown("---")

                        # Add title for metrics section
                        st.markdown("### 📊 Classification Results Summary")

                        metric_col1, metric_col2, metric_col3 = st.columns(3)

                        with metric_col1:
                            st.markdown('<div class="metric-box">', unsafe_allow_html=True)
                            st.metric("Topics Found", len(results))
                            st.markdown('</div>', unsafe_allow_html=True)

                        with metric_col2:
                            avg_conf = np.mean([c for _, c in results])
                            st.markdown('<div class="metric-box">', unsafe_allow_html=True)
                            st.metric("Avg. Confidence", f"{avg_conf:.1%}")
                            st.markdown('</div>', unsafe_allow_html=True)

                        with metric_col3:
                            max_conf = max([c for _, c in results])
                            st.markdown('<div class="metric-box">', unsafe_allow_html=True)
                            st.metric("Max. Confidence", f"{max_conf:.1%}")
                            st.markdown('</div>', unsafe_allow_html=True)


                        
                        st.markdown("---")
                        
                        # Download button
                        result_text = "CLASSIFICATION RESULTS\n"
                        result_text += "=" * 50 + "\n\n"
                        for idx, (label, confidence) in enumerate(results, 1):
                            result_text += f"{idx}. {label}\n"
                            result_text += f"   Confidence: {confidence:.2%}\n\n"
                        
                        
        else:
            st.info("👈 Enter text in the input box and click 'Classify Topics' to begin.")
            
            # Show how it works
            st.markdown("### 🎯 How It Works")
            st.markdown("""
            The classifier analyzes Portuguese municipal texts using:
            
            1. **Feature Extraction**
               - TF-IDF vectors (n-grams 1-3)
               - BERTimbau contextual embeddings
            
            2. **Ensemble Models**
               - 1x LogisticRegression
               - 3x GradientBoosting (different hyperparameters)
            
            3. **Adaptive Weighting**
               - Rare labels: Higher LogReg weight
               - Common labels: Higher GB weight
            
            4. **Dynamic Thresholds**
               - Optimized per label on validation set
            """)


if __name__ == "__main__":
    main()