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 (
+ '
'
+ )
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 = (
''
'
'
'
'
- '
尚未產生資料
'
- '
請先於「主頁」完成分析流程
'
+ '
尚未產生資料
'
+ '
請先於「主頁」完成分析流程
'
'
'
)
@@ -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=...) 注入的