un2266 Claude Opus 4.8 commited on
Commit
f2c94db
·
1 Parent(s): 89ca669

運算階段重構:步驟列收斂3步、分析運算內容區塊、誠實失敗、導覽閘門、多項修正

Browse files

步驟列 6→3 步(C2/C3):文本上傳/語料說明/分析運算
- 移除「格式驗證」:它在任何靜止狀態都不停留(通過時同 generator 內連續
完成、失敗時退回文本上傳要求重傳),逐行細節仍在技術執行紀錄
- 合併「指標計算/統計分析/報告產出」成單一「分析運算」:三段是同一個
generator 依序跑完、中間無使用者操作關卡

分析運算內容區塊(_build_analysis_html,取代 computing_html 的單行提示)
- 運算中顯示三子階段進度+轉圈動畫,運算後變分段摘要(標題+狀態+一句
結論+查看入口);報告失敗時該段顯示 ! 並說明其他結果不受影響
- 每段「查看…」入口純 JS 橋接,點了等同點左側對應導覽項目
- state 新增 analysis 子階段追蹤,由 pipeline 邊跑邊寫

A1 誠實失敗:報告撰寫失敗不再標「全部完成」+✓
- pipeline 明確標記各子階段 done/failed;任一失敗 → 分析運算步驟標紅、
總結文案改為「分析完成(研究報告未產出)…指標與統計結果均已完成」
- WARNING 進對話區(原本只進技術執行紀錄,使用者看不到)

A2 結果導覽閘門:語言指標分析/統計四項/LLM輔助分析預設 disabled,由
_populate_tabs 依各結果是否真的產出逐一啟用(報告失敗時 LLM輔助分析維持
灰的,是失敗的第二道確認);AI對話與技術執行紀錄不在此列

C4:完成後步驟列不再隱藏(改互斥頁面模型後,隱藏會讓 AI對話頁整片空白,
分析運算內容區塊現在填滿 Canvas)

C1:統計比較分析 Canvas 內的分頁列隱藏(與左欄四項重複)。踩坑兩種隱藏法
都失敗——display:none 讓 .click() 不觸發切換;clip/縮 1px 觸發 Gradio 溢出
邏輯把分頁全塞進 overflow dropdown。正解:保留 width:100% 只用 height:0+
opacity:0 視覺隱藏

B1 修 loading 圓圈:border 簡寫在這版 Gradio 不生效,拆長寫法
B2 關掉 Gradio .generating 呼吸藍框(頂部列與內容區塊各一個,礙眼)
B3 下載捷徑分隔線+四顆按鈕包進 nav_dl_group,無 zip 時整組隱藏(不再有
孤兒分隔線/兩條線貼一起)

D1 兩處「上方分頁」過期文案改指向左側導覽列
D2 格式要求「使用 I 語料淬鍊」錯字修正並補工具連結

E1 移除 .tabs > .tab-nav 整組死規則
E2 移除 on_chat_upload_submit 死函式
(E3 空 Column/E4 CSS 抽檔/E5 _TAB_OUTPUTS 重構留待下一輪)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (1) hide show
  1. app_agent.py +365 -199
app_agent.py CHANGED
@@ -101,7 +101,7 @@ SCENARIOS = {
101
  "upload_prompt": (
102
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
103
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分人工翻譯與機器翻譯。\n"
104
- "3. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n"
105
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
106
  ),
107
  "desc": "焦點在人機差異的語言特徵。",
@@ -113,7 +113,7 @@ SCENARIOS = {
113
  "upload_prompt": (
114
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
115
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分全譯與編譯。\n"
116
- "3. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n"
117
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
118
  ),
119
  "desc": "焦點在文本完整性和改寫策略。",
@@ -125,7 +125,7 @@ SCENARIOS = {
125
  "upload_prompt": (
126
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
127
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同群組的譯者。\n"
128
- "3. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n"
129
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
130
  ),
131
  "desc": "焦點在個人風格差異。排除時代因素,討論譯者個人的語言偏好。",
@@ -137,7 +137,7 @@ SCENARIOS = {
137
  "upload_prompt": (
138
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
139
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同時期的譯本。\n"
140
- "3. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n"
141
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
142
  ),
143
  "desc": "分析焦點在歷時變化、語言現代化、翻譯規範的演變。時間維度是核心變數。",
@@ -1377,6 +1377,15 @@ def make_initial_state(session_id: str) -> dict:
1377
  "done_steps": [], # 已完成步驟(1-based)
1378
  "current_step": None, # 目前執行步驟
1379
  "failed_step": None, # 執行失敗時卡在哪一步(1-based),成功時維持 None
 
 
 
 
 
 
 
 
 
1380
  }
1381
 
1382
 
@@ -1388,13 +1397,26 @@ def make_initial_state(session_id: str) -> dict:
1388
  # 格式驗證通過就直接把 current_step 設成「指標計算」,導致使用者在填表的整段
1389
  # 期間,步驟列都亮著「指標計算 進行中」的呼吸動畫,畫面在說電腦正在算,實際
1390
  # 上是在等人填表。收斂後每一步都對應一件使用者感知得到的事。
 
 
 
 
 
 
 
 
 
1391
  _STEPS = [
1392
  (1, "文本上傳"),
1393
- (2, "格式驗證"),
1394
- (3, "語料說明"),
1395
- (4, "指標計算"),
1396
- (5, "統計分析"),
1397
- (6, "報告產出"),
 
 
 
 
1398
  ]
1399
 
1400
 
@@ -1413,15 +1435,12 @@ def _build_header_html(done_steps: list = None, current_step: int = None,
1413
  """
1414
  done_steps = done_steps or []
1415
 
1416
- # 全部完成後整條步驟列收起來,把畫面高度讓給結果內容——此時瀏覽結果的
1417
- # 角色已經交接給左側結果導覽(空間型),時間軸型的步驟列講完它要講的事
1418
- # 就沒有存在必要。失敗時例外:步驟列要留著標示卡在哪一步。
1419
- if failed_step is None and all(num in done_steps for num, _ in _STEPS):
1420
- return (
1421
- '<div class="app-header">'
1422
- ' <p class="title-line">譯彩紛呈 <span class="en">SpectraEngine</span></p>'
1423
- '</div>'
1424
- )
1425
 
1426
  pills = []
1427
  for num, name in _STEPS:
@@ -1656,23 +1675,31 @@ def run_computation_pipeline_streaming(state: dict):
1656
 
1657
  results_paths = []
1658
  anal = None
 
1659
 
1660
- # ── 步驟 1:指標計算 ─────────────────────────────────────────────────────
1661
- yield "【步驟 1/3】正在計算文本指標……", False, state
 
1662
  try:
1663
  idx_df, xlsx_path, csv_path = run_indicator_calculation(
1664
  state["cleaned_files"], output_dir, _RESOURCES
1665
  )
1666
  results_paths.extend([str(xlsx_path), str(csv_path)])
 
 
 
1667
  yield f" [OK] 指標計算完成({len(idx_df)} 個文本)", False, state
1668
  except Exception as e:
 
 
1669
  state["state"] = "COMPLETE"
1670
- state["failed_step"] = 4
1671
- yield f"[ERROR] 指標計算失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
1672
  return
1673
 
1674
- # ── 步驟 2:統計分析 ─────────────────────────────────────────────────────
1675
- yield "【步驟 2/3】正在執行統計分析……", False, state
 
1676
  try:
1677
  anal = run_statistical_analysis(csv_path, output_dir)
1678
  for key in ["desc_path","diff_path","imp_path","imp_fig_path",
@@ -1685,19 +1712,26 @@ def run_computation_pipeline_streaming(state: dict):
1685
  k: str(v) if isinstance(v, Path) else v
1686
  for k, v in anal.items() if k != "df"
1687
  }
 
 
 
 
 
1688
  yield (
1689
  f" [OK] 統計分析完成({anal['sig_count']}/{anal['total_indicators']} 個顯著指標,"
1690
  f"隨機森林準確率 {anal['accuracy']:.4f})"
1691
  ), False, state
1692
  except Exception as e:
 
 
1693
  state["state"] = "COMPLETE"
1694
  state["output_files"] = results_paths
1695
- state["failed_step"] = 5
1696
- yield f"[ERROR] 統計分析失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
1697
  return
1698
 
1699
- # ── 步驟 3:LLM 研究報告 ────────────────────────────────────────────────
1700
- yield "【步驟 3/3】主 Agent 正在撰寫研究報告(預計需要 30-90 秒)……", False, state
 
1701
  try:
1702
  rpt = run_report_writing(
1703
  scenario_key = state["scenario"],
@@ -1707,26 +1741,44 @@ def run_computation_pipeline_streaming(state: dict):
1707
  output_dir = output_dir,
1708
  )
1709
  results_paths.extend([str(rpt["docx_path"]), str(rpt["txt_path"])])
 
 
1710
  yield " [OK] 研究報告撰寫完成", False, state
1711
  except Exception as e:
1712
- yield f" [WARNING] LLM 報告撰寫失敗({e}),其他分析結果仍可下載。", False, state
 
 
 
 
 
 
1713
 
1714
  # ── 完成 ─────────────────────────────────────────────────────────────────
1715
  state["output_files"] = results_paths
1716
  state["state"] = "COMPLETE"
1717
 
 
1718
  sig_info = ""
1719
  if anal:
1720
  sig_info = (
1721
  f"顯著指標:**{anal['sig_count']}/{anal['total_indicators']}** 個(p < {ALPHA})。\n"
1722
  f"隨機森林分類準確率:**{anal['accuracy']:.4f}**。\n"
1723
  )
1724
- summary = (
1725
- f"**全部分析完成!**\n\n"
1726
- f"共分析 **{len(state['cleaned_files'])}** 個文本。\n"
1727
- f"{sig_info}\n"
1728
- f"各項結果請點選上方各分頁查看。"
1729
- )
 
 
 
 
 
 
 
 
 
1730
  yield summary, True, state
1731
 
1732
 
@@ -2156,60 +2208,33 @@ footer {
2156
  color: #fff;
2157
  }
2158
  /* ── 3. TABS ───────────────────────────────────────────────────── */
2159
- /* 注意:這裡原本用 backdrop-filter 做毛玻璃效果,搭配 position: sticky
2160
- 會讓瀏覽器每個捲動影格都要重新計算模糊,頁面一長(大表格、多張圖)就會
2161
- 卡很久(見使用者回報「滑鼠往下滑畫面會卡住好久」)。改用不透明背景取代
2162
- 模糊,視覺上仍有「浮在內容上方」的區隔感,但渲染成本低很多
2163
- ⚠ 已知問題(2026-07-22 Phase 8-D 排查時發現,尚未修):這整組
2164
- `.tabs > .tab-nav` 選擇器在目前這版 Gradio 的實際 DOM 完全不會命中——
2165
- 真正的分頁按鈕列 class 是 `.tabs > .tab-wrapper > .tab-container`(用
2166
- devtools 逐層確認過),沒有任何節點叫 `.tab-nav`。下面這整組 sticky/
2167
- 背景/hover/selected 樣式從寫下來那天起可能就是死規則,畫面上看起來
2168
- 還算合理是因為套用 Gradio 自己的預設樣式,不是這裡的客製化在生效。
2169
- 影響範圍是「統計比較分析」巢狀子分頁的視覺,不是本次 Phase 8-D 導覽
2170
- 功能的範圍(導覽的捲動/切換邏輯已改用正確的 `.tab-container` 選擇器,
2171
- 見 _APP_HEAD_SCRIPT 的 bindResultNav())。下次要做這批巢狀分頁的視覺
2172
- 打磨(sticky 分頁列、選中底色等)時,先把這整組選擇器換成
2173
- `.tab-container` 再動手,不要照抄這裡的 `.tab-nav`。 */
2174
- .tabs > .tab-nav {
2175
- background: var(--c-surface) !important;
2176
- border-bottom: 1px solid var(--c-border) !important;
2177
- padding: 0 1rem !important;
2178
- gap: 0 !important;
2179
- position: sticky; top: 0; z-index: 50;
2180
- }
2181
- .tabs > .tab-nav > button {
2182
- color: var(--c-text-subtle) !important;
2183
- font-weight: 600 !important;
2184
- font-size: 1.20rem !important;
2185
- padding: 0.70rem 1.1rem !important;
2186
- border: none !important;
2187
- border-bottom: 2px solid transparent !important;
2188
- border-radius: 0 !important;
2189
- background: transparent !important;
2190
- transition: color .18s ease, border-color .18s ease, background .18s ease !important;
2191
- cursor: pointer !important;
2192
- white-space: nowrap;
2193
- }
2194
- .tabs > .tab-nav > button:hover {
2195
- color: var(--c-text-soft) !important;
2196
- background: rgba(var(--c-accent-rgb),.05) !important;
2197
- }
2198
- .tabs > .tab-nav > button.selected {
2199
- color: var(--c-accent-strong) !important;
2200
- border-bottom-color: var(--c-accent) !important;
2201
- background: rgba(var(--c-accent-rgb),.07) !important;
2202
- }
2203
  .tabitem { padding: 1.5rem 1.25rem !important; }
2204
- /* 巢狀子分頁(統計比較分析內,敘述統計/差異性分析/監督式學習/非監督式學習)
2205
- 維持原字級。Phase 8-C 前這裡靠 .tabitem 祖先跟外層分頁列的 .tabs 區分;
2206
- 外層 gr.Tabs() 拿掉後,這是全站唯一剩下的 .tabs,改用 canvas_stat_col
2207
- 的 elem_id 精準定位,避免套用上面給「全頁分頁列」設計的 1.20rem 大字級。
2208
- 注意:選擇器用 .tab-container(不是 .tab-nav,見上方「已知問題」註解),
2209
- 且要排除 .visually-hidden 那份(窄螢幕溢出選單用的隱藏副本),否則兩份
2210
- 都會命中但視覺上只有一份看得到,容易誤以為規則沒生效 */
2211
- #canvas_stat .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button {
2212
- font-size: 0.80rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
2213
  }
2214
 
2215
  /* ── 4. CHATBOT ────────────────────────────────────────────────── */
@@ -3071,31 +3096,98 @@ img.button-icon { opacity: .82; }
3071
  從原本的 gr.Tab()(自帶 .tabitem padding)改成 gr.Column,這裡補上
3072
  跟上面 upload-zone/metadata-zone 一致的垂直間距,避免緊貼在一起 */
3073
  .canvas-result { margin-top: 1.2rem !important; }
3074
- /* Phase 8-D:運算中提示——跟 upload-zone/metadata-zone 一樣的圓角/間距
3075
- 慣例,但刻意用中性 surface2 底色(不是強調色漸層),因為這裡純粹是
3076
- 唯讀進度提示,不是需要操作的表單卡片,避免視覺上混淆成可互動元件 */
3077
- .computing-zone {
3078
- display: flex;
3079
- align-items: center;
3080
- gap: .7rem;
3081
  border: 1px solid var(--c-border) !important;
3082
  border-radius: 14px !important;
3083
  background: var(--c-surface2) !important;
3084
- padding: 1rem 1.3rem !important;
3085
  margin-top: 1.2rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3086
  font-size: var(--font-size-base) !important;
3087
  font-weight: 600 !important;
3088
- color: var(--c-text-soft) !important;
3089
  }
3090
- .computing-spinner {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3091
  flex: none;
3092
  width: 20px; height: 20px;
3093
  border-radius: 50%;
3094
- border: 2.5px solid rgba(var(--c-accent-rgb),.25);
3095
- border-top-color: var(--c-accent);
 
 
3096
  animation: computingSpin .8s linear infinite;
3097
  }
3098
  @keyframes computingSpin { to { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3099
  .upload-area, [data-testid="droparea"] {
3100
  border: 2px dashed rgba(var(--c-accent-rgb),.28) !important;
3101
  border-radius: 12px !important;
@@ -3497,6 +3589,16 @@ _APP_HEAD_SCRIPT = """
3497
  if (btn) btn.click();
3498
  });
3499
 
 
 
 
 
 
 
 
 
 
 
3500
  /* 2026-07-23:頁面切換模型。nav-col 的每個項目現在是真正的
3501
  「頁面」——結果類項目(語言指標分析/統計比較分析/LLM輔助分析/
3502
  技術執行紀錄)點進去時互斥顯示、收起對話欄、Canvas 佔滿寬度;
@@ -4058,9 +4160,18 @@ def create_app():
4058
  "AI對話", icon=str(BASE_DIR / "assets" / "icons" / "home.svg"),
4059
  elem_id="nav_top_btn", elem_classes=["result-nav-btn"], variant="secondary",
4060
  )
 
 
 
 
 
 
 
 
4061
  nav_idx_btn = gr.Button(
4062
  "語言指標分析", icon=str(BASE_DIR / "assets" / "icons" / "trending-up.svg"),
4063
  elem_id="nav_idx_btn", elem_classes=["result-nav-btn"], variant="secondary",
 
4064
  )
4065
  # 2026-07-24 千卉決定:拿掉「統計比較分析」這層 Accordion
4066
  # (標題文字與收合效果都不要),四個子頁直接跟其他導覽項目
@@ -4072,48 +4183,59 @@ def create_app():
4072
  # 見 _APP_HEAD_SCRIPT 的 applyActivePage()。
4073
  nav_stat_sub_0 = gr.Button(
4074
  "敘述統計", icon=str(BASE_DIR / "assets" / "icons" / "table-cells.svg"),
4075
- elem_id="nav_stat_sub_0", elem_classes=["result-nav-btn"], variant="secondary")
 
4076
  nav_stat_sub_1 = gr.Button(
4077
  "差異性分析", icon=str(BASE_DIR / "assets" / "icons" / "scale.svg"),
4078
- elem_id="nav_stat_sub_1", elem_classes=["result-nav-btn"], variant="secondary")
 
4079
  nav_stat_sub_2 = gr.Button(
4080
  "監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "academic-cap.svg"),
4081
- elem_id="nav_stat_sub_2", elem_classes=["result-nav-btn"], variant="secondary")
 
4082
  nav_stat_sub_3 = gr.Button(
4083
  "非監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "squares-2x2.svg"),
4084
- elem_id="nav_stat_sub_3", elem_classes=["result-nav-btn"], variant="secondary")
 
4085
  nav_report_btn = gr.Button(
4086
  "LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
4087
  elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
 
4088
  )
4089
  # ── 下載捷徑(2026-07-24)────────────────────────────
4090
  # 上面四項是「去看結果」,這四顆是「把結果帶走」,性質
4091
  # 不同所以用分隔線隔開。原本使用者要拿到全部產出得點進
4092
  # 三個結果頁各按各的下載鈕(共 15 個檔案),這裡各打成
4093
  # 一包 zip。內容為空的那幾包不會產生,對應按鈕就不出現
4094
- # (例如 LLM 報告失敗時「下載研究報告」不會亮出來),
4095
- # 可見性由 _populate_tabs 依 zip 是否存在決定。
4096
- gr.HTML('<div class="result-nav-divider"></div>')
4097
- nav_dl_idx_btn = gr.DownloadButton(
4098
- "下載指標數據", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4099
- visible=False, elem_id="nav_dl_idx_btn",
4100
- elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4101
- )
4102
- nav_dl_stat_btn = gr.DownloadButton(
4103
- "下載統計圖表", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4104
- visible=False, elem_id="nav_dl_stat_btn",
4105
- elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4106
- )
4107
- nav_dl_report_btn = gr.DownloadButton(
4108
- "下載研究報告", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4109
- visible=False, elem_id="nav_dl_report_btn",
4110
- elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4111
- )
4112
- nav_dl_all_btn = gr.DownloadButton(
4113
- "下載完整分析資料", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4114
- visible=False, elem_id="nav_dl_all_btn",
4115
- elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4116
- )
 
 
 
 
 
 
4117
 
4118
  # 2026-07-23:技術執行紀錄從手風琴轉正成獨立頁面(原本
4119
  # exec_log_accordion 在 canvas-col 底部,現在跟其他三個
@@ -4139,9 +4261,9 @@ def create_app():
4139
  # (見下方 upload_row),對話框目前也沒有其他打字用途
4140
  # (全站掃過沒有任何地方會解析使用者自由輸入文字),暫時
4141
  # 只留著當畫面元素,不接 sources=["upload"],避免顯示
4142
- # 附加檔案的迴紋針卻沒有作用。chat_upload.submit() 的
4143
- # wiring 已停用(見下方 on_chat_upload_submit 註解),
4144
- # 函式保留、之後如果對話框有真正用途要復活直接接回去即可。
4145
  with gr.Column(visible=True, elem_id="chat_upload_zone", elem_classes=["upload-zone-plain"]) as chat_upload_col:
4146
  chat_upload = gr.MultimodalTextbox(
4147
  file_types=[".txt"],
@@ -4234,10 +4356,10 @@ def create_app():
4234
  meta_cancel_btn = gr.Button("略過", scale=1, variant="secondary")
4235
  meta_confirm_btn = gr.Button("確認並開始分析", scale=2, variant="primary")
4236
 
4237
- # Phase 8-D:運算中提示(指標計算/統計分析/報告撰寫這三步
4238
- # 沒有真百分比,只有粗粒度階段字樣+轉圈動畫,讓使用者不用
4239
- # 展開下方技術執行紀錄也能一眼看到系統還在跑)。空字串=
4240
- # 不顯示,見 _build_computing_html/_computing_update。
4241
  computing_html = gr.HTML(value="", elem_id="computing_zone")
4242
 
4243
  # Phase 8-C:結果內容從 tab2/tab3/tab6(原本 gr.Tabs() 分頁頁面)搬進
@@ -4350,6 +4472,9 @@ def create_app():
4350
  canvas_exec_col, # 25 技術執行紀錄轉正成獨立頁面(2026-07-23)
4351
  nav_dl_idx_btn, nav_dl_stat_btn, # 26-27 導覽列下載捷徑(2026-07-24)
4352
  nav_dl_report_btn, nav_dl_all_btn, # 28-29
 
 
 
4353
  ]
4354
  _N_TAB = len(_TAB_OUTPUTS)
4355
 
@@ -4367,12 +4492,16 @@ def create_app():
4367
  if state is None:
4368
  return R
4369
 
4370
- # 下載捷徑(26-29)是少數必須「明確設回隱藏」的輸出:其他結果元件
4371
- # 都住在 canvas_*_col 裡,重置時整個 Column 被藏起來,殘值看不到;
4372
- # 這四顆住在 result_nav_col,而 result_nav_col 會在下次選定情境時
4373
- # 重新顯示,不主動歸零的話上一輪的 zip 會跟著冒出來。
 
4374
  for _i in (26, 27, 28, 29):
4375
  R[_i] = gr.update(visible=False, value=None)
 
 
 
4376
 
4377
  output_dir = Path(state["session_dir"]) / "output"
4378
  uploaded = state.get("uploaded_files", [])
@@ -4515,25 +4644,34 @@ def create_app():
4515
  for _i, _key in ((26, "idx"), (27, "stat"), (28, "report"), (29, "all")):
4516
  if zips.get(_key):
4517
  R[_i] = gr.update(value=zips[_key], visible=True)
 
 
 
4518
 
4519
- # ── 顯示各分頁 Tab(index 20-22)──────────────────────────────
4520
  any_tab_ready = False
4521
  if csv_path.exists():
4522
  R[20] = gr.update(visible=True) # 語言指標分析
 
4523
  any_tab_ready = True
4524
- if desc_path.exists() or imp_path.exists() \
4525
- or any(p.exists() for p in [dendro_path, elbow_path, pca_fig_path]):
 
4526
  R[21] = gr.update(visible=True) # 統計比較分析
 
 
 
4527
  any_tab_ready = True
4528
  if txt_files or docx_files:
4529
  R[22] = gr.update(visible=True) # 研究報告
 
4530
  any_tab_ready = True
4531
 
4532
  # 分頁內容(大表格、多張圖)從「不可見」變「可見」到實際整理進畫面
4533
  # 中間有一段沒有任何提示的空檔,加上這段運算本身就要跑數分鐘,使用
4534
  # 者常誤以為系統當掉了——這裡明確用 toast 告知「現在真的可以點了」
4535
  if any_tab_ready:
4536
- gr.Info("分析結果已就緒,可切換上方分頁查看!")
4537
 
4538
  return R
4539
 
@@ -4588,28 +4726,65 @@ def create_app():
4588
  # 4/5/6,這個字典的 key 是 _STEPS 的步驟編號,改步驟列一定要一起改),
4589
  # 不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不顯示、不用另外
4590
  # 接一個 visibility 輸出,wiring 只多一個輸出點���
4591
- _COMPUTING_STEP_LABELS = {
4592
- 4: ("1", "指標計算"),
4593
- 5: ("2", "統計分析"),
4594
- 6: ("3", "報告撰寫"),
 
 
 
 
 
 
 
 
 
4595
  }
4596
 
4597
- def _build_computing_html(state):
4598
  if not state:
4599
  return ""
4600
- info = _COMPUTING_STEP_LABELS.get(state.get("current_step"))
4601
- if not info:
4602
- return ""
4603
- n, label = info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4604
  return (
4605
- '<div class="computing-zone">'
4606
- '<span class="computing-spinner" aria-hidden="true"></span>'
4607
- f'<span>運算中…(步驟 {n}/3 {label})</span>'
4608
  '</div>'
4609
  )
4610
 
4611
  def _computing_update(state):
4612
- return gr.update(value=_build_computing_html(state))
4613
 
4614
  # ── 文字送出(streaming generator)──────────────────────────────────
4615
  # ── 研究情境選擇(封閉式固定四選一,按鈕直接觸發,不再解析打字輸入)──
@@ -4720,40 +4895,36 @@ def create_app():
4720
  yield chat_history, log_history, state, gr.update(visible=True), gr.update()
4721
  return
4722
 
4723
- # STEP 1 文本上傳(開始處理即關閉上傳彈窗,避免遮罩擋住操作)
4724
  state["current_step"] = 1
4725
- log_history = _log_append(log_history, "⏳ STEP 1 接收上傳檔案中……")
4726
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4727
 
4728
  recv_msg, state = handle_uploaded_files(files, state)
4729
  chat_history = chat_history + [_assistant_msg(recv_msg)]
4730
- state["done_steps"] = list(set(state.get("done_steps", []) + [1]))
4731
- state["current_step"] = 2
4732
- log_history = _log_append(log_history, f"✅ STEP 1 已接收 {len(files)} 個檔案")
4733
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4734
 
4735
- # STEP 2 格式驗證
4736
- log_history = _log_append(log_history, "⏳ STEP 2 格式驗證中……")
 
 
 
4737
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4738
 
4739
  check_msg, state, has_errors = run_format_check(state)
4740
  chat_history = chat_history + [_assistant_msg(check_msg)]
4741
 
4742
  if has_errors:
4743
- log_history = _log_append(log_history, "⚠️ STEP 2 格式不符,請重新上傳")
4744
- # 退回等待重傳:step 1 要從 done_steps 拿掉才會顯示成「進行中」。
4745
- # _build_header_html 的判斷順序是 done > current,只設 current_step
4746
- # 而不清除 done 的話,步驟列會停在「✓ 文本上傳」,跟畫面正在要求
4747
- # 使用者補傳檔案互相矛盾。
4748
- state["done_steps"] = [s for s in state.get("done_steps", []) if s != 1]
4749
  state["current_step"] = 1
4750
  yield chat_history, log_history, state, gr.update(visible=True), _todo_update(state)
4751
  return
4752
 
4753
- state["done_steps"] = list(set(state.get("done_steps", []) + [2]))
4754
- state["current_step"] = 3
4755
- log_history = _log_append(log_history, "✅ STEP 2 格式驗證通過")
4756
- # 格式通過後:顯示 metadata 說明表單,等使用者填寫後再開始分析
4757
  state["state"] = "METADATA_INPUT"
4758
  meta_hint = "請填寫語料 Metadata 說明(原文資訊、群組層與個體層),完成後點「確認並開始分析」。"
4759
  chat_history = chat_history + [_assistant_msg(meta_hint)]
@@ -4811,29 +4982,31 @@ def create_app():
4811
  # 2026-07-23:技術執行紀錄轉正成獨立頁面後,可見性只在情境選定
4812
  # (on_scenario_pick)與重置(on_reset_confirm)時切換一次,
4813
  # 這裡不用再逐步更新開合狀態/摘要標籤,改回傳 gr.update() 不動。
4814
- # 「語料說明」(step 3) 在這裡收尾:confirm 與 skip 兩條路徑都會走進
4815
  # _run_computation,標記在這一處就好,不用在兩個 handler 各寫一次。
4816
- state["done_steps"] = list(set(state.get("done_steps", []) + [3]))
4817
- state["current_step"] = 4
4818
- log_history = _log_append(log_history, "⏳ STEP 4 計算語言指標中……")
 
 
 
 
4819
  yield (chat_history, log_history, state, _todo_update(state),
4820
  gr.update(), _computing_update(state))
4821
  for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
4822
- if "指標計算完成" in prog_msg:
4823
- state["done_steps"] = list(set(state.get("done_steps", []) + [4])); state["current_step"] = 5
4824
- elif "步驟 2/3" in prog_msg or "統計分析" in prog_msg and "完成" not in prog_msg:
4825
- state["current_step"] = 5
4826
- elif "統計分析完成" in prog_msg or ("統計" in prog_msg and "完成" in prog_msg):
4827
- state["done_steps"] = list(set(state.get("done_steps", []) + [5])); state["current_step"] = 6
4828
- elif "步驟 3/3" in prog_msg or "報告" in prog_msg and "完成" not in prog_msg:
4829
- state["current_step"] = 6
4830
- elif is_final:
4831
- # 舊版這裡是 +[7, 8](報告撰寫與下載成果寫死在同一行一起標記,
4832
- # 因為下載成果本來就不是獨立運算階段),收斂後合併成 step 6
4833
- state["done_steps"] = list(set(state.get("done_steps", []) + [6])); state["current_step"] = None
4834
  log_history = _log_append(log_history, prog_msg)
4835
  if is_final:
4836
  chat_history = chat_history + [_assistant_msg(prog_msg)]
 
 
 
 
 
 
 
 
 
 
4837
  yield (chat_history, log_history, state, _todo_update(state),
4838
  gr.update(), _computing_update(state))
4839
 
@@ -4872,17 +5045,10 @@ def create_app():
4872
  outputs=[upload_row, file_input],
4873
  )
4874
 
4875
- # ── Phase 7 產物,Phase 8 停用 ──────────────────────────────────────
4876
- # on_chat_upload_submit 原本是 MultimodalTextbox 送出時的薄轉接生成器
4877
- # (拆 {"text":...,"files":[...]} 丟給 on_upload_confirm)。千卉實測
4878
- # 現行版本後,決定上傳改回正式走 upload_row 拖曳卡片(業主原本設計),
4879
- # 對話輸入框回歸純展示、不接上傳,所以這裡不再呼叫 chat_upload.submit()
4880
- # 綁定;函式本體保留,之後對話框如果有真正用途要接上傳,直接把下面
4881
- # 這段 wiring 恢復即可,邏輯不用重寫。
4882
- def on_chat_upload_submit(mm_value, chat_history, log_history, state):
4883
- files = (mm_value or {}).get("files", [])
4884
- yield from on_upload_confirm(files, chat_history, log_history, state)
4885
-
4886
  _ANALYSIS_START_MSG = (
4887
  "已收到,開始計算語言指標與統計分析,這個步驟可能需要數分鐘,請耐心等候……\n"
4888
  "(可於下方「技術執行紀錄」查看執行進度)"
 
101
  "upload_prompt": (
102
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
103
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分人工翻譯與機器翻譯。\n"
104
+ "3. 斷詞要求:必須先使用 [語料淬鍊](https://github.com/suhsiung/ckip_segmentation_sumin) 工具完成斷詞。\n"
105
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
106
  ),
107
  "desc": "焦點在人機差異的語言特徵。",
 
113
  "upload_prompt": (
114
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
115
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分全譯與編譯。\n"
116
+ "3. 斷詞要求:必須先使用 [語料淬鍊](https://github.com/suhsiung/ckip_segmentation_sumin) 工具完成斷詞。\n"
117
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
118
  ),
119
  "desc": "焦點在文本完整性和改寫策略。",
 
125
  "upload_prompt": (
126
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
127
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同群組的譯者。\n"
128
+ "3. 斷詞要求:必須先使用 [語料淬鍊](https://github.com/suhsiung/ckip_segmentation_sumin) 工具完成斷詞。\n"
129
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
130
  ),
131
  "desc": "焦點在個人風格差異。排除時代因素,討論譯者個人的語言偏好。",
 
137
  "upload_prompt": (
138
  "1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
139
  "2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同時期的譯本。\n"
140
+ "3. 斷詞要求:必須先使用 [語料淬鍊](https://github.com/suhsiung/ckip_segmentation_sumin) 工具完成斷詞。\n"
141
  "4. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。"
142
  ),
143
  "desc": "分析焦點在歷時變化、語言現代化、翻譯規範的演變。時間維度是核心變數。",
 
1377
  "done_steps": [], # 已完成步驟(1-based)
1378
  "current_step": None, # 目前執行步驟
1379
  "failed_step": None, # 執行失敗時卡在哪一步(1-based),成功時維持 None
1380
+ # 「分析運算」步驟底下三個子階段的即時狀態,供 Canvas 內容區塊分段呈現
1381
+ # (見 _build_analysis_html)。status: pending / running / done / failed;
1382
+ # line: 給人看的一句結果或失敗原因。由 run_computation_pipeline_streaming
1383
+ # 邊跑邊寫入。
1384
+ "analysis": {
1385
+ "idx": {"status": "pending", "line": ""},
1386
+ "stat": {"status": "pending", "line": ""},
1387
+ "report": {"status": "pending", "line": ""},
1388
+ },
1389
  }
1390
 
1391
 
 
1397
  # 格式驗證通過就直接把 current_step 設成「指標計算」,導致使用者在填表的整段
1398
  # 期間,步驟列都亮著「指標計算 進行中」的呼吸動畫,畫面在說電腦正在算,實際
1399
  # 上是在等人填表。收斂後每一步都對應一件使用者感知得到的事。
1400
+ # 2026-07-24 再收斂:8→6 之後又拿掉兩步。
1401
+ # ① 移除「格式驗證」:它在任何靜止狀態下都不會停留——通過時跟前一步在同一個
1402
+ # generator 內連續完成、中間沒有使用者動作;失敗時步驟列退回「文本上傳
1403
+ # 進行中」(要求重新上傳),也不停在格式驗證。它從來不對應一個使用者能看到
1404
+ # 的畫面狀態,逐行細節仍在技術執行紀錄裡。
1405
+ # ② 合併「指標計算/統計分析/報告產出」成單一「分析運算」:三段是同一個
1406
+ # generator 依序跑完的運算,中間沒有任何使用者操作關卡。三段各自有失敗
1407
+ # 可能,但「壞在哪一段、其他段有沒有救」這種細節,藥丸大小的步驟列講不
1408
+ # 清楚,改由 Canvas 的分析運算內容區塊分段呈現(見 _build_analysis_html)。
1409
  _STEPS = [
1410
  (1, "文本上傳"),
1411
+ (2, "語料說明"),
1412
+ (3, "分析運算"),
1413
+ ]
1414
+
1415
+ # 分析運算底下的三個子階段(key 用於 state["analysis"]、內容區塊分段、入口連結)
1416
+ _ANALYSIS_STAGES = [
1417
+ ("idx", "指標計算", "idx"),
1418
+ ("stat", "統計分析", "stat"),
1419
+ ("report", "報告撰寫", "report"),
1420
  ]
1421
 
1422
 
 
1435
  """
1436
  done_steps = done_steps or []
1437
 
1438
+ # 2026-07-24:原本全部完成後整條步驟列收起、只留產品名,理由是「把高度讓給
1439
+ # 結果內容」。但那是舊的「結果堆疊往下捲」模型下的決定;7-23 改成互斥頁面
1440
+ # 切換後,完成時若停在 AI 對話頁,Canvas 根本沒有結果內容,讓出來的是一片
1441
+ # 空白——步驟列一消失,畫面就整片空掉(千卉實測回報「連步驟列都消失了」)。
1442
+ # 改成完成後步驟列留在原地、全部標 ✓,收尾感與畫面穩定都靠這排 ✓ 維持,
1443
+ # Canvas 的分析運算內容區塊則負責承載完成摘要。
 
 
 
1444
 
1445
  pills = []
1446
  for num, name in _STEPS:
 
1675
 
1676
  results_paths = []
1677
  anal = None
1678
+ A = state["analysis"] # 三個子階段的即時狀態,邊跑邊寫,Canvas 內容區塊據此渲染
1679
 
1680
+ # ── 指標計算 ─────────────────────────────────────────────────────────────
1681
+ A["idx"]["status"] = "running"
1682
+ yield "【指標計算】正在計算文本指標……", False, state
1683
  try:
1684
  idx_df, xlsx_path, csv_path = run_indicator_calculation(
1685
  state["cleaned_files"], output_dir, _RESOURCES
1686
  )
1687
  results_paths.extend([str(xlsx_path), str(csv_path)])
1688
+ n_ind = max(len(idx_df.columns) - sum(c in idx_df.columns for c in ("Text","Type1","Type2")), 0)
1689
+ A["idx"]["status"] = "done"
1690
+ A["idx"]["line"] = f"{len(idx_df)} 個文本 × {n_ind} 項語言指標"
1691
  yield f" [OK] 指標計算完成({len(idx_df)} 個文本)", False, state
1692
  except Exception as e:
1693
+ A["idx"]["status"] = "failed"
1694
+ A["idx"]["line"] = f"計算失敗:{e}"
1695
  state["state"] = "COMPLETE"
1696
+ state["output_files"] = results_paths
1697
+ yield f"[ERROR] 指標計算失敗:{e}", True, state
1698
  return
1699
 
1700
+ # ── 統計分析 ─────────────────────────────────────────────────────────────
1701
+ A["stat"]["status"] = "running"
1702
+ yield "【統計分析】正在執行統計分析……", False, state
1703
  try:
1704
  anal = run_statistical_analysis(csv_path, output_dir)
1705
  for key in ["desc_path","diff_path","imp_path","imp_fig_path",
 
1712
  k: str(v) if isinstance(v, Path) else v
1713
  for k, v in anal.items() if k != "df"
1714
  }
1715
+ A["stat"]["status"] = "done"
1716
+ A["stat"]["line"] = (
1717
+ f"{anal['sig_count']}/{anal['total_indicators']} 個顯著指標"
1718
+ f"(p < {ALPHA})、分類準確率 {anal['accuracy']:.4f}"
1719
+ )
1720
  yield (
1721
  f" [OK] 統計分析完成({anal['sig_count']}/{anal['total_indicators']} 個顯著指標,"
1722
  f"隨機森林準確率 {anal['accuracy']:.4f})"
1723
  ), False, state
1724
  except Exception as e:
1725
+ A["stat"]["status"] = "failed"
1726
+ A["stat"]["line"] = f"分析失敗:{e}"
1727
  state["state"] = "COMPLETE"
1728
  state["output_files"] = results_paths
1729
+ yield f"[ERROR] 統計分析失敗:{e}", True, state
 
1730
  return
1731
 
1732
+ # ── 報告撰寫 ─────────────────────────────────────────────────────────────
1733
+ A["report"]["status"] = "running"
1734
+ yield "【報告撰寫】主 Agent 正在撰寫研究報告(約需 30-90 秒)……", False, state
1735
  try:
1736
  rpt = run_report_writing(
1737
  scenario_key = state["scenario"],
 
1741
  output_dir = output_dir,
1742
  )
1743
  results_paths.extend([str(rpt["docx_path"]), str(rpt["txt_path"])])
1744
+ A["report"]["status"] = "done"
1745
+ A["report"]["line"] = "研究報告已產出"
1746
  yield " [OK] 研究報告撰寫完成", False, state
1747
  except Exception as e:
1748
+ # A1(畫面說謊):報告失敗以前只 yield 一則 WARNING 進技術執行紀錄,然後
1749
+ # 照常往下走到「全部分析完成」,步驟列還標 ✓——使用者拿到一份少了報告卻
1750
+ # 顯示全部完成的結果。改成明確標記 failed,讓內容區塊與頂部步驟列都據實
1751
+ # 呈現;指標與統計已完成、可下載,這點也講清楚不讓人以為全毀。
1752
+ A["report"]["status"] = "failed"
1753
+ A["report"]["line"] = f"報告產出失敗:{e}"
1754
+ yield f" [WARNING] 研究報告撰寫失敗({e}),指標與統計結果不受影響,仍可查看與下載。", False, state
1755
 
1756
  # ── 完成 ─────────────────────────────────────────────────────────────────
1757
  state["output_files"] = results_paths
1758
  state["state"] = "COMPLETE"
1759
 
1760
+ report_ok = A["report"]["status"] == "done"
1761
  sig_info = ""
1762
  if anal:
1763
  sig_info = (
1764
  f"顯著指標:**{anal['sig_count']}/{anal['total_indicators']}** 個(p < {ALPHA})。\n"
1765
  f"隨機森林分類準確率:**{anal['accuracy']:.4f}**。\n"
1766
  )
1767
+ if report_ok:
1768
+ summary = (
1769
+ f"**分析完成!**\n\n"
1770
+ f"共分析 **{len(state['cleaned_files'])}** 個文本。\n"
1771
+ f"{sig_info}\n"
1772
+ f"請點選左側導覽列查看各項結果,或使用下載捷徑取回檔案。"
1773
+ )
1774
+ else:
1775
+ summary = (
1776
+ f"**分析完成(研究報告未產出)。**\n\n"
1777
+ f"共分析 **{len(state['cleaned_files'])}** 個文本。\n"
1778
+ f"{sig_info}\n"
1779
+ f"LLM 研究報告因故未能產出,但**指標與統計結果均已完成**,"
1780
+ f"請點選左側導覽列查看,或使用下載捷徑取回檔案。"
1781
+ )
1782
  yield summary, True, state
1783
 
1784
 
 
2208
  color: #fff;
2209
  }
2210
  /* ── 3. TABS ───────────────────────────────────────────────────── */
2211
+ /* 2026-07-24(E1):這裡原本有一整組 `.tabs > .tab-nav` sticky/背景/hover/
2212
+ selected 樣式,但這版 Gradio 的實際 DOM 是 `.tabs > .tab-wrapper >
2213
+ .tab-container`、沒有任何 `.tab-nav` 節點,那組規則從寫下來就是死規則,
2214
+ 已整組移除。 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2215
  .tabitem { padding: 1.5rem 1.25rem !important; }
2216
+ /* 2026-07-24(C1):統計比較分析的四個子頁改由左側導覽列驅動後,Canvas 內
2217
+ 這排分頁按鈕列(敘述統計|差異性分析|…)跟左欄四個項目是同一組選項的兩套
2218
+ UI,重複。整排在視覺上隱藏,但 gr.Tabs 結構保留——巢狀分頁的切換仍靠 JS 去
2219
+ 點這排按鈕(見 bindResultNav 的 tabButtons.click)。
2220
+ 踩坑(連踩兩種隱藏法):
2221
+ ① display:none —— .click() 完全不觸發切換,這版 Gradio 的 Svelte on:click
2222
+ 對移出渲染樹的元素不反應。
2223
+ ② 標準 visually-hidden(position:absolute + width:1px + clip)—— 也失敗,
2224
+ 但原因不同:把 wrapper 縮成 1px 寬會觸發 Gradio 自己的分頁溢出邏輯,把
2225
+ 四個分頁全塞進 overflow dropdown,可見容器只剩一顆溢出選單鈕,JS 選擇器
2226
+ (抓可見容器的 > button)就抓不到分頁按鈕了。
2227
+ 正解:必須保留 width:100%,Gradio 才會把四個分頁留在可見容器、不觸發溢出;
2228
+ 只用 height:0 + overflow:hidden + opacity:0 做視覺隱藏(不動寬度、不改
2229
+ position)。按鈕高度歸零但仍在渲染樹、寬度正常,.click() 照常切換。
2230
+ (實測:四顆按鈕在位、點第 4 顆→集群分析、點第 1 顆→敘述統計,正確。) */
2231
+ #canvas_stat .tabs > .tab-wrapper {
2232
+ height: 0 !important;
2233
+ min-height: 0 !important;
2234
+ overflow: hidden !important;
2235
+ opacity: 0 !important;
2236
+ margin: 0 !important;
2237
+ padding: 0 !important;
2238
  }
2239
 
2240
  /* ── 4. CHATBOT ────────────────────────────────────────────────── */
 
3096
  從原本的 gr.Tab()(自帶 .tabitem padding)改成 gr.Column,這裡補上
3097
  跟上面 upload-zone/metadata-zone 一致的垂直間距,避免緊貼在一起 */
3098
  .canvas-result { margin-top: 1.2rem !important; }
3099
+ /* 2026-07-24:「分析運算」內容區塊——運算中顯示三個子階段進度,運算後變成
3100
+ 分段摘要(標題+狀態+一句結論+查看入口)。取代舊的單行 .computing-zone
3101
+ 「運算中…(步驟 N/3)」提示(元件與 elem_id 沿用 computing_zone)。 */
3102
+ .analysis-panel {
 
 
 
3103
  border: 1px solid var(--c-border) !important;
3104
  border-radius: 14px !important;
3105
  background: var(--c-surface2) !important;
3106
+ padding: 1.2rem 1.5rem 1.4rem !important;
3107
  margin-top: 1.2rem !important;
3108
+ }
3109
+ .analysis-panel-title {
3110
+ margin: 0 0 1rem !important;
3111
+ font-size: 1.05rem !important;
3112
+ font-weight: 700 !important;
3113
+ color: var(--c-text) !important;
3114
+ }
3115
+ .analysis-row {
3116
+ display: flex;
3117
+ flex-direction: column;
3118
+ gap: .25rem;
3119
+ padding: .7rem 0;
3120
+ border-top: 1px solid var(--c-border);
3121
+ }
3122
+ .analysis-row:first-of-type { border-top: none; padding-top: 0; }
3123
+ .analysis-row-head { display: flex; align-items: center; gap: .55rem; }
3124
+ .analysis-row-title {
3125
  font-size: var(--font-size-base) !important;
3126
  font-weight: 600 !important;
3127
+ color: var(--c-text) !important;
3128
  }
3129
+ .analysis-row-body {
3130
+ padding-left: 1.65rem; /* 對齊圖示右側,讓結論文字跟標題文字左緣一致 */
3131
+ font-size: .84rem;
3132
+ color: var(--c-text-soft);
3133
+ display: flex;
3134
+ flex-wrap: wrap;
3135
+ align-items: center;
3136
+ gap: .3rem .9rem;
3137
+ }
3138
+ .analysis-row--pending .analysis-row-title,
3139
+ .analysis-row--pending .analysis-row-body { color: var(--c-text-muted) !important; }
3140
+ .analysis-entry {
3141
+ color: var(--c-accent-strong) !important;
3142
+ font-weight: 600;
3143
+ cursor: pointer;
3144
+ text-decoration: none;
3145
+ }
3146
+ .analysis-entry:hover { text-decoration: underline; }
3147
+ /* 狀態記號:完成 ✓、失敗 !、等待中 •,尺寸統一,跟旋轉動畫的 spinner 佔位一致 */
3148
+ .analysis-mark {
3149
+ flex: none;
3150
+ width: 20px; height: 20px;
3151
+ display: inline-flex; align-items: center; justify-content: center;
3152
+ border-radius: 50%;
3153
+ font-size: .72rem; font-weight: 700; line-height: 1;
3154
+ }
3155
+ .analysis-mark--done { background: rgba(34,197,94,.15); color: #16a34a; }
3156
+ .analysis-mark--failed { background: rgba(220,38,38,.15); color: #dc2626; }
3157
+ .analysis-mark--pending{ background: var(--c-surface); color: var(--c-text-muted);
3158
+ border: 1px solid var(--c-border); }
3159
+ /* 轉圈動畫:踩坑(2026-07-24)——border 這個「簡寫」在這版 Gradio 的
3160
+ pipeline 下整條不會生效(跟本檔 background 簡寫的踩坑同一類),實測
3161
+ border-width 算出來是 0px、圓圈整個沒被畫出來,只有同規則裡的
3162
+ border-top-color 長寫法有作用。改成全部拆成長寫法子屬性就正常。 */
3163
+ .analysis-spinner {
3164
  flex: none;
3165
  width: 20px; height: 20px;
3166
  border-radius: 50%;
3167
+ border-width: 2.5px !important;
3168
+ border-style: solid !important;
3169
+ border-color: rgba(var(--c-accent-rgb),.25) !important;
3170
+ border-top-color: var(--c-accent) !important;
3171
  animation: computingSpin .8s linear infinite;
3172
  }
3173
  @keyframes computingSpin { to { transform: rotate(360deg); } }
3174
+ /* 2026-07-24(B2):Gradio 對「正在串流輸出」的元件(block wrapper)自動加
3175
+ .generating class,帶一圈會呼吸的藍色邊框動畫。頂部步驟列與分析運算內容
3176
+ 區塊都是同一個 generator 的輸出,各被套一圈,畫面上兩個藍框一起閃、很礙眼。
3177
+ 這個 app 不需要這個效果,整站關掉。 */
3178
+ .generating {
3179
+ border-color: transparent !important;
3180
+ animation: none !important;
3181
+ box-shadow: none !important;
3182
+ }
3183
+ /* 2026-07-24(A2):結果導覽項目在對應結果產出前是 disabled(見 create_app
3184
+ 的 interactive=False 與 _populate_tabs 的閘門)。給明確的灰階+禁用游標,
3185
+ 讓「還不能點」一眼看得出來,不是壞掉。 */
3186
+ #result_nav_col .result-nav-btn:disabled,
3187
+ #result_nav_col .result-nav-btn[disabled] {
3188
+ opacity: .38 !important;
3189
+ cursor: not-allowed !important;
3190
+ }
3191
  .upload-area, [data-testid="droparea"] {
3192
  border: 2px dashed rgba(var(--c-accent-rgb),.28) !important;
3193
  border-radius: 12px !important;
 
3589
  if (btn) btn.click();
3590
  });
3591
 
3592
+ /* 分析運算內容區塊的「查看…」入口(2026-07-24):純畫面操作,點了
3593
+ 等同點左側對應的結果導覽項目,沿用同一套 document 事件代理——內容
3594
+ 區塊是 gr.HTML,會隨 gr.update 重建,掛在 document 上不用重綁。 */
3595
+ document.addEventListener('click', function (e) {
3596
+ var entry = e.target.closest && e.target.closest('.analysis-entry');
3597
+ if (!entry) return;
3598
+ var navBtn = document.getElementById(entry.getAttribute('data-nav'));
3599
+ if (navBtn) navBtn.click();
3600
+ });
3601
+
3602
  /* 2026-07-23:頁面切換模型。nav-col 的每個項目現在是真正的
3603
  「頁面」——結果類項目(語言指標分析/統計比較分析/LLM輔助分析/
3604
  技術執行紀錄)點進去時互斥顯示、收起對話欄、Canvas 佔滿寬度;
 
4160
  "AI對話", icon=str(BASE_DIR / "assets" / "icons" / "home.svg"),
4161
  elem_id="nav_top_btn", elem_classes=["result-nav-btn"], variant="secondary",
4162
  )
4163
+ # 2026-07-24(A2):結果類導覽項目預設 interactive=False。
4164
+ # 運算完成前它們就已經隨 result_nav_col 顯示,但對應的
4165
+ # canvas_*_col 還是 Gradio visible=False,點下去只會切到一片
4166
+ # 空白(JS 只能清 display,蓋不過 Python 的 visible=False)。
4167
+ # 改成先 disable(灰階),由 _populate_tabs 依各結果是否真的
4168
+ # 產出來逐一啟用——LLM 輔助分析尤其:報告失敗時它維持灰的,
4169
+ # 剛好是失敗訊號的第二道確認。AI 對話與技術執行紀錄不在此列
4170
+ # (前者永遠可回、後者從第一秒就有內容)。
4171
  nav_idx_btn = gr.Button(
4172
  "語言指標分析", icon=str(BASE_DIR / "assets" / "icons" / "trending-up.svg"),
4173
  elem_id="nav_idx_btn", elem_classes=["result-nav-btn"], variant="secondary",
4174
+ interactive=False,
4175
  )
4176
  # 2026-07-24 千卉決定:拿掉「統計比較分析」這層 Accordion
4177
  # (標題文字與收合效果都不要),四個子頁直接跟其他導覽項目
 
4183
  # 見 _APP_HEAD_SCRIPT 的 applyActivePage()。
4184
  nav_stat_sub_0 = gr.Button(
4185
  "敘述統計", icon=str(BASE_DIR / "assets" / "icons" / "table-cells.svg"),
4186
+ elem_id="nav_stat_sub_0", elem_classes=["result-nav-btn"], variant="secondary",
4187
+ interactive=False)
4188
  nav_stat_sub_1 = gr.Button(
4189
  "差異性分析", icon=str(BASE_DIR / "assets" / "icons" / "scale.svg"),
4190
+ elem_id="nav_stat_sub_1", elem_classes=["result-nav-btn"], variant="secondary",
4191
+ interactive=False)
4192
  nav_stat_sub_2 = gr.Button(
4193
  "監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "academic-cap.svg"),
4194
+ elem_id="nav_stat_sub_2", elem_classes=["result-nav-btn"], variant="secondary",
4195
+ interactive=False)
4196
  nav_stat_sub_3 = gr.Button(
4197
  "非監督式學習", icon=str(BASE_DIR / "assets" / "icons" / "squares-2x2.svg"),
4198
+ elem_id="nav_stat_sub_3", elem_classes=["result-nav-btn"], variant="secondary",
4199
+ interactive=False)
4200
  nav_report_btn = gr.Button(
4201
  "LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
4202
  elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
4203
+ interactive=False,
4204
  )
4205
  # ── 下載捷徑(2026-07-24)────────────────────────────
4206
  # 上面四項是「去看結果」,這四顆是「把結果帶走」,性質
4207
  # 不同所以用分隔線隔開。原本使用者要拿到全部產出得點進
4208
  # 三個結果頁各按各的下載鈕(共 15 個檔案),這裡各打成
4209
  # 一包 zip。內容為空的那幾包不會產生,對應按鈕就不出現
4210
+ # (例如 LLM 報告失敗時「下載研究報告」不會亮出來)。
4211
+ # 分隔線與四顆按鈕整組包進 nav_dl_group:沒有任何一包 zip
4212
+ # 時整組隱藏(否則分隔線會孤零零留在畫面上,還跟下面技術
4213
+ # 執行紀錄的分隔線貼在一起變兩條);有 zip 時整組顯示,
4214
+ # 個別按鈕再依自己那包是否存在決定。群組與個別可見性都由
4215
+ # _populate_tabs 設定。
4216
+ with gr.Column(visible=False, elem_id="nav_dl_group",
4217
+ elem_classes=["result-nav-dl-group"]) as nav_dl_group:
4218
+ gr.HTML('<div class="result-nav-divider"></div>')
4219
+ nav_dl_idx_btn = gr.DownloadButton(
4220
+ "下載指標數據", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4221
+ visible=False, elem_id="nav_dl_idx_btn",
4222
+ elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4223
+ )
4224
+ nav_dl_stat_btn = gr.DownloadButton(
4225
+ "下載統計圖表", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4226
+ visible=False, elem_id="nav_dl_stat_btn",
4227
+ elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4228
+ )
4229
+ nav_dl_report_btn = gr.DownloadButton(
4230
+ "下載研究報告", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4231
+ visible=False, elem_id="nav_dl_report_btn",
4232
+ elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4233
+ )
4234
+ nav_dl_all_btn = gr.DownloadButton(
4235
+ "下載完整分析資料", icon=str(BASE_DIR / "assets" / "icons" / "download.svg"),
4236
+ visible=False, elem_id="nav_dl_all_btn",
4237
+ elem_classes=["result-nav-btn", "result-nav-dl"], variant="secondary",
4238
+ )
4239
 
4240
  # 2026-07-23:技術執行紀錄從手風琴轉正成獨立頁面(原本
4241
  # exec_log_accordion 在 canvas-col 底部,現在跟其他三個
 
4261
  # (見下方 upload_row),對話框目前也沒有其他打字用途
4262
  # (全站掃過沒有任何地方會解析使用者自由輸入文字),暫時
4263
  # 只留著當畫面元素,不接 sources=["upload"],避免顯示
4264
+ # 附加檔案的迴紋針卻沒有作用。(原本的 chat_upload.submit()
4265
+ # 轉接函式 on_chat_upload_submit 已於 2026-07-24 當死碼移除,
4266
+ # 之後要復活直接重接 .submit() 即可,見該處註解。)
4267
  with gr.Column(visible=True, elem_id="chat_upload_zone", elem_classes=["upload-zone-plain"]) as chat_upload_col:
4268
  chat_upload = gr.MultimodalTextbox(
4269
  file_types=[".txt"],
 
4356
  meta_cancel_btn = gr.Button("略過", scale=1, variant="secondary")
4357
  meta_confirm_btn = gr.Button("確認並開始分析", scale=2, variant="primary")
4358
 
4359
+ # 「分析運算」內容區塊(2026-07-24 由 Phase 8-D 的單行運算中
4360
+ # 提示擴充而來):運算中顯示三個子階段進度+轉圈動畫,運算後
4361
+ # 變成分段摘要(標題+狀態+一句結論+查看入口)。空字串=運算
4362
+ # 尚未開始、不顯示,見 _build_analysis_html/_computing_update。
4363
  computing_html = gr.HTML(value="", elem_id="computing_zone")
4364
 
4365
  # Phase 8-C:結果內容從 tab2/tab3/tab6(原本 gr.Tabs() 分頁頁面)搬進
 
4472
  canvas_exec_col, # 25 技術執行紀錄轉正成獨立頁面(2026-07-23)
4473
  nav_dl_idx_btn, nav_dl_stat_btn, # 26-27 導覽列下載捷徑(2026-07-24)
4474
  nav_dl_report_btn, nav_dl_all_btn, # 28-29
4475
+ nav_dl_group, # 30 下載捷徑整組(分隔線+四顆)可見性
4476
+ nav_idx_btn, nav_stat_sub_0, nav_stat_sub_1, # 31-33 結果導覽 interactive 閘門(A2)
4477
+ nav_stat_sub_2, nav_stat_sub_3, nav_report_btn, # 34-36
4478
  ]
4479
  _N_TAB = len(_TAB_OUTPUTS)
4480
 
 
4492
  if state is None:
4493
  return R
4494
 
4495
+ # 下載捷徑(26-29)與整組容器(30)是少數必須「明確設回隱藏」的
4496
+ # 輸出:其他結果元件都住在 canvas_*_col 裡,重置時整個 Column 被藏
4497
+ # 起來、殘值看不到;這幾顆住在 result_nav_col,而 result_nav_col 會
4498
+ # 在下次選定情境時重新顯示,不主動歸零的話上一輪的 zip 會跟著冒出
4499
+ # 來。結果導覽閘門(31-36)同理,每次都先設回 disable,有結果再開。
4500
  for _i in (26, 27, 28, 29):
4501
  R[_i] = gr.update(visible=False, value=None)
4502
+ R[30] = gr.update(visible=False) # nav_dl_group(Column,無 value)
4503
+ for _i in (31, 32, 33, 34, 35, 36):
4504
+ R[_i] = gr.update(interactive=False)
4505
 
4506
  output_dir = Path(state["session_dir"]) / "output"
4507
  uploaded = state.get("uploaded_files", [])
 
4644
  for _i, _key in ((26, "idx"), (27, "stat"), (28, "report"), (29, "all")):
4645
  if zips.get(_key):
4646
  R[_i] = gr.update(value=zips[_key], visible=True)
4647
+ # 只要有任何一包 zip,下載捷徑整組(含分隔線)就顯示(B3)
4648
+ if zips:
4649
+ R[30] = gr.update(visible=True)
4650
 
4651
+ # ── 顯示各分頁 Tab(index 20-22)+結果導覽閘門(31-36,A2)──────
4652
  any_tab_ready = False
4653
  if csv_path.exists():
4654
  R[20] = gr.update(visible=True) # 語言指標分析
4655
+ R[31] = gr.update(interactive=True) # nav_idx_btn
4656
  any_tab_ready = True
4657
+ stat_ready = desc_path.exists() or imp_path.exists() \
4658
+ or any(p.exists() for p in [dendro_path, elbow_path, pca_fig_path])
4659
+ if stat_ready:
4660
  R[21] = gr.update(visible=True) # 統計比較分析
4661
+ # 統計四個子頁共用同一頁,統計一產出就四個一起開
4662
+ for _i in (32, 33, 34, 35):
4663
+ R[_i] = gr.update(interactive=True)
4664
  any_tab_ready = True
4665
  if txt_files or docx_files:
4666
  R[22] = gr.update(visible=True) # 研究報告
4667
+ R[36] = gr.update(interactive=True) # nav_report_btn(報告失敗時維持 disable)
4668
  any_tab_ready = True
4669
 
4670
  # 分頁內容(大表格、多張圖)從「不可見」變「可見」到實際整理進畫面
4671
  # 中間有一段沒有任何提示的空檔,加上這段運算本身就要跑數分鐘,使用
4672
  # 者常誤以為系統當掉了——這裡明確用 toast 告知「現在真的可以點了」
4673
  if any_tab_ready:
4674
+ gr.Info("分析結果已就緒,請點選左側導覽列查看各項結果!")
4675
 
4676
  return R
4677
 
 
4726
  # 4/5/6,這個字典的 key 是 _STEPS 的步驟編號,改步驟列一定要一起改),
4727
  # 不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不顯示、不用另外
4728
  # 接一個 visibility 輸出,wiring 只多一個輸出點���
4729
+ # 2026-07-24:原本這裡是「運算中…(步驟 N/3)」單行提示。它跟步驟列
4730
+ # 講同一件事(哪一步)而且那個 N/3 會給錯期待(讓人以為要熬三段,實際
4731
+ # 上前兩段一閃而過、時間幾乎全在報告撰寫)。改成 _build_analysis_html:
4732
+ # 一張分段的「分析運算」內容區塊,運算中顯示每段進度、運算後變成分段
4733
+ # 摘要(標題+狀態+一句結論+查看入口),空間比藥丸大得多,報告失敗時
4734
+ # 也能據實說明「壞的只是報告、其他不受影響」。同一塊 HTML 從頭用到尾,
4735
+ # 取代舊的 computing_html 顯示內容(元件與 elem_id 沿用不變)。
4736
+ # 每段「查看…」入口是純 JS 橋接(見 _APP_HEAD_SCRIPT 的 analysis-entry
4737
+ # 事件代理),點了等同點左側對應導覽項目,不走 Gradio queue。
4738
+ _ANALYSIS_ENTRY = {
4739
+ "idx": ("nav_idx_btn", "查看語言指標分析"),
4740
+ "stat": ("nav_stat_sub_0", "查看統計比較分析"),
4741
+ "report": ("nav_report_btn", "查看 LLM 輔助分析"),
4742
  }
4743
 
4744
+ def _build_analysis_html(state):
4745
  if not state:
4746
  return ""
4747
+ A = state.get("analysis") or {}
4748
+ if not A or all(A.get(k, {}).get("status", "pending") == "pending"
4749
+ for k, _, _ in _ANALYSIS_STAGES):
4750
+ return "" # 運算還沒開始,不顯示
4751
+
4752
+ rows = []
4753
+ for key, title, _ in _ANALYSIS_STAGES:
4754
+ info = A.get(key, {})
4755
+ status = info.get("status", "pending")
4756
+ line = info.get("line", "")
4757
+ if status == "running":
4758
+ icon = '<span class="analysis-spinner" aria-hidden="true"></span>'
4759
+ line = line or "處理中……"
4760
+ elif status == "done":
4761
+ icon = '<span class="analysis-mark analysis-mark--done">✓</span>'
4762
+ elif status == "failed":
4763
+ icon = '<span class="analysis-mark analysis-mark--failed">!</span>'
4764
+ else: # pending
4765
+ icon = '<span class="analysis-mark analysis-mark--pending">•</span>'
4766
+ line = line or "等待中"
4767
+ entry = ""
4768
+ if status == "done" and key in _ANALYSIS_ENTRY:
4769
+ nav_id, label = _ANALYSIS_ENTRY[key]
4770
+ entry = (f'<a class="analysis-entry" role="button" tabindex="0" '
4771
+ f'data-nav="{nav_id}">{label} →</a>')
4772
+ rows.append(
4773
+ '<div class="analysis-row analysis-row--' + status + '">'
4774
+ f'<div class="analysis-row-head">{icon}'
4775
+ f'<span class="analysis-row-title">{title}</span></div>'
4776
+ f'<div class="analysis-row-body">{line}{entry}</div>'
4777
+ '</div>'
4778
+ )
4779
  return (
4780
+ '<div class="analysis-panel">'
4781
+ '<h2 class="analysis-panel-title">分析運算</h2>'
4782
+ + "".join(rows) +
4783
  '</div>'
4784
  )
4785
 
4786
  def _computing_update(state):
4787
+ return gr.update(value=_build_analysis_html(state))
4788
 
4789
  # ── 文字送出(streaming generator)──────────────────────────────────
4790
  # ── 研究情境選擇(封閉式固定四選一,按鈕直接觸發,不再解析打字輸入)──
 
4895
  yield chat_history, log_history, state, gr.update(visible=True), gr.update()
4896
  return
4897
 
4898
+ # 文本上傳(step 1):開始處理即關閉上傳彈窗,避免遮罩擋住操作
4899
  state["current_step"] = 1
4900
+ log_history = _log_append(log_history, "⏳ 接收上傳檔案中……")
4901
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4902
 
4903
  recv_msg, state = handle_uploaded_files(files, state)
4904
  chat_history = chat_history + [_assistant_msg(recv_msg)]
4905
+ log_history = _log_append(log_history, f"✅ 已接收 {len(files)} 個檔案")
 
 
4906
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4907
 
4908
+ # 格式驗證:2026-07-24 起不再是獨立步驟(見 _STEPS 註解),只是「文本
4909
+ # 上傳」這一步內部的一道處理,逐行進技術執行紀錄,不動步驟列。step 1
4910
+ # 在格式驗證通過後才標 done——通過前維持「進行中」,失敗時原地留著要求
4911
+ # 重新上傳,畫面與步驟列不會互相矛盾。
4912
+ log_history = _log_append(log_history, "⏳ 格式驗證中……")
4913
  yield chat_history, log_history, state, gr.update(visible=False), _todo_update(state)
4914
 
4915
  check_msg, state, has_errors = run_format_check(state)
4916
  chat_history = chat_history + [_assistant_msg(check_msg)]
4917
 
4918
  if has_errors:
4919
+ log_history = _log_append(log_history, "⚠️ 格式不符,請重新上傳")
 
 
 
 
 
4920
  state["current_step"] = 1
4921
  yield chat_history, log_history, state, gr.update(visible=True), _todo_update(state)
4922
  return
4923
 
4924
+ state["done_steps"] = list(set(state.get("done_steps", []) + [1]))
4925
+ state["current_step"] = 2
4926
+ log_history = _log_append(log_history, "✅ 格式驗證通過")
4927
+ # 格式通過後:顯示 metadata 說明表單(語料說明=step 2),等使用者填寫
4928
  state["state"] = "METADATA_INPUT"
4929
  meta_hint = "請填寫語料 Metadata 說明(原文資訊、群組層與個體層),完成後點「確認並開始分析」。"
4930
  chat_history = chat_history + [_assistant_msg(meta_hint)]
 
4982
  # 2026-07-23:技術執行紀錄轉正成獨立頁面後,可見性只在情境選定
4983
  # (on_scenario_pick)與重置(on_reset_confirm)時切換一次,
4984
  # 這裡不用再逐步更新開合狀態/摘要標籤,改回傳 gr.update() 不動。
4985
+ # 「語料說明」(step 2) 在這裡收尾:confirm 與 skip 兩條路徑都會走進
4986
  # _run_computation,標記在這一處就好,不用在兩個 handler 各寫一次。
4987
+ # 2026-07-24 步驟列收斂到 3 步:語料說明=2、分析運算=3。分析運算是
4988
+ # 單一步驟,底下三個子階段的細狀態改由 state["analysis"] 承載(見
4989
+ # run_computation_pipeline_streaming),這裡不再逐段搬動 current_step,
4990
+ # 只在最後依子階段結果決定第 3 步是 done 還是 failed。
4991
+ state["done_steps"] = list(set(state.get("done_steps", []) + [2]))
4992
+ state["current_step"] = 3
4993
+ log_history = _log_append(log_history, "⏳ 分析運算開始……")
4994
  yield (chat_history, log_history, state, _todo_update(state),
4995
  gr.update(), _computing_update(state))
4996
  for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
 
 
 
 
 
 
 
 
 
 
 
 
4997
  log_history = _log_append(log_history, prog_msg)
4998
  if is_final:
4999
  chat_history = chat_history + [_assistant_msg(prog_msg)]
5000
+ # 三個子階段任一失敗 → 第 3 步標紅(頂部步驟列據實呈現,
5001
+ # 內容區塊講清楚壞在哪一段);全部成功才標 ✓。
5002
+ stages_failed = any(
5003
+ state["analysis"][k]["status"] == "failed" for k, _, _ in _ANALYSIS_STAGES
5004
+ )
5005
+ state["current_step"] = None
5006
+ if stages_failed:
5007
+ state["failed_step"] = 3
5008
+ else:
5009
+ state["done_steps"] = list(set(state.get("done_steps", []) + [3]))
5010
  yield (chat_history, log_history, state, _todo_update(state),
5011
  gr.update(), _computing_update(state))
5012
 
 
5045
  outputs=[upload_row, file_input],
5046
  )
5047
 
5048
+ # 2026-07-24(E2):這裡原本有 on_chat_upload_submit(Phase 7 的
5049
+ # MultimodalTextbox 轉接生成器),Phase 8 對話框改回純展示後就沒有任何
5050
+ # 地方呼叫它,屬死碼,已移除。之後若對話框要恢復接上傳,直接在 chat_upload
5051
+ # 的 .submit() 包一層拆 {"files":[...]} 丟給 on_upload_confirm 即可。
 
 
 
 
 
 
 
5052
  _ANALYSIS_START_MSG = (
5053
  "已收到,開始計算語言指標與統計分析,這個步驟可能需要數分鐘,請耐心等候……\n"
5054
  "(可於下方「技術執行紀錄」查看執行進度)"