Merge remote-tracking branch 'origin/master' into fix/ppi-kaks-enable

# Conflicts:
#	tests/unit/test_router_analysis_handoff_boundary.py
This commit is contained in:
potaser
2026-08-10 23:24:47 +08:00
68 changed files with 41004 additions and 668 deletions
+59 -3
View File
@@ -6112,8 +6112,9 @@ def run_stage(
target_species = kwargs.get("target_species")
interspecies_ref_species = kwargs.get("interspecies_ref_species")
scope_file = str(kwargs.get("scope_file") or kwargs.get("scopeFile") or "").strip()
analysis_route = str(kwargs.get("analysis_route") or kwargs.get("analysisRoute") or "").strip()
scope_file = str(kwargs.get("scope_file") or kwargs.get("scopeFile") or "").strip()
scope_id = str(kwargs.get("scope_id") or kwargs.get("scopeId") or "").strip()
analysis_route = str(kwargs.get("analysis_route") or kwargs.get("analysisRoute") or "").strip()
analysis_mode = str(kwargs.get("analysis_mode") or kwargs.get("analysisMode") or "").strip()
member_set_id = str(kwargs.get("member_set_id") or kwargs.get("memberSetId") or "").strip()
combo_output_label = str(combo_output_label or kwargs.get("comboOutputLabel") or kwargs.get("combo_output_label") or "").strip()
@@ -8432,7 +8433,62 @@ def run_stage(
else:
return f"❌ PEP failed (exit code: {code}). Log: {log_file}", manifest
if stage == "summary":
if stage == "gene_family_report":
expected_stages = kwargs.get("expected_stages") or kwargs.get("expectedStages") or []
if isinstance(expected_stages, str):
expected_stages = [value.strip() for value in expected_stages.split(",") if value.strip()]
else:
expected_stages = [str(value).strip() for value in expected_stages if str(value).strip()]
report_action = str(kwargs.get("report_action") or kwargs.get("reportAction") or "generate").strip()
if report_action == "generate" and not expected_stages:
return "❌ 基因家族结果报告生成需要非空 expected_stages", manifest
stage_statuses = kwargs.get("stage_statuses") or kwargs.get("stageStatuses") or {}
if isinstance(stage_statuses, str):
try:
stage_statuses = json.loads(stage_statuses)
except json.JSONDecodeError:
return "❌ 基因家族结果报告的 stage_statuses 不是合法 JSON", manifest
if not isinstance(stage_statuses, dict):
return "❌ 基因家族结果报告的 stage_statuses 必须是对象", manifest
report_use_model_raw = kwargs.get("report_use_model", kwargs.get("reportUseModel", True))
rnaseq_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
cmd = [
sys.executable,
"apps/cli/run_gene_family_report.py",
"--workspace",
str(ws),
"--prefix",
prefix,
"--job-id",
job.job_id,
"--expected-stages",
",".join(expected_stages),
"--stage-statuses-json",
json.dumps(stage_statuses, ensure_ascii=False, separators=(",", ":")),
"--action",
report_action,
]
for flag, value in (
("--scope-id", scope_id),
("--analysis-route", analysis_route),
("--member-set-id", member_set_id),
("--rnaseq-scope-id", rnaseq_scope_id),
):
if value:
cmd += [flag, value]
if not _truthy(report_use_model_raw):
cmd.append("--no-model")
code = run_cmd_logged(cmd, log_file, cwd=Path("."))
report_manifests = sorted(
final_dir.glob(f"{prefix}*.gene_family_report.manifest.json"),
key=lambda path: path.stat().st_mtime,
)
report_manifest = report_manifests[-1] if report_manifests else manifest
if code != 0:
return f"❌ 基因家族结果报告生成失败,日志:{log_file}", report_manifest
return f"✅ 基因家族结果报告已生成。日志:{log_file}", report_manifest
if stage == "summary":
summary_manifest_seed = (
ws / "final" / f"{prefix}.identify.manifest.json"
if (ws / "final" / f"{prefix}.identify.manifest.json").exists()
+416
View File
@@ -0,0 +1,416 @@
<!-- Agent状态显示组件 - 可嵌入index.html -->
<template id="agent-status-component">
<div class="agent-status-panel">
<div class="agent-header">
<h3>{{ t('agent-assistant') }}</h3>
<span class="agent-mode">{{ currentMode }}</span>
</div>
<div class="agent-messages" ref="messageContainer">
<div v-for="(msg, index) in messages" :key="index"
:class="['message', msg.role]">
<div class="message-role">{{ getRoleLabel(msg.role) }}</div>
<div class="message-content" v-html="formatMessage(msg.content)"></div>
<div class="message-time">{{ formatTime(msg.timestamp) }}</div>
</div>
</div>
<div class="agent-input">
<textarea v-model="userInput"
:placeholder="t('enter-question')"
@keydown.enter.prevent="sendMessage"
rows="3"></textarea>
<button @click="sendMessage" :disabled="isProcessing">
{{ isProcessing ? t('processing') : t('send') }}
</button>
</div>
<div class="agent-tasks" v-if="tasks.length > 0">
<h4>{{ t('task-list') }}</h4>
<div v-for="task in tasks" :key="task.task_id"
:class="['task-item', task.status]"
@click="selectTask(task)">
<span class="task-status">{{ getStatusIcon(task.status) }}</span>
<span class="task-message">{{ task.message }}</span>
<span class="task-progress">{{ task.progress }}%</span>
</div>
</div>
</div>
</template>
<script src="/static/i18n.js"></script>
// Agent状态组件
const AgentStatusComponent = {
template: '#agent-status-component',
data() {
return {
messages: [],
userInput: '',
isProcessing: false,
currentMode: '',
sessionId: '',
tasks: [],
currentTask: null,
pollingInterval: null
};
},
mounted() {
this.sessionId = 'session_' + Date.now();
this.currentMode = t('standby');
this.loadTasks();
this.startPolling();
},
beforeUnmount() {
this.stopPolling();
},
methods: {
async sendMessage() {
if (!this.userInput.trim() || this.isProcessing) return;
const message = this.userInput.trim();
this.userInput = '';
this.isProcessing = true;
// 添加用户消息
this.addMessage('user', message);
try {
// 调用Agent执行API
const response = await fetch('/api/agent/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
session_id: this.sessionId,
model: 'kimi'
})
});
const data = await response.json();
if (data.success) {
this.currentTask = data.task_id;
this.addMessage('system', t('msg-task-submitted') + data.task_id);
this.loadTasks();
} else {
this.addMessage('error', data.message || t('msg-submit-failed'));
}
} catch (error) {
this.addMessage('error', t('msg-request-failed') + error.message);
}
this.isProcessing = false;
},
async loadTasks() {
try {
const response = await fetch('/api/agent/tasks');
const data = await response.json();
if (data.success) {
this.tasks = data.tasks;
}
} catch (error) {
console.error(t('msg-load-task-failed'), error);
}
},
async checkTaskStatus(taskId) {
try {
const response = await fetch(`/api/agent/status/${taskId}`);
const data = await response.json();
if (data.success && data.task) {
const task = data.task;
// 更新任务状态
const taskIndex = this.tasks.findIndex(t => t.task_id === taskId);
if (taskIndex >= 0) {
this.tasks[taskIndex] = { ...this.tasks[taskIndex], ...task };
}
// 如果任务完成,显示结果
if (task.status === 'completed' && task.result) {
this.addMessage('assistant', task.result.response || t('msg-task-completed'));
// 显示执行结果摘要
if (task.result.execution_result) {
const execResult = task.result.execution_result;
if (execResult.status === 'success') {
const manifest = execResult.manifest || {};
const metrics = manifest.metrics || {};
this.addMessage('system',
t('msg-analysis-complete') + (metrics.final_members || 0) + t('msg-gene-members'));
}
}
} else if (task.status === 'failed') {
this.addMessage('error', task.result?.response || t('msg-task-failed'));
}
}
} catch (error) {
console.error(t('msg-check-task-status-failed'), error);
}
},
selectTask(task) {
this.currentTask = task.task_id;
this.checkTaskStatus(task.task_id);
},
startPolling() {
this.pollingInterval = setInterval(() => {
if (this.currentTask) {
this.checkTaskStatus(this.currentTask);
}
this.loadTasks();
}, 3000); // 每3秒轮询一次
},
stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
},
addMessage(role, content) {
this.messages.push({
role: role,
content: content,
timestamp: new Date().toISOString()
});
// 滚动到底部
setTimeout(() => {
const container = this.$refs.messageContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 100);
},
getRoleLabel(role) {
const keys = {
'user': 'user-role',
'assistant': 'agent-role',
'system': 'system-role',
'error': 'error-role',
'execution': 'execution-role',
'planning': 'planning-role',
'diagnosis': 'diagnosis-role'
};
return keys[role] ? t(keys[role]) : role;
},
getStatusIcon(status) {
const icons = {
'pending': '⏳',
'running': '🔄',
'completed': '✅',
'failed': '❌',
'cancelled': '🚫'
};
return icons[status] || '❓';
},
formatMessage(content) {
// 简单的Markdown格式化
return content
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
t(key) {
return window.t ? window.t(key) : key;
}
}
};
</script>
<style>
.agent-status-panel {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
background: #f9f9f9;
height: 100%;
display: flex;
flex-direction: column;
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.agent-header h3 {
margin: 0;
font-size: 16px;
}
.agent-mode {
font-size: 12px;
color: #666;
background: #e0e0e0;
padding: 2px 8px;
border-radius: 4px;
}
.agent-messages {
flex: 1;
overflow-y: auto;
max-height: 400px;
margin-bottom: 12px;
}
.message {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 8px;
background: white;
border-left: 3px solid #ddd;
}
.message.user {
border-left-color: #4CAF50;
background: #E8F5E9;
}
.message.assistant {
border-left-color: #2196F3;
background: #E3F2FD;
}
.message.error {
border-left-color: #f44336;
background: #FFEBEE;
}
.message.system {
border-left-color: #9E9E9E;
background: #F5F5F5;
font-size: 12px;
}
.message-role {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.message-content {
font-size: 14px;
line-height: 1.5;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
margin-top: 4px;
}
.agent-input {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.agent-input textarea {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
font-family: inherit;
}
.agent-input button {
padding: 8px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.agent-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
.agent-tasks {
border-top: 1px solid #eee;
padding-top: 12px;
}
.agent-tasks h4 {
margin: 0 0 8px 0;
font-size: 14px;
}
.task-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 4px;
background: white;
}
.task-item:hover {
background: #f0f0f0;
}
.task-item.pending {
border-left: 3px solid #FFC107;
}
.task-item.running {
border-left: 3px solid #2196F3;
}
.task-item.completed {
border-left: 3px solid #4CAF50;
}
.task-item.failed {
border-left: 3px solid #f44336;
}
.task-status {
font-size: 14px;
}
.task-message {
flex: 1;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-progress {
font-size: 11px;
color: #666;
}
</style>
+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script src="/static/i18n.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BotanicalBaseAgent</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.full.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@element-plus/icons-vue@2.1.0/dist/index.iife.js"></script>
<link rel="stylesheet" href="conversation-sidebar.css">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
}
.header h1 { margin: 0; font-size: 22px; }
.main-container {
display: flex;
height: calc(100vh - 60px);
overflow: hidden;
}
/* 左侧控制面板 */
.left-panel {
width: 280px;
background: white;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
padding: 15px;
gap: 15px;
overflow-y: auto;
}
/* 中间内容区 */
.center-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 15px;
gap: 15px;
overflow: hidden;
}
/* 右侧结果区 */
.right-panel {
width: 350px;
background: white;
border-left: 1px solid #e4e7ed;
padding: 15px;
overflow-y: auto;
}
.panel-section {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.panel-title {
font-size: 14px;
font-weight: bold;
color: #409EFF;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #409EFF;
}
.chat-area {
flex: 1;
background: white;
border-radius: 8px;
padding: 15px;
overflow-y: auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.chat-message {
margin-bottom: 15px;
padding: 12px 16px;
border-radius: 8px;
max-width: 80%;
}
.chat-message.user {
background: #ecf5ff;
margin-left: auto;
text-align: right;
}
.chat-message.assistant {
background: #f0f9eb;
}
.input-area {
display: flex;
gap: 10px;
}
.stage-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
margin: 4px;
}
.stage-tag.success { background: #67C23A; color: white; }
.stage-tag.running { background: #E6A23C; color: white; }
.stage-tag.pending { background: #909399; color: white; }
</style>
</head>
<body>
<div id="app">
<!-- Header -->
<div class="header">
<h1>{{ t('gf-analysis-system') }}</h1>
</div>
<div class="main-container">
<!-- 左侧:会话侧边栏 + 控制面板 -->
<div class="left-panel">
<!-- 会话侧边栏 -->
<div class="conversation-sidebar" style="width: 100%; margin: -15px; margin-bottom: 0;">
<div class="sidebar-header">
<button class="new-chat-btn" @click="createNewConversation" style="flex: 1;">
<el-icon><plus /></el-icon>
<span>New Chat</span>
</button>
</div>
<div class="search-box" style="padding: 12px;">
<el-input
v-model="searchQuery"
:placeholder="t('search-conv')"
prefix-icon="search"
clearable
size="small">
</el-input>
</div>
<div class="conversation-list" style="max-height: 300px;">
<div class="list-section">
<div class="section-title">{{ t('conv-list') }}</div>
<div v-for="conv in filteredConversations"
:key="conv.id"
class="conversation-item"
:class="{ active: currentConv?.id === conv.id }"
@click="selectConversation(conv)">
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}{{ t('msg-count') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分析控制 -->
<div class="panel-section">
<div class="panel-title">{{ t('analysis-stage') }}</div>
<el-select v-model="selectedStage" :placeholder="t('select-stage')" style="width: 100%;">
<el-option :label="t('identify-label')" value="identify"></el-option>
<el-option :label="t('phylogeny-label')" value="phylogeny"></el-option>
<el-option :label="t('motif-label')" value="motif"></el-option>
<el-option :label="t('domain-label')" value="domain"></el-option>
<el-option :label="t('expression-label')" value="expression"></el-option>
</el-select>
<el-button type="primary" style="width: 100%; margin-top: 10px;" @click="executeAnalysis">
{{ t('execute-analysis') }}
</el-button>
</div>
<!-- 参数设置 -->
<div class="panel-section">
<div class="panel-title">{{ t('param-settings') }}</div>
<el-form :model="params" label-width="80px" size="small">
<el-form-item :label="t('prefix')">
<el-input v-model="params.prefix" :placeholder="t('prefix-placeholder')"></el-input>
</el-form-item>
<el-form-item label="CPU">
<el-input-number v-model="params.cpu" :min="1" :max="32" style="width: 100%;"></el-input-number>
</el-form-item>
</el-form>
</div>
</div>
<!-- 中间:对话区 -->
<div class="center-panel">
<div class="chat-area">
<div v-for="(msg, index) in messages" :key="index"
class="chat-message" :class="msg.role">
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">
{{ msg.role === 'user' ? t('user') : t('ai-assistant') }} · {{ msg.time }}
</div>
<div>{{ msg.content }}</div>
</div>
<div v-if="messages.length === 0" style="text-align: center; color: #999; padding: 40px;">
<el-icon :size="48" style="display: block; margin-bottom: 10px;"><chat-line-round /></el-icon>
<p>{{ t('start-new-chat') }}</p>
<p style="font-size: 12px;">{{ t('or-continue') }}</p>
</div>
</div>
<div class="input-area">
<el-input
v-model="userInput"
type="textarea"
:rows="3"
:placeholder="t('describe-need')"
@keyup.enter.ctrl="sendMessage">
</el-input>
<el-button type="primary" size="large" @click="sendMessage" :disabled="!userInput.trim()">
{{ t('send') }}
</el-button>
</div>
</div>
<!-- 右侧:结果展示 -->
<div class="right-panel">
<div class="panel-title">📊 Analysis Results</div>
<div v-for="stage in stages" :key="stage.name" style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-weight: 500;">{{ stage.label }}</span>
<span class="stage-tag" :class="stage.status">{{ stage.statusText }}</span>
</div>
<el-progress :percentage="stage.progress" :status="stage.status"></el-progress>
</div>
<el-divider></el-divider>
<div style="text-align: center; color: #999; padding: 20px;">
<el-icon :size="48" style="display: block; margin-bottom: 10px;"><picture /></el-icon>
<p>{{ t('results-here') }}</p>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, computed } = Vue;
const app = createApp({
setup() {
const t = window.t;
document.title = 'BotanicalBaseAgent - ' + t('gf-analysis');
// 会话管理
const conversations = ref([
{ id: 1, title: 'Ft.SPL基因家族分析', gene_family: 'SPL', message_count: 12 },
{ id: 2, title: 'Os.TCP进化分析', gene_family: 'TCP', message_count: 5 },
{ id: 3, title: 'HMMER参数配置咨询', gene_family: null, message_count: 8 },
]);
const currentConv = ref(null);
const searchQuery = ref('');
const filteredConversations = computed(() => {
if (!searchQuery.value) return conversations.value;
const query = searchQuery.value.toLowerCase();
return conversations.value.filter(conv =>
conv.title.toLowerCase().includes(query) ||
(conv.gene_family && conv.gene_family.toLowerCase().includes(query))
);
});
const createNewConversation = () => {
const newConv = {
id: Date.now(),
title: t('new-chat') + ' ' + new Date().toLocaleTimeString(),
gene_family: null,
message_count: 0
};
conversations.value.unshift(newConv);
selectConversation(newConv);
};
const selectConversation = (conv) => {
currentConv.value = conv;
// 清空消息并添加欢迎语
messages.value = [{
role: 'assistant',
content: t('initial-welcome').replace('{title}', conv.title),
time: new Date().toLocaleTimeString()
}];
};
// 分析参数
const selectedStage = ref('');
const params = reactive({
prefix: 'Ft.SPL',
cpu: 8
});
// 消息
const messages = ref([]);
const userInput = ref('');
const sendMessage = () => {
if (!userInput.value.trim()) return;
// 添加用户消息
messages.value.push({
role: 'user',
content: userInput.value,
time: new Date().toLocaleTimeString()
});
// 更新当前会话消息数
if (currentConv.value) {
currentConv.value.message_count++;
}
// 模拟AI回复
setTimeout(() => {
messages.value.push({
role: 'assistant',
content: `Received: "${userInput.value}". I will help analyze the gene family. Confirm parameters and click "${t('execute-analysis')}".`,
time: new Date().toLocaleTimeString()
});
if (currentConv.value) {
currentConv.value.message_count++;
}
}, 500);
userInput.value = '';
};
// 分析阶段
const stages = ref([
{ name: 'identify', label: 'Identify', status: 'success', statusText: t('completed'), progress: 100 },
{ name: 'phylogeny', label: 'Phylogeny', status: 'running', statusText: t('running'), progress: 60 },
{ name: 'motif', label: 'Motif', status: 'pending', statusText: t('pending'), progress: 0 },
{ name: 'domain', label: 'Domain', status: 'pending', statusText: t('pending'), progress: 0 },
]);
const executeAnalysis = () => {
if (!selectedStage.value) {
ElementPlus.ElMessage.warning(t('msg-select-stage-first'));
return;
}
ElementPlus.ElMessage.success(t('msg-executing') + selectedStage.value + t('msg-analysis-text'));
};
return {
t,
conversations,
currentConv,
searchQuery,
filteredConversations,
createNewConversation,
selectConversation,
selectedStage,
params,
messages,
userInput,
sendMessage,
stages,
executeAnalysis
};
}
});
app.use(ElementPlus);
// 注册 i18n 翻译函数到 Vue 模板作用域
app.config.globalProperties.t = window.t || ((key) => key);
// 注册图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount('#app');
</script>
</body>
</html>
+193 -13
View File
@@ -1019,7 +1019,7 @@
}
.execution-log-item.assistant .log-content.structured-content {
/* 结构化内容使用rag样式 */
font-size: 13px;
}
.execution-log-item.assistant .log-content.short-content {
@@ -1387,7 +1387,8 @@
<el-divider style="margin: 12px 0;"></el-divider>
<div v-if="false" style="padding:10px; background:#fff; border:1px solid #dcdfe6; border-radius:0px;">
<div @dragover.prevent @drop.prevent="handleLightweightExtensionFolderDrop($event)"
style="padding:10px; background:#fff; border:1px dashed #409EFF; border-radius:0px;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px;">
<el-tooltip content="上传已跑通过的代码和测试数据;执行通过自然语言触发。" placement="top">
<h4 style="margin:0; color:#303133; cursor:help;">拓展分析阶段</h4>
@@ -1413,10 +1414,13 @@
<span>名称</span><el-input v-model="extensionBuild.displayName" size="small" placeholder="例如 HMMER hmmsearch"></el-input>
<span>原始命令</span><el-input v-model="extensionBuild.command" size="small" placeholder="例如 bash run.sh input.fa"></el-input>
<span>环境</span>
<el-select v-model="extensionBuild.runtimeEnv" size="small">
<el-option label="test4py311" value="test4py311"></el-option>
<el-option label="rnaseq" value="rnaseq"></el-option>
<el-option label="rnaR" value="rnaR"></el-option>
<el-select v-model="extensionBuild.runtimeEnv" size="small" filterable>
<el-option
v-for="env in extensionEnvOptions"
:key="env"
:label="env"
:value="env">
</el-option>
</el-select>
<span>原始代码</span><input type="file" multiple @change="onExtensionCodeFilesChange" style="font-size:12px; min-width:0;">
<span v-if="extensionBuild.codeFiles.length"></span>
@@ -1801,6 +1805,20 @@
📂 Select
</el-button>
</div>
<!-- lightWeightExtensionTest:start -->
<div id="lightweight-extension-test-drop-zone"
style="display:none;margin-top:8px;border:1px dashed #409EFF;padding:10px;font-size:12px;color:#606266;background:#f8fbff;"
@dragover.prevent
@drop.prevent="handleLightweightExtensionFolderDrop($event)">
<div style="font-weight:600;color:#303133;">拖入固定结构拓展包</div>
<div style="margin-top:4px;line-height:1.5;">要求包含 raw_code/run.sh 与 raw_test_data/,系统会自动创建 Job、解析环境并开始 lightweight upload。</div>
<input id="lightweight-extension-test-folder-input" type="file" webkitdirectory directory multiple style="display:none" @change="handleLightweightExtensionFolderPick($event)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:8px;">
<span>{{ lightweightExtensionTestStatus }}</span>
<el-button size="small" type="success" @click="openLightweightExtensionFolderPicker">选择文件夹</el-button>
</div>
</div>
<!-- lightWeightExtensionTest:end -->
</div>
</div>
@@ -7955,6 +7973,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
working_dir: '',
output_dir: ''
});
const lightweightExtensionTestStatus = ref('拖入固定结构文件夹,系统将自动创建 Job、解析环境并开始 upload。');
const activeRnaseqScopeId = ref('');
const activeGenotypeScopeId = ref('');
@@ -8131,6 +8150,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
pop_snp_density: 'SNP密度分析',
rnaseq_de: 'RNA-seq DE',
combo: '组合图',
gene_family_report: '基因家族结果报告',
reference_classification: '参考亚家族分类',
subfamily_confirm: '亚家族标签确认',
ppi_string_reference_confirmation: 'Confirm the STRING reference species for PPI',
@@ -9310,6 +9330,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
statusDialogVisible: false,
statusEvents: []
});
const extensionEnvOptions = ref(['test4py311', 'rnaseq', 'rnaR']);
const lightweightExtensions = ref([]);
const lightweightExtensionConversation = reactive({
cards: [],
@@ -10300,6 +10321,7 @@ compare: [[flesh, peel]]`;
{ name: 'pep', label: '1️⃣2️⃣ PEP', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'expression', label: '1️⃣3️⃣ Expression (FPKM)', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'combo', label: '6️⃣ Combo', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'gene_family_report', label: '📄 基因家族结果报告', status: 'pending', images: [], files: [], elapsed_time: 0 },
// RNA-seq stages
{ name: 'rnaseq_qc', label: '1️⃣5️⃣ RNA-seq QC', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'rnaseq_align', label: '1️⃣6️⃣ RNA-seq Align', status: 'pending', images: [], files: [], elapsed_time: 0 },
@@ -15499,6 +15521,54 @@ compare: [[flesh, peel]]`;
return `⏸️ 批量执行已暂停:成功 ${completedCount}/${totalCount},暂停 ${pausedCount}/${totalCount}。真实原因:${reasonText}。`;
};
const openGeneratedGeneFamilyReport = async () => {
await refreshResults(jobInfo.job_id, 'gene_family_report');
const reportStage = stages.find(stage => stage.name === 'gene_family_report');
const reportFile = (reportStage?.files || []).find(file =>
file.role === 'gene_family_report' || String(file.name || '').endsWith('.gene_family_report.md')
);
if (!reportFile) return false;
rightPanelTab.value = 'results';
selectFile('gene_family_report', reportFile);
return true;
};
const submitGeneFamilyReportForBatch = async (stages, pausedCount) => {
if (pausedCount > 0) return false;
const plannedIds = (stages || []).map(stage => getStageId(stage)).filter(
stageId => stageId && !['summary', 'gene_family_report', 'subfamily_confirm'].includes(stageId)
);
const geneFamilyStages = new Set([
'identify', 'phylogeny', 'motif', 'domain', 'structure', 'promoter',
'synteny', 'interspecies_synteny', 'karyotype', 'combo', 'ppi', 'expression', 'pep'
]);
if (!plannedIds.some(stageId => geneFamilyStages.has(stageId))) return false;
const stageStatuses = Object.fromEntries(plannedIds.map(stageId => {
const status = stageExecutionStatus.value[stageId];
return [stageId, status === 'completed' ? 'success' : status === 'failed' ? 'failed' : 'missing'];
}));
const batchParams = batchExecuteParams.value || {};
const planParams = batchParams.plan?.params || {};
const stageParams = (stages || []).find(stage => stage && typeof stage === 'object' && stage.params)?.params || {};
try {
await executeSingleStageWithParams('gene_family_report', {
expectedStages: plannedIds,
stageStatuses,
reportAction: 'generate',
reportUseModel: true,
scopeId: batchParams.scopeId || planParams.scopeId || stageParams.scopeId || '',
analysisRoute: batchParams.analysisRoute || planParams.analysisRoute || stageParams.analysisRoute || '',
memberSetId: batchParams.memberSetId || planParams.memberSetId || stageParams.memberSetId || '',
rnaseqScopeId: batchParams.rnaseqScopeId || planParams.rnaseqScopeId || stageParams.rnaseqScopeId || ''
});
addAgentMessage('assistant', '📄 基因家族结果报告已生成。');
return true;
} catch (err) {
addAgentMessage('assistant', `⚠️ 基因家族结果报告生成失败:${err.message || err}`);
return false;
}
};
const confirmAndStartBatchExecute = async (event) => {
if (!event?.isTrusted) return;
batchExecutionUserConfirmed.value = true;
@@ -15616,6 +15686,7 @@ compare: [[flesh, peel]]`;
}).length;
const totalCount = stages.length;
let continuationOpened = false;
if (failedCount > 0) {
addAgentMessage('assistant', `⚠️ 批量执行完成:成功 ${completedCount}/${totalCount},失败 ${failedCount}/${totalCount}`);
} else if (pausedCount > 0) {
@@ -15623,13 +15694,16 @@ compare: [[flesh, peel]]`;
} else {
addAgentMessage('assistant', `🎉 批量执行完成!成功: ${completedCount}/${totalCount}`);
if (isMethodologyDataPreparationContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openMethodologyDataPreparationContinuation();
continuationOpened = await openMethodologyDataPreparationContinuation();
} else if (isMethodologyPostConfirmContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openMethodologyPostConfirmContinuation();
continuationOpened = await openMethodologyPostConfirmContinuation();
} else if (isContextualPostConfirmContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openContextualPostConfirmContinuation();
continuationOpened = await openContextualPostConfirmContinuation();
}
}
if (pausedCount === 0 && !continuationOpened) {
await submitGeneFamilyReportForBatch(stages, pausedCount);
}
}
};
@@ -18023,6 +18097,16 @@ compare: [[flesh, peel]]`;
requestBody.geoId = confirmParams.geoId || '';
requestBody.idTransform = confirmParams.idTransform || 'remove_T01';
}
if (stageName === 'gene_family_report') {
requestBody.expectedStages = activeStageParams.expectedStages || [];
requestBody.stageStatuses = activeStageParams.stageStatuses || {};
requestBody.reportAction = activeStageParams.reportAction || 'generate';
requestBody.reportUseModel = activeStageParams.reportUseModel !== false;
requestBody.scopeId = activeStageParams.scopeId || '';
requestBody.analysisRoute = activeStageParams.analysisRoute || '';
requestBody.memberSetId = activeStageParams.memberSetId || '';
requestBody.rnaseqScopeId = activeStageParams.rnaseqScopeId || '';
}
if (stageName === 'promoter_plot_revise') {
const elements = Array.isArray(confirmParams.promoterElements) ? confirmParams.promoterElements : [];
const categories = Array.isArray(confirmParams.promoterCategories) ? confirmParams.promoterCategories : [];
@@ -18113,6 +18197,9 @@ compare: [[flesh, peel]]`;
} else if (data.async === false) {
// 同步执行完成,直接返回结果
console.log(`[DEBUG-EXEC] ${stageName}: sync completed`);
if (stageName === 'gene_family_report') {
await openGeneratedGeneFamilyReport();
}
return data;
} else {
throw createStructuredExecuteError(data);
@@ -19381,6 +19468,9 @@ compare: [[flesh, peel]]`;
result: data
});
await handleStageCompleted(stageName, data, {});
if (stageName === 'gene_family_report') {
await openGeneratedGeneFamilyReport();
}
// 更新阶段结果 - 如果有分stage的文件,则更新所有stage的文件
if (stageIdx >= 0 && data.result) {
@@ -19949,6 +20039,10 @@ compare: [[flesh, peel]]`;
};
const extensionFileKey = (file) => `${file?.name || ''}::${file?.size || 0}::${file?.lastModified || 0}`;
const extensionUploadPath = (file) => {
const relative = String(file?.lightweightRelativePath || '').trim().replace(/^\/+/, '');
return relative || String(file?.name || '').trim();
};
const appendExtensionFiles = (target, files) => {
const current = Array.isArray(target) ? target : [];
const merged = [...current];
@@ -19974,6 +20068,53 @@ compare: [[flesh, peel]]`;
if (selected.length) ElMessage.success(`已选择 ${selected.length} 个测试数据文件`);
event.target.value = '';
};
const handleLightweightExtensionFolderDrop = (event) => {
const droppedFiles = Array.from(event?.dataTransfer?.files || []);
if (!droppedFiles.length) {
ElMessage.warning('未检测到可用文件');
return;
}
const codeExtPattern = /\.(py|r|sh|bash|pl|js|ts|java|c|cc|cpp|go|rs|jl|rb|m|smk|nf|ipynb)$/i;
const dataExtPattern = /\.(fastq|fq|gz|fa|fasta|fna|faa|pep|cds|gff|gff3|gtf|csv|tsv|txt|xlsx|xls|vcf|sam|bam)$/i;
const normalized = droppedFiles.map((file) => {
const relativePath = file.webkitRelativePath || file.name;
if (!file.lightweightRelativePath) {
Object.defineProperty(file, 'lightweightRelativePath', {
value: relativePath,
configurable: true
});
}
return file;
});
const codeFiles = [];
const dataFiles = [];
normalized.forEach((file) => {
const filePath = extensionUploadPath(file).toLowerCase();
if (codeExtPattern.test(filePath)) {
codeFiles.push(file);
} else if (dataExtPattern.test(filePath)) {
dataFiles.push(file);
} else {
codeFiles.push(file);
}
});
extensionBuild.codeFiles = appendExtensionFiles(extensionBuild.codeFiles, codeFiles);
extensionBuild.dataFiles = appendExtensionFiles(extensionBuild.dataFiles, dataFiles);
if (!extensionBuild.displayName) {
const firstPath = normalized[0]?.webkitRelativePath || normalized[0]?.name || '';
const folder = firstPath.includes('/') ? firstPath.split('/')[0] : '';
if (folder) {
extensionBuild.displayName = folder.replace(/[^a-zA-Z0-9_-]+/g, '_');
}
}
ElMessage.success(`已拖入 ${normalized.length} 个文件,已自动填充拓展包`);
};
const removeExtensionFile = (kind, file) => {
const key = extensionFileKey(file);
if (kind === 'code') {
@@ -20006,9 +20147,10 @@ compare: [[flesh, peel]]`;
};
const extensionApiUrl = (path) => {
const value = String(path || '');
if (!value.startsWith('/api/extension-stages') && !value.startsWith('/api/extension-stage-runs')) return value;
if (!value.startsWith('/api/extension-stages') && !value.startsWith('/api/extension-stage-runs') && !value.startsWith('/api/lightweight-extension-stages')) return value;
const host = window.location.hostname || '127.0.0.1';
return `${window.location.protocol}//${host}:7880${value}`;
const port = window.location.port;
return port ? `${window.location.protocol}//${host}:${port}${value}` : `${window.location.protocol}//${host}${value}`;
};
const extensionApiFetch = async (path, options = {}) => fetch(extensionApiUrl(path), options);
const pushExtensionStatus = (message, type = 'info', showDialog = false) => {
@@ -20369,9 +20511,10 @@ compare: [[flesh, peel]]`;
form.append('extension_id', extensionId);
if (hasCommand) form.append('original_command', extensionBuild.command);
form.append('operation_token', token);
extensionBuild.codeFiles.forEach(file => form.append('raw_code_files', file));
extensionBuild.dataFiles.forEach(file => form.append('raw_data_files', file));
extensionBuild.codeFiles.forEach(file => form.append('raw_code_files', file, extensionUploadPath(file)));
extensionBuild.dataFiles.forEach(file => form.append('raw_data_files', file, extensionUploadPath(file)));
if (hasDataPaths) form.append('raw_data_paths', extensionBuild.dataPaths);
form.append('runtime_env', extensionBuild.runtimeEnv);
const response = await extensionApiFetch('/api/lightweight-extension-stages/upload', { method: 'POST', body: form });
if (!response.ok) throw new Error(await extensionApiError(response, '原始包可复现检查提交失败'));
ElMessage.success('文件上传成功,已开始生成并验收');
@@ -20616,6 +20759,18 @@ compare: [[flesh, peel]]`;
console.warn('[lightweight extension] list failed', err);
}
};
const loadExtensionEnvOptions = async () => {
try {
const response = await extensionApiFetch('/api/extension-stages/envs');
if (!response.ok) return;
const data = await response.json();
if (data.success && Array.isArray(data.envs) && data.envs.length) {
extensionEnvOptions.value = data.envs;
}
} catch (err) {
console.warn('[extension env options] load failed', err);
}
};
const answerLightweightExtensionHelp = async (message) => {
const query = String(message || '');
const matched = (lightweightExtensions.value || []).find(item => query.includes(item.extension_id));
@@ -23857,6 +24012,7 @@ compare: [[flesh, peel]]`;
startRuntimeMonitor();
loadExtensionStages();
loadLightweightExtensionStages();
loadExtensionEnvOptions();
resumeLightweightOperation();
loadExtensionRuns();
loadMethodologyJobs();
@@ -24189,6 +24345,7 @@ compare: [[flesh, peel]]`;
intentHistory,
uploadedFiles,
extensionBuild,
extensionEnvOptions,
extensionStages,
extensionCompletedStages,
lightweightExtensionConversation,
@@ -24207,6 +24364,7 @@ compare: [[flesh, peel]]`;
extensionFileKey,
onExtensionCodeFilesChange,
onExtensionDataFilesChange,
handleLightweightExtensionFolderDrop,
removeExtensionFile,
buildExtensionStage,
confirmLightweightCandidate,
@@ -24226,6 +24384,7 @@ compare: [[flesh, peel]]`;
validateExtensionStage,
publishExtensionStage,
loadExtensionStages,
loadExtensionEnvOptions,
loadExtensionRuns,
runExtensionStage,
runExtensionStageFromIntent,
@@ -24345,6 +24504,24 @@ compare: [[flesh, peel]]`;
openPreview,
redrawImage,
createNewJob,
lightweightExtensionTestStatus,
handleLightweightExtensionFolderDrop: (event) => window.LightWeightExtensionTest && window.LightWeightExtensionTest.handleDrop({
jobInfo,
extensionBuild,
createNewJob,
appendExtensionFiles,
buildLightweightExtensionStage,
lightweightExtensionTestStatus,
}, event),
handleLightweightExtensionFolderPick: (event) => window.LightWeightExtensionTest && window.LightWeightExtensionTest.handlePick({
jobInfo,
extensionBuild,
createNewJob,
appendExtensionFiles,
buildLightweightExtensionStage,
lightweightExtensionTestStatus,
}, event),
openLightweightExtensionFolderPicker: () => window.LightWeightExtensionTest && window.LightWeightExtensionTest.openPicker(),
// Working DirectorySelect
existingJobs,
jobSelectorVisible,
@@ -24461,6 +24638,9 @@ compare: [[flesh, peel]]`;
document.getElementById('app').style.display = 'block';
console.log('App mounted successfully');
</script>
<!-- lightWeightExtensionTest:wiring:start -->
<script src="/lightWeightExtensionTest.js"></script>
<!-- lightWeightExtensionTest:wiring:end -->
<!-- 深度解读弹窗样式 -->
<style>
.interpretation-dialog .el-dialog__body { padding: 0; max-height: 70vh; overflow-y: auto; }
File diff suppressed because it is too large Load Diff
+712
View File
@@ -0,0 +1,712 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Graph</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus@2.7.1/dist/index.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.7.1/dist/index.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@element-plus/icons-vue"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="/static/i18n.js"></script>
<style>
body { margin: 0; font-family: 'Helvetica Neue', Arial, sans-serif; background: #f5f7fa; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; display: flex; align-items: center; justify-content: space-between; }
.header h1 { margin: 0; font-size: 22px; }
.header .subtitle { font-size: 12px; opacity: 0.8; margin-top: 4px; }
.main-container { display: flex; padding: 15px; gap: 15px; height: calc(100vh - 70px); }
.left-panel { width: 300px; display: flex; flex-direction: column; gap: 10px; }
.control-section { background: white; border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.control-section h4 { margin: 0 0 10px 0; color: #409EFF; font-size: 14px; border-bottom: 2px solid #409EFF; padding-bottom: 8px; }
.stat-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
.stat-item:last-child { border-bottom: none; }
.stat-label { color: #666; }
.stat-value { font-weight: bold; color: #409EFF; }
.filter-section { margin-top: 10px; }
.filter-tag { margin: 4px; cursor: pointer; }
.filter-tag.active { background: #409EFF; color: white; }
.main-panel { flex: 1; display: flex; flex-direction: column; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
.toolbar { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 10px; }
#graph-container { flex: 1; position: relative; }
.right-panel { width: 350px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 15px; overflow-y: auto; }
.right-panel h4 { margin: 0 0 10px 0; color: #67C23A; border-bottom: 2px solid #67C23A; padding-bottom: 8px; }
.node-detail { padding: 10px; background: #f5f7fa; border-radius: 6px; margin-top: 10px; }
.node-detail .label { color: #666; font-size: 12px; margin-bottom: 4px; }
.node-detail .value { font-weight: 500; color: #333; }
.relation-list { margin-top: 10px; }
.relation-item { padding: 8px; background: #f0f9eb; border-radius: 4px; margin-bottom: 8px; font-size: 13px; }
.relation-item .source { color: #409EFF; font-weight: 500; }
.relation-item .rel { color: #67C23A; margin: 0 4px; }
.relation-item .target { color: #E6A23C; font-weight: 500; }
.search-box { margin-bottom: 10px; }
.legend { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 12px; }
.legend-color { width: 12px; height: 12px; border-radius: 50%; }
.legend-symbol { width: 12px; height: 12px; border-radius: 2px; }
.legend-line { width: 20px; height: 2px; }
/* PC语义分类样式 */
.pc-category-header {
font-size: 11px;
color: #666;
font-weight: bold;
margin: 8px 0 4px 0;
padding-left: 4px;
border-left: 3px solid #409EFF;
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<div>
<h1>{{ t('kg-visualization') }}</h1>
<div class="subtitle">{{ t('kg-subtitle') }}</div>
</div>
<el-button type="primary" @click="goBack">
<el-icon><arrow-left /></el-icon> {{ t('back-to-main') }}
</el-button>
</div>
<div class="main-container">
<!-- 左侧面板 -->
<div class="left-panel">
<div class="control-section">
<h4>{{ t('kg-stats') }}</h4>
<div class="stat-item">
<span class="stat-label">{{ t('node-count') }}</span>
<span class="stat-value">{{ stats.nodes }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ t('edge-count') }}</span>
<span class="stat-value">{{ stats.edges }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ t('node-types') }}</span>
<span class="stat-value">{{ stats.nodeTypes }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ t('edge-types') }}</span>
<span class="stat-value">{{ stats.edgeTypes }}</span>
</div>
</div>
<div class="control-section">
<h4>{{ t('node-search') }}</h4>
<el-input
v-model="searchQuery"
:placeholder="t('enter-gene-name')"
clearable
@keyup.enter="searchNode"
>
<template #append>
<el-button @click="searchNode">
<el-icon><search /></el-icon>
</el-button>
</template>
</el-input>
</div>
<div class="control-section">
<h4>{{ t('relation-filter-pc') }}</h4>
<div class="filter-section">
<div v-for="(types, category) in relationTypesByCategory" :key="category" style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 5px; font-weight: bold;">
{{ category === 'activation' ? t('activation-induction') :
category === 'repression' ? t('repression-inhibition') :
category === 'regulation' ? t('regulation-control') :
category === 'expression' ? t('expression-detection') :
category === 'association' ? t('association-interaction') :
category === 'localization' ? t('localization') :
category === 'containment' ? t('containment-composition') :
category === 'participation' ? t('participation-function') : t('other-rel') }}
</div>
<el-tag
v-for="type in types"
:key="type"
class="filter-tag"
:class="{ active: selectedRelations.includes(type) }"
@click="toggleRelationFilter(type)"
size="small"
:style="{ borderColor: relationEdgeStyles[category].color, color: selectedRelations.includes(type) ? '#fff' : relationEdgeStyles[category].color }"
>
{{ type }} ({{ relationCounts[type] }})
</el-tag>
</div>
</div>
</div>
<div class="control-section">
<h4>{{ t('node-legend-pc') }}</h4>
<div class="pc-category-header">Gene/Protein</div>
<div class="legend">
<div class="legend-item" v-for="type in ['gene', 'gene_id', 'gene_group', 'gene_family']" :key="type">
<div class="legend-symbol" :style="{ background: nodeColors[type] }"></div>
<span>{{ nodeTypeLabels[type] }}</span>
</div>
</div>
<div class="pc-category-header">Phenotype</div>
<div class="legend">
<div class="legend-item" v-for="type in ['phenotype', 'attribute']" :key="type">
<div class="legend-symbol" :style="{ background: nodeColors[type] }"></div>
<span>{{ nodeTypeLabels[type] }}</span>
</div>
</div>
<div class="pc-category-header">Cell/Organ/Organism</div>
<div class="legend">
<div class="legend-item" v-for="type in ['tissue', 'species']" :key="type">
<div class="legend-symbol" :style="{ background: nodeColors[type] }"></div>
<span>{{ nodeTypeLabels[type] }}</span>
</div>
</div>
<div class="pc-category-header">Biological Process</div>
<div class="legend">
<div class="legend-item" v-for="type in ['process', 'time', 'condition', 'stress']" :key="type">
<div class="legend-symbol" :style="{ background: nodeColors[type] }"></div>
<span>{{ nodeTypeLabels[type] }}</span>
</div>
</div>
<div class="pc-category-header">Genomic</div>
<div class="legend">
<div class="legend-item" v-for="type in ['location', 'sra_dataset', 'data_type', 'count']" :key="type">
<div class="legend-symbol" :style="{ background: nodeColors[type] }"></div>
<span>{{ nodeTypeLabels[type] }}</span>
</div>
</div>
<h4 style="margin-top: 15px;">🔗 {{ t('edge-legend-pc') }}</h4>
<div class="legend">
<div class="legend-item" v-for="(style, cat) in relationEdgeStyles" :key="cat">
<div class="legend-line" :style="{ background: style.color, height: style.width + 'px', borderTop: style.type === 'dashed' ? '2px dashed ' + style.color : 'none' }"></div>
<span>{{ cat === 'activation' ? 'Activation/Induction' :
cat === 'repression' ? 'Repression/Inhibition' :
cat === 'regulation' ? 'Regulation/Control' :
cat === 'expression' ? 'Expression/Detection' :
cat === 'association' ? 'Association/Interaction' :
cat === 'localization' ? 'Localization' :
cat === 'containment' ? 'Containment/Composition' :
cat === 'participation' ? 'Participation/Function' : 'Other' }}</span>
</div>
</div>
</div>
</div>
<!-- 主面板 -->
<div class="main-panel">
<div class="toolbar">
<el-button-group>
<el-button @click="fitGraph">
<el-icon><full-screen /></el-icon> {{ t('fit-view') }}
</el-button>
<el-button @click="resetFilters">
<el-icon><refresh /></el-icon> {{ t('reset-filters') }}
</el-button>
</el-button-group>
<el-divider direction="vertical"></el-divider>
<span style="color: #666; font-size: 13px;">
{{ t('click-drag-hint') }}
</span>
</div>
<div id="graph-container"></div>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<h4>{{ t('node-detail') }}</h4>
<div v-if="selectedNode" class="node-detail">
<div style="margin-bottom: 15px;">
<div class="label">{{ t('node-name') }}</div>
<div class="value" style="font-size: 18px; color: #409EFF;">{{ selectedNode.name }}</div>
</div>
<div style="margin-bottom: 15px;">
<div class="label">{{ t('node-type-pc') }}</div>
<div class="value">
<el-tag :type="getNodeTypeTag(selectedNode.category)" :style="{ backgroundColor: nodeColors[selectedNode.category] + '20', borderColor: nodeColors[selectedNode.category], color: nodeColors[selectedNode.category] }">
{{ nodeTypeLabels[selectedNode.category] || selectedNode.category }}
</el-tag>
</div>
</div>
<div>
<div class="label">{{ t('connection-count') }}</div>
<div class="value">{{ selectedNode.degree }}{{ t('num-connections') }}</div>
</div>
</div>
<div v-else style="color: #999; text-align: center; padding: 40px;">
{{ t('click-node-detail') }}
</div>
<template v-if="selectedNodeRelations.length > 0">
<h4 style="margin-top: 20px;">🔗 {{ t('related-relations') }} ({{ selectedNodeRelations.length }})</h4>
<div class="relation-list">
<div
v-for="rel in selectedNodeRelations.slice(0, 10)"
:key="rel.id"
class="relation-item"
:style="{ borderLeft: '3px solid ' + relationEdgeStyles[rel.category || relationCategories[rel.name] || 'other'].color }"
>
<span class="source">{{ rel.source }}</span>
<span class="rel" :style="{ color: relationEdgeStyles[rel.category || relationCategories[rel.name] || 'other'].color }">--{{ rel.name }}--></span>
<span class="target">{{ rel.target }}</span>
</div>
<div v-if="selectedNodeRelations.length > 10" style="text-align: center; color: #999; padding: 10px;">
{{ tp('more-relations', { count: selectedNodeRelations.length - 10 }) }}
</div>
</div>
</template>
</div>
</div>
</div>
<script>
const { createApp, ref, onMounted, computed } = Vue;
const { ElMessage } = ElementPlus;
createApp({
setup() {
const t = window.t || ((key) => key);
const tp = window.tp || ((key, params) => {
let s = t(key);
if (params) Object.keys(params).forEach(k => { s = s.replace('{' + k + '}', params[k]); });
return s;
});
document.title = t('kg-title');
const graphData = ref({ nodes: [], edges: [] });
const chart = ref(null);
const searchQuery = ref('');
const selectedNode = ref(null);
const selectedRelations = ref([]);
// PC (Pathway Commons) 语义分类颜色映射
const nodeColors = {
// Gene/Protein - 蓝色
'gene': '#5DADE2',
'gene_id': '#85C1E9',
'gene_group': '#AED6F1',
'gene_family': '#5DADE2',
// Phenotype - 绿色
'phenotype': '#58D68D',
'attribute': '#82E0AA',
// Cell/Organ/Organism - 粉色
'tissue': '#F5B7B1',
'species': '#F1948A',
// Process - 紫色菱形
'process': '#AF7AC5',
'time': '#C39BD3',
'condition': '#D7BDE2',
'stress': '#E8DAEF',
// Location - 浅绿色
'location': '#82E0AA',
// Data - 橙色(更显眼)
'sra_dataset': '#E67E22',
'data_type': '#F39C12',
'count': '#EAEDED',
'unknown': '#95A5A6'
,
'article': '#2ecc71',
'paper_title': '#9b59b6'
};
// PC 语义分类显示名称
const nodeTypeLabels = {
'gene': 'Gene/Protein',
'gene_id': 'Gene Identifier',
'gene_group': 'Gene Group',
'gene_family': 'Gene Family',
'phenotype': 'Phenotype',
'attribute': 'Attribute',
'tissue': 'Cell/Organ/Tissue',
'species': 'Organism',
'process': 'Biological Process',
'time': 'Time/Stage',
'condition': 'Condition',
'stress': 'Stress',
'location': 'Genomic Location',
'sra_dataset': 'SRA Dataset',
'geo_dataset': 'GEO Dataset',
'data_type': 'Data Type',
'count': 'Count/Statistic',
'article': 'Article (PMID)',
'paper_title': 'Paper Title',
'unknown': 'Other'
};
// PC 关系类型分类
const relationCategories = {
// Activation/Induction - 绿色箭头
'induces': 'activation',
'promote': 'activation',
'overexpression_increases': 'activation',
'enhanced_tolerance_to': 'activation',
// Repression/Inhibition - 红色T型
'down_regulated_during': 'repression',
'down_regulation_improves': 'repression',
// Regulation - 蓝色
'regulate': 'regulation',
'regulates': 'regulation',
'regulating': 'regulation',
// Expression - 橙色
'expressed_in': 'expression',
'expressed_during': 'expression',
'was_expressed_during': 'expression',
'expression_reached_peak_at': 'expression',
// Association - 紫色
'collinear_with': 'association',
'was_highly_orthologous_to': 'association',
'related_to': 'association',
'affect': 'association',
// Localization - 深绿
'located_on': 'localization',
'identified_in': 'localization',
// Containment - 青色
'contain': 'containment',
'had': 'containment',
'has_gene_id': 'containment',
'has_data_type': 'containment',
// Participation - 深红
'involved_in': 'participation',
'participate_in': 'participation',
'default': 'other'
};
const relationEdgeStyles = {
'activation': { color: '#27AE60', width: 2, type: 'solid' },
'repression': { color: '#E74C3C', width: 2, type: 'solid' },
'regulation': { color: '#3498DB', width: 2, type: 'solid' },
'expression': { color: '#E67E22', width: 1.5, type: 'solid' },
'association': { color: '#9B59B6', width: 1.5, type: 'dashed' },
'localization': { color: '#16A085', width: 1.5, type: 'solid' },
'containment': { color: '#1ABC9C', width: 1.5, type: 'solid' },
'participation': { color: '#C0392B', width: 2, type: 'solid' },
'other': { color: '#95A5A6', width: 1, type: 'solid' }
};
const stats = computed(() => ({
nodes: graphData.value.nodes.length,
edges: graphData.value.edges.length,
nodeTypes: Object.keys(nodeColors).length,
edgeTypes: relationTypes.value.length
}));
// 按 PC 语义分类的关系类型
const relationTypesByCategory = computed(() => {
const cats = {};
graphData.value.edges.forEach(e => {
const cat = relationCategories[e.relation] || 'other';
if (!cats[cat]) cats[cat] = [];
cats[cat].push(e.relation);
});
// 去重并排序
for (const cat in cats) {
cats[cat] = [...new Set(cats[cat])].sort();
}
return cats;
});
const relationTypes = computed(() => {
const types = new Set();
graphData.value.edges.forEach(e => types.add(e.relation));
return Array.from(types).sort();
});
const relationCounts = computed(() => {
const counts = {};
graphData.value.edges.forEach(e => {
counts[e.relation] = (counts[e.relation] || 0) + 1;
});
return counts;
});
const selectedNodeRelations = computed(() => {
if (!selectedNode.value) return [];
return graphData.value.edges.filter(e =>
e.source === selectedNode.value.id || e.target === selectedNode.value.id
);
});
const loadData = async () => {
try {
// 优先加载包含多组学数据的知识图谱
let response = await fetch('knowledge_graph_with_omics.json');
if (!response.ok) {
// 回退到标准知识图谱
response = await fetch('knowledge_graph.json');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// 验证数据结构
if (!data.nodes || !Array.isArray(data.nodes)) {
throw new Error(t('err-data-format'));
}
if (!data.edges || !Array.isArray(data.edges)) {
console.warn(t('warn-edges-undefined'));
data.edges = [];
}
// 计算节点度数
const nodeDegrees = {};
data.edges.forEach(e => {
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
});
// 根据 PC 语义分类设置节点形状
const getNodeSymbol = (type) => {
const symbolMap = {
'gene': 'circle',
'gene_id': 'circle',
'gene_group': 'circle',
'gene_family': 'circle',
'phenotype': 'rect',
'attribute': 'rect',
'tissue': 'roundRect',
'species': 'roundRect',
'process': 'diamond',
'time': 'diamond',
'condition': 'diamond',
'stress': 'diamond',
'location': 'triangle',
'sra_dataset': 'pin',
'data_type': 'pin',
'count': 'pin'
};
return symbolMap[type] || 'circle';
};
graphData.value = {
nodes: data.nodes.map(n => ({
id: n.id,
name: n.id,
category: n.type || 'unknown',
degree: nodeDegrees[n.id] || 0,
symbol: getNodeSymbol(n.type),
symbolSize: (n.type === 'gene' ? 20 : n.type === 'sra_dataset' ? 18 : 12) + Math.min((nodeDegrees[n.id] || 0) * 0.3, 15),
itemStyle: {
color: nodeColors[n.type] || nodeColors.unknown,
borderColor: '#fff',
borderWidth: 1
}
})),
edges: data.edges.map((e, i) => {
const cat = relationCategories[e.relation] || 'other';
const style = relationEdgeStyles[cat];
return {
id: i,
source: e.source,
target: e.target,
name: e.relation,
relation: e.relation,
category: cat,
lineStyle: {
color: style.color,
width: style.width,
type: style.type,
curveness: 0.1
}
};
})
};
initChart();
ElMessage.success(t('msg-kg-loaded'));
} catch (error) {
ElMessage.error(t('msg-load-data-failed') + error.message);
}
};
const initChart = () => {
const container = document.getElementById('graph-container');
chart.value = echarts.init(container);
const option = {
tooltip: {
formatter: params => {
if (params.dataType === 'node') {
const pcType = nodeTypeLabels[params.data.category] || params.data.category;
return `<b>${params.name}</b><br/>PC: ${pcType}<br/>${t('connection-count').replace('Connections', 'Deg')}: ${params.data.degree}`;
}
const cat = params.data.category || relationCategories[params.name] || 'other';
const catName = cat === 'activation' ? 'Activation' :
cat === 'repression' ? 'Repression' :
cat === 'regulation' ? 'Regulation' :
cat === 'expression' ? 'Expression' :
cat === 'association' ? 'Association' :
cat === 'localization' ? 'Localization' :
cat === 'containment' ? 'Containment' :
cat === 'participation' ? 'Participation' : 'Other';
return `<b>${params.data.source}</b> <span style="color:${relationEdgeStyles[cat].color}">--${params.name}--></span> <b>${params.data.target}</b><br/><small>PC: ${catName}</small>`;
}
},
series: [{
type: 'graph',
layout: 'force',
data: graphData.value.nodes,
links: graphData.value.edges,
roam: true,
draggable: true,
label: {
show: true,
position: 'right',
formatter: '{b}'
},
force: {
repulsion: 300,
gravity: 0.1,
edgeLength: 100,
layoutAnimation: true
},
emphasis: {
focus: 'adjacency',
lineStyle: { width: 4 }
}
}]
};
chart.value.setOption(option);
chart.value.on('click', params => {
if (params.dataType === 'node') {
selectedNode.value = params.data;
} else {
selectedNode.value = null;
}
});
window.addEventListener('resize', () => chart.value.resize());
};
const searchNode = () => {
if (!searchQuery.value || !chart.value) return;
const node = graphData.value.nodes.find(n =>
n.id.toLowerCase().includes(searchQuery.value.toLowerCase())
);
if (node) {
chart.value.dispatchAction({
type: 'highlight',
seriesIndex: 0,
name: node.id
});
selectedNode.value = node;
ElMessage.success(t('msg-node-found') + node.name);
} else {
ElMessage.warning(t('msg-node-not-found'));
}
};
const toggleRelationFilter = (type) => {
const index = selectedRelations.value.indexOf(type);
if (index > -1) {
selectedRelations.value.splice(index, 1);
} else {
selectedRelations.value.push(type);
}
if (selectedRelations.value.length === 0) {
chart.value.setOption({
series: [{ data: graphData.value.nodes, links: graphData.value.edges }]
});
} else {
const filteredEdges = graphData.value.edges.filter(e =>
selectedRelations.value.includes(e.relation)
);
const connectedNodes = new Set();
filteredEdges.forEach(e => {
connectedNodes.add(e.source);
connectedNodes.add(e.target);
});
const filteredNodes = graphData.value.nodes.filter(n =>
connectedNodes.has(n.id)
);
chart.value.setOption({
series: [{ data: filteredNodes, links: filteredEdges }]
});
}
};
const fitGraph = () => {
if (chart.value) {
chart.value.dispatchAction({ type: 'restore' });
}
};
const resetFilters = () => {
selectedRelations.value = [];
selectedNode.value = null;
searchQuery.value = '';
if (chart.value) {
chart.value.setOption({
series: [{ data: graphData.value.nodes, links: graphData.value.edges }]
});
chart.value.dispatchAction({ type: 'restore' });
}
};
const getNodeTypeTag = (type) => {
const typeMap = { 'gene': '', 'gene_id': 'success', 'tissue': 'warning', 'location': 'info' };
return typeMap[type] || '';
};
const goBack = () => {
window.location.href = '/';
};
onMounted(() => {
loadData().then(() => {
// 检查URL参数,如果有highlight则自动搜索高亮
const urlParams = new URLSearchParams(window.location.search);
const highlightEntity = urlParams.get('highlight');
if (highlightEntity) {
searchQuery.value = highlightEntity;
setTimeout(() => {
searchNode();
}, 500); // 等待图表初始化完成
}
});
});
return {
t,
tp,
stats,
searchQuery,
selectedNode,
selectedRelations,
relationTypes,
relationTypesByCategory,
relationCounts,
relationEdgeStyles,
relationCategories,
nodeTypeLabels,
selectedNodeRelations,
nodeColors,
searchNode,
toggleRelationFilter,
fitGraph,
resetFilters,
getNodeTypeTag,
goBack
};
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>
+304
View File
@@ -0,0 +1,304 @@
(function () {
const PY_EXTENSIONS = new Set(['.py']);
const R_EXTENSIONS = new Set(['.r']);
function normalizedRelativePath(file) {
const relative = String(file.webkitRelativePath || file.relativePath || file.name || '').replace(/^\/+/, '');
return relative.split('\\').join('/');
}
function parsePythonImports(content) {
const imports = new Set();
const lines = String(content || '').split(/\r?\n/);
for (const line of lines) {
let match = line.match(/^\s*import\s+([A-Za-z0-9_.,\s]+)/);
if (match) {
match[1].split(',').forEach((item) => {
const token = item.trim().split(/\s+as\s+/i)[0].split('.')[0].trim();
if (token) imports.add(token);
});
}
match = line.match(/^\s*from\s+([A-Za-z0-9_.]+)\s+import\s+/);
if (match) {
const token = match[1].split('.')[0].trim();
if (token) imports.add(token);
}
}
return Array.from(imports);
}
function parseRPackages(content) {
const packages = new Set();
const pattern = /(?:library|require)\(\s*["']?([A-Za-z0-9_.-]+)["']?\s*\)/g;
let match;
const text = String(content || '');
while ((match = pattern.exec(text)) !== null) {
if (match[1]) packages.add(match[1]);
}
return Array.from(packages);
}
function commonPathPrefix(files) {
const paths = files.map((f) => normalizedRelativePath(f).split('/')).filter((parts) => parts.length > 0);
if (!paths.length) return '';
const first = paths[0];
let prefixLen = 0;
for (let i = 0; i < first.length; i++) {
if (!paths.every((parts) => parts[i] === first[i])) break;
prefixLen = i + 1;
}
return first.slice(0, prefixLen).join('/');
}
function sanitizeDisplayName(raw) {
return String(raw || '').replace(/[^A-Za-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '') || 'lightweight_extension';
}
function extractRootName(files) {
const prefix = commonPathPrefix(files);
const base = (prefix && prefix !== 'raw_code' && prefix !== 'raw_test_data')
? sanitizeDisplayName(prefix.split('/')[0])
: 'lightweight_extension_drop';
const ts = Date.now();
return base + '_' + ts;
}
function splitFixedStructure(files) {
const codeFiles = [];
const dataFiles = [];
let hasRunScript = false;
let prefix = commonPathPrefix(files);
if (prefix && !prefix.endsWith('/')) prefix += '/';
const sampled = [];
for (const file of files) {
let relativePath = normalizedRelativePath(file);
if (prefix && relativePath.startsWith(prefix)) {
relativePath = relativePath.slice(prefix.length);
}
if (sampled.length < 10) sampled.push(relativePath);
if (relativePath.startsWith('raw_code/')) {
file.lightweightRelativePath = relativePath.slice('raw_code/'.length) || file.name;
codeFiles.push(file);
if (relativePath === 'raw_code/run.sh') {
hasRunScript = true;
}
} else if (relativePath.startsWith('raw_test_data/')) {
file.lightweightRelativePath = relativePath.slice('raw_test_data/'.length) || file.name;
dataFiles.push(file);
}
}
if (!codeFiles.length) {
const preview = sampled.length ? sampled.slice(0, 10).join(', ') : '(无)';
throw new Error('缺少 raw_code/ 目录(检测到的路径样例: ' + preview + ')');
}
if (!dataFiles.length) {
throw new Error('缺少 raw_test_data/ 目录');
}
if (!hasRunScript) {
throw new Error('缺少 raw_code/run.sh');
}
return { codeFiles, dataFiles };
}
async function extractDependencies(files) {
const pythonImports = new Set();
const rPackages = new Set();
for (const file of files) {
const path = normalizedRelativePath(file);
const lower = path.toLowerCase();
const extension = lower.includes('.') ? lower.slice(lower.lastIndexOf('.')) : '';
if (!PY_EXTENSIONS.has(extension) && !R_EXTENSIONS.has(extension)) {
continue;
}
const content = await file.text();
if (PY_EXTENSIONS.has(extension)) {
parsePythonImports(content).forEach((name) => pythonImports.add(name));
} else {
parseRPackages(content).forEach((name) => rPackages.add(name));
}
}
return {
python_imports: Array.from(pythonImports),
r_packages: Array.from(rPackages),
};
}
async function encodeBase64(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = '';
const chunkSize = 0x8000;
for (let index = 0; index < bytes.length; index += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
}
return btoa(binary);
}
async function importFolderToJob(hooks, rootName, files) {
const payload = {
job_id: String(hooks.jobInfo.job_id || '').trim(),
root_name: rootName,
files: [],
relative_paths: [],
};
for (const file of files) {
payload.files.push(await encodeBase64(file));
payload.relative_paths.push(normalizedRelativePath(file));
}
const response = await fetch('/api/job/import-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.detail || data.error || data.message || 'Job 导入失败');
}
return data;
}
async function resolveRuntimeEnv(files) {
const dependencies = await extractDependencies(files);
console.log('[lightWeightExtensionTest] 提取的依赖:', JSON.stringify(dependencies), '| 扫描文件数:', files.length);
try {
const response = await fetch('/api/extension-stages/envs/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dependencies),
});
const data = await response.json();
console.log('[lightWeightExtensionTest] 后端 env/resolve 原始响应:', response.status, JSON.stringify(data).slice(0, 500));
if (!response.ok || !data.success) {
throw new Error(data.detail || data.error || data.message || '运行环境解析失败');
}
return data;
} catch (e) {
console.error('[lightWeightExtensionTest] env/resolve 调用失败:', e);
throw e;
}
}
function notifyError(message) {
if (window.ElementPlus && window.ElementPlus.ElMessage) {
window.ElementPlus.ElMessage.error(message);
}
}
async function ensureJob(hooks) {
if (String(hooks.jobInfo.job_id || '').trim() && String(hooks.jobInfo.job_id) !== 'default_job') {
return;
}
await hooks.createNewJob();
if (!String(hooks.jobInfo.job_id || '').trim() || String(hooks.jobInfo.job_id) === 'default_job') {
throw new Error('自动创建 Job 失败');
}
}
async function processFolderFiles(hooks, inputFiles) {
const files = Array.from(inputFiles || []).filter((file) => normalizedRelativePath(file));
if (!files.length) {
throw new Error('未读取到任何文件');
}
hooks.lightweightExtensionTestStatus.value = '正在校验目录结构...';
const rootName = extractRootName(files);
const fixed = splitFixedStructure(files);
hooks.lightweightExtensionTestStatus.value = '正在确保 Job 可用...';
await ensureJob(hooks);
hooks.lightweightExtensionTestStatus.value = '正在解析运行环境...';
const envResult = await resolveRuntimeEnv(files);
hooks.extensionBuild.runtimeEnv = envResult.runtime_env || 'test4py311';
console.log(
'[lightWeightExtensionTest] ✓ 选定环境: ' + hooks.extensionBuild.runtimeEnv,
'| 原因: ' + (envResult.reason || 'unknown'),
'| 兜底: ' + (envResult.fallback ? '是' : '否'),
envResult.matches ? '| 候选: ' + JSON.stringify(envResult.matches.map(m => m.env + '(py:' + (m.python_hits || []).join(',') + ' r:' + (m.r_hits || []).join(',') + ')')) : '',
);
hooks.lightweightExtensionTestStatus.value = '正在镜像导入当前 Job...';
await importFolderToJob(hooks, rootName, files);
hooks.extensionBuild.displayName = rootName;
hooks.extensionBuild.command = 'bash run.sh';
hooks.extensionBuild.codeFiles = hooks.appendExtensionFiles([], fixed.codeFiles);
hooks.extensionBuild.dataFiles = hooks.appendExtensionFiles([], fixed.dataFiles);
hooks.extensionBuild.dataPaths = '';
hooks.lightweightExtensionTestStatus.value = `运行环境 ${hooks.extensionBuild.runtimeEnv} 已确定,正在触发 upload...`;
await hooks.buildLightweightExtensionStage();
hooks.lightweightExtensionTestStatus.value = '已开始 upload,系统正在自动轮询并等待候选卡。';
}
async function readEntry(entry, basePath) {
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
Object.defineProperty(file, 'webkitRelativePath', {
value: basePath ? basePath + '/' + entry.name : entry.name,
writable: false,
});
resolve([file]);
});
});
}
if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await new Promise((resolve) => {
reader.readEntries(resolve);
});
const subPath = basePath ? basePath + '/' + entry.name : entry.name;
const nested = await Promise.all(entries.map((e) => readEntry(e, subPath)));
return nested.flat();
}
return [];
}
async function readDroppedFiles(dataTransfer) {
if (!dataTransfer || !dataTransfer.items) {
return Array.from(dataTransfer?.files || []);
}
const items = Array.from(dataTransfer.items);
const entries = items
.map((item) => item.webkitGetAsEntry && item.webkitGetAsEntry())
.filter(Boolean);
if (!entries.length) {
return Array.from(dataTransfer.files || []);
}
const results = await Promise.all(entries.map((entry) => readEntry(entry, '')));
return results.flat();
}
function mountApi() {
window.LightWeightExtensionTest = {
async handleDrop(hooks, event) {
try {
const files = await readDroppedFiles(event?.dataTransfer);
await processFolderFiles(hooks, files);
} catch (error) {
const message = `失败:${error.message || '目录处理失败'}`;
hooks.lightweightExtensionTestStatus.value = message;
notifyError(message);
}
},
async handlePick(hooks, event) {
const files = event?.target?.files || [];
try {
await processFolderFiles(hooks, files);
} catch (error) {
const message = `失败:${error.message || '目录处理失败'}`;
hooks.lightweightExtensionTestStatus.value = message;
notifyError(message);
} finally {
if (event?.target) {
event.target.value = '';
}
}
},
openPicker() {
const input = document.getElementById('lightweight-extension-test-folder-input');
if (input) {
input.click();
}
},
};
}
mountApi();
})();
+801 -39
View File
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
// global wrapper
/*! Element Plus v2.3.14 */
window.en = {
name: "en",
el: {
colorpicker: {
confirm: "OK",
clear: "Clear",
defaultLabel: "color picker",
description: "current color is {color}. press enter to select a new color."
},
datepicker: {
now: "Now",
today: "Today",
cancel: "Cancel",
clear: "Clear",
confirm: "OK",
dateTablePrompt: "Use the arrow keys and enter to select the day of the month",
monthTablePrompt: "Use the arrow keys and enter to select the month",
yearTablePrompt: "Use the arrow keys and enter to select the year",
selectedDate: "Selected date",
selectDate: "Select date",
selectTime: "Select time",
startDate: "Start Date",
startTime: "Start Time",
endDate: "End Date",
endTime: "End Time",
prevYear: "Previous Year",
nextYear: "Next Year",
prevMonth: "Previous Month",
nextMonth: "Next Month",
year: "",
month1: "January",
month2: "February",
month3: "March",
month4: "April",
month5: "May",
month6: "June",
month7: "July",
month8: "August",
month9: "September",
month10: "October",
month11: "November",
month12: "December",
week: "week",
weeks: {
sun: "Sun",
mon: "Mon",
tue: "Tue",
wed: "Wed",
thu: "Thu",
fri: "Fri",
sat: "Sat"
},
weeksFull: {
sun: "Sunday",
mon: "Monday",
tue: "Tuesday",
wed: "Wednesday",
thu: "Thursday",
fri: "Friday",
sat: "Saturday"
},
months: {
jan: "Jan",
feb: "Feb",
mar: "Mar",
apr: "Apr",
may: "May",
jun: "Jun",
jul: "Jul",
aug: "Aug",
sep: "Sep",
oct: "Oct",
nov: "Nov",
dec: "Dec"
}
},
inputNumber: {
decrease: "decrease number",
increase: "increase number"
},
select: {
loading: "Loading",
noMatch: "No matching data",
noData: "No data",
placeholder: "Select"
},
dropdown: {
toggleDropdown: "Toggle Dropdown"
},
cascader: {
noMatch: "No matching data",
loading: "Loading",
placeholder: "Select",
noData: "No data"
},
pagination: {
goto: "Go to",
pagesize: "/page",
total: "Total {total}",
pageClassifier: "",
page: "Page",
prev: "Go to previous page",
next: "Go to next page",
currentPage: "page {pager}",
prevPages: "Previous {pager} pages",
nextPages: "Next {pager} pages",
deprecationWarning: "Deprecated usages detected, please refer to the el-pagination documentation for more details"
},
dialog: {
close: "Close this dialog"
},
drawer: {
close: "Close this dialog"
},
messagebox: {
title: "Message",
confirm: "OK",
cancel: "Cancel",
error: "Illegal input",
close: "Close this dialog"
},
upload: {
deleteTip: "press delete to remove",
delete: "Delete",
preview: "Preview",
continue: "Continue"
},
slider: {
defaultLabel: "slider between {min} and {max}",
defaultRangeStartLabel: "pick start value",
defaultRangeEndLabel: "pick end value"
},
table: {
emptyText: "No Data",
confirmFilter: "Confirm",
resetFilter: "Reset",
clearFilter: "All",
sumText: "Sum"
},
tree: {
emptyText: "No Data"
},
transfer: {
noMatch: "No matching data",
noData: "No data",
titles: ["List 1", "List 2"],
filterPlaceholder: "Enter keyword",
noCheckedFormat: "{total} items",
hasCheckedFormat: "{checked}/{total} checked"
},
image: {
error: "FAILED"
},
pageHeader: {
title: "Back"
},
popconfirm: {
confirmButtonText: "Yes",
cancelButtonText: "No"
}
}
};
+165
View File
@@ -0,0 +1,165 @@
/*! Element Plus v2.3.14 */
var en = {
name: "en",
el: {
colorpicker: {
confirm: "OK",
clear: "Clear",
defaultLabel: "color picker",
description: "current color is {color}. press enter to select a new color."
},
datepicker: {
now: "Now",
today: "Today",
cancel: "Cancel",
clear: "Clear",
confirm: "OK",
dateTablePrompt: "Use the arrow keys and enter to select the day of the month",
monthTablePrompt: "Use the arrow keys and enter to select the month",
yearTablePrompt: "Use the arrow keys and enter to select the year",
selectedDate: "Selected date",
selectDate: "Select date",
selectTime: "Select time",
startDate: "Start Date",
startTime: "Start Time",
endDate: "End Date",
endTime: "End Time",
prevYear: "Previous Year",
nextYear: "Next Year",
prevMonth: "Previous Month",
nextMonth: "Next Month",
year: "",
month1: "January",
month2: "February",
month3: "March",
month4: "April",
month5: "May",
month6: "June",
month7: "July",
month8: "August",
month9: "September",
month10: "October",
month11: "November",
month12: "December",
week: "week",
weeks: {
sun: "Sun",
mon: "Mon",
tue: "Tue",
wed: "Wed",
thu: "Thu",
fri: "Fri",
sat: "Sat"
},
weeksFull: {
sun: "Sunday",
mon: "Monday",
tue: "Tuesday",
wed: "Wednesday",
thu: "Thursday",
fri: "Friday",
sat: "Saturday"
},
months: {
jan: "Jan",
feb: "Feb",
mar: "Mar",
apr: "Apr",
may: "May",
jun: "Jun",
jul: "Jul",
aug: "Aug",
sep: "Sep",
oct: "Oct",
nov: "Nov",
dec: "Dec"
}
},
inputNumber: {
decrease: "decrease number",
increase: "increase number"
},
select: {
loading: "Loading",
noMatch: "No matching data",
noData: "No data",
placeholder: "Select"
},
dropdown: {
toggleDropdown: "Toggle Dropdown"
},
cascader: {
noMatch: "No matching data",
loading: "Loading",
placeholder: "Select",
noData: "No data"
},
pagination: {
goto: "Go to",
pagesize: "/page",
total: "Total {total}",
pageClassifier: "",
page: "Page",
prev: "Go to previous page",
next: "Go to next page",
currentPage: "page {pager}",
prevPages: "Previous {pager} pages",
nextPages: "Next {pager} pages",
deprecationWarning: "Deprecated usages detected, please refer to the el-pagination documentation for more details"
},
dialog: {
close: "Close this dialog"
},
drawer: {
close: "Close this dialog"
},
messagebox: {
title: "Message",
confirm: "OK",
cancel: "Cancel",
error: "Illegal input",
close: "Close this dialog"
},
upload: {
deleteTip: "press delete to remove",
delete: "Delete",
preview: "Preview",
continue: "Continue"
},
slider: {
defaultLabel: "slider between {min} and {max}",
defaultRangeStartLabel: "pick start value",
defaultRangeEndLabel: "pick end value"
},
table: {
emptyText: "No Data",
confirmFilter: "Confirm",
resetFilter: "Reset",
clearFilter: "All",
sumText: "Sum"
},
tree: {
emptyText: "No Data"
},
transfer: {
noMatch: "No matching data",
noData: "No data",
titles: ["List 1", "List 2"],
filterPlaceholder: "Enter keyword",
noCheckedFormat: "{total} items",
hasCheckedFormat: "{checked}/{total} checked"
},
image: {
error: "FAILED"
},
pageHeader: {
title: "Back"
},
popconfirm: {
confirmButtonText: "Yes",
cancelButtonText: "No"
}
}
};
+130
View File
@@ -0,0 +1,130 @@
// global wrapper
/*! Element Plus v2.3.14 */
window.zhCn = {
name: "zh-cn",
el: {
colorpicker: {
confirm: "\u786E\u5B9A",
clear: "\u6E05\u7A7A"
},
datepicker: {
now: "\u6B64\u523B",
today: "\u4ECA\u5929",
cancel: "\u53D6\u6D88",
clear: "\u6E05\u7A7A",
confirm: "\u786E\u5B9A",
selectDate: "\u9009\u62E9\u65E5\u671F",
selectTime: "\u9009\u62E9\u65F6\u95F4",
startDate: "\u5F00\u59CB\u65E5\u671F",
startTime: "\u5F00\u59CB\u65F6\u95F4",
endDate: "\u7ED3\u675F\u65E5\u671F",
endTime: "\u7ED3\u675F\u65F6\u95F4",
prevYear: "\u524D\u4E00\u5E74",
nextYear: "\u540E\u4E00\u5E74",
prevMonth: "\u4E0A\u4E2A\u6708",
nextMonth: "\u4E0B\u4E2A\u6708",
year: "\u5E74",
month1: "1 \u6708",
month2: "2 \u6708",
month3: "3 \u6708",
month4: "4 \u6708",
month5: "5 \u6708",
month6: "6 \u6708",
month7: "7 \u6708",
month8: "8 \u6708",
month9: "9 \u6708",
month10: "10 \u6708",
month11: "11 \u6708",
month12: "12 \u6708",
weeks: {
sun: "\u65E5",
mon: "\u4E00",
tue: "\u4E8C",
wed: "\u4E09",
thu: "\u56DB",
fri: "\u4E94",
sat: "\u516D"
},
months: {
jan: "\u4E00\u6708",
feb: "\u4E8C\u6708",
mar: "\u4E09\u6708",
apr: "\u56DB\u6708",
may: "\u4E94\u6708",
jun: "\u516D\u6708",
jul: "\u4E03\u6708",
aug: "\u516B\u6708",
sep: "\u4E5D\u6708",
oct: "\u5341\u6708",
nov: "\u5341\u4E00\u6708",
dec: "\u5341\u4E8C\u6708"
}
},
select: {
loading: "\u52A0\u8F7D\u4E2D",
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
noData: "\u65E0\u6570\u636E",
placeholder: "\u8BF7\u9009\u62E9"
},
cascader: {
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
loading: "\u52A0\u8F7D\u4E2D",
placeholder: "\u8BF7\u9009\u62E9",
noData: "\u6682\u65E0\u6570\u636E"
},
pagination: {
goto: "\u524D\u5F80",
pagesize: "\u6761/\u9875",
total: "\u5171 {total} \u6761",
pageClassifier: "\u9875",
page: "\u9875",
prev: "\u4E0A\u4E00\u9875",
next: "\u4E0B\u4E00\u9875",
currentPage: "\u7B2C {pager} \u9875",
prevPages: "\u5411\u524D {pager} \u9875",
nextPages: "\u5411\u540E {pager} \u9875",
deprecationWarning: "\u4F60\u4F7F\u7528\u4E86\u4E00\u4E9B\u5DF2\u88AB\u5E9F\u5F03\u7684\u7528\u6CD5\uFF0C\u8BF7\u53C2\u8003 el-pagination \u7684\u5B98\u65B9\u6587\u6863"
},
messagebox: {
title: "\u63D0\u793A",
confirm: "\u786E\u5B9A",
cancel: "\u53D6\u6D88",
error: "\u8F93\u5165\u7684\u6570\u636E\u4E0D\u5408\u6CD5!"
},
upload: {
deleteTip: "\u6309 delete \u952E\u53EF\u5220\u9664",
delete: "\u5220\u9664",
preview: "\u67E5\u770B\u56FE\u7247",
continue: "\u7EE7\u7EED\u4E0A\u4F20"
},
table: {
emptyText: "\u6682\u65E0\u6570\u636E",
confirmFilter: "\u7B5B\u9009",
resetFilter: "\u91CD\u7F6E",
clearFilter: "\u5168\u90E8",
sumText: "\u5408\u8BA1"
},
tree: {
emptyText: "\u6682\u65E0\u6570\u636E"
},
transfer: {
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
noData: "\u65E0\u6570\u636E",
titles: ["\u5217\u8868 1", "\u5217\u8868 2"],
filterPlaceholder: "\u8BF7\u8F93\u5165\u641C\u7D22\u5185\u5BB9",
noCheckedFormat: "\u5171 {total} \u9879",
hasCheckedFormat: "\u5DF2\u9009 {checked}/{total} \u9879"
},
image: {
error: "\u52A0\u8F7D\u5931\u8D25"
},
pageHeader: {
title: "\u8FD4\u56DE"
},
popconfirm: {
confirmButtonText: "\u786E\u5B9A",
cancelButtonText: "\u53D6\u6D88"
}
}
};
+130
View File
@@ -0,0 +1,130 @@
/*! Element Plus v2.3.14 */
var zhCn = {
name: "zh-cn",
el: {
colorpicker: {
confirm: "\u786E\u5B9A",
clear: "\u6E05\u7A7A"
},
datepicker: {
now: "\u6B64\u523B",
today: "\u4ECA\u5929",
cancel: "\u53D6\u6D88",
clear: "\u6E05\u7A7A",
confirm: "\u786E\u5B9A",
selectDate: "\u9009\u62E9\u65E5\u671F",
selectTime: "\u9009\u62E9\u65F6\u95F4",
startDate: "\u5F00\u59CB\u65E5\u671F",
startTime: "\u5F00\u59CB\u65F6\u95F4",
endDate: "\u7ED3\u675F\u65E5\u671F",
endTime: "\u7ED3\u675F\u65F6\u95F4",
prevYear: "\u524D\u4E00\u5E74",
nextYear: "\u540E\u4E00\u5E74",
prevMonth: "\u4E0A\u4E2A\u6708",
nextMonth: "\u4E0B\u4E2A\u6708",
year: "\u5E74",
month1: "1 \u6708",
month2: "2 \u6708",
month3: "3 \u6708",
month4: "4 \u6708",
month5: "5 \u6708",
month6: "6 \u6708",
month7: "7 \u6708",
month8: "8 \u6708",
month9: "9 \u6708",
month10: "10 \u6708",
month11: "11 \u6708",
month12: "12 \u6708",
weeks: {
sun: "\u65E5",
mon: "\u4E00",
tue: "\u4E8C",
wed: "\u4E09",
thu: "\u56DB",
fri: "\u4E94",
sat: "\u516D"
},
months: {
jan: "\u4E00\u6708",
feb: "\u4E8C\u6708",
mar: "\u4E09\u6708",
apr: "\u56DB\u6708",
may: "\u4E94\u6708",
jun: "\u516D\u6708",
jul: "\u4E03\u6708",
aug: "\u516B\u6708",
sep: "\u4E5D\u6708",
oct: "\u5341\u6708",
nov: "\u5341\u4E00\u6708",
dec: "\u5341\u4E8C\u6708"
}
},
select: {
loading: "\u52A0\u8F7D\u4E2D",
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
noData: "\u65E0\u6570\u636E",
placeholder: "\u8BF7\u9009\u62E9"
},
cascader: {
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
loading: "\u52A0\u8F7D\u4E2D",
placeholder: "\u8BF7\u9009\u62E9",
noData: "\u6682\u65E0\u6570\u636E"
},
pagination: {
goto: "\u524D\u5F80",
pagesize: "\u6761/\u9875",
total: "\u5171 {total} \u6761",
pageClassifier: "\u9875",
page: "\u9875",
prev: "\u4E0A\u4E00\u9875",
next: "\u4E0B\u4E00\u9875",
currentPage: "\u7B2C {pager} \u9875",
prevPages: "\u5411\u524D {pager} \u9875",
nextPages: "\u5411\u540E {pager} \u9875",
deprecationWarning: "\u4F60\u4F7F\u7528\u4E86\u4E00\u4E9B\u5DF2\u88AB\u5E9F\u5F03\u7684\u7528\u6CD5\uFF0C\u8BF7\u53C2\u8003 el-pagination \u7684\u5B98\u65B9\u6587\u6863"
},
messagebox: {
title: "\u63D0\u793A",
confirm: "\u786E\u5B9A",
cancel: "\u53D6\u6D88",
error: "\u8F93\u5165\u7684\u6570\u636E\u4E0D\u5408\u6CD5!"
},
upload: {
deleteTip: "\u6309 delete \u952E\u53EF\u5220\u9664",
delete: "\u5220\u9664",
preview: "\u67E5\u770B\u56FE\u7247",
continue: "\u7EE7\u7EED\u4E0A\u4F20"
},
table: {
emptyText: "\u6682\u65E0\u6570\u636E",
confirmFilter: "\u7B5B\u9009",
resetFilter: "\u91CD\u7F6E",
clearFilter: "\u5168\u90E8",
sumText: "\u5408\u8BA1"
},
tree: {
emptyText: "\u6682\u65E0\u6570\u636E"
},
transfer: {
noMatch: "\u65E0\u5339\u914D\u6570\u636E",
noData: "\u65E0\u6570\u636E",
titles: ["\u5217\u8868 1", "\u5217\u8868 2"],
filterPlaceholder: "\u8BF7\u8F93\u5165\u641C\u7D22\u5185\u5BB9",
noCheckedFormat: "\u5171 {total} \u9879",
hasCheckedFormat: "\u5DF2\u9009 {checked}/{total} \u9879"
},
image: {
error: "\u52A0\u8F7D\u5931\u8D25"
},
pageHeader: {
title: "\u8FD4\u56DE"
},
popconfirm: {
confirmButtonText: "\u786E\u5B9A",
cancelButtonText: "\u53D6\u6D88"
}
}
};
+364 -364
View File
@@ -1,373 +1,373 @@
/**
* Conversation Sidebar Component
* 类似ChatGPT的会话侧边栏组件
* Conversation Sidebar component
*/
const ConversationSidebar = {
template: `
<div class="conversation-sidebar" :class="{ collapsed: isCollapsed }">
<!-- 头部:New Chat按钮 -->
<div class="sidebar-header">
<button class="new-chat-btn" @click="createNewConversation">
<el-icon><plus /></el-icon>
<span v-if="!isCollapsed">New Chat</span>
</button>
<button class="toggle-btn" @click="toggleSidebar">
<el-icon><arrow-left v-if="!isCollapsed" /><arrow-right v-else /></el-icon>
</button>
</div>
<!-- 搜索框 -->
<div class="search-box" v-if="!isCollapsed">
<el-input
v-model="searchQuery"
placeholder="搜索会话..."
prefix-icon="search"
clearable
size="small"
/>
</div>
<!-- 会话列表 -->
<div class="conversation-list" v-if="!isCollapsed">
<!-- 今日会话 -->
<div class="list-section" v-if="todayConversations.length > 0">
<div class="section-title">今天</div>
<div
v-for="conv in todayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}条消息</span>
</div>
<div class="item-preview">{{ conv.last_message }}</div>
</div>
<div class="item-actions" @click.stop>
<el-dropdown trigger="click">
<el-icon><more /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="renameConversation(conv)">
<el-icon><edit /></el-icon>重命名
</el-dropdown-item>
<el-dropdown-item @click="archiveConversation(conv)">
<el-icon><folder /></el-icon>归档
</el-dropdown-item>
<el-dropdown-item divided @click="deleteConversation(conv)" type="danger">
<el-icon><delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 昨日会话 -->
<div class="list-section" v-if="yesterdayConversations.length > 0">
<div class="section-title">昨天</div>
<div
v-for="conv in yesterdayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}条</span>
</div>
</div>
</div>
</div>
<!-- 更早会话 -->
<div class="list-section" v-if="olderConversations.length > 0">
<div class="section-title">过去7天</div>
<div
v-for="conv in olderConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredConversations.length === 0" class="empty-state">
<el-icon><chat-line-round /></el-icon>
<p>暂无会话</p>
<p class="hint">点击上方"New Chat"开始新的基因家族分析</p>
</div>
</div>
<!-- 底部:用户信息 -->
<div class="sidebar-footer" v-if="!isCollapsed">
<div class="user-info">
<el-avatar :size="32" :icon="UserFilled" />
<span class="username">{{ userId }}</span>
</div>
</div>
</div>
`,
props: {
userId: {
type: String,
default: 'xia-lab'
},
apiBaseUrl: {
type: String,
default: 'http://localhost:8000/api'
}
},
data() {
return {
isCollapsed: false,
searchQuery: '',
conversations: [],
currentConversationId: null,
loading: false,
// 图标
Plus: ElementPlusIconsVue.Plus,
ArrowLeft: ElementPlusIconsVue.ArrowLeft,
ArrowRight: ElementPlusIconsVue.ArrowRight,
ChatDotRound: ElementPlusIconsVue.ChatDotRound,
ChatLineRound: ElementPlusIconsVue.ChatLineRound,
More: ElementPlusIconsVue.More,
Edit: ElementPlusIconsVue.Edit,
Folder: ElementPlusIconsVue.Folder,
Delete: ElementPlusIconsVue.Delete,
UserFilled: ElementPlusIconsVue.UserFilled,
}
},
computed: {
filteredConversations() {
if (!this.searchQuery) return this.conversations;
const query = this.searchQuery.toLowerCase();
return this.conversations.filter(conv =>
conv.title.toLowerCase().includes(query) ||
(conv.gene_family && conv.gene_family.toLowerCase().includes(query)) ||
conv.last_message.toLowerCase().includes(query)
);
},
todayConversations() {
const today = new Date().toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === today;
});
},
yesterdayConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === yesterday;
});
},
olderConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate < yesterday;
});
}
},
mounted() {
this.loadConversations();
// 定时刷新
this.refreshInterval = setInterval(() => {
this.loadConversations();
}, 30000); // 30秒刷新一次
},
beforeUnmount() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
},
methods: {
async loadConversations() {
try {
this.loading = true;
const response = await fetch(`${this.apiBaseUrl}/conversations/list/${this.userId}`);
if (response.ok) {
const data = await response.json();
this.conversations = data.conversations || [];
}
} catch (error) {
console.error('Failed to load conversations:', error);
// 使用模拟数据
this.conversations = this.getMockConversations();
} finally {
this.loading = false;
}
},
getMockConversations() {
// 模拟数据,用于测试
return [
{
conversation_id: 'conv_001',
title: 'At.WRKY基因家族分析',
gene_family: 'WRKY',
species: 'At',
message_count: 12,
last_message: '系统发育树已生成完成',
updated_at: new Date().toISOString()
},
{
conversation_id: 'conv_002',
title: 'Os.TCP进化分析',
gene_family: 'TCP',
species: 'Os',
message_count: 5,
last_message: '正在构建进化树...',
updated_at: new Date(Date.now() - 3600000).toISOString()
},
{
conversation_id: 'conv_003',
title: 'HMMER参数配置咨询',
message_count: 8,
last_message: 'E-value建议设置为1e-5',
updated_at: new Date(Date.now() - 86400000).toISOString()
}
];
},
toggleSidebar() {
this.isCollapsed = !this.isCollapsed;
this.$emit('toggle', this.isCollapsed);
},
async createNewConversation() {
try {
const response = await fetch(`${this.apiBaseUrl}/conversations/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: this.userId,
title: '新会话 ' + new Date().toLocaleTimeString()
})
});
if (response.ok) {
const data = await response.json();
this.currentConversationId = data.conversation_id;
this.$emit('new-conversation', data);
await this.loadConversations();
}
} catch (error) {
console.error('Failed to create conversation:', error);
// 创建本地会话
const newConv = {
conversation_id: 'conv_' + Date.now(),
title: '新会话',
message_count: 0,
last_message: '',
updated_at: new Date().toISOString()
};
this.conversations.unshift(newConv);
this.currentConversationId = newConv.conversation_id;
this.$emit('new-conversation', newConv);
}
},
selectConversation(conv) {
this.currentConversationId = conv.conversation_id;
this.$emit('select-conversation', conv);
},
async renameConversation(conv) {
const newTitle = prompt('请输入新标题:', conv.title);
if (newTitle && newTitle !== conv.title) {
try {
await fetch(`${this.apiBaseUrl}/conversations/update-title`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id,
title: newTitle
})
});
conv.title = newTitle;
} catch (error) {
console.error('Failed to rename:', error);
conv.title = newTitle; // 本地更新
}
}
},
async archiveConversation(conv) {
try {
await fetch(`${this.apiBaseUrl}/conversations/archive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
} catch (error) {
console.error('Failed to archive:', error);
}
},
async deleteConversation(conv) {
if (confirm(`确定要删除会话 "${conv.title}" 吗?`)) {
try {
await fetch(`${this.apiBaseUrl}/conversations/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
if (this.currentConversationId === conv.conversation_id) {
this.currentConversationId = null;
this.$emit('select-conversation', null);
}
} catch (error) {
console.error('Failed to delete:', error);
}
}
}
}
template: `
<div class="conversation-sidebar" :class="{ collapsed: isCollapsed }">
<!-- Header: New Chat button -->
<div class="sidebar-header">
<button class="new-chat-btn" @click="createNewConversation">
<el-icon><plus /></el-icon>
<span v-if="!isCollapsed">New Chat</span>
</button>
<button class="toggle-btn" @click="toggleSidebar">
<el-icon><arrow-left v-if="!isCollapsed" /><arrow-right v-else /></el-icon>
</button>
</div>
<!-- Search box -->
<div class="search-box" v-if="!isCollapsed">
<el-input
v-model="searchQuery"
placeholder="Search conversations..."
prefix-icon="search"
clearable
size="small"
/>
</div>
<!-- Conversation list -->
<div class="conversation-list" v-if="!isCollapsed">
<!-- Todays conversations -->
<div class="list-section" v-if="todayConversations.length > 0">
<div class="section-title">Today</div>
<div
v-for="conv in todayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }} messages</span>
</div>
<div class="item-preview">{{ conv.last_message }}</div>
</div>
<div class="item-actions" @click.stop>
<el-dropdown trigger="click">
<el-icon><more /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="renameConversation(conv)">
<el-icon><edit /></el-icon>Rename
</el-dropdown-item>
<el-dropdown-item @click="archiveConversation(conv)">
<el-icon><folder /></el-icon>Archive
</el-dropdown-item>
<el-dropdown-item divided @click="deleteConversation(conv)" type="danger">
<el-icon><delete /></el-icon>Delete
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- Yesterdays conversations -->
<div class="list-section" v-if="yesterdayConversations.length > 0">
<div class="section-title">Yesterday</div>
<div
v-for="conv in yesterdayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}</span>
</div>
</div>
</div>
</div>
<!-- Older conversations -->
<div class="list-section" v-if="olderConversations.length > 0">
<div class="section-title">Past 7 days</div>
<div
v-for="conv in olderConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
</div>
</div>
</div>
</div>
<!-- Empty state -->
<div v-if="filteredConversations.length === 0" class="empty-state">
<el-icon><chat-line-round /></el-icon>
<p>No conversations yet</p>
<p class="hint">Click "New Chat" to start gene family analysis</p>
</div>
</div>
<!-- footer: user info -->
<div class="sidebar-footer" v-if="!isCollapsed">
<div class="user-info">
<el-avatar :size="32" :icon="UserFilled" />
<span class="username">{{ userId }}</span>
</div>
</div>
</div>
`,
props: {
userId: {
type: String,
default: 'xia-lab'
},
apiBaseUrl: {
type: String,
default: 'http://localhost:8000/api'
}
},
data() {
return {
isCollapsed: false,
searchQuery: '',
conversations: [],
currentConversationId: null,
loading: false,
// Icons
Plus: ElementPlusIconsVue.Plus,
ArrowLeft: ElementPlusIconsVue.ArrowLeft,
ArrowRight: ElementPlusIconsVue.ArrowRight,
ChatDotRound: ElementPlusIconsVue.ChatDotRound,
ChatLineRound: ElementPlusIconsVue.ChatLineRound,
More: ElementPlusIconsVue.More,
Edit: ElementPlusIconsVue.Edit,
Folder: ElementPlusIconsVue.Folder,
Delete: ElementPlusIconsVue.Delete,
UserFilled: ElementPlusIconsVue.UserFilled,
}
},
computed: {
filteredConversations() {
if (!this.searchQuery) return this.conversations;
const query = this.searchQuery.toLowerCase();
return this.conversations.filter(conv =>
conv.title.toLowerCase().includes(query) ||
(conv.gene_family && conv.gene_family.toLowerCase().includes(query)) ||
conv.last_message.toLowerCase().includes(query)
);
},
todayConversations() {
const today = new Date().toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === today;
});
},
yesterdayConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === yesterday;
});
},
olderConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate < yesterday;
});
}
},
mounted() {
this.loadConversations();
// Auto-refresh
this.refreshInterval = setInterval(() => {
this.loadConversations();
}, 30000); // 30 seconds refresh interval
},
beforeUnmount() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
},
methods: {
async loadConversations() {
try {
this.loading = true;
const response = await fetch(`${this.apiBaseUrl}/conversations/list/${this.userId}`);
if (response.ok) {
const data = await response.json();
this.conversations = data.conversations || [];
}
} catch (error) {
console.error('Failed to load conversations:', error);
// Use mock data
this.conversations = this.getMockConversations();
} finally {
this.loading = false;
}
},
getMockConversations() {
// Mock data for testing
return [
{
conversation_id: 'conv_001',
title: 'At.WRKY gene family analysis',
gene_family: 'WRKY',
species: 'At',
message_count: 12,
last_message: 'Phylogenetic tree done',
updated_at: new Date().toISOString()
},
{
conversation_id: 'conv_002',
title: 'Os.TCP evolution analysis',
gene_family: 'TCP',
species: 'Os',
message_count: 5,
last_message: 'Building tree...',
updated_at: new Date(Date.now() - 3600000).toISOString()
},
{
conversation_id: 'conv_003',
title: 'HMMER config consultation',
message_count: 8,
last_message: 'E-value 1e-5 recommended',
updated_at: new Date(Date.now() - 86400000).toISOString()
}
];
},
toggleSidebar() {
this.isCollapsed = !this.isCollapsed;
this.$emit('toggle', this.isCollapsed);
},
async createNewConversation() {
try {
const response = await fetch(`${this.apiBaseUrl}/conversations/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: this.userId,
title: 'New Chat ' + new Date().toLocaleTimeString()
})
});
if (response.ok) {
const data = await response.json();
this.currentConversationId = data.conversation_id;
this.$emit('new-conversation', data);
await this.loadConversations();
}
} catch (error) {
console.error('Failed to create conversation:', error);
// Create local conversation
const newConv = {
conversation_id: 'conv_' + Date.now(),
title: 'New Chat',
message_count: 0,
last_message: '',
updated_at: new Date().toISOString()
};
this.conversations.unshift(newConv);
this.currentConversationId = newConv.conversation_id;
this.$emit('new-conversation', newConv);
}
},
selectConversation(conv) {
this.currentConversationId = conv.conversation_id;
this.$emit('select-conversation', conv);
},
async renameConversation(conv) {
const newTitle = prompt('Enter new title:', conv.title);
if (newTitle && newTitle !== conv.title) {
try {
await fetch(`${this.apiBaseUrl}/conversations/update-title`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id,
title: newTitle
})
});
conv.title = newTitle;
} catch (error) {
console.error('Failed to rename:', error);
conv.title = newTitle; // local update
}
}
},
async archiveConversation(conv) {
try {
await fetch(`${this.apiBaseUrl}/conversations/archive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
} catch (error) {
console.error('Failed to archive:', error);
}
},
async deleteConversation(conv) {
if (confirm(`Delete conversation "${conv.title}"?`)) {
try {
await fetch(`${this.apiBaseUrl}/conversations/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
if (this.currentConversationId === conv.conversation_id) {
this.currentConversationId = null;
this.$emit('select-conversation', null);
}
} catch (error) {
console.error('Failed to delete:', error);
}
}
}
}
};
// 导出组件
// Export component
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ConversationSidebar };
module.exports = { ConversationSidebar };
}
File diff suppressed because it is too large Load Diff
+891
View File
@@ -0,0 +1,891 @@
// ===== Agent 多语言:卡片 + Agent 消息英文翻译 =====
// key 为中文原文,value 为英文翻译
// 使用 t(key) 获取翻译,tp(key, params) 处理动态占位符
window.__I18N_EN__ = {
"Agent状态": "AgentStatus",
"Self-RAG反思": "Self-RAG Reflection",
"三角形": "Triangle",
"下一步": "Next Step",
"下载": "Download",
"不受支持标记处理策略": "Unsupported Marker Handling Policy",
"中性替换率": "Neutral Substitution Rate",
"中性速率": "Neutral Rate",
"中文": "Chinese",
"丰度引擎": "Abundance Engine",
"主参考": "Primary Reference",
"主参考物种": "Primary reference species",
"主要结果": "Main Results",
"亚家族分类确认": "Subfamily Classification Confirmation",
"亚家族名称距离": "Subfamily Name Distance",
"亚家族外圈弧线": "Subfamily Outer Arc",
"亚家族确认参数": "Subfamily Confirmation Parameters",
"交集": "Intersection",
"仅圈图": "Circle Plot Only",
"仅比较物种": "Comparative Species Only",
"仅目标": "Target Only",
"仅目标物种": "Target Species Only",
"仅矩形图": "Rectangular Only",
"任务列表": "Task List",
"位置分布图": "Position Distribution Map",
"使用拟南芥作为显式兜底": "Use Arabidopsis as Explicit Fallback",
"使用推荐区间": "Use Recommended Range",
"保存方法学方案": "Save Methodology Plan",
"保留": "Retain",
"修订": "Revision",
"倒计时已暂停": "Countdown paused",
"全屏": "Full Screen",
"全屏展示": "Full Screen Display",
"全部": "All",
"全部行": "All Rows",
"关系总数": "Total Edges",
"关闭": "Close",
"内容": "Content",
"内置物种代码": "Built-in Species Code",
"分析家族": "Analysis Family",
"分组角色": "Group Role",
"分组角色不改": "Group Role (Unchanged)",
"删除": "Deleted",
"删除会话": "Delete Session",
"刷新": "Refresh",
"刷新结果": "Refresh Results",
"前景基因": "Foreground Genes",
"功能类别": "Feature Category",
"加入正式列表": "Add to Formal List",
"包含关键词": "Contains Keyword",
"匹配方式": "Match Mode",
"匹配行": "Matched Rows",
"原因": "Reason",
"原始代码": "Original Code",
"原始命令": "Original Command",
"原始文件名": "Original Filename",
"原始证据": "Original Evidence",
"原推荐参考": "Original Recommended Reference",
"参数": "Parameters",
"参考分类": "Reference Classification",
"参考前缀": "Reference Prefix",
"参考基因": "Reference Gene",
"参考基因编号": "Reference Gene ID",
"参考文献": "References",
"参考物种": "Reference Species",
"发送": "Send",
"取消": "Cancel",
"取消预览": "Cancel Preview",
"只会执行已勾选且可执行的阶段": "Only checked and executable stages will run",
"可改": "Editable",
"同时处理的样本数": "Number of Samples Processed Simultaneously",
"名称": "Name",
"后续主参考物种": "Downstream Primary Reference Species",
"启动子元件": "Promoter Elements",
"启动子自定义绘图": "Promoter Custom Plot",
"图例颜色": "Legend Color",
"图类型": "Plot Type",
"图表标题清单": "Chart Title List",
"图表标题驱动复现规划预览": "Chart Title-Driven Reproduction Plan Preview",
"圆形": "Circle",
"基因注释": "Gene Annotation",
"基本参数": "Basic Parameters",
"基础参数": "Basic Parameters",
"处理中...": "Processing...",
"处理方式": "Processing Method",
"多物种处理方式": "Multi-species Processing",
"大小": "Size",
"定量模式": "Quantification Mode",
"实验设计": "Experimental Design",
"实验设计模块": "Experimental Design Module",
"审核备注": "Review Notes",
"富集类型": "Enrichment Type",
"差异表达范围": "Differential Expression Scope",
"已写阶段": "Registered Stages",
"已完成": "Completed",
"已完成阶段": "Completed Stages",
"已应用": "Applied",
"建议参数": "Suggested Parameters",
"开始验证": "Start Validation",
"弧线避让距离": "Arc Avoidance Distance",
"当前参数": "Current Parameters",
"当前参考物种": "Current Reference Species",
"当前选择": "Current Selection",
"待补数据": "Awaiting Data",
"必填": "Required",
"我知道了": "Got It",
"手动区间": "Manual Range",
"执行": "Execute",
"扫描双链": "Scan Both Strands",
"批量执行确认": "Batch Execution Confirmation",
"拉丁名": "Latin Name",
"拓展分析运行参数确认": "Extension Analysis Run Parameter Confirmation",
"拓展分析阶段": "Extension Analysis Stage",
"拓展分析阶段状态": "Extension Analysis Stage Status",
"拓展分析阶段确认": "Extension Analysis Stage Confirmation",
"拓展分析阶段运行结果": "Extension Analysis Stage Run Result",
"按元件": "By Element",
"按单个物种分别分析与确认": "Per-Species Analysis & Confirmation",
"按当前筛选重建成员蛋白": "Rebuild Member Proteins by Current Filter",
"按类别": "By Category",
"授权发布": "Authorize Publish",
"推荐参考数据下载失败": "Recommended Ref Data Download Failed",
"提出修订意见": "Submit Revision Suggestions",
"操作": "Operation",
"数据准备": "Data Preparation",
"数据路径": "Data Path",
"数据需求": "Data Needs",
"文件": "File",
"文件类型": "File Type",
"文件范围": "File Scope",
"文献参数": "Literature Parameters",
"文献工具": "Literature Tools",
"文章涉及物种与下载入口": "Article Species & Download Entry",
"新增": "Added",
"方形": "Square",
"方法参考": "Method Reference",
"方法学文献": "Methodology Papers",
"无": "None",
"明确指定": "Explicitly Specify",
"显示": "Show",
"暂停倒计时": "Pause Countdown",
"暂无已上传文件": "No Uploaded Files Yet",
"暂无拓展分析阶段结果": "No Extension Analysis Stage Results Yet",
"暂无状态记录": "No Status Record Yet",
"最大宽度": "Max Width",
"最小宽度": "Min Width",
"最小非缺口比例": "Min Non-gap Ratio",
"最近到属": "Closest to Genus",
"最近到目": "Closest to Order",
"最近到科": "Closest to Family",
"最近到纲": "Closest to Class",
"有损转换警告": "Lossy Conversion Warning",
"服务器测试数据路径": "Server Test Data Path",
"未从图表标题推断出额外阶段": "No Additional Stages Inferred from Chart Titles",
"未提取到图表标题": "No Chart Title Extracted",
"未识别到内置下载链接": "No Built-in DL Link Identified",
"未识别到参数": "No Parameters Identified",
"未识别到文献工具": "No Literature Tool Identified",
"本次成员鉴定物种": "Current Member Identification Species",
"来源": "Sources",
"染色体标注位置": "Chromosome Annotation Position",
"染色体标注大小": "Chromosome Annotation Size",
"染色体策略": "Chromosome Strategy",
"染色体色块粗细": "Chromosome Block Width",
"查看本次知识检索过程": "View knowledge retrieval process",
"标记大小": "Mark Size",
"标记模式": "Mark Mode",
"标题推断的候选阶段": "Candidate Stages Inferred from Titles",
"树枝粗细": "Branch Width",
"树枝颜色": "Branch Color",
"样本并行数量": "Sample Parallel Count",
"检索结果": "Retrieval Results",
"检索表格内容": "Search Table Content",
"正则匹配": "Regex Match",
"此拓展阶段没有可配置参数": "No Configurable Parameters",
"测试数据": "Test Data",
"涉及阶段": "Involved Stages",
"添加行": "Add Row",
"灰色": "Gray",
"物种": "Species",
"物种代码": "Species Code",
"物种代码不改": "Species Code (Unchanged)",
"物种名": "Species Name",
"物种角色": "Species Role",
"物种角色不改": "Species Role (Unchanged)",
"环境": "Environment",
"留空则自动发现": "Leave empty for auto-discovery",
"目标前缀": "Target Prefix",
"目标物种": "Target Species",
"知识图谱": "Knowledge Graph",
"确定": "Confirm",
"确认亚家族标签": "Confirm Subfamily Labels",
"确认保存": "Confirm Save",
"确认并写入正式配置": "Confirm & Write to Official Config",
"确认并执行": "Confirm & Execute",
"确认执行": "Confirm Execution",
"确认自动注释": "Confirm Auto-Annotation",
"确认重绘": "Confirm Replot",
"种间共线性比较物种": "Interspecies Synteny Comparative Species",
"种间共线性绘图设置": "Interspecies Synteny Plot Settings",
"移除": "Remove",
"窗口长度": "Window Length",
"类型": "Types",
"精确匹配": "Exact Match",
"红色": "Red",
"终止位点": "End Site",
"终止残基": "End Residue",
"绘图模式": "Plot Mode",
"绿色": "Green",
"置信度": "Confidence",
"置信度阈值": "Confidence Threshold",
"群体遗传输入": "Population Genetic Input",
"背景来源": "Background Source",
"背景知识": "Background knowledge",
"自动": "Auto",
"自动从注释提取": "Auto-extract from Annotation",
"自动保守窗口": "Auto Conserved Window",
"自动判断": "Auto-detect",
"节点名称": "Node Name",
"节点总数": "Total Nodes",
"节点类型": "Node Types",
"菱形": "Diamond",
"蓝色": "Blue",
"蛋白": "Protein",
"蛋白文件": "Protein Files",
"表达矩阵": "Expression Matrix",
"诊断动作": "Diagnostic Actions",
"说明": "Description",
"请先试运行以记录真实输出": "Please Run a Trial First to Record Actual Output",
"请选择共线性分析类型": "Please select synteny analysis type",
"请选择模式物种": "Select Model Species",
"起始位点": "Start Site",
"起始残基": "Start Residue",
"跳数": "Hops",
"输出": "Output",
"输出前缀": "Output Prefix",
"输出标签": "Output Label",
"运行值": "Run Value",
"运行结果": "Run Result",
"进化树图修订参数": "Phylogenetic Tree Revision Parameters",
"连接关系数": "Connections",
"迭代": "Iteration",
"选择其他提取结果": "Select Other Extraction Result",
"选择工具": "Select Tool",
"选择已完成撰写的拓展阶段": "Select Completed Extension Stage",
"通常留空": "Usually leave empty",
"重写查询": "Rewrite Query",
"重新生成说明": "Regenerate Description",
"重置": "Reset",
"重置视图": "Reset View",
"链接": "Link",
"链特异性": "Library Strandedness",
"隐藏": "Hide",
"需检查": "Needs Review",
"需要补充": "Need to Supplement",
"非时间实验留空": "Leave empty for non-time experiments",
"项目": "Project",
"预览": "Preview",
"预览到表格": "Preview to Table",
"预览命令": "Preview Commands",
"黑色": "Black",
"默认值": "Default Value",
"提取方法学": "Extract",
"刷新记录": "Refresh",
"打开最新提取结果": "Open Latest Extraction",
"查看最新提取状态": "View Latest Status",
"打开所选提取结果": "Open Selected Extraction",
"查看所选提取状态": "View Selected Status",
"生成并发布": "Generate & Publish",
"运行已发布": "Run Published",
"当前会话": "Current Session",
"更多会话": "More Sessions",
"新建": "New",
"新建会话": "New Session",
"使用小Tip": "Tips",
"一、分析模块快速指令": "1. Analysis Module Quick Commands",
"选择输入框上方\"Analysis\"": "Select \"Analysis\" above input box",
"粘贴数据位置": "Paste data location",
"核对配置文件": "Review config files",
"执行数据准备": "Run data preparation",
"执行XX物种的亚家族分类": "Run subfamily classification for XX species",
"执行XX物种的基因家族分析": "Run gene family analysis for XX species",
"执行rnaseq分析": "Run RNA-seq analysis",
"二、文章复现步骤": "2. Article Reproduction Steps",
"点击\"浏览\"上传文献": "Click \"Browse\" to upload paper",
"点击提取方法学": "Click Extract Methodology",
"核对分析步骤": "Review analysis steps",
"点击保存方案": "Click Save Plan",
"上传数据": "Upload data",
"发送\"按已保存的方法学方案继续执行\"": "Send \"Continue with saved methodology plan\"",
"三、RAG快速指令": "3. RAG Quick Commands",
"选择输入框上方\"RAG\"": "Select \"RAG\" above input box",
"输入问题": "Enter question",
"点击\"知识图谱\"查看关系": "Click \"Knowledge Graph\" to view relations",
"知识图谱": "Knowledge Graph",
"重新加载": "Reload",
"重置视图": "Reset View",
"2.2 MSA Replot / 区间重绘": "2.2 MSA Replot",
"2.3 Tree Replot / 进化树重绘": "2.3 Tree Replot",
"2.5 亚家族确认": "2.5 Subfamily Confirmation",
"6.1 启动子自定义绘图": "6.1 Promoter Custom Plot",
"7️⃣ 种内Synteny": "7️⃣ Intraspecies Synteny",
"7.1 种内共线性 Circos 重绘": "7.1 Intraspecies Synteny Circos Replot",
"8️⃣ Karyotype": "8️⃣ Karyotype",
"9️⃣ 种间Synteny": "9️⃣ Interspecies Synteny",
"基因家族": "Gene Family",
"WGS2GWAS(遗传变异与群体分析)": "WGS2GWAS",
"基因家族 promoter 扫描": "Gene family promoter scanning",
"新会话": "New Session",
"新会话已创建,请选择或创建 Working Directory": "New session created. Please select or create a Working Directory",
"创建会话失败": "Failed to create session",
"会话已删除": "Session deleted",
"删除会话失败": "Failed to delete session",
"对话历史已清空": "Chat history cleared",
"错误日志已清空": "Error logs cleared",
"重启 Celery": "Restart Celery",
"重启项目": "Restart Project",
"提交配置好的项目重启脚本": "Submit configured project restart script",
"未配置 PLANTAI_RESTART_SCRIPT 或 restart_plantai_9000.sh": "PLANTAI_RESTART_SCRIPT or restart_plantai_9000.sh not configured",
"未配置项目重启脚本": "Project restart script not configured",
"这会提交项目重启脚本,当前页面可能短暂断开。": "This will submit the project restart script. The page may briefly disconnect.",
"确认重启": "Confirm Restart",
"项目重启脚本已提交": "Project restart script submitted",
"项目重启未提交": "Project restart not submitted",
"基因家族鉴定": "Gene Family Identification",
"系统发育分析": "Phylogenetic Analysis",
"Motif分析": "Motif Analysis",
"结构域分析": "Domain Analysis",
"基因结构分析": "Gene Structure Analysis",
"启动子元件预测": "Promoter Element Prediction",
"TF结合位点预测": "TF Binding Site Prediction",
"种内共线性分析": "Intraspecies Synteny Analysis",
"种间共线性分析": "Interspecies Synteny Analysis",
"核型/染色体分布分析": "Karyotype/Chromosome Distribution",
"PPI分析": "PPI Analysis",
"Ka/Ks分析": "Ka/Ks Analysis",
"蛋白理化性质与亚细胞定位": "Protein Properties & Subcellular Localization",
"表达矩阵分析": "Expression Matrix Analysis",
"RNA-seq QC": "RNA-seq QC",
"RNA-seq Align": "RNA-seq Align",
"RNA-seq Quant": "RNA-seq Quant",
"RNA-seq Sample QC": "RNA-seq Sample QC",
"GWAS VCF排序与HapMap转换": "GWAS VCF Sort & HapMap Conversion",
"GWAS TASSEL亲缘关系矩阵": "GWAS TASSEL Kinship Matrix",
"GWAS表型准备": "GWAS Phenotype Preparation",
"GWAS协变量准备": "GWAS Covariate Preparation",
"GWAS TASSEL MLM/CMLM": "GWAS TASSEL MLM/CMLM",
"GWAS MLM/CMLM绘图": "GWAS MLM/CMLM Plotting",
"GWAS EMMAX": "GWAS EMMAX",
"GWAS EMMAX绘图": "GWAS EMMAX Plotting",
"GWAS TASSEL GLM": "GWAS TASSEL GLM",
"群体PCA分析": "Population PCA Analysis",
"群体系统树分析": "Population Phylogenetic Tree",
"群体结构分析": "Population Structure Analysis",
"群体LD衰减分析": "Population LD Decay Analysis",
"SNP密度分析": "SNP Density Analysis",
"RNA-seq DE": "RNA-seq DE",
"组合图": "Combo Plot",
"参考亚家族分类": "Reference Subfamily Classification",
"亚家族标签确认": "Subfamily Label Confirmation",
"PPI/KaKs 参考数据": "PPI/KaKs Reference Data",
"PPI/KaKs 兜底确认": "PPI/KaKs Fallback Confirmation",
"亚家族确认输出": "Subfamily Confirmation Output",
"MSA区间重绘": "MSA Region Replot",
"MSA区间重绘已提交": "MSA Replot submitted",
"MSA区间重绘完成": "MSA Replot completed",
"MSA区间重绘失败": "MSA Replot failed",
"启动子自定义绘图已提交": "Promoter custom plot submitted",
"启动子自定义绘图完成": "Promoter custom plot completed",
"启动子自定义绘图失败": "Promoter custom plot failed",
"种内共线性 Circos 重绘已提交": "Intraspecies Circos replot submitted",
"种内共线性 Circos 重绘完成,已切换到新图": "Intraspecies Circos replot completed, switched to new figure",
"种内共线性 Circos 重绘失败": "Intraspecies Circos replot failed",
"亚家族确认 - Reference Classification": "Subfamily Confirmation - Reference Classification",
"这会停止当前 profile 的 Celery worker 并重新启动。若有任务正在运行,后端会默认拒绝执行。": "This will stop and restart the Celery worker for the current profile. Running tasks will be rejected by default.",
"Celery 已重启并完成队列检查": "Celery restarted and queue check complete",
"Celery 重启未完成": "Celery restart incomplete",
// ---- Card status_label (8) ----
"失败": "Failed",
"运行中": "Running",
"待确认": "Pending",
"待执行": "Pending",
"待核对": "Review",
"待授权": "Authorize",
"已取消": "Cancelled",
// ---- Card title (10) ----
"拓展分析候选核对": "Extension Candidate Review",
"拓展分析说明生成失败": "Description Generation Failed",
"正在生成拓展分析": "Generating Extension",
"运行结果核对": "Result Review",
"输入文件和参数": "Input Files & Parameters",
"正在恢复拓展分析状态": "Restoring Extension State",
// ---- Card purpose/summary (15) ----
"原始命令、测试数据和执行事实已经保留;这里只重新生成说明,不会重新运行原始命令。": "Raw command, test data and execution facts are preserved; only regenerate description without re-running.",
"可以直接重试,或取消后检查错误原因。": "Retry or cancel and check error cause.",
"这张卡用于核对系统从真实执行中锁定的命令、输入、参数和输出。": "Review system-locked command, inputs, parameters and outputs from real execution.",
"系统正在确定执行入口,并运行上传的代码和测试数据,提取不可修改的执行事实。": "Determining entry point, running uploaded code/test data, extracting immutable execution facts.",
"原始包可复现检查中…": "Checking reproducibility of original package…",
"这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。": "Run confirmed tool with same test data; generate verifiable results.",
"确认无误后点击“开始验证”。": "Click \"Start Validation\" after confirming.",
"这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。": "View actual command, output and logs; publication requires review confirmation.",
"这张卡用于确认将已经验证的工具发布为可复用的拓展分析。": "Confirm publishing validated tool as reusable extension.",
"确认后才会发布;不会自动发布。": "Publication requires explicit confirmation; not automatic.",
"这张卡用于确认本次新数据运行的输入文件和原始命令已有参数;确认后立即执行,不会再次弹出确认。": "Confirm input files and parameters for this run; executes immediately without re-confirmation.",
"这张卡展示本次新数据运行生成的文件、标准输出和日志。": "Files, stdout and logs generated by this run.",
"页面已恢复;系统继续轮询同一个操作,不会重复提交。": "Page restored; continuing to poll the same operation without re-submitting.",
"请在页面下方的主对话输入框说明需要修订的内容,然后发送。命令和真实执行事实不会被修改。": "Describe revisions in the main chat input below. Commands and execution facts are immutable.",
"已取消;没有提交后续执行。": "Cancelled; no further execution submitted.",
// ---- Card rows labels (13) ----
"实际命令": "Actual Command",
"输出文件": "Output Files",
"标准输出": "Standard Output",
"见执行日志": "See execution log",
"功能": "Function",
"输入文件": "Input Files",
"可改参数": "Configurable Parameters",
"标准输出(直接展示)": "Stdout (direct display)",
"(无文件输出)": "(No file output)",
"(无)": "(None)",
// ---- Card error/misc (25+) ----
"操作失败": "Operation failed",
"读取拓展分析说明失败": "Failed to read extension help",
"读取拓展分析状态失败": "Failed to read extension status",
"阶段:": "Phase: ",
"原因:": "Cause: ",
"无法读取操作状态": "Cannot read operation status",
"提交失败": "Submission failed",
"原始包可复现检查提交失败": "Reproducibility check submission failed",
"阶段:upload": "Phase: Upload",
"已恢复修订;请核对最新候选。": "Revision restored; please review latest candidate.",
"✓ 语义说明已重新生成;执行命令未重新运行。": "✓ Description regenerated; command not re-run.",
"✓ 执行入口已成功复现;候选信息已生成。": "✓ Entry point reproduced successfully; candidate info generated.",
"✓ 验证已完成;结果核对卡已生成。": "✓ Validation complete; result review card generated.",
"✓ 已核对运行结果。": "✓ Results reviewed.",
"✓ 已显式授权并发布。": "✓ Explicitly authorized and published.",
"✓ 已确认输入与参数;本次运行已完成。": "✓ Inputs and parameters confirmed; run completed.",
"✓ 已恢复并完成;候选核对卡已生成。": "✓ Restored and completed; candidate review card generated.",
"✓ 已恢复确认;可以开始验证。": "✓ Confirmation restored; ready to validate.",
"✓ 已恢复验证;结果核对卡已生成。": "✓ Validation restored; result review card generated.",
"✓ 已恢复结果核对;可以授权发布。": "✓ Review restored; ready to publish.",
"✓ 已恢复并发布。": "✓ Restored and published.",
"✓ 已恢复运行;本次结果卡已生成。": "✓ Run restored; result card generated.",
"✓ 已确认候选|未作修订": "✓ Candidate confirmed | no revision",
"✓ 已确认候选|已修订:": "✓ Candidate confirmed | revised: ",
"文件上传成功,已开始生成并验收": "Upload successful; generation and validation started",
"请填写名称并提供测试数据文件或服务器路径;原始命令和原始代码至少提供一项。": "Please provide name and test data files or server path; at least command or code is required.",
"请写明修订意见。": "Please specify revision feedback.",
"读取验证结果失败": "Failed to read validation results",
"读取运行说明失败": "Failed to read run help",
"请先选择实际运行 Job。": "Please select a job first.",
"请填写每个输入文件。": "Please fill in each input file.",
"已修订:": "Revised: ",
"正在恢复 ": "Restoring ",
"…": "…",
" 提交失败": " submission failed",
" 原始包可复现检查提交失败": " Reproducibility check submission failed",
"执行入口已在隔离环境运行。请核对系统记录的事实与说明;确认后才会验证。": "Entry point ran in isolated environment. Review system-recorded facts and descriptions; validation proceeds after confirmation.",
// ---- Agent message: status/confirm/cancel (10) ----
"config.xlsx 已确认,继续当前目标:": "config.xlsx confirmed; continuing current goal: ",
"✅ 已确认使用拟南芥作为 PPI/KaKs 的显式兜底参考;该选择只写入 Ortholog/PPI/KaKs 专用参数,其它常规阶段不受影响。": "✅ Arabidopsis confirmed as PPI/KaKs explicit fallback reference; affects only Ortholog/PPI/KaKs parameters.",
"✅ 已确认自动注释,正在重新提交": "✅ Auto-annotation confirmed; re-submitting",
"✅ 已选择项目参考物种": "✅ Project reference species selected",
"已停止等待,本次未继续获取回答。": "Stopped waiting; no further response.",
"抱歉,我没有理解你的意思。请尝试更详细地描述。": "Sorry, I didn't understand. Please try describing in more detail.",
"抱歉,服务暂时不可用,请稍后重试。": "Sorry, service temporarily unavailable. Please try again later.",
// ---- Agent message: pause/block/skip (13) ----
"⏸️ PPI/KaKs 前置数据检查未通过,已暂停批量执行;请补齐参考注释包或确认参考物种后重新执行。": "⏸️ PPI/KaKs prerequisite check failed; batch paused. Add reference annotation or confirm reference species.",
"⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。": "⏸️ identify completed; methodology chain requires confirmed gene family members before continuing phylogeny.",
"⏸️ 已打开 identify 成员初筛确认表;保存后会生成 curated_members,再继续亚家族证据阶段。": "⏸️ identify member confirmation table opened; save to generate curated_members, then continue subfamily evidence stages.",
"⏸️ 当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。": "⏸️ Foreground batch in progress; WGS plan saved. Re-run after current batch finishes.",
",其它阶段继续执行。": ", other stages continue.",
"⏸️ 已按选择先暂停 Ortholog/PPI/KaKs,其它阶段继续执行。": "⏸️ Paused Ortholog/PPI/KaKs as selected; other stages continue.",
"⏹️ 后端预检失败,当前批次已停止;请重新拉取规划后继续。": "⏹️ Backend pre-check failed; batch stopped. Re-pull plan to continue.",
// ---- Agent message: replan/continue (5) ----
"🔁 亚家族标签已确认,正在重新规划确认后常规基因家族分析...": "🔁 Subfamily labels confirmed; re-planning post-confirmation gene family analysis...",
"确认后常规分析没有新的可执行阶段。": "No new executable stages after confirmation.",
// ---- Agent message: submit/execute (7) ----
"⚙️ 提交阶段: ": "⚙️ Submitting stage: ",
"按 per_species 拆分 ": "Split by per_species ",
// ---- Agent message: complete/success (7) ----
"✅ PPI/KaKs 前置数据检查通过:当前批次已有 Ortholog,将先生成同源映射再继续下游分析。": "✅ PPI/KaKs check passed: Ortholog exists in current batch; generating mapping first.",
// ---- Agent message: download/background (5) ----
"⬇️ 正在提交推荐近源参考物种注释包后台下载;Ortholog/PPI/KaKs 将等待参考数据,其它阶段继续执行。": "⬇️ Submitting reference annotation download; Ortholog/PPI/KaKs wait for data, other stages continue.",
// ---- Agent message: autofill (2) ----
"已根据上传文件生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、链特异性和分组角色,确认后再写入正式 samples.yaml。": "samples.yaml draft generated. Review run, sample_id, condition, replicate, strand specificity and group roles in the dialog, then confirm to write.",
"已根据上传文件生成 config.xlsx 草稿。请在弹窗中核对物种代码、文件类型、物种角色和 Gene_Family,确认后再写入正式 config.xlsx。": "config.xlsx draft generated. Review species code, file type, species role and Gene_Family in the dialog, then confirm to write.",
// ---- Agent message: methodology (4) ----
"已保存方法学方案到当前 job。下一步请上传/填写目标物种和参考物种数据,并确认 config.xlsx 后再执行方法学流程。": "Methodology plan saved. Next: upload target/reference species data, confirm config.xlsx, then execute.",
"❌ 方法学执行规划失败:": "❌ Methodology execution planning failed: ",
// ---- Agent message: replot/revision (4 to fix) ----
"进化树图重绘已提交:": "Phylogeny tree redraw submitted: ",
"进化树图重绘失败:": "Phylogeny tree redraw failed: ",
"Combo 图修订已提交:": "Combo plot revision submitted: ",
"Combo 图修订失败:": "Combo plot revision failed: ",
// ---- Agent message: failure/interrupt (10) ----
"无法执行分析:": "Cannot execute analysis: ",
"服务暂时不可用:": "Service temporarily unavailable: ",
"⚠️ 批量执行完成:成功 ": "⚠️ Batch complete: success ",
"⚠️ 以下阶段因依赖失败被跳过: ": "⚠️ Stages skipped due to dependency failure: ",
"⚠️ 以下阶段等待上游暂停项处理后继续: ": "⚠️ Stages waiting for upstream paused items: ",
"未生成可执行阶段。未检测到当前 scope 的完成结果时,不再把空计划解释为已完成;请检查阶段名称或前置依赖。": "No executable stages generated. Empty plan is not interpreted as completed. Check stage names or dependencies.",
// ---- Agent message: extension analysis (6) ----
"拓展分析阶段:": "Extension stage: ",
"拓展分析阶段失败:": "Extension stage failed: ",
"已执行拓展分析阶段:": "Extension stage executed: ",
"拓展分析阶段执行失败:": "Extension stage execution failed: ",
"拓展分析帮助暂时不可用:": "Extension help temporarily unavailable: ",
"上传可执行代码、测试数据和原始命令,核对候选后验证并授权发布;发布后可指定输入和原命令已有参数运行。": "Upload executable code, test data and original command; review candidate, validate, then authorize publishing.",
// ---- Agent message: other (5) ----
"📌 当前已有前台批次执行中,新任务已转为后台执行:": "📌 Foreground batch running; new task moved to background: ",
"已识别到 RNA-seq/FASTQ 和参考资源": "RNA-seq/FASTQ and reference resources detected",
"已把目录中的可识别参考资源导入当前 job": "Reference resources imported into current job",
"我识别到你是在提供数据路径,但该路径下没有发现": "Data path detected but no usable files found",
"已选择:": "Selected: ",
// ---- Agent message: remaining dynamic patterns ----
"❌ 批量执行已取消": "❌ Batch execution cancelled",
"⏭️ 跳过阶段: ": "⏭️ Skipped stage: ",
"🚀 准备提交依赖驱动执行,共 ": "🚀 Preparing dependency-driven execution: ",
"🎉 批量执行完成!成功: ": "🎉 Batch complete! Success: ",
",失败 ": ", failed ",
"跳过阶段: ": "Skipped stage: ",
" 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。": " paused: prepare reference species PEP/CDS or Ortholog mapping first.",
" 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。": " paused subsequent stages depending on confirmed_subfamily.tsv; save the subfamily confirmation table and re-pull the plan.",
"参考数据已就绪": "Reference data ready",
" 已恢复为待执行,将随当前批次继续调度。": " restored as pending; will continue with current batch.",
" 开始续跑 Ortholog/PPI/KaKs。": " resuming Ortholog/PPI/KaKs.",
" 重新提交(自动注释背景)": " re-submit (auto-annotation background)",
" 已提交后端,task_id=": " submitted to backend, task_id=",
" 准备执行 ": " preparing to execute ",
" 个阶段,请确认参数": " stages; please confirm parameters",
" 个阶段...": " stages...",
" 个分析阶段,请确认参数": " analysis stages; please confirm parameters",
"亚家族确认后已规划 ": "Post-subfamily-confirmation plan: ",
" 个常规分析阶段": " regular analysis stages",
"PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ": "PPI/KaKs check passed: downloaded reference ",
",将用该参考先生成 Ortholog mapping。": ", generating Ortholog mapping first.",
"PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(": "PPI/KaKs check passed: Ortholog mapping exists (",
"),将复用该映射执行 ": "), reusing mapping for ",
"),将直接执行 ": "), executing directly ",
"),已自动插入 Ortholog,将先生成 mapping 再执行 ": "), Ortholog auto-inserted; generating mapping then executing ",
",已自动插入 Ortholog": ", Ortholog auto-inserted",
"推荐参考数据下载完成:": "Reference download complete: ",
")。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": "). Written to Ortholog/PPI/Ka/Ks specific reference species.",
" 已按用户确认使用 bcftools 完成": " completed with bcftools as confirmed by user",
" 前端连接短暂中断,正在按后端产物状态恢复": " frontend connection interrupted; recovering from backend state",
"亚家族确认已完成,但后端当前 workflowPhase 为 ": "Subfamily confirmation completed; backend workflowPhase is ",
",未自动打开常规分析。": "; no auto-analysis opened.",
"亚家族分类已完成预测,但确认表尚未保存;": "Subfamily classification predicted; confirmation table not yet saved; ",
"已调出": "Opened ",
"亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击\"确认亚家族标签\"后保存。": " subfamily classification confirmation table. Revise confirmed_subfamily in the dialog, then click \"Confirm Subfamily Labels\" to save.",
"亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。": " subfamily classification confirmation table not opened: no confirmable results found. Confirm the species has completed reference_classification.",
"确认后常规分析没有新的可执行阶段。": "No new executable stages after confirmation.",
"⚠️ 检测到 ": "⚠️ Detected ",
",将在确认后启动本地自动注释。": "; auto-annotation will start after confirmation.",
"🧬 自动背景注释与 ": "🧬 Auto-background annotation and ",
" 已提交后端,task_id=": " submitted to backend, task_id=",
"⚠️ 亚家族确认后自动续跑规划失败:": "⚠️ Post-subfamily-confirm auto-plan failed: ",
"; Ortholog/PPI/KaKs ": "; Ortholog/PPI/KaKs ",
"推荐参考数据下载已转入后台:": "Reference download moved to background: ",
",task_id=": ", task_id=",
"后台批次完成:成功 ": "Background batch complete: success ",
"后台执行阶段: ": "Background stage: ",
"后台阶段 ": "Background stage ",
" 完成": " done",
"已保存方法学方案,但暂不执行:": "Methodology plan saved, not executing: ",
"。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击\"确认并执行\"。": ". Review/fill data in the config.xlsx auto-detect dialog, confirm to write then click \"Confirm & Execute\".",
"当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。": "Foreground batch in progress; WGS plan saved. Re-run after current batch finishes.",
"拓展分析阶段执行失败:": "Extension stage execution failed: ",
"已把路径中的 ": "Soft-linked ",
" 个可识别文件软链接到当前 job": " recognizable files to current job",
" 个可识别文件软链接到当前 job,但没有发现需要自动填写 config.xlsx 或 samples.yaml 的文件类型;请检查目录或继续补充文件。": " recognizable files linked to current job; no files found for config.xlsx/samples.yaml auto-fill. Check directory or add more files.",
"前": " ago",
"秒前": "sec ago",
"分钟前": "min ago",
"小时前": "hr ago",
"天前": "d ago",
"待机": "Idle",
"分析中": "Analyzing",
"检索中": "Retrieving",
"反思中": "Reflecting",
"生成中": "Generating",
"完成": "Complete",
"错误": "Error",
"处理中...": "Processing...",
"发送": "Send",
"任务列表": "Tasks",
" 个常规分析阶段,请确认参数": " regular analysis stages; please confirm parameters",
" 亚家族分类已完成预测,但确认表尚未保存;后续会继续显示为待确认。": " subfamily classification predicted; confirmation table not yet saved; will show as pending.",
" 到后端;系统会先生成 ": " to backend; system will generate ",
" 前端连接短暂中断,正在按后端产物状态恢复,不会立即判定阶段失败": " frontend connection interrupted; recovering from backend artifact state. Stage will not be marked as failed.",
" 已有同一分析范围的有效结果,无需重复执行。": " already has valid results for the same analysis scope. No need to re-execute.",
" 的依赖尚未完成": " dependency not yet completed",
" 重新提交(自动注释背景)...": " re-submitting (auto-annotation background)...",
" 提交失败": " submission failed",
" 原始包可复现检查提交失败": " reproducibility check submission failed",
"Combo 图修订已提交:task_id=": "Combo plot revision submitted: task_id=",
"⏳ 推荐参考数据下载已转入后台:": "⏳ Reference download moved to background: ",
"⏸️ 以下阶段等待上游暂停项处理后继续: ": "⏸️ Stages waiting for upstream paused items: ",
"⚙️ 亚家族确认后已规划 ": "⚙️ Post-subfamily-confirm plan: ",
"⚙️ 准备执行 ": "⚙️ Preparing to execute ",
"⚙️ 后台执行阶段: ": "⚙️ Background stage: ",
"⚠️ 后台批次中以下阶段因依赖失败被跳过: ": "⚠️ Background batch stages skipped due to dependency failure: ",
"✅ PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ": "✅ PPI/KaKs check passed: downloaded reference ",
"✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(": "✅ PPI/KaKs check passed: Ortholog mapping found (",
"✅ 后台阶段 ": "✅ Background stage ",
"✅ 已确认自动注释,正在重新提交 ": "✅ Auto-annotation confirmed; re-submitting ",
"✅ 已选择项目参考物种 ": "✅ Selected project reference species: ",
"✅ 推荐参考数据下载完成:": "✅ Reference download complete: ",
"✅ 阶段 ": "✅ Stage ",
"☒ 已恢复并完成。": "☒ Restored and completed.",
"❌ 无法执行分析:": "❌ Cannot execute analysis: ",
"❌ 服务暂时不可用:": "❌ Service temporarily unavailable: ",
"。结果已写入当前 job workspace,输出:": ". Results written to current job workspace, output: ",
"。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击“确认并执行”。": ". Review/fill data in the config.xlsx auto-detect dialog; confirm to write then click \"Confirm & Execute\".",
"亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击“确认亚家族标签”后保存。": "subfamily classification confirmation table. Revise confirmed_subfamily in the dialog, then click \"Confirm Subfamily Labels\" to save.",
"亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明\"不需要亚家族\"。": "Subfamily label confirmation skipped; subsequent stages depending on confirmed_subfamily.tsv have been stopped. For regular analysis without subfamily labels, please restart and explicitly state \"no subfamily\".",
"发送\"按已保存的方法学方案继续执行\"": "Send \"Continue with saved methodology plan\"",
"后台批次无法继续:": "Background batch cannot continue: ",
"已打开 config.xlsx 自动识别表。你可以直接修订单行,或用批量修订区筛选、预览、Apply。": "Opened config.xlsx auto-identification table. You can edit individual rows, or use the batch revision area to filter, preview, and Apply.",
"已打开 samples.yaml 自动识别表。你可以直接修订样本、分组、重复和对照组。": "Opened samples.yaml auto-identification table. You can revise samples, groups, replicates and controls directly.",
"已把目录中的可识别参考资源导入当前 job,并生成 config.xlsx 草稿。请在弹窗中核对和修订,点击“确认并写入正式配置”后再执行正式分析。": "Imported recognizable reference resources into current job and generated config.xlsx draft. Please review and revise in the dialog, then click \"Confirm & Write to Official Config\" before executing.",
"已识别到 RNA-seq FASTQ 和参考资源,优先打开 samples.yaml 草稿供你核对样本、分组和对照;config.xlsx 草稿也已生成。": "RNA-seq FASTQ and reference resources detected; opening samples.yaml draft for sample/group/control review. config.xlsx draft also generated.",
"已识别到 RNA-seq/FASTQ 数据并生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、control_group 后再写入正式 samples.yaml。": "RNA-seq/FASTQ data detected and samples.yaml draft generated. Review run, sample_id, condition, replicate, control_group in the dialog, then confirm to write.",
"已重新识别并生成 config.xlsx 草稿。请在弹窗中核对并修订,确认后才会写入正式 config.xlsx。": "Re-identified and generated config.xlsx draft. Review and revise in the dialog; writes to official config.xlsx only after confirmation.",
"已重新识别并生成 samples.yaml 草稿。请确认 run、sample_id、condition、replicate 和 control_group 后再写入正式 samples.yaml。": "Re-identified and generated samples.yaml draft. Confirm run, sample_id, condition, replicate, and control_group before writing to official samples.yaml.",
"没有找到可展示的结果。": "No displayable results found.",
"点击\"浏览\"上传文献": "Click \"Browse\" to upload paper",
"点击\"知识图谱\"查看关系": "Click \"Knowledge Graph\" to view relations",
"请先核对 config.xlsx 草稿。": "Please review the config.xlsx draft first.",
"请补充你的需求。": "Please clarify your requirements.",
"进化树图重绘已提交:task_id=": "Phylogeny tree redraw submitted: task_id=",
"选择输入框上方\"Analysis\"": "Select \"Analysis\" above the input box",
"选择输入框上方\"RAG\"": "Select \"RAG\" above the input box",
",再继续富集分析。": ", then continue enrichment analysis.",
",将用该参考先生成 Ortholog mapping。": ", will generate Ortholog mapping from this reference first.",
";Ortholog/PPI/KaKs 已恢复为待执行,将随当前批次继续调度。": "; Ortholog/PPI/KaKs restored as pending; will continue with current batch.",
",将按该参考检查或生成 Ortholog,并执行 ": "; will check or generate Ortholog from this reference, and execute ",
";开始续跑 Ortholog/PPI/KaKs。": "; resuming Ortholog/PPI/KaKs.",
"),将复用该映射执行 ": "), will reuse this mapping and execute ",
"),将直接执行 ": "), will execute directly ",
",已自动插入 Ortholog,将先生成 mapping 再执行 ": ", Ortholog auto-inserted; generating mapping first then executing ",
")。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": "). Written to Ortholog/PPI/Ka/Ks specific reference species.",
" 已按用户确认使用 bcftools 完成": " completed with bcftools as user confirmed",
" 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。": " has paused subsequent stages depending on confirmed_subfamily.tsv; save the subfamily confirmation table and re-pull the plan.",
" 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。": "paused: prepare reference species PEP/CDS or Ortholog mapping first.",
" 个分析阶段,请确认参数": " analysis stages, please confirm parameters",
"📦 后台批次完成:成功 ": "📦 Background batch complete: success ",
"🔄 阶段 ": "🔄 Stage ",
"🚀 阶段 ": "🚀 Stage ",
"config.xlsx 已确认,继续当前目标:": "config.xlsx confirmed; continuing current goal: ",
"按 per_species 拆分 ": "Split by per_species ",
"⚠️ 检测到 ": "⚠️ Detected ",
",将在确认后启动本地自动注释。": "; auto-annotation will start after confirmation.",
"。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": ". Written to Ortholog/PPI/Ka/Ks specific reference species.",
"🚀 准备提交依赖驱动执行,共 ": "🚀 Preparing dependency-driven execution: ",
" 个阶段...": " stages...",
"⚠️ 批量执行完成:成功 ": "⚠️ Batch complete: success ",
",失败 ": ", failed ",
"🎉 批量执行完成!成功: ": "🎉 Batch complete! Success: ",
"⏭️ 跳过阶段: ": "⏭️ Skipped stage: ",
"⚙️ 提交阶段: ": "⚙️ Submitting stage: ",
"⏭️ ": "⏭️ ",
" 已提交后端,task_id=": " submitted to backend, task_id=",
" 完成": " complete",
",task_id=": ", task_id=",
"⚠️ 亚家族确认后自动续跑规划失败:": "⚠️ Post-subfamily-confirm auto-plan failed: ",
"亚家族确认已完成,但后端当前 workflowPhase 为 ": "Subfamily confirmation complete; backend workflowPhase is ",
",未自动打开常规分析。": "; no auto-analysis opened.",
"已调出": "Opened: ",
"亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。": "Subfamily classification confirmation table not opened: no confirmable results found. Confirm the species has completed reference_classification.",
"拓展分析帮助暂时不可用:": "Extension help temporarily unavailable: ",
"已保存方法学方案,但暂不执行:": "Methodology plan saved; not executing: ",
"🧬 自动背景注释与 ": "🧬 Auto-background annotation and ",
"参考数据已就绪": "Reference data ready",
"Combo 图修订失败:": "Combo plot revision failed: ",
"进化树图重绘失败:": "Phylogeny tree redraw failed: ",
",其它阶段继续执行。": ", other stages continue.",
" 个阶段,请确认参数": " stages, please confirm parameters",
"—": "—",
"✓ 已恢复并完成。": "✓ Restored and completed.",
"后台批次完成:成功 ": "Background batch complete: success ",
"后台执行阶段: ": "Background stage: ",
"拓展分析阶段执行失败:": "Extension stage execution failed: ",
"拓展分析阶段:": "Extension stage: ",
"已执行拓展分析阶段:": "Extension stage executed: ",
"拓展分析阶段失败:": "Extension stage failed: ",
"MSA区间重绘已提交": "MSA replot submitted",
"MSA区间重绘失败": "MSA replot failed",
"已选择:": "Selected: ",
"📌 当前已有前台批次执行中,新任务已转为后台执行:": "📌 Foreground batch running; new task moved to background: ",
",已自动插入 Ortholog,将先生成 mapping 再执行": ", Ortholog auto-inserted; generating mapping first then executing",
"推荐参考数据下载完成:": "Reference download complete: ",
"ABORTED": "ABORTED",
"的依赖尚未完成": "dependency not yet completed",
"亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明": "Subfamily label confirmation skipped; stages depending on confirmed_subfamily.tsv stopped. For regular analysis without subfamily labels, restart and explicitly state: ",
"不需要亚家族": "no subfamily",
"。": ".",
",将复用该映射执行 ": ", will reuse this mapping and execute ",
";将按该参考检查或生成 Ortholog,并执行 ": "; will check or generate Ortholog from this reference, and execute ",
"⚠️ 以下阶段因依赖失败被跳过: ": "⚠️ Stages skipped due to dependency failure: ",
"⚠️ 以下阶段等待上游暂停项处理后继续: ": "⚠️ Stages waiting for upstream paused items: ",
"启动子自定义绘图已提交": "Promoter custom plot submitted",
"启动子自定义绘图失败": "Promoter custom plot failed",
"种内共线性 Circos 重绘已提交": "Intraspecies Circos replot submitted",
"种内共线性 Circos 重绘失败": "Intraspecies Circos replot failed",
" 重新提交(自动注释背景)": " re-submit (auto-annotation background)",
" 已暂停依赖 confirmed_subfamily.tsv": " has paused stages depending on confirmed_subfamily.tsv",
";": ";",
" 个分析阶段": " analysis stages",
" 个阶段": " stages",
// ===== 阶段 7:ElMessage/ElMessageBox i18n keys =====
// 注意:key 为中文原文(zh 模式直接返回 key),value 为 English
// tp() 类 key 需要同时注册变量 placeholder 版本和 static 文案
// ElMessageBox
"删除会话": "Delete Session",
"取消 PPI": "Cancel PPI",
"自动注释并继续": "Auto-annotate & Continue",
"我先去上传背景文件": "Upload Background Files First",
"确认设计并写入": "Confirm Design & Write",
"返回修改": "Return to Modify",
"确认第一候选": "Confirm First Candidate",
"使用 bcftools 重试": "Retry with bcftools",
"GATK 执行失败": "GATK Execution Failed",
"确认 DE 分析设计": "Confirm DE Analysis Design",
"确认 PPI 的 STRING 近源参考物种": "Confirm STRING Close Reference Species for PPI",
// autoAnnotation 标题
"GO/KEGG 富集背景缺失": "GO/KEGG Enrichment Background Missing",
"KEGG 富集背景缺失": "KEGG Enrichment Background Missing",
"GO 富集背景缺失": "GO Enrichment Background Missing",
"RNA-seq 富集背景缺失": "RNA-seq Enrichment Background Missing",
// 会话管理
"示例下载功能当前不可用": "Example download is currently unavailable",
"已Clear当前会话": "Current session cleared",
// autoFill
"请至少填写一个要预览修改的字段": "Please fill at least one field to preview modifications",
"没有匹配的草稿行": "No matching draft rows",
"请先预览要应用的修改": "Please preview modifications before applying",
"请先生成或加载草稿": "Please generate or load a draft first",
"生成草稿失败": "Failed to generate draft",
"确认失败": "Confirmation failed",
"已写入正式 config.xlsx": "Formal config.xlsx written",
"已写入正式 samples.yaml": "Formal samples.yaml written",
"请输入文件路径": "Please enter file path",
"加载失败": "Load failed",
// RAG 解读
"请求解读服务失败": "Interpretation service request failed",
"RAG解读已刷新": "RAG interpretation refreshed",
"RAG解读刷新失败": "RAG interpretation refresh failed",
"下载报告失败": "Failed to download report",
"JSON数据已加载到控制台": "JSON data loaded to console",
"解读内容已复制": "Interpretation content copied",
"复制失败": "Copy failed",
// 知识图谱
"知识图谱加载完成": "Knowledge graph loaded",
"未找到匹配的节点": "No matching node found",
"视图已重置": "View reset",
"请先通过问答加载子图,或点击「重新加载」": "Please load subgraph via Q&A first, or click Reload",
// PPI/STRING
"当前没有可用 job_id": "No job_id currently available",
"该 ID 已存在": "This ID already exists",
"至少保留一个 gene ID": "Keep at least one gene ID",
// Working Directory
"切换失败": "Switch failed",
// 图片/可视化
"请先选择一张图片": "Please select an image first",
"无法确定启动子输出前缀。": "Cannot determine promoter output prefix.",
"请至少选择一个启动子元件或类别。": "Please select at least one promoter element or category.",
"请至少选择一种图类型。": "Please select at least one plot type.",
"无法确定进化树输出标签,请先选择树图或填写输出标签。": "Cannot determine phylogeny tree output label. Please select a tree image or fill in the output label first.",
"进化树图重绘完成,已切换到新图": "Phylogeny tree replot complete; switched to new image",
"无法确定种内共线性输出标签,请先选择 Circos 图。": "Cannot determine intraspecies synteny output label. Please select a Circos image first.",
"无法确定 combo 输出标签,请先选择 combo 图。": "Cannot determine combo output label. Please select a combo image first.",
"Combo 修订至少选择两个类型。": "Combo revision requires at least two panel types.",
"Combo 图修订完成,已切换到新图": "Combo plot revision complete; switched to new image",
"无法确定MSA输出标签,请先选择对应图片或填写输出标签。": "Cannot determine MSA output label. Please select corresponding image or fill in output label.",
"请填写MSA alignment 起始列和终止列,或选择其他模式。": "Please fill in MSA alignment start/end columns or choose another mode.",
"请填写参考基因及其起止残基编号。": "Please fill in reference gene and its start/end residue numbers.",
// GWAS/诊断
"请填写 GWAS 物种简称(用于结果前缀)": "Please fill in GWAS species abbreviation (for result prefix)",
"请先选择有效 VCF": "Please select a valid VCF first",
"请先SelectAnalysis Stage": "Please select an analysis stage first",
"诊断未发现明确问题,请查看阶段错误日志。": "Diagnosis found no clear issues. Please check stage error logs.",
"诊断仅提示信息,无需确认修复。": "Diagnosis is informational only; no fix confirmation needed.",
"✅ 已应用修复方案,继续执行...": "✅ Fix applied; continuing execution...",
"❌ 应用修复失败": "❌ Fix application failed",
"❌ 网络错误,请重试": "❌ Network error; please retry",
"⏸ 已跳过修复;该问题会阻断下游,后续阶段已暂停。": "⏸ Fix skipped; issue blocks downstream; subsequent stages paused.",
"已关闭诊断提示。": "Diagnosis prompt closed.",
// 任务/消息
"当前消息未成功写入会话历史,刷新后可能丢失": "Message not persisted to session history; may be lost on refresh",
"路径已复制": "Path copied",
// 拓展分析
"当前筛选没有成员,未重建 final_members。": "No members matching current filter; final_members not rebuilt.",
"当前选择为按物种分别分析,但后端没有返回完整的分物种亚家族结果;将尝试打开可用的联合结果。": "Per-species analysis selected but backend returned incomplete results; attempting to open available joint results.",
"亚家族预测已完成,但未能自动打开确认表;请发送\"调出亚家族确认表\"手动打开。": "Subfamily prediction completed but confirmation table could not open automatically; please send \"show subfamily review table\" manually.",
"没有可确认的亚家族标签。": "No subfamily labels to confirm.",
"已请求停止当前拓展分析;状态仍会继续轮询直到进程退出。": "Stop requested for current extension; polling will continue until process exits.",
"No running task to stop": "No running task to stop",
"Task(s) stopped (backend + Celery when available)": "Task(s) stopped (backend + Celery when available)",
"File uploaded successfully": "File uploaded successfully",
"未检测到可用文件": "No usable files detected",
"请先生成拓展分析阶段。": "Please generate the extension stage first.",
"发布前验收已通过": "Pre-publish validation passed",
"请填写名称,并上传原始代码。": "Please provide name and upload source code.",
"拓展分析阶段已生成并发布": "Extension stage generated and published",
"确认已保存": "Confirmation saved",
"验证成功": "Validation successful",
"验证未通过": "Validation not passed",
"拓展分析阶段已发布": "Extension stage published",
"拓展分析阶段执行完成": "Extension stage execution complete",
"拓展分析阶段执行未成功,请查看结果日志": "Extension stage execution incomplete; please check result logs",
"该阶段已在正式列表中": "This stage is already in the formal list",
"已加入正式阶段列表,并默认纳入本次执行": "Added to formal stage list and included in this execution",
// 方法学
"请先选择 PDF 或 MinerU content_list JSON 文献文件": "Please select a PDF or MinerU content_list JSON literature file first",
"方法学提取启动失败": "Methodology extraction failed to start",
"方法学 sandbox 示例读取失败": "Failed to read methodology sandbox examples",
"请先选择或输入 methodology_dir": "Please select or enter methodology_dir first",
"方法学 sandbox 已构建": "Methodology sandbox built",
"请先构建临时库": "Please build the sandbox first",
"请输入方法学问题": "Please enter methodology question",
"方法学 sandbox 查询失败": "Methodology sandbox query failed",
"方法学方案已保存,可先准备数据,稍后再按该方案执行": "Methodology plan saved; you may prepare data first and execute later",
"方法学方案保存失败": "Failed to save methodology plan",
"当前已有批量任务在执行,请等待完成后再提交方法学流程": "A batch task is currently running; please wait for completion before submitting methodology workflow",
"请至少选择一个可执行的 bbav5 阶段": "Please select at least one executable bbav5 stage",
"亚家族参考物种未写入当前 job 配置,已生成方法学本地计划,请在批量确认页核对参考物种": "Subfamily reference species not written to current job config; local methodology plan generated. Please verify reference species on batch confirmation page.",
"方法学执行规划失败": "Methodology execution planning failed",
// Other
"请Input内容": "Please enter content",
"请先选择或创建 Working Directory": "Please select or create a Working Directory first",
"规划请求失败": "Plan request failed",
"重新绘图功能开发中...": "Replot feature is under development...",
// ===== tp() keys — 使用完整中文模板作为 key =====
"确定删除会话「{title}」吗?该操作会从列表中移除,但保留后台记录。": "Delete session \"{title}\"? This removes it from the list but keeps backend records.",
"切换到: {title}": "Switched to: {title}",
"预览 {rows} 行:{fields}。确认无误后点击 Apply。": "Preview {rows} rows: {fields}. Click Apply once confirmed.",
"已应用 {rows} 行修改。": "Applied {rows} row modifications.",
"已取消预览,表格已恢复到预览前。": "Preview cancelled, table restored.",
"链特异性为必填:还有 {count} 个样本未填写({examples})": "Library strandness required: {count} sample(s) unfilled ({examples})",
"设计提示:{error};将保留原流程的兼容处理。": "Design tip: {error}; keeping original compatible handling.",
"已加载 {count} files": "Loaded {count} files",
"加载图谱失败: {message}": "Graph load failed: {message}",
"找到节点: {name}": "Node found: {name}",
"读取表格失败: {reason}": "Table read failed: {reason}",
"PPI Reset 已完成: {reset_id}": "PPI Reset complete: {reset_id}",
"STRING 链接文件内容:{preview}": "STRING link file content: {preview}",
"读取文件失败: {message}": "File read failed: {message}",
"已创建新Working Directory: {job_id}": "New Working Directory created: {job_id}",
"创建失败: {message}": "Creation failed: {message}",
"获取Working Directory列表失败: {message}": "Failed to get Working Directory list: {message}",
"已切换到Working Directory: {job_id}": "Switched to Working Directory: {job_id}",
"切换失败: {message}": "Switch failed: {message}",
"进化树图重绘失败:{message}": "Phylogeny tree replot failed: {message}",
"Combo 图修订失败:{message}": "Combo plot revision failed: {message}",
"任务: {message}\n状态: {status}\n进度: {progress}%": "Task: {message}\nStatus: {status}\nProgress: {progress}%",
"停止拓展分析失败:{message}": "Failed to stop extension: {message}",
"已选择 {count} 个原始代码文件": "{count} code file(s) selected",
"已选择 {count} 个测试数据文件": "{count} test data file(s) selected",
"已拖入 {count} 个文件,已自动填充拓展包": "{count} file(s) dropped; extension package auto-filled",
"❌ {message}": "❌ {message}",
"Upload complete. Latest Excel is now used as config.xlsx.{warningText}": "Upload complete. Latest Excel is now used as config.xlsx.{warningText}",
"{title},需要自动注释": "{title}, Auto-annotation Required",
// String close species (tp with multi-line)
"目标物种不在 STRING 固定快照中。请确认是否采用排序第一的近源物种:": "Target species not in STRING fixed snapshot. Confirm using the top-ranked close species:",
"确认后才会下载参考 PEP、生成 Ortholog,并执行 PPI。": "Reference PEP will be downloaded, Ortholog generated, and PPI executed only after confirmation.",
"是否切换为\"自动注释 + 继续富集\"?": "Switch to \"Auto-annotation + Continue Enrichment\"?"
};