un2266 Claude Opus 4.8 commited on
Commit
5033f69
·
1 Parent(s): 26358d8

步驟列收斂 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 CHANGED
@@ -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
- state["done_steps"] = [1]
1450
- state["current_step"] = 2
 
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"] = 5
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"] = 6
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
- align-items: center;
1947
- flex-wrap: wrap;
 
1948
  gap: .9rem;
1949
  text-align: left;
1950
- padding: .85rem 1.25rem;
1951
  position: relative;
1952
  margin: 0 0 1.25rem;
1953
- border: 1px solid var(--c-border);
1954
- border-radius: 16px;
1955
- background: linear-gradient(180deg, rgba(var(--c-accent-rgb),.07) 0%, rgba(var(--c-bg-rgb),.45) 100%);
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 改單行版面後,steps 改成跟標題
2013
- 同一行左右排列(flex-wrap 讓視窗窄時自動換行),不再置中、不再需要
2014
- 靠 margin-top 跟上方大標題拉開距離(間距已交給 .app-header 的 gap) */
 
2015
  .app-header .steps {
2016
  display: flex; justify-content: flex-start; flex-wrap: wrap;
2017
- gap: 4px; margin: 0; flex: 1 1 auto; min-width: 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; gap: .5rem !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: 1px solid rgba(var(--c-accent-rgb),.40) !important;
2768
  border-radius: 16px !important;
2769
- background: linear-gradient(180deg, rgba(var(--c-accent-rgb),.06) 0%, var(--c-surface) 100%) !important;
2770
  padding: 1.4rem 1.5rem !important;
2771
  margin: .6rem 0 0 !important;
2772
  width: 100% !important;
2773
- box-shadow: 0 16px 50px rgba(15,23,42,.20) !important;
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, .upload-zone:focus-within {
2790
- border-color: rgba(var(--c-accent-rgb),.6) !important;
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
- .result-nav-btn, .result-nav-subbtn {
 
 
 
 
 
 
 
 
 
 
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: .6rem .7rem !important;
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: 6px !important;
2905
- color: var(--c-text-muted) !important;
 
 
2906
  }
2907
- /* 踩坑:縮窄寬度後 Gradio 自己的 secondary 按鈕邊框(Svelte 編譯出的
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
- box-shadow: none !important;
2917
  }
2918
- .result-nav-subbtn:hover { background: rgba(var(--c-accent-rgb),.06) !important; color: var(--c-text-soft) !important; }
 
 
 
 
 
 
 
 
 
 
 
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: .6rem .7rem !important;
2927
  font-size: .88rem !important;
2928
- font-weight: 600 !important;
 
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: rgba(var(--c-accent-rgb),.10) !important;
 
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
- 降低視覺權重區隔(字級/顏色都比照 result-nav-subbtn 而非 result-nav-btn)*/
2946
  .result-nav-divider {
2947
  height: 1px;
2948
  background: var(--c-border);
2949
  margin: .5rem .3rem;
2950
  }
2951
- .result-nav-btn--muted {
 
 
 
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: 1px solid rgba(var(--c-accent-rgb),.40) !important;
2959
  border-radius: 16px !important;
2960
- background: linear-gradient(180deg, rgba(var(--c-accent-rgb),.06) 0%, var(--c-surface) 100%) !important;
2961
  padding: 1.4rem 1.6rem !important;
2962
  margin: 1rem 0 !important;
2963
- box-shadow: 0 16px 50px rgba(15,23,42,.20) !important;
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
- with gr.Accordion("統計比較分析", open=False, elem_id="nav_stat_accordion",
 
 
 
 
3893
  elem_classes=["result-nav-accordion"]):
3894
- nav_stat_sub_0 = gr.Button("敘述統計", elem_id="nav_stat_sub_0",
3895
- elem_classes=["result-nav-subbtn"], variant="secondary")
3896
- nav_stat_sub_1 = gr.Button("差異性分析", elem_id="nav_stat_sub_1",
3897
- elem_classes=["result-nav-subbtn"], variant="secondary")
3898
- nav_stat_sub_2 = gr.Button("監督式學習", elem_id="nav_stat_sub_2",
3899
- elem_classes=["result-nav-subbtn"], variant="secondary")
3900
- nav_stat_sub_3 = gr.Button("非監督式學習", elem_id="nav_stat_sub_3",
3901
- elem_classes=["result-nav-subbtn"], variant="secondary")
 
 
 
 
 
 
 
 
 
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
- # 沿用跟 _accordion_update 完全一樣的判斷(current_step 在 5/6/7 之間
4353
- # 才顯示),不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不
4354
- # 顯示、不用另外接一個 visibility 輸出,wiring 只多一個輸出點。
 
4355
  _COMPUTING_STEP_LABELS = {
4356
- 5: ("1", "指標計算"),
4357
- 6: ("2", "統計分析"),
4358
- 7: ("3", "報告撰寫"),
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, "✅ STEP 1 情境選擇完成")
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 2:確認訊息已經導向上傳卡片的「格式要求」區塊,不再另外用
4408
- # LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘內容
4409
- state["current_step"] = 2
4410
- log_history = _log_append(log_history, "⏳ STEP 2 上傳引導中……")
4411
- yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
4412
- gr.update(), gr.update(), gr.update(), gr.update())
4413
-
4414
- state["done_steps"] = [1, 2]
4415
- state["current_step"] = 3
4416
  state["state"] = "UPLOAD_WAIT"
4417
- log_history = _log_append(log_history, "✅ STEP 2 完成,等待上傳文本")
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 3(開始處理即關閉上傳彈窗,避免遮罩擋住操作)
4491
- state["current_step"] = 3
4492
- log_history = _log_append(log_history, "⏳ STEP 3 接收上傳檔案中……")
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", []) + [3]))
4498
- state["current_step"] = 4
4499
- log_history = _log_append(log_history, f"✅ STEP 3 已接收 {len(files)} 個檔案")
4500
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4501
 
4502
- # STEP 4
4503
- log_history = _log_append(log_history, "⏳ STEP 4 格式驗證中……")
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 4 格式不符,請重新上傳")
4511
- state["current_step"] = 3
 
 
 
 
 
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", []) + [4]))
4516
- state["current_step"] = 5
4517
- log_history = _log_append(log_history, "✅ STEP 4 格式驗證通過")
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
- log_history = _log_append(log_history, "⏳ STEP 5 計算語言指標中……")
 
 
 
 
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", []) + [5])); state["current_step"] = 6
4582
  elif "步驟 2/3" in prog_msg or "統計分析" in prog_msg and "完成" not in prog_msg:
4583
- state["current_step"] = 6
4584
  elif "統計分析完成" in prog_msg or ("統計" in prog_msg and "完成" in prog_msg):
4585
- state["done_steps"] = list(set(state.get("done_steps", []) + [6])); state["current_step"] = 7
4586
  elif "步驟 3/3" in prog_msg or "報告" in prog_msg and "完成" not in prog_msg:
4587
- state["current_step"] = 7
4588
  elif is_final:
4589
- state["done_steps"] = list(set(state.get("done_steps", []) + [7, 8])); state["current_step"] = None
 
 
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)]
assets/icons/academic-cap.svg ADDED
assets/icons/scale.svg ADDED
assets/icons/squares-2x2.svg ADDED
assets/icons/table-cells.svg ADDED