rocky250 commited on
Commit
cde0ec7
·
verified ·
1 Parent(s): 6bf49b4

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +230 -67
src/streamlit_app.py CHANGED
@@ -9,7 +9,6 @@ from transformers import AutoModel
9
 
10
  st.set_page_config(page_title="Political Sentiment", page_icon="🇧🇩", layout="wide")
11
 
12
-
13
  class BanglaPoliticalNet(nn.Module):
14
  def __init__(self, num_classes=5):
15
  super().__init__()
@@ -39,7 +38,9 @@ class BanglaPoliticalNet(nn.Module):
39
  cnn_features.append(F.relu(cnn_out))
40
 
41
  cnn_concat = torch.cat(cnn_features, dim=-1)
42
- attn_out, _ = self.attention(cnn_concat, cnn_concat, cnn_concat)
 
 
43
  attn_pooled = attn_out[:, 0, :]
44
 
45
  logits = self.classifier(attn_pooled)
@@ -49,53 +50,203 @@ st.markdown("""
49
  <style>
50
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
51
 
52
- html, body, [class*="css"] { font-family: 'Inter', sans-serif; color: #000000; }
53
- .stApp { background-color: #f4f6f9; }
54
- h1, h2, h3 { color: #111827 !important; }
55
- .main-card { background-color: white; padding: 30px; border-radius: 15px; box-shadow: 0 4px 10px rgba(0,0,0,0.08); margin-bottom: 25px; text-align: center; border: 1px solid #e5e7eb; }
56
- .result-title { color: #374151; font-size: 16px; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 10px; font-weight: 700; }
57
- .result-value { font-size: 48px; font-weight: 800; margin: 0; }
58
- .section-header { font-size: 20px; font-weight: 700; color: #1f2937; margin-bottom: 20px; border-left: 5px solid #2563eb; padding-left: 10px; }
59
- .model-card { background-color: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); margin-bottom: 15px; border: 1px solid #e5e7eb; }
60
- .model-card:hover { transform: translateY(-3px); box-shadow: 0 8px 15px rgba(0,0,0,0.1); }
61
- .model-name { color: #4b5563; font-size: 14px; font-weight: 600; margin-bottom: 8px; border-bottom: 2px solid #f3f4f6; padding-bottom: 5px; }
62
- .prob-row { margin-bottom: 15px; }
63
- .prob-label { font-size: 14px; color: #111827; font-weight: 600; margin-bottom: 5px; display: flex; justify-content: space-between; }
64
- .prob-bar-bg { width: 100%; height: 10px; background-color: #e5e7eb; border-radius: 5px; overflow: hidden; }
65
- .prob-bar-fill { height: 100%; border-radius: 5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </style>
67
  """, unsafe_allow_html=True)
68
 
69
  id2label = {0: 'Very Negative', 1: 'Negative', 2: 'Neutral', 3: 'Positive', 4: 'Very Positive'}
70
- label_colors = {'Very Negative': '#DC2626', 'Negative': '#EA580C', 'Neutral': '#6B7280', 'Positive': '#16A34A', 'Very Positive': '#15803D'}
 
 
 
 
 
 
71
 
72
  @st.cache_resource
73
  def load_models():
74
  models = {}
75
 
76
  standard_models = {
77
- "BanglaBERT": "rocky250/political-banglabert",
78
- "mBERT": "rocky250/political-mbert",
79
- "B-Base": "rocky250/political-bbase",
80
- "XLM-R": "rocky250/political-xlmr"
81
  }
82
 
83
  for name, repo in standard_models.items():
84
  try:
85
  tokenizer = AutoTokenizer.from_pretrained(repo)
86
  model = AutoModelForSequenceClassification.from_pretrained(repo)
87
- models[name] = (tokenizer, model)
88
  except:
89
- st.warning(f"Could not load {name}")
90
 
91
-
92
  try:
93
- creative_tokenizer = AutoTokenizer.from_pretrained("rocky250/bangla-political")
94
- creative_model = CreativeBanglaPoliticalNet(num_classes=5)
95
- creative_model.load_state_dict(torch.load("rocky250/bangla-political/pytorch_model.bin"))
96
- models["Creative Hybrid"] = (creative_tokenizer, creative_model)
 
97
  except:
98
- st.info("Bangla-political model not available - using standard models")
99
 
100
  return models
101
 
@@ -105,13 +256,12 @@ def predict_single_model(text, model_name):
105
  clean_text = normalize(text)
106
  tokenizer, model = models_dict[model_name]
107
 
108
- device = next(model.parameters()).device if hasattr(model, 'parameters') else torch.device('cpu')
109
-
110
  inputs = tokenizer(clean_text, return_tensors="pt", truncation=True, padding=True, max_length=128).to(device)
111
 
112
  with torch.no_grad():
113
  if "Creative" in model_name:
114
- logits = model(**inputs)
115
  else:
116
  outputs = model(**inputs)
117
  logits = outputs.logits
@@ -127,7 +277,7 @@ def predict_ensemble(text):
127
  all_probs = []
128
  all_predictions = []
129
 
130
- for name in models_dict.keys():
131
  try:
132
  pred, probs = predict_single_model(clean_text, name)
133
  all_probs.append(probs)
@@ -141,28 +291,39 @@ def predict_ensemble(text):
141
  return final_pred, all_predictions, avg_probs
142
  return "Error", [], np.zeros(5)
143
 
144
- st.markdown("<h1 style='text-align: center;'>Political Sentiment Analysis</h1>", unsafe_allow_html=True)
145
- st.markdown("<p style='text-align: center; color: #6B7280; font-size: 18px;'>Advanced Bengali Political Text Analysis with Custom Hybrid Architecture</p>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- # Model selection
148
  col1, col2 = st.columns([3, 1])
149
  with col1:
150
- user_input = st.text_area("", height=120, placeholder="এই বক্সে বাংলা রাজনৈতিক মন্তব্য লিখুন...", label_visibility="collapsed")
 
 
151
 
152
  with col2:
153
- st.markdown("<div style='height: 40px'></div>", unsafe_allow_html=True)
154
  mode = st.radio("Analysis Mode:",
155
  ["Single Model", "Ensemble (Recommended)"],
156
- horizontal=True,
157
- label_visibility="collapsed")
158
 
159
  model_options = {f"({i+1}) {name}": name for i, name in enumerate(models_dict.keys())}
160
  selected_model = st.selectbox("Select Model:", list(model_options.keys()), index=0)
161
 
162
- analyze_btn = st.button("Analyze Sentiment", type="primary", use_container_width=True)
163
 
164
  if analyze_btn and user_input.strip():
165
- with st.spinner('Processing with advanced models...'):
166
  if mode == "Single Model":
167
  model_name = model_options[selected_model]
168
  final_res, probs = predict_single_model(user_input, model_name)
@@ -170,9 +331,10 @@ if analyze_btn and user_input.strip():
170
  col1, col2 = st.columns([1, 2])
171
  with col1:
172
  st.markdown(f"""
173
- <div class="main-card" style="border-top: 6px solid {label_colors[final_res]}">
174
  <div class="result-title">{model_name}</div>
175
  <div class="result-value" style="color: {label_colors[final_res]}">{final_res}</div>
 
176
  </div>
177
  """, unsafe_allow_html=True)
178
 
@@ -182,30 +344,29 @@ if analyze_btn and user_input.strip():
182
  label = id2label[i]
183
  prob = probs[i] * 100
184
  color = label_colors[label]
185
- opacity = "1.0" if prob > 5 else "0.4"
186
 
187
  st.markdown(f"""
188
- <div class="prob-row" style="opacity: {opacity}">
189
  <div class="prob-label">
190
- <span>{label}</span>
191
- <span>{prob:.1f}%</span>
192
  </div>
193
  <div class="prob-bar-bg">
194
- <div class="prob-bar-fill" style="width: {min(prob, 100)}%; background-color: {color};"></div>
195
  </div>
196
  </div>
197
  """, unsafe_allow_html=True)
198
 
199
- else: # Ensemble mode
200
  final_res, all_votes, avg_probs = predict_ensemble(user_input)
201
 
202
- main_col, details_col = st.columns([1, 1.5], gap="large")
203
 
204
  with main_col:
205
  st.markdown(f"""
206
- <div class="main-card" style="border-top: 6px solid {label_colors[final_res]}">
207
- <div class="result-title">Ensemble Consensus</div>
208
- <div class="result-value" style="color: {label_colors[final_res]}">{final_res}</div>
209
  </div>
210
  """, unsafe_allow_html=True)
211
 
@@ -215,43 +376,45 @@ if analyze_btn and user_input.strip():
215
  label = id2label[i]
216
  prob = avg_probs[i] * 100
217
  color = label_colors[label]
218
- opacity = "1.0" if prob > 1 else "0.3"
219
 
220
  st.markdown(f"""
221
- <div class="prob-row" style="opacity: {opacity}">
222
  <div class="prob-label">
223
  <span>{label}</span>
224
- <span>{prob:.1f}%</span>
225
  </div>
226
  <div class="prob-bar-bg">
227
- <div class="prob-bar-fill" style="width: {min(prob, 100)}%; background-color: {color};"></div>
228
  </div>
229
  </div>
230
  """, unsafe_allow_html=True)
231
 
232
  with details_col:
233
  st.markdown('<div class="section-header">Individual Model Votes</div>', unsafe_allow_html=True)
234
- cols = st.columns(2)
235
- for idx, (name, vote) in enumerate(zip(models_dict.keys(), all_votes[:4])):
236
- with cols[idx % 2]:
237
  color = label_colors[vote]
238
  st.markdown(f"""
239
  <div class="model-card">
240
  <div class="model-name">{name}</div>
241
- <div style="color: {color}; font-weight: 700; font-size: 20px;">{vote}</div>
242
  </div>
243
  """, unsafe_allow_html=True)
244
 
245
  elif analyze_btn and not user_input.strip():
246
- st.warning("অনুগ্রহ করে কিছু টেক্সট লিখুন!")
247
 
248
- with st.expander("Example Political Texts"):
249
  examples = [
250
  "সরকারের এই নীতি দেশকে ধ্বংসের দিকে নিয়ে যাবে!",
251
  "চমৎকার সিদ্ধান্ত! দেশের জন্য গর্বিত। ভালো চলবে!",
252
  "রাজনীতির কোনো পরিবর্তন হবে না, সব একই রকম"
253
  ]
254
- for example in examples:
255
- if st.button(example, key=example[:20]):
256
- st.session_state.user_input = example
257
- st.rerun()
 
 
 
 
9
 
10
  st.set_page_config(page_title="Political Sentiment", page_icon="🇧🇩", layout="wide")
11
 
 
12
  class BanglaPoliticalNet(nn.Module):
13
  def __init__(self, num_classes=5):
14
  super().__init__()
 
38
  cnn_features.append(F.relu(cnn_out))
39
 
40
  cnn_concat = torch.cat(cnn_features, dim=-1)
41
+ proj = nn.Linear(384, self.hidden_size).to(input_ids.device)
42
+ attn_input = proj(cnn_concat)
43
+ attn_out, _ = self.attention(attn_input, attn_input, attn_input)
44
  attn_pooled = attn_out[:, 0, :]
45
 
46
  logits = self.classifier(attn_pooled)
 
50
  <style>
51
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
52
 
53
+ html, body, [class*="css"] {
54
+ font-family: 'Inter', sans-serif !important;
55
+ color: #1f2937 !important;
56
+ }
57
+
58
+ .stApp {
59
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
60
+ }
61
+
62
+ h1, h2, h3 {
63
+ color: #ffffff !important;
64
+ text-shadow: 0 2px 4px rgba(0,0,0,0.3);
65
+ }
66
+
67
+ .stTextArea textarea {
68
+ background-color: #ffffff !important;
69
+ color: #1f2937 !important;
70
+ border: 2px solid #e5e7eb !important;
71
+ border-radius: 12px !important;
72
+ padding: 16px !important;
73
+ font-size: 16px !important;
74
+ }
75
+
76
+ .stTextArea label {
77
+ color: #ffffff !important;
78
+ font-weight: 700 !important;
79
+ }
80
+
81
+ .main-card {
82
+ background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
83
+ padding: 35px;
84
+ border-radius: 20px;
85
+ box-shadow: 0 20px 40px rgba(0,0,0,0.15);
86
+ margin-bottom: 25px;
87
+ text-align: center;
88
+ border: 1px solid rgba(255,255,255,0.3);
89
+ backdrop-filter: blur(10px);
90
+ }
91
+
92
+ .result-title {
93
+ color: #475569 !important;
94
+ font-size: 16px;
95
+ text-transform: uppercase;
96
+ letter-spacing: 1.5px;
97
+ margin-bottom: 12px;
98
+ font-weight: 700;
99
+ }
100
+
101
+ .result-value {
102
+ font-size: 52px;
103
+ font-weight: 800;
104
+ margin: 0;
105
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1);
106
+ }
107
+
108
+ .section-header {
109
+ font-size: 22px;
110
+ font-weight: 700;
111
+ color: #1e293b !important;
112
+ margin-bottom: 20px;
113
+ border-left: 6px solid #3b82f6;
114
+ padding-left: 15px;
115
+ background: rgba(255,255,255,0.8);
116
+ padding: 12px 20px;
117
+ border-radius: 10px;
118
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
119
+ }
120
+
121
+ .model-card {
122
+ background: linear-gradient(145deg, #ffffff 0%, #f1f5f9 100%);
123
+ padding: 25px;
124
+ border-radius: 16px;
125
+ box-shadow: 0 8px 25px rgba(0,0,0,0.12);
126
+ margin-bottom: 20px;
127
+ border: 1px solid rgba(255,255,255,0.5);
128
+ transition: all 0.3s ease;
129
+ }
130
+
131
+ .model-card:hover {
132
+ transform: translateY(-5px);
133
+ box-shadow: 0 20px 40px rgba(0,0,0,0.2);
134
+ }
135
+
136
+ .model-name {
137
+ color: #334155 !important;
138
+ font-size: 15px;
139
+ font-weight: 700;
140
+ margin-bottom: 12px;
141
+ border-bottom: 3px solid #e2e8f0;
142
+ padding-bottom: 8px;
143
+ }
144
+
145
+ .prob-row {
146
+ margin-bottom: 18px;
147
+ background: rgba(255,255,255,0.9);
148
+ padding: 15px;
149
+ border-radius: 12px;
150
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
151
+ }
152
+
153
+ .prob-label {
154
+ font-size: 15px;
155
+ color: #1e293b !important;
156
+ font-weight: 700;
157
+ margin-bottom: 8px;
158
+ display: flex;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ }
162
+
163
+ .prob-bar-bg {
164
+ width: 100%;
165
+ height: 14px;
166
+ background: linear-gradient(90deg, #f1f5f9, #e2e8f0);
167
+ border-radius: 7px;
168
+ overflow: hidden;
169
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
170
+ }
171
+
172
+ .prob-bar-fill {
173
+ height: 100%;
174
+ border-radius: 7px;
175
+ transition: width 0.8s ease;
176
+ box-shadow: 0 0 20px rgba(0,0,0,0.2);
177
+ }
178
+
179
+ .stButton > button {
180
+ background: linear-gradient(45deg, #3b82f6, #1d4ed8) !important;
181
+ color: white !important;
182
+ border: none !important;
183
+ border-radius: 12px !important;
184
+ padding: 14px 28px !important;
185
+ font-weight: 700 !important;
186
+ font-size: 16px !important;
187
+ box-shadow: 0 8px 25px rgba(59,130,246,0.4) !important;
188
+ transition: all 0.3s ease !important;
189
+ }
190
+
191
+ .stButton > button:hover {
192
+ transform: translateY(-2px) !important;
193
+ box-shadow: 0 12px 35px rgba(59,130,246,0.6) !important;
194
+ }
195
+
196
+ .stRadio > div > label {
197
+ color: #ffffff !important;
198
+ font-weight: 600 !important;
199
+ }
200
+
201
+ .stSelectbox > label {
202
+ color: #ffffff !important;
203
+ font-weight: 600 !important;
204
+ }
205
+
206
+ .stExpander {
207
+ background: rgba(255,255,255,0.1) !important;
208
+ border-radius: 12px !important;
209
+ border: 1px solid rgba(255,255,255,0.2) !important;
210
+ }
211
  </style>
212
  """, unsafe_allow_html=True)
213
 
214
  id2label = {0: 'Very Negative', 1: 'Negative', 2: 'Neutral', 3: 'Positive', 4: 'Very Positive'}
215
+ label_colors = {
216
+ 'Very Negative': '#ef4444',
217
+ 'Negative': '#f97316',
218
+ 'Neutral': '#64748b',
219
+ 'Positive': '#22c55e',
220
+ 'Very Positive': '#16a34a'
221
+ }
222
 
223
  @st.cache_resource
224
  def load_models():
225
  models = {}
226
 
227
  standard_models = {
228
+ "BanglaBERT": "rocky250/Sentiment-banglabert",
229
+ "mBERT": "rocky250/Sentiment-mbert",
230
+ "B-Base": "rocky250/Sentiment-bbase",
231
+ "XLM-R": "rocky250/Sentiment-xlmr"
232
  }
233
 
234
  for name, repo in standard_models.items():
235
  try:
236
  tokenizer = AutoTokenizer.from_pretrained(repo)
237
  model = AutoModelForSequenceClassification.from_pretrained(repo)
238
+ models[name] = (tokenizer, model.to('cuda' if torch.cuda.is_available() else 'cpu'))
239
  except:
240
+ continue
241
 
 
242
  try:
243
+ SA_tokenizer = AutoTokenizer.from_pretrained("rocky250/bangla-political")
244
+ model_SA = BanglaPoliticalNet(num_classes=5)
245
+ model_SA.load_state_dict(torch.load("rocky250/bangla-political/pytorch_model.bin", map_location='cpu'))
246
+ model_SA = model_SA.to('cuda' if torch.cuda.is_available() else 'cpu')
247
+ models["Creative Model"] = (SA_tokenizer, model_SA)
248
  except:
249
+ pass
250
 
251
  return models
252
 
 
256
  clean_text = normalize(text)
257
  tokenizer, model = models_dict[model_name]
258
 
259
+ device = next(model.parameters()).device
 
260
  inputs = tokenizer(clean_text, return_tensors="pt", truncation=True, padding=True, max_length=128).to(device)
261
 
262
  with torch.no_grad():
263
  if "Creative" in model_name:
264
+ logits = model(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask'])
265
  else:
266
  outputs = model(**inputs)
267
  logits = outputs.logits
 
277
  all_probs = []
278
  all_predictions = []
279
 
280
+ for name in list(models_dict.keys())[:4]:
281
  try:
282
  pred, probs = predict_single_model(clean_text, name)
283
  all_probs.append(probs)
 
291
  return final_pred, all_predictions, avg_probs
292
  return "Error", [], np.zeros(5)
293
 
294
+ st.markdown("""
295
+ <div style='
296
+ text-align: center;
297
+ background: rgba(255,255,255,0.1);
298
+ padding: 30px;
299
+ border-radius: 20px;
300
+ margin-bottom: 30px;
301
+ backdrop-filter: blur(20px);
302
+ '>
303
+ <h1 style='font-size: 3.5rem; margin: 0; background: linear-gradient(45deg, #ffffff, #e2e8f0); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 800;'>🇧🇩 Political Sentiment Analysis</h1>
304
+ <p style='font-size: 1.3rem; color: rgba(255,255,255,0.95); margin: 10px 0 0 0; font-weight: 500;'>Advanced Bengali Political Text Analysis with Custom Hybrid Architecture</p>
305
+ </div>
306
+ """, unsafe_allow_html=True)
307
 
 
308
  col1, col2 = st.columns([3, 1])
309
  with col1:
310
+ user_input = st.text_area("Enter Bengali political text:", height=140,
311
+ placeholder="এই বক্সে বাংলা রাজনৈতিক মন্তব্য লিখুন...",
312
+ help="Type or paste Bengali political text for sentiment analysis")
313
 
314
  with col2:
315
+ st.markdown("<div style='height: 20px'></div>", unsafe_allow_html=True)
316
  mode = st.radio("Analysis Mode:",
317
  ["Single Model", "Ensemble (Recommended)"],
318
+ horizontal=True)
 
319
 
320
  model_options = {f"({i+1}) {name}": name for i, name in enumerate(models_dict.keys())}
321
  selected_model = st.selectbox("Select Model:", list(model_options.keys()), index=0)
322
 
323
+ analyze_btn = st.button("ANALYZE SENTIMENT", type="primary", use_container_width=True)
324
 
325
  if analyze_btn and user_input.strip():
326
+ with st.spinner('Processing with advanced AI models...'):
327
  if mode == "Single Model":
328
  model_name = model_options[selected_model]
329
  final_res, probs = predict_single_model(user_input, model_name)
 
331
  col1, col2 = st.columns([1, 2])
332
  with col1:
333
  st.markdown(f"""
334
+ <div class="main-card" style="border-top: 8px solid {label_colors[final_res]}">
335
  <div class="result-title">{model_name}</div>
336
  <div class="result-value" style="color: {label_colors[final_res]}">{final_res}</div>
337
+ <div style="font-size: 18px; color: #64748b; margin-top: 15px;">Confidence: {max(probs)*100:.1f}%</div>
338
  </div>
339
  """, unsafe_allow_html=True)
340
 
 
344
  label = id2label[i]
345
  prob = probs[i] * 100
346
  color = label_colors[label]
 
347
 
348
  st.markdown(f"""
349
+ <div class="prob-row">
350
  <div class="prob-label">
351
+ <span style="font-weight: 700;">{label}</span>
352
+ <span style="font-weight: 700; color: {color};">{prob:.1f}%</span>
353
  </div>
354
  <div class="prob-bar-bg">
355
+ <div class="prob-bar-fill" style="width: {min(prob, 100)}%; background: linear-gradient(90deg, {color}, {color}cc);"></div>
356
  </div>
357
  </div>
358
  """, unsafe_allow_html=True)
359
 
360
+ else:
361
  final_res, all_votes, avg_probs = predict_ensemble(user_input)
362
 
363
+ main_col, details_col = st.columns([1, 1.4])
364
 
365
  with main_col:
366
  st.markdown(f"""
367
+ <div class="main-card" style="border-top: 8px solid {label_colors[final_res]}; box-shadow: 0 25px 50px rgba(0,0,0,0.2);">
368
+ <div class="result-title" style="font-size: 18px;">ENSEMBLE CONSENSUS</div>
369
+ <div class="result-value" style="color: {label_colors[final_res]}; font-size: 60px;">{final_res}</div>
370
  </div>
371
  """, unsafe_allow_html=True)
372
 
 
376
  label = id2label[i]
377
  prob = avg_probs[i] * 100
378
  color = label_colors[label]
 
379
 
380
  st.markdown(f"""
381
+ <div class="prob-row">
382
  <div class="prob-label">
383
  <span>{label}</span>
384
+ <span style="color: {color};">{prob:.1f}%</span>
385
  </div>
386
  <div class="prob-bar-bg">
387
+ <div class="prob-bar-fill" style="width: {min(prob, 100)}%; background: linear-gradient(90deg, {color}, {color}cc);"></div>
388
  </div>
389
  </div>
390
  """, unsafe_allow_html=True)
391
 
392
  with details_col:
393
  st.markdown('<div class="section-header">Individual Model Votes</div>', unsafe_allow_html=True)
394
+ model_cols = st.columns(2)
395
+ for idx, (name, vote) in enumerate(zip(list(models_dict.keys())[:4], all_votes[:4])):
396
+ with model_cols[idx % 2]:
397
  color = label_colors[vote]
398
  st.markdown(f"""
399
  <div class="model-card">
400
  <div class="model-name">{name}</div>
401
+ <div style="color: {color}; font-weight: 800; font-size: 24px; margin-top: 8px;">{vote}</div>
402
  </div>
403
  """, unsafe_allow_html=True)
404
 
405
  elif analyze_btn and not user_input.strip():
406
+ st.error("অনুগ্রহ করে কিছু টেক্সট লিখুন!")
407
 
408
+ with st.expander("Example Political Texts", expanded=False):
409
  examples = [
410
  "সরকারের এই নীতি দেশকে ধ্বংসের দিকে নিয়ে যাবে!",
411
  "চমৎকার সিদ্ধান্ত! দেশের জন্য গর্বিত। ভালো চলবে!",
412
  "রাজনীতির কোনো পরিবর্তন হবে না, সব একই রকম"
413
  ]
414
+ example_cols = st.columns(3)
415
+ for idx, example in enumerate(examples):
416
+ with example_cols[idx]:
417
+ if st.button(example[:40] + "..." if len(example) > 40 else example,
418
+ use_container_width=True):
419
+ st.session_state.user_input = example
420
+ st.rerun()