""" app.py ------ Dash app 進入點。 職責(僅此而已): 1. 建立 Dash app 實例 2. 組裝 AppShell layout 3. 掛載全域 callback(navbar、主題、MathJax) 4. 啟動 dev server Build pipeline(MD → JSON)已移至 build.py。 - 若希望每次啟動都重建:取消下方 build_if_needed 的 force=True 呼叫 - 若只想手動重建:直接執行 python build.py --force """ import dash from dash import Dash, html, dcc from dash import Input, Output, clientside_callback import dash_mantine_components as dmc from pathlib import Path from layout.shell.navbar import generate_navbar from layout.shell.header import generate_header from layout.shell.main import generate_main from layout.shell.footer import generate_footer from layout.shell.callbacks import generate_toggle_navbar, switch_light_dark # 確保頁面 section 切換 callback 在啟動時就被註冊 import layout.page.page_callbacks # noqa: F401 # 學生登入元件 from student_login import create_student_store, create_login_modal, create_student_badge # Firebase quiz hooks(答題記錄) from firebase_quiz_hooks import create_dummy_stores import firebase_quiz_hooks # noqa: F401 ← 掛載 callback # Session 狀態保存(重整後恢復 section、答題、解法)由 assets/session_state.js 處理 from session_state import create_session_stores # Build pipeline:只在啟動時呼叫,不影響 app 本體邏輯 from build import build_if_needed BASE_DIR = Path(__file__).resolve().parent CONFIG_PATH = BASE_DIR / "paths.json" import os if not os.getenv("SPACE_ID"): # 不在 HF 上才 build build_if_needed(CONFIG_PATH, force=False) # --------------------------------------------------------------------------- # App 實例 # --------------------------------------------------------------------------- app = Dash( __name__, use_pages=True, external_stylesheets=dmc.styles.ALL, suppress_callback_exceptions=True, ) # --------------------------------------------------------------------------- # Callbacks # --------------------------------------------------------------------------- generate_toggle_navbar() switch_light_dark() # --------------------------------------------------------------------------- # Layout 元件 # --------------------------------------------------------------------------- header = generate_header() navbar = generate_navbar() main = generate_main() footer = generate_footer() layout = dmc.AppShell( id="appshell", children=[ dcc.Store(id="noop-store"), html.Div(id="theme-dummy", style={"display": "none"}), # ── 學生登入相關 ── create_student_store(), # localStorage 學號 Store create_login_modal(), # 學號輸入 Modal *create_dummy_stores(), # Firebase 虛擬 Store *create_session_stores(), # Session 狀態 Store header, navbar, main, footer, ], header={"height": "4rem"}, navbar={ "width": {"xs": 200, "sm": 250, "md": 300, "lg": 300}, "breakpoint": "sm", "collapsed": {"mobile": True, "desktop": False}, }, footer={"height": "2rem"}, padding="lg", ) app.layout = dmc.MantineProvider( id="mantine-provider", children=layout, defaultColorScheme="light", ) # --------------------------------------------------------------------------- # MathJax:每次換頁都重新 typeset # --------------------------------------------------------------------------- clientside_callback( """ function(pathname) { if (window.MathJax) { window.MathJax.typeset(); } return ""; } """, Output("noop-store", "data"), Input("_pages_location", "pathname"), ) # --------------------------------------------------------------------------- # 啟動 # --------------------------------------------------------------------------- server = app.server # 給 gunicorn 用:gunicorn app:server if __name__ == "__main__": # app.run(debug=True) # 部署時改用: app.run(host="0.0.0.0", port=7860, debug=False)