Spaces:
Sleeping
運算階段重構:步驟列收斂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>
- app_agent.py +365 -199
|
@@ -101,7 +101,7 @@ SCENARIOS = {
|
|
| 101 |
"upload_prompt": (
|
| 102 |
"1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
|
| 103 |
"2. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分人工翻譯與機器翻譯。\n"
|
| 104 |
-
"3. 斷詞要求:必須先使用
|
| 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. 斷詞要求:必須先使用
|
| 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. 斷詞要求:必須先使用
|
| 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. 斷詞要求:必須先使用
|
| 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 |
-
|
| 1396 |
-
|
| 1397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 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 |
-
# ──
|
| 1661 |
-
|
|
|
|
| 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["
|
| 1671 |
-
yield f"[ERROR] 指標計算失敗:{e}
|
| 1672 |
return
|
| 1673 |
|
| 1674 |
-
# ──
|
| 1675 |
-
|
|
|
|
| 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 |
-
|
| 1696 |
-
yield f"[ERROR] 統計分析失敗:{e}\n\n請重新整理頁面,重新開始分析。", True, state
|
| 1697 |
return
|
| 1698 |
|
| 1699 |
-
# ──
|
| 1700 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1730 |
yield summary, True, state
|
| 1731 |
|
| 1732 |
|
|
@@ -2156,60 +2208,33 @@ footer {
|
|
| 2156 |
color: #fff;
|
| 2157 |
}
|
| 2158 |
/* ── 3. TABS ───────────────────────────────────────────────────── */
|
| 2159 |
-
/*
|
| 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 |
-
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
| 2212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
/*
|
| 3075 |
-
|
| 3076 |
-
|
| 3077 |
-
.
|
| 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:
|
| 3085 |
margin-top: 1.2rem !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3086 |
font-size: var(--font-size-base) !important;
|
| 3087 |
font-weight: 600 !important;
|
| 3088 |
-
color: var(--c-text
|
| 3089 |
}
|
| 3090 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3091 |
flex: none;
|
| 3092 |
width: 20px; height: 20px;
|
| 3093 |
border-radius: 50%;
|
| 3094 |
-
border: 2.5px
|
| 3095 |
-
border-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 4096 |
-
|
| 4097 |
-
|
| 4098 |
-
|
| 4099 |
-
|
| 4100 |
-
|
| 4101 |
-
|
| 4102 |
-
|
| 4103 |
-
|
| 4104 |
-
|
| 4105 |
-
|
| 4106 |
-
|
| 4107 |
-
|
| 4108 |
-
|
| 4109 |
-
|
| 4110 |
-
|
| 4111 |
-
|
| 4112 |
-
|
| 4113 |
-
|
| 4114 |
-
|
| 4115 |
-
|
| 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 |
-
#
|
| 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 |
-
# 不顯示,見
|
| 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 |
-
# 這
|
| 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 |
-
|
| 4525 |
-
|
|
|
|
| 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 |
-
|
| 4592 |
-
|
| 4593 |
-
|
| 4594 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4595 |
}
|
| 4596 |
|
| 4597 |
-
def
|
| 4598 |
if not state:
|
| 4599 |
return ""
|
| 4600 |
-
|
| 4601 |
-
if not
|
| 4602 |
-
|
| 4603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4604 |
return (
|
| 4605 |
-
'<div class="
|
| 4606 |
-
'<
|
| 4607 |
-
|
| 4608 |
'</div>'
|
| 4609 |
)
|
| 4610 |
|
| 4611 |
def _computing_update(state):
|
| 4612 |
-
return gr.update(value=
|
| 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 |
-
#
|
| 4724 |
state["current_step"] = 1
|
| 4725 |
-
log_history = _log_append(log_history, "⏳
|
| 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 |
-
|
| 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 |
-
#
|
| 4736 |
-
|
|
|
|
|
|
|
|
|
|
| 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, "⚠️
|
| 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", []) + [
|
| 4754 |
-
state["current_step"] =
|
| 4755 |
-
log_history = _log_append(log_history, "✅
|
| 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
|
| 4815 |
# _run_computation,標記在這一處就好,不用在兩個 handler 各寫一次。
|
| 4816 |
-
|
| 4817 |
-
state["
|
| 4818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 4876 |
-
#
|
| 4877 |
-
#
|
| 4878 |
-
#
|
| 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 |
"(可於下方「技術執行紀錄」查看執行進度)"
|