Spaces:
Sleeping
Sleeping
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 驗證:首頁四卡渲染正確、點卡進入原有情境選擇流程(對話訊息
與格式要求皆正確)、開始新分析可正確返回首頁。
- 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
| 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)──────────────────────────────
|