diff --git "a/app_agent.py" "b/app_agent.py" --- "a/app_agent.py" +++ "b/app_agent.py" @@ -99,11 +99,9 @@ SCENARIOS = { "title": "同一原文的人工翻譯 vs 機器翻譯", "short": "人機對比", "upload_prompt": ( - "請上傳欲分析的文本,文本檔名設計用 **A_xx.txt、B_xx.txt** " - "作為區分人工或機器翻譯。\n" - "上傳的文本必須使用 [ckip_segmentation_sumin](https://github.com/suhsiung/ckip_segmentation_sumin) " - "工具完成斷詞後的 `.txt` 檔案," - "內容格式為:`詞_詞性`、`_詞性` 或空白行。" + "1. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分人工翻譯與機器翻譯。\n" + "2. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n" + "3. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。" ), "desc": "焦點在人機差異的語言特徵。", "scenario_module_key": "scenario_2", @@ -112,11 +110,9 @@ SCENARIOS = { "title": "全譯 vs 編譯(節譯、改寫)", "short": "全譯vs編譯", "upload_prompt": ( - "請上傳欲分析的文本,文本檔名設計用 **A_xx.txt、B_xx.txt** " - "作為區分全譯或編譯。\n" - "上傳的文本必須使用 [ckip_segmentation_sumin](https://github.com/suhsiung/ckip_segmentation_sumin) " - "工具完成斷詞後的 `.txt` 檔案," - "內容格式為:`詞_詞性`、`_詞性` 或空白行。" + "1. 檔名規範:請以 **A_xx.txt**、**B_xx.txt** 等格式區分全譯與編譯。\n" + "2. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n" + "3. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。" ), "desc": "焦點在文本完整性和改寫策略。", "scenario_module_key": "scenario_3", @@ -125,11 +121,9 @@ SCENARIOS = { "title": "不同譯者的同期譯本", "short": "譯者風格", "upload_prompt": ( - "請上傳欲分析的文本,文本檔名設計用 **A_xx.txt、B_xx.txt、C_xx.txt…** " - "作為區分不同的譯者。\n" - "上傳的文本必須使用 [ckip_segmentation_sumin](https://github.com/suhsiung/ckip_segmentation_sumin) " - "工具完成斷詞後的 `.txt` 檔案," - "內容格式為:`詞_詞性`、`_詞性` 或空白行。" + "1. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同群組的譯者。\n" + "2. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n" + "3. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。" ), "desc": "焦點在個人風格差異。排除時代因素,討論譯者個人的語言偏好。", "scenario_module_key": "scenario_4", @@ -138,11 +132,9 @@ SCENARIOS = { "title": "同一原文的不同時期人工譯本", "short": "歷時重譯", "upload_prompt": ( - "請上傳欲分析的文本,文本檔名設計用 **A_xx.txt、B_xx.txt、C_xx.txt…** " - "作為區分時期。\n" - "上傳的文本必須使用 [ckip_segmentation_sumin](https://github.com/suhsiung/ckip_segmentation_sumin) " - "工具完成斷詞後的 `.txt` 檔案," - "內容格式為:`詞_詞性`、`_詞性` 或空白行。" + "1. 檔名規範:請以 **A_xx.txt**、**B_xx.txt**、**C_xx.txt** 等格式區分不同時期的譯本。\n" + "2. 斷詞要求:必須先使用 I 語料淬鍊 工具完成斷詞。\n" + "3. 內容格式:僅限 `詞_詞性`、`_詞性` 或空白行。" ), "desc": "分析焦點在歷時變化、語言現代化、翻譯規範的演變。時間維度是核心變數。", "scenario_module_key": "scenario_1", @@ -1381,74 +1373,57 @@ def make_initial_state(session_id: str) -> dict: } -def get_intro_message() -> str: - sys_p = ( - "你是「重譯文本分析系統」的主 Agent(智能助理)," - "請用繁體中文、專業而友善的語氣向使用者介紹本系統," - "並說明四種研究情境,最後邀請使用者選擇情境。" - "回覆請控制在 400 字以內。" - ) - user_p = ( - "請介紹本系統並列出以下四種研究情境,請使用者選擇:\n" - "1. 同一原文的人工翻譯 vs 機器翻譯\n" - "2. 全譯 vs 編譯(節譯、改寫)\n" - "3. 不同譯者的同期譯本\n" - "4. 同一原文的不同時期人工譯本\n" - "詢問使用者:「請輸入數字 1、2、3 或 4 選擇您要進行的研究情境。」" - ) - try: - return call_main_agent(sys_p, user_p) - except Exception as e: - return ( - "歡迎使用 TransPrism II 分析引擎 SpectraEngine。\n\n" - "請依照您的研究設計選擇分析情境:\n\n" - "1 人工翻譯 vs 機器翻譯\n" - "2 全譯 vs 編譯/節譯/改寫\n" - "3 同期不同譯者譯本比較\n" - "4 不同時期重譯本的歷時比較\n\n" - "輸入 1、2、3 或 4 。" - ) +_STEPS = [ + (1, "情境選擇"), + (2, "上傳引導"), + (3, "文本上傳"), + (4, "格式驗證"), + (5, "指標計算"), + (6, "統計分析"), + (7, "報告撰寫"), + (8, "下載成果"), +] -def _build_todo_list(sc_num: str, done_steps: list = None, current_step: int = None) -> str: + +def _build_header_html(done_steps: list = None, current_step: int = None) -> str: """ - 即時 to-do list 執行進度(確定性,不依賴 LLM)。 + 產生完整標題區塊 HTML(標題+動態步驟列+進度條),確定性、不依賴 LLM。 + 取代原本寫死不會變的步驟列,以及原本另外放在右欄、跟這裡資訊重複的 + 「執行進度」面板——進度資訊現在只在這裡出現一次。 done_steps: 已完成步驟(1-based list) current_step: 目前執行步驟(1-based int) """ done_steps = done_steps or [] - sc_info = SCENARIOS.get(sc_num, {}) - sc_title = sc_info.get("short", sc_info.get("title", "")) - - steps = [ - (1, "情境選擇"), - (2, "上傳引導"), - (3, "文本上傳"), - (4, "格式驗證"), - (5, "指標計算"), - (6, "統計分析"), - (7, "報告撰寫"), - (8, "成果下載"), - ] - - total = len(steps) - done_count = len([s for s in done_steps if s <= total]) - pct = int(done_count / total * 100) - - lines = [ - f"**執行進度|{sc_title}**", - f"`{'█' * (done_count * 2)}{'░' * ((total - done_count) * 2)}` {pct}%", - "", - ] - for num, name in steps: + pills = [] + for num, name in _STEPS: if num in done_steps: - lines.append(f"✅ ~~{num}. {name}~~") + pills.append( + f'' + f'✓{name}' + ) elif num == current_step: - lines.append(f"⏳ **{num}. {name}**") + pills.append( + f'' + f'{num}{name}' + ) else: - lines.append(f"☐ {num}. {name}") + pills.append( + f'' + f'{num}{name}' + ) + + pct = int(len(done_steps) / len(_STEPS) * 100) - return "\n\n".join(lines) + return ( + '
' + '

譯彩紛呈:重譯文本分析系統

' + '

TransPrism: An Analytical Framework for Retranslation

' + '

II 分析引擎 SpectraEngine

' + f'
{"".join(pills)}
' + f'
' + '
' + ) def handle_scenario_select(user_input: str, state: dict): @@ -1464,39 +1439,17 @@ def handle_scenario_select(user_input: str, state: dict): state["done_steps"] = [1] state["current_step"] = 2 - sys_p = ( - "你是重譯文本分析系統的主 Agent,請用繁體中文確認使用者的研究情境選擇," - "並說明接下來將由工作程序 Agent 協助上傳文本。語氣專業友善,50 字以內。" + # 固定文字,不經 LLM:確保每次措辭一致,並直接引導使用者去看上傳卡片裡的 + # 格式要求,不需要另外由工作程序 Agent 生成一則重複格式細節的引導訊息 + confirm_text = ( + f"已確認您的研究情境為「{sc_num}. {sc_info['title']}」。" + "接下來請您上傳文本檔案。" ) - user_p = f"使用者選擇了研究情境 {sc_num}:{sc_info['title']}" - try: - confirm_text = call_main_agent(sys_p, user_p) - except Exception: - confirm_text = f"已記錄研究情境:**{sc_info['title']}**。" state["state"] = "UPLOAD_GUIDE" return (confirm_text, state) -def get_upload_guide_message(scenario_num: str) -> str: - """工作程序 Agent 根據情境提供上傳說明。""" - sc_info = SCENARIOS[scenario_num] - sys_p = ( - "你是重譯文本分析系統的工作程序 Agent,請用繁體中文說明文本上傳格式要求。" - "語氣清晰、條列式,120 字以內。" - "必須強調:文本必須使用 https://github.com/suhsiung/ckip_segmentation_sumin 工具完成斷詞," - "才能上傳。最後提示使用者上傳檔案。" - ) - user_p = f"研究情境:{sc_info['title']}\n上傳說明:\n{sc_info['upload_prompt']}" - try: - return call_worker_agent(sys_p, user_p) - except: - return ( - f"**【工作程序 Agent】文本上傳說明**\n\n" - f"{sc_info['upload_prompt']}\n\n" - "請在下方選擇並上傳文本檔案(可多選)。" - ) - def handle_uploaded_files(files, state: dict): """ @@ -1671,15 +1624,15 @@ def run_computation_pipeline_streaming(state: dict): _EMPTY_HINT = ( '
' '
' '' + 'viewBox="0 0 24 24" stroke="var(--c-accent)" stroke-width="1.5">' '' '
' - '

尚未產生資料

' - '

請先於「主頁」完成分析流程

' + '

尚未產生資料

' + '

請先於「主頁」完成分析流程

' '
' ) @@ -1694,20 +1647,97 @@ _CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&display=swap'); /* ── 1. TOKENS & RESET ─────────────────────────────────────────── */ -:root, .dark { - --c-bg: #020617; - --c-surface: #0d1526; - --c-surface2: #111827; - --c-border: rgba(14,165,233,.11); - --c-border-hover: rgba(14,165,233,.30); - --c-accent: #0ea5e9; - --c-accent2: #818cf8; - --c-accent3: #c084fc; - --c-text: #e2e8f0; - --c-text-muted: #94a3b8; - --c-text-subtle: #475569; - --c-success: #34d399; - --c-warn: #fbbf24; +/* 預設淺色;系統設定為深色模式時(或明確帶 .dark class)自動切換深色 */ +:root { + --c-bg: #f8fafc; + --c-surface: #ffffff; + --c-surface2: #f1f5f9; + --c-surface3: #ffffff; + --c-surface3-border: #e2e8f0; + --c-border: rgba(2,132,199,.16); + --c-border-hover: rgba(2,132,199,.35); + --c-accent: #0284c7; + --c-accent-strong: #0369a1; + --c-accent2: #4f46e5; + --c-accent2-strong: #4338ca; + --c-accent3: #9333ea; + --c-accent-rgb: 2,132,199; + --c-accent2-rgb: 79,70,229; + --c-accent3-rgb: 147,51,234; + --c-bg-rgb: 255,255,255; + --c-text: #1e293b; + --c-text-soft: #334155; + --c-text-muted: #475569; + --c-text-subtle: #64748b; + --c-success: #047857; + --c-success-rgb: 4,120,87; + --c-warn: #b45309; + --c-user-bg: #dcfce7; + --c-user-border: #86efac; + --c-user-text: #166534; + --c-code-bg: #e0f2fe; + --c-code-text: #b45309; + --c-input-bg: #ffffff; + --c-input-border: rgba(2,132,199,.22); + --c-link-hover: #075985; + --c-scrollbar: #cbd5e1; + --c-scrollbar-hover: #94a3b8; + + --body-background-fill: var(--c-bg) !important; + --background-fill-primary: var(--c-surface) !important; + --background-fill-secondary: #eef2f7 !important; + --block-background-fill: var(--c-surface) !important; + --block-border-color: var(--c-border) !important; + --border-color-primary: var(--c-border) !important; + --color-accent: var(--c-accent) !important; + --color-accent-soft: rgba(2,132,199,.08) !important; + --body-text-color: var(--c-text) !important; + --block-label-text-color: var(--c-text-muted) !important; + --block-title-text-color: var(--c-text-soft) !important; + --input-background-fill: var(--c-input-bg) !important; + --input-border-color: var(--c-input-border) !important; + --button-primary-background-fill: var(--c-accent) !important; + --button-primary-text-color: #fff !important; + --shadow-drop: 0 1px 3px rgba(15,23,42,.10) !important; +} +/* 注意:系統深色模式的 @media 覆寫改放在 launch(head=...) 注入的獨立 +""" + +# gr.Blocks(js=...) 在此版 Gradio 實測不會執行,所有需要在頁面載入後跑的 JS +# 一律改用 launch(head=...) 注入 +""" + +_APP_HEAD = _DARK_MODE_HEAD_CSS + _APP_HEAD_SCRIPT + def create_app(): with gr.Blocks( - title="重譯文本分析系統Agent", + title="譯彩紛呈 II 分析引擎 SpectraEngine", theme=gr.themes.Base( primary_hue=gr.themes.colors.sky, secondary_hue=gr.themes.colors.indigo, @@ -2390,70 +2714,14 @@ def create_app(): font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], ), css=_CUSTOM_CSS, - js="""() => { - document.body.classList.add('dark'); - document.title = '重譯文本分析系統Agent'; - - function fixBubbleWrap() { - /* 直接鎖定 .bubble-wrap,強制 column + flex-start */ - document.querySelectorAll('.main-chat .bubble-wrap, .main-chat div[role="log"]').forEach(function(el) { - el.style.setProperty('flex-direction', 'column', 'important'); - el.style.setProperty('justify-content', 'flex-start', 'important'); - el.style.setProperty('align-items', 'flex-start', 'important'); - el.style.setProperty('align-content', 'flex-start', 'important'); - el.scrollTop = 0; - }); - } - - /* 多次觸發確保 Svelte hydration 後生效 */ - [300, 800, 1500, 3000].forEach(function(t) { setTimeout(fixBubbleWrap, t); }); - - /* 持續監聽 DOM 變化(每次訊息更新後重設) */ - var mo = new MutationObserver(function(mutations) { - var needsFix = mutations.some(function(m) { - return m.target.closest && m.target.closest('.main-chat'); - }); - if (needsFix) { fixBubbleWrap(); } - }); - setTimeout(function() { - var root = document.querySelector('.main-chat'); - if (root) mo.observe(root, { childList: true, subtree: true, attributes: true }); - }, 500); - - /* metadata 表單:出現時自動捲入視野;消失時捲回頂部 */ - var metaWasVisible = false; - function watchMetadata() { - var zone = document.getElementById('metadata_zone'); - var visible = !!(zone && zone.offsetParent !== null); - if (visible && !metaWasVisible) { - zone.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } else if (!visible && metaWasVisible) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - metaWasVisible = visible; - } - setInterval(watchMetadata, 400); -}""", + # 注意:gr.Blocks(js=...) 在目前這版 Gradio 實測不會執行(已用 + # document.querySelectorAll('.main-chat .bubble-wrap')[0].getAttribute('style') + # 驗證從未被寫入過 inline style),因此所有互動用 JS 都改放到 + # launch(head=...) 注入的