Akilashamnaka12 commited on
Commit
c1742e5
Β·
verified Β·
1 Parent(s): 4f6cba1

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +1724 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,1726 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import string, time, re, random
5
+ from collections import Counter
6
+
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # PAGE CONFIG
9
+ # ─────────────────────────────────────────────────────────────────────────────
10
+ st.set_page_config(
11
+ page_title="NewsLens AI β€” Daily Mirror Intelligence",
12
+ page_icon="β—‰",
13
+ layout="wide",
14
+ initial_sidebar_state="collapsed",
15
+ )
16
+
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+ # NLTK
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ import nltk
21
+
22
+ @st.cache_resource(show_spinner=False)
23
+ def _nltk():
24
+ for p in ["punkt","punkt_tab","stopwords","wordnet"]:
25
+ nltk.download(p, quiet=True)
26
+
27
+ _nltk()
28
+ from nltk.tokenize import word_tokenize
29
+ from nltk.corpus import stopwords
30
+ from nltk.stem import WordNetLemmatizer
31
+
32
+ # ─────────────────────────────────────────────────────────────────────────────
33
+ # MODELS
34
+ # ─────────────────────────────────────────────────────────────────────────────
35
+ @st.cache_resource(show_spinner=False)
36
+ def load_clf():
37
+ from transformers import pipeline
38
+ return pipeline("text-classification",
39
+ model="Akilashamnaka12/news_classifier_model",
40
+ truncation=True, max_length=512)
41
+
42
+ @st.cache_resource(show_spinner=False)
43
+ def load_qa():
44
+ from transformers import AutoTokenizer, AutoModelForQuestionAnswering, pipeline
45
+ n = "deepset/roberta-base-squad2"
46
+ return pipeline("question-answering",
47
+ model=AutoModelForQuestionAnswering.from_pretrained(n),
48
+ tokenizer=AutoTokenizer.from_pretrained(n))
49
+
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ # CONSTANTS
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+ LABEL_MAP = {"LABEL_0":"Business","LABEL_1":"Opinion",
54
+ "LABEL_2":"Political_gossip","LABEL_3":"Sports","LABEL_4":"World_news"}
55
+
56
+ CATS = {
57
+ "Business": {"icon":"πŸ’Ό","color":"#0071e3","bg":"#f0f7ff","desc":"Finance & Economy"},
58
+ "Opinion": {"icon":"πŸ’¬","color":"#34c759","bg":"#f0fdf4","desc":"Views & Editorials"},
59
+ "Political_gossip": {"icon":"πŸ›οΈ", "color":"#ff3b30","bg":"#fff1f2","desc":"Politics & Governance"},
60
+ "Sports": {"icon":"⚽","color":"#ff9f0a","bg":"#fff7ed","desc":"Matches & Athletics"},
61
+ "World_news": {"icon":"🌍","color":"#5e5ce6","bg":"#f5f3ff","desc":"International Affairs"},
62
+ }
63
+
64
+ _sw = set(stopwords.words("english"))
65
+ _lem = WordNetLemmatizer()
66
+
67
+ def preprocess(t):
68
+ if not isinstance(t,str) or not t.strip(): return ""
69
+ t = t.lower().translate(str.maketrans("","",string.punctuation))
70
+ tokens = [_lem.lemmatize(w) for w in word_tokenize(t)
71
+ if w not in _sw and w.isalpha()]
72
+ return " ".join(tokens)
73
+
74
+ def resolve(r): return LABEL_MAP.get(r, r)
75
+
76
+ def word_cloud_html(text, n=65):
77
+ words = re.findall(r'\b[a-zA-Z]{4,}\b', text.lower())
78
+ stops = {"this","that","with","have","will","from","they","been","were",
79
+ "their","there","also","which","when","into","than","then","about",
80
+ "more","over","some","such","just","very","even","only","most","said"}
81
+ freq = Counter(w for w in words if w not in stops)
82
+ top = freq.most_common(n)
83
+ if not top: return "<p style='color:#86868b;text-align:center'>Not enough text.</p>"
84
+ mx = top[0][1]
85
+ pal = ["#0071e3","#34c759","#ff3b30","#ff9f0a","#5e5ce6","#00c7be","#ff6b9d"]
86
+ out = ""
87
+ for word,cnt in top:
88
+ sz = 0.76 + (cnt/mx)*1.85
89
+ col = random.choice(pal)
90
+ op = 0.45 + (cnt/mx)*0.55
91
+ fw = 300 + int((cnt/mx)*500)
92
+ rot = random.choice([-3,-1,0,0,0,1,3])
93
+ out += (f'<span style="font-size:{sz:.2f}rem;color:{col};opacity:{op:.2f};'
94
+ f'font-weight:{fw};display:inline-block;margin:3px 8px;'
95
+ f'transform:rotate({rot}deg);cursor:default;transition:all .2s;"'
96
+ f' onmouseover="this.style.opacity=1;this.style.transform=\'scale(1.22)\'"'
97
+ f' onmouseout="this.style.opacity={op:.2f};this.style.transform=\'rotate({rot}deg)\'">'
98
+ f'{word}</span>')
99
+ return f'<div style="text-align:center;line-height:2.6;padding:1.5rem 1rem">{out}</div>'
100
+
101
+ # ─────────────────────────────────────────────────────────────────────────────
102
+ # ═══════════════════════ MASTER CSS ═══════════════════════════════════════════
103
+ # ─────────────────────────────────────────────────────────────────────────────
104
+ st.markdown("""
105
+ <style>
106
+ /* ══════════════════════════════════════════════
107
+ TOKENS β€” Apple design system
108
+ ══════════════════════════════════════════════ */
109
+ :root{
110
+ /* Backgrounds */
111
+ --bg-primary: #ffffff;
112
+ --bg-secondary: #f5f5f7;
113
+ --bg-tertiary: #fbfbfd;
114
+ --bg-dark: #1d1d1f;
115
+ --bg-darker: #000000;
116
+
117
+ /* Text */
118
+ --text-primary: #1d1d1f;
119
+ --text-secondary: #6e6e73;
120
+ --text-tertiary: #86868b;
121
+ --text-on-dark: #f5f5f7;
122
+ --text-on-dark-2: rgba(245,245,247,0.6);
123
+
124
+ /* Accent */
125
+ --blue: #0071e3;
126
+ --blue-hv: #0077ed;
127
+ --green: #34c759;
128
+ --red: #ff3b30;
129
+ --orange: #ff9f0a;
130
+ --violet: #5e5ce6;
131
+
132
+ /* Structure */
133
+ --border: rgba(0,0,0,0.08);
134
+ --border-mid: rgba(0,0,0,0.12);
135
+ --border-dark: rgba(255,255,255,0.10);
136
+ --radius-sm: 10px;
137
+ --radius-md: 18px;
138
+ --radius-lg: 24px;
139
+ --radius-pill: 999px;
140
+
141
+ /* Shadows */
142
+ --shadow-xs: 0 1px 3px rgba(0,0,0,0.05),0 2px 8px rgba(0,0,0,0.04);
143
+ --shadow-sm: 0 2px 6px rgba(0,0,0,0.06),0 6px 20px rgba(0,0,0,0.05);
144
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.08),0 16px 48px rgba(0,0,0,0.06);
145
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.12),0 32px 64px rgba(0,0,0,0.08);
146
+
147
+ /* Spacing */
148
+ --page-max: 1040px;
149
+ --page-pad: 2.5rem;
150
+ --section-v: 5rem;
151
+ }
152
+
153
+ /* ══════════════════════════════════════════════
154
+ KILL STREAMLIT CHROME
155
+ ══════════════════════════════════════════════ */
156
+ #MainMenu,footer,header,.stDeployButton,
157
+ [data-testid="stToolbar"],
158
+ section[data-testid="stSidebar"]{display:none!important}
159
+ .block-container{padding:0!important;max-width:100%!important}
160
+ .stApp{background:var(--bg-primary)!important}
161
+
162
+ /* ══════════════════════════════════════════════
163
+ BASE TYPOGRAPHY
164
+ ══════════════════════════════════════════════ */
165
+ @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap');
166
+
167
+ html,body,[class*="css"]{
168
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif;
169
+ color:var(--text-primary);
170
+ background:var(--bg-primary);
171
+ -webkit-font-smoothing:antialiased;
172
+ -moz-osx-font-smoothing:grayscale;
173
+ }
174
+
175
+ /* ══════════════════════════════════════════════
176
+ FROSTED GLASS NAVIGATION BAR
177
+ ══════════════════════════════════════════════ */
178
+ #nav{
179
+ position:sticky;top:0;z-index:1000;
180
+ height:52px;
181
+ background:rgba(255,255,255,0.80);
182
+ backdrop-filter:blur(20px) saturate(180%);
183
+ -webkit-backdrop-filter:blur(20px) saturate(180%);
184
+ border-bottom:1px solid var(--border);
185
+ display:flex;align-items:center;
186
+ padding:0 var(--page-pad);
187
+ }
188
+ .nav-inner{
189
+ max-width:var(--page-max);margin:0 auto;width:100%;
190
+ display:flex;align-items:center;gap:0;
191
+ }
192
+ .nav-logo{
193
+ font-size:1.05rem;font-weight:700;
194
+ letter-spacing:-.02em;color:var(--text-primary);
195
+ white-space:nowrap;margin-right:auto;
196
+ }
197
+ .nav-logo span{color:var(--blue);}
198
+ .nav-items{
199
+ display:flex;align-items:center;gap:0;
200
+ position:absolute;left:50%;transform:translateX(-50%);
201
+ }
202
+ .nav-item{
203
+ font-size:.82rem;font-weight:400;
204
+ color:var(--text-secondary);
205
+ padding:0 1.1rem;cursor:pointer;
206
+ transition:color .15s;
207
+ border:none;background:none;
208
+ letter-spacing:-.01em;
209
+ }
210
+ .nav-item:hover{color:var(--text-primary);}
211
+ .nav-item.on{color:var(--text-primary);font-weight:500;}
212
+ .nav-badge{
213
+ font-size:.68rem;font-weight:500;
214
+ background:var(--blue);color:#fff;
215
+ border-radius:var(--radius-pill);
216
+ padding:2px 8px;margin-left:auto;
217
+ letter-spacing:.02em;
218
+ }
219
+
220
+ /* ══════════════════════════════════════════════
221
+ HERO β€” Cinematic full-bleed
222
+ ══════════════════════════════════════════════ */
223
+ #hero{
224
+ position:relative;
225
+ min-height:88vh;
226
+ display:flex;align-items:flex-end;
227
+ overflow:hidden;
228
+ background:var(--bg-darker);
229
+ }
230
+ .hero-bg{
231
+ position:absolute;inset:0;
232
+ background-image:url('https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=1920&q=85');
233
+ background-size:cover;background-position:center 30%;
234
+ opacity:.55;
235
+ transition:opacity .5s;
236
+ }
237
+ .hero-overlay{
238
+ position:absolute;inset:0;
239
+ background:linear-gradient(
240
+ to bottom,
241
+ rgba(0,0,0,0) 0%,
242
+ rgba(0,0,0,0) 30%,
243
+ rgba(0,0,0,.55) 75%,
244
+ rgba(0,0,0,.90) 100%
245
+ );
246
+ }
247
+ .hero-mesh{
248
+ position:absolute;inset:0;
249
+ background:
250
+ radial-gradient(ellipse at 20% 60%,rgba(0,113,227,.20) 0%,transparent 55%),
251
+ radial-gradient(ellipse at 80% 30%,rgba(94,92,230,.15) 0%,transparent 50%);
252
+ }
253
+ .hero-content{
254
+ position:relative;z-index:2;
255
+ width:100%;
256
+ max-width:var(--page-max);
257
+ margin:0 auto;
258
+ padding:0 var(--page-pad) 5rem;
259
+ }
260
+ .hero-kicker{
261
+ display:inline-flex;align-items:center;gap:8px;
262
+ border:1px solid rgba(255,255,255,.20);
263
+ background:rgba(255,255,255,.08);
264
+ backdrop-filter:blur(12px);
265
+ border-radius:var(--radius-pill);
266
+ padding:5px 16px;margin-bottom:1.6rem;
267
+ font-size:.71rem;font-weight:500;
268
+ color:rgba(255,255,255,.75);
269
+ letter-spacing:.08em;text-transform:uppercase;
270
+ }
271
+ .kicker-dot{
272
+ width:6px;height:6px;border-radius:50%;
273
+ background:#34c759;
274
+ box-shadow:0 0 0 0 rgba(52,199,89,.5);
275
+ animation:ping 2s ease-in-out infinite;
276
+ }
277
+ @keyframes ping{
278
+ 0%,100%{box-shadow:0 0 0 0 rgba(52,199,89,.4);}
279
+ 50% {box-shadow:0 0 0 8px rgba(52,199,89,0);}
280
+ }
281
+ h1.h-display{
282
+ font-size:clamp(3.2rem,6.5vw,6rem);
283
+ font-weight:700;letter-spacing:-.04em;
284
+ line-height:1.0;color:#ffffff;margin:0 0 1.1rem;
285
+ }
286
+ h1.h-display em{
287
+ font-style:normal;
288
+ background:linear-gradient(90deg,#60b0ff 0%,#a78bfa 50%,#34c759 100%);
289
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;
290
+ background-clip:text;
291
+ }
292
+ .h-sub{
293
+ font-size:1.15rem;font-weight:300;
294
+ color:rgba(255,255,255,.65);
295
+ line-height:1.6;letter-spacing:-.01em;
296
+ max-width:540px;margin-bottom:2.2rem;
297
+ }
298
+ .h-actions{display:flex;gap:14px;flex-wrap:wrap;}
299
+ .btn-primary{
300
+ background:var(--blue);color:#fff;
301
+ padding:11px 26px;border-radius:var(--radius-pill);
302
+ font-size:.88rem;font-weight:600;
303
+ letter-spacing:-.01em;border:none;cursor:pointer;
304
+ transition:all .2s;text-decoration:none;display:inline-block;
305
+ }
306
+ .btn-primary:hover{background:var(--blue-hv);transform:translateY(-1px);
307
+ box-shadow:0 6px 20px rgba(0,113,227,.35);}
308
+ .btn-ghost{
309
+ background:rgba(255,255,255,.12);
310
+ border:1px solid rgba(255,255,255,.25);
311
+ color:#fff;
312
+ padding:10px 24px;border-radius:var(--radius-pill);
313
+ font-size:.88rem;font-weight:500;
314
+ backdrop-filter:blur(8px);cursor:pointer;
315
+ transition:all .2s;text-decoration:none;display:inline-block;
316
+ }
317
+ .btn-ghost:hover{background:rgba(255,255,255,.18);transform:translateY(-1px);}
318
+
319
+ /* ══════════════════════════════════════════════
320
+ FEATURE BAR β€” Apple product row
321
+ ══════════════════════════════════════════════ */
322
+ #feat-bar{
323
+ display:grid;grid-template-columns:repeat(4,1fr);
324
+ background:var(--bg-primary);
325
+ border-bottom:1px solid var(--border);
326
+ }
327
+ .fb-cell{
328
+ padding:1.8rem 2rem;
329
+ border-right:1px solid var(--border);
330
+ display:flex;align-items:center;gap:14px;
331
+ transition:background .2s;
332
+ }
333
+ .fb-cell:last-child{border-right:none;}
334
+ .fb-cell:hover{background:var(--bg-secondary);}
335
+ .fb-icon{
336
+ width:44px;height:44px;border-radius:var(--radius-sm);
337
+ display:flex;align-items:center;justify-content:center;
338
+ font-size:1.3rem;flex-shrink:0;
339
+ }
340
+ .fb-title{font-size:.88rem;font-weight:600;color:var(--text-primary);letter-spacing:-.01em;}
341
+ .fb-sub{font-size:.75rem;color:var(--text-secondary);margin-top:2px;line-height:1.4;}
342
+
343
+ /* ══════════════════════════════════════════════
344
+ PAGE SECTIONS
345
+ ═══════════════════════════════════════��══════ */
346
+ .section{padding:var(--section-v) var(--page-pad);background:var(--bg-primary);}
347
+ .section-alt{padding:var(--section-v) var(--page-pad);background:var(--bg-secondary);}
348
+ .section-dark{padding:var(--section-v) var(--page-pad);background:var(--bg-dark);}
349
+ .section-inner{max-width:var(--page-max);margin:0 auto;}
350
+
351
+ /* ══════════════════════════════════════════════
352
+ SECTION HEADERS
353
+ ══════════════════════════════════════════════ */
354
+ .s-label{
355
+ font-size:.71rem;font-weight:600;
356
+ letter-spacing:.1em;text-transform:uppercase;
357
+ color:var(--blue);margin-bottom:.6rem;display:block;
358
+ }
359
+ .s-label-green{color:var(--green)!important;}
360
+ .s-label-violet{color:var(--violet)!important;}
361
+ .s-label-light{color:rgba(0,113,227,.75)!important;}
362
+
363
+ h2.s-h{
364
+ font-size:clamp(2rem,4vw,3rem);font-weight:700;
365
+ letter-spacing:-.035em;line-height:1.08;
366
+ color:var(--text-primary);margin:0 0 .7rem;
367
+ }
368
+ h2.s-h-light{color:var(--text-on-dark)!important;}
369
+ .s-p{
370
+ font-size:1.05rem;font-weight:300;
371
+ color:var(--text-secondary);line-height:1.65;
372
+ letter-spacing:-.01em;max-width:500px;margin-bottom:3rem;
373
+ }
374
+ .s-p-light{color:var(--text-on-dark-2)!important;}
375
+
376
+ /* ══════════════════════════════════════════════
377
+ CARDS
378
+ ══════════════════════════════════════════════ */
379
+ .card{
380
+ background:var(--bg-primary);
381
+ border:1px solid var(--border);
382
+ border-radius:var(--radius-md);
383
+ overflow:hidden;
384
+ box-shadow:var(--shadow-xs);
385
+ transition:box-shadow .3s,transform .25s;
386
+ }
387
+ .card:hover{box-shadow:var(--shadow-md);transform:translateY(-3px);}
388
+ .card-alt{background:var(--bg-secondary);}
389
+ .card-dark{background:#2c2c2e;border-color:rgba(255,255,255,.08);}
390
+ .card-body{padding:1.8rem 2rem;}
391
+ .card-label{
392
+ font-size:.7rem;font-weight:600;
393
+ letter-spacing:.1em;text-transform:uppercase;
394
+ color:var(--blue);margin-bottom:.5rem;display:block;
395
+ }
396
+ .card-title{
397
+ font-size:1.15rem;font-weight:600;
398
+ letter-spacing:-.02em;color:var(--text-primary);
399
+ margin-bottom:.35rem;line-height:1.25;
400
+ }
401
+ .card-title-light{color:var(--text-on-dark)!important;}
402
+ .card-desc{font-size:.83rem;color:var(--text-secondary);line-height:1.6;font-weight:400;}
403
+ .card-desc-light{color:var(--text-on-dark-2)!important;}
404
+
405
+ /* ══════════════════════════════════════════════
406
+ IMAGE HERO CARDS (section banners)
407
+ ══════════════════════════════════════════════ */
408
+ .img-card{
409
+ position:relative;border-radius:var(--radius-lg);
410
+ overflow:hidden;min-height:200px;
411
+ display:flex;align-items:flex-end;margin-bottom:2.5rem;
412
+ box-shadow:var(--shadow-md);
413
+ }
414
+ .img-card-bg{
415
+ position:absolute;inset:0;
416
+ background-size:cover;background-position:center;
417
+ filter:brightness(.38) saturate(.7);
418
+ }
419
+ .img-card-overlay{
420
+ position:absolute;inset:0;
421
+ background:linear-gradient(105deg,
422
+ rgba(0,0,0,.88) 0%,
423
+ rgba(0,0,0,.40) 60%,
424
+ transparent 100%);
425
+ }
426
+ .img-card-body{
427
+ position:relative;z-index:2;
428
+ padding:2.2rem 2.6rem;width:100%;
429
+ }
430
+ .ic-tag{
431
+ font-size:.68rem;font-weight:600;
432
+ letter-spacing:.12em;text-transform:uppercase;
433
+ color:rgba(255,255,255,.45);margin-bottom:.5rem;display:block;
434
+ }
435
+ .ic-title{
436
+ font-size:2rem;font-weight:700;
437
+ letter-spacing:-.03em;color:#fff;line-height:1.1;
438
+ }
439
+ .ic-sub{
440
+ font-size:.85rem;color:rgba(255,255,255,.5);
441
+ margin-top:.4rem;font-weight:300;
442
+ }
443
+
444
+ /* ══════════════════════════════════════════════
445
+ STAT TILES
446
+ ══════════════════════════════════════════════ */
447
+ .stat-grid{
448
+ display:grid;grid-template-columns:repeat(5,1fr);
449
+ gap:10px;margin:1.5rem 0;
450
+ }
451
+ .stat-tile{
452
+ background:var(--bg-secondary);
453
+ border:1px solid var(--border);
454
+ border-radius:var(--radius-md);
455
+ padding:1.2rem 1rem;text-align:center;
456
+ transition:all .2s;cursor:default;
457
+ }
458
+ .stat-tile:hover{
459
+ background:var(--bg-primary);
460
+ box-shadow:var(--shadow-sm);
461
+ transform:translateY(-2px);
462
+ }
463
+ .st-icon{font-size:1.5rem;margin-bottom:.45rem;display:block;}
464
+ .st-num{
465
+ font-size:1.9rem;font-weight:700;
466
+ letter-spacing:-.04em;line-height:1;
467
+ }
468
+ .st-lbl{
469
+ font-size:.66rem;font-weight:500;
470
+ color:var(--text-tertiary);margin-top:4px;
471
+ letter-spacing:.01em;
472
+ }
473
+
474
+ /* ══════════════════════════════════════════════
475
+ CATEGORY LEGEND
476
+ ══════════════════════��═══════════════════════ */
477
+ .cat-item{
478
+ display:flex;align-items:center;gap:14px;
479
+ padding:12px 0;border-bottom:1px solid var(--border);
480
+ transition:background .15s;
481
+ }
482
+ .cat-pip{
483
+ width:8px;height:8px;border-radius:50%;flex-shrink:0;
484
+ }
485
+ .cat-icon-box{
486
+ width:34px;height:34px;border-radius:10px;
487
+ display:flex;align-items:center;justify-content:center;
488
+ font-size:1rem;flex-shrink:0;
489
+ border:1px solid var(--border);
490
+ background:var(--bg-secondary);
491
+ }
492
+ .cat-name{font-size:.88rem;font-weight:600;color:var(--text-primary);letter-spacing:-.01em;}
493
+ .cat-desc{font-size:.74rem;color:var(--text-secondary);margin-top:1px;font-weight:400;}
494
+
495
+ /* ══════════════════════════════════════════════
496
+ ANSWER DISPLAY
497
+ ══════════════════════════════════════════════ */
498
+ .answer-wrap{
499
+ background:#f0f7ff;
500
+ border:1px solid rgba(0,113,227,.15);
501
+ border-left:3px solid var(--blue);
502
+ border-radius:0 var(--radius-md) var(--radius-md) 0;
503
+ padding:1.8rem 2rem;margin-top:1.4rem;
504
+ }
505
+ .answer-chip{
506
+ display:inline-block;
507
+ background:var(--blue);color:#fff;
508
+ font-size:.66rem;font-weight:600;
509
+ letter-spacing:.1em;text-transform:uppercase;
510
+ padding:3px 10px;border-radius:var(--radius-pill);
511
+ margin-bottom:.75rem;
512
+ }
513
+ .answer-text{
514
+ font-size:1.4rem;font-weight:600;
515
+ letter-spacing:-.025em;color:var(--text-primary);
516
+ line-height:1.4;
517
+ }
518
+ .answer-meta{
519
+ font-size:.78rem;color:var(--text-secondary);
520
+ margin-top:.8rem;font-weight:400;
521
+ }
522
+ .answer-meta strong{color:var(--blue);font-weight:600;}
523
+
524
+ /* ══════════════════════════════════════════════
525
+ SUMMARY
526
+ ══════════════════════════════════════════════ */
527
+ .summary-wrap{
528
+ background:var(--bg-secondary);
529
+ border:1px solid var(--border);
530
+ border-radius:var(--radius-md);
531
+ padding:1.6rem 2rem;margin-top:1.2rem;
532
+ }
533
+ .summary-chip{
534
+ display:inline-block;
535
+ background:var(--text-primary);color:#fff;
536
+ font-size:.66rem;font-weight:600;
537
+ letter-spacing:.1em;text-transform:uppercase;
538
+ padding:3px 10px;border-radius:var(--radius-pill);margin-bottom:.7rem;
539
+ }
540
+ .summary-text{
541
+ font-size:.95rem;font-weight:400;
542
+ color:var(--text-primary);line-height:1.8;letter-spacing:-.01em;
543
+ }
544
+
545
+ /* ══════════════════════════════════════════════
546
+ CONF BARS
547
+ ══════════════════════════════════════════════ */
548
+ .conf-row{display:flex;align-items:center;gap:12px;margin-bottom:9px;}
549
+ .conf-lbl{
550
+ width:118px;font-size:.78rem;font-weight:500;
551
+ color:var(--text-secondary);flex-shrink:0;letter-spacing:-.01em;
552
+ }
553
+ .conf-bg{
554
+ flex:1;height:4px;
555
+ background:rgba(0,0,0,.07);
556
+ border-radius:999px;overflow:hidden;
557
+ }
558
+ .conf-fg{height:100%;border-radius:999px;}
559
+ .conf-pct{
560
+ width:38px;text-align:right;
561
+ font-size:.75rem;font-weight:600;color:var(--text-primary);
562
+ }
563
+
564
+ /* ══════════════════════════════════════════════
565
+ TIPS
566
+ ══════════════════════════════════════════════ */
567
+ .tip-row{
568
+ display:flex;gap:14px;padding:12px 0;
569
+ border-bottom:1px solid var(--border);
570
+ align-items:flex-start;
571
+ }
572
+ .tip-num{
573
+ font-size:.7rem;font-weight:600;
574
+ color:var(--blue);width:18px;flex-shrink:0;padding-top:2px;
575
+ }
576
+ .tip-title{
577
+ font-size:.87rem;font-weight:600;
578
+ color:var(--text-primary);letter-spacing:-.01em;
579
+ }
580
+ .tip-body{font-size:.77rem;color:var(--text-secondary);margin-top:2px;line-height:1.5;}
581
+
582
+ /* ══════════════════════════════════════════════
583
+ METRIC CARDS (Insights)
584
+ ══════════════════════════════════════════════ */
585
+ .metric-card{
586
+ background:var(--bg-primary);
587
+ border:1px solid var(--border);
588
+ border-radius:var(--radius-md);
589
+ padding:1.8rem 1.6rem;
590
+ text-align:center;
591
+ box-shadow:var(--shadow-xs);
592
+ transition:all .2s;
593
+ }
594
+ .metric-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);}
595
+ .metric-val{
596
+ font-size:2.6rem;font-weight:700;
597
+ letter-spacing:-.05em;line-height:1;margin-bottom:.5rem;
598
+ }
599
+ .metric-lbl{
600
+ font-size:.76rem;font-weight:500;
601
+ color:var(--text-secondary);letter-spacing:.01em;
602
+ text-transform:uppercase;
603
+ }
604
+
605
+ /* ══════════════════════════════════════════════
606
+ SPOTLIGHT
607
+ ══════════════════════════════════════════════ */
608
+ .spotlight{
609
+ background:var(--bg-primary);
610
+ border:1px solid var(--border);
611
+ border-radius:var(--radius-lg);
612
+ padding:2rem 2.2rem;
613
+ box-shadow:var(--shadow-sm);
614
+ }
615
+ .spot-badge{
616
+ display:inline-flex;align-items:center;gap:6px;
617
+ padding:4px 12px;border-radius:var(--radius-pill);
618
+ font-size:.76rem;font-weight:600;
619
+ border:1px solid;margin-right:8px;margin-bottom:1rem;
620
+ }
621
+ .spot-text{
622
+ font-size:.93rem;color:var(--text-secondary);
623
+ line-height:1.8;font-weight:400;letter-spacing:-.01em;
624
+ }
625
+
626
+ /* ══════════════════════════════════════════════
627
+ WORD CLOUD
628
+ ══════════════════════════════════════════════ */
629
+ .wc-wrap{
630
+ background:var(--bg-primary);
631
+ border:1px solid var(--border);
632
+ border-radius:var(--radius-md);
633
+ min-height:260px;padding:1.5rem;
634
+ box-shadow:var(--shadow-xs);
635
+ }
636
+
637
+ /* ══════════════════════════════════════════════
638
+ EMPTY STATE
639
+ ══════════════════════════════════════════════ */
640
+ .empty-state{
641
+ text-align:center;padding:5rem 2rem;
642
+ border:1px dashed var(--border);
643
+ border-radius:var(--radius-lg);
644
+ background:var(--bg-secondary);
645
+ }
646
+ .empty-icon{font-size:2.8rem;opacity:.3;display:block;margin-bottom:1rem;}
647
+ .empty-title{
648
+ font-size:1.05rem;font-weight:600;
649
+ color:var(--text-primary);letter-spacing:-.02em;margin-bottom:.35rem;
650
+ }
651
+ .empty-sub{font-size:.83rem;color:var(--text-secondary);}
652
+
653
+ /* ══════════════════════════════════════════════
654
+ DIVIDERS
655
+ ══════════════════════════════════════════════ */
656
+ .div-line{border:none;height:1px;margin:0;background:var(--border);}
657
+ .div-gap {border:none;height:1px;margin:2rem 0;background:var(--border);}
658
+
659
+ /* ══════════════════════════════════════════════
660
+ STREAMLIT WIDGET RESETS β€” Apple-quality
661
+ ══════════════════════════════════════════════ */
662
+ /* Primary button */
663
+ .stButton>button{
664
+ background:var(--blue)!important;color:#fff!important;
665
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
666
+ font-weight:600!important;font-size:.85rem!important;
667
+ letter-spacing:-.01em!important;border:none!important;
668
+ border-radius:var(--radius-pill)!important;
669
+ padding:.62rem 1.6rem!important;
670
+ box-shadow:0 2px 8px rgba(0,113,227,.2)!important;
671
+ transition:all .2s!important;
672
+ }
673
+ .stButton>button:hover{
674
+ background:var(--blue-hv)!important;
675
+ transform:translateY(-1px)!important;
676
+ box-shadow:0 6px 18px rgba(0,113,227,.3)!important;
677
+ }
678
+ .stButton>button:active{transform:scale(.98)!important;}
679
+
680
+ /* Download button */
681
+ .stDownloadButton>button{
682
+ background:transparent!important;
683
+ color:var(--blue)!important;
684
+ border:1.5px solid rgba(0,113,227,.35)!important;
685
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
686
+ font-weight:500!important;font-size:.82rem!important;
687
+ border-radius:var(--radius-pill)!important;
688
+ padding:.55rem 1.4rem!important;
689
+ transition:all .2s!important;
690
+ }
691
+ .stDownloadButton>button:hover{
692
+ background:#f0f7ff!important;border-color:var(--blue)!important;
693
+ }
694
+
695
+ /* File uploader */
696
+ div[data-testid="stFileUploader"]{
697
+ background:var(--bg-secondary)!important;
698
+ border:1.5px dashed var(--border-mid)!important;
699
+ border-radius:var(--radius-md)!important;
700
+ transition:all .2s!important;
701
+ }
702
+ div[data-testid="stFileUploader"]:hover{
703
+ border-color:var(--blue)!important;
704
+ background:#f0f7ff!important;
705
+ }
706
+ div[data-testid="stFileUploader"] *{color:var(--text-secondary)!important;}
707
+
708
+ /* Text inputs */
709
+ .stTextArea textarea,.stTextInput input{
710
+ background:var(--bg-primary)!important;
711
+ border:1px solid var(--border-mid)!important;
712
+ border-radius:var(--radius-sm)!important;
713
+ color:var(--text-primary)!important;
714
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
715
+ font-size:.9rem!important;font-weight:400!important;
716
+ transition:all .2s!important;
717
+ box-shadow:inset 0 1px 3px rgba(0,0,0,.04)!important;
718
+ }
719
+ .stTextArea textarea:focus,.stTextInput input:focus{
720
+ border-color:var(--blue)!important;
721
+ box-shadow:0 0 0 3px rgba(0,113,227,.1),inset 0 1px 3px rgba(0,0,0,.04)!important;
722
+ outline:none!important;
723
+ }
724
+ .stTextArea label,.stTextInput label{
725
+ font-size:.75rem!important;font-weight:600!important;
726
+ color:var(--text-primary)!important;letter-spacing:.02em!important;
727
+ text-transform:uppercase!important;
728
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
729
+ }
730
+
731
+ /* Select */
732
+ .stSelectbox label{
733
+ font-size:.75rem!important;font-weight:600!important;
734
+ color:var(--text-primary)!important;letter-spacing:.02em!important;
735
+ text-transform:uppercase!important;
736
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
737
+ }
738
+ .stSelectbox [data-baseweb="select"]>div{
739
+ background:var(--bg-primary)!important;
740
+ border:1px solid var(--border-mid)!important;
741
+ border-radius:var(--radius-sm)!important;
742
+ color:var(--text-primary)!important;font-weight:400!important;
743
+ }
744
+ .stSelectbox [data-baseweb="select"]>div:focus-within{
745
+ border-color:var(--blue)!important;
746
+ box-shadow:0 0 0 3px rgba(0,113,227,.1)!important;
747
+ }
748
+
749
+ /* Slider */
750
+ .stSlider label{
751
+ font-size:.75rem!important;font-weight:600!important;
752
+ color:var(--text-primary)!important;letter-spacing:.02em!important;
753
+ text-transform:uppercase!important;
754
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
755
+ }
756
+ div[data-baseweb="slider"]>div>div>div{background:var(--blue)!important;}
757
+
758
+ /* Radio */
759
+ .stRadio>label{
760
+ font-size:.75rem!important;font-weight:600!important;
761
+ color:var(--text-primary)!important;letter-spacing:.02em!important;
762
+ text-transform:uppercase!important;
763
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
764
+ }
765
+ .stRadio [data-testid="stMarkdownContainer"] p{
766
+ font-size:.85rem!important;color:var(--text-primary)!important;
767
+ }
768
+
769
+ /* Progress */
770
+ .stProgress>div>div{background:var(--blue)!important;border-radius:999px!important;}
771
+
772
+ /* Tabs β€” pill style */
773
+ .stTabs [data-baseweb="tab-list"]{
774
+ background:var(--bg-secondary)!important;
775
+ border:1px solid var(--border)!important;
776
+ border-radius:var(--radius-pill)!important;
777
+ padding:4px!important;gap:2px!important;
778
+ margin-bottom:2rem!important;
779
+ box-shadow:var(--shadow-xs)!important;
780
+ display:inline-flex!important;width:auto!important;
781
+ }
782
+ .stTabs [data-baseweb="tab"]{
783
+ font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Manrope',sans-serif!important;
784
+ font-size:.8rem!important;font-weight:500!important;
785
+ color:var(--text-secondary)!important;
786
+ padding:.45rem 1.2rem!important;
787
+ border-radius:var(--radius-pill)!important;
788
+ border:none!important;background:transparent!important;
789
+ transition:all .15s!important;letter-spacing:-.01em!important;
790
+ }
791
+ .stTabs [data-baseweb="tab"]:hover{color:var(--text-primary)!important;}
792
+ .stTabs [aria-selected="true"]{
793
+ background:var(--bg-primary)!important;
794
+ color:var(--text-primary)!important;
795
+ box-shadow:var(--shadow-xs)!important;
796
+ font-weight:600!important;
797
+ }
798
+ .stTabs [data-baseweb="tab-border"]{display:none!important;}
799
+ .stTabs [data-baseweb="tab-panel"]{padding-top:0!important;}
800
+
801
+ /* Dataframe */
802
+ .stDataFrame{border-radius:var(--radius-sm)!important;overflow:hidden!important;
803
+ border:1px solid var(--border)!important;box-shadow:var(--shadow-xs)!important;}
804
+
805
+ /* Alerts */
806
+ .stAlert{border-radius:var(--radius-sm)!important;}
807
+
808
+ /* Expander */
809
+ details{background:var(--bg-primary)!important;border:1px solid var(--border)!important;
810
+ border-radius:var(--radius-sm)!important;}
811
+ details summary{font-size:.83rem!important;color:var(--text-secondary)!important;}
812
+
813
+ /* Scrollbar */
814
+ ::-webkit-scrollbar{width:5px;height:5px;}
815
+ ::-webkit-scrollbar-track{background:var(--bg-secondary);}
816
+ ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.1);border-radius:999px;}
817
+ ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.2);}
818
+
819
+ /* ══════════════════════════════════════════════
820
+ ANIMATIONS
821
+ ══════════════════════════════════════════════ */
822
+ @keyframes riseIn{
823
+ from{opacity:0;transform:translateY(20px);}
824
+ to{opacity:1;transform:translateY(0);}
825
+ }
826
+ .rise{animation:riseIn .5s cubic-bezier(.22,1,.36,1) both;}
827
+ .r1{animation-delay:.05s}.r2{animation-delay:.12s}
828
+ .r3{animation-delay:.19s}.r4{animation-delay:.26s}
829
+
830
+ /* ══════════════════════════════════════════════
831
+ FOOTER
832
+ ══════════════════════════════════════════════ */
833
+ #footer{
834
+ background:var(--bg-secondary);
835
+ border-top:1px solid var(--border);
836
+ padding:3rem var(--page-pad) 2.5rem;
837
+ }
838
+ .footer-inner{max-width:var(--page-max);margin:0 auto;}
839
+ .footer-top{
840
+ display:flex;justify-content:space-between;
841
+ align-items:flex-start;flex-wrap:wrap;gap:2rem;
842
+ padding-bottom:2rem;
843
+ border-bottom:1px solid var(--border);margin-bottom:1.5rem;
844
+ }
845
+ .footer-brand{
846
+ font-size:.95rem;font-weight:700;
847
+ color:var(--text-primary);letter-spacing:-.02em;margin-bottom:.3rem;
848
+ }
849
+ .footer-brand span{color:var(--blue);}
850
+ .footer-tagline{font-size:.78rem;color:var(--text-secondary);}
851
+ .footer-links{display:flex;gap:2rem;flex-wrap:wrap;}
852
+ .footer-link{font-size:.78rem;color:var(--text-secondary);transition:color .15s;}
853
+ .footer-link:hover{color:var(--text-primary);}
854
+ .footer-copy{font-size:.72rem;color:var(--text-tertiary);}
855
+ </style>
856
+ """, unsafe_allow_html=True)
857
+
858
+ # ─────────────────────────────────────────────────────────────────────────────
859
+ # STATE
860
+ # ─────────────────────────────────────────────────────────────────────────────
861
+ if "page" not in st.session_state:
862
+ st.session_state["page"] = "classify"
863
+
864
+ # ─────────────────────────────────────────────────────────────────────────────
865
+ # ══════════════ NAVIGATION ═══════════════════════════════════════════════════
866
+ # ─────────────────────────────────────────────────────────────────────────────
867
+ pg = st.session_state["page"]
868
+
869
+ st.markdown(f"""
870
+ <div id="nav">
871
+ <div class="nav-inner">
872
+ <div class="nav-logo">β—‰ News<span>Lens AI</span></div>
873
+ <div class="nav-items">
874
+ <a class="nav-item {'on' if pg=='classify' else ''}">Classify</a>
875
+ <a class="nav-item {'on' if pg=='qa' else ''}">Q &amp; A</a>
876
+ <a class="nav-item {'on' if pg=='insights' else ''}">Insights</a>
877
+ </div>
878
+ <div class="nav-badge">DA3111</div>
879
+ </div>
880
+ </div>
881
+ """, unsafe_allow_html=True)
882
+
883
+ # Nav button row (functional, visually hidden by CSS)
884
+ c1,c2,c3,_ = st.columns([1,1,1,6])
885
+ with c1:
886
+ if st.button("Classify", key="nb1", use_container_width=True):
887
+ st.session_state["page"] = "classify"; st.rerun()
888
+ with c2:
889
+ if st.button("Q & A", key="nb2", use_container_width=True):
890
+ st.session_state["page"] = "qa"; st.rerun()
891
+ with c3:
892
+ if st.button("Insights", key="nb3", use_container_width=True):
893
+ st.session_state["page"] = "insights"; st.rerun()
894
+
895
+ # ─────────────────────────────────────────────────────────────────────────────
896
+ # ══════════════ HERO ═════════════════════════════════════════════════════════
897
+ # ─────────────────────────────────────────────────────────────────────────────
898
+ st.markdown("""
899
+ <div id="hero">
900
+ <div class="hero-bg"></div>
901
+ <div class="hero-overlay"></div>
902
+ <div class="hero-mesh"></div>
903
+ <div style="width:100%">
904
+ <div class="hero-content">
905
+ <div class="hero-kicker">
906
+ <span class="kicker-dot"></span>
907
+ Daily Mirror Β· AI Intelligence Β· Assignment 01
908
+ </div>
909
+ <h1 class="h-display">
910
+ News that<br><em>understands itself.</em>
911
+ </h1>
912
+ <p class="h-sub">
913
+ Classify articles, extract answers, and surface visual
914
+ insights from Daily Mirror news β€” powered by fine-tuned
915
+ Hugging Face Transformers.
916
+ </p>
917
+ <div class="h-actions">
918
+ <a class="btn-primary">Get Started β†’</a>
919
+ <a class="btn-ghost">Learn More</a>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ </div>
924
+ """, unsafe_allow_html=True)
925
+
926
+ # Feature bar
927
+ st.markdown("""
928
+ <div id="feat-bar">
929
+ <div class="fb-cell">
930
+ <div class="fb-icon" style="background:#eff6ff;">🧠</div>
931
+ <div>
932
+ <div class="fb-title">DistilBERT Classifier</div>
933
+ <div class="fb-sub">Fine-tuned on 5 news categories</div>
934
+ </div>
935
+ </div>
936
+ <div class="fb-cell">
937
+ <div class="fb-icon" style="background:#f0fdf4;">πŸ’¬</div>
938
+ <div>
939
+ <div class="fb-title">RoBERTa Q&A</div>
940
+ <div class="fb-sub">Extractive answers with highlights</div>
941
+ </div>
942
+ </div>
943
+ <div class="fb-cell">
944
+ <div class="fb-icon" style="background:#faf5ff;">πŸ“Š</div>
945
+ <div>
946
+ <div class="fb-title">Visual Insights</div>
947
+ <div class="fb-sub">Charts, word clouds, distributions</div>
948
+ </div>
949
+ </div>
950
+ <div class="fb-cell">
951
+ <div class="fb-icon" style="background:#fff7ed;">βš™οΈ</div>
952
+ <div>
953
+ <div class="fb-title">NLP Preprocessing</div>
954
+ <div class="fb-sub">7-step NLTK pipeline built in</div>
955
+ </div>
956
+ </div>
957
+ </div>
958
+ <hr class="div-line">
959
+ """, unsafe_allow_html=True)
960
+
961
+ # ─────────────────────────────────────────────────────────────────────────────
962
+ # ══════════════ PAGE: CLASSIFY ═══════════════════════════════════════════════
963
+ # ─────────────────────────────────────────────────────────────────────────────
964
+ if pg == "classify":
965
+
966
+ # ── Section header ──────────────────────────────────────────────────────
967
+ st.markdown("""
968
+ <div class="section">
969
+ <div class="section-inner">
970
+ <span class="s-label rise r1">Component 01 Β· Text Classification</span>
971
+ <h2 class="s-h rise r2">Every article,<br>perfectly categorised.</h2>
972
+ <p class="s-p rise r3">Upload your CSV and a fine-tuned DistilBERT model
973
+ instantly sorts each article into one of five categories β€”
974
+ Business, Opinion, Political Gossip, Sports, or World News.</p>
975
+ </div>
976
+ </div>
977
+ """, unsafe_allow_html=True)
978
+
979
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
980
+
981
+ # Image banner
982
+ st.markdown("""
983
+ <div class="img-card rise r1">
984
+ <div class="img-card-bg"
985
+ style="background-image:url('https://images.unsplash.com/photo-1495020689067-958852a7765e?w=1400&q=80');">
986
+ </div>
987
+ <div class="img-card-overlay"></div>
988
+ <div class="img-card-body">
989
+ <span class="ic-tag">Upload Β· Preprocess Β· Classify Β· Download</span>
990
+ <div class="ic-title">News Classification at Scale</div>
991
+ <div class="ic-sub">7-step preprocessing pipeline Β· Batch inference Β· CSV output</div>
992
+ </div>
993
+ </div>
994
+ """, unsafe_allow_html=True)
995
+
996
+ col_L, col_R = st.columns([3, 2], gap="large")
997
+
998
+ # ── LEFT COLUMN ─────────────────────────────────────────────────────────
999
+ with col_L:
1000
+
1001
+ # Upload card
1002
+ st.markdown('<div class="card rise r2"><div class="card-body">', unsafe_allow_html=True)
1003
+ st.markdown('<span class="card-label">Step 01 β€” Upload</span>', unsafe_allow_html=True)
1004
+ st.markdown('<div class="card-title">Select your CSV file</div>', unsafe_allow_html=True)
1005
+ st.markdown(
1006
+ '<div class="card-desc" style="margin-bottom:1.3rem">Requires a '
1007
+ '<code style="background:#f1f5f9;border:1px solid #e2e8f0;border-radius:5px;'
1008
+ 'padding:1px 7px;font-size:.82rem;color:#0071e3;">content</code>'
1009
+ ' column. Compatible with the evaluation.csv provided with this assignment.</div>',
1010
+ unsafe_allow_html=True)
1011
+
1012
+ uploaded = st.file_uploader("", type=["csv"], key="cls_upload",
1013
+ label_visibility="collapsed")
1014
+
1015
+ if uploaded:
1016
+ df = pd.read_csv(uploaded)
1017
+ st.success(f"βœ“ &nbsp; {len(df):,} records loaded &nbsp;Β·&nbsp; {len(df.columns)} columns")
1018
+
1019
+ if "content" not in df.columns:
1020
+ st.error(f"Column `content` not found. "
1021
+ f"Found: **{', '.join(df.columns.tolist())}**")
1022
+ st.stop()
1023
+
1024
+ with st.expander("Preview β€” first 5 rows"):
1025
+ st.dataframe(df.head(), use_container_width=True)
1026
+
1027
+ st.markdown("<br>", unsafe_allow_html=True)
1028
+
1029
+ if st.button("Run Classification Pipeline", key="run_cls"):
1030
+ with st.status("βš™οΈ Preprocessing text (7 steps)…",
1031
+ expanded=False) as s:
1032
+ cleaned = df["content"].fillna("").apply(preprocess).tolist()
1033
+ s.update(label="βœ… Preprocessing complete", state="complete")
1034
+
1035
+ with st.spinner("Loading model β€” first run takes ~30s…"):
1036
+ clf = load_clf()
1037
+
1038
+ prog = st.progress(0, text="Classifying articles…")
1039
+ preds, confs = [], []
1040
+
1041
+ for i in range(0, len(cleaned), 16):
1042
+ batch = [t if t.strip() else " " for t in cleaned[i:i+16]]
1043
+ results = clf(batch, truncation=True, max_length=512)
1044
+ for r in results:
1045
+ preds.append(resolve(r["label"]))
1046
+ confs.append(round(r["score"], 4))
1047
+ pct = min(int((i+16)/len(cleaned)*100), 100)
1048
+ prog.progress(pct, text=f"Classifying… {pct}%")
1049
+ time.sleep(0.01)
1050
+ prog.empty()
1051
+
1052
+ out = df.copy()
1053
+ out["class"] = preds
1054
+ out["confidence"] = confs
1055
+ st.session_state["out_df"] = out
1056
+ st.success("βœ… Classification complete β€” results ready below.")
1057
+
1058
+ st.markdown("</div></div>", unsafe_allow_html=True)
1059
+
1060
+ # Results
1061
+ if "out_df" in st.session_state:
1062
+ out = st.session_state["out_df"]
1063
+ counts = out["class"].value_counts()
1064
+
1065
+ # Stat tiles
1066
+ st.markdown('<div class="stat-grid rise r3">', unsafe_allow_html=True)
1067
+ for label, meta in CATS.items():
1068
+ n = counts.get(label, 0)
1069
+ st.markdown(f"""
1070
+ <div class="stat-tile"
1071
+ style="border-top:2px solid {meta['color']};">
1072
+ <span class="st-icon">{meta['icon']}</span>
1073
+ <div class="st-num" style="color:{meta['color']}">{n}</div>
1074
+ <div class="st-lbl">{label.replace('_',' ')}</div>
1075
+ </div>""", unsafe_allow_html=True)
1076
+ st.markdown("</div>", unsafe_allow_html=True)
1077
+
1078
+ # Tabbed results
1079
+ st.markdown('<div class="card rise r4" style="margin-top:1rem">', unsafe_allow_html=True)
1080
+ st.markdown('<div class="card-body">', unsafe_allow_html=True)
1081
+ st.markdown('<span class="card-label">Results</span>', unsafe_allow_html=True)
1082
+ st.markdown('<div class="card-title" style="margin-bottom:1.2rem">'
1083
+ 'Classified Articles</div>', unsafe_allow_html=True)
1084
+
1085
+ all_t, *cat_ts = st.tabs(
1086
+ ["All Articles"] +
1087
+ [f"{CATS[l]['icon']} {l.replace('_',' ')}" for l in CATS]
1088
+ )
1089
+ with all_t:
1090
+ st.dataframe(out[["content","class","confidence"]],
1091
+ use_container_width=True, height=320)
1092
+ for i, label in enumerate(CATS):
1093
+ with cat_ts[i]:
1094
+ sub = out[out["class"]==label][["content","confidence"]]
1095
+ if sub.empty:
1096
+ st.info(f"No articles classified as **{label.replace('_',' ')}**.")
1097
+ else:
1098
+ st.dataframe(sub, use_container_width=True, height=280)
1099
+
1100
+ st.markdown("<br>", unsafe_allow_html=True)
1101
+
1102
+ avg_c = out["confidence"].mean() if "confidence" in out.columns else 0
1103
+ hi = (out["confidence"]>=0.9).sum() if "confidence" in out.columns else 0
1104
+ st.markdown(
1105
+ f'<p style="font-size:.8rem;color:var(--text-secondary);margin-bottom:1rem;">'
1106
+ f'Average confidence &nbsp;<strong style="color:var(--blue)">{avg_c:.1%}</strong>'
1107
+ f'&nbsp; Β· &nbsp;'
1108
+ f'High confidence β‰₯ 90% &nbsp;<strong style="color:var(--blue)">{hi}</strong>'
1109
+ f'</p>', unsafe_allow_html=True)
1110
+
1111
+ st.download_button(
1112
+ "⬇ Download output.csv",
1113
+ data=out.to_csv(index=False).encode("utf-8"),
1114
+ file_name="output.csv", mime="text/csv",
1115
+ )
1116
+ st.markdown("</div></div>", unsafe_allow_html=True)
1117
+
1118
+ else:
1119
+ st.markdown("""
1120
+ <div class="empty-state rise r3">
1121
+ <span class="empty-icon">β—‰</span>
1122
+ <div class="empty-title">No file selected yet</div>
1123
+ <div class="empty-sub">Upload your evaluation.csv above to begin</div>
1124
+ </div>""", unsafe_allow_html=True)
1125
+
1126
+ # ── RIGHT COLUMN ────────────────────────────────────────────────────────
1127
+ with col_R:
1128
+ st.markdown('<div class="card rise r2" style="position:sticky;top:72px">', unsafe_allow_html=True)
1129
+ st.markdown('<div class="card-body">', unsafe_allow_html=True)
1130
+ st.markdown('<span class="card-label">Reference</span>', unsafe_allow_html=True)
1131
+ st.markdown('<div class="card-title" style="margin-bottom:1.3rem">'
1132
+ 'Five News Categories</div>', unsafe_allow_html=True)
1133
+
1134
+ for label, meta in CATS.items():
1135
+ st.markdown(f"""
1136
+ <div class="cat-item">
1137
+ <div class="cat-pip" style="background:{meta['color']}"></div>
1138
+ <div class="cat-icon-box">{meta['icon']}</div>
1139
+ <div>
1140
+ <div class="cat-name">{label.replace('_',' ')}</div>
1141
+ <div class="cat-desc">{meta['desc']}</div>
1142
+ </div>
1143
+ </div>""", unsafe_allow_html=True)
1144
+
1145
+ if "out_df" in st.session_state:
1146
+ st.markdown('<hr class="div-gap">', unsafe_allow_html=True)
1147
+ st.markdown('<span class="card-label" style="color:var(--violet)">'
1148
+ 'Distribution Chart</span>', unsafe_allow_html=True)
1149
+ st.bar_chart(
1150
+ st.session_state["out_df"]["class"].value_counts(),
1151
+ use_container_width=True, height=190,
1152
+ )
1153
+
1154
+ st.markdown("</div></div>", unsafe_allow_html=True)
1155
+
1156
+ st.markdown("</div></div>", unsafe_allow_html=True) # /section-inner /section-alt
1157
+
1158
+
1159
+ # ─────────────────────────────────────────────────────────────────────────────
1160
+ # ══════════════ PAGE: Q&A ════════════════════════════════════════════════════
1161
+ # ─────────────────────────────────────────────────────────────────────────────
1162
+ elif pg == "qa":
1163
+
1164
+ st.markdown("""
1165
+ <div class="section">
1166
+ <div class="section-inner">
1167
+ <span class="s-label s-label-green rise r1">Component 02 Β· Question-Answering</span>
1168
+ <h2 class="s-h rise r2">Ask anything.<br>Get precise answers.</h2>
1169
+ <p class="s-p rise r3">
1170
+ Paste any news article and ask a natural language question.
1171
+ The AI reads the passage and extracts an exact, source-referenced answer
1172
+ β€” powered by deepset/roberta-base-squad2 (SQuAD 2.0).
1173
+ </p>
1174
+ </div>
1175
+ </div>
1176
+ """, unsafe_allow_html=True)
1177
+
1178
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1179
+
1180
+ # Image banner
1181
+ st.markdown("""
1182
+ <div class="img-card rise r1">
1183
+ <div class="img-card-bg"
1184
+ style="background-image:url('https://images.unsplash.com/photo-1457369804613-52c61a468e7d?w=1400&q=80');
1185
+ background-position:center 50%;">
1186
+ </div>
1187
+ <div class="img-card-overlay"></div>
1188
+ <div class="img-card-body">
1189
+ <span class="ic-tag">Extractive QA Β· RoBERTa Β· SQuAD 2.0</span>
1190
+ <div class="ic-title">Intelligence That Reads Closely</div>
1191
+ <div class="ic-sub">Ask in plain language Β· Get source-highlighted answers</div>
1192
+ </div>
1193
+ </div>
1194
+ """, unsafe_allow_html=True)
1195
+
1196
+ col_qa, col_side = st.columns([3, 2], gap="large")
1197
+
1198
+ with col_qa:
1199
+ st.markdown('<div class="card rise r2"><div class="card-body">', unsafe_allow_html=True)
1200
+ st.markdown('<span class="card-label" style="color:var(--green)">'
1201
+ 'Input</span>', unsafe_allow_html=True)
1202
+ st.markdown('<div class="card-title" style="margin-bottom:1.2rem">'
1203
+ 'Paste article &amp; ask</div>', unsafe_allow_html=True)
1204
+
1205
+ src = st.radio("Text Source",
1206
+ ["Paste article text", "Pick from classified results"],
1207
+ horizontal=True, key="qa_src")
1208
+ context = ""
1209
+
1210
+ if src == "Paste article text":
1211
+ context = st.text_area(
1212
+ "News Article",
1213
+ height=210,
1214
+ placeholder="Paste any Daily Mirror news article here…",
1215
+ key="qa_ctx",
1216
+ )
1217
+ else:
1218
+ if "out_df" not in st.session_state:
1219
+ st.info("ℹ️ Run the **Classify** pipeline first to use this option.")
1220
+ else:
1221
+ out_df = st.session_state["out_df"]
1222
+ sel_cat = st.selectbox(
1223
+ "Filter Category",
1224
+ ["All"] + [l.replace("_"," ") for l in CATS],
1225
+ key="qa_cat",
1226
+ )
1227
+ pool = (out_df if sel_cat == "All"
1228
+ else out_df[out_df["class"].isin(
1229
+ [sel_cat, sel_cat.replace(" ","_")])])
1230
+
1231
+ if not pool.empty:
1232
+ idx = st.selectbox(
1233
+ "Select Article",
1234
+ pool.index.tolist(),
1235
+ format_func=lambda i:
1236
+ f"#{i} β€” {str(pool.loc[i,'content'])[:72]}…",
1237
+ key="qa_idx",
1238
+ )
1239
+ row = pool.loc[idx]
1240
+ context = str(row["content"])
1241
+ lbl = row.get("class","")
1242
+ meta = CATS.get(lbl, {"icon":"β—‰","color":"#1d1d1f","bg":"#f5f5f7"})
1243
+ conf_v = row.get("confidence", None)
1244
+
1245
+ st.markdown(f"""
1246
+ <div style="display:inline-flex;align-items:center;gap:6px;
1247
+ background:{meta['bg']};
1248
+ border:1px solid {meta['color']}30;
1249
+ border-radius:var(--radius-pill);
1250
+ padding:4px 14px;margin:.6rem 0 .9rem;
1251
+ font-size:.78rem;font-weight:600;color:{meta['color']};">
1252
+ {meta['icon']}&nbsp; {lbl.replace('_',' ')}
1253
+ {f" &nbsp;Β·&nbsp; {conf_v:.1%}" if conf_v else ""}
1254
+ </div>
1255
+ <div style="background:var(--bg-secondary);border:1px solid var(--border);
1256
+ border-radius:var(--radius-sm);padding:1rem 1.2rem;
1257
+ font-size:.87rem;color:var(--text-secondary);
1258
+ line-height:1.7;max-height:160px;overflow-y:auto;
1259
+ margin-bottom:.8rem;">{context}</div>
1260
+ """, unsafe_allow_html=True)
1261
+
1262
+ st.markdown("<br>", unsafe_allow_html=True)
1263
+ question = st.text_input(
1264
+ "Your Question",
1265
+ placeholder="e.g. Who announced the new policy?",
1266
+ key="qa_q",
1267
+ )
1268
+ st.markdown("<br>", unsafe_allow_html=True)
1269
+
1270
+ if st.button("Extract Answer", key="run_qa"):
1271
+ if not context.strip():
1272
+ st.warning("⚠️ Please provide article text.")
1273
+ elif not question.strip():
1274
+ st.warning("⚠️ Please enter a question.")
1275
+ else:
1276
+ with st.spinner("Reading the passage…"):
1277
+ qa_pipe = load_qa()
1278
+ result = qa_pipe(question=question, context=context)
1279
+
1280
+ ans = result["answer"]
1281
+ score = result["score"]
1282
+ s, e = result["start"], result["end"]
1283
+
1284
+ highlighted = (
1285
+ context[:s]
1286
+ + f'<mark style="background:#dbeafe;color:#1d4ed8;'
1287
+ f'padding:0 3px;border-radius:3px;font-weight:500;">'
1288
+ f'{context[s:e]}</mark>'
1289
+ + context[e:]
1290
+ )
1291
+
1292
+ st.markdown(f"""
1293
+ <div class="answer-wrap">
1294
+ <span class="answer-chip">Answer</span>
1295
+ <div class="answer-text">{ans}</div>
1296
+ <div class="answer-meta">
1297
+ Confidence &nbsp;<strong>{score:.1%}</strong>
1298
+ &nbsp;Β·&nbsp; deepset/roberta-base-squad2
1299
+ </div>
1300
+ </div>""", unsafe_allow_html=True)
1301
+
1302
+ with st.expander("View highlighted source context"):
1303
+ st.markdown(
1304
+ f'<div style="font-size:.87rem;line-height:1.8;'
1305
+ f'color:var(--text-secondary);">{highlighted}</div>',
1306
+ unsafe_allow_html=True)
1307
+
1308
+ st.markdown("</div></div>", unsafe_allow_html=True)
1309
+
1310
+ with col_side:
1311
+ st.markdown('<div class="card rise r2"><div class="card-body">', unsafe_allow_html=True)
1312
+ st.markdown('<span class="card-label" style="color:var(--green)">'
1313
+ 'Tips</span>', unsafe_allow_html=True)
1314
+ st.markdown('<div class="card-title" style="margin-bottom:1rem">'
1315
+ 'Better questions,<br>better answers</div>', unsafe_allow_html=True)
1316
+
1317
+ for i, (t, d) in enumerate([
1318
+ ("Who Β· What Β· When Β· Where",
1319
+ "Factual questions extract the sharpest answers"),
1320
+ ("Provide full context",
1321
+ "Longer passages give the model more evidence to work from"),
1322
+ ("Stay specific",
1323
+ "Narrow, focused questions outperform vague ones every time"),
1324
+ ("Full sentence questions",
1325
+ "Questions ending with '?' consistently perform best"),
1326
+ ("Avoid yes / no",
1327
+ "Open-ended questions return richer, more informative answers"),
1328
+ ]):
1329
+ st.markdown(f"""
1330
+ <div class="tip-row">
1331
+ <span class="tip-num">{i+1:02}</span>
1332
+ <div>
1333
+ <div class="tip-title">{t}</div>
1334
+ <div class="tip-body">{d}</div>
1335
+ </div>
1336
+ </div>""", unsafe_allow_html=True)
1337
+
1338
+ st.markdown("</div></div>", unsafe_allow_html=True)
1339
+
1340
+ st.markdown('<div class="card rise r3" style="margin-top:1rem">', unsafe_allow_html=True)
1341
+ st.markdown('<div class="card-body">', unsafe_allow_html=True)
1342
+ st.markdown('<span class="card-label">Model</span>', unsafe_allow_html=True)
1343
+ for k, v in [
1344
+ ("Architecture", "RoBERTa Base"),
1345
+ ("Training Data", "SQuAD 2.0"),
1346
+ ("Task Type", "Extractive Q&A"),
1347
+ ("Provider", "deepset Β· Hugging Face"),
1348
+ ]:
1349
+ st.markdown(f"""
1350
+ <div style="display:flex;justify-content:space-between;align-items:center;
1351
+ padding:9px 0;border-bottom:1px solid var(--border);
1352
+ font-size:.83rem;">
1353
+ <span style="color:var(--text-secondary);font-weight:400;">{k}</span>
1354
+ <span style="font-weight:500;color:var(--text-primary);">{v}</span>
1355
+ </div>""", unsafe_allow_html=True)
1356
+ st.markdown("</div></div>", unsafe_allow_html=True)
1357
+
1358
+ st.markdown("</div></div>", unsafe_allow_html=True)
1359
+
1360
+
1361
+ # ─────────────────────────────────────────────────────────────────────────────
1362
+ # ══════════════ PAGE: INSIGHTS ═══════════════════════════════════════════════
1363
+ # ─────────────────────────────────────────────────────────────────────────────
1364
+ elif pg == "insights":
1365
+
1366
+ # Dark hero section
1367
+ st.markdown("""
1368
+ <div class="section-dark">
1369
+ <div class="section-inner">
1370
+ <span class="s-label s-label-violet rise r1"
1371
+ style="color:rgba(167,139,250,.85);">
1372
+ Component 03 Β· Visual Insights
1373
+ </span>
1374
+ <h2 class="s-h s-h-light rise r2">Clarity from<br>every angle.</h2>
1375
+ <p class="s-p s-p-light rise r3">
1376
+ Distribution breakdowns, word clouds, confidence analysis,
1377
+ and article spotlights β€” everything you need to understand
1378
+ your classified corpus at a glance.
1379
+ </p>
1380
+ </div>
1381
+ </div>
1382
+ <hr class="div-line">
1383
+ """, unsafe_allow_html=True)
1384
+
1385
+ if "out_df" not in st.session_state:
1386
+ st.markdown("""
1387
+ <div class="section"><div class="section-inner">
1388
+ <div class="empty-state">
1389
+ <span class="empty-icon">β—ˆ</span>
1390
+ <div class="empty-title">No classified data yet</div>
1391
+ <div class="empty-sub">
1392
+ Run the <strong>Classify</strong> pipeline first,
1393
+ then return here for visual insights.
1394
+ </div>
1395
+ </div>
1396
+ </div></div>""", unsafe_allow_html=True)
1397
+ st.stop()
1398
+
1399
+ out_df = st.session_state["out_df"]
1400
+ total = len(out_df)
1401
+ counts = out_df["class"].value_counts()
1402
+
1403
+ # ── Section A: Distribution ──────────────────────────────────────────
1404
+ st.markdown("""
1405
+ <div class="section">
1406
+ <div class="section-inner">
1407
+ <span class="s-label rise r1">01 Β· Distribution</span>
1408
+ <h2 class="s-h rise r2" style="font-size:2.2rem;margin-bottom:.5rem;">
1409
+ How your corpus breaks down.
1410
+ </h2>
1411
+ </div>
1412
+ </div>
1413
+ """, unsafe_allow_html=True)
1414
+
1415
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1416
+
1417
+ col_da, col_db = st.columns([2, 3], gap="large")
1418
+
1419
+ with col_da:
1420
+ st.markdown('<div class="card rise r1"><div class="card-body">', unsafe_allow_html=True)
1421
+ st.markdown('<span class="card-label">Breakdown</span>', unsafe_allow_html=True)
1422
+ for label, meta in CATS.items():
1423
+ n = counts.get(label, 0)
1424
+ pct = n / total if total > 0 else 0
1425
+ st.markdown(f"""
1426
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
1427
+ <span style="font-size:1.1rem;width:24px;text-align:center">{meta['icon']}</span>
1428
+ <div style="flex:1">
1429
+ <div style="display:flex;justify-content:space-between;
1430
+ font-size:.82rem;font-weight:500;
1431
+ color:var(--text-primary);margin-bottom:5px;">
1432
+ <span>{label.replace('_',' ')}</span>
1433
+ <span style="color:{meta['color']};font-weight:600;">
1434
+ {n} Β· {pct:.0%}
1435
+ </span>
1436
+ </div>
1437
+ <div class="conf-bg">
1438
+ <div class="conf-fg"
1439
+ style="width:{pct*100:.1f}%;background:{meta['color']}">
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+ </div>""", unsafe_allow_html=True)
1444
+ st.markdown("</div></div>", unsafe_allow_html=True)
1445
+
1446
+ with col_db:
1447
+ try:
1448
+ import plotly.express as px
1449
+ cdf = counts.reset_index()
1450
+ cdf.columns = ["Category","Count"]
1451
+ cdf["Label"] = cdf["Category"].str.replace("_"," ")
1452
+ cmap = {k: CATS[k]["color"] for k in CATS}
1453
+ fig = px.bar(cdf, x="Label", y="Count", color="Category",
1454
+ color_discrete_map=cmap, text="Count",
1455
+ labels={"Label":"","Count":""})
1456
+ fig.update_layout(
1457
+ plot_bgcolor="white",paper_bgcolor="white",
1458
+ font=dict(family="-apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif",
1459
+ size=12,color="#1d1d1f"),
1460
+ showlegend=False,margin=dict(l=0,r=0,t=10,b=0),
1461
+ xaxis=dict(showgrid=False,color="#86868b",
1462
+ tickfont=dict(size=11,color="#6e6e73")),
1463
+ yaxis=dict(gridcolor="#f5f5f7",color="#86868b"),
1464
+ )
1465
+ fig.update_traces(textposition="outside",
1466
+ textfont=dict(size=12,color="#1d1d1f"),
1467
+ marker_line_width=0,
1468
+ marker_corner_radius=6)
1469
+ st.plotly_chart(fig, use_container_width=True)
1470
+ except ImportError:
1471
+ st.bar_chart(counts, use_container_width=True, height=270)
1472
+
1473
+ st.markdown("</div></div>", unsafe_allow_html=True)
1474
+
1475
+ # ── Section B: Word Cloud ────────────────────────────────────────────
1476
+ st.markdown("""
1477
+ <div class="section">
1478
+ <div class="section-inner">
1479
+ <span class="s-label rise r1">02 Β· Word Cloud</span>
1480
+ <h2 class="s-h rise r2" style="font-size:2.2rem;margin-bottom:.5rem;">
1481
+ The language of the news.
1482
+ </h2>
1483
+ </div>
1484
+ </div>
1485
+ """, unsafe_allow_html=True)
1486
+
1487
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1488
+
1489
+ col_wl, col_wr = st.columns([2, 3], gap="large")
1490
+
1491
+ with col_wl:
1492
+ st.markdown('<div class="card rise r1"><div class="card-body">', unsafe_allow_html=True)
1493
+ st.markdown('<span class="card-label">Configure</span>', unsafe_allow_html=True)
1494
+ st.markdown('<div class="card-title" style="margin-bottom:1rem">'
1495
+ 'Build word cloud</div>', unsafe_allow_html=True)
1496
+
1497
+ wc_sel = st.selectbox("Category Filter",
1498
+ ["All"]+[l.replace("_"," ") for l in CATS],
1499
+ key="wc_cat")
1500
+ wc_n = st.slider("Number of Words", 20, 120, 70, key="wc_n")
1501
+ st.markdown("<br>", unsafe_allow_html=True)
1502
+
1503
+ if st.button("Generate Word Cloud", key="run_wc"):
1504
+ lbl = wc_sel.replace(" ","_") if wc_sel != "All" else "All"
1505
+ corpus = (" ".join(out_df["content"].fillna("").tolist()) if lbl == "All"
1506
+ else " ".join(
1507
+ out_df[out_df["class"].isin([lbl,wc_sel])]["content"]
1508
+ .fillna("").tolist()))
1509
+ try:
1510
+ from wordcloud import WordCloud
1511
+ import matplotlib.pyplot as plt
1512
+ import matplotlib.colors as mcolors
1513
+
1514
+ accent = CATS.get(lbl,{}).get("color","#0071e3")
1515
+ processed = preprocess(corpus)
1516
+
1517
+ def _cf(*a,**k):
1518
+ r,g,b = mcolors.to_rgb(accent)
1519
+ f = random.uniform(.45,1.)
1520
+ return f"rgb({int(r*f*255)},{int(g*f*255)},{int(b*f*255)})"
1521
+
1522
+ wc = WordCloud(width=900,height=360,
1523
+ background_color="white",
1524
+ color_func=_cf,max_words=wc_n,
1525
+ prefer_horizontal=.82).generate(processed)
1526
+ fig_wc,ax = plt.subplots(figsize=(12,4))
1527
+ ax.imshow(wc,interpolation="bilinear"); ax.axis("off")
1528
+ fig_wc.patch.set_facecolor("white"); plt.tight_layout(pad=0)
1529
+ st.session_state["wc_fig"] = fig_wc
1530
+ st.session_state["wc_html"] = None
1531
+ except ImportError:
1532
+ st.session_state["wc_html"] = word_cloud_html(preprocess(corpus), wc_n)
1533
+ st.session_state["wc_fig"] = None
1534
+
1535
+ st.markdown("</div></div>", unsafe_allow_html=True)
1536
+
1537
+ with col_wr:
1538
+ st.markdown('<div class="wc-wrap rise r1">', unsafe_allow_html=True)
1539
+ st.markdown('<span class="card-label" style="display:block;margin-bottom:.8rem">'
1540
+ 'Word Frequency Canvas</span>', unsafe_allow_html=True)
1541
+
1542
+ if st.session_state.get("wc_fig"):
1543
+ import matplotlib.pyplot as plt
1544
+ st.pyplot(st.session_state["wc_fig"])
1545
+ elif st.session_state.get("wc_html"):
1546
+ st.markdown(st.session_state["wc_html"], unsafe_allow_html=True)
1547
+ else:
1548
+ st.markdown("""
1549
+ <div style="text-align:center;padding:5rem 1rem;">
1550
+ <div style="font-size:3rem;opacity:.12;margin-bottom:1rem">β—Ž</div>
1551
+ <div style="font-size:.95rem;color:var(--text-tertiary);">
1552
+ Configure and generate your word cloud
1553
+ </div>
1554
+ </div>""", unsafe_allow_html=True)
1555
+
1556
+ st.markdown("</div>", unsafe_allow_html=True)
1557
+
1558
+ st.markdown("</div></div>", unsafe_allow_html=True)
1559
+
1560
+ # ── Section C: Confidence ────────────────────────────────────────────
1561
+ if "confidence" in out_df.columns:
1562
+ st.markdown("""
1563
+ <div class="section">
1564
+ <div class="section-inner">
1565
+ <span class="s-label rise r1">03 Β· Confidence Analysis</span>
1566
+ <h2 class="s-h rise r2" style="font-size:2.2rem;margin-bottom:.5rem;">
1567
+ How certain is the model?
1568
+ </h2>
1569
+ </div>
1570
+ </div>
1571
+ """, unsafe_allow_html=True)
1572
+
1573
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1574
+
1575
+ c1,c2,c3 = st.columns(3, gap="large")
1576
+ for col,(val,lbl,color) in zip([c1,c2,c3],[
1577
+ (f"{out_df['confidence'].mean():.1%}","Average Confidence","#0071e3"),
1578
+ (str((out_df["confidence"]>=.9).sum()),"High Confidence β‰₯ 90%","#34c759"),
1579
+ (str((out_df["confidence"]<.7).sum()), "Low Confidence < 70%", "#ff3b30"),
1580
+ ]):
1581
+ with col:
1582
+ st.markdown(f"""
1583
+ <div class="metric-card rise r1">
1584
+ <div class="metric-val" style="color:{color}">{val}</div>
1585
+ <div class="metric-lbl">{lbl}</div>
1586
+ </div>""", unsafe_allow_html=True)
1587
+
1588
+ st.markdown("<br>", unsafe_allow_html=True)
1589
+
1590
+ try:
1591
+ import plotly.express as px
1592
+ cmap = {k: CATS[k]["color"] for k in CATS}
1593
+ fig2 = px.histogram(out_df,x="confidence",color="class",
1594
+ nbins=25,color_discrete_map=cmap,
1595
+ labels={"confidence":"Confidence Score","class":""})
1596
+ fig2.update_layout(
1597
+ plot_bgcolor="white",paper_bgcolor="white",
1598
+ font=dict(family="-apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif",
1599
+ size=11,color="#1d1d1f"),
1600
+ margin=dict(l=0,r=0,t=10,b=0),bargap=.06,
1601
+ xaxis=dict(showgrid=False,color="#86868b"),
1602
+ yaxis=dict(gridcolor="#f5f5f7",color="#86868b"),
1603
+ legend=dict(bgcolor="white",bordercolor="#e2e2e7",borderwidth=1,
1604
+ font=dict(size=11)),
1605
+ )
1606
+ st.plotly_chart(fig2, use_container_width=True)
1607
+ except ImportError:
1608
+ st.dataframe(out_df.groupby("class")["confidence"].describe().round(3),
1609
+ use_container_width=True)
1610
+
1611
+ st.markdown("</div></div>", unsafe_allow_html=True)
1612
+
1613
+ # ── Section D: Article Length ────────────────────────────────────────
1614
+ st.markdown("""
1615
+ <div class="section">
1616
+ <div class="section-inner">
1617
+ <span class="s-label rise r1">04 Β· Article Length</span>
1618
+ <h2 class="s-h rise r2" style="font-size:2.2rem;margin-bottom:.5rem;">
1619
+ Word count by category.
1620
+ </h2>
1621
+ </div>
1622
+ </div>
1623
+ """, unsafe_allow_html=True)
1624
+
1625
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1626
+ out_df["word_count"] = out_df["content"].fillna("").apply(lambda x: len(x.split()))
1627
+
1628
+ try:
1629
+ import plotly.express as px
1630
+ cmap = {k: CATS[k]["color"] for k in CATS}
1631
+ fig3 = px.box(out_df,x="class",y="word_count",color="class",
1632
+ color_discrete_map=cmap,points="outliers",
1633
+ labels={"class":"","word_count":"Word Count"})
1634
+ fig3.update_layout(
1635
+ plot_bgcolor="white",paper_bgcolor="white",
1636
+ font=dict(family="-apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif",
1637
+ size=11,color="#1d1d1f"),
1638
+ showlegend=False,margin=dict(l=0,r=0,t=10,b=0),
1639
+ xaxis=dict(showgrid=False,color="#86868b",
1640
+ tickfont=dict(size=11,color="#6e6e73")),
1641
+ yaxis=dict(gridcolor="#f5f5f7",color="#86868b"),
1642
+ )
1643
+ st.plotly_chart(fig3, use_container_width=True)
1644
+ except ImportError:
1645
+ st.dataframe(out_df.groupby("class")["word_count"].describe().round(1),
1646
+ use_container_width=True)
1647
+
1648
+ st.markdown("</div></div>", unsafe_allow_html=True)
1649
+
1650
+ # ── Section E: Spotlight ─────────────────────────────────────────────
1651
+ st.markdown("""
1652
+ <div class="section">
1653
+ <div class="section-inner">
1654
+ <span class="s-label rise r1">05 Β· Article Spotlight</span>
1655
+ <h2 class="s-h rise r2" style="font-size:2.2rem;margin-bottom:.5rem;">
1656
+ Discover a random article.
1657
+ </h2>
1658
+ </div>
1659
+ </div>
1660
+ """, unsafe_allow_html=True)
1661
+
1662
+ st.markdown('<div class="section-alt"><div class="section-inner">', unsafe_allow_html=True)
1663
+
1664
+ if st.button("Shuffle Article", key="spot"):
1665
+ row = out_df.sample(1).iloc[0]
1666
+ label = row.get("class","")
1667
+ meta = CATS.get(label, {"icon":"β—‰","color":"#1d1d1f","bg":"#f5f5f7"})
1668
+ conf_v = row.get("confidence", None)
1669
+ text = str(row["content"])
1670
+ wc_c = len(text.split())
1671
+
1672
+ st.markdown(f"""
1673
+ <div class="spotlight" style="border-top:2px solid {meta['color']};">
1674
+ <div style="display:flex;align-items:center;gap:8px;
1675
+ margin-bottom:1.3rem;flex-wrap:wrap;">
1676
+ <span class="spot-badge"
1677
+ style="color:{meta['color']};border-color:{meta['color']}30;
1678
+ background:{meta['bg']};">
1679
+ {meta['icon']}&nbsp; {label.replace('_',' ')}
1680
+ </span>
1681
+ {f'<span class="spot-badge" style="color:var(--text-secondary);border-color:var(--border);background:var(--bg-secondary);">{conf_v:.1%} confidence</span>' if conf_v else ""}
1682
+ <span class="spot-badge"
1683
+ style="color:var(--text-secondary);border-color:var(--border);
1684
+ background:var(--bg-secondary);">
1685
+ {wc_c} words
1686
+ </span>
1687
+ </div>
1688
+ <div class="spot-text">
1689
+ {text[:640]}{"…" if len(text)>640 else ""}
1690
+ </div>
1691
+ </div>""", unsafe_allow_html=True)
1692
+
1693
+ st.markdown("</div></div>", unsafe_allow_html=True)
1694
 
1695
+ # ─────────────────────────────────────────────────────────────────────────────
1696
+ # ══════════════ FOOTER ═══════════════════════════════════════════════════════
1697
+ # ─────────────────────────────────────────────────────────────────────────────
1698
+ st.markdown("""
1699
+ <div id="footer">
1700
+ <div class="footer-inner">
1701
+ <div class="footer-top">
1702
+ <div>
1703
+ <div class="footer-brand">β—‰ News<span>Lens</span> AI</div>
1704
+ <div class="footer-tagline">
1705
+ Daily Mirror Β· AI Intelligence Β· DA3111 Text Analytics
1706
+ </div>
1707
+ </div>
1708
+ <div class="footer-links">
1709
+ <span class="footer-link">Streamlit</span>
1710
+ <span class="footer-link">Hugging Face</span>
1711
+ <span class="footer-link">Transformers</span>
1712
+ <span class="footer-link">NLTK</span>
1713
+ <span class="footer-link">Plotly</span>
1714
+ </div>
1715
+ </div>
1716
+ <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;">
1717
+ <p class="footer-copy">
1718
+ Copyright Β© 2026 NewsLens AI. DA3111 Text Analytics Assignment 01.
1719
+ </p>
1720
+ <p class="footer-copy">
1721
+ Model: Akilashamnaka12/news_classifier_model
1722
+ </p>
1723
+ </div>
1724
+ </div>
1725
+ </div>
1726
+ """, unsafe_allow_html=True)