Spaces:
Sleeping
Sleeping
修進工作區後左側導覽列消失、技術執行紀錄空白缺標題、下載捷徑提早缺件三個 bug
Browse files- result_nav_col/canvas_exec_col 第一次從 visible=False 翻 True 時,
Gradio 前端只認觸發它的 generator 第一個 yield 才會真的掛進 DOM;
on_start_analysis_show 原本延到第二個 yield 才設,導致「開始研究
分析」後左側導覽列只剩「回首頁」、硬點技術執行紀錄也是空白。搬到
第一個 yield,比照 on_scenario_pick 已驗證可行的寫法。
- 技術執行紀錄頁補上獨立的靜態標題,不再依賴逐行累加的 log 內容。
- 下載研究報告/下載完整分析資料兩顆導覽列捷徑,改成等 LLM 報告階段
真正跑到終態(done/failed)才生成與顯示,避免報告還在寫的時候就
把缺件的 zip 遞出去。
- app_agent.py +74 -26
app_agent.py
CHANGED
|
@@ -1668,18 +1668,23 @@ _ZIP_GROUPS = [
|
|
| 1668 |
]
|
| 1669 |
|
| 1670 |
|
| 1671 |
-
def _make_result_zips(output_dir: Path, grouped: dict) -> dict:
|
| 1672 |
"""把各組產出打包成 zip,回傳 {group_key: zip 路徑字串}。
|
| 1673 |
|
| 1674 |
grouped:{group_key: [檔案路徑, ...]},不含 "all"(由本函式自己合併)。
|
| 1675 |
只打包真實存在的檔案;某組完全沒有檔案就不產生該包(對應的下載捷徑
|
| 1676 |
也就不會出現),例如 LLM 報告失敗時「研究報告」那包不該憑空生出來。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
"""
|
| 1678 |
targets = {k: [f for f in v if Path(f).exists()] for k, v in grouped.items()}
|
| 1679 |
|
| 1680 |
# 整包 = 各組去重合併。只有一組有東西時,整包跟那一組內容完全一樣,
|
| 1681 |
# 多給一顆按鈕只會讓人猶豫該點哪個,這種情況不另外產生整包。
|
| 1682 |
-
if sum(1 for v in targets.values() if v) >= 2:
|
| 1683 |
seen = set()
|
| 1684 |
targets["all"] = [f for v in targets.values() for f in v
|
| 1685 |
if not (f in seen or seen.add(f))]
|
|
@@ -3119,6 +3124,14 @@ def create_app():
|
|
| 3119 |
# 跟其他三個結果頁一樣的 visible=True/False 語意,交給
|
| 3120 |
# applyActivePage() JS 決定要不要顯示。
|
| 3121 |
with gr.Column(visible=False, elem_id="canvas_exec", elem_classes=["canvas-result"]) as canvas_exec_col:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3122 |
exec_log = gr.Markdown(
|
| 3123 |
value="",
|
| 3124 |
elem_id="exec_log",
|
|
@@ -3303,19 +3316,30 @@ def create_app():
|
|
| 3303 |
# 下載清單只留敘述統計技術報告跟 LLM 輔助報告兩份,複製成使用者
|
| 3304 |
# 指定的檔名(保留原始檔案不動,避免影響其他邏輯依賴原檔名);
|
| 3305 |
# 原始 .txt 不提供下載,僅用於上方 R[18] 的文字顯示
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3306 |
dl_rpt = []
|
| 3307 |
-
if
|
| 3308 |
-
|
| 3309 |
-
|
| 3310 |
-
|
| 3311 |
-
|
| 3312 |
-
|
| 3313 |
-
|
| 3314 |
-
|
| 3315 |
-
|
| 3316 |
-
|
| 3317 |
-
|
| 3318 |
-
|
|
|
|
| 3319 |
if dl_rpt:
|
| 3320 |
R[19] = gr.update(value=dl_rpt, visible=True)
|
| 3321 |
|
|
@@ -3323,11 +3347,16 @@ def create_app():
|
|
| 3323 |
# 刻意沿用上面已經整理好的清單變數,不另外去 output_dir 掃檔案:
|
| 3324 |
# 掃出來的會含 LLM_Analysis-*.txt 這種只供畫面顯示、不提供下載的
|
| 3325 |
# 中間產物,也含前一次打包留下的 .zip 自己。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3326 |
zips = _make_result_zips(output_dir, {
|
| 3327 |
"idx": [str(csv_path)] + [str(p) for p in xlsx_list[-1:]],
|
| 3328 |
"stat": [str(p) for p in [desc_path, diff_path, box_path]] + imp_dl + cluster_dl,
|
| 3329 |
"report": dl_rpt,
|
| 3330 |
-
})
|
| 3331 |
state["zip_paths"] = zips
|
| 3332 |
for _i, _key in ((26, "idx"), (27, "stat"), (28, "report"), (29, "all")):
|
| 3333 |
if zips.get(_key):
|
|
@@ -3606,6 +3635,24 @@ def create_app():
|
|
| 3606 |
# yield 1:workspace_row 從 visible=False 翻回 True 的那一批(祖先
|
| 3607 |
# 翻轉),scenario_select_row 本來就要在這時候顯示 True。子元件的
|
| 3608 |
# 補償刻意留到下一個 yield,不跟這批同時處理(理由見上方大段註解)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3609 |
yield (
|
| 3610 |
gr.update(visible=False), # homepage_col
|
| 3611 |
gr.update(visible=True), # workspace_row
|
|
@@ -3613,8 +3660,8 @@ def create_app():
|
|
| 3613 |
gr.update(value=_build_header_html(current_step=1)), # app_header_html:站上第1步「研究選擇」
|
| 3614 |
state,
|
| 3615 |
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # upload_row/metadata_row/canvas_idx/stat/report 留到下一個 yield
|
| 3616 |
-
gr.update(), # result_nav_col
|
| 3617 |
-
gr.update(), # canvas_exec_col
|
| 3618 |
gr.update(), # nav_dl_group 留到第三個 yield
|
| 3619 |
)
|
| 3620 |
# yield 2(4-C 補償,緊接著上一個 yield,同一個 generator/連線):
|
|
@@ -3622,7 +3669,8 @@ def create_app():
|
|
| 3622 |
# (要等對應結果產出才 enable,見 create_app 的 interactive=False
|
| 3623 |
# 與 _populate_tabs 的閘門,這批不動那個閘門邏輯),改成「進工作區
|
| 3624 |
# 就顯示」只是讓使用者一開始就看得到完整的流程地圖,不是讓他們能
|
| 3625 |
-
# 提前點。
|
|
|
|
| 3626 |
yield (
|
| 3627 |
gr.update(), gr.update(), gr.update(), gr.update(), state,
|
| 3628 |
gr.update(visible=False), # upload_row
|
|
@@ -3630,18 +3678,18 @@ def create_app():
|
|
| 3630 |
gr.update(visible=False), # canvas_idx_col
|
| 3631 |
gr.update(visible=False), # canvas_stat_col
|
| 3632 |
gr.update(visible=False), # canvas_report_col
|
| 3633 |
-
gr.update(
|
| 3634 |
-
gr.update(
|
| 3635 |
gr.update(), # nav_dl_group 留到下一個 yield(見下方說明)
|
| 3636 |
)
|
| 3637 |
# yield 3:nav_dl_group(下載捷徑那組分隔線+四顆按鈕)住在
|
| 3638 |
# result_nav_col 裡面,是比 result_nav_col 更深一層的子元件。
|
| 3639 |
-
# result_nav_col
|
| 3640 |
-
#
|
| 3641 |
-
#
|
| 3642 |
-
#
|
| 3643 |
-
#
|
| 3644 |
-
#
|
| 3645 |
yield (
|
| 3646 |
gr.update(), gr.update(), gr.update(), gr.update(), state,
|
| 3647 |
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
|
|
|
| 1668 |
]
|
| 1669 |
|
| 1670 |
|
| 1671 |
+
def _make_result_zips(output_dir: Path, grouped: dict, allow_all: bool = True) -> dict:
|
| 1672 |
"""把各組產出打包成 zip,回傳 {group_key: zip 路徑字串}。
|
| 1673 |
|
| 1674 |
grouped:{group_key: [檔案路徑, ...]},不含 "all"(由本函式自己合併)。
|
| 1675 |
只打包真實存在的檔案;某組完全沒有檔案就不產生該包(對應的下載捷徑
|
| 1676 |
也就不會出現),例如 LLM 報告失敗時「研究報告」那包不該憑空生出來。
|
| 1677 |
+
|
| 1678 |
+
allow_all:呼叫端還有「必然會出現、但目前尚未跑完」的組別時傳
|
| 1679 |
+
False,即使目前已有 ≥2 組非空也先不合併「整包」——避免「完整分析
|
| 1680 |
+
資料」在報告撰寫階段還沒結束前,就用 idx+stat 兩組拼出一包名不副實
|
| 1681 |
+
的「完整」資料(2026-08-06 踩坑,見呼叫端 report_stage_done 註解)。
|
| 1682 |
"""
|
| 1683 |
targets = {k: [f for f in v if Path(f).exists()] for k, v in grouped.items()}
|
| 1684 |
|
| 1685 |
# 整包 = 各組去重合併。只有一組有東西時,整包跟那一組內容完全一樣,
|
| 1686 |
# 多給一顆按鈕只會讓人猶豫該點哪個,這種情況不另外產生整包。
|
| 1687 |
+
if allow_all and sum(1 for v in targets.values() if v) >= 2:
|
| 1688 |
seen = set()
|
| 1689 |
targets["all"] = [f for v in targets.values() for f in v
|
| 1690 |
if not (f in seen or seen.add(f))]
|
|
|
|
| 3124 |
# 跟其他三個結果頁一樣的 visible=True/False 語意,交給
|
| 3125 |
# applyActivePage() JS 決定要不要顯示。
|
| 3126 |
with gr.Column(visible=False, elem_id="canvas_exec", elem_classes=["canvas-result"]) as canvas_exec_col:
|
| 3127 |
+
# 千卉回報(2026-08-06):這頁一直缺大標題文字。原因是
|
| 3128 |
+
# 另外三個結果頁(語言指標分析/敘述統計/研究報告)的
|
| 3129 |
+
# 「### 標題」都是烤進 _populate_tabs 每次重新賦值的內容
|
| 3130 |
+
# 字串裡(一次性結果,整段換新);但這頁是逐行往下累加
|
| 3131 |
+
# 的執行 log(見 _log_append),不會整段重寫,所以從沒
|
| 3132 |
+
# 機會把標題烤進 exec_log 的值裡。改成跟內容分開、獨立
|
| 3133 |
+
# 一顆靜態 gr.Markdown,不受 log 累加邏輯影響。
|
| 3134 |
+
gr.Markdown("### 技術執行紀錄")
|
| 3135 |
exec_log = gr.Markdown(
|
| 3136 |
value="",
|
| 3137 |
elem_id="exec_log",
|
|
|
|
| 3316 |
# 下載清單只留敘述統計技術報告跟 LLM 輔助報告兩份,複製成使用者
|
| 3317 |
# 指定的檔名(保留原始檔案不動,避免影響其他邏輯依賴原檔名);
|
| 3318 |
# 原始 .txt 不提供下載,僅用於上方 R[18] 的文字顯示
|
| 3319 |
+
#
|
| 3320 |
+
# 千卉發現的踩坑(2026-08-06):final_rpts(敘述統計技術報告)
|
| 3321 |
+
# 是統計階段一完成就存在,早於 LLM 報告撰寫階段。若不設防,
|
| 3322 |
+
# 「報告撰寫」子階段還在跑的當下,這裡就會把只有統計報告、缺
|
| 3323 |
+
# LLM 報告的清單當成「研究報告」打包出去——使用者這時點下載,
|
| 3324 |
+
# 拿到的檔案內容不完整。改成整段等 report 子階段真正跑到終態
|
| 3325 |
+
# (done 或 failed,不再是 running/pending)才組清單;報告若
|
| 3326 |
+
# 失敗,final_rpts 仍在,讓使用者至少能拿到統計報告,不會被
|
| 3327 |
+
# 卡死拿不到任何東西。
|
| 3328 |
+
report_stage_done = state["analysis"].get("report", {}).get("status") in ("done", "failed")
|
| 3329 |
dl_rpt = []
|
| 3330 |
+
if report_stage_done:
|
| 3331 |
+
if final_rpts:
|
| 3332 |
+
ts = final_rpts[0].stem.split("Final_Report-")[-1]
|
| 3333 |
+
stats_dst = output_dir / f"TransPrism_stats_{ts}.docx"
|
| 3334 |
+
if not stats_dst.exists():
|
| 3335 |
+
shutil.copy(str(final_rpts[0]), str(stats_dst))
|
| 3336 |
+
dl_rpt.append(str(stats_dst))
|
| 3337 |
+
if docx_files:
|
| 3338 |
+
ts = docx_files[0].stem.split("LLM_Analysis-")[-1]
|
| 3339 |
+
llm_dst = output_dir / f"TransPrism_LLM Report_{ts}.docx"
|
| 3340 |
+
if not llm_dst.exists():
|
| 3341 |
+
shutil.copy(str(docx_files[0]), str(llm_dst))
|
| 3342 |
+
dl_rpt.append(str(llm_dst))
|
| 3343 |
if dl_rpt:
|
| 3344 |
R[19] = gr.update(value=dl_rpt, visible=True)
|
| 3345 |
|
|
|
|
| 3347 |
# 刻意沿用上面已經整理好的清單變數,不另外去 output_dir 掃檔案:
|
| 3348 |
# 掃出來的會含 LLM_Analysis-*.txt 這種只供畫面顯示、不提供下載的
|
| 3349 |
# 中間產物,也含前一次打包留下的 .zip 自己。
|
| 3350 |
+
#
|
| 3351 |
+
# allow_all=report_stage_done:「完整分析資料」整包同理,report
|
| 3352 |
+
# 子階段還在跑時,即使 idx/stat 兩組都已完成(湊到 ≥2 組的舊
|
| 3353 |
+
# 門檻),也不該急著把「完整」兩字掛出來卻漏掉報告——見上面
|
| 3354 |
+
# report_stage_done 的踩坑說明。
|
| 3355 |
zips = _make_result_zips(output_dir, {
|
| 3356 |
"idx": [str(csv_path)] + [str(p) for p in xlsx_list[-1:]],
|
| 3357 |
"stat": [str(p) for p in [desc_path, diff_path, box_path]] + imp_dl + cluster_dl,
|
| 3358 |
"report": dl_rpt,
|
| 3359 |
+
}, allow_all=report_stage_done)
|
| 3360 |
state["zip_paths"] = zips
|
| 3361 |
for _i, _key in ((26, "idx"), (27, "stat"), (28, "report"), (29, "all")):
|
| 3362 |
if zips.get(_key):
|
|
|
|
| 3635 |
# yield 1:workspace_row 從 visible=False 翻回 True 的那一批(祖先
|
| 3636 |
# 翻轉),scenario_select_row 本來就要在這時候顯示 True。子元件的
|
| 3637 |
# 補償刻意留到下一個 yield,不跟這批同時處理(理由見上方大段註解)。
|
| 3638 |
+
#
|
| 3639 |
+
# result_nav_col/canvas_exec_col 例外:2026-08-06 千卉實測發現,
|
| 3640 |
+
# 「元件第一次從 visible=False 翻成 True(此生第一次掛進 DOM)」
|
| 3641 |
+
# 這件事,只有在觸發它的 generator 的第一個 yield 裡設定才會真的
|
| 3642 |
+
# 生效——on_scenario_pick 的 result_nav_col/canvas_exec_col 就是
|
| 3643 |
+
# 這樣,在第一個 yield 就設 visible=True(見該函式),所以按鈕跟
|
| 3644 |
+
# 內容區塊真的會出現。這裡原本學 4-C 的做法把兩者延到下一個 yield
|
| 3645 |
+
# 才設,結果變成「Python 端送出了 visible=True,前端卻從頭到尾
|
| 3646 |
+
# 沒把這批按鈕跟內容掛進 DOM」——使用者點「開始研究分析」進入
|
| 3647 |
+
# 研究選擇畫面後,左側導覽列只剩「回首頁」,AI對話/技術執行紀錄
|
| 3648 |
+
# 等全部消失;就算硬點技術執行紀錄,右邊內容區塊也是一片空白
|
| 3649 |
+
# (canvas_exec_col 同樣沒掛進 DOM,不是只缺標題文字)。4-C 的
|
| 3650 |
+
# 「延一個 yield」技巧解的是另一種問題(曾經 True 過的元件被自動
|
| 3651 |
+
# 還原),不適用在「這輩子第一次要 True」的元件上,兩者不能套
|
| 3652 |
+
# 同一招。canvas_exec_col 跟著 result_nav_col 一起搬進第一個
|
| 3653 |
+
# yield,也符合 nav_exec_btn 建構時刻意不設 interactive=False
|
| 3654 |
+
# 的原始設計意圖(見該按鈕附近註解:「技術執行紀錄……從第一秒
|
| 3655 |
+
# 就有內容」,代表它從一開始就該是可點、有內容可看的狀態)。
|
| 3656 |
yield (
|
| 3657 |
gr.update(visible=False), # homepage_col
|
| 3658 |
gr.update(visible=True), # workspace_row
|
|
|
|
| 3660 |
gr.update(value=_build_header_html(current_step=1)), # app_header_html:站上第1步「研究選擇」
|
| 3661 |
state,
|
| 3662 |
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # upload_row/metadata_row/canvas_idx/stat/report 留到下一個 yield
|
| 3663 |
+
gr.update(visible=True), # result_nav_col:第一次掛載必須跟祖先同一個 yield,見上方說明
|
| 3664 |
+
gr.update(visible=True), # canvas_exec_col:同上,且從第一秒就該有內容可看
|
| 3665 |
gr.update(), # nav_dl_group 留到第三個 yield
|
| 3666 |
)
|
| 3667 |
# yield 2(4-C 補償,緊接著上一個 yield,同一個 generator/連線):
|
|
|
|
| 3669 |
# (要等對應結果產出才 enable,見 create_app 的 interactive=False
|
| 3670 |
# 與 _populate_tabs 的閘門,這批不動那個閘門邏輯),改成「進工作區
|
| 3671 |
# 就顯示」只是讓使用者一開始就看得到完整的流程地圖,不是讓他們能
|
| 3672 |
+
# 提前點。result_nav_col/canvas_exec_col 本身已在上一個 yield
|
| 3673 |
+
# 掛載完成,��裡不再重複設定(重複設 visible=True 無害,但沒必要)。
|
| 3674 |
yield (
|
| 3675 |
gr.update(), gr.update(), gr.update(), gr.update(), state,
|
| 3676 |
gr.update(visible=False), # upload_row
|
|
|
|
| 3678 |
gr.update(visible=False), # canvas_idx_col
|
| 3679 |
gr.update(visible=False), # canvas_stat_col
|
| 3680 |
gr.update(visible=False), # canvas_report_col
|
| 3681 |
+
gr.update(), # result_nav_col:已在第一個 yield 掛載完成,這裡不再重複設定
|
| 3682 |
+
gr.update(), # canvas_exec_col:同上,這裡不再重複設定
|
| 3683 |
gr.update(), # nav_dl_group 留到下一個 yield(見下方說明)
|
| 3684 |
)
|
| 3685 |
# yield 3:nav_dl_group(下載捷徑那組分隔線+四顆按鈕)住在
|
| 3686 |
# result_nav_col 裡面,是比 result_nav_col 更深一層的子元件。
|
| 3687 |
+
# result_nav_col 已在第一個 yield 翻成 visible=True,若 nav_dl_group
|
| 3688 |
+
# 太早跟著壓 False,會被同一種 still-mounted-snapback 蓋回舊值
|
| 3689 |
+
# (跟 metadata_row 那組是同一條地雷,只是換了一層祖先/子元件);
|
| 3690 |
+
# 這裡隔了兩個 yield 才動 nav_dl_group,比原本的一個 yield 間隔
|
| 3691 |
+
# 更保守安全,之後有 zip 產出時 _populate_tabs 會自己把它翻回
|
| 3692 |
+
# True,不受影響。
|
| 3693 |
yield (
|
| 3694 |
gr.update(), gr.update(), gr.update(), gr.update(), state,
|
| 3695 |
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|