修改task_id生成逻辑

This commit is contained in:
xunbu
2025-07-15 13:55:03 +08:00
parent 8745329d2c
commit 93004e9838
2 changed files with 187 additions and 235 deletions

View File

@@ -6,6 +6,7 @@ import logging
import os import os
import socket import socket
import time import time
import uuid
from contextlib import asynccontextmanager, closing from contextlib import asynccontextmanager, closing
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any, Optional, Literal, Union from typing import List, Dict, Any, Optional, Literal, Union
@@ -281,11 +282,11 @@ DocuTranslate 后端服务 API提供文档翻译、状态查询、结果下
**注意**: 所有任务状态都保存在服务进程的内存中,服务重启将导致所有任务信息丢失。 **注意**: 所有任务状态都保存在服务进程的内存中,服务重启将导致所有任务信息丢失。
### 主要工作流程: ### 主要工作流程:
1. **`POST /service/translate`**: 提交文件和翻译参数,启动一个后台任务,并获取 `task_id`。 1. **`POST /service/translate`**: 提交文件和翻译参数,启动一个后台任务。服务会自动生成并返回一个唯一的 `task_id`。
2. **`GET /service/status/{{task_id}}`**: 使用 `task_id` 轮询此端点,获取任务的实时状态。 2. **`GET /service/status/{{task_id}}`**: 使用获取到的 `task_id` 轮询此端点,获取任务的实时状态。
3. **`GET /service/logs/{{task_id}}`**: (可选) 获取实时的翻译日志。 3. **`GET /service/logs/{{task_id}}`**: (可选) 获取实时的翻译日志。
4. **`GET /service/download/{{task_id}}/{{file_type}}`**: 任务完成后 (当 `download_ready` 为 `true` 时),通过此端点下载结果文件。 4. **`GET /service/download/{{task_id}}/{{file_type}}`**: 任务完成后 (当 `download_ready` 为 `true` 时),通过此端点下载结果文件。
5. **`GET /service/download_content/{{task_id}}/{{file_type}}`**: 任务完成后以JSON格式获取文件内容。 5. **`GET /service/content/{{task_id}}/{{file_type}}`**: 任务完成后(当 `download_ready` 为 `true` 时)以JSON格式获取文件内容。
6. **`POST /service/cancel/{{task_id}}`**: (可选) 取消一个正在进行的任务。 6. **`POST /service/cancel/{{task_id}}`**: (可选) 取消一个正在进行的任务。
7. **`POST /service/release/{{task_id}}`**: (可选) 当任务不再需要时,释放其在服务器上占用的所有资源。 7. **`POST /service/release/{{task_id}}`**: (可选) 当任务不再需要时,释放其在服务器上占用的所有资源。
@@ -302,14 +303,9 @@ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# =================================================================== # ===================================================================
# --- Pydantic Models for Service API --- # --- Pydantic Models for Service API (MODIFIED) ---
# =================================================================== # ===================================================================
class TranslateServiceRequest(BaseModel): class TranslateServiceRequest(BaseModel):
task_id: str = Field(
default="0",
description="任务的唯一标识符。用于后续跟踪任务状态和结果。",
examples=["task-b2865b93"]
)
base_url: str = Field( base_url: str = Field(
..., ...,
description="LLM API的基础URL例如 OpenAI, deepseek, 或任何兼容OpenAI的接口。", description="LLM API的基础URL例如 OpenAI, deepseek, 或任何兼容OpenAI的接口。",
@@ -386,7 +382,6 @@ class TranslateServiceRequest(BaseModel):
class Config: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"task_id": "task-b2865b93-85d7-40a8-b118-a61048698585",
"base_url": "https://api.openai.com/v1", "base_url": "https://api.openai.com/v1",
"apikey": "sk-your-api-key-here", "apikey": "sk-your-api-key-here",
"model_id": "gpt-4o", "model_id": "gpt-4o",
@@ -407,7 +402,7 @@ class TranslateServiceRequest(BaseModel):
# =================================================================== # ===================================================================
# --- Service Endpoints (/service) --- # --- Service Endpoints (/service) (MODIFIED) ---
# =================================================================== # ===================================================================
@service_router.post( @service_router.post(
@@ -417,20 +412,20 @@ class TranslateServiceRequest(BaseModel):
接收一个包含文件内容Base64编码和翻译参数的JSON请求启动一个后台翻译任务。 接收一个包含文件内容Base64编码和翻译参数的JSON请求启动一个后台翻译任务。
- **异步处理**: 此端点会立即返回,不会等待翻译完成。 - **异步处理**: 此端点会立即返回,不会等待翻译完成。
- **任务ID**: 成功启动后,返回任务ID (`task_id`)。 - **任务ID**: 成功启动后,服务会自动生成并返回任务ID (`task_id`)。
- **后续步骤**: 客户端应使用返回的 `task_id` 轮询 `/service/status/{task_id}` 接口来获取任务进度和结果。 - **后续步骤**: 客户端应使用返回的 `task_id` 轮询 `/service/status/{task_id}` 接口来获取任务进度和结果。
""", """,
responses={ responses={
200: { 200: {
"description": "翻译任务成功启动。", "description": "翻译任务成功启动。",
"content": {"application/json": {"example": {"task_started": True, "task_id": "task-b2865b93", "content": {"application/json": {"example": {"task_started": True, "task_id": "b2865b93",
"message": "翻译任务已成功启动,请稍候..."}}} "message": "翻译任务已成功启动,请稍候..."}}}
}, },
400: {"description": "请求体中的Base64文件内容无效。", 400: {"description": "请求体中的Base64文件内容无效。",
"content": {"application/json": {"example": {"detail": "无效的Base64文件内容: Incorrect padding"}}}}, "content": {"application/json": {"example": {"detail": "无效的Base64文件内容: Incorrect padding"}}}},
429: {"description": "同一任务ID已在进行中无法重复提交", "content": { 429: {"description": "服务器内部任务冲突,请重试", "content": {
"application/json": { "application/json": {
"example": {"task_started": False, "message": "任务ID 'task-b2865b93' 正在进行中,请稍后再试。"}}}}, "example": {"task_started": False, "message": "任务ID 'b2865b93' 正在进行中,请稍后再试。"}}}},
500: {"description": "服务器内部错误,导致任务启动失败。", 500: {"description": "服务器内部错误,导致任务启动失败。",
"content": { "content": {
"application/json": { "application/json": {
@@ -440,25 +435,27 @@ class TranslateServiceRequest(BaseModel):
async def service_translate(request: TranslateServiceRequest = Body(..., description="翻译任务的详细参数和文件内容。")): async def service_translate(request: TranslateServiceRequest = Body(..., description="翻译任务的详细参数和文件内容。")):
""" """
提交一个文件进行翻译,并启动一个后台任务。 提交一个文件进行翻译,并启动一个后台任务。
文件内容需以Base64编码。 文件内容需以Base64编码任务ID将由后端自动生成并返回
返回任务ID后续可凭此ID查询状态和下载结果。 后续可凭此ID查询状态和下载结果。
""" """
task_id = uuid.uuid4().hex[:8]
try: try:
file_contents = base64.b64decode(request.file_content) file_contents = base64.b64decode(request.file_content)
except (binascii.Error, TypeError) as e: except (binascii.Error, TypeError) as e:
raise HTTPException(status_code=400, detail=f"无效的Base64文件内容: {e}") raise HTTPException(status_code=400, detail=f"无效的Base64文件内容: {e}")
params = request.model_dump(exclude={'file_name', 'file_content', 'task_id'}) params = request.model_dump(exclude={'file_name', 'file_content'})
try: try:
response_data = await _start_translation_task( response_data = await _start_translation_task(
task_id=request.task_id, task_id=task_id,
params=params, params=params,
file_contents=file_contents, file_contents=file_contents,
original_filename=request.file_name original_filename=request.file_name
) )
return JSONResponse(content=response_data) return JSONResponse(content=response_data)
except HTTPException as e: except HTTPException as e:
# Re-raise as JSONResponse to fit the documented response model # 重新包装为JSONResponse以匹配文档中的响应模型
if e.status_code == 429: if e.status_code == 429:
return JSONResponse(status_code=e.status_code, content={"task_started": False, "message": e.detail}) return JSONResponse(status_code=e.status_code, content={"task_started": False, "message": e.detail})
if e.status_code == 500: if e.status_code == 500:
@@ -488,7 +485,7 @@ async def service_translate(request: TranslateServiceRequest = Body(..., descrip
} }
) )
async def service_cancel_translate( async def service_cancel_translate(
task_id: str = FastApiPath(..., description="要取消的任务的ID", example="task-b2865b93")): task_id: str = FastApiPath(..., description="要取消的任务的ID", example="b2865b93")):
"""根据任务ID取消一个正在进行的翻译任务。""" """根据任务ID取消一个正在进行的翻译任务。"""
try: try:
response_data = _cancel_translation_logic(task_id) response_data = _cancel_translation_logic(task_id)
@@ -511,7 +508,7 @@ async def service_cancel_translate(
200: { 200: {
"description": "任务资源已成功释放。", "description": "任务资源已成功释放。",
"content": { "content": {
"application/json": {"example": {"released": True, "message": "任务 'task-b2865b93' 的资源已释放。"}} "application/json": {"example": {"released": True, "message": "任务 'b2865b93' 的资源已释放。"}}
} }
}, },
404: { 404: {
@@ -522,7 +519,7 @@ async def service_cancel_translate(
} }
) )
async def service_release_task( async def service_release_task(
task_id: str = FastApiPath(..., description="要释放资源的任务的ID", example="task-b2865b93") task_id: str = FastApiPath(..., description="要释放资源的任务的ID", example="b2865b93")
): ):
"""根据任务ID释放其占用的所有服务器资源。""" """根据任务ID释放其占用的所有服务器资源。"""
if task_id not in tasks_state: if task_id not in tasks_state:
@@ -575,7 +572,7 @@ async def service_release_task(
"processing": { "processing": {
"summary": "处理中", "summary": "处理中",
"value": { "value": {
"task_id": "task-b2865b93", "task_id": "b2865b93",
"is_processing": True, "is_processing": True,
"status_message": "正在翻译: 15/50 块", "status_message": "正在翻译: 15/50 块",
"error_flag": False, "error_flag": False,
@@ -594,7 +591,7 @@ async def service_release_task(
"completed": { "completed": {
"summary": "已完成", "summary": "已完成",
"value": { "value": {
"task_id": "task-b2865b93", "task_id": "b2865b93",
"is_processing": False, "is_processing": False,
"status_message": "翻译成功!用时 123.45 秒。", "status_message": "翻译成功!用时 123.45 秒。",
"error_flag": False, "error_flag": False,
@@ -604,16 +601,16 @@ async def service_release_task(
"task_start_time": 1678886400.123, "task_start_time": 1678886400.123,
"task_end_time": 1678886523.573, "task_end_time": 1678886523.573,
"downloads": { "downloads": {
"markdown": "/service/download/task-b2865b93/markdown", "markdown": "/service/download/b2865b93/markdown",
"markdown_zip": "/service/download/task-b2865b93/markdown_zip", "markdown_zip": "/service/download/b2865b93/markdown_zip",
"html": "/service/download/task-b2865b93/html" "html": "/service/download/b2865b93/html"
} }
} }
}, },
"error": { "error": {
"summary": "出错", "summary": "出错",
"value": { "value": {
"task_id": "task-b2865b93", "task_id": "b2865b93",
"is_processing": False, "is_processing": False,
"status_message": "翻译过程中发生错误 (用时 45.67 秒): APIConnectionError(...)", "status_message": "翻译过程中发生错误 (用时 45.67 秒): APIConnectionError(...)",
"error_flag": True, "error_flag": True,
@@ -640,7 +637,7 @@ async def service_release_task(
} }
) )
async def service_get_status( async def service_get_status(
task_id: str = FastApiPath(..., description="要查询状态的任务的ID", example="task-b2865b93")): task_id: str = FastApiPath(..., description="要查询状态的任务的ID", example="b2865b93")):
"""根据任务ID获取任务的当前状态和结果下载链接。""" """根据任务ID获取任务的当前状态和结果下载链接。"""
task_state = tasks_state.get(task_id) task_state = tasks_state.get(task_id)
if not task_state: if not task_state:
@@ -692,7 +689,7 @@ async def service_get_status(
} }
) )
async def service_get_logs( async def service_get_logs(
task_id: str = FastApiPath(..., description="要获取日志的任务的ID", example="task-b2865b93")): task_id: str = FastApiPath(..., description="要获取日志的任务的ID", example="b2865b93")):
"""获取指定任务ID自上次查询以来的新日志。""" """获取指定任务ID自上次查询以来的新日志。"""
if task_id not in tasks_log_queues: if task_id not in tasks_log_queues:
raise HTTPException(status_code=404, detail=f"找不到任务ID '{task_id}' 的日志队列。") raise HTTPException(status_code=404, detail=f"找不到任务ID '{task_id}' 的日志队列。")
@@ -730,7 +727,7 @@ FileType = Literal["markdown", "markdown_zip", "html"]
} }
) )
async def service_download_file( async def service_download_file(
task_id: str = FastApiPath(..., description="已完成任务的ID", example="task-b2865b93"), task_id: str = FastApiPath(..., description="已完成任务的ID", example="b2865b93"),
file_type: FileType = FastApiPath(..., description="要下载的文件类型。", example="html") file_type: FileType = FastApiPath(..., description="要下载的文件类型。", example="html")
): ):
"""根据任务ID和文件类型下载翻译结果。""" """根据任务ID和文件类型下载翻译结果。"""
@@ -756,7 +753,7 @@ async def service_download_file(
@service_router.get( @service_router.get(
"/download_content/{task_id}/{file_type}", "/content/{task_id}/{file_type}",
summary="下载翻译结果内容 (JSON)", summary="下载翻译结果内容 (JSON)",
description=""" description="""
根据任务ID和文件类型以JSON格式返回翻译结果的内容。该接口总是返回一个JSON对象。 根据任务ID和文件类型以JSON格式返回翻译结果的内容。该接口总是返回一个JSON对象。
@@ -778,7 +775,7 @@ async def service_download_file(
"summary": "Markdown 内容", "summary": "Markdown 内容",
"value": { "value": {
"file_type": "markdown", "file_type": "markdown",
"filename": "my_doc_translated.md", "original_filename": "my_doc.pdf",
"content": "# 标题\n\n这是翻译后的Markdown内容..." "content": "# 标题\n\n这是翻译后的Markdown内容..."
} }
}, },
@@ -786,7 +783,7 @@ async def service_download_file(
"summary": "HTML 内容", "summary": "HTML 内容",
"value": { "value": {
"file_type": "html", "file_type": "html",
"filename": "my_doc_translated.html", "original_filename": "my_doc.pdf",
"content": "<h1>标题</h1><p>这是翻译后的HTML内容...</p>" "content": "<h1>标题</h1><p>这是翻译后的HTML内容...</p>"
} }
}, },
@@ -794,7 +791,7 @@ async def service_download_file(
"summary": "ZIP 内容 (Base64)", "summary": "ZIP 内容 (Base64)",
"value": { "value": {
"file_type": "markdown_zip", "file_type": "markdown_zip",
"filename": "my_doc_translated.zip", "filename": "my_doc.pdf",
"content": "UEsDBBQAAAAIA... (base64-encoded string)" "content": "UEsDBBQAAAAIA... (base64-encoded string)"
} }
} }
@@ -808,8 +805,8 @@ async def service_download_file(
}, },
} }
) )
async def service_download_content( async def service_content(
task_id: str = FastApiPath(..., description="已完成任务的ID", example="task-b2865b93"), task_id: str = FastApiPath(..., description="已完成任务的ID", example="b2865b93"),
file_type: FileType = FastApiPath(..., description="要获取内容的文件类型。", example="html") file_type: FileType = FastApiPath(..., description="要获取内容的文件类型。", example="html")
): ):
"""根据任务ID和文件类型以JSON格式返回内容。zip文件会进行Base64编码。""" """根据任务ID和文件类型以JSON格式返回内容。zip文件会进行Base64编码。"""
@@ -821,10 +818,9 @@ async def service_download_content(
raise HTTPException(status_code=404, detail="内容尚未准备好。") raise HTTPException(status_code=404, detail="内容尚未准备好。")
content_map = { content_map = {
"markdown": (task_state.get("markdown_content"), f"{task_state['original_filename_stem']}_translated.md"), "markdown": (task_state.get("markdown_content"), task_state['original_filename']),
"markdown_zip": (task_state.get("markdown_zip_content"), "markdown_zip": (task_state.get("markdown_zip_content"),task_state['original_filename']),
f"{task_state['original_filename_stem']}_translated.zip"), "html": (task_state.get("html_content"), task_state['original_filename']),
"html": (task_state.get("html_content"), f"{task_state['original_filename_stem']}_translated.html"),
} }
raw_content, filename = content_map.get(file_type, (None, None)) raw_content, filename = content_map.get(file_type, (None, None))
@@ -837,7 +833,7 @@ async def service_download_content(
return JSONResponse(content={ return JSONResponse(content={
"file_type": file_type, "file_type": file_type,
"filename": filename, "original_filename": filename,
"content": final_content "content": final_content
}) })
@@ -871,7 +867,7 @@ async def service_get_engin_list():
responses={ responses={
200: { 200: {
"description": "成功返回任务ID列表。", "description": "成功返回任务ID列表。",
"content": {"application/json": {"example": ["task-b2865b93", "task-another-one", "0"]}} "content": {"application/json": {"example": ["b2865b93", "f4e2a1c8"]}}
} }
} }
) )
@@ -1021,10 +1017,11 @@ def run_app(port: int | None = None):
if port_to_use != initial_port: print(f"端口 {initial_port} 被占用,将使用端口 {port_to_use} 代替") if port_to_use != initial_port: print(f"端口 {initial_port} 被占用,将使用端口 {port_to_use} 代替")
print(f"正在启动 DocuTranslate WebUI 版本号:{__version__}") print(f"正在启动 DocuTranslate WebUI 版本号:{__version__}")
print(f"请用浏览器访问 http://127.0.0.1:{port_to_use}") print(f"请用浏览器访问 http://127.0.0.1:{port_to_use}")
print(f"服务接口文档: http://127.0.0.1:{port_to_use}/docs")
uvicorn.run(app, host=None, port=port_to_use, workers=1) uvicorn.run(app, host=None, port=port_to_use, workers=1)
except Exception as e: except Exception as e:
print(f"启动失败: {e}") print(f"启动失败: {e}")
if __name__ == "__main__": if __name__ == "__main__":
run_app() run_app()

View File

@@ -39,6 +39,12 @@
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* === 修改点: 为等待ID的状态添加样式 === */
.task-id-placeholder {
color: var(--bs-secondary-color);
font-style: italic;
}
.log-area { .log-area {
height: 150px; height: 150px;
background-color: var(--bs-tertiary-bg); background-color: var(--bs-tertiary-bg);
@@ -67,7 +73,6 @@
background-color: var(--bs-secondary-bg); background-color: var(--bs-secondary-bg);
} }
/* --- 修改点: 添加了文件选中后的样式 --- */
.file-drop-area.file-selected { .file-drop-area.file-selected {
border-style: solid; border-style: solid;
border-color: var(--bs-success); border-color: var(--bs-success);
@@ -88,7 +93,6 @@
width: 100%; width: 100%;
} }
/* --- MODIFIED: Styles for the new Offcanvas Preview --- */
#previewOffcanvas { #previewOffcanvas {
--bs-offcanvas-width: 95vw; --bs-offcanvas-width: 95vw;
max-width: 1600px; max-width: 1600px;
@@ -118,7 +122,6 @@
overflow: auto; overflow: auto;
} }
/* split.js gutter style */
.gutter { .gutter {
background-color: var(--bs-tertiary-bg); background-color: var(--bs-tertiary-bg);
border-left: 1px solid var(--bs-border-color); border-left: 1px solid var(--bs-border-color);
@@ -127,7 +130,6 @@
.gutter.gutter-horizontal { .gutter.gutter-horizontal {
cursor: col-resize; cursor: col-resize;
} }
/* --- END OF MODIFICATION --- */
.preview-pane iframe, .preview-pane pre { .preview-pane iframe, .preview-pane pre {
@@ -386,14 +388,14 @@
<template id="taskCardTemplate"> <template id="taskCardTemplate">
<div class="card mb-3 task-card"> <div class="card mb-3 task-card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">任务 ID: <code class="task-id-display"></code></span> <!-- === 修改点: 初始显示占位符而不是ID === -->
<span class="fw-bold">任务 ID: <code class="task-id-display"><span class="task-id-placeholder">等待提交...</span></code></span>
<button type="button" class="btn-close remove-task-btn" aria-label="Close"></button> <button type="button" class="btn-close remove-task-btn" aria-label="Close"></button>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-5">
<input type="file" class="d-none file-input"> <input type="file" class="d-none file-input">
<!-- --- 修改点: 更新拖放区域的内部结构以支持两种状态 --- -->
<div class="file-drop-area"> <div class="file-drop-area">
<div class="file-drop-default"> <div class="file-drop-default">
<i class="bi bi-cloud-arrow-up fs-1"></i> <i class="bi bi-cloud-arrow-up fs-1"></i>
@@ -517,6 +519,7 @@
<script src="https://unpkg.com/split.js/dist/split.min.js"></script> <script src="https://unpkg.com/split.js/dist/split.min.js"></script>
<script type="module"> <script type="module">
// === 以下是修改后的 JavaScript 代码 ===
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
@@ -553,7 +556,6 @@
const noTaskPlaceholder = document.getElementById('no-task-placeholder'); const noTaskPlaceholder = document.getElementById('no-task-placeholder');
const taskCardTemplate = document.getElementById('taskCardTemplate'); const taskCardTemplate = document.getElementById('taskCardTemplate');
// MODIFIED: Offcanvas and preview elements
const previewOffcanvasEl = document.getElementById('previewOffcanvas'); const previewOffcanvasEl = document.getElementById('previewOffcanvas');
const previewOffcanvas = new bootstrap.Offcanvas(previewOffcanvasEl); const previewOffcanvas = new bootstrap.Offcanvas(previewOffcanvasEl);
const previewOffcanvasLabel = document.getElementById('previewOffcanvasLabel'); const previewOffcanvasLabel = document.getElementById('previewOffcanvasLabel');
@@ -568,9 +570,10 @@
// --- Global State --- // --- Global State ---
let defaultParams = {}; let defaultParams = {};
const tasks = {}; // { taskId: { elements: {...}, state: {...}, intervals: {...} } } // === 修改点: tasks对象的键现在是前端生成的cardId, 用于管理UI。后端返回的taskId将存在state中。
let isAdminMode = false; // Flag for admin view const tasks = {}; // { cardId: { elements: {...}, state: {...}, intervals: {...} } }
let previewSplitInstance = null; // To hold the Split.js instance let isAdminMode = false;
let previewSplitInstance = null;
const apiHrefMap = { const apiHrefMap = {
"https://openrouter.ai/api/v1": "https://openrouter.ai/settings/keys", "https://openrouter.ai/api/v1": "https://openrouter.ai/settings/keys",
@@ -584,7 +587,8 @@
}; };
// --- Utility Functions --- // --- Utility Functions ---
const generateTaskId = () => Math.random().toString(36).substring(2, 10); // === 修改点: 此函数现在生成的是临时的UI卡片ID (cardId)
const generateCardId = () => `card_${Math.random().toString(36).substring(2, 10)}`;
const saveToStorage = (key, value) => { try { localStorage.setItem(key, value); } catch (e) { console.warn("Save to storage failed:", e); } }; const saveToStorage = (key, value) => { try { localStorage.setItem(key, value); } catch (e) { console.warn("Save to storage failed:", e); } };
const getFromStorage = (key, defaultValue = '') => { try { return localStorage.getItem(key) || defaultValue; } catch (e) { console.warn("Read from storage failed:", e); return defaultValue; } }; const getFromStorage = (key, defaultValue = '') => { try { return localStorage.getItem(key) || defaultValue; } catch (e) { console.warn("Read from storage failed:", e); return defaultValue; } };
@@ -593,7 +597,6 @@
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = () => { reader.onload = () => {
// Result is "data:...,base64,XYZ...", we only want "XYZ..."
const base64String = reader.result.split(',')[1]; const base64String = reader.result.split(',')[1];
resolve(base64String); resolve(base64String);
}; };
@@ -660,25 +663,23 @@
} }
function saveTaskIds() { function saveTaskIds() {
if (isAdminMode) return; // In admin mode, do not save task list to localStorage if (isAdminMode) return;
// === 修改点: 保存已提交到后端的任务ID === // === 修改点: 保存的是已经从后端获取到ID的任务
// 这是解决问题的核心。通过只持久化那些已经成功提交给后端处理的任务, const submittedTaskIds = Object.values(tasks)
// 我们可以防止未提交的任务(这些任务在后端不存在)在页面刷新后被恢复。 .map(task => task.state.backendTaskId)
// 如果一个未提交的任务是页面上唯一的任务,刷新后 `localStorage` 中的任务列表会是空的, .filter(id => id); // 过滤掉null或undefined
// `init`函数在初始化时会发现没有任务,从而新建一个空白任务,符合预期。
const submittedTaskIds = Object.keys(tasks).filter(taskId => tasks[taskId].state.isSubmitted);
saveToStorage('active_task_ids', JSON.stringify(submittedTaskIds)); saveToStorage('active_task_ids', JSON.stringify(submittedTaskIds));
} }
// --- Task Card Management --- // --- Task Card Management ---
function createTaskCard(taskId = null, restoreState = false) { // === 修改点: taskId现在是后端ID, cardId是前端ID。恢复时taskId就是cardId。
if (!taskId) { function createTaskCard(backendTaskId = null, restoreState = false) {
taskId = generateTaskId(); // cardId是用于在前端tasks对象中唯一标识一个卡片的ID
} const cardId = backendTaskId || generateCardId();
const cardFragment = taskCardTemplate.content.cloneNode(true); const cardFragment = taskCardTemplate.content.cloneNode(true);
const cardElement = cardFragment.querySelector('.task-card'); const cardElement = cardFragment.querySelector('.task-card');
cardElement.dataset.taskId = taskId; cardElement.dataset.cardId = cardId; // 使用cardId作为标识
const elements = { const elements = {
card: cardElement, card: cardElement,
@@ -702,17 +703,19 @@
startBtn: cardElement.querySelector('.start-translate-btn'), startBtn: cardElement.querySelector('.start-translate-btn'),
}; };
elements.taskIdDisplay.textContent = taskId; // === 修改点: 如果是恢复任务直接显示ID否则显示占位符
if (restoreState && backendTaskId) {
elements.taskIdDisplay.textContent = backendTaskId;
}
tasks[taskId] = { tasks[cardId] = {
elements, elements,
state: { state: {
backendTaskId: backendTaskId, // 存储从后端获取的真实ID
isTranslating: false, isTranslating: false,
file: null, file: null,
htmlUrl: null, htmlUrl: null,
fileNameStem: null, fileNameStem: null,
// 如果任务是从之前的会话中恢复的restoreState为true
// 那么它一定是一个已经提交过的任务。
isSubmitted: restoreState isSubmitted: restoreState
}, },
intervals: { intervals: {
@@ -721,41 +724,42 @@
} }
}; };
addEventListenersToCard(taskId); addEventListenersToCard(cardId);
taskContainer.prepend(cardElement); taskContainer.prepend(cardElement);
updateTaskPlaceholderVisibility(); updateTaskPlaceholderVisibility();
if (!restoreState) { if (restoreState && backendTaskId) {
saveTaskIds(); // 如果是恢复任务立即用backendTaskId检查状态
} else { pollStatus(backendTaskId, true);
// If restoring, immediately check status
pollStatus(taskId, true);
} }
} }
async function removeTask(taskId) { async function removeTask(cardId) {
stopPolling(taskId); const task = tasks[cardId];
tasks[taskId].elements.card.remove(); if (!task) return;
delete tasks[taskId];
const backendTaskId = task.state.backendTaskId;
if (backendTaskId) {
stopPolling(backendTaskId);
await fetch(`/service/release/${backendTaskId}`,{method: 'POST'});
}
task.elements.card.remove();
delete tasks[cardId];
saveTaskIds(); saveTaskIds();
updateTaskPlaceholderVisibility(); updateTaskPlaceholderVisibility();
await fetch(`service/release/${taskId}`,{method: 'POST'});
} }
function addEventListenersToCard(taskId) { function addEventListenersToCard(cardId) {
const { elements, state } = tasks[taskId]; const { elements } = tasks[cardId];
elements.removeBtn.addEventListener('click', () => removeTask(taskId)); elements.removeBtn.addEventListener('click', () => removeTask(cardId));
// File Drop/Select Logic
elements.fileDropArea.addEventListener('click', () => elements.fileInput.click()); elements.fileDropArea.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', () => handleFileSelect(taskId)); elements.fileInput.addEventListener('change', () => handleFileSelect(cardId));
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
elements.fileDropArea.addEventListener(eventName, e => { elements.fileDropArea.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }, false);
e.preventDefault();
e.stopPropagation();
}, false);
}); });
['dragenter', 'dragover'].forEach(eventName => { ['dragenter', 'dragover'].forEach(eventName => {
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false); elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false);
@@ -766,46 +770,38 @@
elements.fileDropArea.addEventListener('drop', e => { elements.fileDropArea.addEventListener('drop', e => {
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
elements.fileInput.files = e.dataTransfer.files; elements.fileInput.files = e.dataTransfer.files;
handleFileSelect(taskId); handleFileSelect(cardId);
} }
}, false); }, false);
// Main Action Button
elements.startBtn.addEventListener('click', () => { elements.startBtn.addEventListener('click', () => {
if (state.isTranslating) { if (tasks[cardId].state.isTranslating) {
cancelTranslation(taskId); cancelTranslation(cardId);
} else { } else {
startTranslation(taskId); startTranslation(cardId);
} }
}); });
} }
function handleFileSelect(taskId) { function handleFileSelect(cardId) {
const { elements, state } = tasks[taskId]; const { elements, state } = tasks[cardId];
const file = elements.fileInput.files[0]; const file = elements.fileInput.files[0];
if (file) { if (file) {
state.file = file; state.file = file;
// 更新外部文件名显示
elements.fileNameDisplay.textContent = file.name; elements.fileNameDisplay.textContent = file.name;
elements.fileNameDisplayWrapper.style.display = 'block'; elements.fileNameDisplayWrapper.style.display = 'block';
// 更新拖放区外观为“已选择”状态
elements.fileDropArea.classList.add('file-selected'); elements.fileDropArea.classList.add('file-selected');
elements.fileDropDefault.style.display = 'none'; elements.fileDropDefault.style.display = 'none';
elements.fileDropSelected.style.display = 'block'; elements.fileDropSelected.style.display = 'block';
// 清除任何之前的错误状态
elements.fileDropArea.classList.remove('input-error'); elements.fileDropArea.classList.remove('input-error');
elements.fileNameDisplay.classList.remove('input-error-text'); elements.fileNameDisplay.classList.remove('input-error-text');
} }
} }
// --- Core Translation Logic --- // --- Core Translation Logic ---
async function startTranslation(taskId) { async function startTranslation(cardId) {
const { elements, state } = tasks[taskId]; const { elements, state } = tasks[cardId];
// --- Validation ---
if (!state.file) { if (!state.file) {
elements.statusMessage.textContent = '请先选择一个文件。'; elements.statusMessage.textContent = '请先选择一个文件。';
elements.statusMessage.className = 'status-message small text-danger'; elements.statusMessage.className = 'status-message small text-danger';
@@ -832,7 +828,6 @@
return; return;
} }
// --- UI Update for Starting ---
state.isTranslating = true; state.isTranslating = true;
elements.startBtn.disabled = true; elements.startBtn.disabled = true;
elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 初始化...`; elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 初始化...`;
@@ -845,10 +840,8 @@
try { try {
const fileContentBase64 = await fileToBase64(state.file); const fileContentBase64 = await fileToBase64(state.file);
state.isSubmitted = true; // === 修改点: payload不再包含task_id ===
const payload = { const payload = {
task_id: taskId,
base_url: baseUrlInput.value, base_url: baseUrlInput.value,
apikey: apikeyInput.value, apikey: apikeyInput.value,
model_id: modelInput.value, model_id: modelInput.value,
@@ -874,22 +867,29 @@
const result = await response.json(); const result = await response.json();
if (response.ok && result.task_started) { if (response.ok && result.task_started) {
// === 修改点: 任务成功提交后保存其ID === // === 修改点: 从后端响应中获取 task_id ===
// 由于 saveTaskIds 只保存 isSubmitted 为 true 的任务, const backendTaskId = result.task_id;
// 在这里调用它可以确保这个新提交的任务ID被持久化。 state.backendTaskId = backendTaskId;
saveTaskIds(); state.isSubmitted = true;
// === 修改点: 更新UI显示 task_id ===
elements.taskIdDisplay.textContent = backendTaskId;
elements.taskIdDisplay.classList.remove('task-id-placeholder');
saveTaskIds(); // 保存已提交的任务ID
elements.statusMessage.textContent = result.message || '任务已开始,正在处理...'; elements.statusMessage.textContent = result.message || '任务已开始,正在处理...';
elements.statusMessage.className = 'status-message small text-info'; elements.statusMessage.className = 'status-message small text-info';
elements.startBtn.innerHTML = `<i class="bi bi-stop-circle-fill me-1"></i>取消翻译`; elements.startBtn.innerHTML = `<i class="bi bi-stop-circle-fill me-1"></i>取消翻译`;
elements.startBtn.classList.replace('btn-primary', 'btn-danger'); elements.startBtn.classList.replace('btn-primary', 'btn-danger');
elements.startBtn.disabled = false; elements.startBtn.disabled = false;
startPolling(taskId);
// === 修改点: 使用从后端获取的ID开始轮询 ===
startPolling(backendTaskId);
} else { } else {
throw new Error(result.message || `请求失败 (${response.status})`); throw new Error(result.message || `请求失败 (${response.status})`);
} }
} catch (error) { } catch (error) {
// === 修改点: 如果提交失败或过程中出现任何异常,将任务状态重置为未提交 ===
// 这样可以防止一个失败的任务被错误地标记为“已提交”,从而避免了刷新后出现问题。
state.isSubmitted = false; state.isSubmitted = false;
console.error('请求失败:', error); console.error('请求失败:', error);
elements.statusMessage.textContent = `启动失败: ${error.message}`; elements.statusMessage.textContent = `启动失败: ${error.message}`;
@@ -902,13 +902,18 @@
} }
} }
async function cancelTranslation(taskId) { async function cancelTranslation(cardId) {
const { elements } = tasks[taskId]; const task = tasks[cardId];
if (!task || !task.state.backendTaskId) return;
const { elements } = task;
const backendTaskId = task.state.backendTaskId;
elements.startBtn.disabled = true; elements.startBtn.disabled = true;
elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在取消...`; elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在取消...`;
try { try {
const response = await fetch(`/service/cancel/${taskId}`, { method: 'POST' }); const response = await fetch(`/service/cancel/${backendTaskId}`, { method: 'POST' });
const result = await response.json(); const result = await response.json();
if (response.ok && result.cancelled) { if (response.ok && result.cancelled) {
@@ -926,27 +931,37 @@
} }
// --- Polling --- // --- Polling ---
function startPolling(taskId) { // === 修改点: taskId参数现在总是后端的ID
stopPolling(taskId); function startPolling(backendTaskId) {
const { intervals } = tasks[taskId]; stopPolling(backendTaskId);
intervals.log = setInterval(() => pollLogs(taskId), 2000); // 找到对应的卡片
intervals.status = setInterval(() => pollStatus(taskId), 1500); const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
pollLogs(taskId); if (!card) return;
pollStatus(taskId);
card.intervals.log = setInterval(() => pollLogs(backendTaskId), 2000);
card.intervals.status = setInterval(() => pollStatus(backendTaskId), 1500);
pollLogs(backendTaskId);
pollStatus(backendTaskId);
} }
function stopPolling(taskId) { function stopPolling(backendTaskId) {
const { intervals } = tasks[taskId]; const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
if (!card) return;
const { intervals } = card;
if (intervals.log) clearInterval(intervals.log); if (intervals.log) clearInterval(intervals.log);
if (intervals.status) clearInterval(intervals.status); if (intervals.status) clearInterval(intervals.status);
intervals.log = null; intervals.log = null;
intervals.status = null; intervals.status = null;
} }
async function pollLogs(taskId) { async function pollLogs(backendTaskId) {
const { elements } = tasks[taskId]; const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
if (!card) return;
const { elements } = card;
try { try {
const response = await fetch(`/service/logs/${taskId}`); const response = await fetch(`/service/logs/${backendTaskId}`);
if (!response.ok) return; if (!response.ok) return;
const data = await response.json(); const data = await response.json();
if (data.logs && data.logs.length > 0) { if (data.logs && data.logs.length > 0) {
@@ -954,23 +969,35 @@
elements.logArea.scrollTop = elements.logArea.scrollHeight; elements.logArea.scrollTop = elements.logArea.scrollHeight;
} }
} catch (error) { } catch (error) {
console.warn(`[${taskId}] Error polling logs:`, error); console.warn(`[${backendTaskId}] Error polling logs:`, error);
} }
} }
async function pollStatus(taskId, isRestore = false) { async function pollStatus(backendTaskId, isRestore = false) {
const { elements, state } = tasks[taskId]; const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
if (!card) {
// 如果是恢复任务时找不到卡片,可能是在其他地方被删除了
if (isRestore) {
console.warn(`Restored task ${backendTaskId} not found in UI, removing from storage.`);
const savedTaskIds = JSON.parse(getFromStorage('active_task_ids', '[]'));
const newIds = savedTaskIds.filter(id => id !== backendTaskId);
saveToStorage('active_task_ids', JSON.stringify(newIds));
}
return;
}
const { elements, state } = card;
const cardId = card.elements.card.dataset.cardId;
try { try {
const response = await fetch(`/service/status/${taskId}`); const response = await fetch(`/service/status/${backendTaskId}`);
if (!response.ok) { if (!response.ok) {
if (response.status === 404 && isRestore && state.isSubmitted) { if (response.status === 404 && isRestore) {
await removeTask(taskId); await removeTask(cardId);
} }
return; return;
} }
const status = await response.json(); const status = await response.json();
// 页面刷新时,从后端状态恢复文件名显示
if (status.original_filename && (!state.file || isRestore)) { if (status.original_filename && (!state.file || isRestore)) {
elements.fileNameDisplay.textContent = status.original_filename; elements.fileNameDisplay.textContent = status.original_filename;
elements.fileNameDisplayWrapper.style.display = 'block'; elements.fileNameDisplayWrapper.style.display = 'block';
@@ -980,7 +1007,7 @@
elements.statusMessage.className = `status-message small ${status.error_flag ? 'text-danger' : 'text-info'}`; elements.statusMessage.className = `status-message small ${status.error_flag ? 'text-danger' : 'text-info'}`;
if (!status.is_processing) { if (!status.is_processing) {
stopPolling(taskId); stopPolling(backendTaskId);
state.isTranslating = false; state.isTranslating = false;
elements.startBtn.disabled = false; elements.startBtn.disabled = false;
elements.startBtn.innerHTML = `<i class="bi bi-play-fill me-1"></i>重新翻译`; elements.startBtn.innerHTML = `<i class="bi bi-play-fill me-1"></i>重新翻译`;
@@ -989,8 +1016,6 @@
if (status.download_ready && !status.error_flag) { if (status.download_ready && !status.error_flag) {
elements.statusMessage.className = 'status-message small text-success'; elements.statusMessage.className = 'status-message small text-success';
// Use download URLs directly from the service response
state.htmlUrl = status.downloads.html; state.htmlUrl = status.downloads.html;
state.fileNameStem = status.original_filename_stem; state.fileNameStem = status.original_filename_stem;
@@ -998,8 +1023,8 @@
elements.mdLink.href = status.downloads.markdown; elements.mdLink.href = status.downloads.markdown;
elements.mdZipLink.href = status.downloads.markdown_zip; elements.mdZipLink.href = status.downloads.markdown_zip;
elements.previewBtn.onclick = () => setupPreview(taskId); elements.previewBtn.onclick = () => setupPreview(cardId);
elements.pdfBtn.onclick = () => downloadPdf(taskId); elements.pdfBtn.onclick = () => downloadPdf(cardId);
elements.downloadButtons.style.display = 'flex'; elements.downloadButtons.style.display = 'flex';
} else { } else {
@@ -1013,29 +1038,27 @@
elements.progress.style.display = 'block'; elements.progress.style.display = 'block';
elements.downloadButtons.style.display = 'none'; elements.downloadButtons.style.display = 'none';
if (isRestore && !tasks[taskId].intervals.status) { if (isRestore && !card.intervals.status) {
startPolling(taskId); startPolling(backendTaskId);
} }
} }
} catch (error) { } catch (error) {
console.error(`[${taskId}] Error polling status:`, error); console.error(`[${backendTaskId}] Error polling status:`, error);
elements.statusMessage.textContent = '状态更新出错。'; elements.statusMessage.textContent = '状态更新出错。';
elements.statusMessage.className = 'status-message small text-danger'; elements.statusMessage.className = 'status-message small text-danger';
} }
} }
// --- MODIFIED: Download and Preview --- // --- Download and Preview (No changes needed here, they use cardId to get state) ---
function setupPreview(taskId) { function setupPreview(cardId) {
const { state } = tasks[taskId]; const { state } = tasks[cardId];
if (!state.htmlUrl) return; if (!state.htmlUrl) return;
// Clear previous content
const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p'); const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p');
if (existingOriginalContent) existingOriginalContent.remove(); if (existingOriginalContent) existingOriginalContent.remove();
translatedPreviewFrame.src = 'about:blank'; translatedPreviewFrame.src = 'about:blank';
// Render original file preview
if (state.file) { if (state.file) {
const fileType = state.file.type; const fileType = state.file.type;
const fileExtension = state.file.name.split('.').pop().toLowerCase(); const fileExtension = state.file.name.split('.').pop().toLowerCase();
@@ -1062,7 +1085,6 @@
originalPreviewPane.appendChild(p); originalPreviewPane.appendChild(p);
} }
// Fetch and render translated HTML
fetch(state.htmlUrl) fetch(state.htmlUrl)
.then(resp => resp.ok ? resp.text() : Promise.reject(`HTTP error ${resp.status}`)) .then(resp => resp.ok ? resp.text() : Promise.reject(`HTTP error ${resp.status}`))
.then(html => { .then(html => {
@@ -1085,8 +1107,8 @@
}; };
} }
function downloadPdf(taskId) { function downloadPdf(cardId) {
const { elements, state } = tasks[taskId]; const { elements, state } = tasks[cardId];
if (!state.htmlUrl) return; if (!state.htmlUrl) return;
elements.pdfBtn.disabled = true; elements.pdfBtn.disabled = true;
@@ -1119,73 +1141,50 @@
} }
function setPreviewDisplayMode(mode) { function setPreviewDisplayMode(mode) {
// Always destroy the previous split instance to avoid errors
if (previewSplitInstance) { if (previewSplitInstance) {
previewSplitInstance.destroy(); previewSplitInstance.destroy();
previewSplitInstance = null; previewSplitInstance = null;
} }
// Ensure containers are visible and reset styles before applying new ones
originalPreviewContainer.style.display = 'flex'; originalPreviewContainer.style.display = 'flex';
originalPreviewContainer.style.width = ''; originalPreviewContainer.style.width = '';
translatedPreviewContainer.style.width = ''; translatedPreviewContainer.style.width = '';
if (mode === 'bilingual') { if (mode === 'bilingual') {
previewOffcanvasLabel.textContent = '双语预览'; previewOffcanvasLabel.textContent = '双语预览';
// Re-initialize Split.js for resizable panes
previewSplitInstance = Split(['#originalPreviewContainer', '#translatedPreviewContainer'], { previewSplitInstance = Split(['#originalPreviewContainer', '#translatedPreviewContainer'], {
sizes: [50, 50], sizes: [50, 50], minSize: 200, gutterSize: 10, cursor: 'col-resize',
minSize: 200, // Minimum pane size in pixels
gutterSize: 10,
cursor: 'col-resize',
}); });
// Update button states
setBilingualViewBtn.classList.add('btn-primary'); setBilingualViewBtn.classList.add('btn-primary');
setBilingualViewBtn.classList.remove('btn-outline-primary'); setBilingualViewBtn.classList.remove('btn-outline-primary');
setTranslatedOnlyViewBtn.classList.remove('btn-primary'); setTranslatedOnlyViewBtn.classList.remove('btn-primary');
setTranslatedOnlyViewBtn.classList.add('btn-outline-primary'); setTranslatedOnlyViewBtn.classList.add('btn-outline-primary');
} else {
} else { // translationOnly
previewOffcanvasLabel.textContent = '译文预览'; previewOffcanvasLabel.textContent = '译文预览';
// Hide original, make translated pane full-width
originalPreviewContainer.style.display = 'none'; originalPreviewContainer.style.display = 'none';
translatedPreviewContainer.style.width = '100%'; translatedPreviewContainer.style.width = '100%';
// Update button states
setTranslatedOnlyViewBtn.classList.add('btn-primary'); setTranslatedOnlyViewBtn.classList.add('btn-primary');
setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary'); setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary');
setBilingualViewBtn.classList.remove('btn-primary'); setBilingualViewBtn.classList.remove('btn-primary');
setBilingualViewBtn.classList.add('btn-outline-primary'); setBilingualViewBtn.classList.add('btn-outline-primary');
} }
} }
// --- End of MODIFICATION ---
// --- Initialization --- // --- Initialization ---
async function init() { async function init() {
// Determine if in admin mode
isAdminMode = window.location.pathname === '/admin'; isAdminMode = window.location.pathname === '/admin';
// Fetch metadata
try { try {
const [metaRes, enginRes, paramsRes] = await Promise.all([ const [metaRes, enginRes, paramsRes] = await Promise.all([
fetch("/service/meta"), fetch("/service/meta"), fetch('/service/engin-list'), fetch("/service/default-params")
fetch('/service/engin-list'),
fetch("/service/default-params")
]); ]);
const meta = await metaRes.json(); const meta = await metaRes.json();
versionDisplay.textContent = `v${meta.version}`; versionDisplay.textContent = `v${meta.version}`;
const enginList = await enginRes.json(); const enginList = await enginRes.json();
Array.from(convertEnginSelect.options).forEach(option => { Array.from(convertEnginSelect.options).forEach(option => {
if (!enginList.includes(option.value)) { if (!enginList.includes(option.value)) {
option.disabled = true; option.disabled = true; option.textContent += " (不可用)";
option.textContent += " (不可用)";
} }
}); });
defaultParams = await paramsRes.json(); defaultParams = await paramsRes.json();
} catch (error) { } catch (error) {
console.error("Initialization failed:", error); console.error("Initialization failed:", error);
@@ -1193,7 +1192,6 @@
return; return;
} }
// Load settings
platformSelect.value = getFromStorage('translator_last_platform', 'https://api.openai.com/v1'); platformSelect.value = getFromStorage('translator_last_platform', 'https://api.openai.com/v1');
updatePlatformUI(); updatePlatformUI();
convertEnginSelect.value = getFromStorage('translator_convert_engin', 'mineru'); convertEnginSelect.value = getFromStorage('translator_convert_engin', 'mineru');
@@ -1204,12 +1202,10 @@
refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true'; refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true';
customPromptTranslateArea.value = getFromStorage("custom_prompt_translate"); customPromptTranslateArea.value = getFromStorage("custom_prompt_translate");
// Setup sliders
setupSlider(chunkSizeSlider, chunkSizeDisplay, chunkSizeReset, 'chunk_size', defaultParams); setupSlider(chunkSizeSlider, chunkSizeDisplay, chunkSizeReset, 'chunk_size', defaultParams);
setupSlider(concurrentSlider, concurrentDisplay, concurrentReset, 'concurrent', defaultParams); setupSlider(concurrentSlider, concurrentDisplay, concurrentReset, 'concurrent', defaultParams);
setupSlider(temperatureSlider, temperatureDisplay, temperatureReset, 'temperature', defaultParams); setupSlider(temperatureSlider, temperatureDisplay, temperatureReset, 'temperature', defaultParams);
// Restore tasks based on mode
if (isAdminMode) { if (isAdminMode) {
document.title = "DocuTranslate - Admin Panel"; document.title = "DocuTranslate - Admin Panel";
try { try {
@@ -1225,16 +1221,14 @@
} }
updateTaskPlaceholderVisibility(); updateTaskPlaceholderVisibility();
} else { } else {
// Normal mode: Restore from localStorage
const savedTaskIds = JSON.parse(getFromStorage('active_task_ids', '[]')); const savedTaskIds = JSON.parse(getFromStorage('active_task_ids', '[]'));
if (savedTaskIds.length > 0) { if (savedTaskIds.length > 0) {
savedTaskIds.forEach(taskId => createTaskCard(taskId, true)); savedTaskIds.forEach(taskId => createTaskCard(taskId, true));
} else { } else {
createTaskCard(); // Create one new task by default createTaskCard();
} }
} }
// Add event listeners for settings
platformSelect.addEventListener('change', updatePlatformUI); platformSelect.addEventListener('change', updatePlatformUI);
apikeyInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_apikey`, e.target.value)); apikeyInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_apikey`, e.target.value));
modelInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_model_id`, e.target.value)); modelInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_model_id`, e.target.value));
@@ -1253,53 +1247,14 @@
} }
// --- Theme switcher logic --- // --- Theme switcher logic ---
const getPreferredTheme = () => { const getPreferredTheme = () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme) { return storedTheme; } return 'auto'; };
const storedTheme = localStorage.getItem('theme'); const setTheme = theme => { if (theme === 'auto') { document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); } else { document.documentElement.setAttribute('data-bs-theme', theme); } };
if (storedTheme) { const showActiveTheme = (theme) => { document.querySelectorAll('[data-bs-theme-value]').forEach(element => { element.classList.remove('active'); }); const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`); if (activeButton) { activeButton.classList.add('active'); } };
return storedTheme;
}
return 'auto';
};
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-bs-theme', theme);
}
};
const showActiveTheme = (theme) => {
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active');
});
const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`);
if (activeButton) {
activeButton.classList.add('active');
}
};
const preferredTheme = getPreferredTheme(); const preferredTheme = getPreferredTheme();
setTheme(preferredTheme); setTheme(preferredTheme);
showActiveTheme(preferredTheme); showActiveTheme(preferredTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme === 'auto' || !storedTheme) { setTheme('auto'); } });
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => { toggle.addEventListener('click', () => { const theme = toggle.getAttribute('data-bs-theme-value'); localStorage.setItem('theme', theme); setTheme(theme); showActiveTheme(theme); }); });
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'auto' || !storedTheme) {
setTheme('auto');
}
});
document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value');
localStorage.setItem('theme', theme);
setTheme(theme);
showActiveTheme(theme);
});
});
// --- Start the application --- // --- Start the application ---
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);