Spaces:
Sleeping
排版重構 Phase 2:nav-col 真正的頁面切換模型
Browse files千卉確認的核心行為:情境選擇→上傳→格式驗證→Metadata→運算中這段流程
進行中,對話欄常駐顯示;點導覽列任何一個結果類項目(語言指標分析/
統計比較分析/LLM輔助分析/技術執行紀錄)互斥顯示、收起對話欄、
Canvas 佔滿寬度;點「AI對話」(原「回到上傳/表單」改名)對話欄彈回。
這是全新邏輯,取代 8-C/8-D「有資料就全部堆疊、使用者自己往下捲」的
舊行為(8-C 程式碼註解本來就把這個標成「8-D 待辦」,8-D 當時只做了
捲動+子分頁切換,沒真的做互斥頁面,這次補完)。
- 技術執行紀錄從手風琴(exec_log_accordion)轉正成獨立頁面
(canvas_exec_col),跟其他三個結果頁同一種 visible=True/False 語意,
不再需要 open/label 收合狀態邏輯,_accordion_update/_accordion_label
兩個函式跟著移除;nav-col 新增「技術執行紀錄」項目,跟主要三項用
分隔線+降低視覺權重區隔
- 純 JS 實作(applyActivePage(),_APP_HEAD_SCRIPT):在 .main-layout-row
設 data-page 屬性+chat-collapsed class,canvas-scroll 底下 7 個直屬
區塊按頁面歸屬只清除或強制 inline display:none,絕不強制顯示——
Python 的 visible=False(資料是否存在)永遠優先,沿用 8-E 驗證過的
inline style 優先權技巧。延續這批 nav 按鈕「刻意不接 Python .click()」
的既有設計原則,不新增 Gradio queue 往返
- 「開始新分析」reset 流程新增把頁面狀態重置回 chat(.main-layout-row
這個 DOM 節點在 Gradio 重置時不會銷毀重建,data-page 不會自動清空)
兩個新踩坑:
1. Gradio 的 Svelte reactivity 在元件 visible prop 改變時會整個重寫
inline style(不是疊加,是覆蓋),把 JS 手動加的 !important 覆寫
一併洗掉——運算完成那一刻 canvas_idx_col 被 Python 設 visible=True
就會把我的 display:none 蓋掉,混進當前頁面。修法沿用 _watchZone/
_syncMobileHamburger 同一套輪詢自我修正手法,不奢求一次設定永久有效
2. .main-layout-row(gr.Row 本身)從來沒設過 flex-wrap:nowrap——三欄
各自「內部」的 nowrap 不影響 row 怎麼排這三欄。平常剛好不超版所以
從沒被觸發過,這次 canvas-col 拿掉 max-width 上限後,寬表格把
intrinsic 寬度撐到跟 nav-col 放不下同一行,觸發 Gradio 預設的
flex-wrap:wrap,canvas-col 整個換到 nav-col 下面,頁面被撐高、
nav-col 跟著被捲出視窗外。修法:main-layout-row 本身也要明確 nowrap
已用 Playwright 完整驗證:情境選定後預設在 AI對話頁;上傳→格式驗證→
Metadata略過→運算完成全程對話欄正確常駐、無結果內容洩漏;點語言指標
分析/統計比較分析(含4子分頁)/LLM輔助分析/技術執行紀錄皆正確切頁、
對話欄收起、Canvas佔滿、nav active高亮正確;點AI對話對話欄正確彈回;
reset後選新情境正確回到chat頁初始狀態;手機375px寬度漢堡抽屜點導覽
項目正確關閉抽屜+切頁;全程console 0 error。
- app_agent.py +212 -68
- assets/icons/folder.svg +1 -0
|
@@ -2256,27 +2256,16 @@ div[role="log"][aria-label="chatbot conversation"],
|
|
| 2256 |
text-underline-offset: 2px !important;
|
| 2257 |
}
|
| 2258 |
|
| 2259 |
-
/* 技術執行紀錄 Accordion
|
| 2260 |
-
|
| 2261 |
-
|
| 2262 |
-
|
| 2263 |
-
border: 1px solid var(--c-surface3-border) !important;
|
| 2264 |
-
border-radius: 0 0 14px 14px !important;
|
| 2265 |
-
background: var(--c-surface) !important;
|
| 2266 |
-
box-shadow: none !important;
|
| 2267 |
-
margin-top: 0 !important;
|
| 2268 |
-
}
|
| 2269 |
-
#exec_log_accordion .label-wrap {
|
| 2270 |
-
color: var(--c-text-muted) !important;
|
| 2271 |
-
font-size: var(--font-size-base) !important;
|
| 2272 |
-
}
|
| 2273 |
|
| 2274 |
/* 執行視窗(簡易訊息顯示欄,非對話氣泡) */
|
| 2275 |
-
/*
|
| 2276 |
-
|
| 2277 |
-
|
| 2278 |
-
|
| 2279 |
-
撐開高度,只用頁面本身的捲軸,維持單一捲動體驗 */
|
| 2280 |
.exec-log {
|
| 2281 |
background: transparent !important;
|
| 2282 |
border: none !important;
|
|
@@ -2377,6 +2366,34 @@ div[role="log"][aria-label="chatbot conversation"],
|
|
| 2377 |
overflow-x: hidden !important;
|
| 2378 |
overflow-y: auto !important;
|
| 2379 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2380 |
/* Phase 8-C 踩坑:結果內容搬進 canvas-scroll 後,內容總高度遠超過
|
| 2381 |
canvas-col 給的固定 height,理論上該讓 canvas-scroll 自己捲動就好;但
|
| 2382 |
exec_log_accordion(技術執行紀錄)被壓縮成只剩標題列高度(22px),內容
|
|
@@ -2533,6 +2550,7 @@ img.button-icon { opacity: .82; }
|
|
| 2533 |
.nav-col.collapsed .nav-collapse-row { justify-content: center; }
|
| 2534 |
.nav-col.collapsed .scenario-buttons,
|
| 2535 |
.nav-col.collapsed .result-nav-accordion,
|
|
|
|
| 2536 |
.nav-col.collapsed .reset-confirm { display: none !important; }
|
| 2537 |
.nav-col.collapsed .reset-btn,
|
| 2538 |
.nav-col.collapsed .result-nav-btn {
|
|
@@ -2756,6 +2774,31 @@ img.button-icon { opacity: .82; }
|
|
| 2756 |
font-weight: 600 !important;
|
| 2757 |
color: var(--c-text-soft) !important;
|
| 2758 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2759 |
/* 語料 Metadata 說明表單卡片 */
|
| 2760 |
.metadata-zone {
|
| 2761 |
border: 1px solid rgba(var(--c-accent-rgb),.40) !important;
|
|
@@ -3193,10 +3236,64 @@ _APP_HEAD_SCRIPT = """
|
|
| 3193 |
}
|
| 3194 |
[300, 800, 1500, 3000].forEach(function(t) { setTimeout(bindScenarioCards, t); });
|
| 3195 |
|
| 3196 |
-
/*
|
| 3197 |
-
|
| 3198 |
-
|
| 3199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3200 |
注意:result_nav_col 建構時是 visible=False(跟 sc_btn_1..4
|
| 3201 |
一度踩過的坑同一類),如果比照 bindScenarioCards() 用
|
| 3202 |
setTimeout 抓固定��間點的按鈕,情境選定前按鈕還沒掛載,之後
|
|
@@ -3213,16 +3310,27 @@ _APP_HEAD_SCRIPT = """
|
|
| 3213 |
document.addEventListener('click', function (e) {
|
| 3214 |
if (e.target.closest('#nav_top_btn')) {
|
| 3215 |
closeMobileDrawerIfOpen();
|
|
|
|
| 3216 |
var scrollBox = document.querySelector('.canvas-scroll');
|
| 3217 |
if (scrollBox) scrollBox.scrollTo({ top: 0, behavior: 'smooth' });
|
| 3218 |
return;
|
| 3219 |
}
|
| 3220 |
-
if (e.target.closest('#nav_idx_btn')) {
|
| 3221 |
-
|
| 3222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3223 |
for (var i = 0; i < 4; i++) {
|
| 3224 |
if (e.target.closest('#nav_stat_sub_' + i)) {
|
| 3225 |
closeMobileDrawerIfOpen();
|
|
|
|
| 3226 |
scrollToId('canvas_stat');
|
| 3227 |
/* 注意:這版 Gradio 的巢狀分頁 DOM 結構是
|
| 3228 |
.tabs > .tab-wrapper > .tab-container(兩個,
|
|
@@ -3318,6 +3426,16 @@ _APP_HEAD_SCRIPT = """
|
|
| 3318 |
if (navCol2 && navCol2.classList.contains('collapsed')) setNavCollapsed(false);
|
| 3319 |
closeMobileDrawerIfOpen();
|
| 3320 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3321 |
});
|
| 3322 |
document.addEventListener('keydown', function (e) {
|
| 3323 |
if (e.key === 'Escape') closeMobileDrawerIfOpen();
|
|
@@ -3389,11 +3507,32 @@ _APP_HEAD_SCRIPT = """
|
|
| 3389 |
try { saved = localStorage.getItem('spectra_nav_collapsed'); } catch (err) { saved = null; }
|
| 3390 |
if (saved === '1') setNavCollapsed(true);
|
| 3391 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3392 |
setInterval(function () {
|
| 3393 |
_watchZone('upload_zone');
|
| 3394 |
_watchZone('metadata_zone');
|
| 3395 |
_syncMobileHamburger();
|
| 3396 |
_initNavCollapseIfNeeded();
|
|
|
|
|
|
|
| 3397 |
}, 400);
|
| 3398 |
}
|
| 3399 |
if (document.readyState === 'loading') {
|
|
@@ -3540,7 +3679,7 @@ def create_app():
|
|
| 3540 |
gr.Markdown("確定要開始新分析嗎?目前進度將會遺失。")
|
| 3541 |
with gr.Row(elem_classes=["upload-actions"]):
|
| 3542 |
reset_cancel_btn = gr.Button("取消", scale=1, variant="secondary")
|
| 3543 |
-
reset_confirm_btn = gr.Button("確定重新開始", scale=2, variant="primary")
|
| 3544 |
|
| 3545 |
# Phase 8-D:結果導覽——情境選定後顯示,讓使用者不用手動
|
| 3546 |
# 往下捲一大段就能跳到 Canvas 對應區塊。這幾顆按鈕刻意不接
|
|
@@ -3551,8 +3690,14 @@ def create_app():
|
|
| 3551 |
# 下一輪視覺打磨(Heroicons 圖示)一起補,這次先求導覽功能
|
| 3552 |
# 正確可用。
|
| 3553 |
with gr.Column(visible=False, elem_id="result_nav_col", elem_classes=["result-nav"]) as result_nav_col:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3554 |
nav_top_btn = gr.Button(
|
| 3555 |
-
"
|
| 3556 |
elem_id="nav_top_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3557 |
)
|
| 3558 |
nav_idx_btn = gr.Button(
|
|
@@ -3573,6 +3718,16 @@ def create_app():
|
|
| 3573 |
"LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
|
| 3574 |
elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3575 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3576 |
|
| 3577 |
with gr.Column(scale=3, min_width=280, elem_classes=["chat-col"]):
|
| 3578 |
# 高度不再寫死 px,改用 CSS flex-grow 讓對話視窗跟輸入框依
|
|
@@ -3769,10 +3924,13 @@ def create_app():
|
|
| 3769 |
file_count="multiple", interactive=False, visible=False,
|
| 3770 |
)
|
| 3771 |
|
| 3772 |
-
# 技術執行紀錄
|
| 3773 |
-
#
|
| 3774 |
-
#
|
| 3775 |
-
|
|
|
|
|
|
|
|
|
|
| 3776 |
exec_log = gr.Markdown(
|
| 3777 |
value="",
|
| 3778 |
elem_id="exec_log",
|
|
@@ -3792,6 +3950,7 @@ def create_app():
|
|
| 3792 |
canvas_idx_col, canvas_stat_col, canvas_report_col, # 20-22 (visibility)
|
| 3793 |
tab_imp_file, tab_cluster_file, # 23-24 新增下載按鈕(僅能往尾端加,
|
| 3794 |
# 不可插入既有 index 中間,見程式工作守則)
|
|
|
|
| 3795 |
]
|
| 3796 |
_N_TAB = len(_TAB_OUTPUTS)
|
| 3797 |
|
|
@@ -4004,28 +4163,6 @@ def create_app():
|
|
| 4004 |
)
|
| 4005 |
)
|
| 4006 |
|
| 4007 |
-
# ── 技術執行紀錄摺疊面板:長時間運算(指標計算/統計分析/報告撰寫)
|
| 4008 |
-
# 時自動展開,其餘時間收合,避免長時間等待時看起來像沒有反應,
|
| 4009 |
-
# 但平常又不會有一堆技術字樣佔用版面。
|
| 4010 |
-
# 收合時的標題列也會帶上最新一行技術紀錄的摘要,不用點開展開
|
| 4011 |
-
# 就能掃一眼知道系統目前在做什麼 ──────────────────────────────
|
| 4012 |
-
def _accordion_label(log_history):
|
| 4013 |
-
if not log_history:
|
| 4014 |
-
return "技術執行紀錄"
|
| 4015 |
-
last_line = log_history.strip().split("\n\n")[-1]
|
| 4016 |
-
last_line = last_line.replace("`›`", "").strip()
|
| 4017 |
-
if len(last_line) > 34:
|
| 4018 |
-
last_line = last_line[:34] + "…"
|
| 4019 |
-
return f"技術執行紀錄 {last_line}" if last_line else "技術執行紀錄"
|
| 4020 |
-
|
| 4021 |
-
def _accordion_update(state, log_history):
|
| 4022 |
-
if not state:
|
| 4023 |
-
return gr.update()
|
| 4024 |
-
return gr.update(
|
| 4025 |
-
open=state.get("current_step") in (5, 6, 7),
|
| 4026 |
-
label=_accordion_label(log_history),
|
| 4027 |
-
)
|
| 4028 |
-
|
| 4029 |
# ── Phase 8-D:Canvas 運算中提示(粗粒度階段文字+轉圈動畫)─────
|
| 4030 |
# 沿用跟 _accordion_update 完全一樣的判斷(current_step 在 5/6/7 之間
|
| 4031 |
# 才顯示),不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不
|
|
@@ -4058,7 +4195,7 @@ def create_app():
|
|
| 4058 |
def on_scenario_pick(sc_num, chat_history, log_history, state):
|
| 4059 |
if state is None:
|
| 4060 |
yield (chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(),
|
| 4061 |
-
gr.update(), gr.update(), gr.update())
|
| 4062 |
return
|
| 4063 |
|
| 4064 |
sc_info = SCENARIOS[sc_num]
|
|
@@ -4067,23 +4204,27 @@ def create_app():
|
|
| 4067 |
# 首頁一旦選定情境即隱藏,換成對話+操作區(workspace_row);
|
| 4068 |
# 結果導覽(result_nav_col)也在這時候一併顯示(雖然還沒有結果
|
| 4069 |
# 可跳轉,但導覽列本身跟開始新分析按鈕一樣是常駐的操作區塊)。
|
|
|
|
|
|
|
|
|
|
| 4070 |
# 這幾個 visibility 只需要在第一個 yield 就切換一次,後續 yield
|
| 4071 |
# 沿用 gr.update() 不動即可,不用每次都重複賦值。
|
| 4072 |
yield (chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(),
|
| 4073 |
-
gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
|
|
|
|
| 4074 |
|
| 4075 |
reply, state = handle_scenario_select(sc_num, state)
|
| 4076 |
log_history = _log_append(log_history, "✅ STEP 1 情境選擇完成")
|
| 4077 |
chat_history = chat_history + [_assistant_msg(reply)]
|
| 4078 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4079 |
-
gr.update(), gr.update(), gr.update())
|
| 4080 |
|
| 4081 |
# STEP 2:確認訊息已經導向上傳卡片的「格式要求」區塊,不再另外用
|
| 4082 |
# LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘內容
|
| 4083 |
state["current_step"] = 2
|
| 4084 |
log_history = _log_append(log_history, "⏳ STEP 2 上傳引導中……")
|
| 4085 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4086 |
-
gr.update(), gr.update(), gr.update())
|
| 4087 |
|
| 4088 |
state["done_steps"] = [1, 2]
|
| 4089 |
state["current_step"] = 3
|
|
@@ -4097,14 +4238,14 @@ def create_app():
|
|
| 4097 |
)
|
| 4098 |
yield (chat_history, log_history, state, gr.update(visible=True),
|
| 4099 |
_todo_update(state), gr.update(visible=False), gr.update(value=hint),
|
| 4100 |
-
gr.update(), gr.update(), gr.update())
|
| 4101 |
|
| 4102 |
for _sc_num, _sc_btn in (("1", sc_btn_1), ("2", sc_btn_2), ("3", sc_btn_3), ("4", sc_btn_4)):
|
| 4103 |
_sc_btn.click(
|
| 4104 |
fn=functools.partial(on_scenario_pick, _sc_num),
|
| 4105 |
inputs=[chatbot, exec_log, session_state],
|
| 4106 |
outputs=[chatbot, exec_log, session_state, upload_row, app_header_html, scenario_btn_col,
|
| 4107 |
-
upload_hint_md, homepage_col, workspace_row, result_nav_col],
|
| 4108 |
).then(
|
| 4109 |
fn=_populate_tabs,
|
| 4110 |
inputs=[session_state],
|
|
@@ -4136,7 +4277,7 @@ def create_app():
|
|
| 4136 |
gr.update(visible=False), # canvas_stat_col 統計比較分析
|
| 4137 |
gr.update(visible=False), # canvas_report_col LLM輔助分析
|
| 4138 |
gr.update(visible=False), # reset_confirm_row(關掉確認彈窗自己)
|
| 4139 |
-
|
| 4140 |
gr.update(visible=True), # homepage_col(Phase 8:重新開始=回到首頁選情境)
|
| 4141 |
gr.update(visible=False), # workspace_row
|
| 4142 |
gr.update(value=None), # file_input(Phase 8:upload_row 變正式入口後一併清空)
|
|
@@ -4151,7 +4292,7 @@ def create_app():
|
|
| 4151 |
outputs=[chatbot, exec_log, session_state, app_header_html,
|
| 4152 |
scenario_btn_col, upload_row, chat_upload, metadata_row,
|
| 4153 |
canvas_idx_col, canvas_stat_col, canvas_report_col,
|
| 4154 |
-
reset_confirm_row,
|
| 4155 |
computing_html, result_nav_col],
|
| 4156 |
)
|
| 4157 |
|
|
@@ -4244,9 +4385,12 @@ def create_app():
|
|
| 4244 |
return "\n".join(L)
|
| 4245 |
|
| 4246 |
def _run_computation(chat_history, log_history, state):
|
|
|
|
|
|
|
|
|
|
| 4247 |
log_history = _log_append(log_history, "⏳ STEP 5 計算語言指標中……")
|
| 4248 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4249 |
-
|
| 4250 |
for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
|
| 4251 |
if "指標計算完成" in prog_msg:
|
| 4252 |
state["done_steps"] = list(set(state.get("done_steps", []) + [5])); state["current_step"] = 6
|
|
@@ -4262,7 +4406,7 @@ def create_app():
|
|
| 4262 |
if is_final:
|
| 4263 |
chat_history = chat_history + [_assistant_msg(prog_msg)]
|
| 4264 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4265 |
-
|
| 4266 |
|
| 4267 |
def _show_metadata_form(state):
|
| 4268 |
_NO = gr.update()
|
|
@@ -4347,7 +4491,7 @@ def create_app():
|
|
| 4347 |
# 在主對話視窗立即顯示「開始分析」訊息,避免長時間運算期間畫面看起來像沒有反應
|
| 4348 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 4349 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
| 4350 |
-
_todo_update(state),
|
| 4351 |
for ch, lh, st, todo, acc, comp in _run_computation(chat_history, log_history, state):
|
| 4352 |
chat_history, log_history, state = ch, lh, st
|
| 4353 |
yield chat_history, log_history, state, gr.update(visible=False), todo, acc, comp
|
|
@@ -4366,7 +4510,7 @@ def create_app():
|
|
| 4366 |
meta_ngroups, meta_method, meta_group_df, meta_indiv_df,
|
| 4367 |
chatbot, exec_log, session_state],
|
| 4368 |
outputs=[chatbot, exec_log, session_state, metadata_row, app_header_html,
|
| 4369 |
-
|
| 4370 |
).then(
|
| 4371 |
fn=_unlock_meta, inputs=None, outputs=[meta_confirm_btn, meta_cancel_btn],
|
| 4372 |
).then(fn=_populate_tabs, inputs=[session_state], outputs=_TAB_OUTPUTS)
|
|
@@ -4379,7 +4523,7 @@ def create_app():
|
|
| 4379 |
log_history = _log_append(log_history, "⏭️ 已略過 Metadata,直接開始分析")
|
| 4380 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 4381 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
| 4382 |
-
_todo_update(state),
|
| 4383 |
for ch, lh, st, todo, acc, comp in _run_computation(chat_history, log_history, state):
|
| 4384 |
chat_history, log_history, state = ch, lh, st
|
| 4385 |
yield chat_history, log_history, state, gr.update(visible=False), todo, acc, comp
|
|
@@ -4390,7 +4534,7 @@ def create_app():
|
|
| 4390 |
fn=on_metadata_skip,
|
| 4391 |
inputs=[chatbot, exec_log, session_state],
|
| 4392 |
outputs=[chatbot, exec_log, session_state, metadata_row, app_header_html,
|
| 4393 |
-
|
| 4394 |
).then(
|
| 4395 |
fn=_unlock_meta, inputs=None, outputs=[meta_confirm_btn, meta_cancel_btn],
|
| 4396 |
).then(fn=_populate_tabs, inputs=[session_state], outputs=_TAB_OUTPUTS)
|
|
|
|
| 2256 |
text-underline-offset: 2px !important;
|
| 2257 |
}
|
| 2258 |
|
| 2259 |
+
/* 2026-07-23:技術執行紀錄從 Accordion 轉正成獨立頁面(canvas_exec_col),
|
| 2260 |
+
不再是接在對話視窗卡片下方的「抽屜」,改用跟 canvas_idx_col 等其他三個
|
| 2261 |
+
結果頁共用的 .canvas-result class(已在 gr.Column 宣告時掛上),這裡不用
|
| 2262 |
+
再另外寫專屬的卡片外框樣式,避免跟共用樣式重複定義出兩套外觀。 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2263 |
|
| 2264 |
/* 執行視窗(簡易訊息顯示欄,非對話氣泡) */
|
| 2265 |
+
/* 原本這裡還設了 max-height + overflow-y:auto 讓內部自己捲動,但整個頁面
|
| 2266 |
+
本身也會捲動,兩個捲動容器疊在一起、捲軸位置又離得很近,看起來像「兩條
|
| 2267 |
+
捲軸」造成混亂——拿掉內部捲動限制,讓內容自然撐開高度,只用頁面本身的
|
| 2268 |
+
捲軸,維持單一捲動體驗 */
|
|
|
|
| 2269 |
.exec-log {
|
| 2270 |
background: transparent !important;
|
| 2271 |
border: none !important;
|
|
|
|
| 2366 |
overflow-x: hidden !important;
|
| 2367 |
overflow-y: auto !important;
|
| 2368 |
}
|
| 2369 |
+
/* 2026-07-23:頁面切換模型。點結果類 nav 項目時 JS 會在 .main-layout-row
|
| 2370 |
+
上加這個 class(見 _APP_HEAD_SCRIPT 的 applyActivePage()),對話欄收起、
|
| 2371 |
+
Canvas 佔滿剩下的寬度。canvas-col 的 flex 要蓋過 Gradio 自己依 scale=7
|
| 2372 |
+
算出的 inline flex-grow,才會真的補滿收起對話欄後空出來的寬度(同一類
|
| 2373 |
+
問題 8-E 的 min-width 已經踩過一次,這裡沿用同一個「stylesheet
|
| 2374 |
+
!important 蓋得過沒加 !important 的 inline style」原則)。
|
| 2375 |
+
踩坑(新案例,跟 8-B 的 .column flex-wrap 是同一大類但這次是 .row 本身
|
| 2376 |
+
從沒被設過):.main-layout-row(gr.Row)本身從來沒設過 flex-wrap:nowrap
|
| 2377 |
+
——nav-col/chat-col/canvas-col 各自「內部」的 flex-wrap:nowrap 只管
|
| 2378 |
+
它們自己的子元件怎麼排,不影響 main-layout-row 怎麼排這三欄本身。平常
|
| 2379 |
+
三欄用 flex-grow 精準分配寬度、剛好不超版所以wrap從沒被觸發過;這次
|
| 2380 |
+
canvas-col 拿掉 max-width 上限、又改用 flex-basis:auto 之後,內容較寬
|
| 2381 |
+
的表格會把 canvas-col 的 intrinsic 寬度撐到跟 nav-col 放不進同一行,
|
| 2382 |
+
Gradio 預設的 flex-wrap:wrap 就把 canvas-col 換到 nav-col 下面另起
|
| 2383 |
+
一行——整個版面變成上下堆疊、頁面被撐到 1200px+ 高,捲動 canvas 內容
|
| 2384 |
+
時連 nav-col 都跟著被推出視窗外。修法:main-layout-row 本身也要明確
|
| 2385 |
+
nowrap,不能只顧內部三欄各自的 nowrap。 */
|
| 2386 |
+
.main-layout-row {
|
| 2387 |
+
flex-wrap: nowrap !important;
|
| 2388 |
+
}
|
| 2389 |
+
.main-layout-row.chat-collapsed .chat-col {
|
| 2390 |
+
display: none !important;
|
| 2391 |
+
}
|
| 2392 |
+
.main-layout-row.chat-collapsed .canvas-col {
|
| 2393 |
+
flex: 1 1 auto !important;
|
| 2394 |
+
max-width: none !important;
|
| 2395 |
+
min-width: 0 !important;
|
| 2396 |
+
}
|
| 2397 |
/* Phase 8-C 踩坑:結果內容搬進 canvas-scroll 後,內容總高度遠超過
|
| 2398 |
canvas-col 給的固定 height,理論上該讓 canvas-scroll 自己捲動就好;但
|
| 2399 |
exec_log_accordion(技術執行紀錄)被壓縮成只剩標題列高度(22px),內容
|
|
|
|
| 2550 |
.nav-col.collapsed .nav-collapse-row { justify-content: center; }
|
| 2551 |
.nav-col.collapsed .scenario-buttons,
|
| 2552 |
.nav-col.collapsed .result-nav-accordion,
|
| 2553 |
+
.nav-col.collapsed .result-nav-divider,
|
| 2554 |
.nav-col.collapsed .reset-confirm { display: none !important; }
|
| 2555 |
.nav-col.collapsed .reset-btn,
|
| 2556 |
.nav-col.collapsed .result-nav-btn {
|
|
|
|
| 2774 |
font-weight: 600 !important;
|
| 2775 |
color: var(--c-text-soft) !important;
|
| 2776 |
}
|
| 2777 |
+
/* 2026-07-23:頁面切換模型——目前使用者所在的那一頁在導覽列高亮,
|
| 2778 |
+
applyActivePage() JS 幫對應按鈕加/移除這個 class。視覺先求「看得出
|
| 2779 |
+
選中誰」��基本可用,細部樣式(跟 Bootstrap 借來的分組/層級規範)留給
|
| 2780 |
+
下一輪 nav-col 視覺重做一起處理,這裡不重複做兩次。 */
|
| 2781 |
+
.result-nav-btn.active {
|
| 2782 |
+
background: rgba(var(--c-accent-rgb),.10) !important;
|
| 2783 |
+
color: var(--c-accent-strong) !important;
|
| 2784 |
+
font-weight: 700 !important;
|
| 2785 |
+
}
|
| 2786 |
+
.result-nav-accordion.active .label-wrap {
|
| 2787 |
+
color: var(--c-accent-strong) !important;
|
| 2788 |
+
font-weight: 700 !important;
|
| 2789 |
+
}
|
| 2790 |
+
/* 技術執行紀錄:低優先度診斷工具,跟上面三個主要分析結果用分隔線+
|
| 2791 |
+
降低視覺權重區隔(字級/顏色都比照 result-nav-subbtn 而非 result-nav-btn)*/
|
| 2792 |
+
.result-nav-divider {
|
| 2793 |
+
height: 1px;
|
| 2794 |
+
background: var(--c-border);
|
| 2795 |
+
margin: .5rem .3rem;
|
| 2796 |
+
}
|
| 2797 |
+
.result-nav-btn--muted {
|
| 2798 |
+
font-size: .82rem !important;
|
| 2799 |
+
font-weight: 500 !important;
|
| 2800 |
+
color: var(--c-text-muted) !important;
|
| 2801 |
+
}
|
| 2802 |
/* 語料 Metadata 說明表單卡片 */
|
| 2803 |
.metadata-zone {
|
| 2804 |
border: 1px solid rgba(var(--c-accent-rgb),.40) !important;
|
|
|
|
| 3236 |
}
|
| 3237 |
[300, 800, 1500, 3000].forEach(function(t) { setTimeout(bindScenarioCards, t); });
|
| 3238 |
|
| 3239 |
+
/* 2026-07-23:頁面切換模型。nav-col 的每個項目現在是真正的
|
| 3240 |
+
「頁面」——結果類項目(語言指標分析/統計比較分析/LLM輔助分析/
|
| 3241 |
+
技術執行紀錄)點進去時互斥顯示、收起對話欄、Canvas 佔滿寬度;
|
| 3242 |
+
「AI對話」(原「回到上傳/表單」)點進去對話欄彈回、Canvas 顯示
|
| 3243 |
+
上傳/表單/運算中內容。跟 8-C/8-D 的舊行為(有資料就全部堆疊、
|
| 3244 |
+
使用者自己往下捲)完全不同:現在同一時間只顯示使用者當前選的
|
| 3245 |
+
那一頁,不管其他頁有沒有資料。
|
| 3246 |
+
技術做法:canvas-scroll 底下 7 個直屬區塊分成 5 組頁面歸屬
|
| 3247 |
+
(upload_zone/metadata_zone/computing_zone 屬於 chat 頁;
|
| 3248 |
+
canvas_idx/canvas_stat/canvas_report/canvas_exec 各自獨立頁),
|
| 3249 |
+
JS 只清除或強制 inline display:none,絕不強制顯示——Python 那層
|
| 3250 |
+
`visible=False`(資料是否存在的判斷,見 _populate_tabs)永遠
|
| 3251 |
+
優先,這裡只負責「同一頁的東西藏起來,不動 Python 已經決定隱藏
|
| 3252 |
+
的東西」(沿用 8-E 的 style.display='' / 'none' !important
|
| 3253 |
+
技巧)。 */
|
| 3254 |
+
var _PAGE_OWNED_IDS = {
|
| 3255 |
+
chat: ['upload_zone', 'metadata_zone', 'computing_zone'],
|
| 3256 |
+
idx: ['canvas_idx'],
|
| 3257 |
+
stat: ['canvas_stat'],
|
| 3258 |
+
report: ['canvas_report'],
|
| 3259 |
+
exec: ['canvas_exec']
|
| 3260 |
+
};
|
| 3261 |
+
var _PAGE_NAV_IDS = {
|
| 3262 |
+
chat: 'nav_top_btn', idx: 'nav_idx_btn', stat: 'nav_stat_accordion',
|
| 3263 |
+
report: 'nav_report_btn', exec: 'nav_exec_btn'
|
| 3264 |
+
};
|
| 3265 |
+
function applyActivePage(page) {
|
| 3266 |
+
var mainRow = document.querySelector('.main-layout-row');
|
| 3267 |
+
if (!mainRow) return;
|
| 3268 |
+
mainRow.setAttribute('data-page', page);
|
| 3269 |
+
mainRow.classList.toggle('chat-collapsed', page !== 'chat');
|
| 3270 |
+
Object.keys(_PAGE_OWNED_IDS).forEach(function (key) {
|
| 3271 |
+
_PAGE_OWNED_IDS[key].forEach(function (id) {
|
| 3272 |
+
var el = document.getElementById(id);
|
| 3273 |
+
if (!el) return;
|
| 3274 |
+
if (key === page) {
|
| 3275 |
+
el.style.removeProperty('display');
|
| 3276 |
+
} else {
|
| 3277 |
+
el.style.setProperty('display', 'none', 'important');
|
| 3278 |
+
}
|
| 3279 |
+
});
|
| 3280 |
+
});
|
| 3281 |
+
Object.keys(_PAGE_NAV_IDS).forEach(function (key) {
|
| 3282 |
+
var btn = document.getElementById(_PAGE_NAV_IDS[key]);
|
| 3283 |
+
if (btn) btn.classList.toggle('active', key === page);
|
| 3284 |
+
});
|
| 3285 |
+
}
|
| 3286 |
+
function _initActivePageIfNeeded() {
|
| 3287 |
+
var mainRow = document.querySelector('.main-layout-row');
|
| 3288 |
+
if (!mainRow || mainRow.dataset.pageInit) return;
|
| 3289 |
+
mainRow.dataset.pageInit = '1';
|
| 3290 |
+
applyActivePage('chat');
|
| 3291 |
+
}
|
| 3292 |
+
|
| 3293 |
+
/* 左側結果導覽(result_nav_col)——純粹是「切換頁面+捲動+切換
|
| 3294 |
+
巢狀子分頁」的畫面操作,不牽動任何後端狀態,所以這幾顆按鈕在
|
| 3295 |
+
Python 端完全沒有掛 .click(),改用這裡的事件代理直接處理,不用
|
| 3296 |
+
走 Gradio queue 來回。
|
| 3297 |
注意:result_nav_col 建構時是 visible=False(跟 sc_btn_1..4
|
| 3298 |
一度踩過的坑同一類),如果比照 bindScenarioCards() 用
|
| 3299 |
setTimeout 抓固定��間點的按鈕,情境選定前按鈕還沒掛載,之後
|
|
|
|
| 3310 |
document.addEventListener('click', function (e) {
|
| 3311 |
if (e.target.closest('#nav_top_btn')) {
|
| 3312 |
closeMobileDrawerIfOpen();
|
| 3313 |
+
applyActivePage('chat');
|
| 3314 |
var scrollBox = document.querySelector('.canvas-scroll');
|
| 3315 |
if (scrollBox) scrollBox.scrollTo({ top: 0, behavior: 'smooth' });
|
| 3316 |
return;
|
| 3317 |
}
|
| 3318 |
+
if (e.target.closest('#nav_idx_btn')) {
|
| 3319 |
+
closeMobileDrawerIfOpen(); applyActivePage('idx'); scrollToId('canvas_idx'); return;
|
| 3320 |
+
}
|
| 3321 |
+
if (e.target.closest('#nav_report_btn')) {
|
| 3322 |
+
closeMobileDrawerIfOpen(); applyActivePage('report'); scrollToId('canvas_report'); return;
|
| 3323 |
+
}
|
| 3324 |
+
if (e.target.closest('#nav_exec_btn')) {
|
| 3325 |
+
closeMobileDrawerIfOpen(); applyActivePage('exec'); scrollToId('canvas_exec'); return;
|
| 3326 |
+
}
|
| 3327 |
+
if (e.target.closest('#nav_stat_accordion .label-wrap')) {
|
| 3328 |
+
closeMobileDrawerIfOpen(); applyActivePage('stat'); scrollToId('canvas_stat'); return;
|
| 3329 |
+
}
|
| 3330 |
for (var i = 0; i < 4; i++) {
|
| 3331 |
if (e.target.closest('#nav_stat_sub_' + i)) {
|
| 3332 |
closeMobileDrawerIfOpen();
|
| 3333 |
+
applyActivePage('stat');
|
| 3334 |
scrollToId('canvas_stat');
|
| 3335 |
/* 注意:這版 Gradio 的巢狀分頁 DOM 結構是
|
| 3336 |
.tabs > .tab-wrapper > .tab-container(兩個,
|
|
|
|
| 3426 |
if (navCol2 && navCol2.classList.contains('collapsed')) setNavCollapsed(false);
|
| 3427 |
closeMobileDrawerIfOpen();
|
| 3428 |
}
|
| 3429 |
+
if (e.target.closest('#reset_confirm_btn')) {
|
| 3430 |
+
/* 2026-07-23:真的按下「確定重新開始」才重置頁面切換
|
| 3431 |
+
狀態回 chat(不能在 #reset_btn 那支就做,那個只是
|
| 3432 |
+
打開確認彈窗,使用者還可能按取消)。.main-layout-row
|
| 3433 |
+
這個 DOM 節點在 Gradio 重置流程中不會被銷毀重建,
|
| 3434 |
+
data-page/chat-collapsed 這些 attribute 不會自動
|
| 3435 |
+
清掉,若不主動重置,下一次選新情境時畫面會直接卡在
|
| 3436 |
+
上一次分析結束前所在的那一頁。 */
|
| 3437 |
+
applyActivePage('chat');
|
| 3438 |
+
}
|
| 3439 |
});
|
| 3440 |
document.addEventListener('keydown', function (e) {
|
| 3441 |
if (e.key === 'Escape') closeMobileDrawerIfOpen();
|
|
|
|
| 3507 |
try { saved = localStorage.getItem('spectra_nav_collapsed'); } catch (err) { saved = null; }
|
| 3508 |
if (saved === '1') setNavCollapsed(true);
|
| 3509 |
}
|
| 3510 |
+
function _reapplyActivePage() {
|
| 3511 |
+
/* 踩坑(2026-07-23,全新踩坑類別):applyActivePage() 用
|
| 3512 |
+
el.style.setProperty('display','none','important') 隱藏
|
| 3513 |
+
非當前頁區塊,理論上一次設定就該一直有效。但實測發現運算
|
| 3514 |
+
完成後(_populate_tabs 把 canvas_idx_col 等設 visible=True)
|
| 3515 |
+
canvas_idx/canvas_stat 又跑出來混進 chat 頁——代表 Gradio
|
| 3516 |
+
的 Svelte reactivity 在元件的 visible prop 改變時,會自己
|
| 3517 |
+
整個重寫該元素的 style 屬性(不是「疊加」而是「覆蓋」),
|
| 3518 |
+
把我手動加的 inline !important 一併洗掉。這跟先前踩過的
|
| 3519 |
+
「Python 的 gr.update 值改變時整個重新渲染」是同一個大類,
|
| 3520 |
+
只是這次連 inline style 都被連帶清空,不只是 min-width 那種
|
| 3521 |
+
單一屬性優先權的問題。修法沿用這個檔案一貫的解法:不奢求
|
| 3522 |
+
一次設定永久有效,改成跟 _watchZone/_syncMobileHamburger
|
| 3523 |
+
一樣搭這裡的輪詢,每 400ms 重新套用一次目前頁面對應的
|
| 3524 |
+
隱藏規則,Gradio 重新渲染蓋掉的話下一個 tick 就修回來。 */
|
| 3525 |
+
var mainRow = document.querySelector('.main-layout-row');
|
| 3526 |
+
var page = mainRow && mainRow.getAttribute('data-page');
|
| 3527 |
+
if (page) applyActivePage(page);
|
| 3528 |
+
}
|
| 3529 |
setInterval(function () {
|
| 3530 |
_watchZone('upload_zone');
|
| 3531 |
_watchZone('metadata_zone');
|
| 3532 |
_syncMobileHamburger();
|
| 3533 |
_initNavCollapseIfNeeded();
|
| 3534 |
+
_initActivePageIfNeeded();
|
| 3535 |
+
_reapplyActivePage();
|
| 3536 |
}, 400);
|
| 3537 |
}
|
| 3538 |
if (document.readyState === 'loading') {
|
|
|
|
| 3679 |
gr.Markdown("確定要開始新分析嗎?目前進度將會遺失。")
|
| 3680 |
with gr.Row(elem_classes=["upload-actions"]):
|
| 3681 |
reset_cancel_btn = gr.Button("取消", scale=1, variant="secondary")
|
| 3682 |
+
reset_confirm_btn = gr.Button("確定重新開始", elem_id="reset_confirm_btn", scale=2, variant="primary")
|
| 3683 |
|
| 3684 |
# Phase 8-D:結果導覽——情境選定後顯示,讓使用者不用手動
|
| 3685 |
# 往下捲一大段就能跳到 Canvas 對應區塊。這幾顆按鈕刻意不接
|
|
|
|
| 3690 |
# 下一輪視覺打磨(Heroicons 圖示)一起補,這次先求導覽功能
|
| 3691 |
# 正確可用。
|
| 3692 |
with gr.Column(visible=False, elem_id="result_nav_col", elem_classes=["result-nav"]) as result_nav_col:
|
| 3693 |
+
# 2026-07-23 改名「AI對話」+改變功能定位:原本純粹是
|
| 3694 |
+
# 「捲回 Canvas 最上面」的畫面操作,現在身兼「切回流程頁」
|
| 3695 |
+
# 的角色(見 applyActivePage() JS)——流程進行中(上傳/
|
| 3696 |
+
# 表單/運算中)對話欄常駐顯示,點結果類項目才收起;點
|
| 3697 |
+
# 這顆按鈕會讓對話欄彈回來。圖示維持 home(語意上仍是
|
| 3698 |
+
#「回到主要工作流程」,不用重畫新圖示)。
|
| 3699 |
nav_top_btn = gr.Button(
|
| 3700 |
+
"AI對話", icon=str(BASE_DIR / "assets" / "icons" / "home.svg"),
|
| 3701 |
elem_id="nav_top_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3702 |
)
|
| 3703 |
nav_idx_btn = gr.Button(
|
|
|
|
| 3718 |
"LLM輔助分析", icon=str(BASE_DIR / "assets" / "icons" / "document.svg"),
|
| 3719 |
elem_id="nav_report_btn", elem_classes=["result-nav-btn"], variant="secondary",
|
| 3720 |
)
|
| 3721 |
+
# 2026-07-23:技術執行紀錄從手風琴轉正成獨立頁面(原本
|
| 3722 |
+
# exec_log_accordion 在 canvas-col 底部,現在跟其他三個
|
| 3723 |
+
# 結果頁同一種待遇)。用分隔線隔開,視覺上表達「低優先度
|
| 3724 |
+
# 診斷工具,不是主要分析結果」,避免跟上面三項混為一談。
|
| 3725 |
+
gr.HTML('<div class="result-nav-divider"></div>')
|
| 3726 |
+
nav_exec_btn = gr.Button(
|
| 3727 |
+
"技術執行紀錄", icon=str(BASE_DIR / "assets" / "icons" / "folder.svg"),
|
| 3728 |
+
elem_id="nav_exec_btn", elem_classes=["result-nav-btn", "result-nav-btn--muted"],
|
| 3729 |
+
variant="secondary",
|
| 3730 |
+
)
|
| 3731 |
|
| 3732 |
with gr.Column(scale=3, min_width=280, elem_classes=["chat-col"]):
|
| 3733 |
# 高度不再寫死 px,改用 CSS flex-grow 讓對話視窗跟輸入框依
|
|
|
|
| 3924 |
file_count="multiple", interactive=False, visible=False,
|
| 3925 |
)
|
| 3926 |
|
| 3927 |
+
# 2026-07-23:技術執行紀錄從手風琴(exec_log_accordion)
|
| 3928 |
+
# 轉正成獨立頁面,跟 canvas_idx_col/canvas_stat_col/
|
| 3929 |
+
# canvas_report_col 同一種待遇——不再需要 open/label
|
| 3930 |
+
# 收合狀態邏輯(原本靠 _accordion_update 控制),改用
|
| 3931 |
+
# 跟其他三個結果頁一樣的 visible=True/False 語意,交給
|
| 3932 |
+
# applyActivePage() JS 決定要不要顯示。
|
| 3933 |
+
with gr.Column(visible=False, elem_id="canvas_exec", elem_classes=["canvas-result"]) as canvas_exec_col:
|
| 3934 |
exec_log = gr.Markdown(
|
| 3935 |
value="",
|
| 3936 |
elem_id="exec_log",
|
|
|
|
| 3950 |
canvas_idx_col, canvas_stat_col, canvas_report_col, # 20-22 (visibility)
|
| 3951 |
tab_imp_file, tab_cluster_file, # 23-24 新增下載按鈕(僅能往尾端加,
|
| 3952 |
# 不可插入既有 index 中間,見程式工作守則)
|
| 3953 |
+
canvas_exec_col, # 25 技術執行紀錄轉正成獨立頁面(2026-07-23)
|
| 3954 |
]
|
| 3955 |
_N_TAB = len(_TAB_OUTPUTS)
|
| 3956 |
|
|
|
|
| 4163 |
)
|
| 4164 |
)
|
| 4165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4166 |
# ── Phase 8-D:Canvas 運算中提示(粗粒度階段文字+轉圈動畫)─────
|
| 4167 |
# 沿用跟 _accordion_update 完全一樣的判斷(current_step 在 5/6/7 之間
|
| 4168 |
# 才顯示),不新增運算邏輯;用 gr.HTML() 單一元件承載,空字串=不
|
|
|
|
| 4195 |
def on_scenario_pick(sc_num, chat_history, log_history, state):
|
| 4196 |
if state is None:
|
| 4197 |
yield (chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(),
|
| 4198 |
+
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4199 |
return
|
| 4200 |
|
| 4201 |
sc_info = SCENARIOS[sc_num]
|
|
|
|
| 4204 |
# 首頁一旦選定情境即隱藏,換成對話+操作區(workspace_row);
|
| 4205 |
# 結果導覽(result_nav_col)也在這時候一併顯示(雖然還沒有結果
|
| 4206 |
# 可跳轉,但導覽列本身跟開始新分析按鈕一樣是常駐的操作區塊)。
|
| 4207 |
+
# 技術執行紀錄(canvas_exec_col)跟著同一時間顯示——它從一開始
|
| 4208 |
+
# 就有內容(這段 log 本身),不像其他三個結果頁要等運算完成才有
|
| 4209 |
+
# 東西可看,不用透過 _populate_tabs 的檔案存在判斷來決定可見性。
|
| 4210 |
# 這幾個 visibility 只需要在第一個 yield 就切換一次,後續 yield
|
| 4211 |
# 沿用 gr.update() 不動即可,不用每次都重複賦值。
|
| 4212 |
yield (chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(),
|
| 4213 |
+
gr.update(visible=False), gr.update(visible=True), gr.update(visible=True),
|
| 4214 |
+
gr.update(visible=True))
|
| 4215 |
|
| 4216 |
reply, state = handle_scenario_select(sc_num, state)
|
| 4217 |
log_history = _log_append(log_history, "✅ STEP 1 情境選擇完成")
|
| 4218 |
chat_history = chat_history + [_assistant_msg(reply)]
|
| 4219 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4220 |
+
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4221 |
|
| 4222 |
# STEP 2:確認訊息已經導向上傳卡片的「格式要求」區塊,不再另外用
|
| 4223 |
# LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘內容
|
| 4224 |
state["current_step"] = 2
|
| 4225 |
log_history = _log_append(log_history, "⏳ STEP 2 上傳引導中……")
|
| 4226 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
|
| 4227 |
+
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4228 |
|
| 4229 |
state["done_steps"] = [1, 2]
|
| 4230 |
state["current_step"] = 3
|
|
|
|
| 4238 |
)
|
| 4239 |
yield (chat_history, log_history, state, gr.update(visible=True),
|
| 4240 |
_todo_update(state), gr.update(visible=False), gr.update(value=hint),
|
| 4241 |
+
gr.update(), gr.update(), gr.update(), gr.update())
|
| 4242 |
|
| 4243 |
for _sc_num, _sc_btn in (("1", sc_btn_1), ("2", sc_btn_2), ("3", sc_btn_3), ("4", sc_btn_4)):
|
| 4244 |
_sc_btn.click(
|
| 4245 |
fn=functools.partial(on_scenario_pick, _sc_num),
|
| 4246 |
inputs=[chatbot, exec_log, session_state],
|
| 4247 |
outputs=[chatbot, exec_log, session_state, upload_row, app_header_html, scenario_btn_col,
|
| 4248 |
+
upload_hint_md, homepage_col, workspace_row, result_nav_col, canvas_exec_col],
|
| 4249 |
).then(
|
| 4250 |
fn=_populate_tabs,
|
| 4251 |
inputs=[session_state],
|
|
|
|
| 4277 |
gr.update(visible=False), # canvas_stat_col 統計比較分析
|
| 4278 |
gr.update(visible=False), # canvas_report_col LLM輔助分析
|
| 4279 |
gr.update(visible=False), # reset_confirm_row(關掉確認彈窗自己)
|
| 4280 |
+
gr.update(visible=False), # canvas_exec_col 技術執行紀錄(2026-07-23 轉正後,跟其他三個結果頁一樣重置為隱藏)
|
| 4281 |
gr.update(visible=True), # homepage_col(Phase 8:重新開始=回到首頁選情境)
|
| 4282 |
gr.update(visible=False), # workspace_row
|
| 4283 |
gr.update(value=None), # file_input(Phase 8:upload_row 變正式入口後一併清空)
|
|
|
|
| 4292 |
outputs=[chatbot, exec_log, session_state, app_header_html,
|
| 4293 |
scenario_btn_col, upload_row, chat_upload, metadata_row,
|
| 4294 |
canvas_idx_col, canvas_stat_col, canvas_report_col,
|
| 4295 |
+
reset_confirm_row, canvas_exec_col, homepage_col, workspace_row, file_input,
|
| 4296 |
computing_html, result_nav_col],
|
| 4297 |
)
|
| 4298 |
|
|
|
|
| 4385 |
return "\n".join(L)
|
| 4386 |
|
| 4387 |
def _run_computation(chat_history, log_history, state):
|
| 4388 |
+
# 2026-07-23:技術執行紀錄轉正成獨立頁面後,可見性只在情境選定
|
| 4389 |
+
# (on_scenario_pick)與重置(on_reset_confirm)時切換一次,
|
| 4390 |
+
# 這裡不用再逐步更新開合狀態/摘要標籤,改回傳 gr.update() 不動。
|
| 4391 |
log_history = _log_append(log_history, "⏳ STEP 5 計算語言指標中……")
|
| 4392 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4393 |
+
gr.update(), _computing_update(state))
|
| 4394 |
for prog_msg, is_final, state in run_computation_pipeline_streaming(state):
|
| 4395 |
if "指標計算完成" in prog_msg:
|
| 4396 |
state["done_steps"] = list(set(state.get("done_steps", []) + [5])); state["current_step"] = 6
|
|
|
|
| 4406 |
if is_final:
|
| 4407 |
chat_history = chat_history + [_assistant_msg(prog_msg)]
|
| 4408 |
yield (chat_history, log_history, state, _todo_update(state),
|
| 4409 |
+
gr.update(), _computing_update(state))
|
| 4410 |
|
| 4411 |
def _show_metadata_form(state):
|
| 4412 |
_NO = gr.update()
|
|
|
|
| 4491 |
# 在主對話視窗立即顯示「開始分析」訊息,避免長時間運算期間畫面看起來像沒有反應
|
| 4492 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 4493 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
| 4494 |
+
_todo_update(state), gr.update(), _computing_update(state))
|
| 4495 |
for ch, lh, st, todo, acc, comp in _run_computation(chat_history, log_history, state):
|
| 4496 |
chat_history, log_history, state = ch, lh, st
|
| 4497 |
yield chat_history, log_history, state, gr.update(visible=False), todo, acc, comp
|
|
|
|
| 4510 |
meta_ngroups, meta_method, meta_group_df, meta_indiv_df,
|
| 4511 |
chatbot, exec_log, session_state],
|
| 4512 |
outputs=[chatbot, exec_log, session_state, metadata_row, app_header_html,
|
| 4513 |
+
canvas_exec_col, computing_html],
|
| 4514 |
).then(
|
| 4515 |
fn=_unlock_meta, inputs=None, outputs=[meta_confirm_btn, meta_cancel_btn],
|
| 4516 |
).then(fn=_populate_tabs, inputs=[session_state], outputs=_TAB_OUTPUTS)
|
|
|
|
| 4523 |
log_history = _log_append(log_history, "⏭️ 已略過 Metadata,直接開始分析")
|
| 4524 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 4525 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
| 4526 |
+
_todo_update(state), gr.update(), _computing_update(state))
|
| 4527 |
for ch, lh, st, todo, acc, comp in _run_computation(chat_history, log_history, state):
|
| 4528 |
chat_history, log_history, state = ch, lh, st
|
| 4529 |
yield chat_history, log_history, state, gr.update(visible=False), todo, acc, comp
|
|
|
|
| 4534 |
fn=on_metadata_skip,
|
| 4535 |
inputs=[chatbot, exec_log, session_state],
|
| 4536 |
outputs=[chatbot, exec_log, session_state, metadata_row, app_header_html,
|
| 4537 |
+
canvas_exec_col, computing_html],
|
| 4538 |
).then(
|
| 4539 |
fn=_unlock_meta, inputs=None, outputs=[meta_confirm_btn, meta_cancel_btn],
|
| 4540 |
).then(fn=_populate_tabs, inputs=[session_state], outputs=_TAB_OUTPUTS)
|
|
|