un2266 commited on
Commit
aea3107
·
1 Parent(s): 1daf41e

Phase 8-A:首頁 4 張情境卡片(gr.HTML()+JS橋接,取代按鈕式選擇)

Browse files

情境選擇搬到首頁,垂直置中展示 4 張圖文卡片(標題+說明,啟用原本死碼的
SCENARIOS[n]["desc"])。Gradio 無原生卡片元件,卡片點擊透過 _APP_HEAD_SCRIPT
注入的 JS 監聽器橋接到隱藏版 sc_btn_1..4(.sc-bridge-btn),.click() 綁定
邏輯完全不動。隱藏按鈕改放進一開始就 visible=True 的 homepage_col(而非
workspace_row 內),因為 Gradio 對建構時 visible=False 的容器是整段不掛載
進 DOM,放錯位置會導致 JS 找不到可點的按鈕。

on_scenario_pick/on_reset_confirm 的 outputs tuple 尾端新增 homepage_col/
workspace_row 兩個 visibility 輸出,控制首頁與對話+操作區的顯示切換。
scenario_btn_col 原本包按鈕的位置留空,作為 Phase 8-F 清理債務。

已用 Playwright 驗證:首頁四卡渲染正確、點卡進入原有情境選擇流程(對話訊息
與格式要求皆正確)、開始新分析可正確返回首頁。

Files changed (1) hide show
  1. app_agent.py +186 -12
app_agent.py CHANGED
@@ -1680,6 +1680,34 @@ _EMPTY_HINT = (
1680
  '</div>'
1681
  )
1682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1683
  _CUSTOM_CSS = """
1684
  /* ═══════════════════════════════════════════════════════════════
1685
  重譯文本分析系統 Agent — Refined Dark Academic Dashboard
@@ -2262,6 +2290,68 @@ div[role="log"][aria-label="chatbot conversation"],
2262
  font-weight: 600 !important;
2263
  font-size: 0.92rem !important;
2264
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2265
  /* ── 6. UPLOAD AREA(醒目卡片)─────────────────────────────────── */
2266
  /* 注意:max-width + margin auto 是舊版單欄置中版面留下的樣式,現在
2267
  upload-zone 是右側 action-col 裡的元件,要跟同一欄的 scenario-buttons
@@ -2726,6 +2816,24 @@ _APP_HEAD_SCRIPT = """
2726
  /* 多次觸發確保 Svelte hydration 後生效 */
2727
  [300, 800, 1500, 3000].forEach(function(t) { setTimeout(fixBubbleWrap, t); });
2728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2729
  /* 持續監聽 DOM 變化(每次訊息更新後重設) */
2730
  var mo = new MutationObserver(function(mutations) {
2731
  var needsFix = mutations.some(function(m) {
@@ -2788,6 +2896,39 @@ _APP_HEAD_SCRIPT = """
2788
  _APP_HEAD = _DARK_MODE_HEAD_CSS + _APP_HEAD_SCRIPT
2789
 
2790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2791
  def create_app():
2792
  with gr.Blocks(
2793
  title="譯彩紛呈 II 分析引擎 SpectraEngine",
@@ -2815,11 +2956,32 @@ def create_app():
2815
  # Tab 0: 主頁
2816
  # ══════════════════════════════════════════════════════════════
2817
  with gr.Tab("🏠 主頁"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2818
  # 電腦版:左側對話視窗、右側操作區(按鈕/上傳卡片)並排;
2819
  # 手機寬度(見 CSS .main-layout-row 的 media query)改回上下堆疊。
2820
  # 對話視窗為畫面上唯一主要焦點;技術執行紀錄預設收合,
2821
  # 只在長時間運算(指標計算/統計分析/報告撰寫)時自動展開
2822
- with gr.Row(elem_classes=["main-layout-row"]):
2823
  with gr.Column(scale=6, min_width=320, elem_classes=["chat-col"]):
2824
  chatbot = gr.Chatbot(
2825
  label="對話視窗",
@@ -2850,11 +3012,13 @@ def create_app():
2850
  with gr.Column(scale=4, min_width=280, elem_classes=["action-col"]):
2851
  # 研究情境是封閉式的固定四選一,改用可點擊按鈕取代打字輸入
2852
  # 數字再解析,選定後即隱藏,對話視窗回歸純輸出角色
 
 
 
 
 
2853
  with gr.Column(elem_classes=["scenario-buttons"]) as scenario_btn_col:
2854
- sc_btn_1 = gr.Button("1 人工翻譯 vs 機器翻譯", variant="secondary")
2855
- sc_btn_2 = gr.Button("2 全譯 vs 編譯/節譯/改寫", variant="secondary")
2856
- sc_btn_3 = gr.Button("3 同期不同譯者譯本比較", variant="secondary")
2857
- sc_btn_4 = gr.Button("4 不同時期重譯本的歷時比較", variant="secondary")
2858
 
2859
  # Phase 7 過渡:此卡片永久隱藏,不再被任何 .click()/yield
2860
  # 設回 visible=True,保留純作程式碼備援(回退時把新的
@@ -3273,24 +3437,30 @@ def create_app():
3273
  # ── 研究情境選擇(封閉式固定四選一,按鈕直接觸發,不再解析打字輸入)──
3274
  def on_scenario_pick(sc_num, chat_history, log_history, state):
3275
  if state is None:
3276
- yield chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update()
3277
  return
3278
 
3279
  sc_info = SCENARIOS[sc_num]
3280
  chat_history = chat_history + [_user_msg(sc_info["title"])]
3281
  log_history = _log_append(log_history, "使用者選擇研究情境中……")
3282
- yield chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update()
 
 
 
 
3283
 
3284
  reply, state = handle_scenario_select(sc_num, state)
3285
  log_history = _log_append(log_history, "✅ STEP 1 情境選擇完成")
3286
  chat_history = chat_history + [_assistant_msg(reply)]
3287
- yield chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update()
 
3288
 
3289
  # STEP 2:確認訊息已經導向上傳卡片的「格式要求」區塊,不再另外用
3290
  # LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘內容
3291
  state["current_step"] = 2
3292
  log_history = _log_append(log_history, "⏳ STEP 2 上傳引導中……")
3293
- yield chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update()
 
3294
 
3295
  state["done_steps"] = [1, 2]
3296
  state["current_step"] = 3
@@ -3303,13 +3473,15 @@ def create_app():
3303
  f"**格式要求**\n\n{sc_info['upload_prompt']}"
3304
  )
3305
  yield (chat_history, log_history, state, gr.update(visible=True),
3306
- _todo_update(state), gr.update(visible=False), gr.update(value=hint))
 
3307
 
3308
  for _sc_num, _sc_btn in (("1", sc_btn_1), ("2", sc_btn_2), ("3", sc_btn_3), ("4", sc_btn_4)):
3309
  _sc_btn.click(
3310
  fn=functools.partial(on_scenario_pick, _sc_num),
3311
  inputs=[chatbot, exec_log, session_state],
3312
- outputs=[chatbot, exec_log, session_state, chat_upload_col, app_header_html, scenario_btn_col, upload_hint_md],
 
3313
  ).then(
3314
  fn=_populate_tabs,
3315
  inputs=[session_state],
@@ -3342,6 +3514,8 @@ def create_app():
3342
  gr.update(visible=False), # tab6 LLM輔助分析
3343
  gr.update(visible=False), # reset_confirm_row(關掉確認彈窗自己)
3344
  _accordion_update(state, new_log), # exec_log_accordion(收合並更新摘要文字)
 
 
3345
  )
3346
 
3347
  reset_btn.click(fn=lambda: gr.update(visible=True), outputs=[reset_confirm_row])
@@ -3350,7 +3524,7 @@ def create_app():
3350
  fn=on_reset_confirm,
3351
  outputs=[chatbot, exec_log, session_state, app_header_html,
3352
  scenario_btn_col, chat_upload_col, chat_upload, metadata_row, tab2, tab3, tab6,
3353
- reset_confirm_row, exec_log_accordion],
3354
  )
3355
 
3356
  # ── 檔案上傳確認(streaming generator)──────────────────────────────
 
1680
  '</div>'
1681
  )
1682
 
1683
+ # ── SVG 圖示(Phase 8:導覽/操作圖示改用線條 icon,不再用 emoji)───────────
1684
+ # 手繪 Heroicons 風格(24x24 viewBox、stroke=currentColor、圓角線頭),
1685
+ # 不是逐點對照官方套件,純線條輪廓、視覺重量統一,供 _icon() 組裝使用。
1686
+ _ICON_PATHS = {
1687
+ "folder": '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>',
1688
+ "chevron-down": '<polyline points="6,9 12,15 18,9"/>',
1689
+ "chevron-double-left": '<polyline points="17,6 11,12 17,18"/><polyline points="11,6 5,12 11,18"/>',
1690
+ "bars-3": '<line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="17" x2="20" y2="17"/>',
1691
+ "x": '<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>',
1692
+ "home": '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V20a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V9.5"/>',
1693
+ "chart-bar": '<line x1="4" y1="20" x2="20" y2="20"/><rect x="5" y="12" width="3" height="8"/><rect x="10.5" y="7" width="3" height="13"/><rect x="16" y="14" width="3" height="6"/>',
1694
+ "trending-up": '<polyline points="4,17 9,11 13,14 20,6"/><polyline points="14,6 20,6 20,12"/>',
1695
+ "document": '<path d="M7 3h7l5 5v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v5h5"/><line x1="8.5" y1="12" x2="15.5" y2="12"/><line x1="8.5" y1="15.5" x2="15.5" y2="15.5"/>',
1696
+ "plus": '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
1697
+ "download": '<path d="M12 3v12"/><polyline points="7,10 12,15 17,10"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/>',
1698
+ }
1699
+
1700
+
1701
+ def _icon(name: str, size: int = 20) -> str:
1702
+ """回傳單一 SVG 圖示字串,供 gr.HTML() 內容與 gr.Button(icon=...) 檔案外的行內用法共用。"""
1703
+ path = _ICON_PATHS.get(name, "")
1704
+ return (
1705
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
1706
+ f'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" '
1707
+ f'stroke-linecap="round" stroke-linejoin="round">{path}</svg>'
1708
+ )
1709
+
1710
+
1711
  _CUSTOM_CSS = """
1712
  /* ═══════════════════════════════════════════════════════════════
1713
  重譯文本分析系統 Agent — Refined Dark Academic Dashboard
 
2290
  font-weight: 600 !important;
2291
  font-size: 0.92rem !important;
2292
  }
2293
+ /* ── 5d. HOMEPAGE(Phase 8:首頁 4 張情境卡片)──────────────────── */
2294
+ /* 隱藏版情境按鈕:純作 JS 橋接目標(見 _APP_HEAD_SCRIPT),
2295
+ 點卡片=模擬點這裡的按鈕,Python 端 .click() 綁定完全不動 */
2296
+ .sc-bridge-btn { display: none !important; }
2297
+
2298
+ .homepage-col { max-width: 50rem; margin: 2.5rem auto 3rem; padding: 0 1rem; }
2299
+ .homepage-hero { text-align: center; margin-bottom: 2.4rem; }
2300
+ .homepage-hero .kicker {
2301
+ display: inline-flex; align-items: center; gap: .5rem;
2302
+ font-family: 'Crimson Pro', serif; font-style: italic;
2303
+ font-size: 1.05rem; color: var(--c-text-muted);
2304
+ }
2305
+ .homepage-hero .kicker::before, .homepage-hero .kicker::after {
2306
+ content: ""; width: 28px; height: 1px; background: var(--c-border-hover);
2307
+ }
2308
+ .homepage-hero h1 {
2309
+ margin: 1rem 0 .6rem; font-size: 2.1rem; line-height: 1.3; font-weight: 700;
2310
+ background: linear-gradient(100deg, var(--c-accent) 10%, var(--c-accent2) 90%);
2311
+ -webkit-background-clip: text; background-clip: text; color: transparent;
2312
+ }
2313
+ .homepage-hero .subtitle { margin: 0 0 .35rem; font-size: 1.05rem; color: var(--c-text-muted); }
2314
+ .homepage-hero .engine-line { margin: 0; font-size: .95rem; font-weight: 600; color: var(--c-text-soft); }
2315
+ .homepage-hero .engine-line .en { color: var(--c-accent-strong); }
2316
+ .homepage-hero .lede {
2317
+ margin: 1.8rem 0 0; text-align: left; font-size: var(--font-size-base);
2318
+ line-height: 1.8; color: var(--c-text-muted);
2319
+ }
2320
+ .scenario-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 2rem; }
2321
+ @media (max-width: 640px) { .scenario-grid { grid-template-columns: 1fr; } }
2322
+ .scenario-card {
2323
+ all: unset; box-sizing: border-box; cursor: pointer;
2324
+ display: flex; gap: 1rem; align-items: flex-start; text-align: left;
2325
+ padding: 1.3rem 1.4rem;
2326
+ background: var(--c-surface);
2327
+ border: 1px solid rgba(var(--c-accent-rgb),.40) !important;
2328
+ border-radius: 16px;
2329
+ box-shadow: 0 16px 50px rgba(15,23,42,.20);
2330
+ transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
2331
+ }
2332
+ .scenario-card:hover, .scenario-card:focus-visible {
2333
+ border-color: rgba(var(--c-accent-rgb),.7) !important;
2334
+ background: linear-gradient(180deg, rgba(var(--c-accent-rgb),.06) 0%, var(--c-surface) 100%);
2335
+ }
2336
+ .scenario-card:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
2337
+ .scenario-card .sc-index {
2338
+ flex: none; font-family: 'Crimson Pro', serif; font-weight: 600;
2339
+ font-size: 1.8rem; line-height: 1; padding-top: .1rem;
2340
+ color: rgba(var(--c-accent-rgb),.4); transition: color 180ms ease;
2341
+ }
2342
+ .scenario-card:hover .sc-index, .scenario-card:focus-visible .sc-index { color: var(--c-accent); }
2343
+ .scenario-card .sc-body { min-width: 0; display: flex; flex-direction: column; }
2344
+ .scenario-card .sc-title { margin: 0 0 .35rem; font-size: 1.02rem; font-weight: 700; color: var(--c-text); line-height: 1.4; }
2345
+ .scenario-card .sc-desc { margin: 0; font-size: .85rem; color: var(--c-text-muted); line-height: 1.65; }
2346
+ .homepage-secondary { display: flex; justify-content: center; margin-top: 2rem; }
2347
+ .homepage-secondary .placeholder-link {
2348
+ display: inline-flex; align-items: center; gap: .4rem;
2349
+ font-size: .88rem; color: var(--c-text-subtle); cursor: not-allowed;
2350
+ }
2351
+ .homepage-secondary .placeholder-link svg { width: 1rem; height: 1rem; opacity: .6; }
2352
+ .homepage-footnote { margin-top: 2rem; text-align: center; font-size: .82rem; color: var(--c-text-subtle); }
2353
+ @media (prefers-reduced-motion: reduce) { .scenario-card { transition: none; } }
2354
+
2355
  /* ── 6. UPLOAD AREA(醒目卡片)─────────────────────────────────── */
2356
  /* 注意:max-width + margin auto 是舊版單欄置中版面留下的樣式,現在
2357
  upload-zone 是右側 action-col 裡的元件,要跟同一欄的 scenario-buttons
 
2816
  /* 多次觸發確保 Svelte hydration 後生效 */
2817
  [300, 800, 1500, 3000].forEach(function(t) { setTimeout(fixBubbleWrap, t); });
2818
 
2819
+ /* 首頁情境卡片(Phase 8):Gradio 沒有原生卡片元件,卡片本身是
2820
+ gr.HTML() 刻的純展示內容,點擊時模擬點擊對應的隱藏版原生按鈕
2821
+ (sc_btn_1..4,見 create_app() 的 .sc-bridge-btn),Python 端
2822
+ on_scenario_pick 的 .click() 綁定完全不用改 */
2823
+ function bindScenarioCards() {
2824
+ var grid = document.getElementById('homepage_scenario_grid');
2825
+ if (!grid || grid._bound) return;
2826
+ grid._bound = true;
2827
+ grid.addEventListener('click', function (e) {
2828
+ var card = e.target.closest('.scenario-card');
2829
+ if (!card) return;
2830
+ var n = card.getAttribute('data-scenario');
2831
+ var btn = document.getElementById('sc_btn_' + n);
2832
+ if (btn) btn.click();
2833
+ });
2834
+ }
2835
+ [300, 800, 1500, 3000].forEach(function(t) { setTimeout(bindScenarioCards, t); });
2836
+
2837
  /* 持續監聽 DOM 變化(每次訊息更新後重設) */
2838
  var mo = new MutationObserver(function(mutations) {
2839
  var needsFix = mutations.some(function(m) {
 
2896
  _APP_HEAD = _DARK_MODE_HEAD_CSS + _APP_HEAD_SCRIPT
2897
 
2898
 
2899
+ def _build_homepage_html() -> str:
2900
+ """首頁 4 張情境卡片(取代原本按鈕式選擇),資料來源沿用 SCENARIOS 字典。"""
2901
+ cards = []
2902
+ for n in ("1", "2", "3", "4"):
2903
+ sc = SCENARIOS[n]
2904
+ cards.append(
2905
+ '<div class="scenario-card" role="listitem" tabindex="0" '
2906
+ f'data-scenario="{n}">'
2907
+ f'<span class="sc-index en">{n.zfill(2)}</span>'
2908
+ '<span class="sc-body">'
2909
+ f'<span class="sc-title">{sc["title"]}</span>'
2910
+ f'<span class="sc-desc">{sc["desc"]}</span>'
2911
+ '</span></div>'
2912
+ )
2913
+ return (
2914
+ '<div class="homepage-col">'
2915
+ '<div class="homepage-hero">'
2916
+ '<span class="kicker">重譯文本量化分析平台</span>'
2917
+ '<h1>譯彩紛呈:重譯文本分析系統</h1>'
2918
+ '<p class="subtitle en">TransPrism: An Analytical Framework for Retranslation</p>'
2919
+ '<p class="engine-line"><span class="en">II</span> 分析引擎 <span class="en">SpectraEngine</span></p>'
2920
+ '<p class="lede">上傳已斷詞的譯本文本,系統將自動計算 <span class="en">75</span> 項語言指標並進行統計分析,'
2921
+ '協助您完成重譯研究的量化論證。請先選擇最符合您研究設計的比較情境:</p>'
2922
+ '</div>'
2923
+ f'<div class="scenario-grid" id="homepage_scenario_grid" role="list">{"".join(cards)}</div>'
2924
+ '<div class="homepage-secondary">'
2925
+ f'<span class="placeholder-link">{_icon("folder", 16)}讀取進度(功能開發中)</span>'
2926
+ '</div>'
2927
+ '<p class="homepage-footnote">選擇任一情境即開始分析;建議同時只執行一項分析,以確保運算速度。</p>'
2928
+ '</div>'
2929
+ )
2930
+
2931
+
2932
  def create_app():
2933
  with gr.Blocks(
2934
  title="譯彩紛呈 II 分析引擎 SpectraEngine",
 
2956
  # Tab 0: 主頁
2957
  # ══════════════════════════════════════════════════════════════
2958
  with gr.Tab("🏠 主頁"):
2959
+ # Phase 8:首頁改成 4 張情境卡片(gr.HTML()+JS橋接,Gradio 無原生
2960
+ # 卡片元件,點卡片=模擬點擊下方隱藏版 sc_btn_1..4,見
2961
+ # _APP_HEAD_SCRIPT 的 bindScenarioCards())。情境選定後這塊隱藏,
2962
+ # workspace_row(對話+操作區)才出現。
2963
+ with gr.Column(visible=True) as homepage_col:
2964
+ gr.HTML(_build_homepage_html())
2965
+ # 隱藏版情境按鈕:純作 JS 橋接目標(.sc-bridge-btn,見 CSS/
2966
+ # bindScenarioCards()),.click() 綁定邏輯完全不動。必須放在
2967
+ # 一開始就 visible=True 的容器裡(homepage_col),因為 Gradio
2968
+ # 對「建構時就 visible=False」的容器是整段不掛載進 DOM,不是
2969
+ # 只用 CSS display:none 藏起來——放在 workspace_row 裡會導致
2970
+ # 這幾顆按鈕在情境選定前根本不存在於 DOM,JS 找不到可點的對象。
2971
+ sc_btn_1 = gr.Button("1 人工翻譯 vs 機器翻譯", variant="secondary",
2972
+ elem_id="sc_btn_1", elem_classes=["sc-bridge-btn"])
2973
+ sc_btn_2 = gr.Button("2 全譯 vs 編譯/節譯/改寫", variant="secondary",
2974
+ elem_id="sc_btn_2", elem_classes=["sc-bridge-btn"])
2975
+ sc_btn_3 = gr.Button("3 同期不同譯者譯本比較", variant="secondary",
2976
+ elem_id="sc_btn_3", elem_classes=["sc-bridge-btn"])
2977
+ sc_btn_4 = gr.Button("4 不同時期重譯本的歷時比較", variant="secondary",
2978
+ elem_id="sc_btn_4", elem_classes=["sc-bridge-btn"])
2979
+
2980
  # 電腦版:左側對話視窗、右側操作區(按鈕/上傳卡片)並排;
2981
  # 手機寬度(見 CSS .main-layout-row 的 media query)改回上下堆疊。
2982
  # 對話視窗為畫面上唯一主要焦點;技術執行紀錄預設收合,
2983
  # 只在長時間運算(指標計算/統計分析/報告撰寫)時自動展開
2984
+ with gr.Row(elem_classes=["main-layout-row"], visible=False) as workspace_row:
2985
  with gr.Column(scale=6, min_width=320, elem_classes=["chat-col"]):
2986
  chatbot = gr.Chatbot(
2987
  label="對話視窗",
 
3012
  with gr.Column(scale=4, min_width=280, elem_classes=["action-col"]):
3013
  # 研究情境是封閉式的固定四選一,改用可點擊按鈕取代打字輸入
3014
  # 數字再解析,選定後即隱藏,對話視窗回歸純輸出角色
3015
+ # Phase 8:情境選擇按鈕已搬到 homepage_col(見上方,理由是
3016
+ # Gradio 對建構時 visible=False 的容器不掛載進 DOM,見該處
3017
+ # 註解)。這個 Column 目前是空的,只留來讓 on_scenario_pick/
3018
+ # on_reset_confirm 既有的可見性輸出繼續運作,不動更多 wiring;
3019
+ # Phase 8-F 全站清理時一併拿掉。
3020
  with gr.Column(elem_classes=["scenario-buttons"]) as scenario_btn_col:
3021
+ pass
 
 
 
3022
 
3023
  # Phase 7 過渡:此卡片永久隱藏,不再被任何 .click()/yield
3024
  # 設回 visible=True,保留純作程式碼備援(回退時把新的
 
3437
  # ── 研究情境選擇(封閉式固定四選一,按鈕直接觸發,不再解析打字輸入)──
3438
  def on_scenario_pick(sc_num, chat_history, log_history, state):
3439
  if state is None:
3440
+ yield chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
3441
  return
3442
 
3443
  sc_info = SCENARIOS[sc_num]
3444
  chat_history = chat_history + [_user_msg(sc_info["title"])]
3445
  log_history = _log_append(log_history, "使用者選擇研究情境中……")
3446
+ # 首頁一旦選定情境即隱藏,換成對話+操作區(workspace_row);
3447
+ # 這兩個 visibility 只需要在第一個 yield 就切換一次,後續 yield
3448
+ # 沿用 gr.update() 不動即可,不用每次都重複賦值。
3449
+ yield (chat_history, log_history, state, gr.update(), gr.update(), gr.update(), gr.update(),
3450
+ gr.update(visible=False), gr.update(visible=True))
3451
 
3452
  reply, state = handle_scenario_select(sc_num, state)
3453
  log_history = _log_append(log_history, "✅ STEP 1 情境選擇完成")
3454
  chat_history = chat_history + [_assistant_msg(reply)]
3455
+ yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
3456
+ gr.update(), gr.update())
3457
 
3458
  # STEP 2:確認訊息已經導向上傳卡片的「格式要求」區塊,不再另外用
3459
  # LLM 生成一則重複格式細節的引導訊息,避免對話視窗出現冗餘內容
3460
  state["current_step"] = 2
3461
  log_history = _log_append(log_history, "⏳ STEP 2 上傳引導中……")
3462
+ yield (chat_history, log_history, state, gr.update(), _todo_update(state), gr.update(), gr.update(),
3463
+ gr.update(), gr.update())
3464
 
3465
  state["done_steps"] = [1, 2]
3466
  state["current_step"] = 3
 
3473
  f"**格式要求**\n\n{sc_info['upload_prompt']}"
3474
  )
3475
  yield (chat_history, log_history, state, gr.update(visible=True),
3476
+ _todo_update(state), gr.update(visible=False), gr.update(value=hint),
3477
+ gr.update(), gr.update())
3478
 
3479
  for _sc_num, _sc_btn in (("1", sc_btn_1), ("2", sc_btn_2), ("3", sc_btn_3), ("4", sc_btn_4)):
3480
  _sc_btn.click(
3481
  fn=functools.partial(on_scenario_pick, _sc_num),
3482
  inputs=[chatbot, exec_log, session_state],
3483
+ outputs=[chatbot, exec_log, session_state, chat_upload_col, app_header_html, scenario_btn_col,
3484
+ upload_hint_md, homepage_col, workspace_row],
3485
  ).then(
3486
  fn=_populate_tabs,
3487
  inputs=[session_state],
 
3514
  gr.update(visible=False), # tab6 LLM輔助分析
3515
  gr.update(visible=False), # reset_confirm_row(關掉確認彈窗自己)
3516
  _accordion_update(state, new_log), # exec_log_accordion(收合並更新摘要文字)
3517
+ gr.update(visible=True), # homepage_col(Phase 8:重新開始=回到首頁選情境)
3518
+ gr.update(visible=False), # workspace_row
3519
  )
3520
 
3521
  reset_btn.click(fn=lambda: gr.update(visible=True), outputs=[reset_confirm_row])
 
3524
  fn=on_reset_confirm,
3525
  outputs=[chatbot, exec_log, session_state, app_header_html,
3526
  scenario_btn_col, chat_upload_col, chat_upload, metadata_row, tab2, tab3, tab6,
3527
+ reset_confirm_row, exec_log_accordion, homepage_col, workspace_row],
3528
  )
3529
 
3530
  # ── 檔案上傳確認(streaming generator)──────────────────────────────