Spaces:
Sleeping
Sleeping
| """ | |
| components/cards.py | |
| -------------------- | |
| 所有卡片元件的單一來源。 | |
| 公開 API: | |
| create_chapter_card(data) → dmc.Card (章節卡片,原 Chapter_card.py) | |
| render_card_from_node(card_node) → dmc.Card (結構化 node 卡片,新版) | |
| generate_custom_markdown_card(...) → dmc.Card (舊版 Markdown 字串卡片,保留供過渡期使用) | |
| 遷移建議: | |
| - 新頁面請使用 render_card_from_node(接受 JSON 結構化資料) | |
| - generate_custom_markdown_card 標記為 deprecated,待舊頁面遷移完成後刪除 | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from dash import html, dcc | |
| import dash_mantine_components as dmc | |
| from dash_iconify import DashIconify | |
| from components.render_utils import _inline_math, _block_math, _runs_to_children | |
| try: | |
| import dash_latex | |
| DashLatex = dash_latex.DashLatex | |
| except Exception: | |
| DashLatex = None | |
| # --------------------------------------------------------------------------- | |
| # 1. 章節卡片(原 Chapter_card.py) | |
| # --------------------------------------------------------------------------- | |
| def create_chapter_card(data: dict) -> dmc.Card: | |
| """ | |
| 產生章節總覽用的卡片。 | |
| data 欄位: | |
| title str 卡片標題 | |
| badges List[Tuple[str, str]] [(label, color), ...] | |
| description List[str] 說明文字(最多 4 行) | |
| link str 「單元學習」按鈕連結 | |
| test str 「小測驗」按鈕連結 | |
| trick str (選用) 「技巧學習」按鈕連結 | |
| """ | |
| buttons = [ | |
| dmc.Anchor( | |
| href=data["link"], | |
| children=dmc.Button( | |
| "單元學習", variant="light", color="orange", size="sm", radius="xl" | |
| ), | |
| ) | |
| ] | |
| if "trick" in data: | |
| buttons.append( | |
| dmc.Anchor( | |
| href=data["trick"], | |
| children=dmc.Button( | |
| "技巧學習", variant="outline", color="gray", size="sm", radius="xl" | |
| ), | |
| ) | |
| ) | |
| buttons.append( | |
| dmc.Anchor( | |
| href=data["test"], | |
| children=dmc.Button( | |
| "小測驗", variant="filled", color="blue", size="sm", radius="xl" | |
| ), | |
| ) | |
| ) | |
| # 說明文字補足到 4 行,保持卡片高度一致 | |
| desc_items = [ | |
| dmc.Text(line, size="sm", c="dimmed") for line in data["description"] | |
| ] | |
| desc_items += [ | |
| dmc.Text(" ", size="sm") | |
| for _ in range(4 - len(data["description"])) | |
| ] | |
| return dmc.Card( | |
| children=[ | |
| dmc.Text(data["title"], fw=700, size="lg"), | |
| dmc.Group( | |
| gap="xs", | |
| mt="xs", | |
| mb="sm", | |
| children=[ | |
| dmc.Badge(label, color=color, radius="md") | |
| for label, color in data["badges"] | |
| ], | |
| ), | |
| dmc.Stack(gap=0, style={"minHeight": 100}, children=desc_items), | |
| dmc.Flex( | |
| gap="sm", | |
| wrap="wrap", | |
| justify="flex-start", | |
| mt="sm", | |
| children=buttons, | |
| ), | |
| ], | |
| withBorder=True, | |
| shadow="sm", | |
| radius="md", | |
| style={ | |
| "width": { | |
| "base": 280, | |
| "sm": 320, | |
| "md": 350, | |
| "lg": 400, | |
| } | |
| }, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # 2. 結構化 node 卡片(原 generate_common_card.py:render_card_from_node) | |
| # --------------------------------------------------------------------------- | |
| def render_card_from_node(card_node: dict) -> dmc.Card: | |
| """ | |
| 把 Test2ChMd 產生的卡片 JSON node 渲染成 DMC Card。 | |
| card_node 格式: | |
| { | |
| "kind": str, # 卡片種類(例如「定義」「定理」) | |
| "headline": str, # 標題文字 | |
| "body": List[block], # 內容 block 清單 | |
| } | |
| body block 支援的 type: | |
| paragraph / list / math / url / picture / horizontal_rule | |
| """ | |
| kind = (card_node.get("kind") or "卡片").strip() | |
| headline = (card_node.get("headline") or "").strip() | |
| header = dmc.CardSection( | |
| dmc.Group( | |
| [ | |
| dmc.Badge(kind, color="violet", variant="filled"), | |
| dmc.Text(headline, fw=700, style={"whiteSpace": "pre-wrap"}), | |
| ], | |
| gap="sm", | |
| ), | |
| withBorder=True, | |
| inheritPadding=True, | |
| py=6, | |
| style={ | |
| "backgroundColor": dmc.DEFAULT_THEME["colors"]["violet"][7], | |
| "color": "white", | |
| }, | |
| ) | |
| body_children: list[Any] = [] | |
| for b in card_node.get("body", []): | |
| t = (b.get("type") or "").lower() | |
| if t == "paragraph": | |
| body_children.append( | |
| html.Div(_runs_to_children(b.get("runs") or [])) | |
| ) | |
| elif t == "list": | |
| items = [ | |
| dmc.ListItem(_runs_to_children(it.get("runs") or [])) | |
| for it in b.get("content", []) | |
| ] | |
| body_children.append( | |
| dmc.List(items, listStyleType="none", spacing="xs", withPadding=True) | |
| ) | |
| elif t == "math": | |
| body_children.append(_block_math(b.get("latex", ""))) | |
| elif t == "url": | |
| body_children.append( | |
| dcc.Markdown( | |
| f"[{b.get('text')}]({b.get('href')})", link_target="_blank" | |
| ) | |
| ) | |
| elif t == "picture": | |
| body_children.append( | |
| html.Img( | |
| src=b.get("src"), | |
| title=b.get("alt") or "", | |
| style={ | |
| "display": "inline-block", | |
| "margin": "6px", | |
| "maxWidth": "100%", | |
| "height": "auto", | |
| }, | |
| ) | |
| ) | |
| elif t == "horizontal_rule": | |
| body_children.append(dmc.Divider(my=12)) | |
| return dmc.Card( | |
| [header, dmc.Stack(body_children, gap="xs", px="md", py="sm")], | |
| withBorder=True, | |
| shadow="md", | |
| radius="md", | |
| mb=14, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # 3. 舊版 Markdown 字串卡片(deprecated,保留至頁面遷移完成) | |
| # --------------------------------------------------------------------------- | |
| def generate_custom_markdown_card( | |
| title: str, | |
| paragraphs: list[dict], | |
| card_color: str = "#f8f9fa", | |
| title_bg_color: str = dmc.DEFAULT_THEME["colors"]["green"][7], | |
| latex_list: list[str] | None = None, | |
| ) -> dmc.Card: | |
| """ | |
| .. deprecated:: | |
| 請改用 render_card_from_node。 | |
| 此函式將在所有舊頁面遷移完成後刪除。 | |
| paragraphs 格式: | |
| [{"text": str, "align": "left|center|right", "color": str}, ...] | |
| latex_list 格式: | |
| ["latex_str_1", "latex_str_2", ...] (有序清單) | |
| """ | |
| def _render_paragraph(p: dict) -> html.Div: | |
| return html.Div( | |
| dcc.Markdown(p["text"], mathjax=True), | |
| style={ | |
| "textAlign": p.get("align", "left"), | |
| "color": p.get("color", "black"), | |
| "marginBottom": "0.5rem", | |
| }, | |
| ) | |
| content: list[Any] = [_render_paragraph(p) for p in paragraphs] | |
| if latex_list: | |
| # 需要 dash_latex;若未安裝則退回純文字 | |
| list_items = [] | |
| for latex in latex_list: | |
| if DashLatex: | |
| list_items.append( | |
| dmc.ListItem(DashLatex(f"${latex}$"), mt=10) | |
| ) | |
| else: | |
| list_items.append( | |
| dmc.ListItem(dcc.Markdown(f"${latex}$", mathjax=True), mt=10) | |
| ) | |
| content.append( | |
| dmc.List( | |
| children=list_items, | |
| listStyleType="ordered", | |
| spacing="sm", | |
| ) | |
| ) | |
| return dmc.Card( | |
| children=[ | |
| dmc.CardSection( | |
| dmc.Group( | |
| children=[ | |
| dmc.Text( | |
| title, | |
| fw=500, | |
| style={"color": dmc.DEFAULT_THEME["colors"]["gray"][0]}, | |
| ) | |
| ] | |
| ), | |
| withBorder=True, | |
| inheritPadding=True, | |
| py=3, | |
| style={"backgroundColor": title_bg_color}, | |
| ), | |
| html.Div(children=content, style={"padding": "10px"}), | |
| ], | |
| withBorder=True, | |
| shadow="md", | |
| radius="md", | |
| style={"backgroundColor": card_color}, | |
| ) | |