# ui.py # 此檔案負責 Gradio 用戶界面設計 # 包含登入界面、儀表板、管理員界面等 import gradio as gr import pandas as pd from config import ACTION_MAP, USER_DB, SYSTEM_WEIGHTS from models import log_history, AI_READY from auth import process_login, fast_login, logout, toggle_role, simulate_click, test_anomaly, unblock_user from security import explain_abnormal_log from plotting import get_dashboard_html, get_plot_data, timer_update from utils import update_system_weights, get_current_time from plotting import get_plot_data_for_api # 自訂 CSS custom_css = """ .mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); } .login-container { max-width: 500px; margin: 0 auto; padding: 20px; } .ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;} .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; } .headless-tabs > div:first-child { display: none !important; } .headless-tabs { border: none !important; background: transparent !important; } /* 統計儀表板精緻化樣式 */ .gradio-container .stat-dashboard { display: flex !important; gap: 20px !important; margin-bottom: 20px !important; justify-content: space-between !important; flex-wrap: nowrap !important; } .gradio-container .stat-card { flex: 1 !important; background-color: #ffffff !important; padding: 20px !important; border-radius: 12px !important; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; display: flex !important; align-items: center !important; gap: 20px !important; position: relative !important; overflow: hidden !important; transition: transform 0.2s ease-in-out !important; min-width: 250px !important; } .gradio-container .stat-card:hover { transform: translateY(-2px) !important; } /* 左側邊框色塊 */ .gradio-container .card-total { border-left: 6px solid #10b981 !important; } .gradio-container .card-anomaly { border-left: 6px solid #ef4444 !important; } .gradio-container .card-status { border-left: 6px solid #3b82f6 !important; border-top: 6px solid #10b981 !important; } /* 圖示容器 */ .gradio-container .card-icon-container { width: 50px !important; height: 50px !important; display: flex !important; align-items: center !important; justify-content: center !important; } .gradio-container .icon-bg { width: 45px !important; height: 45px !important; border-radius: 10px !important; display: flex !important; align-items: center !important; justify-content: center !important; } /* 文字內容區域 */ .gradio-container .card-content { flex: 1 !important; display: flex !important; flex-direction: column !important; } .gradio-container .card-label { color: #4b5563 !important; font-size: 14px !important; font-weight: 600 !important; margin-bottom: 4px !important; } .gradio-container .card-value-group { display: flex !important; align-items: baseline !important; gap: 8px !important; } .gradio-container .card-value { font-size: 28px !important; font-weight: 800 !important; line-height: 1.2 !important; color: #111827 !important; } .gradio-container .card-unit { font-size: 14px !important; color: #6b7280 !important; font-weight: normal !important; } .gradio-container .card-sub-text { color: #9ca3af !important; font-size: 12px !important; margin-top: 4px !important; } /* 趨勢/狀態標籤 */ .gradio-container .card-trend { font-size: 12px !important; padding: 2px 8px !important; border-radius: 6px !important; font-weight: bold !important; margin-left: 8px !important; background-color: #fee2e2 !important; color: #ef4444 !important; } .gradio-container .trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; } /* 系統狀態呼吸燈效果 */ .gradio-container .status-dot-container { width: 45px !important; height: 45px !important; display: flex !important; align-items: center !important; justify-content: center !important; } .gradio-container .status-dot { width: 30px !important; height: 30px !important; border-radius: 50% !important; position: relative !important; } .gradio-container .pulse-green { background-color: #10b981 !important; box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7) !important; animation: pulse-green 2s infinite !important; } .gradio-container .pulse-yellow { background-color: #f59e0b !important; box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7) !important; animation: pulse-yellow 2s infinite !important; } .gradio-container .pulse-red { background-color: #ef4444 !important; box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important; animation: pulse-red 2s infinite !important; } @keyframes pulse-green { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } } @keyframes pulse-yellow { 0% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); } } @keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } """ def toggle_time_input(mode): """切換時間輸入模式""" return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)")) def full_ui_update(selected_key): """更新所有 UI 元件:儀表板、圖表、所有 Log、異常 Log""" # 1. 獲取儀表板 HTML dash_html = get_dashboard_html() # 2. 獲取圖表 (直接使用 matplotlib Figure,交給 gr.Plot 渲染) plot_fig = get_plot_data(selected_key) # 3. 獲取 Log 資料 df_all = pd.DataFrame(log_history) abnormal_logs = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])] df_abn = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]) return dash_html, plot_fig, df_all, df_abn def create_demo(): """創建 Gradio 應用""" with gr.Blocks(title="模擬校園入口與資安防護系統") as demo: gr.HTML(f"") demo.css = custom_css store_df_all = gr.State(value=pd.DataFrame(log_history)) store_df_abn = gr.State(value=pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])) dashboard_timer = gr.Timer(value=60) with gr.Row(): with gr.Column(scale=1, elem_classes="mock-panel"): gr.Markdown("### ⚙️ 環境變數模擬器") mock_ip = gr.Dropdown( choices=["140.124.71.55 (校內預設)", "140.124.18.22 (宿舍)", "61.228.45.112 (家裡)", "210.61.47.88 (公共場所)", "45.33.22.11 (國外異常IP)"], value="140.124.71.55 (校內預設)", allow_custom_value=True, label="模擬來源 IP" ) mock_cookie = gr.Textbox(label="模擬 Cookie Session ID", value="sess_baseline") gr.Markdown("---") time_mode = gr.Radio( label="時間設定模式", choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"], value="真實時間 (目前時間)" ) custom_time_input = gr.Textbox( label="輸入自訂時間", placeholder="格式: YYYY-MM-DD HH:MM:SS", value="2026-03-17 03:00:00", visible=False ) gr.Markdown("---") gr.Markdown("### 📊 即時 Log 與 AI 判定區") log_display = gr.Dataframe( value=pd.DataFrame(log_history), headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True ) with gr.Column(scale=2): with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router: with gr.Tab("Login", id="login_route"): with gr.Column(elem_classes="login-container"): gr.Markdown("
校園入口網站 Taipei Tech Portal
") login_msg = gr.Markdown(value="", visible=True) with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin") with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234") with gr.Row(): gr.HTML("
F E I K
") with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)") login_btn = gr.Button("登入 Login", variant="primary") gr.Markdown("---") gr.Markdown("
🛠️ 開發與測試專用捷徑
") with gr.Row(): btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary") btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary") with gr.Tab("Dashboard", id="dashboard_route"): with gr.Column(): with gr.Row(): gr.Markdown("### 資訊系統") welcome_text = gr.Markdown(value="", elem_classes="text-right") logout_btn = gr.Button("登出", size="sm") with gr.Row(): btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary") gr.Markdown("---") with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs: with gr.Tab("👨‍🎓 學生專區", id="student_tab"): with gr.Accordion("▼ 1. 教務系統", open=True): with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm") btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm") btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm") with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm") btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm") btn_course = gr.Button("▶ 課程系統", size="sm") with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm") with gr.Accordion("▼ 2. 學務系統", open=True): with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm") btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm") btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm") with gr.Accordion("▼ 3. 資訊服務", open=True): with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm") btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm") with gr.Accordion("▼ 惡意操作測試區", open=True): btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm") with gr.Tab("🛡️ 管理員專區", id="admin_tab"): stat_dashboard_display = gr.HTML(value=get_dashboard_html()) with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"): action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=list(ACTION_MAP.keys())[0]) action_plot = gr.Plot(label="圖表顯示區") with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"): gr.Markdown("動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。") with gr.Row(): overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100) time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100) ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100) with gr.Row(): device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100) geo_weight = gr.Number(label="🌍 地理偏移敏感度 (%)", value=50, minimum=0, maximum=100) freq_weight = gr.Number(label="⚡ 行為頻率敏感度 (%)", value=60, minimum=0, maximum=100) weights_status = gr.Textbox(value="", visible=False) with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"): initial_abnormal = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])] abnormal_log_display = gr.Dataframe( value=pd.DataFrame(initial_abnormal) if initial_abnormal else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]), headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True ) with gr.Row(): abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4) btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1) ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False) with gr.Accordion("▼ 使用者狀態管理", open=True, elem_classes="weight-card"): from config import FIXED_TEST_EMAIL gr.Markdown(f"手動解除對測試帳號 ({FIXED_TEST_EMAIL}) 的封鎖狀態。") btn_unblock = gr.Button("🔓 解除封鎖 (Unblock User)", variant="primary") unblock_status = gr.Markdown(value="") with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"): gr.Markdown("點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。") gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)") with gr.Row(): t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm") t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm") with gr.Row(): t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm") t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm") with gr.Row(): t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm") t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm") gr.Markdown("#### 2. 期末與成績相關 (具備週期性)") with gr.Row(): t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm") t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm") with gr.Row(): t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm") t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm") gr.Markdown("#### 3. 校務與行政服務 (生活與常規)") with gr.Row(): t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm") t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm") with gr.Row(): t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm") t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm") with gr.Row(): t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm") t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm") gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)") with gr.Row(): t_istudy_norm = gr.Button("✅ 正常: 北科i學園PLUS (10-18 16:00)", size="sm") t_istudy_anom = gr.Button("⚠️ 異常: 北科i學園PLUS (08-20 02:00)", size="sm") with gr.Row(): t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm") t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm") with gr.Row(): t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm") t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm") # 事件綁定 def sync_log_displays(df_all, df_abn): return df_all, df_abn store_df_all.change(fn=sync_log_displays, inputs=[store_df_all, store_df_abn], outputs=[log_display, abnormal_log_display]) demo.load( fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display] ) dashboard_timer.tick( fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display] ) action_dropdown.change(fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display]) time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input) btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output]) def handle_unblock(): from config import FIXED_TEST_EMAIL success = unblock_user(FIXED_TEST_EMAIL) if success: gr.Info(f"已成功解除 {FIXED_TEST_EMAIL} 的封鎖狀態。") return "✅ 狀態已重設為 Active" else: return "❌ 找不到該使用者或已是 Active 狀態" btn_unblock.click(fn=handle_unblock, inputs=[], outputs=[unblock_status]) update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input] login_btn.click(fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input], outputs=update_outputs) logout_btn.click(fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, store_df_all, store_df_abn, abnormal_log_input]) fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] fast_outputs = update_outputs + [acc_input] btn_fast_student.click( fn=lambda ip, c, tm, ct: fast_login("student", ip, c, tm, ct), inputs=fast_inputs, outputs=fast_outputs, api_name=None ) btn_fast_admin.click( fn=lambda ip, c, tm, ct: fast_login("admin", ip, c, tm, ct), inputs=fast_inputs, outputs=fast_outputs, api_name=None ) btn_toggle_role.click(fn=toggle_role, inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input], outputs=[role_tabs, welcome_text, store_df_all, store_df_abn, abnormal_log_input, acc_input]) action_mapping = [ (btn_eval, "click_eval_system"), (btn_pre_select, "click_pre_select_system"), (btn_add_drop, "click_add_drop_system"), (btn_istudy, "click_istudy"), (btn_card, "click_card_loss"), (btn_course, "click_course_system"), (btn_score, "click_score_query"), (btn_leave, "click_leave_system"), (btn_dorm, "click_dorm_system"), (btn_scholarship, "click_scholarship"), (btn_webmail, "click_webmail"), (btn_vdesk, "click_vdesk"), (btn_hack_score, "malicious_sql_injection") ] for btn, action_name in action_mapping: btn.click( fn=lambda ip, c, acc, tm, ct, n=action_name: simulate_click(ip, c, acc, n, tm, ct), inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], outputs=[store_df_all, store_df_abn, abnormal_log_input], api_name=None ) test_mapping = [ (t_pre_sel_norm, "click_pre_select_system", "2026-06-13 11:00:00"), (t_pre_sel_anom, "click_pre_select_system", "2026-10-15 13:00:00"), (t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"), (t_add_drop_norm, "click_add_drop_system", "2026-02-18 14:00:00"), (t_add_drop_anom, "click_add_drop_system", "2026-05-15 12:00:00"), (t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"), (t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00") ] for btn, action_name, test_time_str in test_mapping: btn.click( fn=lambda ip, c, acc, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str), inputs=[mock_ip, mock_cookie, acc_input], outputs=[store_df_all, store_df_abn, abnormal_log_input], api_name=None ) return demo