yomnafarag95 commited on
Commit
42c894e
Β·
verified Β·
1 Parent(s): 5ab0b46

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +374 -0
  2. label_encoder.joblib +3 -0
  3. model.joblib +3 -0
  4. requirements.txt +5 -2
  5. scaler.joblib +3 -0
app.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import joblib, numpy as np, pandas as pd, re, os, datetime, matplotlib, base64, io
3
+ import matplotlib.pyplot as plt, matplotlib.patches as mpatches
4
+ from collections import Counter
5
+ matplotlib.use("Agg")
6
+
7
+ st.set_page_config(page_title="Firewall Log Classifier", layout="wide", initial_sidebar_state="collapsed")
8
+ MODEL_DIR = os.path.dirname(os.path.abspath(__file__))
9
+
10
+ @st.cache_resource
11
+ def load_artifacts():
12
+ m=joblib.load(os.path.join(MODEL_DIR,"model.joblib"))
13
+ s=joblib.load(os.path.join(MODEL_DIR,"scaler.joblib"))
14
+ l=joblib.load(os.path.join(MODEL_DIR,"label_encoder.joblib"))
15
+ return m,s,l
16
+ model,scaler,le=load_artifacts()
17
+ AC={"allow":"#7ec8e3","deny":"#f4a261","drop":"#f9e07f","reset-both":"#b39ddb"}
18
+ for k,v in [("page","home"),("chat_history",[]),("last_result",None),("uploaded_results",[]),("file_name","")]:
19
+ if k not in st.session_state: st.session_state[k]=v
20
+
21
+ def parse_log_line(line):
22
+ nums=[int(n) for n in re.findall(r"\b(\d+)\b",line)]; nums+=[0]*(11-len(nums)); return nums[:11]
23
+ def predict(features):
24
+ a=np.array(features,dtype=float).reshape(1,-1); sc=scaler.transform(a)
25
+ lbl=model.predict(sc)[0]; pr=model.predict_proba(sc)[0]
26
+ act=le.inverse_transform([lbl])[0]; conf=float(np.max(pr))
27
+ pd_={le.inverse_transform([i])[0]:round(float(p)*100,1) for i,p in enumerate(pr)}
28
+ return act,conf,pd_
29
+ def bc(a): return AC.get(a,"#888")
30
+ def add_hist(log,action,conf):
31
+ st.session_state.chat_history.append({"time":datetime.datetime.now().strftime("%H:%M:%S"),"log":log[:80]+("..." if len(log)>80 else ""),"action":action,"confidence":conf})
32
+ def fig_to_b64(fig):
33
+ buf=io.BytesIO(); fig.savefig(buf,format="png",dpi=120,bbox_inches="tight",transparent=True); buf.seek(0)
34
+ return base64.b64encode(buf.read()).decode()
35
+
36
+ def lfig(w=2.5,h=1.5):
37
+ fig,ax=plt.subplots(figsize=(w,h))
38
+ fig.patch.set_alpha(0); ax.set_facecolor((0.94,0.97,1.0,0.5))
39
+ ax.tick_params(colors="#4a7a9a",labelsize=5); ax.xaxis.label.set_color("#4a7a9a"); ax.yaxis.label.set_color("#4a7a9a")
40
+ for sp in ax.spines.values(): sp.set_edgecolor("#b0d8f0"); sp.set_linewidth(0.6)
41
+ return fig,ax
42
+
43
+ # ── CSS ───────────────────────────────────────────────────────────────────────
44
+ st.markdown("""
45
+ <style>
46
+ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300&family=Outfit:wght@300;400;500;600&display=swap');
47
+ *,html,body,[class*="css"]{font-family:'Outfit',sans-serif;box-sizing:border-box;}
48
+ .stApp{background:radial-gradient(ellipse at 15% 60%,#8ecfee 0%,#b0dcf5 25%,#d8eefa 45%,#eef6fc 65%,#ffffff 80%,#cce8f5 100%);background-attachment:fixed;min-height:100vh;}
49
+ #MainMenu,footer,header{visibility:hidden;}
50
+ .block-container{padding:0.5rem 2rem 0.2rem 2rem !important;max-width:100% !important;}
51
+ section[data-testid="stSidebar"]{display:none !important;}
52
+ [data-testid="collapsedControl"]{display:none !important;}
53
+
54
+ /* ── Greeting ── */
55
+ .greeting-glow{position:relative;text-align:center;margin-bottom:36px;animation:floatUp 0.85s cubic-bezier(0.16,1,0.3,1) both;}
56
+ .greeting-glow::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-55%);width:600px;height:320px;background:radial-gradient(ellipse at center,rgba(255,255,255,0.82) 0%,rgba(255,255,255,0.45) 45%,transparent 75%);border-radius:50%;pointer-events:none;z-index:0;}
57
+ .greeting-glow>*{position:relative;z-index:1;}
58
+ .greeting-hi{font-family:'Cormorant Garamond',serif;font-size:clamp(2.4rem,5vw,3.8rem);font-weight:300;color:#1a4a72;line-height:1.25;margin-bottom:8px;}
59
+ .greeting-hi strong{font-weight:600;color:#0a7ab5;}
60
+ .greeting-sub{font-size:0.95rem;color:#6a9abb;font-weight:300;}
61
+ @keyframes floatUp{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:translateY(0)}}
62
+
63
+ /* ── Textarea ── */
64
+ .stTextArea textarea{border:1px solid rgba(140,200,235,0.35) !important;border-radius:16px !important;background:white !important;font-size:0.97rem !important;color:#2a4060 !important;padding:20px 22px !important;resize:none !important;box-shadow:0 8px 32px rgba(80,160,220,0.10) !important;outline:none !important;}
65
+ .stTextArea textarea:focus{border-color:rgba(74,176,232,0.45) !important;outline:none !important;}
66
+ .stTextArea textarea::placeholder{color:#aac8de !important;font-family:'Cormorant Garamond',serif !important;font-style:italic !important;}
67
+ .stTextArea label{display:none !important;}
68
+ .stTextArea,.stTextArea>div,.stTextArea>div>div{border:none !important;box-shadow:none !important;background:transparent !important;outline:none !important;}
69
+ [data-testid="stFileUploader"]{visibility:hidden !important;height:0 !important;overflow:hidden !important;margin:0 !important;padding:0 !important;}
70
+
71
+ /* ── All Streamlit buttons: transparent, white border, Cormorant, #1a4a72 ── */
72
+ div.stButton > button,
73
+ div.stButton > button:hover,
74
+ div.stButton > button:focus,
75
+ div.stButton > button:active {
76
+ background: transparent !important;
77
+ background-color: transparent !important;
78
+ border: 1.5px solid rgba(255,255,255,0.85) !important;
79
+ border-radius: 12px !important;
80
+ color: #1a4a72 !important;
81
+ -webkit-text-fill-color: #1a4a72 !important;
82
+ font-family: 'Cormorant Garamond', serif !important;
83
+ font-size: 1.2rem !important;
84
+ font-weight: 500 !important;
85
+ letter-spacing: 0.06em !important;
86
+ box-shadow: none !important;
87
+ outline: none !important;
88
+ padding: 11px 0 !important;
89
+ width: 100% !important;
90
+ transition: background 0.2s ease, border-color 0.2s ease !important;
91
+ }
92
+ div.stButton > button:hover {
93
+ background: rgba(255,255,255,0.22) !important;
94
+ border-color: white !important;
95
+ }
96
+ div.stButton > button p {
97
+ font-family: 'Cormorant Garamond', serif !important;
98
+ font-size: 1.2rem !important;
99
+ font-weight: 500 !important;
100
+ color: #1a4a72 !important;
101
+ -webkit-text-fill-color: #1a4a72 !important;
102
+ letter-spacing: 0.06em !important;
103
+ margin: 0 !important;
104
+ }
105
+
106
+ /* ── Button columns: force full width at every level ── */
107
+ [data-testid="stHorizontalBlock"] { gap: 6px !important; }
108
+ div.stButton { width: 100% !important; display: block !important; }
109
+ div.stButton > button { display: block !important; }
110
+ .metric-card{background:rgba(255,255,255,0.50);backdrop-filter:blur(6px);border:1px solid rgba(180,220,245,0.55);border-radius:14px;padding:12px 10px;text-align:center;}
111
+ .metric-card .label{font-size:0.63rem;color:#90b8d4;text-transform:uppercase;letter-spacing:0.12em;margin-bottom:7px;font-weight:600;}
112
+ .metric-card .value{font-family:'Cormorant Garamond',serif;font-size:2rem;font-weight:600;color:#1a7ab5;line-height:1;}
113
+ .metric-card .sub{font-size:0.7rem;color:#90b8d4;margin-top:5px;}
114
+ .result-card{background:rgba(255,255,255,0.55);backdrop-filter:blur(8px);border-radius:16px;padding:18px 24px;box-shadow:0 4px 20px rgba(80,160,220,0.10);border:1px solid rgba(180,220,245,0.50);margin-top:16px;animation:floatUp 0.7s cubic-bezier(0.16,1,0.3,1) both;}
115
+ .sec-head{font-size:0.64rem;font-weight:600;letter-spacing:0.14em;text-transform:uppercase;color:#7ab0d0;border-bottom:1px solid rgba(140,200,235,0.26);padding-bottom:7px;margin-bottom:14px;margin-top:28px;}
116
+ .chat-item{background:white;border:1px solid rgba(140,200,235,0.26);border-left:3px solid #4ab0e8;border-radius:0 12px 12px 0;padding:14px 20px;margin:8px 0;font-size:0.92rem;color:#2a4060;}
117
+ .chat-item .timestamp{font-size:0.68rem;letter-spacing:0.10em;text-transform:uppercase;color:#90b8d4;margin-bottom:4px;font-weight:600;}
118
+ .chat-action-badge{display:inline-block;padding:2px 12px;border-radius:20px;font-size:0.74rem;letter-spacing:0.10em;text-transform:uppercase;font-weight:600;margin-top:6px;}
119
+ .chart-card{background:rgba(255,255,255,0.55);backdrop-filter:blur(10px);border:1px solid rgba(160,215,245,0.55);border-radius:16px;padding:10px 12px 6px;box-shadow:0 4px 18px rgba(80,160,220,0.10),inset 0 1px 0 rgba(255,255,255,0.8);}
120
+ .chart-title{font-size:0.6rem;font-weight:600;letter-spacing:0.12em;text-transform:uppercase;color:#5a9abf;margin-bottom:4px;}
121
+ .stDataFrame{border:1px solid rgba(140,200,235,0.30) !important;border-radius:12px !important;overflow:hidden !important;}
122
+ .stDataFrame > div,.stDataFrame [data-testid="stDataFrameResizable"]{background:white !important;border-radius:12px !important;}
123
+ .stDataFrame iframe{background:white !important;color-scheme:light !important;}
124
+ [data-testid="stImage"],[data-testid="stpyplot"]{margin:0 !important;padding:0 !important;}
125
+ .element-container{margin-bottom:0 !important;}
126
+ ::-webkit-scrollbar{width:5px;}::-webkit-scrollbar-thumb{background:rgba(74,176,232,0.35);border-radius:10px;}
127
+ </style>
128
+ """, unsafe_allow_html=True)
129
+
130
+
131
+ # ── HOME ──────────────────────────────────────────────────────────────────────
132
+ if st.session_state.page == "home":
133
+ st.markdown("<div style='height:14vh'></div>", unsafe_allow_html=True)
134
+ _, cx, _ = st.columns([1, 2.4, 1])
135
+ with cx:
136
+ st.markdown("""<div class="greeting-glow">
137
+ <div class="greeting-hi">Hi, I am <strong>Log Classifier</strong><br>How can I help you today?</div>
138
+ </div>""", unsafe_allow_html=True)
139
+
140
+ log_input = st.text_area("log", placeholder="Ask me anything...", height=160,
141
+ key="log_text_input", label_visibility="collapsed")
142
+
143
+ uploaded_file = st.file_uploader("u", type=["txt","csv","log"],
144
+ label_visibility="collapsed", key="home_uploader")
145
+ if uploaded_file:
146
+ content=uploaded_file.read().decode("utf-8",errors="ignore")
147
+ lines=[l.strip() for l in content.splitlines() if l.strip()]; results=[]
148
+ for line in lines:
149
+ try:
150
+ feats=parse_log_line(line); act,conf,probs=predict(feats)
151
+ results.append({"log":line[:60],"action":act,"confidence":round(conf*100,1),"probabilities":probs})
152
+ except: pass
153
+ st.session_state.update({"uploaded_results":results,"file_name":uploaded_file.name,"page":"dashboard"}); st.rerun()
154
+
155
+ # All 5 buttons in one row, spanning the full width of the input box
156
+ c1, c2, c3, c4, c5 = st.columns(5)
157
+ with c1: new_chat_btn = st.button("New Chat", key="btn_new_chat")
158
+ with c2: history_btn = st.button("Chat History", key="btn_chat_history")
159
+ with c3: analyze_btn = st.button("Analyze", key="btn_analyze")
160
+ with c4: upload_btn = st.button("Upload", key="btn_upload")
161
+ with c5: dashboard_btn = st.button("Dashboard", key="btn_dashboard")
162
+
163
+ if new_chat_btn:
164
+ st.session_state.update({"chat_history":[],"last_result":None,"uploaded_results":[],"file_name":"","page":"home"}); st.rerun()
165
+ if history_btn:
166
+ st.session_state.page = "history"; st.rerun()
167
+
168
+ if analyze_btn:
169
+ if log_input.strip():
170
+ try:
171
+ feats=parse_log_line(log_input); act,conf,probs=predict(feats)
172
+ st.session_state.last_result={"log":log_input,"action":act,"confidence":round(conf*100,1),"probabilities":probs,"features":feats}
173
+ add_hist(log_input,act,round(conf*100,1)); st.rerun()
174
+ except Exception as e: st.error(f"Prediction error: {e}")
175
+ else: st.warning("Please paste a log entry before analyzing.")
176
+ if upload_btn: st.info("Drag and drop a file above.")
177
+ if dashboard_btn: st.session_state.page="dashboard"; st.rerun()
178
+
179
+ if st.session_state.last_result:
180
+ r=st.session_state.last_result; color=bc(r["action"])
181
+ pb="".join([f'<span style="display:inline-block;margin-right:12px;font-size:0.8rem;color:#4a7a9a;"><span style="color:{bc(k)};font-weight:600;">{k}</span> {v}%</span>' for k,v in r["probabilities"].items()])
182
+ st.markdown(f'''<div class="result-card">
183
+ <div style="font-size:0.6rem;font-weight:600;letter-spacing:0.14em;text-transform:uppercase;color:#7ab0d0;margin-bottom:10px;">Prediction Result</div>
184
+ <div style="display:flex;align-items:baseline;gap:12px;margin-bottom:8px;">
185
+ <span style="font-family:'Cormorant Garamond',serif;font-size:2.2rem;font-weight:600;color:{color};">{r["action"].upper()}</span>
186
+ <span style="font-size:0.85rem;color:#6a9abb;font-weight:300;">{r["confidence"]}% confidence</span>
187
+ </div>
188
+ <div style="padding-top:6px;border-top:1px solid rgba(140,200,235,0.25);">{pb}</div>
189
+ </div>''', unsafe_allow_html=True)
190
+
191
+ # ── HISTORY ───────────────────────────────────────────────────────────────────
192
+ elif st.session_state.page == "history":
193
+ st.markdown("<div style='height:14vh'></div>", unsafe_allow_html=True)
194
+ _, hcol, _ = st.columns([1, 2.4, 1])
195
+ with hcol:
196
+ st.markdown('<div class="back-btn" style="margin-bottom:18px">', unsafe_allow_html=True)
197
+ if st.button("← Back to Home", key="hist_back"): st.session_state.page="home"; st.rerun()
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+ st.markdown('<div class="sec-head" style="margin-top:0">Chat History</div>', unsafe_allow_html=True)
200
+ if not st.session_state.chat_history:
201
+ st.markdown('<div class="chat-item">No history yet in this session.</div>', unsafe_allow_html=True)
202
+ else:
203
+ for item in reversed(st.session_state.chat_history):
204
+ color=bc(item["action"])
205
+ st.markdown(f'<div class="chat-item"><div class="timestamp">{item["time"]}</div><div style="margin-bottom:6px;">{item["log"]}</div><span class="chat-action-badge" style="background:{color}20;color:{color};border:1px solid {color}50;">{item["action"].upper()}</span><span style="font-size:0.76rem;color:#90b8d4;margin-left:10px;">{item["confidence"]}% confidence</span></div>', unsafe_allow_html=True)
206
+
207
+ # ── DASHBOARD ─────────────────────────────────────────────────────────────────
208
+ elif st.session_state.page == "dashboard":
209
+ hd1,hd2=st.columns([6,1])
210
+ with hd1: st.markdown('<div style=\'font-family:"Cormorant Garamond",serif;font-size:1.5rem;font-weight:400;color:#1a4a72;margin-bottom:4px;\'>Dashboard <span style="font-size:0.58rem;color:#7ab0d0;text-transform:uppercase;letter-spacing:0.1em;font-weight:600;vertical-align:middle;margin-left:6px;">Firewall Log Classification</span></div>', unsafe_allow_html=True)
211
+ with hd2:
212
+ if st.button("← Home", key="dash_back"): st.session_state.page="home"; st.rerun()
213
+
214
+ all_results=[]
215
+ if st.session_state.last_result: all_results.append(st.session_state.last_result)
216
+ all_results+=list(st.session_state.uploaded_results)
217
+
218
+ if not all_results:
219
+ st.info("No predictions yet.")
220
+ else:
221
+ actions=[r["action"] for r in all_results]; count=Counter(actions); labels=["allow","deny","drop","reset-both"]
222
+ total=len(all_results)
223
+ allow_n=count.get("allow",0); block_n=count.get("deny",0)+count.get("drop",0); reset_n=count.get("reset-both",0)
224
+ allow_pct=round(allow_n/total*100) if total else 0
225
+ block_pct=round(block_n/total*100) if total else 0
226
+
227
+ # ── Row 1: metric cards ──
228
+ card_style="background:rgba(255,255,255,0.60);backdrop-filter:blur(8px);border-radius:12px;padding:7px 12px;box-shadow:0 3px 14px rgba(80,160,220,0.08),inset 0 1px 0 rgba(255,255,255,0.9);"
229
+ lbl_style="font-size:0.50rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#7ab0d0;margin-bottom:2px;"
230
+ sub_style="font-size:0.55rem;color:#90b8d4;margin-top:2px;"
231
+ m1,m2,m3,m4=st.columns(4)
232
+ m1.markdown(f'<div style="{card_style}border:1px solid rgba(160,215,245,0.55);"><div style="{lbl_style}">Total</div><div style="font-family:\'Cormorant Garamond\',serif;font-size:1.6rem;font-weight:600;color:#1a4a72;line-height:1;">{total}</div><div style="{sub_style}">entries classified</div></div>',unsafe_allow_html=True)
233
+ m2.markdown(f'<div style="{card_style}border:1px solid rgba(126,200,227,0.45);"><div style="{lbl_style}">Allow</div><div style="font-family:\'Cormorant Garamond\',serif;font-size:1.6rem;font-weight:600;color:#7ec8e3;line-height:1;">{allow_n}</div><div style="{sub_style}">{allow_pct}% of traffic</div></div>',unsafe_allow_html=True)
234
+ m3.markdown(f'<div style="{card_style}border:1px solid rgba(244,162,97,0.35);"><div style="{lbl_style}">Blocked</div><div style="font-family:\'Cormorant Garamond\',serif;font-size:1.6rem;font-weight:600;color:#f4a261;line-height:1;">{block_n}</div><div style="{sub_style}">{block_pct}% of traffic</div></div>',unsafe_allow_html=True)
235
+ m4.markdown(f'<div style="{card_style}border:1px solid rgba(179,157,219,0.40);"><div style="{lbl_style}">Reset-Both</div><div style="font-family:\'Cormorant Garamond\',serif;font-size:1.6rem;font-weight:600;color:#b39ddb;line-height:1;">{reset_n}</div><div style="{sub_style}">terminated</div></div>',unsafe_allow_html=True)
236
+
237
+ # ── Row 2: charts ──
238
+ ch1, ch2 = st.columns(2)
239
+
240
+ with ch1:
241
+ st.markdown('<div style="background:rgba(255,255,255,0.60);backdrop-filter:blur(10px);border:1px solid rgba(160,215,245,0.50);border-radius:14px;padding:7px 12px 4px;box-shadow:0 3px 14px rgba(80,160,220,0.08);"><div style="font-size:0.50rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#5a9abf;margin-bottom:3px;">Action Distribution</div>', unsafe_allow_html=True)
242
+ fig_p,ax_p=plt.subplots(figsize=(2.8,1.7))
243
+ fig_p.patch.set_alpha(0); ax_p.set_facecolor((0,0,0,0))
244
+ non_zero=[(count.get(l,0),bc(l),l) for l in labels if count.get(l,0)>0]
245
+ if non_zero:
246
+ s_,c_,l_=zip(*non_zero)
247
+ wedges,_,at=ax_p.pie(s_,labels=None,colors=c_,autopct="%1.0f%%",startangle=90,
248
+ pctdistance=0.68,wedgeprops={"linewidth":2,"edgecolor":"white","width":0.48})
249
+ for t in at: t.set_fontsize(6.5); t.set_color("white"); t.set_fontweight("bold")
250
+ for w in wedges: w.set_alpha(0.90)
251
+ ax_p.text(0,0,f"{total}\nlogs",ha="center",va="center",fontsize=7.5,color="#1a4a72",fontweight="bold",linespacing=1.3)
252
+ all_patches=[mpatches.Patch(color=bc(l),alpha=0.9,label=f"{l.upper()} {count.get(l,0)}") for l in labels]
253
+ ax_p.legend(handles=all_patches,loc="lower center",bbox_to_anchor=(0.5,-0.06),ncol=2,fontsize=5.5,framealpha=0,labelcolor="#2a5a82")
254
+ fig_p.tight_layout(pad=0.1)
255
+ st.pyplot(fig_p, use_container_width=False); plt.close(fig_p)
256
+ st.markdown('</div>', unsafe_allow_html=True)
257
+
258
+ with ch2:
259
+ st.markdown('<div style="background:rgba(255,255,255,0.60);backdrop-filter:blur(10px);border:1px solid rgba(160,215,245,0.50);border-radius:14px;padding:7px 12px 4px;box-shadow:0 3px 14px rgba(80,160,220,0.08);"><div style="font-size:0.50rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#5a9abf;margin-bottom:3px;">Confidence per Entry <span style="color:#4ab0e8;font-weight:400;">β€” 80% threshold</span></div>', unsafe_allow_html=True)
260
+ fig_b,ax_b=plt.subplots(figsize=(3.4,1.7))
261
+ fig_b.patch.set_alpha(0); ax_b.set_facecolor((0.95,0.97,1.0,0.5))
262
+ confs=[r["confidence"] for r in all_results[-12:]]; bar_c=[bc(r["action"]) for r in all_results[-12:]]
263
+ x_pos=list(range(len(confs)))
264
+ bars=ax_b.bar(x_pos,confs,color=bar_c,edgecolor="white",linewidth=0.8,width=0.62,alpha=0.88)
265
+ ax_b.axhline(y=80,color="#4ab0e8",linewidth=0.8,linestyle="--",alpha=0.8)
266
+ ax_b.set_ylim(0,120); ax_b.set_yticks([0,50,100]); ax_b.set_yticklabels(["0%","50%","100%"],fontsize=5.5)
267
+ ax_b.set_xticks(x_pos); ax_b.set_xticklabels([str(i+1) for i in x_pos],fontsize=5.5)
268
+ ax_b.tick_params(colors="#4a7a9a",length=2); ax_b.grid(axis="y",color="#d0eaf8",linewidth=0.5,alpha=0.9)
269
+ for sp in ax_b.spines.values(): sp.set_edgecolor("#c8e0f0"); sp.set_linewidth(0.5)
270
+ for b,v in zip(bars,confs): ax_b.text(b.get_x()+b.get_width()/2,v+1,f"{v:.0f}%",ha="center",color="#2a5a82",fontsize=4.5,fontweight="bold")
271
+ seen={}
272
+ for r in all_results:
273
+ if r["action"] not in seen: seen[r["action"]]=bc(r["action"])
274
+ ax_b.legend([mpatches.Patch(color=c,alpha=0.88) for c in seen.values()],[k.upper() for k in seen],
275
+ loc="upper right",fontsize=5,framealpha=0.5,labelcolor="#2a5a82",edgecolor="#d0e8f4")
276
+ fig_b.tight_layout(pad=0.2)
277
+ st.pyplot(fig_b, use_container_width=False); plt.close(fig_b)
278
+ st.markdown('</div>', unsafe_allow_html=True)
279
+
280
+ # ── Row 3: white HTML table ──
281
+ rows_html="".join([f'<tr><td style="padding:5px 10px;font-size:0.78rem;color:#2a4060;border-bottom:1px solid rgba(200,230,248,0.5);max-width:500px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{"("+r["log"][:65]+")" if not isinstance(r["log"],str) else r["log"][:65]}</td><td style="padding:5px 10px;font-size:0.75rem;font-weight:600;color:{bc(r["action"])};border-bottom:1px solid rgba(200,230,248,0.5);white-space:nowrap;">{r["action"].upper()}</td><td style="padding:5px 10px;font-size:0.75rem;color:#4a7a9a;border-bottom:1px solid rgba(200,230,248,0.5);white-space:nowrap;">{r["confidence"]}%</td></tr>' for r in all_results])
282
+ st.markdown(f"""
283
+ <div style="background:white;border:1px solid rgba(140,200,235,0.35);border-radius:12px;overflow:hidden;margin-top:6px;">
284
+ <table style="width:100%;border-collapse:collapse;background:white;">
285
+ <thead><tr style="background:rgba(220,240,252,0.85);">
286
+ <th style="padding:6px 10px;font-size:0.60rem;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:#1a4a72;text-align:left;">Log</th>
287
+ <th style="padding:6px 10px;font-size:0.60rem;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:#1a4a72;text-align:left;">Action</th>
288
+ <th style="padding:6px 10px;font-size:0.60rem;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:#1a4a72;text-align:left;">Confidence</th>
289
+ </tr></thead>
290
+ <tbody>{rows_html}</tbody>
291
+ </table>
292
+ </div>""", unsafe_allow_html=True)
293
+
294
+ # ── FORCE STYLES via JavaScript β€” runs after React renders, always wins ───────
295
+ st.markdown("""
296
+ <script>
297
+ (function() {
298
+ function styleBtn(btn) {
299
+ var s = btn.style;
300
+ s.setProperty('background', 'transparent', 'important');
301
+ s.setProperty('background-color', 'transparent', 'important');
302
+ s.setProperty('border', '1.5px solid rgba(255,255,255,0.85)', 'important');
303
+ s.setProperty('border-radius', '10px', 'important');
304
+ s.setProperty('display', 'block', 'important');
305
+ s.setProperty('color', '#1a4a72', 'important');
306
+ s.setProperty('-webkit-text-fill-color', '#1a4a72', 'important');
307
+ s.setProperty('font-family', "'Cormorant Garamond', serif", 'important');
308
+ s.setProperty('font-size', '1.2rem', 'important');
309
+ s.setProperty('font-weight', '500', 'important');
310
+ s.setProperty('letter-spacing', '0.06em', 'important');
311
+ s.setProperty('box-shadow', 'none', 'important');
312
+ s.setProperty('outline', 'none', 'important');
313
+ s.setProperty('width', '100%', 'important');
314
+ s.setProperty('text-align', 'center', 'important');
315
+ s.setProperty('padding', '11px 0', 'important');
316
+ s.setProperty('cursor', 'pointer', 'important');
317
+ // Force parent div.stButton to full width too
318
+ if (btn.parentElement) {
319
+ btn.parentElement.style.setProperty('width', '100%', 'important');
320
+ btn.parentElement.style.setProperty('display', 'block','important');
321
+ }
322
+ // Style the <p> inside (Streamlit wraps text in <p>)
323
+ var p = btn.querySelector('p');
324
+ if (p) {
325
+ p.style.setProperty('font-family', "'Cormorant Garamond', serif", 'important');
326
+ p.style.setProperty('font-size', '1.2rem', 'important');
327
+ p.style.setProperty('font-weight', '500', 'important');
328
+ p.style.setProperty('color', '#1a4a72', 'important');
329
+ p.style.setProperty('-webkit-text-fill-color', '#1a4a72', 'important');
330
+ p.style.setProperty('letter-spacing', '0.06em', 'important');
331
+ p.style.setProperty('margin', '0', 'important');
332
+ }
333
+ btn.addEventListener('mouseenter', function(){
334
+ btn.style.setProperty('background','rgba(255,255,255,0.22)','important');
335
+ btn.style.setProperty('border-color','white','important');
336
+ });
337
+ btn.addEventListener('mouseleave', function(){
338
+ btn.style.setProperty('background','transparent','important');
339
+ btn.style.setProperty('border','1.5px solid rgba(255,255,255,0.85)','important');
340
+ });
341
+ }
342
+
343
+ function applyAll() {
344
+ document.querySelectorAll('div.stButton > button').forEach(styleBtn);
345
+ }
346
+
347
+ applyAll();
348
+ [200, 500, 1000, 2000].forEach(function(d){ setTimeout(applyAll, d); });
349
+
350
+ var observer = new MutationObserver(function(mutations) {
351
+ var needsUpdate = false;
352
+ mutations.forEach(function(m) {
353
+ if (m.addedNodes.length) needsUpdate = true;
354
+ });
355
+ if (needsUpdate) applyAll();
356
+ });
357
+ observer.observe(document.body, { childList: true, subtree: true });
358
+
359
+ // Force dataframe iframe white
360
+ function styleDataframes() {
361
+ document.querySelectorAll('.stDataFrame iframe').forEach(function(iframe) {
362
+ try {
363
+ var doc = iframe.contentDocument || iframe.contentWindow.document;
364
+ if (!doc) return;
365
+ var s = doc.createElement('style');
366
+ s.textContent = 'body,html,table,thead,tbody,tr,th,td,div{background:white !important;color:#2a4060 !important;} thead th,thead td{background:rgba(220,240,252,0.95) !important;color:#1a4a72 !important;font-weight:700 !important;text-transform:uppercase !important;font-size:0.7rem !important;letter-spacing:0.06em !important;} tr:nth-child(even) td{background:rgba(235,247,255,0.7) !important;}';
367
+ doc.head.appendChild(s);
368
+ } catch(e) {}
369
+ });
370
+ }
371
+ [500,1000,2000,3000].forEach(function(d){ setTimeout(styleDataframes, d); });
372
+ })();
373
+ </script>
374
+ """, unsafe_allow_html=True)
label_encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfe75e6d2a847020f3ca10fb09c74e2982dbf85616ef5c310225a4c13c6cee38
3
+ size 508
model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27dfde6db68fa8cabdb5ddaec8257b094c6d03d921103446cd1a113dea00bdfc
3
+ size 8236641
requirements.txt CHANGED
@@ -1,3 +1,6 @@
1
- altair
 
 
2
  pandas
3
- streamlit
 
 
1
+ streamlit
2
+ joblib
3
+ numpy
4
  pandas
5
+ matplotlib
6
+ scikit-learn
scaler.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05ae874220fa3824577365214cfebea1e6b3cdca775a06bec5f74abe097feeb5
3
+ size 863