un2266 commited on
Commit
b1088c8
·
1 Parent(s): 9280581

Phase 8-B:nav-col/chat-col/canvas-col 三欄式工作區

Browse files

workspace_row 從原本 chat-col(scale6)+action-col(scale4) 兩欄改成
nav-col(scale2,固定窄寬)+chat-col(scale4)+canvas-col(scale7)三欄:
- nav-col:開始新分析/確認彈窗、scenario_btn_col(空,留給wiring相容);結果
導覽留給 Phase 8-D
- chat-col:chatbot+chat_upload(常駐輸入框),exec_log_accordion 搬出
- canvas-col:頂部步驟列(app_header_html,原本在全頁最上方、首頁也會看到,
現在只在 workspace_row 顯示後才出現)+canvas-scroll(新增的捲動子容器,
裝 upload_row/metadata_row/exec_log_accordion)

過程中排查並修好三個真實 CSS bug(皆已寫入程式碼註解,供之後參考):
1. canvas-col 內容一度水平位移一整欄寬度——根因是 Gradio 的 .column 預設
帶 flex-wrap:wrap,一旦父層設固定 height,排不下的子元件會往右開新欄
而非垂直捲動;修法:flex-wrap:nowrap。
2. 連帶補上 overflow-x:hidden + min-width:0(flex 子項防撐爆,見程式工作
守則13.1)。
3. 步驟列想用 position:sticky 貼在 Canvas 頂端,但 Gradio 自動包的 wrapper
高度只比步驟列本身高一點點,sticky 的可移動範圍被那層 wrapper 框住,
捲動超過那一點高度就整個跟著捲走。改用結構性分離:步驟列留在
canvas-col 本身(不捲動),另外包一層 canvas-scroll 才是真正捲動區。

已用 Playwright 完整驗證:三欄正確渲染、對話/Canvas 各自獨立捲動不互相
干擾、步驟列全程固定可見、上傳→格式驗證→Metadata 表單全流程正常、開始
新分析可正確返回首頁。

Files changed (1) hide show
  1. app_agent.py +185 -104
app_agent.py CHANGED
@@ -2278,7 +2278,60 @@ div[role="log"][aria-label="chatbot conversation"],
2278
  /* ── 5b. COLUMN LAYOUT ─────────────────────────────────────────── */
2279
  /* gap: 0 讓對話視窗跟緊接著的技術執行紀錄 Accordion 完全貼合,
2280
  視覺上合併成一張卡片(見「融合」設計討論) */
2281
- .chat-col { display: flex !important; flex-direction: column !important; gap: 0 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2282
 
2283
  /* ── 5c. SCENARIO BUTTONS(取代原本打字輸入研究情境的輸入列)────────── */
2284
  .scenario-buttons {
@@ -2701,11 +2754,22 @@ a:hover { color: var(--c-link-hover); }
2701
  }
2702
 
2703
  /* ── 17. RESPONSIVE ────────────────────────────────────────────── */
2704
- /* 主頁左右並排(左:對話視窗,右:按鈕/上傳卡片)僅限桌面寬度;
2705
- 手機或窄視窗(≤900px)改回上下堆疊,避免右欄被擠壓到放不下上傳卡片 */
 
 
2706
  @media (max-width: 900px) {
2707
  .main-layout-row { flex-direction: column !important; }
2708
- .main-layout-row .action-col { width: 100% !important; flex: 1 1 auto !important; }
 
 
 
 
 
 
 
 
 
2709
  }
2710
  @media (max-width: 768px) {
2711
  .app-header h1 { font-size: 1.6rem !important; }
@@ -2948,9 +3012,6 @@ def create_app():
2948
  # launch(head=...) 注入的 <script>(見 _APP_HEAD_SCRIPT)。
2949
  ) as demo:
2950
 
2951
- # ── Header(含動態步驟列,取代原本右側「執行進度」面板)──────────
2952
- app_header_html = gr.HTML(_build_header_html())
2953
-
2954
  session_state = gr.State(None)
2955
 
2956
  with gr.Tabs():
@@ -2979,23 +3040,39 @@ def create_app():
2979
  sc_btn_4 = gr.Button("4 不同時期重譯本的歷時比較", variant="secondary",
2980
  elem_id="sc_btn_4", elem_classes=["sc-bridge-btn"])
2981
 
2982
- # 電腦版:左側對話視窗、右側操作區(按鈕/上傳卡片)並排;
2983
- # 手機寬度(見 CSS .main-layout-row 的 media query)改回上下堆疊。
2984
- # 對話視窗為畫面上唯一主要焦點;技術執行紀錄預設收合,
2985
- # 只在長時間運算(指標計算/統計分析/報告撰寫)時自動展開
2986
  with gr.Row(elem_classes=["main-layout-row"], visible=False) as workspace_row:
2987
- with gr.Column(scale=6, min_width=320, elem_classes=["chat-col"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2988
  chatbot = gr.Chatbot(
2989
  show_label=False,
2990
  height=400,
2991
  elem_classes=["main-chat"],
2992
  )
2993
- with gr.Accordion("技術執行紀錄", open=False, elem_id="exec_log_accordion") as exec_log_accordion:
2994
- exec_log = gr.Markdown(
2995
- value="",
2996
- elem_id="exec_log",
2997
- elem_classes=["exec-log"],
2998
- )
2999
  # Phase 8(千卉實測後回頭調整):對話輸入框改回「常駐顯示、
3000
  # 不負責上傳」——業主原本設計就是用拖曳/點擊上傳的卡片
3001
  # (見下方 upload_row),對話框目前也沒有其他打字用途
@@ -3012,94 +3089,98 @@ def create_app():
3012
  show_label=False,
3013
  )
3014
 
3015
- with gr.Column(scale=4, min_width=280, elem_classes=["action-col"]):
3016
- # 研究情境是封閉式的固定四選一,改用可點擊按鈕取代打字輸入
3017
- # 數字再解析,選定後即隱藏,對話視窗回歸純輸出角色
3018
- # Phase 8:情境選擇按鈕已搬到 homepage_col(見上方,理由是
3019
- # Gradio 對建構時 visible=False 的容器不掛載進 DOM,見該處
3020
- # 註解)。這個 Column 目前是空的,只留來讓 on_scenario_pick/
3021
- # on_reset_confirm 既有的可見性輸出繼續運作,不動更多 wiring;
3022
- # Phase 8-F 全站清理時一併拿掉。
3023
- with gr.Column(elem_classes=["scenario-buttons"]) as scenario_btn_col:
3024
- pass
3025
-
3026
- # Phase 8(千卉實測後回頭調整):這張卡片改回正式使用中的
3027
- # 上傳入口(拖曳/點擊上傳),符合業主原本設計;Phase 7
3028
- # 曾經把它永久隱藏、改用對話框上傳,這次反過來。程式邏輯
3029
- # 完全沒動,只是把可見性開關從 chat_upload_col 換回這裡。
3030
- with gr.Column(visible=False, elem_id="upload_zone", elem_classes=["upload-zone"]) as upload_row:
3031
- upload_hint_md = gr.Markdown(value="", elem_classes=["upload-hint"])
3032
- file_input = gr.File(
3033
- show_label=False,
3034
- file_types=[".txt"], file_count="multiple",
3035
- )
3036
- with gr.Row(elem_classes=["upload-actions"]):
3037
- cancel_btn = gr.Button("取消", scale=1, variant="secondary")
3038
- upload_btn = gr.Button("確認上傳", scale=2, variant="primary")
3039
-
3040
- # 「開始新分析」與其確認彈窗:換情境/重新開始都走這一套共用
3041
- # 機制(暫放這裡,Phase 8 左側導覽重排時會把按鈕搬過去,
3042
- # 邏輯不用重寫)。情境是封閉式按鈕選擇,不透過對話框打字,
3043
- # 所以「換情境」也統一走這顆按鈕,不恢復文字解析路徑。
3044
- reset_btn = gr.Button("🆕 開始新分析", elem_classes=["reset-btn"], variant="secondary")
3045
- with gr.Column(visible=False, elem_classes=["reset-confirm"]) as reset_confirm_row:
3046
- gr.Markdown("確定要開始新分析嗎?目前進度將會遺失。")
3047
- with gr.Row(elem_classes=["upload-actions"]):
3048
- reset_cancel_btn = gr.Button("取消", scale=1, variant="secondary")
3049
- reset_confirm_btn = gr.Button("確定重新開始", scale=2, variant="primary")
3050
-
3051
- # ── 語料 Metadata 說明表單(上傳後、分析前)──────────────────
3052
- with gr.Column(visible=False, elem_id="metadata_zone", elem_classes=["metadata-zone"]) as metadata_row:
3053
- gr.Markdown("### 📑 語料 Metadata 說明\n請於正式分析前說明語料來源、範圍與比較結構,將寫入研究報告。")
3054
-
3055
- gr.Markdown("**原文資訊**")
3056
- with gr.Row():
3057
- meta_title = gr.Textbox(label="原文標題", scale=2)
3058
- meta_author = gr.Textbox(label="作者", scale=2)
3059
- meta_lang = gr.Textbox(label="語言", scale=1)
3060
- with gr.Row():
3061
- meta_pubyear = gr.Textbox(label="出版年", scale=1)
3062
- meta_version = gr.Textbox(label="版本", scale=2)
3063
-
3064
- # 比較結構(僅情境 3、4 需要;情境 1、2 固定二群)
3065
- with gr.Group(visible=False) as meta_compare_group:
3066
- gr.Markdown("**比較結構**")
3067
- with gr.Row():
3068
- meta_ngroups = gr.Radio(
3069
- ["二群", "三群"], label="群組數量", value="二群", scale=1,
3070
- )
3071
- meta_method = gr.Radio(
3072
- ["群組間比較", "個體間比較"], label="比較方式",
3073
- value="群組間比較", scale=1,
3074
- )
3075
-
3076
- gr.Markdown("**群組層資訊**(共通屬性)")
3077
- meta_group_df = gr.Dataframe(
3078
- headers=["群組標籤", "翻譯類型", "翻譯方向", "譯者背景", "目標讀者"],
3079
- datatype=["str"] * 5,
3080
- col_count=(5, "fixed"),
3081
- row_count=(1, "dynamic"),
3082
- interactive=True,
3083
- label="群組層",
3084
- )
3085
 
3086
- gr.Markdown("**個體層資訊**(每個譯本/檔案)")
3087
- meta_indiv_df = gr.Dataframe(
3088
- headers=["檔案", "譯者/系統名稱", "出版年", "譯者母語", "後編輯程度", "重譯序位"],
3089
- datatype=["str"] * 6,
3090
- col_count=(6, "fixed"),
3091
- row_count=(1, "dynamic"),
3092
- interactive=True,
3093
- label="個體層",
3094
- )
3095
 
3096
- gr.Markdown(
3097
- "以下資訊會寫入報告開頭,您也可以先略過(略過後這次分析將不會用到這些補充說明)",
3098
- elem_classes=["upload-hint"],
3099
- )
3100
- with gr.Row(elem_classes=["upload-actions"]):
3101
- meta_cancel_btn = gr.Button("略過", scale=1, variant="secondary")
3102
- meta_confirm_btn = gr.Button("確認並開始分析", scale=2, variant="primary")
 
 
 
 
 
 
 
 
 
 
3103
 
3104
  # ══════════════════════════════════════════════════════════════
3105
  # 結果分頁:初始隱藏,分析完成後顯示
 
2278
  /* ── 5b. COLUMN LAYOUT ─────────────────────────────────────────── */
2279
  /* gap: 0 讓對話視窗跟緊接著的技術執行紀錄 Accordion 完全貼合,
2280
  視覺上合併成一張卡片(見「融合」設計討論) */
2281
+ /* Phase 8-B:三欄式工作區。nav-col 窄、固定寬度不隨視窗縮放;chat-col/
2282
+ canvas-col 各自獨立捲動(各自的內容不會因為對方變長而互相影響),高度
2283
+ 統一扣掉頁面上方 Tab 列與外距抓一個概略值,之後看實際渲染再微調 */
2284
+ .nav-col {
2285
+ flex: 0 0 220px !important;
2286
+ max-width: 220px !important;
2287
+ display: flex !important;
2288
+ flex-direction: column !important;
2289
+ gap: .6rem !important;
2290
+ }
2291
+ /* 真因排查記錄:Gradio 的 .column 預設會帶 flex-wrap:wrap。平常沒設高度時
2292
+ 不會有事,但這裡額外設了固定 height 讓內容用捲動取代撐高,一旦內容總高
2293
+ 度超過這個 height,flex-wrap:wrap 會把「排不下」的子元件換到*右邊新開一
2294
+ 欄*(水平並排),而不是照預期垂直捲動——這就是 canvas-col 一度出現內容
2295
+ 跑到欄外、往右位移一整欄寬度的真正原因(不是 sticky、也不是 overflow-x
2296
+ 單獨造成的,那兩個只是讓症狀被看見的地方)。修法:flex-wrap 明確關掉。 */
2297
+ .chat-col {
2298
+ display: flex !important;
2299
+ flex-direction: column !important;
2300
+ flex-wrap: nowrap !important;
2301
+ gap: 0 !important;
2302
+ height: calc(100vh - 150px) !important;
2303
+ overflow-x: hidden !important;
2304
+ overflow-y: auto !important;
2305
+ min-width: 0 !important;
2306
+ }
2307
+ /* canvas-col 本身「不」捲動——它只負責把步驟列(固定)跟 canvas-scroll
2308
+ (捲動)疊在一起、限制在同一個總高度內。踩坑記錄:原本想讓步驟列用
2309
+ position:sticky 貼在 canvas-col 頂端,但 sticky 元素只能在「自己最近的
2310
+ 容器」範圍內移動,而 Gradio 自動包在 gr.HTML() 外面的 wrapper 高度只比
2311
+ 步驟列本身高一點點,捲動超過那一點高度後 sticky 就跟著捲走、貼不住。
2312
+ 改成結構性分離:步驟列跟 canvas-scroll 是兩個獨立區塊,前者天生不參與
2313
+ 捲動,後者自己 overflow-y:auto,兩者高度互不相干,不用依賴 sticky。 */
2314
+ .canvas-col {
2315
+ display: flex !important;
2316
+ flex-direction: column !important;
2317
+ flex-wrap: nowrap !important;
2318
+ gap: 0 !important;
2319
+ height: calc(100vh - 150px) !important;
2320
+ overflow: hidden !important;
2321
+ min-width: 0 !important;
2322
+ }
2323
+ .canvas-col .app-header {
2324
+ flex-shrink: 0 !important;
2325
+ }
2326
+ .canvas-scroll {
2327
+ display: flex !important;
2328
+ flex-direction: column !important;
2329
+ flex-wrap: nowrap !important;
2330
+ flex: 1 1 auto !important;
2331
+ min-height: 0 !important;
2332
+ overflow-x: hidden !important;
2333
+ overflow-y: auto !important;
2334
+ }
2335
 
2336
  /* ── 5c. SCENARIO BUTTONS(取代原本打字輸入研究情境的輸入列)────────── */
2337
  .scenario-buttons {
 
2754
  }
2755
 
2756
  /* ── 17. RESPONSIVE ────────────────────────────────────────────── */
2757
+ /* 三欄式工作區(nav-col/chat-col/canvas-col)僅限桌面寬度;手機或窄視窗
2758
+ (≤900px)改回上下堆疊,且各欄不再各自獨立捲動(避免小螢幕出現多層
2759
+ 巢狀捲軸,體驗更差),改用頁面本身捲動。桌面版收合成 icon bar、手機版
2760
+ 漢堡抽屜留給 Phase 8-E 一起做 */
2761
  @media (max-width: 900px) {
2762
  .main-layout-row { flex-direction: column !important; }
2763
+ .main-layout-row .nav-col,
2764
+ .main-layout-row .chat-col,
2765
+ .main-layout-row .canvas-col {
2766
+ width: 100% !important;
2767
+ max-width: 100% !important;
2768
+ flex: 1 1 auto !important;
2769
+ height: auto !important;
2770
+ overflow-y: visible !important;
2771
+ }
2772
+ .canvas-scroll { overflow-y: visible !important; min-height: 0 !important; }
2773
  }
2774
  @media (max-width: 768px) {
2775
  .app-header h1 { font-size: 1.6rem !important; }
 
3012
  # launch(head=...) 注入的 <script>(見 _APP_HEAD_SCRIPT)。
3013
  ) as demo:
3014
 
 
 
 
3015
  session_state = gr.State(None)
3016
 
3017
  with gr.Tabs():
 
3040
  sc_btn_4 = gr.Button("4 不同時期重譯本的歷時比較", variant="secondary",
3041
  elem_id="sc_btn_4", elem_classes=["sc-bridge-btn"])
3042
 
3043
+ # Phase 8-B:三欄式工作區——nav-col(窄,導覽/重置)+chat-col
3044
+ # (對話視窗+輸入框,獨立捲動)+canvas-col(步驟列+上傳/表單/
3045
+ # 執行紀錄,獨立捲動)。手機寬度見 CSS 的 media query 改上下堆疊。
 
3046
  with gr.Row(elem_classes=["main-layout-row"], visible=False) as workspace_row:
3047
+ with gr.Column(scale=2, min_width=200, elem_classes=["nav-col"]) as nav_col:
3048
+ # 研究情境是封閉式的固定四選一,改用可點擊按鈕取代打字輸入
3049
+ # 數字再解析,選定後即隱藏,對話視窗回歸純輸出角色
3050
+ # Phase 8:情境選擇按鈕已搬到 homepage_col(見上方,理由是
3051
+ # Gradio 對建構時 visible=False 的容器不掛載進 DOM,見該處
3052
+ # 註解)。這個 Column 目前是空的,只留來讓 on_scenario_pick/
3053
+ # on_reset_confirm 既有的可見性輸出繼續運作,不動更多 wiring;
3054
+ # Phase 8-F 全站清理時一併拿掉。
3055
+ with gr.Column(elem_classes=["scenario-buttons"]) as scenario_btn_col:
3056
+ pass
3057
+
3058
+ # 「開始新分析」與其確認彈窗:換情境/重新開始都走這一套共用
3059
+ # 機制。情境是封閉式按鈕選擇,不透過對話框打字,所以「換
3060
+ # 情境」也統一走這顆按鈕,不恢復文字解析路徑。
3061
+ reset_btn = gr.Button("🆕 開始新分析", elem_classes=["reset-btn"], variant="secondary")
3062
+ with gr.Column(visible=False, elem_classes=["reset-confirm"]) as reset_confirm_row:
3063
+ gr.Markdown("確定要開始新分析嗎?目前進度將會遺失。")
3064
+ with gr.Row(elem_classes=["upload-actions"]):
3065
+ reset_cancel_btn = gr.Button("取消", scale=1, variant="secondary")
3066
+ reset_confirm_btn = gr.Button("確定重新開始", scale=2, variant="primary")
3067
+ # 結果導覽(🏠對話/📊語言指標分析/📈統計比較分析/📝LLM
3068
+ # 輔助分析)與下載區留給 Phase 8-D 一起做,這裡先不放。
3069
+
3070
+ with gr.Column(scale=4, min_width=300, elem_classes=["chat-col"]):
3071
  chatbot = gr.Chatbot(
3072
  show_label=False,
3073
  height=400,
3074
  elem_classes=["main-chat"],
3075
  )
 
 
 
 
 
 
3076
  # Phase 8(千卉實測後回頭調整):對話輸入框改回「常駐顯示、
3077
  # 不負責上傳」——業主原本設計就是用拖曳/點擊上傳的卡片
3078
  # (見下方 upload_row),對話框目前也沒有其他打字用途
 
3089
  show_label=False,
3090
  )
3091
 
3092
+ with gr.Column(scale=7, min_width=380, elem_classes=["canvas-col"]):
3093
+ # 頂部步驟列+情境標籤:原本在全頁最上方(首頁也會看到),
3094
+ # Phase 8-B 搬進 Canvas 內部,只在 workspace_row 顯示後
3095
+ # 才出現,首頁不再看到。
3096
+ # 注意:這裡不能靠 app_header_html 自己 position:sticky
3097
+ # 達到「頂部固定」——它的直接 wrapper(Gradio 自動包的
3098
+ # .block)高度只比它自己的內容高一點點,sticky 的可移動
3099
+ # 範圍被��層 wrapper 框住,捲動一小段(wrapper 多出來的
3100
+ # 高度)後就整個跟著捲走,不會真的貼在 Canvas 頂端不動
3101
+ # (已實測踩坑)。正確做法:步驟列留在捲動區外面(跟著
3102
+ # canvas-col 本身固定不動),下面另外包一層
3103
+ # canvas-scroll 才是真正會捲動的區域,兩者高度各自獨立。
3104
+ app_header_html = gr.HTML(_build_header_html())
3105
+
3106
+ with gr.Column(elem_classes=["canvas-scroll"]):
3107
+ # Phase 8(千卉實測後回頭調整):這張卡片改回正式使用
3108
+ # 中的上傳入口(拖曳/點擊上傳),符合業主原本設計;
3109
+ # Phase 7 曾經把它永久隱藏、改用對話框上傳,這次反
3110
+ # 過來。程式邏輯完全沒動,只是把可見性開關從
3111
+ # chat_upload_col 換回這裡。
3112
+ with gr.Column(visible=False, elem_id="upload_zone", elem_classes=["upload-zone"]) as upload_row:
3113
+ upload_hint_md = gr.Markdown(value="", elem_classes=["upload-hint"])
3114
+ file_input = gr.File(
3115
+ show_label=False,
3116
+ file_types=[".txt"], file_count="multiple",
3117
+ )
3118
+ with gr.Row(elem_classes=["upload-actions"]):
3119
+ cancel_btn = gr.Button("取消", scale=1, variant="secondary")
3120
+ upload_btn = gr.Button("確認上傳", scale=2, variant="primary")
3121
+
3122
+ # ── 語料 Metadata 說明表單(上傳後、分析前)───────
3123
+ with gr.Column(visible=False, elem_id="metadata_zone", elem_classes=["metadata-zone"]) as metadata_row:
3124
+ gr.Markdown("### 📑 語料 Metadata 說明\n請於正式分析前說明語料來源、範圍與比較結構,將寫入研究報告。")
3125
+
3126
+ gr.Markdown("**原文資訊**")
3127
+ with gr.Row():
3128
+ meta_title = gr.Textbox(label="原文標題", scale=2)
3129
+ meta_author = gr.Textbox(label="作者", scale=2)
3130
+ meta_lang = gr.Textbox(label="語言", scale=1)
3131
+ with gr.Row():
3132
+ meta_pubyear = gr.Textbox(label="出版年", scale=1)
3133
+ meta_version = gr.Textbox(label="版本", scale=2)
3134
+
3135
+ # 比較結構(僅情境 3、4 需要;情境 1、2 固定二群)
3136
+ with gr.Group(visible=False) as meta_compare_group:
3137
+ gr.Markdown("**比較結構**")
3138
+ with gr.Row():
3139
+ meta_ngroups = gr.Radio(
3140
+ ["二群", "三群"], label="群組數量", value="二群", scale=1,
3141
+ )
3142
+ meta_method = gr.Radio(
3143
+ ["群組間比較", "個體間比較"], label="比較方式",
3144
+ value="群組間比較", scale=1,
3145
+ )
3146
+
3147
+ gr.Markdown("**群組層資訊**(共通屬性)")
3148
+ meta_group_df = gr.Dataframe(
3149
+ headers=["群組標籤", "翻譯類型", "翻譯方向", "譯者背景", "目標讀者"],
3150
+ datatype=["str"] * 5,
3151
+ col_count=(5, "fixed"),
3152
+ row_count=(1, "dynamic"),
3153
+ interactive=True,
3154
+ label="群組層",
3155
+ )
 
 
 
 
 
 
3156
 
3157
+ gr.Markdown("**個體層資訊**(每個譯本/檔案)")
3158
+ meta_indiv_df = gr.Dataframe(
3159
+ headers=["檔案", "譯者/系統名稱", "出版年", "譯者母語", "後編輯程度", "重譯序位"],
3160
+ datatype=["str"] * 6,
3161
+ col_count=(6, "fixed"),
3162
+ row_count=(1, "dynamic"),
3163
+ interactive=True,
3164
+ label="個體層",
3165
+ )
3166
 
3167
+ gr.Markdown(
3168
+ "以下資訊會寫入報告開頭,您也可以先略過(略過後這次分析將不會用到這些補充說明)",
3169
+ elem_classes=["upload-hint"],
3170
+ )
3171
+ with gr.Row(elem_classes=["upload-actions"]):
3172
+ meta_cancel_btn = gr.Button("略過", scale=1, variant="secondary")
3173
+ meta_confirm_btn = gr.Button("確認並開始分析", scale=2, variant="primary")
3174
+
3175
+ # 技術執行紀錄:Phase 8-B 從 chat-col 搬到 Canvas 底
3176
+ # 部,跟中段內容同一塊可捲動捲軸,非 sticky,點擊
3177
+ # 往下展開。
3178
+ with gr.Accordion("技術執行紀錄", open=False, elem_id="exec_log_accordion") as exec_log_accordion:
3179
+ exec_log = gr.Markdown(
3180
+ value="",
3181
+ elem_id="exec_log",
3182
+ elem_classes=["exec-log"],
3183
+ )
3184
 
3185
  # ══════════════════════════════════════════════════════════════
3186
  # 結果分頁:初始隱藏,分析完成後顯示