Spaces:
Running
步驟列收斂 8 步 → 6 步 + 導覽列與頁殼視覺調整
Browse files步驟列:
- _STEPS 改為 文本上傳/格式驗證/語料說明/指標計算/統計分析/報告產出
- 移除的 4 步都不帶資訊:情境選擇已搬到首頁、上傳引導與文本上傳在同一個
generator 內連續完成、報告撰寫與下載成果本來就寫在同一行一起標記
- 新增「語料說明」步驟,修正填表期間步驟列誤亮「指標計算 進行中」
- failed_step 5,6 → 4,5;_COMPUTING_STEP_LABELS key 5,6,7 → 4,5,6
- 格式驗證失敗退回重傳時,一併把該步從 done_steps 移除(原本只設
current_step,_build_header_html 判斷順序 done > current,步驟列會停在
「✓ 文本上傳」與畫面要求補傳自相矛盾)
- 全部完成後步驟列整條隱藏,只留產品名
視覺:
- 頂部列扁平化(去框線/漸層/圓角,只留 1px 下邊框,產品名與步驟列改上下兩行)
- .upload-zone/.metadata-zone 去框線/陰影/底色
- .nav-col/.chat-col 各加 border-right
- 導覽列改 Bootstrap 5.3 nav-underline 垂直版(選取項左緣 2px 主色線)
- 統計比較分析 4 個子頁改一般項目+各配圖示、左緣齊平,手風琴預設展開
- 主項目 .result-nav-btn 改用 #result_nav_col 祖先提高 specificity,蓋掉
Gradio 的 button.svelte-xxx.secondary 預設框線(子項目先前已修,主項目漏了)
- 新增 table-cells/scale/academic-cap/squares-2x2 四個 Heroicons 圖示
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app_agent.py +205 -123
- assets/icons/academic-cap.svg +1 -0
- assets/icons/scale.svg +1 -0
- assets/icons/squares-2x2.svg +1 -0
- assets/icons/table-cells.svg +1 -0
|
@@ -1379,15 +1379,20 @@ def make_initial_state(session_id: str) -> dict:
|
|
| 1379 |
|
| 1380 |
|
| 1381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
_STEPS = [
|
| 1383 |
-
(1, "
|
| 1384 |
-
(2, "
|
| 1385 |
-
(3, "
|
| 1386 |
-
(4, "
|
| 1387 |
-
(5, "
|
| 1388 |
-
(6, "
|
| 1389 |
-
(7, "報告撰寫"),
|
| 1390 |
-
(8, "下載成果"),
|
| 1391 |
]
|
| 1392 |
|
| 1393 |
|
|
@@ -1405,6 +1410,17 @@ def _build_header_html(done_steps: list = None, current_step: int = None,
|
|
| 1405 |
使用者容易誤以為系統還在運算而手滑刷新頁面)
|
| 1406 |
"""
|
| 1407 |
done_steps = done_steps or []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1408 |
pills = []
|
| 1409 |
for num, name in _STEPS:
|
| 1410 |
if num == failed_step:
|
|
@@ -1446,8 +1462,9 @@ def handle_scenario_select(user_input: str, state: dict):
|
|
| 1446 |
sc_num = m.group()
|
| 1447 |
sc_info = SCENARIOS[sc_num]
|
| 1448 |
state["scenario"] = sc_num
|
| 1449 |
-
|
| 1450 |
-
state["
|
|
|
|
| 1451 |
|
| 1452 |
# 固定文字,不經 LLM:確保每次措辭一致,並直接引導使用者去看上傳卡片裡的
|
| 1453 |
# 格式要求,不需要另外由工作程序 Agent 生成一則重複格式細節的引導訊息
|
|
@@ -1598,7 +1615,7 @@ def run_computation_pipeline_streaming(state: dict):
|
|
| 1598 |
yield f" [OK] 指標計算完成({len(idx_df)} 個文本)", False, state
|
| 1599 |
except Exception as e:
|
| 1600 |
state["state"] = "COMPLETE"
|
| 1601 |
-
state["failed_step"] =
|
| 1602 |
yield f"[ERROR] 指標計算失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
|
| 1603 |
return
|
| 1604 |
|
|
@@ -1623,7 +1640,7 @@ def run_computation_pipeline_streaming(state: dict):
|
|
| 1623 |
except Exception as e:
|
| 1624 |
state["state"] = "COMPLETE"
|
| 1625 |
state["output_files"] = results_paths
|
| 1626 |
-
state["failed_step"] =
|
| 1627 |
yield f"[ERROR] 統計分析失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
|
| 1628 |
return
|
| 1629 |
|
|
@@ -1941,18 +1958,22 @@ footer {
|
|
| 1941 |
.md { display: block !important; }
|
| 1942 |
|
| 1943 |
/* ── 2. HEADER(卡片化,2026-07-23 精簡成單行)───────────────────── */
|
|
|
|
|
|
|
|
|
|
| 1944 |
.app-header {
|
| 1945 |
display: flex;
|
| 1946 |
-
|
| 1947 |
-
|
|
|
|
| 1948 |
gap: .9rem;
|
| 1949 |
text-align: left;
|
| 1950 |
-
padding: .
|
| 1951 |
position: relative;
|
| 1952 |
margin: 0 0 1.25rem;
|
| 1953 |
-
border:
|
| 1954 |
-
border-
|
| 1955 |
-
background:
|
| 1956 |
}
|
| 1957 |
/* 注意:這三個選擇器的漸層文字底色(background-image: linear-gradient(...var()...))
|
| 1958 |
改在 launch(head=...) 注入的 <style> 裡設定(見 _DARK_MODE_HEAD_CSS)——
|
|
@@ -2009,12 +2030,13 @@ footer {
|
|
| 2009 |
color: var(--c-accent2-strong);
|
| 2010 |
border: 1px solid rgba(var(--c-accent2-rgb),.30);
|
| 2011 |
}
|
| 2012 |
-
/* step indicators:2026-07-23 header 改
|
| 2013 |
-
|
| 2014 |
-
|
|
|
|
| 2015 |
.app-header .steps {
|
| 2016 |
display: flex; justify-content: flex-start; flex-wrap: wrap;
|
| 2017 |
-
gap: 4px; margin: 0;
|
| 2018 |
}
|
| 2019 |
.step-pill {
|
| 2020 |
display: inline-flex; align-items: center; gap: 6px;
|
|
@@ -2382,6 +2404,14 @@ div[role="log"][aria-label="chatbot conversation"],
|
|
| 2382 |
——雖然不會再拖走頂部列/footer了(那兩個已經在 .contain 外面),但
|
| 2383 |
chat-col/canvas-col 會跟著被迫一起捲動,體驗還是不對。讓三欄各自
|
| 2384 |
獨立捲動,互不影響。 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2385 |
.nav-col {
|
| 2386 |
flex: 0 0 220px !important;
|
| 2387 |
max-width: 220px !important;
|
|
@@ -2391,6 +2421,9 @@ div[role="log"][aria-label="chatbot conversation"],
|
|
| 2391 |
height: 100% !important;
|
| 2392 |
overflow-y: auto !important;
|
| 2393 |
overflow-x: hidden !important;
|
|
|
|
|
|
|
|
|
|
| 2394 |
}
|
| 2395 |
/* 真因排查記錄:Gradio 的 .column 預設會帶 flex-wrap:wrap。平常沒設高度時
|
| 2396 |
不會有事,但這裡額外設了固定 height 讓內容用捲動取代撐高,一旦內容總高
|
|
@@ -2406,6 +2439,7 @@ div[role="log"][aria-label="chatbot conversation"],
|
|
| 2406 |
height: 100% !important;
|
| 2407 |
overflow: hidden !important;
|
| 2408 |
min-width: 0 !important;
|
|
|
|
| 2409 |
}
|
| 2410 |
/* 對話視窗:輸入框高度比例 5:1(千卉要求,2026-07-23)。用 flex-grow
|
| 2411 |
依 chat-col 當下的可用高度分配,不寫死 px,縮放視窗時比例仍會維持;
|
|
@@ -2681,8 +2715,13 @@ img.button-icon { opacity: .82; }
|
|
| 2681 |
.nav-col.collapsed .result-nav-accordion,
|
| 2682 |
.nav-col.collapsed .result-nav-divider,
|
| 2683 |
.nav-col.collapsed .reset-confirm { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2684 |
.nav-col.collapsed .reset-btn,
|
| 2685 |
-
.nav-col.collapsed .result-nav-btn {
|
| 2686 |
justify-content: center !important;
|
| 2687 |
padding: .6rem !important;
|
| 2688 |
font-size: 0 !important;
|
|
@@ -2695,8 +2734,18 @@ img.button-icon { opacity: .82; }
|
|
| 2695 |
/* 統計比較分析 Accordion 沒有 icon= 參數,改用 CSS ::before 掛圖示
|
| 2696 |
(data-URI,同一份 chart-bar 線稿,色值同步 assets/icons/*.svg 的
|
| 2697 |
#475569/深色模式同樣靠 filter 反相,寫法比照上面 img.button-icon)。 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2698 |
#nav_stat_accordion .label-wrap {
|
| 2699 |
-
display: flex !important; align-items: center !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2700 |
}
|
| 2701 |
#nav_stat_accordion .label-wrap::before {
|
| 2702 |
content: "";
|
|
@@ -2763,14 +2812,18 @@ img.button-icon { opacity: .82; }
|
|
| 2763 |
/* 注意:max-width + margin auto 是舊版單欄置中版面留下的樣式,現在
|
| 2764 |
upload-zone 是右側 action-col 裡的元件,要跟同一欄的 scenario-buttons
|
| 2765 |
同寬��撐滿欄寬),不能再置中收窄 */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2766 |
.upload-zone {
|
| 2767 |
-
border:
|
| 2768 |
border-radius: 16px !important;
|
| 2769 |
-
background:
|
| 2770 |
padding: 1.4rem 1.5rem !important;
|
| 2771 |
margin: .6rem 0 0 !important;
|
| 2772 |
width: 100% !important;
|
| 2773 |
-
box-shadow:
|
| 2774 |
animation: popupIn .25s cubic-bezier(.2,.8,.3,1) !important;
|
| 2775 |
}
|
| 2776 |
@keyframes popupIn {
|
|
@@ -2786,9 +2839,8 @@ img.button-icon { opacity: .82; }
|
|
| 2786 |
margin-bottom: .9rem;
|
| 2787 |
letter-spacing: .02em;
|
| 2788 |
}
|
| 2789 |
-
.upload-zone:hover
|
| 2790 |
-
|
| 2791 |
-
}
|
| 2792 |
/* 對話輸入框(Phase 8:不再負責上傳,改回純展示用的常駐輸入框):
|
| 2793 |
刻意跟上面「醒目卡片」風格的 .upload-zone 拉開視覺重量差異,
|
| 2794 |
不用強調色框線/陰影/彈出動畫,也不掛 ::before 的標題文字,
|
|
@@ -2861,13 +2913,26 @@ img.button-icon { opacity: .82; }
|
|
| 2861 |
標籤(暫不用圖示,等下一輪視覺打磨補 Heroicons SVG),維持跟
|
| 2862 |
scenario-buttons 一致的「貼齊左邊、無強調色底」低視覺重量風格,
|
| 2863 |
避免搶過主要操作按鈕的注意力 */
|
|
|
|
|
|
|
|
|
|
| 2864 |
.result-nav {
|
| 2865 |
margin-top: 1rem !important;
|
| 2866 |
padding-top: 1rem !important;
|
| 2867 |
border-top: 1px solid var(--c-border) !important;
|
| 2868 |
gap: .3rem !important;
|
| 2869 |
}
|
| 2870 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2871 |
width: 100% !important;
|
| 2872 |
justify-content: flex-start !important;
|
| 2873 |
text-align: left !important;
|
|
@@ -2875,92 +2940,87 @@ img.button-icon { opacity: .82; }
|
|
| 2875 |
background: transparent !important;
|
| 2876 |
box-shadow: none !important;
|
| 2877 |
}
|
| 2878 |
-
.result-nav-btn {
|
| 2879 |
-
padding: .
|
| 2880 |
font-size: .88rem !important;
|
| 2881 |
-
font-weight: 600 !important;
|
| 2882 |
-
border-radius: 8px !important;
|
| 2883 |
-
color: var(--c-text-soft) !important;
|
| 2884 |
-
}
|
| 2885 |
-
.result-nav-btn:hover { background: rgba(var(--c-accent-rgb),.08) !important; color: var(--c-text) !important; }
|
| 2886 |
-
/* 2026-07-23:子項目縮排改參考 Bootstrap 官方 Sidebars 範例的
|
| 2887 |
-
.btn-toggle-nav a 寫法(getbootstrap.com/docs/5.0/examples/sidebars/
|
| 2888 |
-
sidebars.css)——原本只用 padding-left 撐出縮排,視覺上子項目還是
|
| 2889 |
-
「跟父項同一個滿版矩形、只是文字往右挪」,看不太出來是巢狀關係。
|
| 2890 |
-
Bootstrap 的做法是用 margin-left 真的把整個元素往右推、露出左邊的
|
| 2891 |
-
縮排空隙,寬度也跟著縮小(不是滿版),加上一致的 margin-top 取代
|
| 2892 |
-
padding 撐出的行距,這樣視覺上才會讀出「這是掛在上一層底下的子選單」
|
| 2893 |
-
而不是另一個平級按鈕。數值比例沿用 Bootstrap 原案(margin-left
|
| 2894 |
-
1.25rem、margin-top .125rem、padding .1875rem .5rem),只在這個
|
| 2895 |
-
專案 --font-size-base 是 0.875rem(比 Bootstrap 預設 1rem 小)的
|
| 2896 |
-
前提下等比例微調過,色彩/字體維持這個專案原本的 token 不動。 */
|
| 2897 |
-
.result-nav-subbtn {
|
| 2898 |
-
width: calc(100% - 1.1rem) !important;
|
| 2899 |
-
margin-left: 1.1rem !important;
|
| 2900 |
-
margin-top: .15rem !important;
|
| 2901 |
-
padding: .3rem .6rem !important;
|
| 2902 |
-
font-size: .8rem !important;
|
| 2903 |
font-weight: 500 !important;
|
| 2904 |
-
border-radius:
|
| 2905 |
-
|
|
|
|
|
|
|
| 2906 |
}
|
| 2907 |
-
|
| 2908 |
-
scoped 選擇器實測是 button.svelte-xxx.secondary 這種 type+class 組合,
|
| 2909 |
-
specificity 比純 class 選擇器 .result-nav-subbtn 高,即使兩邊都加
|
| 2910 |
-
!important 也會贏)冒出來蓋掉上面共用規則的 border:none。用
|
| 2911 |
-
#result_nav_col 這個 id 當祖先提高 specificity 才蓋得過去,純加
|
| 2912 |
-
!important 不夠。 */
|
| 2913 |
-
#result_nav_col .result-nav-subbtn {
|
| 2914 |
-
border: none !important;
|
| 2915 |
background: transparent !important;
|
| 2916 |
-
|
| 2917 |
}
|
| 2918 |
-
.result-nav-subbtn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2919 |
.result-nav-accordion {
|
| 2920 |
margin: 0 !important;
|
| 2921 |
border: none !important;
|
| 2922 |
background: transparent !important;
|
| 2923 |
box-shadow: none !important;
|
|
|
|
|
|
|
| 2924 |
}
|
|
|
|
|
|
|
| 2925 |
.result-nav-accordion .label-wrap {
|
| 2926 |
-
padding: .
|
| 2927 |
font-size: .88rem !important;
|
| 2928 |
-
font-weight:
|
|
|
|
| 2929 |
color: var(--c-text-soft) !important;
|
| 2930 |
}
|
| 2931 |
/* 2026-07-23:頁面切換模型——目前使用者所在的那一頁在導覽列高亮,
|
| 2932 |
applyActivePage() JS 幫對應按鈕加/移除這個 class。視覺先求「看得出
|
| 2933 |
選中誰」的基本可用,細部樣式(跟 Bootstrap 借來的分組/層級規範)留給
|
| 2934 |
下一輪 nav-col 視覺重做一起處理,這裡不重複做兩次。 */
|
| 2935 |
-
.result-nav-btn.active {
|
| 2936 |
-
background:
|
|
|
|
| 2937 |
color: var(--c-accent-strong) !important;
|
| 2938 |
font-weight: 700 !important;
|
| 2939 |
}
|
| 2940 |
.result-nav-accordion.active .label-wrap {
|
|
|
|
| 2941 |
color: var(--c-accent-strong) !important;
|
| 2942 |
font-weight: 700 !important;
|
| 2943 |
}
|
| 2944 |
-
/* 技術執行紀錄:低優先度診斷工具,跟上面
|
| 2945 |
-
|
| 2946 |
.result-nav-divider {
|
| 2947 |
height: 1px;
|
| 2948 |
background: var(--c-border);
|
| 2949 |
margin: .5rem .3rem;
|
| 2950 |
}
|
| 2951 |
-
|
|
|
|
|
|
|
|
|
|
| 2952 |
font-size: .82rem !important;
|
| 2953 |
font-weight: 500 !important;
|
| 2954 |
color: var(--c-text-muted) !important;
|
| 2955 |
}
|
| 2956 |
-
/* 語料 Metadata 說明表單卡片
|
|
|
|
| 2957 |
.metadata-zone {
|
| 2958 |
-
border:
|
| 2959 |
border-radius: 16px !important;
|
| 2960 |
-
background:
|
| 2961 |
padding: 1.4rem 1.6rem !important;
|
| 2962 |
margin: 1rem 0 !important;
|
| 2963 |
-
box-shadow:
|
| 2964 |
animation: popupIn .25s cubic-bezier(.2,.8,.3,1) !important;
|
| 2965 |
}
|
| 2966 |
.metadata-zone .gr-box, .metadata-zone .form { background: transparent !important; }
|
|
@@ -3889,16 +3949,29 @@ def create_app():
|
|
| 3889 |
"語言指標分析", icon=str(BASE_DIR / "assets" / "icons" / "trending-up.svg"),
|
| 3890 |
elem_id="nav_idx_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3891 |
)
|
| 3892 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3893 |
elem_classes=["result-nav-accordion"]):
|
| 3894 |
-
|
| 3895 |
-
|
| 3896 |
-
|
| 3897 |
-
|
| 3898 |
-
|
| 3899 |
-
|
| 3900 |
-
|
| 3901 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3902 |
nav_report_btn = gr.Button(
|
| 3903 |
"LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
|
| 3904 |
elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
|
@@ -4349,13 +4422,14 @@ def create_app():
|
|
| 4349 |
)
|
| 4350 |
|
| 4351 |
# ── Phase 8-D:Canvas 運算中提示(粗粒度階段文字+轉圈動畫)─────
|
| 4352 |
-
#
|
| 4353 |
-
#
|
| 4354 |
-
# 顯示、不用另外
|
|
|
|
| 4355 |
_COMPUTING_STEP_LABELS = {
|
| 4356 |
-
|
| 4357 |
-
|
| 4358 |
-
|
| 4359 |
}
|
| 4360 |
|
| 4361 |
def _build_computing_html(state):
|
|
@@ -4399,22 +4473,19 @@ def create_app():
|
|
| 4399 |
gr.update(visible=True))
|
| 4400 |
|
| 4401 |
reply, state = handle_scenario_select(sc_num, state)
|
| 4402 |
-
log_history = _log_append(log_history, "✅
|
| 4403 |
chat_history = chat_history + [_assistant_msg(reply)]
|
| 4404 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4405 |
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4406 |
|
| 4407 |
-
# STEP
|
| 4408 |
-
# LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘
|
| 4409 |
-
|
| 4410 |
-
|
| 4411 |
-
|
| 4412 |
-
|
| 4413 |
-
|
| 4414 |
-
state["done_steps"] = [1, 2]
|
| 4415 |
-
state["current_step"] = 3
|
| 4416 |
state["state"] = "UPLOAD_WAIT"
|
| 4417 |
-
log_history = _log_append(log_history, "
|
| 4418 |
# 情境已選定,按鈕收起;對話視窗自此回歸純輸出角色。
|
| 4419 |
# 格式要求直接寫進上傳卡片本身(upload_hint_md),使用者不用往上
|
| 4420 |
# 捲回對話視窗找 AI 剛剛說過的規則——卡片本身就要自己講清楚。
|
|
@@ -4487,34 +4558,39 @@ def create_app():
|
|
| 4487 |
yield chat_history, log_history, state, gr.update(visible=True), gr.update()
|
| 4488 |
return
|
| 4489 |
|
| 4490 |
-
# STEP
|
| 4491 |
-
state["current_step"] =
|
| 4492 |
-
log_history = _log_append(log_history, "⏳ STEP
|
| 4493 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4494 |
|
| 4495 |
recv_msg, state = handle_uploaded_files(files, state)
|
| 4496 |
chat_history = chat_history + [_assistant_msg(recv_msg)]
|
| 4497 |
-
state["done_steps"] = list(set(state.get("done_steps", []) + [
|
| 4498 |
-
state["current_step"] =
|
| 4499 |
-
log_history = _log_append(log_history, f"✅ STEP
|
| 4500 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4501 |
|
| 4502 |
-
# STEP
|
| 4503 |
-
log_history = _log_append(log_history, "⏳ STEP
|
| 4504 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4505 |
|
| 4506 |
check_msg, state, has_errors = run_format_check(state)
|
| 4507 |
chat_history = chat_history + [_assistant_msg(check_msg)]
|
| 4508 |
|
| 4509 |
if has_errors:
|
| 4510 |
-
log_history = _log_append(log_history, "⚠️ STEP
|
| 4511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4512 |
yield chat_history, log_history, state, gr.update(visible=True), _todo_update(state)
|
| 4513 |
return
|
| 4514 |
|
| 4515 |
-
state["done_steps"] = list(set(state.get("done_steps", []) + [
|
| 4516 |
-
state["current_step"] =
|
| 4517 |
-
log_history = _log_append(log_history, "✅ STEP
|
| 4518 |
# 格式通過後:顯示 metadata 說明表單,等使用者填寫後再開始分析
|
| 4519 |
state["state"] = "METADATA_INPUT"
|
| 4520 |
meta_hint = "請填寫語料 Metadata 說明(原文資訊、群組層與個體層),完成後點「確認並開始分析」。"
|
|
@@ -4573,20 +4649,26 @@ def create_app():
|
|
| 4573 |
# 2026-07-23:技術執行紀錄轉正成獨立頁面後,可見性只在情境選定
|
| 4574 |
# (on_scenario_pick)與重置(on_reset_confirm)時切換一次,
|
| 4575 |
# 這裡不用再逐步更新開合狀態/摘要標籤,改回傳 gr.update() 不動。
|
| 4576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4577 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4578 |
gr.update(), _computing_update(state))
|
| 4579 |
for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
|
| 4580 |
if "指標計算完成" in prog_msg:
|
| 4581 |
-
state["done_steps"] = list(set(state.get("done_steps", []) + [
|
| 4582 |
elif "步驟 2/3" in prog_msg or "統計分析" in prog_msg and "完成" not in prog_msg:
|
| 4583 |
-
state["current_step"] =
|
| 4584 |
elif "統計分析完成" in prog_msg or ("統計" in prog_msg and "完成" in prog_msg):
|
| 4585 |
-
state["done_steps"] = list(set(state.get("done_steps", []) + [
|
| 4586 |
elif "步驟 3/3" in prog_msg or "報告" in prog_msg and "完成" not in prog_msg:
|
| 4587 |
-
state["current_step"] =
|
| 4588 |
elif is_final:
|
| 4589 |
-
|
|
|
|
|
|
|
| 4590 |
log_history = _log_append(log_history, prog_msg)
|
| 4591 |
if is_final:
|
| 4592 |
chat_history = chat_history + [_assistant_msg(prog_msg)]
|
|
|
|
| 1379 |
|
| 1380 |
|
| 1381 |
|
| 1382 |
+
# 2026-07-23 步驟列收斂:原本 8 步裡有 4 步不帶資訊——①情境選擇在 Phase 8-A
|
| 1383 |
+
# 搬去首頁後,使用者在工作區看到的是別的畫面做完的事;①② 在同一個 generator
|
| 1384 |
+
# 內連續完成,中間沒有任何使用者動作;⑦⑧ 本來就寫死在同一行一起標記(下載
|
| 1385 |
+
# 成果不是獨立運算階段)。同時補上原本完全沒有步驟的「語料說明」表單——先前
|
| 1386 |
+
# 格式驗證通過就直接把 current_step 設成「指標計算」,導致使用者在填表的整段
|
| 1387 |
+
# 期間,步驟列都亮著「指標計算 進行中」的呼吸動畫,畫面在說電腦正在算,實際
|
| 1388 |
+
# 上是在等人填表。收斂後每一步都對應一件使用者感知得到的事。
|
| 1389 |
_STEPS = [
|
| 1390 |
+
(1, "文本上傳"),
|
| 1391 |
+
(2, "格式驗證"),
|
| 1392 |
+
(3, "語料說明"),
|
| 1393 |
+
(4, "指標計算"),
|
| 1394 |
+
(5, "統計分析"),
|
| 1395 |
+
(6, "報告產出"),
|
|
|
|
|
|
|
| 1396 |
]
|
| 1397 |
|
| 1398 |
|
|
|
|
| 1410 |
使用者容易誤以為系統還在運算而手滑刷新頁面)
|
| 1411 |
"""
|
| 1412 |
done_steps = done_steps or []
|
| 1413 |
+
|
| 1414 |
+
# 全部完成後整條步驟列收起來,把畫面高度讓給結果內容——此時瀏覽結果的
|
| 1415 |
+
# 角色已經交接給左側結果導覽(空間型),時間軸型的步驟列講完它要講的事
|
| 1416 |
+
# 就沒有存在必要。失敗時例外:步驟列要留著標示卡在哪一步。
|
| 1417 |
+
if failed_step is None and all(num in done_steps for num, _ in _STEPS):
|
| 1418 |
+
return (
|
| 1419 |
+
'<div class="app-header">'
|
| 1420 |
+
' <p class="title-line">譯彩紛呈 <span class="en">SpectraEngine</span></p>'
|
| 1421 |
+
'</div>'
|
| 1422 |
+
)
|
| 1423 |
+
|
| 1424 |
pills = []
|
| 1425 |
for num, name in _STEPS:
|
| 1426 |
if num == failed_step:
|
|
|
|
| 1462 |
sc_num = m.group()
|
| 1463 |
sc_info = SCENARIOS[sc_num]
|
| 1464 |
state["scenario"] = sc_num
|
| 1465 |
+
# 情境選擇本身已不是步驟列的一步(在首頁完成),選完直接站上「文本上傳」
|
| 1466 |
+
state["done_steps"] = []
|
| 1467 |
+
state["current_step"] = 1
|
| 1468 |
|
| 1469 |
# 固定文字,不經 LLM:確保每次措辭一致,並直接引導使用者去看上傳卡片裡的
|
| 1470 |
# 格式要求,不需要另外由工作程序 Agent 生成一則重複格式細節的引導訊息
|
|
|
|
| 1615 |
yield f" [OK] 指標計算完成({len(idx_df)} 個文本)", False, state
|
| 1616 |
except Exception as e:
|
| 1617 |
state["state"] = "COMPLETE"
|
| 1618 |
+
state["failed_step"] = 4
|
| 1619 |
yield f"[ERROR] 指標計算失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
|
| 1620 |
return
|
| 1621 |
|
|
|
|
| 1640 |
except Exception as e:
|
| 1641 |
state["state"] = "COMPLETE"
|
| 1642 |
state["output_files"] = results_paths
|
| 1643 |
+
state["failed_step"] = 5
|
| 1644 |
yield f"[ERROR] 統計分析失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
|
| 1645 |
return
|
| 1646 |
|
|
|
|
| 1958 |
.md { display: block !important; }
|
| 1959 |
|
| 1960 |
/* ── 2. HEADER(卡片化,2026-07-23 精簡成單行)───────────────────── */
|
| 1961 |
+
/* 2026-07-23 千卉實機回饋:原本整塊是「有外框+漸層底色+圓角」的卡片,
|
| 1962 |
+
看起來像一個獨立區塊漂在畫面上方。改成完全扁平:拿掉外框、底色、圓角,
|
| 1963 |
+
只留一條下邊框當工作區的水平分界;產品名與步驟列改上下兩行。 */
|
| 1964 |
.app-header {
|
| 1965 |
display: flex;
|
| 1966 |
+
flex-direction: column;
|
| 1967 |
+
align-items: flex-start;
|
| 1968 |
+
flex-wrap: nowrap;
|
| 1969 |
gap: .9rem;
|
| 1970 |
text-align: left;
|
| 1971 |
+
padding: 1.5rem 1.25rem 1.1rem;
|
| 1972 |
position: relative;
|
| 1973 |
margin: 0 0 1.25rem;
|
| 1974 |
+
border: none;
|
| 1975 |
+
border-bottom: 1px solid var(--c-border);
|
| 1976 |
+
background: none;
|
| 1977 |
}
|
| 1978 |
/* 注意:這三個選擇器的漸層文字底色(background-image: linear-gradient(...var()...))
|
| 1979 |
改在 launch(head=...) 注入的 <style> 裡設定(見 _DARK_MODE_HEAD_CSS)——
|
|
|
|
| 2030 |
color: var(--c-accent2-strong);
|
| 2031 |
border: 1px solid rgba(var(--c-accent2-rgb),.30);
|
| 2032 |
}
|
| 2033 |
+
/* step indicators:2026-07-23 二度調整——header 改成上下兩行(產品名一行、
|
| 2034 |
+
步驟列一行),所以這裡不再需要 flex:1 去跟標題搶同一行的水平空間,改成
|
| 2035 |
+
自己佔滿整行寬度。兩行之間的間距一樣交給 .app-header 的 gap,不另設
|
| 2036 |
+
margin-top,避免兩邊疊加。 */
|
| 2037 |
.app-header .steps {
|
| 2038 |
display: flex; justify-content: flex-start; flex-wrap: wrap;
|
| 2039 |
+
gap: 4px; margin: 0; width: 100%; min-width: 0;
|
| 2040 |
}
|
| 2041 |
.step-pill {
|
| 2042 |
display: inline-flex; align-items: center; gap: 6px;
|
|
|
|
| 2404 |
——雖然不會再拖走頂部列/footer了(那兩個已經在 .contain 外面),但
|
| 2405 |
chat-col/canvas-col 會跟著被迫一起捲動,體驗還是不對。讓三欄各自
|
| 2406 |
獨立捲動,互不影響。 */
|
| 2407 |
+
/* border-right:導覽欄與對話欄各在自己的右緣掛一條 1px 垂直分隔線
|
| 2408 |
+
(千卉 2026-07-23 指定),三欄之間才有明確的邊界。畫在欄位自己的
|
| 2409 |
+
邊框上、不是插入額外的分隔元件,這樣它會自動跟著欄高走完整條。
|
| 2410 |
+
|
| 2411 |
+
padding 左右 8px(千卉 2026-07-23):欄裡的東西都不要貼著兩側邊緣。
|
| 2412 |
+
留白設在欄位本身而不是逐項設定,「開始新分析」按鈕、收合鈕、結果導覽
|
| 2413 |
+
項目才會全部吃到同一份,不會有人漏掉——先前只設在 .result-nav 上時,
|
| 2414 |
+
「開始新分析」因為是 .nav-col 的直接子項,就沒被涵蓋到。 */
|
| 2415 |
.nav-col {
|
| 2416 |
flex: 0 0 220px !important;
|
| 2417 |
max-width: 220px !important;
|
|
|
|
| 2421 |
height: 100% !important;
|
| 2422 |
overflow-y: auto !important;
|
| 2423 |
overflow-x: hidden !important;
|
| 2424 |
+
border-right: 1px solid var(--c-border) !important;
|
| 2425 |
+
padding-left: 8px !important;
|
| 2426 |
+
padding-right: 8px !important;
|
| 2427 |
}
|
| 2428 |
/* 真因排查記錄:Gradio 的 .column 預設會帶 flex-wrap:wrap。平常沒設高度時
|
| 2429 |
不會有事,但這裡額外設了固定 height 讓內容用捲動取代撐高,一旦內容總高
|
|
|
|
| 2439 |
height: 100% !important;
|
| 2440 |
overflow: hidden !important;
|
| 2441 |
min-width: 0 !important;
|
| 2442 |
+
border-right: 1px solid var(--c-border) !important;
|
| 2443 |
}
|
| 2444 |
/* 對話視窗:輸入框高度比例 5:1(千卉要求,2026-07-23)。用 flex-grow
|
| 2445 |
依 chat-col 當下的可用高度分配,不寫死 px,縮放視窗時比例仍會維持;
|
|
|
|
| 2715 |
.nav-col.collapsed .result-nav-accordion,
|
| 2716 |
.nav-col.collapsed .result-nav-divider,
|
| 2717 |
.nav-col.collapsed .reset-confirm { display: none !important; }
|
| 2718 |
+
/* 選擇器裡的 #result_nav_col 是為了贏過下方 nav-underline 樣式的
|
| 2719 |
+
`#result_nav_col .result-nav-btn`(1 個 id + 1 個 class = 1,1,0)。
|
| 2720 |
+
兩邊都掛 !important 時由 specificity 決勝,純 class 的
|
| 2721 |
+
`.nav-col.collapsed .result-nav-btn`(0,3,0)會輸,收合後的置中
|
| 2722 |
+
與 padding 就不會生效。加上 id 祖先後是 1,3,0,穩定勝出。 */
|
| 2723 |
.nav-col.collapsed .reset-btn,
|
| 2724 |
+
.nav-col.collapsed #result_nav_col .result-nav-btn {
|
| 2725 |
justify-content: center !important;
|
| 2726 |
padding: .6rem !important;
|
| 2727 |
font-size: 0 !important;
|
|
|
|
| 2734 |
/* 統計比較分析 Accordion 沒有 icon= 參數,改用 CSS ::before 掛圖示
|
| 2735 |
(data-URI,同一份 chart-bar 線稿,色值同步 assets/icons/*.svg 的
|
| 2736 |
#475569/深色模式同樣靠 filter 反相,寫法比照上面 img.button-icon)。 */
|
| 2737 |
+
/* justify-content:Gradio 給 .label-wrap 的預設是 space-between(標題文字
|
| 2738 |
+
靠左、開合箭頭靠右)。這裡是三個 flex 子項(::before 圖示、標題文字、
|
| 2739 |
+
箭頭),space-between 會把可用空間平均塞進三者之間,標題文字被推到整
|
| 2740 |
+
列中間,跟旁邊其他導覽項目的文字對不齊。改成 flex-start 讓圖示與文字
|
| 2741 |
+
靠左並排(跟其他項目一致),箭頭再單獨用 margin-left:auto 推到最右。
|
| 2742 |
+
gap 對齊其他項目:圖示 18px 之後空 10px 才是文字。 */
|
| 2743 |
#nav_stat_accordion .label-wrap {
|
| 2744 |
+
display: flex !important; align-items: center !important;
|
| 2745 |
+
justify-content: flex-start !important; gap: 10px !important;
|
| 2746 |
+
}
|
| 2747 |
+
#nav_stat_accordion .label-wrap > .icon {
|
| 2748 |
+
margin-left: auto !important;
|
| 2749 |
}
|
| 2750 |
#nav_stat_accordion .label-wrap::before {
|
| 2751 |
content: "";
|
|
|
|
| 2812 |
/* 注意:max-width + margin auto 是舊版單欄置中版面留下的樣式,現在
|
| 2813 |
upload-zone 是右側 action-col 裡的元件,要跟同一欄的 scenario-buttons
|
| 2814 |
同寬��撐滿欄寬),不能再置中收窄 */
|
| 2815 |
+
/* 2026-07-23 千卉實機回饋:原本的強調色外框+大範圍陰影+漸層底色讓這張
|
| 2816 |
+
卡片像一個浮起來的彈窗。改成扁平——外框、陰影、底色全部拿掉,只保留
|
| 2817 |
+
圓角與內邊距維持內容的呼吸空間。彈出動畫保留,它負責的是「這塊剛剛才
|
| 2818 |
+
出現」的狀態提示,跟視覺重量無關。 */
|
| 2819 |
.upload-zone {
|
| 2820 |
+
border: none !important;
|
| 2821 |
border-radius: 16px !important;
|
| 2822 |
+
background: none !important;
|
| 2823 |
padding: 1.4rem 1.5rem !important;
|
| 2824 |
margin: .6rem 0 0 !important;
|
| 2825 |
width: 100% !important;
|
| 2826 |
+
box-shadow: none !important;
|
| 2827 |
animation: popupIn .25s cubic-bezier(.2,.8,.3,1) !important;
|
| 2828 |
}
|
| 2829 |
@keyframes popupIn {
|
|
|
|
| 2839 |
margin-bottom: .9rem;
|
| 2840 |
letter-spacing: .02em;
|
| 2841 |
}
|
| 2842 |
+
/* 原本這裡有 .upload-zone:hover/:focus-within 加深 border-color 的規則,
|
| 2843 |
+
2026-07-23 卡片扁平化拿掉外框後就沒有東西可加深了,一併移除。 */
|
|
|
|
| 2844 |
/* 對話輸入框(Phase 8:不再負責上傳,改回純展示用的常駐輸入框):
|
| 2845 |
刻意跟上面「醒目卡片」風格的 .upload-zone 拉開視覺重量差異,
|
| 2846 |
不用強調色框線/陰影/彈出動畫,也不掛 ::before 的標題文字,
|
|
|
|
| 2913 |
標籤(暫不用圖示,等下一輪視覺打磨補 Heroicons SVG),維持跟
|
| 2914 |
scenario-buttons 一致的「貼齊左邊、無強調色底」低視覺重量風格,
|
| 2915 |
避免搶過主要操作按鈕的注意力 */
|
| 2916 |
+
/* 左右留白已上移到 .nav-col(見該處註解),這裡不再重複設定,避免疊加成
|
| 2917 |
+
雙倍縮排。上緣的 border-top 因此會跟著欄位的 padding 內縮 8px,跟下方
|
| 2918 |
+
.result-nav-divider 本來就有的內縮一致。 */
|
| 2919 |
.result-nav {
|
| 2920 |
margin-top: 1rem !important;
|
| 2921 |
padding-top: 1rem !important;
|
| 2922 |
border-top: 1px solid var(--c-border) !important;
|
| 2923 |
gap: .3rem !important;
|
| 2924 |
}
|
| 2925 |
+
/* 2026-07-23:導覽項目改用 Bootstrap 5.3 `.nav-underline` 的視覺模型
|
| 2926 |
+
(getbootstrap.com/docs/5.3/components/navs-tabs/)——未選取的項目是
|
| 2927 |
+
純文字,沒有邊框也沒有底色;選取的項目只換文字顏色再加一條粗線。官方
|
| 2928 |
+
的線畫在下緣(橫向分頁用),這裡是垂直導覽,依慣例改畫在左緣。
|
| 2929 |
+
千卉回饋原本「看起來像很多按鈕」的真正原因不是這裡的樣式寫錯,而是
|
| 2930 |
+
下面這個選擇器少了 id 祖先:Gradio 自己的 secondary 按鈕樣式(Svelte
|
| 2931 |
+
編譯出 button.svelte-xxx.secondary 這種 type+class 組合)specificity
|
| 2932 |
+
比純 class 高,即使掛 !important 也贏不了,所以 border:none 一直沒生效、
|
| 2933 |
+
Gradio 預設的框線與底色整個透出來。子項目當初已經用 #result_nav_col
|
| 2934 |
+
修過(見下方註解),主項目漏掉了,這次補上。 */
|
| 2935 |
+
#result_nav_col .result-nav-btn {
|
| 2936 |
width: 100% !important;
|
| 2937 |
justify-content: flex-start !important;
|
| 2938 |
text-align: left !important;
|
|
|
|
| 2940 |
background: transparent !important;
|
| 2941 |
box-shadow: none !important;
|
| 2942 |
}
|
| 2943 |
+
#result_nav_col .result-nav-btn {
|
| 2944 |
+
padding: .5rem .75rem !important;
|
| 2945 |
font-size: .88rem !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2946 |
font-weight: 500 !important;
|
| 2947 |
+
border-radius: 0 !important;
|
| 2948 |
+
border-left: 2px solid transparent !important;
|
| 2949 |
+
color: var(--c-text-soft) !important;
|
| 2950 |
+
transition: color .15s ease-out, border-color .15s ease-out !important;
|
| 2951 |
}
|
| 2952 |
+
#result_nav_col .result-nav-btn:hover {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2953 |
background: transparent !important;
|
| 2954 |
+
color: var(--c-text) !important;
|
| 2955 |
}
|
| 2956 |
+
/* 歷史紀錄:這裡原本有一整組 .result-nav-subbtn 樣式,把統計比較分析底下
|
| 2957 |
+
的四頁做成「小一號字+灰一階+真縮排」的子選單(縮排寫法是 2026-07-23
|
| 2958 |
+
從 Bootstrap 官方 Sidebars 範例的 .btn-toggle-nav a 移植過來的,用
|
| 2959 |
+
margin-left 真的把元素往右推、寬度跟著縮,而不是只用 padding-left 讓
|
| 2960 |
+
文字位移)。同日千卉決定四頁改用跟其他導覽項目一模一樣的樣式並各自配
|
| 2961 |
+
圖示、左緣齊平,這組樣式整個不再需要,連同 class 一起移除。
|
| 2962 |
+
Gradio secondary 按鈕邊框那個 specificity 坑不會因此消失——它現在由
|
| 2963 |
+
上方 `#result_nav_col .result-nav-btn` 那組共用規則一併處理。 */
|
| 2964 |
+
/* padding 左右歸零:Gradio 幫 gr.Accordion 的外層 block 掛了 .padded
|
| 2965 |
+
(水平 12px)。四個子頁改成一般項目、要跟其他導覽項目左緣齊平之後,
|
| 2966 |
+
這 12px 會讓它們整組往右位移,看起來像還有縮排沒拿乾淨。垂直方向的
|
| 2967 |
+
padding 不動,那是手風琴自己的行距。 */
|
| 2968 |
.result-nav-accordion {
|
| 2969 |
margin: 0 !important;
|
| 2970 |
border: none !important;
|
| 2971 |
background: transparent !important;
|
| 2972 |
box-shadow: none !important;
|
| 2973 |
+
padding-left: 0 !important;
|
| 2974 |
+
padding-right: 0 !important;
|
| 2975 |
}
|
| 2976 |
+
/* 手風琴的標題列(統計比較分析)也是一個導覽項目,字重/內距/左緣線
|
| 2977 |
+
都跟 .result-nav-btn 對齊,不然它會是這排裡唯一長得不一樣的一項 */
|
| 2978 |
.result-nav-accordion .label-wrap {
|
| 2979 |
+
padding: .5rem .75rem !important;
|
| 2980 |
font-size: .88rem !important;
|
| 2981 |
+
font-weight: 500 !important;
|
| 2982 |
+
border-left: 2px solid transparent !important;
|
| 2983 |
color: var(--c-text-soft) !important;
|
| 2984 |
}
|
| 2985 |
/* 2026-07-23:頁面切換模型——目前使用者所在的那一頁在導覽列高亮,
|
| 2986 |
applyActivePage() JS 幫對應按鈕加/移除這個 class。視覺先求「看得出
|
| 2987 |
選中誰」的基本可用,細部樣式(跟 Bootstrap 借來的分組/層級規範)留給
|
| 2988 |
下一輪 nav-col 視覺重做一起處理,這裡不重複做兩次。 */
|
| 2989 |
+
#result_nav_col .result-nav-btn.active {
|
| 2990 |
+
background: transparent !important;
|
| 2991 |
+
border-left-color: var(--c-accent-strong) !important;
|
| 2992 |
color: var(--c-accent-strong) !important;
|
| 2993 |
font-weight: 700 !important;
|
| 2994 |
}
|
| 2995 |
.result-nav-accordion.active .label-wrap {
|
| 2996 |
+
border-left-color: var(--c-accent-strong) !important;
|
| 2997 |
color: var(--c-accent-strong) !important;
|
| 2998 |
font-weight: 700 !important;
|
| 2999 |
}
|
| 3000 |
+
/* 技術執行紀錄:低優先度診斷工具,跟上面的分析結果用分隔線+降低視覺
|
| 3001 |
+
權重(字級與顏色都比其他導覽項目再低一階)區隔開來 */
|
| 3002 |
.result-nav-divider {
|
| 3003 |
height: 1px;
|
| 3004 |
background: var(--c-border);
|
| 3005 |
margin: .5rem .3rem;
|
| 3006 |
}
|
| 3007 |
+
/* 同樣要帶 #result_nav_col,否則純 class(0,1,0)會被上方
|
| 3008 |
+
`#result_nav_col .result-nav-btn`(1,1,0)的字級/顏色蓋掉,
|
| 3009 |
+
技術執行紀錄就不再是降權的樣子 */
|
| 3010 |
+
#result_nav_col .result-nav-btn--muted {
|
| 3011 |
font-size: .82rem !important;
|
| 3012 |
font-weight: 500 !important;
|
| 3013 |
color: var(--c-text-muted) !important;
|
| 3014 |
}
|
| 3015 |
+
/* 語料 Metadata 說明表單卡片:跟 .upload-zone 同批扁平化(2026-07-23),
|
| 3016 |
+
兩者是同一種「Canvas 裡的操作區塊」,樣式要保持一致 */
|
| 3017 |
.metadata-zone {
|
| 3018 |
+
border: none !important;
|
| 3019 |
border-radius: 16px !important;
|
| 3020 |
+
background: none !important;
|
| 3021 |
padding: 1.4rem 1.6rem !important;
|
| 3022 |
margin: 1rem 0 !important;
|
| 3023 |
+
box-shadow: none !important;
|
| 3024 |
animation: popupIn .25s cubic-bezier(.2,.8,.3,1) !important;
|
| 3025 |
}
|
| 3026 |
.metadata-zone .gr-box, .metadata-zone .form { background: transparent !important; }
|
|
|
|
| 3949 |
"語言指標分析", icon=str(BASE_DIR / "assets" / "icons" / "trending-up.svg"),
|
| 3950 |
elem_id="nav_idx_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3951 |
)
|
| 3952 |
+
# open=True(千卉 2026-07-23):收合狀態下這一項跟旁邊四個
|
| 3953 |
+
# 主項目長得一樣,看不出底下還有四個子頁面,使用者不會知道
|
| 3954 |
+
# 要去點開。導覽列的作用是把可去的地方攤開來給人看,預設收
|
| 3955 |
+
# 起來等於把內容藏住。仍然保留可收合,只是不再預設收起。
|
| 3956 |
+
with gr.Accordion("統計比較分析", open=True, elem_id="nav_stat_accordion",
|
| 3957 |
elem_classes=["result-nav-accordion"]):
|
| 3958 |
+
# 2026-07-23 千卉決定:這四頁改用跟其他導覽項目完全
|
| 3959 |
+
# 一樣的樣式(elem_classes 從 result-nav-subbtn 換成
|
| 3960 |
+
# result-nav-btn)並各自配圖示,縮排也拿掉、左緣齊平。
|
| 3961 |
+
# 原本的「小一號字+灰一階+真縮排」子選單樣式(Phase 3
|
| 3962 |
+
# 借 Bootstrap Sidebars 的那套)連同 CSS 一併移除。
|
| 3963 |
+
nav_stat_sub_0 = gr.Button(
|
| 3964 |
+
"敘述統計", icon=str(BASE_DIR / "assets" / "icons" / "table-cells.svg"),
|
| 3965 |
+
elem_id="nav_stat_sub_0", elem_classes=["result-nav-btn"], variant="secondary")
|
| 3966 |
+
nav_stat_sub_1 = gr.Button(
|
| 3967 |
+
"差異性分析", icon=str(BASE_DIR / "assets" / "icons" / "scale.svg"),
|
| 3968 |
+
elem_id="nav_stat_sub_1", elem_classes=["result-nav-btn"], variant="secondary")
|
| 3969 |
+
nav_stat_sub_2 = gr.Button(
|
| 3970 |
+
"監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "academic-cap.svg"),
|
| 3971 |
+
elem_id="nav_stat_sub_2", elem_classes=["result-nav-btn"], variant="secondary")
|
| 3972 |
+
nav_stat_sub_3 = gr.Button(
|
| 3973 |
+
"非監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "squares-2x2.svg"),
|
| 3974 |
+
elem_id="nav_stat_sub_3", elem_classes=["result-nav-btn"], variant="secondary")
|
| 3975 |
nav_report_btn = gr.Button(
|
| 3976 |
"LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
|
| 3977 |
elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
|
|
|
| 4422 |
)
|
| 4423 |
|
| 4424 |
# ── Phase 8-D:Canvas 運算中提示(粗粒度階段文字+轉圈動畫)─────
|
| 4425 |
+
# 只在三段運算階段顯示(2026-07-23 步驟列收斂後 key 從 5/6/7 改成
|
| 4426 |
+
# 4/5/6,這個字典的 key 是 _STEPS 的步驟編號,改步驟列一定要一起改),
|
| 4427 |
+
# 不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不顯示、不用另外
|
| 4428 |
+
# 接一個 visibility 輸出,wiring 只多一個輸出點。
|
| 4429 |
_COMPUTING_STEP_LABELS = {
|
| 4430 |
+
4: ("1", "指標計算"),
|
| 4431 |
+
5: ("2", "統計分析"),
|
| 4432 |
+
6: ("3", "報告撰寫"),
|
| 4433 |
}
|
| 4434 |
|
| 4435 |
def _build_computing_html(state):
|
|
|
|
| 4473 |
gr.update(visible=True))
|
| 4474 |
|
| 4475 |
reply, state = handle_scenario_select(sc_num, state)
|
| 4476 |
+
log_history = _log_append(log_history, "✅ 情境選擇完成")
|
| 4477 |
chat_history = chat_history + [_assistant_msg(reply)]
|
| 4478 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4479 |
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4480 |
|
| 4481 |
+
# STEP 1 文本上傳:確認訊息已經導向上傳卡片的「格式要求」區塊,不再
|
| 4482 |
+
# 另外用 LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘
|
| 4483 |
+
# 內容。這裡原本是獨立的「上傳引導」步驟,但它跟前一步在同一個
|
| 4484 |
+
# generator 內連續完成、中間沒有任何使用者動作,步驟列上永遠只會一
|
| 4485 |
+
# 起亮起,2026-07-23 併進「文本上傳」,只留等待上傳的狀態切換。
|
| 4486 |
+
state["current_step"] = 1
|
|
|
|
|
|
|
|
|
|
| 4487 |
state["state"] = "UPLOAD_WAIT"
|
| 4488 |
+
log_history = _log_append(log_history, "⏳ STEP 1 等待上傳文本……")
|
| 4489 |
# 情境已選定,按鈕收起;對話視窗自此回歸純輸出角色。
|
| 4490 |
# 格式要求直接寫進上傳卡片本身(upload_hint_md),使用者不用往上
|
| 4491 |
# 捲回對話視窗找 AI 剛剛說過的規則——卡片本身就要自己講清楚。
|
|
|
|
| 4558 |
yield chat_history, log_history, state, gr.update(visible=True), gr.update()
|
| 4559 |
return
|
| 4560 |
|
| 4561 |
+
# STEP 1 文本上傳(開始處理即關閉上傳彈窗,避免遮罩擋住操作)
|
| 4562 |
+
state["current_step"] = 1
|
| 4563 |
+
log_history = _log_append(log_history, "⏳ STEP 1 接收上傳檔案中……")
|
| 4564 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4565 |
|
| 4566 |
recv_msg, state = handle_uploaded_files(files, state)
|
| 4567 |
chat_history = chat_history + [_assistant_msg(recv_msg)]
|
| 4568 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [1]))
|
| 4569 |
+
state["current_step"] = 2
|
| 4570 |
+
log_history = _log_append(log_history, f"✅ STEP 1 已接收 {len(files)} 個檔案")
|
| 4571 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4572 |
|
| 4573 |
+
# STEP 2 格式驗證
|
| 4574 |
+
log_history = _log_append(log_history, "⏳ STEP 2 格式驗證中……")
|
| 4575 |
yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
|
| 4576 |
|
| 4577 |
check_msg, state, has_errors = run_format_check(state)
|
| 4578 |
chat_history = chat_history + [_assistant_msg(check_msg)]
|
| 4579 |
|
| 4580 |
if has_errors:
|
| 4581 |
+
log_history = _log_append(log_history, "⚠️ STEP 2 格式不符,請重新上傳")
|
| 4582 |
+
# 退回等待重傳:step 1 要從 done_steps 拿掉才會顯示成「進行中」。
|
| 4583 |
+
# _build_header_html 的判斷順序是 done > current,只設 current_step
|
| 4584 |
+
# 而不清除 done 的話,步驟列會停在「✓ 文本上傳」,跟畫面正在要求
|
| 4585 |
+
# 使用者補傳檔案互相矛盾。
|
| 4586 |
+
state["done_steps"] = [s for s in state.get("done_steps", []) if s != 1]
|
| 4587 |
+
state["current_step"] = 1
|
| 4588 |
yield chat_history, log_history, state, gr.update(visible=True), _todo_update(state)
|
| 4589 |
return
|
| 4590 |
|
| 4591 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [2]))
|
| 4592 |
+
state["current_step"] = 3
|
| 4593 |
+
log_history = _log_append(log_history, "✅ STEP 2 格式驗證通過")
|
| 4594 |
# 格式通過後:顯示 metadata 說明表單,等使用者填寫後再開始分析
|
| 4595 |
state["state"] = "METADATA_INPUT"
|
| 4596 |
meta_hint = "請填寫語料 Metadata 說明(原文資訊、群組層與個體層),完成後點「確認並開始分析」。"
|
|
|
|
| 4649 |
# 2026-07-23:技術執行紀錄轉正成獨立頁面後,可見性只在情境選定
|
| 4650 |
# (on_scenario_pick)與重置(on_reset_confirm)時切換一次,
|
| 4651 |
# 這裡不用再逐步更新開合狀態/摘要標籤,改回傳 gr.update() 不動。
|
| 4652 |
+
# 「語料說明」(step 3) 在這裡收尾:confirm 與 skip 兩條路徑都會走進
|
| 4653 |
+
# _run_computation,標記在這一處就好,不用在兩個 handler 各寫一次。
|
| 4654 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [3]))
|
| 4655 |
+
state["current_step"] = 4
|
| 4656 |
+
log_history = _log_append(log_history, "⏳ STEP 4 計算語言指標中……")
|
| 4657 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4658 |
gr.update(), _computing_update(state))
|
| 4659 |
for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
|
| 4660 |
if "指標計算完成" in prog_msg:
|
| 4661 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [4])); state["current_step"] = 5
|
| 4662 |
elif "步驟 2/3" in prog_msg or "統計分析" in prog_msg and "完成" not in prog_msg:
|
| 4663 |
+
state["current_step"] = 5
|
| 4664 |
elif "統計分析完成" in prog_msg or ("統計" in prog_msg and "完成" in prog_msg):
|
| 4665 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [5])); state["current_step"] = 6
|
| 4666 |
elif "步驟 3/3" in prog_msg or "報告" in prog_msg and "完成" not in prog_msg:
|
| 4667 |
+
state["current_step"] = 6
|
| 4668 |
elif is_final:
|
| 4669 |
+
# 舊版這裡是 +[7, 8](報告撰寫與下載成果寫死在同一行一起標記,
|
| 4670 |
+
# 因為下載成果本來就不是獨立運算階段),收斂後合併成 step 6
|
| 4671 |
+
state["done_steps"] = list(set(state.get("done_steps", []) + [6])); state["current_step"] = None
|
| 4672 |
log_history = _log_append(log_history, prog_msg)
|
| 4673 |
if is_final:
|
| 4674 |
chat_history = chat_history + [_assistant_msg(prog_msg)]
|
|
|
|
|
|
|
|
|