4074 lines
203 KiB
HTML
Executable File
4074 lines
203 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<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">
|
||
<link rel="stylesheet" href="conversation-sidebar.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>
|
||
<script src="conversation-sidebar.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; }
|
||
.header h1 { margin: 0; font-size: 22px; }
|
||
.main-container { display: flex; padding: 10px; gap: 10px; height: calc(100vh - 70px); overflow: hidden; box-sizing: border-box; }
|
||
|
||
/* 会话侧边栏 */
|
||
.conversation-sidebar-container {
|
||
flex-shrink: 0;
|
||
height: 100%;
|
||
}
|
||
|
||
/* 左侧:控制区 - 固定无滚动 */
|
||
.left-panel {
|
||
width: 240px;
|
||
min-width: 200px;
|
||
max-width: 280px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
height: 100%;
|
||
}
|
||
.left-panel .btn-section {
|
||
flex-shrink: 0;
|
||
max-height: fit-content;
|
||
}
|
||
.btn-section { background: white; border-radius: 6px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); flex-shrink: 0; }
|
||
.btn-section h4 { margin: 0 0 8px 0; color: #409EFF; font-size: 13px; }
|
||
|
||
/* Analysis StageSelect */
|
||
.stage-select { margin-bottom: 10px; }
|
||
|
||
/* Working Directory显示 */
|
||
.work-dir { background: #f0f0f0; border-radius: 4px; padding: 8px; font-size: 11px; color: #666; word-break: break-all; }
|
||
.work-dir-label { font-weight: bold; color: #409EFF; }
|
||
.work-dir .dir-item { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
|
||
.work-dir .dir-path { flex: 1; word-break: break-all; }
|
||
|
||
/* 中间:会话区 - 自适应可滚动 */
|
||
.middle-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||
padding: 10px;
|
||
min-width: 320px;
|
||
overflow: hidden;
|
||
height: 100%;
|
||
max-height: 100%;
|
||
}
|
||
/* 可拖拽区域样式 */
|
||
.resizable { resize: both; overflow: auto; }
|
||
.resizable-v { resize: vertical; overflow: auto; }
|
||
.resizable:hover, .resizable-v:hover { cursor: ns-resize; }
|
||
|
||
/* 执行日志区域 */
|
||
.chat-area { flex: 1; overflow-y: auto; margin-bottom: 10px; border: 1px solid #e4e7ed; border-radius: 4px; padding: 10px; min-height: 120px; resize: vertical; max-height: 400px; }
|
||
.chat-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 8px; max-width: 85%; }
|
||
.chat-msg.user { background: #ecf5ff; margin-left: auto; text-align: right; }
|
||
.chat-msg.assistant { background: #f0f9eb; }
|
||
.chat-msg .time { font-size: 11px; color: #999; margin-top: 5px; }
|
||
|
||
/* 右侧:结果展示区 - 自适应可滚动 */
|
||
.right-panel {
|
||
width: 380px;
|
||
min-width: 320px;
|
||
max-width: 420px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||
padding: 10px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
.right-panel h4 { margin: 0 0 10px 0; color: #67C23A; border-bottom: 2px solid #67C23A; padding-bottom: 8px; }
|
||
|
||
.stage-section { margin-bottom: 15px; padding: 10px; background: #f9f9f9; border-radius: 6px; }
|
||
.stage-title { font-weight: bold; color: #606266; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
|
||
.stage-title .status { width: 8px; height: 8px; border-radius: 50%; }
|
||
.status.pending { background: #909399; }
|
||
.status.running { background: #E6A23C; animation: pulse 1s infinite; }
|
||
.status.success { background: #67C23A; }
|
||
.status.failed { background: #F56C6C; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||
|
||
/* 图片网格 */
|
||
.img-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; max-height: 300px; overflow-y: auto; resize: vertical; }
|
||
.img-item { position: relative; cursor: pointer; }
|
||
.img-item img { width: 100%; height: 100px; object-fit: cover; border-radius: 4px; border: 1px solid #e4e7ed; }
|
||
.img-item:hover { opacity: 0.9; }
|
||
|
||
/* 文件列表 */
|
||
.file-list { font-size: 12px; }
|
||
.file-item { padding: 4px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
|
||
|
||
/* 参数对话框 */
|
||
.param-group { margin-bottom: 15px; }
|
||
.param-group h5 { margin: 0 0 8px 0; color: #409EFF; font-size: 13px; }
|
||
.param-row { display: flex; gap: 10px; margin-bottom: 8px; }
|
||
.param-row .el-input, .param-row .el-input-number { flex: 1; }
|
||
|
||
/* 图片预览对话框 */
|
||
.preview-dialog .el-dialog__body { padding: 10px; text-align: center; }
|
||
.preview-img { max-width: 100%; max-height: 70vh; }
|
||
.param-adjust { margin-top: 15px; padding: 15px; background: #f5f7fa; border-radius: 8px; }
|
||
|
||
/* 新版结果区域样式 */
|
||
.result-file-section { max-height: 250px; overflow-y: auto; margin-bottom: 10px; border: 1px solid #eee; border-radius: 6px; }
|
||
.result-file-section .el-collapse { border: none; }
|
||
.result-file-section .el-collapse-item__header { font-size: 13px; }
|
||
.result-file-section .el-collapse-item__content { padding: 0; }
|
||
|
||
.file-list { font-size: 12px; }
|
||
.file-item { padding: 8px 16px; cursor: pointer; display: flex; justify-content: space-between; border-bottom: 1px solid #f5f5f5; transition: background 0.2s; }
|
||
.file-item:hover { background: #f5f7fa; }
|
||
.file-item.active { background: #ecf5ff; color: #409EFF; font-weight: 500; }
|
||
|
||
.preview-section { border: 1px solid #eee; border-radius: 6px; display: flex; flex-direction: column; min-height: 200px; max-height: 400px; resize: vertical; overflow: hidden; }
|
||
.preview-section .preview-header { padding: 10px 12px; border-bottom: 1px solid #eee; display: flex; align-items: center; justify-content: space-between; background: #fafafa; }
|
||
.preview-section .preview-title { font-size: 13px; font-weight: 500; }
|
||
.preview-section .preview-tools { display: flex; gap: 4px; }
|
||
.preview-section .preview-content { flex: 1; overflow: auto; background: #f5f7fa; display: flex; align-items: center; justify-content: center; min-height: 200px; }
|
||
.preview-section .preview-footer { padding: 8px 12px; border-top: 1px solid #eee; display: flex; align-items: center; justify-content: space-between; background: #fafafa; font-size: 12px; color: #666; }
|
||
.preview-section .zoom-controls { display: flex; align-items: center; gap: 8px; }
|
||
|
||
.preview-placeholder { color: #999; text-align: center; }
|
||
.preview-placeholder .el-icon { font-size: 48px; margin-bottom: 8px; display: block; }
|
||
.preview-error { color: #F56C6C; text-align: center; }
|
||
.preview-error .el-icon { font-size: 48px; margin-bottom: 8px; display: block; }
|
||
|
||
.img-container {
|
||
transform-origin: center center;
|
||
transition: transform 0.2s ease;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.img-container img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
/* ========== Agent Mode Enhanced Styles ========== */
|
||
.agent-header {
|
||
animation: slideDown 0.3s ease;
|
||
}
|
||
|
||
/* 模式切换按钮白色文字 */
|
||
.agent-header .el-switch__label {
|
||
color: white !important;
|
||
}
|
||
.agent-header .el-switch__label.is-active {
|
||
color: white !important;
|
||
}
|
||
|
||
/* ========== Agent Status Component Styles ========== */
|
||
.agent-status-panel {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
background: #f9f9f9;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.agent-status-panel .agent-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.agent-status-panel .agent-header h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.agent-status-panel .agent-mode {
|
||
font-size: 12px;
|
||
color: #666;
|
||
background: #e0e0e0;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.agent-status-panel .agent-messages {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
max-height: 400px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.agent-status-panel .message {
|
||
margin-bottom: 12px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
background: white;
|
||
border-left: 3px solid #ddd;
|
||
}
|
||
|
||
.agent-status-panel .message.user {
|
||
border-left-color: #4CAF50;
|
||
background: #E8F5E9;
|
||
}
|
||
|
||
.agent-status-panel .message.assistant {
|
||
border-left-color: #2196F3;
|
||
background: #E3F2FD;
|
||
}
|
||
|
||
.agent-status-panel .message.error {
|
||
border-left-color: #f44336;
|
||
background: #FFEBEE;
|
||
}
|
||
|
||
.agent-status-panel .message.system {
|
||
border-left-color: #9E9E9E;
|
||
background: #F5F5F5;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.agent-status-panel .message-role {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.agent-status-panel .message-content {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.agent-status-panel .message-time {
|
||
font-size: 11px;
|
||
color: #999;
|
||
text-align: right;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.agent-status-panel .agent-input {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.agent-status-panel .agent-input textarea {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
resize: none;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.agent-status-panel .agent-input button {
|
||
padding: 8px 16px;
|
||
background: #2196F3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.agent-status-panel .agent-input button:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.agent-status-panel .agent-tasks {
|
||
border-top: 1px solid #eee;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.agent-status-panel .agent-tasks h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.agent-status-panel .task-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 8px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin-bottom: 4px;
|
||
background: white;
|
||
}
|
||
|
||
.agent-status-panel .task-item:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.agent-status-panel .task-item.pending {
|
||
border-left: 3px solid #FFC107;
|
||
}
|
||
|
||
.agent-status-panel .task-item.running {
|
||
border-left: 3px solid #2196F3;
|
||
}
|
||
|
||
.agent-status-panel .task-item.completed {
|
||
border-left: 3px solid #4CAF50;
|
||
}
|
||
|
||
.agent-status-panel .task-item.failed {
|
||
border-left: 3px solid #f44336;
|
||
}
|
||
|
||
.agent-status-panel .task-status {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.agent-status-panel .task-message {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.agent-status-panel .task-progress {
|
||
font-size: 11px;
|
||
color: #666;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from { opacity: 0; transform: translateY(-10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.agent-chat-history {
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
.intent-card {
|
||
animation: fadeIn 0.4s ease;
|
||
}
|
||
|
||
.agent-message.user {
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.agent-message.assistant {
|
||
animation: fadeIn 0.3s ease 0.1s both;
|
||
}
|
||
|
||
/* 合并后的Agent执行日志区域 */
|
||
.agent-execution-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background: linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 执行日志条目 - Zoom Out宽度 */
|
||
.execution-log-item {
|
||
padding: 6px 10px;
|
||
margin-bottom: 6px;
|
||
background: white;
|
||
border-radius: 6px;
|
||
border-left: 3px solid #409EFF;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
max-width: 50%;
|
||
}
|
||
|
||
.execution-log-item .log-time {
|
||
color: #909399;
|
||
font-size: 11px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.execution-log-item .log-content {
|
||
color: #303133;
|
||
}
|
||
|
||
/* 错误信息框 - 放在右侧底部 */
|
||
.error-log-panel {
|
||
margin-top: auto;
|
||
padding: 12px;
|
||
background: #fef0f0;
|
||
border: 1px solid #fde2e2;
|
||
border-radius: 8px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.error-log-panel pre {
|
||
margin: 0;
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
color: #f56c6c;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Vue未加载时的提示 -->
|
||
<div id="loading-msg" style="padding: 50px; text-align: center; font-size: 18px; color: #666;">
|
||
正在加载页面...
|
||
</div>
|
||
|
||
<div id="app" style="display: none;">
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<h1>🌱 BotanicalBaseAgent - 基因家族分析系统</h1>
|
||
</div>
|
||
|
||
<div class="main-container">
|
||
<!-- 左侧:控制区 -->
|
||
<div class="left-panel">
|
||
<!-- Analysis StageSelect -->
|
||
<div class="btn-section">
|
||
<h4>▶ Analysis Stage</h4>
|
||
<el-select v-model="selectedStage" placeholder="SelectAnalysis Stage" style="width:100%" @change="onStageChange">
|
||
<el-option label="===== 数据准备 =====" value="prepare_header" disabled></el-option>
|
||
<el-option label="Genome Prepare - Genome数据预处理" value="genome_prepare"></el-option>
|
||
<el-option label="===== 核心分析 =====" value="core_header" disabled></el-option>
|
||
<el-option label="Identify - Gene Family Identification" value="identify"></el-option>
|
||
<el-option label="Phylogeny - Phylogenetic Analysis" value="phylogeny"></el-option>
|
||
<el-option label="PEP - Protein Property Analysis" value="pep"></el-option>
|
||
<el-option label="Motif - Motif Analysis" value="motif"></el-option>
|
||
<el-option label="Domain - Protein Domain" value="domain"></el-option>
|
||
<el-option label="Structure - Gene Structure" value="structure"></el-option>
|
||
<el-option label="Promoter - Cis-acting Elements" value="promoter"></el-option>
|
||
<el-option label="Synteny - Synteny Analysis(Circos)" value="synteny"></el-option>
|
||
<el-option label="Karyotype - Karyotype Analysis" value="karyotype"></el-option>
|
||
<el-option label="Interspecies Synteny - 种间Synteny Analysis" value="interspecies_synteny"></el-option>
|
||
<el-option label="PPI - Protein Interaction" value="ppi"></el-option>
|
||
<el-option label="KaKs - KaKs Analysis" value="kaks"></el-option>
|
||
<el-option label="Expression (使用FPKM) - Differential Expression" value="expression"></el-option>
|
||
<el-option label="RNA-seq (Raw Data) - Complete Pipeline from FASTQ" value="rnaseq"></el-option>
|
||
<el-option label="Combo - Combined Plot(Tree+Motif+Structure+Domain)" value="combo"></el-option>
|
||
<el-option label="Run All - Complete Pipeline" value="all"></el-option>
|
||
<!-- RNA-seq拆分选项 -->
|
||
<el-option label="===== RNA-Seq 分析 =====" value="rnaseq_header" disabled></el-option>
|
||
<el-option label="RNA-Seq QC - Quality Control" value="rnaseq_qc"></el-option>
|
||
<el-option label="RNA-Seq Align - Alignment" value="rnaseq_align"></el-option>
|
||
<el-option label="RNA-Seq Quant - Quantification" value="rnaseq_quant"></el-option>
|
||
<el-option label="RNA-Seq DE - 差异表达" value="rnaseq_de"></el-option>
|
||
<el-option label="RNA-Seq Enrich - Enrichment Analysis" value="rnaseq_enrich"></el-option>
|
||
</el-select>
|
||
|
||
<el-button
|
||
type="primary"
|
||
style="width:100%; margin-top:8px;"
|
||
@click="showConfirm"
|
||
:disabled="!selectedStage || isExecuting"
|
||
:loading="isExecuting">
|
||
{{ isExecuting ? '⏳ Executing...' : '🚀 Execute Analysis' }}
|
||
</el-button>
|
||
|
||
<!-- Stop Execution按钮 -->
|
||
<el-button
|
||
v-if="isExecuting"
|
||
type="danger"
|
||
style="width:100%; margin-top:8px;"
|
||
@click="stopExecution"
|
||
:disabled="isStopping">
|
||
{{ isStopping ? '⏹ Stopping...' : '⏹ Stop Execution' }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- Naming Convention示例 -->
|
||
<div class="btn-section" style="background: #f0f9ff; border-left: 3px solid #409EFF;">
|
||
<h4 style="color: #409EFF;">📋 File Naming Convention</h4>
|
||
<div style="font-size: 11px; color: #606266; line-height: 1.8;">
|
||
<div style="margin-bottom: 6px;"><b>Genome:</b> <code style="background: #e6f7ff; padding: 2px 6px; border-radius: 3px;">At.genome.fasta</code></div>
|
||
<div style="margin-bottom: 6px;"><b>GFF:</b> <code style="background: #e6f7ff; padding: 2px 6px; border-radius: 3px;">At.gff3</code></div>
|
||
<div style="margin-bottom: 6px;"><b>CDS:</b> <code style="background: #e6f7ff; padding: 2px 6px; border-radius: 3px;">At.cds.fasta</code></div>
|
||
<div style="margin-bottom: 6px;"><b>PEP:</b> <code style="background: #e6f7ff; padding: 2px 6px; border-radius: 3px;">At.pep.fasta</code></div>
|
||
<div style="margin-bottom: 6px;"><b>HMM:</b> <code style="background: #fff7e6; padding: 2px 6px; border-radius: 3px;">WRKY.hmm</code></div>
|
||
<div><b>Subfamily:</b> <code style="background: #f6ffed; padding: 2px 6px; border-radius: 3px;">At_WRKY.subfamily</code></div>
|
||
</div>
|
||
<div style="margin-top: 8px; font-size: 10px; color: #999;">
|
||
💡 <i>At=Arabidopsis, WRKY=Gene Family</i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传区域 -->
|
||
<div class="btn-section">
|
||
<h4>📁 Upload Files <span v-if="uploadedFiles.length > 0" style="font-size: 11px; color: #909399;">({{ uploadedFiles.length }})</span></h4>
|
||
<el-upload
|
||
class="upload-btn"
|
||
action="/api/upload"
|
||
name="files"
|
||
:auto-upload="true"
|
||
:on-success="handleUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
:multiple="true"
|
||
:file-list="uploadedFiles"
|
||
show-file-list="true"
|
||
style="max-height: 105px; overflow-y: auto;">
|
||
<el-button type="info" style="width:100%">
|
||
<el-icon><upload-filled /></el-icon> Click to Upload
|
||
</el-button>
|
||
</el-upload>
|
||
|
||
<el-divider style="margin: 10px 0;"></el-divider>
|
||
|
||
<!-- InputFile Path -->
|
||
<el-input
|
||
v-model="inputPath"
|
||
placeholder="RNA-seq file path (e.g., /data/rnaseq/samples.yaml)"
|
||
@keyup.enter="loadFromPath">
|
||
<template #append>
|
||
<el-button @click="loadFromPath">Load</el-button>
|
||
</template>
|
||
</el-input>
|
||
|
||
<!-- Download Examples -->
|
||
<el-dropdown @command="handleDownloadExample" style="width:100%; margin-top:8px;">
|
||
<el-button type="warning" size="small" style="width:100%">
|
||
📥 Download Examples
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="samples">samples.yaml (RNA-seq完整配置-新格式)</el-dropdown-item>
|
||
<el-dropdown-item command="geneID">geneID (基因ID列表)</el-dropdown-item>
|
||
<el-dropdown-item command="sample">sample.txt (表达分析样本-旧格式)</el-dropdown-item>
|
||
<el-dropdown-item command="fpkm">expression.fpkm.txt (表达量矩阵)</el-dropdown-item>
|
||
<el-dropdown-item command="annotation_config">annotation_config.yaml (功能注释配置)</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
|
||
<!-- Basic Parameters -->
|
||
<div class="btn-section">
|
||
<div style="display: flex; gap: 10px; align-items: center;">
|
||
<el-input v-model="params.prefix" placeholder="Prefix (e.g., Ft.SPL)" size="small" style="flex: 1;"></el-input>
|
||
<el-input v-model="params.cpu" placeholder="CPU" size="small" style="width: 60px;"></el-input>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Working Directory -->
|
||
<div class="btn-section" style="flex: 1; min-height: 0; overflow: hidden;">
|
||
<h4>📂 Working Directory</h4>
|
||
<div class="work-dir" style="font-size: 10px;">
|
||
<div class="dir-item">
|
||
<span class="work-dir-label">Job:</span>
|
||
<span class="dir-path">{{ jobInfo.job_id }}</span>
|
||
<el-button size="small" type="text" @click="copyPath(jobInfo.job_id)">
|
||
<el-icon><document-copy /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
<div class="dir-item">
|
||
<span class="work-dir-label">Input:</span>
|
||
<span class="dir-path">{{ jobInfo.input_dir }}</span>
|
||
<el-button size="small" type="text" @click="copyPath(jobInfo.input_dir)">
|
||
<el-icon><document-copy /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
<div class="dir-item">
|
||
<span class="work-dir-label">Output:</span>
|
||
<span class="dir-path">{{ jobInfo.output_dir }}</span>
|
||
<el-button size="small" type="text" @click="copyPath(jobInfo.output_dir)">
|
||
<el-icon><document-copy /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
||
<el-button size="small" type="warning" style="flex: 1;" @click="createNewJob">
|
||
➕ New
|
||
</el-button>
|
||
<el-button size="small" type="primary" style="flex: 1;" @click="showJobSelector">
|
||
📂 Select
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间:合并后的Agent执行日志区 -->
|
||
<div class="middle-panel">
|
||
<!-- 合并后的Agent智能助手头部 -->
|
||
<div class="agent-header" style="margin-bottom: 10px; padding: 10px 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: white; flex-shrink: 0;">
|
||
<!-- 标题行 -->
|
||
<div style="font-size: 18px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center;">
|
||
<el-icon style="margin-right: 8px; font-size: 20px;"><chat-dot-round /></el-icon>
|
||
Agent Assistant / Execution Log
|
||
</div>
|
||
|
||
<!-- 模型选择和模式切换行 -->
|
||
<div style="font-size: 13px; opacity: 0.95; display: flex; gap: 15px; align-items: center; justify-content: space-between;">
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<!-- 模型Select器 -->
|
||
<el-select v-model="selectedModel" size="small" style="width: 140px;" popper-class="model-select-dropdown" @change="onModelChange">
|
||
<el-option key="kimi" label="🧠 Kimi (Default)" value="kimi"></el-option>
|
||
<el-option key="glm" label="🔬 GLM-4" value="glm"></el-option>
|
||
<el-option key="minimax" label="⚡ MiniMax" value="minimax"></el-option>
|
||
<el-option key="llama3" label="🦙 Llama3 (Local)" value="llama3"></el-option>
|
||
<el-option key="qwen2" label="🤖 Qwen2 (Local)" value="qwen2"></el-option>
|
||
<el-option key="ollama" label="📍 Ollama (Local)" value="ollama"></el-option>
|
||
</el-select>
|
||
<span v-if="agentSession.messages.length > 0">Rounds: {{ Math.floor(agentSession.messages.length / 2) }}</span>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; align-items: center;">
|
||
<!-- 偏好模式切换按钮 -->
|
||
<span style="font-size: 11px; opacity: 0.8; margin-right: 5px;">Preference:</span>
|
||
<el-switch
|
||
v-model="preferenceMode"
|
||
active-text="📚 Literature"
|
||
inactive-text="🔧 Skill"
|
||
@change="togglePreferenceMode"
|
||
style="--el-switch-on-color: #67C23A; --el-switch-off-color: #409EFF;">
|
||
</el-switch>
|
||
<el-button v-if="agentSession.messages.length > 0" size="small" type="info" plain @click="clearAgentSession">
|
||
<el-icon><delete /></el-icon> Clear
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 会话选择器 - 红框区域 -->
|
||
<div style="background: white; border: 1px solid #e4e7ed; border-radius: 8px; padding: 10px 15px; margin-bottom: 10px; display: flex; align-items: center; gap: 15px; flex-shrink: 0;">
|
||
<span style="font-size: 13px; color: #606266; white-space: nowrap;">💬 当前会话:</span>
|
||
|
||
<!-- 最近会话快捷按钮 -->
|
||
<div style="display: flex; gap: 8px; flex: 1; overflow-x: auto;">
|
||
<el-button
|
||
v-for="conv in recentConversations"
|
||
:key="conv.conversation_id"
|
||
:type="currentConversationId === conv.conversation_id ? 'primary' : 'default'"
|
||
size="small"
|
||
@click="selectConversation(conv)"
|
||
style="white-space: nowrap;">
|
||
{{ conv.title }}
|
||
<el-tag v-if="conv.gene_family" size="small" type="success" style="margin-left: 5px;">{{ conv.gene_family }}</el-tag>
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 更多会话下拉框 -->
|
||
<el-dropdown @command="selectConversation" trigger="click">
|
||
<el-button type="default" size="small">
|
||
更多会话 <el-icon><arrow-down /></el-icon>
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item
|
||
v-for="conv in moreConversations"
|
||
:key="conv.conversation_id"
|
||
:command="conv">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; width: 200px;">
|
||
<span>{{ conv.title }}</span>
|
||
<el-tag v-if="conv.gene_family" size="small" type="success">{{ conv.gene_family }}</el-tag>
|
||
</div>
|
||
</el-dropdown-item>
|
||
<el-dropdown-item divided @click="createNewConversation">
|
||
<el-icon><plus /></el-icon> 新建会话
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
|
||
<!-- 新建会话按钮 -->
|
||
<el-button type="primary" size="small" @click="createNewConversation">
|
||
<el-icon><plus /></el-icon> 新建
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 合并后的对话和执行日志区域 -->
|
||
<div class="agent-chat-history" ref="agentChatArea" style="flex: 1; overflow-y: auto; margin-bottom: 8px; border: 1px solid #e4e7ed; border-radius: 8px; padding: 10px; background: #fafbfc; min-height: 150px;">
|
||
<!-- 欢迎消息 -->
|
||
<div v-if="agentSession.messages.length === 0 && messages.length === 0" style="text-align: center; padding: 20px 15px; color: #909399;">
|
||
<el-icon style="font-size: 36px; margin-bottom: 10px;"><chat-square /></el-icon>
|
||
<div style="font-size: 14px; margin-bottom: 8px;">欢迎使用 BotanicalBaseAgent</div>
|
||
<div style="font-size: 12px; line-height: 1.5;">
|
||
基因鉴定、进化树、Motif Analysis、结构域预测<br>
|
||
<el-tag size="small" type="info" style="margin-top:8px;">💡 试试说:"帮我分析 Ft SPL 基因家族"</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Agent对话消息 -->
|
||
<div v-for="(msg, idx) in agentSession.messages" :key="'agent_'+idx" :class="['agent-message', msg.role]" style="margin-bottom: 15px; animation: fadeIn 0.3s ease;">
|
||
<!-- 用户消息 -->
|
||
<div v-if="msg.role === 'user'" style="display: flex; justify-content: flex-end;">
|
||
<div style="max-width: 80%; background: #409EFF; color: white; padding: 12px 16px; border-radius: 12px 12px 2px 12px; box-shadow: 0 2px 8px rgba(64,158,255,0.3);">
|
||
<div style="font-size: 14px; line-height: 1.5;">{{ msg.content }}</div>
|
||
<div style="font-size: 11px; opacity: 0.8; margin-top: 6px; text-align: right;">{{ msg.time }}</div>
|
||
</div>
|
||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #409EFF; margin-left: 10px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">我</div>
|
||
</div>
|
||
|
||
<!-- AI消息 -->
|
||
<div v-else style="display: flex;">
|
||
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); margin-right: 10px; display: flex; align-items: center; justify-content: center; color: white;">
|
||
<el-icon><magic-stick /></el-icon>
|
||
</div>
|
||
<div style="max-width: 80%;">
|
||
<div style="background: white; padding: 12px 16px; border-radius: 2px 12px 12px 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border: 1px solid #e4e7ed;">
|
||
<div style="font-size: 14px; line-height: 1.6; color: #303133;" v-html="formatMessage(msg.content)"></div>
|
||
<div style="font-size: 11px; color: #909399; margin-top: 8px;">{{ msg.time }}</div>
|
||
</div>
|
||
<div v-if="msg.intent" class="intent-card" style="margin-top: 8px; padding: 10px 12px; background: #f0f9eb; border-radius: 8px; border-left: 3px solid #67C23A;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
||
<el-icon style="color: #67C23A;"><check /></el-icon>
|
||
<span style="font-weight: 600; color: #67C23A; font-size: 13px;">意图识别成功</span>
|
||
<el-tag size="small" :type="getTaskTypeColor(msg.intent.taskType)" style="margin-left: auto;">{{ msg.intent.taskType || '未知任务' }}</el-tag>
|
||
</div>
|
||
<div v-if="msg.intent.stage" style="font-size: 12px; color: #606266; margin-bottom: 4px;">
|
||
<b>Analysis Stage:</b> {{ getStageLabel(msg.intent.stage) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 执行日志 - Zoom Out宽度 -->
|
||
<div v-for="(msg, idx) in messages" :key="'log_'+idx" class="execution-log-item">
|
||
<div class="log-time">{{ msg.time }}</div>
|
||
<div class="log-content" v-html="msg.content"></div>
|
||
</div>
|
||
|
||
<!-- 加载指示器 -->
|
||
<div v-if="nlLoading" style="display: flex; align-items: center; justify-content: center; padding: 20px;">
|
||
<el-icon class="is-loading" style="font-size: 24px; color: #409EFF;"><loading /></el-icon>
|
||
<span style="margin-left: 10px; color: #606266;">AI 正在思考中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- V2: RAG对话历史显示 -->
|
||
<div v-if="ragMode && ragHistory.length > 0" style="margin-top:10px; padding: 10px; background: #f5f7fa; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||
<div v-for="(msg, idx) in ragHistory.slice(-5)" :key="idx" style="margin-bottom: 8px; padding: 8px; border-radius: 4px;" :style="msg.role === 'user' ? 'background: #ecf5ff; text-align: right;' : 'background: #f0f9eb;'">
|
||
<div style="font-size: 11px; color: #999; margin-bottom: 4px;">{{ msg.time }}</div>
|
||
<div>{{ msg.content.substring(0, 100) }}{{ msg.content.length > 100 ? '...' : '' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自然语言Input框 -->
|
||
<div class="nl-input-area" style="flex-shrink: 0; padding-top: 5px;">
|
||
<el-input
|
||
v-model="nlInput"
|
||
:placeholder="ragMode ? 'Input问题,如:SPL基因家族功能' : '描述需求,如:帮我分析Ft SPL基因家族'"
|
||
@keyup.enter="handleNlInput"
|
||
:disabled="executing"
|
||
type="textarea"
|
||
:rows="2"
|
||
style="width:100%;">
|
||
</el-input>
|
||
<el-button @click="handleNlInput" :loading="nlLoading" :disabled="executing" style="margin-top: 5px; width:100%; height: 32px;" :type="ragMode ? 'success' : 'primary'">
|
||
{{ ragMode ? '📚 Ask' : '💬 Send' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:结果展示区(新版:上部分文件列表 + 下部分图片展示) -->
|
||
<div class="right-panel">
|
||
<h4 style="margin: 0 0 8px 0; font-size: 14px;">📊 Analysis Results</h4>
|
||
|
||
<!-- Running Async Task -->
|
||
<div v-if="currentTask && currentTask.stage" style="margin-bottom: 8px; padding: 8px; background: #e6f7ff; border: 1px solid #91d5ff; border-radius: 6px;">
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<div style="font-size: 16px; color: #1890ff; animation: rotating 2s linear infinite;">⏳</div>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; color: #096dd9; font-size: 12px;">
|
||
{{ currentTask.stage.toUpperCase() }} Executing
|
||
</div>
|
||
<div style="font-size: 11px; color: #666;">
|
||
⏱️ {{ executeElapsed }}s | Refresh every 5s
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 6px;">
|
||
<el-button
|
||
type="danger"
|
||
size="small"
|
||
@click="stopExecution"
|
||
:loading="isStopping"
|
||
:disabled="isStopping">
|
||
⏹ Stop
|
||
</el-button>
|
||
<el-tag type="primary" size="small">Polling</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
@keyframes rotating {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
|
||
<!-- Result Files Section -->
|
||
<div class="result-file-section" style="flex: 1; overflow-y: auto; min-height: 0;">
|
||
<el-collapse v-model="activeStages" accordion style="font-size: 12px;">
|
||
<!-- MCScanX HTML结果(独立项,可展开) -->
|
||
<el-collapse-item name="mcscanx_html" v-if="mcscanxHtmlFiles && mcscanxHtmlFiles.length > 0">
|
||
<template #title>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:10px;">
|
||
<span>
|
||
<span class="status success" style="display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;"></span>
|
||
🔗 MCScanX HTML Results
|
||
</span>
|
||
<span style="color:#999;font-size:12px;">{{ mcscanxHtmlFiles.length }} files</span>
|
||
</div>
|
||
</template>
|
||
<div class="file-list">
|
||
<div
|
||
v-for="f in mcscanxHtmlFiles"
|
||
:key="f.name"
|
||
class="file-item"
|
||
@click="openMcscanxHtml(f)">
|
||
<span>🌐 {{ f.name }}</span>
|
||
<span style="color:#999;font-size:11px;">{{ formatSize(f.size) }}</span>
|
||
</div>
|
||
</div>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item
|
||
v-for="stage in stages"
|
||
:key="stage.name"
|
||
:name="stage.name"
|
||
:title="stage.label + ' (' + (stage.files ? stage.files.length : 0) + ')'">
|
||
|
||
<template #title>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:10px;">
|
||
<span>
|
||
<span :class="'status ' + stage.status" style="display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;"></span>
|
||
{{ stage.label }}
|
||
<span v-if="stage.elapsed_time > 0" style="color:#67c23a;font-size:11px;margin-left:6px;">(Time: {{ stage.elapsed_time }}s)</span>
|
||
</span>
|
||
<span style="color:#999;font-size:12px;">{{ (stage.files ? stage.files.length : 0) + (stage.images ? stage.images.length : 0) }} files</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 图片文件列表 -->
|
||
<div v-if="stage.images && stage.images.length" class="file-list">
|
||
<div
|
||
v-for="(img, idx) in stage.images"
|
||
:key="'img_'+idx"
|
||
class="file-item"
|
||
:class="{ active: selectedFile === getFileName(img) && selectedResultStage === stage.name }"
|
||
@click="selectImageFile(stage.name, img)">
|
||
<span>🖼️ {{ getFileName(img) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 普通文件列表 -->
|
||
<div v-if="stage.files && stage.files.length" class="file-list">
|
||
<div
|
||
v-for="f in stage.files"
|
||
:key="f.name"
|
||
class="file-item"
|
||
:class="{ active: selectedFile === f.name && selectedResultStage === stage.name }"
|
||
@click="selectFile(stage.name, f)">
|
||
<span>
|
||
{{ isImageFile(f.name) ? '🖼️' : (f.name.endsWith('.ppi_url.txt') ? '🔗' : '📄') }}
|
||
{{ f.name }}
|
||
<span v-if="f.name.endsWith('.ppi_url.txt')" style="color:#409EFF;font-size:11px;">(点击打开STRING)</span>
|
||
</span>
|
||
<span style="color:#999;font-size:11px;">{{ formatSize(f.size) }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else style="color:#999;font-size:12px;padding:8px;">No files</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
|
||
<!-- Agent Tasks列表 -->
|
||
<div v-if="agentTasks.length > 0" class="agent-tasks-section" style="margin-bottom: 10px; border: 1px solid #e4e7ed; border-radius: 4px; padding: 8px; background: #f5f7fa;">
|
||
<div style="font-weight: bold; color: #409EFF; margin-bottom: 6px; font-size: 12px;">
|
||
<el-icon><list /></el-icon> Agent Tasks ({{ agentTasks.length }})
|
||
</div>
|
||
<div v-for="task in agentTasks.slice(-5)" :key="task.task_id"
|
||
style="padding: 6px; margin-bottom: 4px; border-radius: 4px; font-size: 11px; cursor: pointer;"
|
||
:style="task.status === 'completed' ? 'background: #f0f9eb; border-left: 3px solid #67C23A;' :
|
||
task.status === 'failed' ? 'background: #fef0f0; border-left: 3px solid #f56c6c;' :
|
||
task.status === 'running' ? 'background: #fdf6ec; border-left: 3px solid #e6a23c;' :
|
||
'background: white; border-left: 3px solid #909399;'"
|
||
@click="showTaskDetail(task)">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span style="font-weight: 500;">{{ task.message.substring(0, 20) }}{{ task.message.length > 20 ? '...' : '' }}</span>
|
||
<el-tag size="small" :type="task.status === 'completed' ? 'success' : task.status === 'failed' ? 'danger' : task.status === 'running' ? 'warning' : 'info'" style="font-size: 10px;">
|
||
{{ task.status === 'completed' ? '✓' : task.status === 'failed' ? '✗' : task.status === 'running' ? '⟳' : '⏸' }} {{ task.progress }}%
|
||
</el-tag>
|
||
</div>
|
||
<div style="color: #909399; font-size: 10px; margin-top: 2px;">
|
||
{{ task.current_stage || 'Pending' }} | {{ formatTime(task.start_time) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下部分:Image Preview(Zoom In版)-->
|
||
<div class="preview-section" style="flex-shrink: 0; min-height: 280px; max-height: 400px;">
|
||
<div class="preview-header" style="padding: 8px 12px; display: flex; align-items: center; gap: 10px;">
|
||
<!-- 固定名称位置,多出来隐藏 -->
|
||
<span class="preview-title" style="font-size: 13px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="selectedImage">
|
||
<span v-if="selectedImage">{{ selectedImage }}</span>
|
||
<span v-else style="color:#999;">Select Image</span>
|
||
</span>
|
||
<!-- 功能按钮 -->
|
||
<div class="preview-tools" v-if="selectedImage" style="gap: 6px; display: flex; flex-shrink: 0;">
|
||
<el-button size="small" @click="zoomIn" title="Zoom In" style="padding: 6px 12px; min-width: 36px;">➕</el-button>
|
||
<el-button size="small" @click="zoomOut" title="Zoom Out" style="padding: 6px 12px; min-width: 36px;">➖</el-button>
|
||
<el-button size="small" @click="resetZoom" title="Reset" style="padding: 6px 12px; min-width: 36px;">⟲</el-button>
|
||
<el-button size="small" @click="downloadImage" title="Download" style="padding: 6px 12px; min-width: 36px;">⬇</el-button>
|
||
<!-- 新增全屏按钮 -->
|
||
<el-button size="small" type="primary" @click="toggleFullscreen" title="Fullscreen" style="padding: 6px 12px; min-width: 60px;">
|
||
<el-icon><full-screen /></el-icon> 全屏
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<!-- Zoom In预览区域 -->
|
||
<div class="preview-content" ref="previewContentRef" style="height: 220px; min-height: 200px;">
|
||
<div v-if="selectedImage" class="img-container" :style="{ transform: 'scale(' + zoomLevel/100 + ')' }">
|
||
<img :src="selectedImageUrl" :alt="selectedImage" @load="onImageLoad" @error="onImageError" style="max-height: 200px; min-height: 180px;">
|
||
</div>
|
||
<div v-else class="preview-placeholder" style="font-size: 14px;">
|
||
<el-icon style="font-size: 48px; margin-bottom: 10px;"><picture-filled /></el-icon>
|
||
<span>Select图片查看</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
||
<el-button size="small" @click="refreshResults" style="flex: 1;">
|
||
<el-icon><refresh /></el-icon> Refresh
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 执行日志显示框 - 成功和失败都显示 -->
|
||
<div v-if="logContent" class="execution-log-panel" style="margin-top: 10px; border: 2px solid #dcdfe6; border-radius: 6px; padding: 10px; background: #f5f7fa; max-height: 200px; overflow: hidden;"
|
||
:style="logContent.includes('Error') || logContent.includes('Traceback') || logContent.includes('失败') ? 'border-color: #f56c6c;' : 'border-color: #67c23a;'">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||
<span style="font-weight: bold; font-size: 12px;" :style="logContent.includes('Error') || logContent.includes('Traceback') || logContent.includes('失败') ? 'color: #f56c6c;' : 'color: #67c23a;'">
|
||
<el-icon style="margin-right: 4px;"><warning /></el-icon>
|
||
{{ logContent.includes('Error') || logContent.includes('Traceback') || logContent.includes('失败') ? 'Execution Error' : 'Execution Log' }}
|
||
</span>
|
||
<el-button size="small" type="info" link @click="clearErrorLog" style="font-size: 11px;">Clear</el-button>
|
||
</div>
|
||
<pre style="font-size: 11px; max-height: 150px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; background: #fff; padding: 8px; border-radius: 4px; border: 1px solid #e4e7ed;">{{ logContent }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 参数确认对话框 -->
|
||
<el-dialog
|
||
v-model="confirmDialogVisible"
|
||
title="⚙️ 参数确认与调整"
|
||
width="600px"
|
||
:close-on-click-modal="false">
|
||
|
||
<div class="param-dialog">
|
||
<div class="param-group">
|
||
<h5>Basic Parameters</h5>
|
||
<div class="param-row">
|
||
<el-input v-model="confirmParams.prefix" placeholder="Prefix">
|
||
<template #prepend>Prefix</template>
|
||
</el-input>
|
||
<el-input-number v-model="confirmParams.cpu" :min="1" :max="32">
|
||
<template #prepend>CPU</template>
|
||
</el-input-number>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Genome Prepare 参数 -->
|
||
<div v-if="confirmParams.stage === 'genome_prepare'" class="param-group">
|
||
<h5>Genome Prepare 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="SelectGenome数据来源格式,auto会自动检测" placement="top">
|
||
<el-select v-model="confirmParams.genomePrepareType" placeholder="Select数据库类型" style="width:100%">
|
||
<el-option label="自动检测 (auto)" value="auto" />
|
||
<el-option label="JGI 格式" value="jgi" />
|
||
<el-option label="Ensembl 格式" value="ensembl" />
|
||
<el-option label="NCBI 格式" value="ncbi" />
|
||
</el-select>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #f6f8fa; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📦 Input文件要求(支持.gz压缩):</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
<div><b>JGI:</b> *protein_primaryTranscriptOnly.fa + *gene_exons.gff3 (+ *genome.fa)</div>
|
||
<div><b>Ensembl:</b> *.pep.all.fa + *.gff3 (+ *.cds.all.fa)</div>
|
||
<div><b>NCBI:</b> *_protein.faa + *_genomic.gff (+ *_cds_from_genomic.fna)</div>
|
||
</div>
|
||
<div style="color: #409EFF; margin-top: 8px;">
|
||
💡 <b>智能提取:</b>如果没有蛋白文件,但有 genome.fa + GFF,系统会自动用 gffread 提取蛋白序列
|
||
</div>
|
||
<div style="color: #67C23A; margin-top: 8px;">
|
||
✅ Output文件将保存到 input/{job_id}/ 目录供后续分析使用
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'identify'" class="param-group">
|
||
<h5>Identify 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="Pfam Domain 显著性阈值,越小越严格" placement="top">
|
||
<el-input-number v-model="confirmParams.idEvalue" :step="0.001" :min="0" style="width:100%">
|
||
<template #prepend>Domain E-value</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
<el-tooltip content="HMM 模型覆盖度阈值,用于过滤低覆盖度匹配" placement="top">
|
||
<el-input-number v-model="confirmParams.idCoverage" :step="0.05" :min="0" :max="1" style="width:100%">
|
||
<template #prepend>HMM Coverage</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-tooltip content="BLAST 显著性阈值,用于初步筛选候选基因" placement="top">
|
||
<el-input-number v-model="confirmParams.blastEvalue" :step="0.0001" :min="0" style="width:100%">
|
||
<template #prepend>Blast E-value</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-tooltip content="必需包含的 Pfam 蛋白家族ID,多个用逗号分隔" placement="top">
|
||
<el-input v-model="confirmParams.requiredPfams" placeholder="如 SBP,LRR" style="width:100%">
|
||
<template #prepend>Required Pfams</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-tooltip content="用于辅助鉴定的参考物种,多个用逗号分隔" placement="top">
|
||
<el-input v-model="confirmParams.refSpecies" placeholder="如 Arabidopsis_thaliana" style="width:100%">
|
||
<template #prepend>参考物种</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'phylogeny'" class="param-group">
|
||
<h5>Phylogeny 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="MAFFT 多序列Alignment参数,如 --auto --thread -1" placement="top">
|
||
<el-input v-model="confirmParams.mafftParams">
|
||
<template #prepend>MAFFT</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-tooltip content="IQ-TREE 建树参数,如 -m MFP -bb 1000" placement="top">
|
||
<el-input v-model="confirmParams.iqtreeParams">
|
||
<template #prepend>IQ-TREE</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'motif'" class="param-group">
|
||
<h5>Motif 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="MEME 挖掘的保守 Motif 数量" placement="top">
|
||
<el-input-number v-model="confirmParams.nmotifs" :min="1" :max="50" style="width:100%">
|
||
<template #prepend>Motif数量</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-tooltip content="Motif 最小氨基酸长度" placement="top">
|
||
<el-input-number v-model="confirmParams.minw" :min="3" :max="20" style="width:100%">
|
||
<template #prepend>最小宽度</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
<el-tooltip content="Motif 最大氨基酸长度" placement="top">
|
||
<el-input-number v-model="confirmParams.maxw" :min="10" :max="300" style="width:100%">
|
||
<template #prepend>最大宽度</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'domain'" class="param-group">
|
||
<h5>Domain 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="蛋白质结构域检索的 E-value 阈值" placement="top">
|
||
<el-input-number v-model="confirmParams.domEvalue" :step="0.001" :min="0" style="width:100%">
|
||
<template #prepend>E-value</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
<el-tooltip content="蛋白质结构域覆盖度阈值" placement="top">
|
||
<el-input-number v-model="confirmParams.domCoverage" :step="0.05" :min="0" :max="1" style="width:100%">
|
||
<template #prepend>Coverage</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Expression 参数 -->
|
||
<div v-if="confirmParams.stage === 'expression'" class="param-group">
|
||
<h5>Expression 参数 (FPKM/TPM矩阵)</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="从NCBI GEODownload表达量数据,留空则使用已Upload Files" placement="top">
|
||
<el-input v-model="confirmParams.geoId" placeholder="InputGEO编号 (如 GSE111937)" style="width:100%">
|
||
<template #prepend>GEO编号</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row" style="margin-top:10px">
|
||
<el-tooltip content="基因ID转换方式:去除.T01后缀或添加.T01后缀" placement="top">
|
||
<el-select v-model="confirmParams.idTransform" placeholder="SelectID转换方式" style="width:100%">
|
||
<el-option label="不转换 (none)" value="none" />
|
||
<el-option label="去除.T01后缀 (remove_T01)" value="remove_T01" />
|
||
<el-option label="添加.T01后缀 (add_T01)" value="add_T01" />
|
||
</el-select>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #f6f8fa; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📊 表达量数据来源(Select一种):</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
<div>✅ <b>方式1:</b>直接上传 - 上传表达量矩阵(*.fpkm.txt/*.tpm.txt)+基因ID+sample.txt到Input目录</div>
|
||
<div>✅ <b>方式2:</b>GEO编号 - InputGEO编号(如 GSE111937)自动Download</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq'" class="param-group">
|
||
<h5>RNA-seq 参数 (原始FASTQ数据Complete Pipeline)</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="SelectInput模式:上传模式需要手动上传参考Genome和GTF,Download模式可自动检索物种数据" placement="top">
|
||
<el-select v-model="confirmParams.rnaseqMode" placeholder="SelectInput模式" style="width:100%">
|
||
<el-option label="上传模式 (upload) - 上传参考Genome+GTF" value="upload" />
|
||
<el-option label="检索Download模式 (download) - 自动检索物种数据" value="download" />
|
||
</el-select>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row" style="margin-top:10px" v-if="confirmParams.rnaseqMode === 'download'">
|
||
<el-tooltip content="Input物种拉丁名,如 Arabidopsis thaliana" placement="top">
|
||
<el-input v-model="confirmParams.speciesName" placeholder="Input物种拉丁名 (如 Arabidopsis thaliana)" style="width:100%">
|
||
<template #prepend>物种名</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #f6f8fa; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>🔬 RNA-seq 数据来源:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
<div v-if="confirmParams.rnaseqMode === 'upload'">
|
||
✅ <b>上传模式:</b>需要上传以下文件到Input目录:<br>
|
||
• 参考Genome (*.fa/*.fasta)<br>
|
||
• 基因注释 (*.gtf/*.gff)<br>
|
||
• 样本配置文件 (samples.yaml)<br>
|
||
• 原始测序文件 (*_R1.fastq.gz, *_R2.fastq.gz)
|
||
</div>
|
||
<div v-else>
|
||
🔍 <b>检索Download模式:</b>Input物种拉丁名后,系统会自动搜索并返回参考Genome和GTF的FTPDownload链接
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq QC 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq_qc'" class="param-group">
|
||
<h5>RNA-seq QC 参数 (Quality Control - Step 1)</h5>
|
||
<div class="param-row" style="margin-bottom:10px">
|
||
<el-tooltip content="Select数据来源方式:Upload Files或指定已有数据路径" placement="top">
|
||
<el-select v-model="confirmParams.rnaseqDataMode" placeholder="Select数据来源" style="width:100%">
|
||
<el-option label="📤 Upload Files到input目录" value="upload" />
|
||
<el-option label="🔗 使用已有数据路径(自动创建软链接)" value="symlink" />
|
||
</el-select>
|
||
</el-tooltip>
|
||
</div>
|
||
<div v-if="confirmParams.rnaseqDataMode === 'symlink'" class="param-row" style="margin-bottom:10px">
|
||
<el-tooltip content="填写Raw Data所在目录的绝对路径,多个路径用逗号分隔,如 /data/SRR1,/data/SRR2" placement="top">
|
||
<el-input v-model="confirmParams.rnaseqDataPath" placeholder="填写数据目录绝对路径,多个路径用逗号分隔" style="width:100%">
|
||
<template #prepend>数据路径</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #f6f8fa; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📋 所需文件:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;" v-if="confirmParams.rnaseqDataMode === 'upload'">
|
||
• 样本配置文件 <b>samples.yaml</b><br>
|
||
• 原始测序文件 <b>*_R1.fastq.gz, *_R2.fastq.gz</b><br>
|
||
• 参考Genome <b>*.fa/*.fasta</b><br>
|
||
• 基因注释 <b>*.gtf/*.gff</b><br>
|
||
<br>
|
||
<div style="color: #E6A23C;">💡 可通过上方"Download Examples文件"获取samples.yaml模板</div>
|
||
</div>
|
||
<div style="color: #606266; line-height: 1.8;" v-else>
|
||
• 数据路径下需包含:<b>samples.yaml</b> + <b>FASTQ文件</b><br>
|
||
• input目录仍需上传:<b>参考Genome</b> + <b>基因注释</b><br>
|
||
• 支持多个路径(逗号分隔),自动合并所有FASTQ文件<br>
|
||
• 系统将自动创建FASTQ文件的软链接<br>
|
||
<br>
|
||
<div style="color: #E6A23C;">💡 适合大数据量/多组织/多时期,避免重复上传</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq Align 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq_align'" class="param-group">
|
||
<h5>RNA-seq Align 参数 (Alignment - Step 2)</h5>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用上一步Output:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 使用 QC Output的 clean reads<br>
|
||
• 使用上一步的参考Genome和GTF<br>
|
||
<br>
|
||
<b>无需再次Upload Files</b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq Quant 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq_quant'" class="param-group">
|
||
<h5>RNA-seq Quant 参数 (Quantification - Step 3)</h5>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用上一步Output:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 使用 Align Output的 BAM 文件<br>
|
||
• 使用上一步的 GTF 注释<br>
|
||
<br>
|
||
<b>无需再次Upload Files</b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq DE 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq_de'" class="param-group">
|
||
<h5>RNA-seq DE 参数 (差异表达 - Step 4)</h5>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用上一步Output:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 使用 Quant Output的表达量矩阵<br>
|
||
• 使用 samples.yaml 中的分组信息<br>
|
||
<br>
|
||
<b>无需再次Upload Files</b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq Enrich 参数 -->
|
||
<div v-if="confirmParams.stage === 'rnaseq_enrich'" class="param-group">
|
||
<h5>RNA-seq Enrich 参数 (Enrichment Analysis - Step 5)</h5>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用上一步Output:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 使用 DE Output的差异基因列表<br>
|
||
<br>
|
||
<b>无需再次Upload Files</b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'ppi'" class="param-group">
|
||
<h5>PPI 参数 (Protein Interaction)</h5>
|
||
<div class="param-row">
|
||
<el-select v-model="confirmParams.ppiSpecies" style="width:100%">
|
||
<template #prepend>物种</template>
|
||
<el-option label="拟南芥 (ath)" value="ath"></el-option>
|
||
<el-option label="水稻 (osa)" value="osa"></el-option>
|
||
<el-option label="玉米 (zma)" value="zma"></el-option>
|
||
<el-option label="大豆 (gma)" value="gma"></el-option>
|
||
<el-option label="番茄 (sly)" value="sly"></el-option>
|
||
</el-select>
|
||
</div>
|
||
<div class="param-row">
|
||
<el-input-number v-model="confirmParams.ppiScore" :min="100" :max="1000" :step="50" style="width:100%">
|
||
<template #prepend>置信度阈值</template>
|
||
</el-input-number>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用 Identify Output:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 使用基因家族 ID 列表查询 STRING 数据库<br>
|
||
• 置信度越高,互作关系越可靠<br>
|
||
<b>需要先运行 Identify 阶段</b>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #fff7e6; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📤 同源映射文件(可选):</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• 如需查询非模式植物,请上传同源映射文件到 input 目录<br>
|
||
• 文件名:<code>ortholog_pairs.txt</code> 或 <code>Ft_Ath_orthologs.txt</code><br>
|
||
• 格式:<code>FtGeneID\tAthGeneID</code>(制表符分隔)<br>
|
||
• 系统会自动搜索 workspace/kaks_analysis/ 目录下的映射文件
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="confirmParams.stage === 'kaks'" class="param-group">
|
||
<h5>Ka/Ks 参数 (KaKs Analysis)</h5>
|
||
<div class="param-row">
|
||
<el-input-number v-model="confirmParams.kaksNeutralRate" :min="1e-10" :max="1e-7" :step="1e-9" style="width:100%">
|
||
<template #prepend>中性替换率</template>
|
||
</el-input-number>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📤 需要Upload Files:</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
• <b>cds1.fasta</b> - 物种1 CDS序列 (如 Ft.cds.fasta)<br>
|
||
• <b>cds2.fasta</b> - 物种2 CDS序列 (如 Ath.cds.fasta)<br>
|
||
• <b>pep1.fasta</b> - 物种1蛋白序列 (如 Ft.pep.fasta)<br>
|
||
• <b>pep2.fasta</b> - 物种2蛋白序列 (如 Ath.pep.fasta)<br>
|
||
• <b>ortholog_pairs.txt</b> - 同源基因对<br>
|
||
<b>系统会自动搜索以下位置的文件:</b><br>
|
||
• input/ 目录<br>
|
||
• workspace/<job>/kaks_analysis/ 目录<br>
|
||
• 支持格式:FtGeneID\tAthGeneID(制表符分隔)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; padding: 10px; background: #f0f9eb; border-radius: 4px; font-size: 12px; color: #67C23A;">
|
||
💡 <b>提示:</b>以上参数可根据具体数据进行调整。如不调整,则使用默认参数。
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span style="margin-right: auto; color: #909399; font-size: 12px;">
|
||
⏱️ 已耗时: {{ executeElapsed }}s
|
||
<span v-if="autoExecute" style="color: #E6A23C; margin-left: 10px;">
|
||
⚡ {{ autoExecuteCountdown }}秒后自动执行
|
||
</span>
|
||
</span>
|
||
<el-button @click="confirmDialogVisible = false; clearAutoExecuteTimer()">❌ 取消</el-button>
|
||
<el-button type="primary" @click="doExecute" :loading="executing">✅ 确认执行</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量执行确认对话框 -->
|
||
<el-dialog
|
||
v-model="batchExecuteDialogVisible"
|
||
title="🚀 批量执行确认"
|
||
width="600px"
|
||
:close-on-click-modal="false">
|
||
|
||
<div class="batch-execute-dialog">
|
||
<div style="margin-bottom: 15px;">
|
||
<h4>📋 待执行阶段列表 ({{ batchExecuteStages.length }}个)</h4>
|
||
<div style="max-height: 300px; overflow-y: auto; background: #f5f7fa; padding: 10px; border-radius: 4px;">
|
||
<div v-for="(stage, idx) in batchExecuteStages" :key="typeof stage === 'object' ? stage.id : stage"
|
||
:style="{padding: '6px 10px', margin: '4px 0', borderRadius: '4px', background: idx < batchExecuteIndex ? '#67C23A20' : idx === batchExecuteIndex ? '#409EFF20' : '#fff', borderLeft: '3px solid ' + (idx < batchExecuteIndex ? '#67C23A' : idx === batchExecuteIndex ? '#409EFF' : '#dcdfe6')}">
|
||
<span style="color: #909399; margin-right: 8px;">{{ idx + 1 }}.</span>
|
||
<span :style="{fontWeight: idx === batchExecuteIndex ? 'bold' : 'normal', color: idx < batchExecuteIndex ? '#67C23A' : idx === batchExecuteIndex ? '#409EFF' : '#606266'}">
|
||
{{ typeof stage === 'object' ? stage.name : stage }}
|
||
</span>
|
||
<el-tag v-if="idx < batchExecuteIndex" size="small" type="success" style="margin-left: 8px;">✓</el-tag>
|
||
<el-tag v-if="idx === batchExecuteIndex && isBatchExecuting" size="small" type="primary" style="margin-left: 8px;">执行中...</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isBatchExecuting" style="padding: 10px; background: #f0f9eb; border-radius: 4px; text-align: center;">
|
||
<el-progress :percentage="Math.round((batchExecuteIndex / batchExecuteStages.length) * 100)" :status="batchExecuteIndex === batchExecuteStages.length ? 'success' : ''"></el-progress>
|
||
<div style="margin-top: 8px; color: #67C23A;">
|
||
正在执行: {{ typeof batchExecuteStages[batchExecuteIndex] === 'object' ? batchExecuteStages[batchExecuteIndex].name : batchExecuteStages[batchExecuteIndex] }} ({{ batchExecuteIndex + 1 }}/{{ batchExecuteStages.length }})
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span style="margin-right: auto; color: #909399; font-size: 12px;" v-if="!isBatchExecuting">
|
||
⏱️ {{ batchExecuteCountdown }}秒后自动开始执行
|
||
</span>
|
||
<span style="margin-right: auto; color: #67C23A; font-size: 12px;" v-else>
|
||
批量执行进行中...
|
||
</span>
|
||
<el-button v-if="!isBatchExecuting" @click="cancelBatchExecute">❌ 取消</el-button>
|
||
<el-button v-if="!isBatchExecuting" type="primary" @click="startBatchExecute" :loading="isBatchExecuting">🚀 开始执行</el-button>
|
||
<el-button v-else type="primary" disabled>执行中...</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 图片预览对话框 -->
|
||
<el-dialog
|
||
v-model="previewDialogVisible"
|
||
title="🖼️ 图片预览"
|
||
width="80%"
|
||
class="preview-dialog">
|
||
|
||
<el-image :src="previewImg" class="preview-img" :preview-src-list="[previewImg]" fit="contain"></el-image>
|
||
|
||
<div class="param-adjust" v-if="previewImg">
|
||
<h5>调整参数重新绘图</h5>
|
||
<el-row :gutter="10">
|
||
<el-col :span="12">
|
||
<el-input-number v-model="previewParams.width" :min="400" :max="2000" style="width:100%">
|
||
<template #prepend>宽度</template>
|
||
</el-input-number>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-input-number v-model="previewParams.height" :min="300" :max="1500" style="width:100%">
|
||
<template #prepend>高度</template>
|
||
</el-input-number>
|
||
</el-col>
|
||
</el-row>
|
||
<el-button type="warning" size="small" style="margin-top:10px" @click="redrawImage">
|
||
🔄 重新绘图
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 阶段确认对话框(批量执行用) -->
|
||
<el-dialog
|
||
v-model="stageConfirmDialogVisible"
|
||
:title="`⚙️ 阶段 ${stageConfirmCurrent}/${stageConfirmTotal}: ${selectedStage}`"
|
||
width="600px"
|
||
:close-on-click-modal="false"
|
||
:show-close="false">
|
||
|
||
<div class="stage-confirm-dialog">
|
||
<div style="margin-bottom: 15px; padding: 10px; background: #f0f9eb; border-radius: 4px;">
|
||
<div style="color: #67C23A; font-weight: bold;">
|
||
📋 当前阶段: {{ selectedStage }}
|
||
</div>
|
||
<div style="color: #606266; font-size: 12px; margin-top: 5px;">
|
||
进度: {{ stageConfirmCurrent }} / {{ stageConfirmTotal }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 阶段特定参数 -->
|
||
<div class="param-group">
|
||
<h5>基本参数</h5>
|
||
<div class="param-row">
|
||
<el-input v-model="confirmParams.prefix" placeholder="Prefix">
|
||
<template #prepend>Prefix</template>
|
||
</el-input>
|
||
<el-input-number v-model="confirmParams.cpu" :min="1" :max="32">
|
||
<template #prepend>CPU</template>
|
||
</el-input-number>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PPI 参数 -->
|
||
<div v-if="selectedStage === 'ppi'" class="param-group">
|
||
<h5>PPI 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="参考物种,拟南芥用ath" placement="top">
|
||
<el-input v-model="confirmParams.ppiSpecies" placeholder="ath" style="width:100%">
|
||
<template #prepend>参考物种</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
<el-tooltip content="STRING score阈值,默认700" placement="top">
|
||
<el-input-number v-model="confirmParams.ppiScore" :min="100" :max="1000" style="width:100%">
|
||
<template #prepend>Score阈值</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 8px; background: #f6f8fa; border-radius: 4px; font-size: 12px; color: #606266;">
|
||
💡 基因ID将自动从 final/{{ confirmParams.prefix }}.final_members.id 读取
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KaKs 参数 -->
|
||
<div v-if="selectedStage === 'kaks'" class="param-group">
|
||
<h5>KaKs 参数</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="中性进化速率,默认7.08e-9" placement="top">
|
||
<el-input-number v-model="confirmParams.kaksNeutralRate" :step="0.01" :min="0" style="width:100%">
|
||
<template #prepend>中性速率</template>
|
||
</el-input-number>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Expression 参数 -->
|
||
<div v-if="selectedStage === 'expression'" class="param-group">
|
||
<h5>Expression 参数 (FPKM/TPM矩阵)</h5>
|
||
<div class="param-row">
|
||
<el-tooltip content="从NCBI GEODownload表达量数据,留空则使用已Upload Files" placement="top">
|
||
<el-input v-model="confirmParams.geoId" placeholder="InputGEO编号 (如 GSE111937)" style="width:100%">
|
||
<template #prepend>GEO编号</template>
|
||
</el-input>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-row" style="margin-top:10px">
|
||
<el-tooltip content="基因ID转换方式:去除.T01后缀或添加.T01后缀" placement="top">
|
||
<el-select v-model="confirmParams.idTransform" placeholder="SelectID转换方式" style="width:100%">
|
||
<el-option label="不转换 (none)" value="none" />
|
||
<el-option label="去除.T01后缀 (remove_T01)" value="remove_T01" />
|
||
<el-option label="添加.T01后缀 (add_T01)" value="add_T01" />
|
||
</el-select>
|
||
</el-tooltip>
|
||
</div>
|
||
<div style="margin-top: 10px; padding: 10px; background: #f6f8fa; border-radius: 4px; font-size: 12px;">
|
||
<div style="color: #303133; margin-bottom: 8px;"><b>📊 表达量数据来源(Select一种):</b></div>
|
||
<div style="color: #606266; line-height: 1.8;">
|
||
<div>✅ <b>方式1:</b>直接上传 - 上传表达量矩阵(*.fpkm.txt/*.tpm.txt)+基因ID+sample.txt到Input目录</div>
|
||
<div>✅ <b>方式2:</b>GEO编号 - InputGEO编号(如 GSE111937)自动Download</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RNA-seq 参数 -->
|
||
<div v-if="selectedStage && selectedStage.startsWith('rnaseq')" class="param-group">
|
||
<h5>RNA-seq 参数</h5>
|
||
<div style="padding: 10px; background: #fdf6ec; border-radius: 4px; color: #E6A23C; font-size: 12px;">
|
||
⚠️ RNA-seq分析需要预先上传样本数据。如未上传,此阶段将跳过。
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; padding: 10px; background: #f0f9eb; border-radius: 4px; font-size: 12px; color: #67C23A;">
|
||
💡 <b>提示:</b>15秒内不操作将使用默认参数自动执行
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||
<div style="color: #E6A23C; font-size: 14px;">
|
||
⏱️ {{ stageConfirmCountdown }}秒后自动执行
|
||
</div>
|
||
<div>
|
||
<el-button type="danger" @click="stopBatchExecute" style="margin-right: 10px;">⏹️ 停止全部</el-button>
|
||
<el-button @click="skipStageExecute">⏭️ 跳过</el-button>
|
||
<el-button type="primary" @click="confirmStageExecute">✅ 确认执行</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 执行结果对话框 - 简化版 -->
|
||
<el-dialog
|
||
v-model="resultDialogVisible"
|
||
title="✅ 执行完成"
|
||
width="400px"
|
||
:show-close="false"
|
||
:center="true">
|
||
<div v-if="resultDialogParams.stage" style="text-align: center; padding: 20px 0;">
|
||
<div style="font-size: 48px; color: #67c23a; margin-bottom: 16px;">✓</div>
|
||
<div style="font-size: 18px; font-weight: bold; margin-bottom: 8px;">
|
||
{{ resultDialogParams.stage }} 执行完成
|
||
</div>
|
||
<div style="font-size: 14px; color: #909399;">
|
||
⏱️ 执行时间: {{ resultDialogParams.elapsed }}
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="resultDialogVisible = false" style="width: 100%;">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Working DirectorySelect对话框 -->
|
||
<el-dialog
|
||
v-model="jobSelectorVisible"
|
||
title="Select已有Working Directory"
|
||
width="700px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div v-if="existingJobs.length > 0" style="border: 1px solid #e4e7ed; border-radius: 4px;">
|
||
<div style="display: flex; background: #f5f7fa; padding: 10px; font-weight: bold; border-bottom: 1px solid #e4e7ed;">
|
||
<div style="width: 220px;">Job ID</div>
|
||
<div style="flex: 1;">已完成阶段</div>
|
||
<div style="width: 80px; text-align: center;">操作</div>
|
||
</div>
|
||
<div v-for="(job, index) in existingJobs" :key="job.job_id"
|
||
style="display: flex; padding: 10px; border-bottom: 1px solid #ebeef5; align-items: center;"
|
||
:style="{ background: index % 2 === 1 ? '#fafafa' : '#fff' }">
|
||
<div style="width: 220px; font-size: 13px; word-break: break-all;">{{ job.job_id }}</div>
|
||
<div style="flex: 1;">
|
||
<template v-if="job.completed_stages && job.completed_stages.length > 0">
|
||
<el-tag v-for="stage in job.completed_stages" :key="stage"
|
||
size="small" type="success" style="margin-right: 5px; margin-bottom: 3px;">
|
||
{{ stage }}
|
||
</el-tag>
|
||
</template>
|
||
<span v-else style="color: #999;">无</span>
|
||
</div>
|
||
<div style="width: 80px; text-align: center;">
|
||
<el-button type="primary" size="small" @click="selectJob(job.job_id)">Select</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else style="text-align: center; padding: 40px; color: #999;">
|
||
暂无已有Working Directory
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="jobSelectorVisible = false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
|
||
<!-- Agent状态显示组件模板 -->
|
||
<template id="agent-status-component-template">
|
||
<div class="agent-status-panel">
|
||
<div class="agent-header">
|
||
<h3>🤖 Agent 智能助手</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="Input您的问题,例如:分析SPL基因家族"
|
||
@keydown.enter.prevent="sendMessage"
|
||
rows="3"></textarea>
|
||
<button @click="sendMessage" :disabled="isProcessing">
|
||
{{ isProcessing ? '处理中...' : '发送' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="agent-tasks" v-if="tasks.length > 0">
|
||
<h4>📋 任务列表</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>
|
||
const { createApp, ref, reactive, computed, nextTick, onMounted } = Vue;
|
||
const { ElMessage } = ElementPlus;
|
||
|
||
// ========== Agent Status Component ==========
|
||
const AgentStatusComponent = {
|
||
template: '#agent-status-component-template',
|
||
|
||
data() {
|
||
return {
|
||
messages: [],
|
||
userInput: '',
|
||
isProcessing: false,
|
||
currentMode: '待机',
|
||
sessionId: '',
|
||
tasks: [],
|
||
currentTask: null,
|
||
pollingInterval: null
|
||
};
|
||
},
|
||
|
||
mounted() {
|
||
this.sessionId = 'session_' + Date.now();
|
||
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.currentMode = '处理中';
|
||
|
||
// 添加用户消息
|
||
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,
|
||
job_id: window.parentJobInfo ? window.parentJobInfo.job_id : 'default_job',
|
||
model: 'kimi'
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.currentTask = data.task_id;
|
||
this.addMessage('system', `任务已提交: ${data.task_id}`);
|
||
this.loadTasks();
|
||
} else {
|
||
this.addMessage('error', data.message || '提交失败');
|
||
this.currentMode = '错误';
|
||
}
|
||
} catch (error) {
|
||
this.addMessage('error', `请求失败: ${error.message}`);
|
||
this.currentMode = '错误';
|
||
}
|
||
|
||
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('加载任务失败:', 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 === 'running') {
|
||
this.currentMode = 'Executing';
|
||
} else if (task.status === 'completed') {
|
||
this.currentMode = '已完成';
|
||
} else if (task.status === 'failed') {
|
||
this.currentMode = '失败';
|
||
}
|
||
|
||
// 如果任务完成,显示结果
|
||
if (task.status === 'completed' && task.result) {
|
||
this.addMessage('assistant', task.result.response || '任务完成');
|
||
|
||
// 显示执行结果摘要
|
||
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',
|
||
`✅ 分析完成! 找到 ${metrics.final_members || 0} 个基因成员`);
|
||
}
|
||
}
|
||
} else if (task.status === 'failed') {
|
||
this.addMessage('error', task.result?.response || '任务失败');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('检查任务状态失败:', 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); // 每3s轮询一次
|
||
},
|
||
|
||
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 labels = {
|
||
'user': '👤 用户',
|
||
'assistant': '🤖 Agent',
|
||
'system': '💬 系统',
|
||
'error': '❌ 错误',
|
||
'execution': '⚙️ 执行',
|
||
'planning': '📋 规划',
|
||
'diagnosis': '🔍 诊断'
|
||
};
|
||
return labels[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'
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 调试:确认Vue加载成功
|
||
console.log('Vue loaded:', typeof Vue);
|
||
console.log('ElementPlus loaded:', typeof ElementPlus);
|
||
|
||
const app = createApp({
|
||
setup() {
|
||
// ========== 会话管理 ==========
|
||
const conversations = ref([
|
||
{ conversation_id: 'conv_001', title: 'Ft.SPL分析', gene_family: 'SPL', message_count: 12, updated_at: new Date().toISOString() },
|
||
{ conversation_id: 'conv_002', title: 'Os.TCP进化', gene_family: 'TCP', message_count: 5, updated_at: new Date(Date.now() - 3600000).toISOString() },
|
||
{ conversation_id: 'conv_003', title: 'HMMER参数咨询', gene_family: null, message_count: 8, updated_at: new Date(Date.now() - 86400000).toISOString() },
|
||
{ conversation_id: 'conv_004', title: '拟南芥WRKY鉴定', gene_family: 'WRKY', message_count: 15, updated_at: new Date(Date.now() - 172800000).toISOString() },
|
||
]);
|
||
const currentConversationId = ref('conv_001');
|
||
|
||
// 最近会话(显示为快捷按钮)
|
||
const recentConversations = computed(() => {
|
||
return conversations.value.slice(0, 3);
|
||
});
|
||
|
||
// 更多会话(显示在下拉框中)
|
||
const moreConversations = computed(() => {
|
||
return conversations.value.slice(3);
|
||
});
|
||
|
||
const createNewConversation = () => {
|
||
const newConv = {
|
||
conversation_id: 'conv_' + Date.now(),
|
||
title: '新会话 ' + new Date().toLocaleTimeString(),
|
||
gene_family: null,
|
||
message_count: 0,
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
conversations.value.unshift(newConv);
|
||
currentConversationId.value = newConv.conversation_id;
|
||
ElementPlus.ElMessage.success('新会话已创建');
|
||
};
|
||
|
||
const selectConversation = (conv) => {
|
||
currentConversationId.value = conv.conversation_id;
|
||
ElementPlus.ElMessage.info('切换到: ' + conv.title);
|
||
};
|
||
|
||
// 会话列表过滤(用于侧边栏)
|
||
const filteredConversations = computed(() => {
|
||
return conversations.value;
|
||
});
|
||
|
||
// Analysis StageSelect
|
||
const selectedStage = ref('');
|
||
|
||
// Basic Parameters
|
||
const params = reactive({
|
||
prefix: 'Ft.SPL',
|
||
cpu: 8
|
||
});
|
||
|
||
// Input路径
|
||
const inputPath = ref('');
|
||
|
||
// 任务信息
|
||
const jobInfo = reactive({
|
||
job_id: 'default_job',
|
||
input_dir: '',
|
||
working_dir: '',
|
||
output_dir: ''
|
||
});
|
||
|
||
// 对话消息
|
||
const messages = ref([]);
|
||
const logContent = ref('');
|
||
const chatArea = ref(null);
|
||
|
||
// 自然语言Input
|
||
const nlInput = ref('');
|
||
const nlLoading = ref(false);
|
||
|
||
// V2: RAG对话记忆
|
||
const ragMode = ref(false); // 是否RAG模式
|
||
const preferenceMode = ref(false); // 偏好模式: false=Skill(默认), true=Literature
|
||
const autoDetectedMode = ref('skill'); // 自动检测的模式: 'skill' 或 'faq'
|
||
|
||
// Agent Mode - Model Selection
|
||
const selectedModel = ref('kimi');
|
||
const modelDisplayName = computed(() => {
|
||
const names = {
|
||
kimi: '🧠 Kimi',
|
||
glm: '🔬 GLM-4',
|
||
minimax: '⚡ MiniMax',
|
||
llama3: '🦙 Llama3',
|
||
qwen2: '🤖 Qwen2',
|
||
ollama: '📍 Ollama'
|
||
};
|
||
return names[selectedModel.value] || '🧠 Kimi';
|
||
});
|
||
|
||
// Agent Session State
|
||
const agentSession = reactive({
|
||
messages: [],
|
||
currentTask: null,
|
||
tasks: []
|
||
});
|
||
|
||
// Agent Tasks列表
|
||
const agentTasks = ref([]);
|
||
|
||
// Agent意图历史
|
||
const intentHistory = ref([]);
|
||
|
||
// Upload Files列表
|
||
const uploadedFiles = ref([]);
|
||
|
||
const userId = ref('xia-lab'); // 用户ID
|
||
const sessionId = ref(''); // 会话ID
|
||
const ragHistory = ref([]); // RAG对话历史
|
||
const apiBaseUrl = ref('http://localhost:7867/api'); // API基础URL - bbav5后端端口
|
||
|
||
// V2: 生成唯一ID
|
||
const generateId = () => {
|
||
return 'user_' + Math.random().toString(36).substr(2, 9);
|
||
};
|
||
|
||
// V2: 初始化用户ID
|
||
const initUserId = () => {
|
||
let uid = localStorage.getItem('bbav_user_id');
|
||
if (!uid) {
|
||
uid = generateId();
|
||
localStorage.setItem('bbav_user_id', uid);
|
||
}
|
||
userId.value = uid;
|
||
|
||
let sid = localStorage.getItem('bbav_session_id');
|
||
if (!sid) {
|
||
sid = 'session_' + Date.now();
|
||
localStorage.setItem('bbav_session_id', sid);
|
||
}
|
||
sessionId.value = sid;
|
||
};
|
||
|
||
// V2: 切换偏好模式
|
||
const togglePreferenceMode = (val) => {
|
||
// val 是 switch 组件传入的新值
|
||
// false = Skill (默认), true = Literature
|
||
if (val) {
|
||
ElMessage.success('Preference set to: 📚 Literature (will use RAG for ambiguous queries)');
|
||
} else {
|
||
ElMessage.info('Preference set to: 🔧 Skill (will execute analysis for ambiguous queries)');
|
||
}
|
||
};
|
||
|
||
// V2: 切换RAG模式 (保留用于兼容性)
|
||
const toggleRagMode = (val) => {
|
||
if (val) {
|
||
initUserId();
|
||
ElMessage.success('Switched to Literature Q&A mode');
|
||
} else {
|
||
ElMessage.info('Switched to Skill Execution mode');
|
||
}
|
||
};
|
||
|
||
// V2: ClearRAG会话
|
||
const clearRagSession = () => {
|
||
sessionId.value = 'session_' + Date.now();
|
||
localStorage.setItem('bbav_session_id', sessionId.value);
|
||
ragHistory.value = [];
|
||
ElMessage.success('已Clear当前会话');
|
||
};
|
||
|
||
// 参数确认对话框
|
||
const confirmDialogVisible = ref(false);
|
||
const autoExecute = ref(false); // 自动执行模式
|
||
const autoExecuteCountdown = ref(15); // 15秒倒计时
|
||
let autoExecuteTimer = null; // 倒计时定时器
|
||
|
||
// 批量执行相关
|
||
const batchExecuteDialogVisible = ref(false);
|
||
const batchExecuteStages = ref([]);
|
||
const batchExecuteIndex = ref(0);
|
||
const batchExecuteParams = ref({});
|
||
const batchExecuteCountdown = ref(15);
|
||
let batchExecuteTimer = null;
|
||
const isBatchExecuting = ref(false);
|
||
const shouldStopBatchExecute = ref(false);
|
||
const currentBatchStage = ref('');
|
||
|
||
// 阶段确认对话框
|
||
const stageConfirmDialogVisible = ref(false);
|
||
const stageConfirmCountdown = ref(15);
|
||
let stageConfirmTimer = ref(null);
|
||
let stageConfirmResolve = ref(null);
|
||
const stageConfirmCurrent = ref(0);
|
||
const stageConfirmTotal = ref(0);
|
||
const stageConfirmQueue = ref([]); // 确认对话框队列
|
||
let stageConfirmProcessing = false; // 是否正在处理确认对话框
|
||
|
||
// 依赖驱动的并行执行架构
|
||
const stageDependencies = {
|
||
// 第一阶段:基因家族鉴定(必须最先完成)
|
||
'identify': [],
|
||
|
||
// 第二阶段:依赖鉴定完成,可并行
|
||
'phylogeny': ['identify'],
|
||
'pep': ['identify'],
|
||
'synteny': ['identify'],
|
||
'interspecies_synteny': ['identify'],
|
||
|
||
// 第三阶段:依赖各自前置
|
||
'structure': ['phylogeny'],
|
||
'motif': ['phylogeny'],
|
||
'domain': ['phylogeny'],
|
||
'promoter': ['phylogeny'],
|
||
|
||
'karyotype': ['synteny'],
|
||
'ppi': ['synteny'],
|
||
|
||
'kaks': ['interspecies_synteny'],
|
||
|
||
// RNA-seq 独立流程(严格按顺序执行)
|
||
'rnaseq_qc': [],
|
||
'rnaseq_align': ['rnaseq_qc'],
|
||
'rnaseq_quant': ['rnaseq_align'],
|
||
'rnaseq_de': ['rnaseq_quant'],
|
||
'rnaseq_enrich': ['rnaseq_de'],
|
||
|
||
// 表达分析(依赖鉴定,但可并行)
|
||
'expression': ['identify']
|
||
};
|
||
|
||
// 阶段执行状态跟踪
|
||
const stageExecutionStatus = ref({}); // { stageName: 'pending'|'running'|'completed'|'failed' }
|
||
const runningStages = ref(new Set()); // 当前正在执行的阶段
|
||
const maxParallelStages = 4; // 最大并行数
|
||
|
||
const confirmParams = reactive({
|
||
stage: '',
|
||
prefix: 'Ft.SPL',
|
||
cpu: 8,
|
||
// Identify 参数
|
||
idEvalue: 0.001,
|
||
idCoverage: 0.3,
|
||
blastEvalue: 0.00001,
|
||
requiredPfams: '',
|
||
refSpecies: '',
|
||
// Phylogeny 参数
|
||
mafftParams: '--auto --thread -1',
|
||
iqtreeParams: '-m MFP -bb 1000 -nt AUTO',
|
||
// Motif 参数
|
||
nmotifs: 10,
|
||
minw: 6,
|
||
maxw: 200,
|
||
// Domain 参数
|
||
domEvalue: 0.00001,
|
||
domCoverage: 0.3,
|
||
// Expression 参数
|
||
idTransform: 'remove_T01',
|
||
geoId: '',
|
||
// RNA-seq 参数
|
||
rnaseqMode: 'upload',
|
||
speciesName: '',
|
||
// RNA-seq QC 参数
|
||
rnaseqDataMode: 'upload',
|
||
rnaseqDataPath: '',
|
||
// PPI 参数
|
||
ppiSpecies: 'ath',
|
||
ppiScore: 700,
|
||
// KaKs 参数
|
||
kaksNeutralRate: 7.08e-9,
|
||
// Genome Prepare 参数
|
||
genomePrepareType: 'auto'
|
||
});
|
||
|
||
// 执行状态
|
||
geoId: ''
|
||
|
||
// 执行状态
|
||
const executing = ref(false);
|
||
|
||
const stages = reactive([
|
||
{ name: 'genome_prepare', label: '0️⃣ Genome Prepare', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'identify', label: '1️⃣ Identify', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'phylogeny', label: '2️⃣ Phylogeny', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'motif', label: '3️⃣ Motif', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'domain', label: '4️⃣ Domain', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'structure', label: '5️⃣ Structure', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'promoter', label: '6️⃣ Promoter', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'synteny', label: '7️⃣ Synteny', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'karyotype', label: '8️⃣ Karyotype', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'interspecies_synteny', label: '9️⃣ Interspecies', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'ppi', label: '🔟 PPI', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'kaks', label: '1️⃣1️⃣ KaKs', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'pep', label: '1️⃣2️⃣ PEP', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'expression', label: '1️⃣3️⃣ Expression', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'combo', label: '1️⃣4️⃣ Combo', 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 },
|
||
{ name: 'rnaseq_quant', label: '1️⃣7️⃣ RNA-seq Quant', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'rnaseq_de', label: '1️⃣8️⃣ RNA-seq DE', status: 'pending', images: [], files: [], elapsed_time: 0 },
|
||
{ name: 'rnaseq_enrich', label: '1️⃣9️⃣ RNA-seq Enrich', status: 'pending', images: [], files: [], elapsed_time: 0 }
|
||
]);
|
||
// 新版结果区域 - 展开的阶段
|
||
const activeStages = ref('phylogeny');
|
||
|
||
// 当前选中的文件
|
||
const selectedResultStage = ref('');
|
||
const selectedFile = ref('');
|
||
const selectedImage = ref('');
|
||
const selectedImageUrl = ref('');
|
||
const selectedImageSize = ref('');
|
||
const zoomLevel = ref(100);
|
||
const previewContentRef = ref(null);
|
||
|
||
// 判断是否为图片文件
|
||
const isImageFile = (filename) => {
|
||
const imgExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.bmp', '.webp'];
|
||
return imgExts.some(ext => filename.toLowerCase().endsWith(ext));
|
||
};
|
||
|
||
// Select文件
|
||
const selectFile = (stageName, file) => {
|
||
selectedResultStage.value = stageName;
|
||
selectedFile.value = file.name;
|
||
|
||
// PPI URL 文件:点击后在新窗口打开 STRING 链接
|
||
if (file.name.endsWith('.ppi_url.txt')) {
|
||
fetch(file.path)
|
||
.then(r => r.text())
|
||
.then(text => {
|
||
// 提取 URL:查找 http 开头的行或包含 http 的行
|
||
const lines = text.split('\n');
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
// 直接以 http 开头的行
|
||
if (trimmed.startsWith('http')) {
|
||
window.open(trimmed, '_blank');
|
||
return;
|
||
}
|
||
// 包含 http 的行(如 "View online: https://...")
|
||
const httpMatch = trimmed.match(/(https?:\/\/[^\s]+)/);
|
||
if (httpMatch) {
|
||
window.open(httpMatch[1], '_blank');
|
||
return;
|
||
}
|
||
}
|
||
// 如果没找到 URL,显示文件内容
|
||
ElMessage.info('STRING 链接文件内容:' + text.substring(0, 200));
|
||
})
|
||
.catch(err => {
|
||
ElMessage.error('读取文件失败: ' + err.message);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// TSV/CSV 文件:点击后显示内容预览
|
||
if (file.name.endsWith('.tsv') || file.name.endsWith('.csv')) {
|
||
fetch(file.path)
|
||
.then(r => r.text())
|
||
.then(text => {
|
||
// 显示前 500 字符的预览
|
||
const preview = text.substring(0, 500) + (text.length > 500 ? '...' : '');
|
||
ElMessage.info({
|
||
message: '文件预览(前500字符):\n' + preview,
|
||
duration: 5000,
|
||
showClose: true
|
||
});
|
||
})
|
||
.catch(err => {
|
||
ElMessage.error('读取文件失败: ' + err.message);
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isImageFile(file.name)) {
|
||
selectedImage.value = file.name;
|
||
selectedImageUrl.value = '/api/file/' + stageName + '/' + file.name;
|
||
selectedImageSize.value = formatSize(file.size);
|
||
zoomLevel.value = 100;
|
||
} else {
|
||
selectedImage.value = '';
|
||
selectedImageUrl.value = '';
|
||
}
|
||
};
|
||
|
||
// 缩放图片 - Zoom In
|
||
const zoomIn = () => {
|
||
zoomLevel.value = Math.min(300, zoomLevel.value + 25);
|
||
};
|
||
|
||
// 缩放图片 - Zoom Out
|
||
const zoomOut = () => {
|
||
zoomLevel.value = Math.max(25, zoomLevel.value - 25);
|
||
};
|
||
|
||
// Reset缩放
|
||
const resetZoom = () => {
|
||
zoomLevel.value = 100;
|
||
};
|
||
|
||
// 全屏切换
|
||
const toggleFullscreen = () => {
|
||
if (!document.fullscreenElement) {
|
||
previewContentRef.value?.requestFullscreen();
|
||
} else {
|
||
document.exitFullscreen();
|
||
}
|
||
};
|
||
|
||
// Download图片
|
||
const downloadImage = () => {
|
||
if (selectedImageUrl.value) {
|
||
const link = document.createElement('a');
|
||
link.href = selectedImageUrl.value;
|
||
link.download = selectedImage.value;
|
||
link.click();
|
||
}
|
||
};
|
||
|
||
// 图片加载回调
|
||
const onImageLoad = () => {
|
||
zoomLevel.value = 100;
|
||
};
|
||
|
||
// 图片加载错误
|
||
const onImageError = () => {
|
||
console.error('图片加载失败:', selectedImageUrl.value);
|
||
};
|
||
|
||
// 缩放变化
|
||
const onZoomChange = (val) => {
|
||
zoomLevel.value = val;
|
||
};
|
||
|
||
// 从路径中提取文件名
|
||
const getFileName = (path) => {
|
||
return path.split('/').pop();
|
||
};
|
||
|
||
// Select图片文件
|
||
const selectImageFile = (stageName, imgPath) => {
|
||
const fileName = getFileName(imgPath);
|
||
selectedResultStage.value = stageName;
|
||
selectedFile.value = fileName;
|
||
selectedImage.value = fileName;
|
||
// 直接使用完整路径访问图片
|
||
selectedImageUrl.value = imgPath;
|
||
selectedImageSize.value = '';
|
||
zoomLevel.value = 100;
|
||
};
|
||
|
||
// 图片预览
|
||
const previewDialogVisible = ref(false);
|
||
const previewImg = ref('');
|
||
const previewParams = reactive({ width: 800, height: 600 });
|
||
|
||
// 格式化文件大小
|
||
const formatSize = (bytes) => {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
|
||
return (bytes/1024/1024).toFixed(1) + ' MB';
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (timestamp) => {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||
};
|
||
|
||
// 获取Agent Tasks列表
|
||
const fetchAgentTasks = async () => {
|
||
try {
|
||
const response = await fetch('/api/agent/tasks');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
agentTasks.value = data.tasks || [];
|
||
}
|
||
} catch (err) {
|
||
console.error('获取Agent Tasks失败:', err);
|
||
}
|
||
};
|
||
|
||
// 显示任务详情
|
||
const showTaskDetail = (task) => {
|
||
ElMessage.info(`任务: ${task.message}\n状态: ${task.status}\n进度: ${task.progress}%`);
|
||
};
|
||
|
||
// 定时RefreshAgent Tasks
|
||
let agentTaskTimer = null;
|
||
const startAgentTaskPolling = () => {
|
||
agentTaskTimer = setInterval(fetchAgentTasks, 5000);
|
||
};
|
||
const stopAgentTaskPolling = () => {
|
||
if (agentTaskTimer) clearInterval(agentTaskTimer);
|
||
};
|
||
|
||
// MCScanX HTML文件列表
|
||
const mcscanxHtmlFiles = ref([]);
|
||
|
||
// 获取MCScanX HTML文件列表
|
||
const refreshMcscanxHtml = async () => {
|
||
if (!jobInfo.job_id || jobInfo.job_id === 'default_job') return;
|
||
try {
|
||
const response = await fetch(`/api/mcscanx_html/${jobInfo.job_id}`);
|
||
const data = await response.json();
|
||
if (data.exists) {
|
||
mcscanxHtmlFiles.value = data.files || [];
|
||
} else {
|
||
mcscanxHtmlFiles.value = [];
|
||
}
|
||
} catch (err) {
|
||
console.error('获取MCScanX HTML失败:', err);
|
||
mcscanxHtmlFiles.value = [];
|
||
}
|
||
};
|
||
|
||
// 打开MCScanX HTML文件
|
||
const openMcscanxHtml = (file) => {
|
||
window.open(file.path, '_blank');
|
||
};
|
||
|
||
// 添加消息
|
||
const addMessage = (role, content) => {
|
||
const now = new Date();
|
||
messages.value.push({ role, content, time: now.toLocaleTimeString() });
|
||
nextTick(() => {
|
||
if (chatArea.value) {
|
||
chatArea.value.scrollTop = chatArea.value.scrollHeight;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 复制路径到剪贴板
|
||
const copyPath = async (path) => {
|
||
try {
|
||
await navigator.clipboard.writeText(path);
|
||
ElMessage.success('路径已复制');
|
||
} catch (err) {
|
||
ElMessage.error('复制失败');
|
||
}
|
||
};
|
||
|
||
// Refresh任务信息
|
||
const refreshInfo = async () => {
|
||
try {
|
||
const response = await fetch('/api/info');
|
||
const data = await response.json();
|
||
jobInfo.job_id = data.job_id || 'default_job';
|
||
jobInfo.working_dir = data.working_dir || '';
|
||
jobInfo.input_dir = data.input_dir || '';
|
||
jobInfo.output_dir = data.output_dir || '';
|
||
// 更新全局变量供 Agent 组件使用
|
||
window.parentJobInfo = { ...jobInfo };
|
||
} catch (err) { console.error(err); }
|
||
};
|
||
|
||
// NewWorking Directory
|
||
const createNewJob = async () => {
|
||
try {
|
||
const response = await fetch('/api/new_job', { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
jobInfo.job_id = data.job_id;
|
||
jobInfo.input_dir = data.input_dir;
|
||
jobInfo.working_dir = data.workspace_dir;
|
||
jobInfo.output_dir = data.workspace_dir + '/final';
|
||
// 更新全局变量供 Agent 组件使用
|
||
window.parentJobInfo = { ...jobInfo };
|
||
// Clear结果
|
||
stages.forEach(s => { s.images = []; s.files = []; s.status = 'pending'; });
|
||
messages.value = [];
|
||
logContent.value = '';
|
||
ElMessage.success('已创建新Working Directory: ' + data.job_id);
|
||
}
|
||
} catch (err) {
|
||
ElMessage.error('创建失败: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// 已有Working Directory列表
|
||
const existingJobs = ref([]);
|
||
const jobSelectorVisible = ref(false);
|
||
const selectedJobId = ref('');
|
||
|
||
// 显示Working DirectorySelect器
|
||
const showJobSelector = async () => {
|
||
try {
|
||
const response = await fetch('/api/list_jobs');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
existingJobs.value = data.jobs;
|
||
jobSelectorVisible.value = true;
|
||
}
|
||
} catch (err) {
|
||
ElMessage.error('获取Working Directory列表失败: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// SelectWorking Directory
|
||
const selectJob = async (jobId) => {
|
||
try {
|
||
const response = await fetch('/api/select_job', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ job_id: jobId })
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
jobInfo.job_id = data.job_id;
|
||
jobInfo.input_dir = data.input_dir;
|
||
jobInfo.working_dir = data.workspace_dir;
|
||
jobInfo.output_dir = data.workspace_dir + '/final';
|
||
// 更新全局变量供 Agent 组件使用
|
||
window.parentJobInfo = { ...jobInfo };
|
||
jobSelectorVisible.value = false;
|
||
ElMessage.success('已切换到Working Directory: ' + data.job_id);
|
||
// Refresh结果展示
|
||
await refreshResults();
|
||
} else {
|
||
ElMessage.error(data.error || '切换失败');
|
||
}
|
||
} catch (err) {
|
||
ElMessage.error('切换失败: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// 阶段Select变化
|
||
const onStageChange = (val) => {
|
||
console.log('Select阶段:', val);
|
||
if (val && val.startsWith('rnaseq')) {
|
||
// RNA-seq阶段特殊处理
|
||
loadRnaseqData();
|
||
}
|
||
};
|
||
|
||
// ========== 会话管理方法 ==========
|
||
const onSelectConversation = (conv) => {
|
||
console.log('Select会话:', conv);
|
||
currentConversation.value = conv;
|
||
if (conv) {
|
||
// 加载会话消息
|
||
loadConversationMessages(conv.conversation_id);
|
||
// 更新标题
|
||
document.title = conv.title + ' - BotanicalBaseAgent';
|
||
}
|
||
};
|
||
|
||
const onNewConversation = (conv) => {
|
||
console.log('新建会话:', conv);
|
||
currentConversation.value = conv;
|
||
// 清空当前消息
|
||
messages.value = [];
|
||
agentSession.messages = [];
|
||
// 添加欢迎消息
|
||
const welcomeMsg = {
|
||
role: 'assistant',
|
||
content: '您好!我是BotanicalBaseAgent,专注于基因家族分析。请告诉我您想分析哪个基因家族?',
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
messages.value.push(welcomeMsg);
|
||
agentSession.messages.push(welcomeMsg);
|
||
};
|
||
|
||
const onSidebarToggle = (collapsed) => {
|
||
sidebarCollapsed.value = collapsed;
|
||
console.log('侧边栏状态:', collapsed ? '折叠' : '展开');
|
||
};
|
||
|
||
// 加载会话消息
|
||
const loadConversationMessages = async (conversationId) => {
|
||
try {
|
||
const response = await fetch(`${apiBaseUrl.value}/conversations/${conversationId}/messages`);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
messages.value = data.messages || [];
|
||
agentSession.messages = data.messages || [];
|
||
}
|
||
} catch (e) {
|
||
console.error('加载会话消息失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载RNA-seq数据
|
||
const loadRnaseqData = async () => {
|
||
try {
|
||
const response = await fetch('/api/rnaseq/samples');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
rnaseqSamples.value = data.samples;
|
||
}
|
||
} catch (e) {
|
||
console.error('加载RNA-seq样本失败:', e);
|
||
}
|
||
};
|
||
|
||
const rnaseqSamples = ref([]);
|
||
|
||
// 显示确认对话框
|
||
const showConfirm = () => {
|
||
console.log('[DEBUG] showConfirm called, selectedStage:', selectedStage.value);
|
||
if (!selectedStage.value) {
|
||
ElMessage.warning('请先SelectAnalysis Stage');
|
||
console.log('[DEBUG] showConfirm: no stage selected, returning');
|
||
return;
|
||
}
|
||
confirmParams.stage = selectedStage.value;
|
||
confirmParams.prefix = params.prefix;
|
||
confirmParams.cpu = params.cpu;
|
||
console.log('[DEBUG] showConfirm: setting confirmDialogVisible = true');
|
||
confirmDialogVisible.value = true;
|
||
console.log('[DEBUG] showConfirm: confirmDialogVisible =', confirmDialogVisible.value);
|
||
addMessage('user', `▶ 准备运行: ${selectedStage.value.toUpperCase()}`);
|
||
console.log('[DEBUG] showConfirm: message added');
|
||
|
||
// 启动15秒自动执行倒计时
|
||
startAutoExecuteTimer();
|
||
};
|
||
|
||
// 启动自动执行倒计时
|
||
const startAutoExecuteTimer = () => {
|
||
autoExecute.value = true;
|
||
autoExecuteCountdown.value = 15;
|
||
clearAutoExecuteTimer();
|
||
autoExecuteTimer = setInterval(() => {
|
||
autoExecuteCountdown.value--;
|
||
if (autoExecuteCountdown.value <= 0) {
|
||
clearAutoExecuteTimer();
|
||
if (confirmDialogVisible.value && !executing.value) {
|
||
console.log('[DEBUG] Auto executing after 15s countdown');
|
||
doExecute();
|
||
}
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
// 清除自动执行倒计时
|
||
const clearAutoExecuteTimer = () => {
|
||
if (autoExecuteTimer) {
|
||
clearInterval(autoExecuteTimer);
|
||
autoExecuteTimer = null;
|
||
}
|
||
autoExecute.value = false;
|
||
};
|
||
|
||
// 批量执行相关函数
|
||
const startBatchExecuteTimer = () => {
|
||
batchExecuteCountdown.value = 30; // 增加到30秒,给用户更多时间确认
|
||
clearBatchExecuteTimer();
|
||
|
||
// 添加明显的提示消息
|
||
ElMessage.info(`📋 批量执行确认对话框已打开,${batchExecuteCountdown.value}秒后自动开始执行,请点击确认或取消`);
|
||
|
||
batchExecuteTimer = setInterval(() => {
|
||
batchExecuteCountdown.value--;
|
||
if (batchExecuteCountdown.value <= 0) {
|
||
clearBatchExecuteTimer();
|
||
if (batchExecuteDialogVisible.value && !isBatchExecuting.value) {
|
||
console.log('[DEBUG] Auto starting batch execute after 30s');
|
||
ElMessage.warning('⏱️ 倒计时结束,自动开始执行...');
|
||
startBatchExecute();
|
||
}
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
const clearBatchExecuteTimer = () => {
|
||
if (batchExecuteTimer) {
|
||
clearInterval(batchExecuteTimer);
|
||
batchExecuteTimer = null;
|
||
}
|
||
};
|
||
|
||
const startBatchExecute = async () => {
|
||
clearBatchExecuteTimer();
|
||
batchExecuteDialogVisible.value = false;
|
||
isBatchExecuting.value = true;
|
||
shouldStopBatchExecute.value = false;
|
||
|
||
const stages = batchExecuteStages.value;
|
||
|
||
addAgentMessage('assistant', `🚀 开始依赖驱动的并行执行,共 ${stages.length} 个阶段...`);
|
||
|
||
// 初始化阶段状态
|
||
stageExecutionStatus.value = {};
|
||
|
||
// 检查已完成的阶段(通过manifest文件)
|
||
const checkCompletedStages = async () => {
|
||
const allStages = ['identify', 'phylogeny', 'motif', 'domain', 'structure', 'promoter',
|
||
'synteny', 'karyotype', 'interspecies_synteny', 'ppi', 'kaks', 'pep',
|
||
'expression', 'rnaseq_qc', 'rnaseq_align', 'rnaseq_quant', 'rnaseq_de', 'rnaseq_enrich'];
|
||
const jobId = jobInfo.job_id;
|
||
|
||
for (const stage of allStages) {
|
||
try {
|
||
const response = await fetch(`/api/stage-status?job_id=${jobId}&stage=${stage}`);
|
||
const data = await response.json();
|
||
if (data.completed) {
|
||
stageExecutionStatus.value[stage] = 'completed';
|
||
console.log(`[DEBUG] Stage ${stage} already completed`);
|
||
} else {
|
||
// 明确设置为未完成状态,避免undefined
|
||
stageExecutionStatus.value[stage] = 'pending';
|
||
}
|
||
} catch (e) {
|
||
// 出错时设置为pending
|
||
stageExecutionStatus.value[stage] = 'pending';
|
||
}
|
||
}
|
||
};
|
||
|
||
await checkCompletedStages();
|
||
|
||
// 设置待执行阶段为pending
|
||
stages.forEach(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
if (!stageExecutionStatus.value[stageId]) {
|
||
stageExecutionStatus.value[stageId] = 'pending';
|
||
}
|
||
});
|
||
runningStages.value.clear();
|
||
|
||
// 启动依赖驱动的并行执行
|
||
await executeStagesWithDependencies(stages);
|
||
|
||
isBatchExecuting.value = false;
|
||
currentBatchStage.value = '';
|
||
|
||
if (!shouldStopBatchExecute.value) {
|
||
// 只统计当前批次 stages 中的 completed 数量(避免累积历史状态)
|
||
const completedCount = stages.filter(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
return stageExecutionStatus.value[stageId] === 'completed';
|
||
}).length;
|
||
const failedCount = stages.filter(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
return stageExecutionStatus.value[stageId] === 'failed';
|
||
}).length;
|
||
const totalCount = stages.length;
|
||
|
||
if (failedCount > 0) {
|
||
addAgentMessage('assistant', `⚠️ 批量执行完成:成功 ${completedCount}/${totalCount},失败 ${failedCount}/${totalCount}`);
|
||
} else {
|
||
addAgentMessage('assistant', `🎉 批量执行完成!成功: ${completedCount}/${totalCount}`);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 依赖驱动的并行执行核心逻辑
|
||
const executeStagesWithDependencies = async (stages) => {
|
||
return new Promise((resolve) => {
|
||
const checkAndExecute = async () => {
|
||
// 检查是否停止
|
||
if (shouldStopBatchExecute.value) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 获取可执行的阶段(依赖已完成,且未在执行/完成)
|
||
const executableStages = stages.filter(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
const status = stageExecutionStatus.value[stageId];
|
||
if (status !== 'pending') return false;
|
||
|
||
const deps = stageDependencies[stageId] || [];
|
||
return deps.every(dep => stageExecutionStatus.value[dep] === 'completed');
|
||
});
|
||
|
||
// 检查是否全部完成
|
||
const allCompleted = stages.every(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
return stageExecutionStatus.value[stageId] === 'completed' ||
|
||
stageExecutionStatus.value[stageId] === 'failed';
|
||
});
|
||
|
||
if (allCompleted) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 并行执行可执行的阶段(限制最大并行数)
|
||
const availableSlots = maxParallelStages - runningStages.value.size;
|
||
const stagesToExecute = executableStages.slice(0, availableSlots);
|
||
|
||
if (stagesToExecute.length > 0) {
|
||
console.log('[DEBUG] Executing stages:', stagesToExecute);
|
||
|
||
// 并行启动所有可执行阶段
|
||
const executePromises = stagesToExecute.map(stage =>
|
||
executeStageWithConfirmation(stage, stages.length)
|
||
);
|
||
|
||
await Promise.all(executePromises);
|
||
}
|
||
|
||
// 继续检查(如果有阶段还在运行,稍后再检查)
|
||
if (runningStages.value.size > 0 || executableStages.length > availableSlots) {
|
||
console.log('[DEBUG] Stages still running or waiting for slots, rechecking in 2s...');
|
||
setTimeout(checkAndExecute, 2000);
|
||
} else if (!allCompleted) {
|
||
// 还有阶段未完成,检查是否有依赖失败或等待依赖完成
|
||
const failedDeps = stages.filter(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
const status = stageExecutionStatus.value[stageId];
|
||
if (status === 'pending') {
|
||
const deps = stageDependencies[stageId] || [];
|
||
return deps.some(dep => stageExecutionStatus.value[dep] === 'failed');
|
||
}
|
||
return false;
|
||
});
|
||
|
||
if (failedDeps.length > 0) {
|
||
const failedNames = failedDeps.map(s => typeof s === 'object' ? s.name : s).join(', ');
|
||
addAgentMessage('assistant', `⚠️ 以下阶段因依赖失败被跳过: ${failedNames}`);
|
||
failedDeps.forEach(stage => {
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
stageExecutionStatus.value[stageId] = 'failed';
|
||
});
|
||
}
|
||
|
||
// 重要:继续检查,等待依赖完成或新阶段可执行
|
||
console.log('[DEBUG] Waiting for dependencies, rechecking in 2s...');
|
||
setTimeout(checkAndExecute, 2000);
|
||
} else {
|
||
console.log('[DEBUG] All stages completed or failed, resolving.');
|
||
resolve();
|
||
}
|
||
};
|
||
|
||
// 开始执行
|
||
checkAndExecute();
|
||
});
|
||
};
|
||
|
||
// 执行单个阶段(带确认对话框)
|
||
const executeStageWithConfirmation = async (stage, totalStages) => {
|
||
// stage可能是对象或字符串,统一处理
|
||
const stageId = typeof stage === 'object' ? stage.id : stage;
|
||
const stageName = typeof stage === 'object' ? stage.name : stage;
|
||
|
||
runningStages.value.add(stageId);
|
||
stageExecutionStatus.value[stageId] = 'running';
|
||
currentBatchStage.value = stageId;
|
||
|
||
try {
|
||
// 显示阶段确认对话框(带15秒倒计时)
|
||
const shouldExecute = await showStageConfirmDialog(stage,
|
||
Object.values(stageExecutionStatus.value).filter(s => s === 'completed').length + 1,
|
||
totalStages
|
||
);
|
||
|
||
if (!shouldExecute) {
|
||
addAgentMessage('assistant', `⏭️ 跳过阶段: ${stageName}`);
|
||
stageExecutionStatus.value[stageId] = 'failed'; // 标记为失败以便下游知道
|
||
runningStages.value.delete(stageId);
|
||
return;
|
||
}
|
||
|
||
if (shouldStopBatchExecute.value) {
|
||
stageExecutionStatus.value[stageId] = 'failed';
|
||
runningStages.value.delete(stageId);
|
||
return;
|
||
}
|
||
|
||
addAgentMessage('assistant', `⚙️ 执行阶段: ${stageName}...`);
|
||
|
||
// 执行阶段
|
||
await executeSingleStageWithParams(stageId);
|
||
|
||
stageExecutionStatus.value[stageId] = 'completed';
|
||
addAgentMessage('assistant', `✅ 阶段 ${stageName} 完成`);
|
||
|
||
// 刷新结果展示
|
||
await refreshResults();
|
||
|
||
} catch (err) {
|
||
stageExecutionStatus.value[stageId] = 'failed';
|
||
addAgentMessage('assistant', `❌ 阶段 ${stageName} 失败: ${err.message}`);
|
||
console.error('[ERROR] Stage failed:', stageId, err);
|
||
} finally {
|
||
runningStages.value.delete(stageId);
|
||
}
|
||
};
|
||
|
||
// 显示阶段确认对话框(队列机制,支持并行阶段的动态确认)
|
||
const showStageConfirmDialog = (stage, current, total) => {
|
||
return new Promise((resolve) => {
|
||
// 将确认请求加入队列
|
||
stageConfirmQueue.value.push({
|
||
stage,
|
||
current,
|
||
total,
|
||
resolve
|
||
});
|
||
|
||
// 尝试处理队列
|
||
processStageConfirmQueue();
|
||
});
|
||
};
|
||
|
||
// 处理确认对话框队列
|
||
const processStageConfirmQueue = () => {
|
||
// 如果正在显示对话框或队列为空,则等待
|
||
if (stageConfirmProcessing || stageConfirmQueue.value.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 取出队列中的第一个确认请求
|
||
const request = stageConfirmQueue.value.shift();
|
||
stageConfirmProcessing = true;
|
||
|
||
// stage可能是对象或字符串
|
||
const stageId = typeof request.stage === 'object' ? request.stage.id : request.stage;
|
||
const stageName = typeof request.stage === 'object' ? request.stage.name : request.stage;
|
||
|
||
// 设置当前阶段参数
|
||
selectedStage.value = stageId;
|
||
confirmParams.stage = stageId;
|
||
confirmParams.prefix = batchExecuteParams.value.params?.prefix || 'Ft.SPL';
|
||
confirmParams.cpu = batchExecuteParams.value.params?.cpu || 8;
|
||
|
||
// 特殊处理:ppi 自动设置基因ID文件
|
||
if (stageId === 'ppi') {
|
||
confirmParams.ppiSpecies = 'ath';
|
||
confirmParams.ppiScore = 700;
|
||
}
|
||
|
||
// 显示确认对话框
|
||
stageConfirmDialogVisible.value = true;
|
||
stageConfirmCountdown.value = 15;
|
||
stageConfirmResolve.value = request.resolve;
|
||
stageConfirmCurrent.value = request.current;
|
||
stageConfirmTotal.value = request.total;
|
||
|
||
console.log(`[DEBUG] Showing confirm dialog for ${stageName} (${request.current}/${request.total})`);
|
||
|
||
// 启动15秒倒计时
|
||
clearStageConfirmTimer();
|
||
stageConfirmTimer.value = setInterval(() => {
|
||
stageConfirmCountdown.value--;
|
||
if (stageConfirmCountdown.value <= 0) {
|
||
clearStageConfirmTimer();
|
||
stageConfirmDialogVisible.value = false;
|
||
stageConfirmProcessing = false;
|
||
request.resolve(true); // 自动确认执行
|
||
|
||
// 处理队列中的下一个
|
||
setTimeout(processStageConfirmQueue, 100);
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
const clearStageConfirmTimer = () => {
|
||
if (stageConfirmTimer.value) {
|
||
clearInterval(stageConfirmTimer.value);
|
||
stageConfirmTimer.value = null;
|
||
}
|
||
};
|
||
|
||
const confirmStageExecute = () => {
|
||
clearStageConfirmTimer();
|
||
stageConfirmDialogVisible.value = false;
|
||
if (stageConfirmResolve.value) {
|
||
stageConfirmResolve.value(true);
|
||
stageConfirmResolve.value = null;
|
||
}
|
||
stageConfirmProcessing = false;
|
||
|
||
// 处理队列中的下一个
|
||
setTimeout(processStageConfirmQueue, 100);
|
||
};
|
||
|
||
const skipStageExecute = () => {
|
||
clearStageConfirmTimer();
|
||
stageConfirmDialogVisible.value = false;
|
||
if (stageConfirmResolve.value) {
|
||
stageConfirmResolve.value(false);
|
||
stageConfirmResolve.value = null;
|
||
}
|
||
stageConfirmProcessing = false;
|
||
|
||
// 处理队列中的下一个
|
||
setTimeout(processStageConfirmQueue, 100);
|
||
};
|
||
|
||
const stopBatchExecute = () => {
|
||
shouldStopBatchExecute.value = true;
|
||
clearStageConfirmTimer();
|
||
stageConfirmDialogVisible.value = false;
|
||
if (stageConfirmResolve.value) {
|
||
stageConfirmResolve.value(false);
|
||
stageConfirmResolve.value = null;
|
||
}
|
||
stageConfirmProcessing = false;
|
||
|
||
// 清空队列
|
||
stageConfirmQueue.value = [];
|
||
};
|
||
|
||
// 显示是否继续对话框
|
||
const showContinueDialog = (stage, errorMsg) => {
|
||
return new Promise((resolve) => {
|
||
// 简化为自动继续,实际可以添加确认对话框
|
||
resolve(true);
|
||
});
|
||
};
|
||
|
||
const cancelBatchExecute = () => {
|
||
clearBatchExecuteTimer();
|
||
batchExecuteDialogVisible.value = false;
|
||
isBatchExecuting.value = false;
|
||
addAgentMessage('assistant', '❌ 批量执行已取消');
|
||
};
|
||
|
||
// 执行单个阶段(带完整参数)
|
||
const executeSingleStageWithParams = async (stageName) => {
|
||
const requestBody = {
|
||
stage: stageName,
|
||
job_id: jobInfo.job_id,
|
||
prefix: confirmParams.prefix,
|
||
cpu: confirmParams.cpu
|
||
};
|
||
|
||
// 根据阶段添加特定参数
|
||
if (stageName === 'ppi') {
|
||
requestBody.ppiSpecies = confirmParams.ppiSpecies || 'ath';
|
||
requestBody.ppiScore = confirmParams.ppiScore || 700;
|
||
}
|
||
|
||
if (stageName === 'kaks') {
|
||
requestBody.kaksNeutralRate = confirmParams.kaksNeutralRate || 7.08e-9;
|
||
}
|
||
|
||
if (stageName === 'expression') {
|
||
requestBody.geoId = confirmParams.geoId || '';
|
||
requestBody.idTransform = confirmParams.idTransform || 'remove_T01';
|
||
}
|
||
|
||
if (stageName.startsWith('rnaseq')) {
|
||
requestBody.rnaseqMode = confirmParams.rnaseqMode || 'upload';
|
||
requestBody.speciesName = confirmParams.speciesName || '';
|
||
}
|
||
|
||
console.log('[DEBUG] Executing stage with params:', stageName, requestBody);
|
||
|
||
const response = await fetch('/api/execute', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.task_id) {
|
||
// 轮询任务状态
|
||
return await pollTaskUntilComplete(data.task_id, stageName);
|
||
} else {
|
||
throw new Error(data.message || '执行失败');
|
||
}
|
||
};
|
||
|
||
// 执行单个阶段(内部函数)
|
||
const executeSingleStage = async (stageName) => {
|
||
return new Promise((resolve, reject) => {
|
||
// 启动执行
|
||
const doExecuteInternal = async () => {
|
||
try {
|
||
const response = await fetch('/api/execute', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
stage: stageName,
|
||
job_id: jobInfo.job_id,
|
||
prefix: confirmParams.prefix,
|
||
cpu: confirmParams.cpu
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.task_id) {
|
||
// 轮询任务状态
|
||
await pollTaskUntilComplete(data.task_id, stageName);
|
||
resolve(data);
|
||
} else {
|
||
reject(new Error(data.message || '执行失败'));
|
||
}
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
};
|
||
|
||
doExecuteInternal();
|
||
});
|
||
};
|
||
|
||
const pollTaskUntilComplete = async (taskId, stageName) => {
|
||
return new Promise((resolve, reject) => {
|
||
const checkStatus = async () => {
|
||
try {
|
||
const response = await fetch(`/api/task/${taskId}`);
|
||
const data = await response.json();
|
||
|
||
// 后端返回: completed, failed, running, pending
|
||
if (data.status === 'completed' || data.status === 'success') {
|
||
resolve(data);
|
||
} else if (data.status === 'failed') {
|
||
reject(new Error(data.message || '任务失败'));
|
||
} else {
|
||
// 继续轮询 (running 或 pending)
|
||
console.log(`[DEBUG] Task ${taskId} status: ${data.status}, continuing poll...`);
|
||
setTimeout(checkStatus, 2000);
|
||
}
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
};
|
||
|
||
checkStatus();
|
||
});
|
||
};
|
||
|
||
// 执行
|
||
let executeStartTime = null;
|
||
let executeTimer = null;
|
||
const executeElapsed = ref('0.0');
|
||
const resultDialogVisible = ref(false);
|
||
const resultDialogParams = ref({});
|
||
|
||
// 异步任务轮询相关
|
||
let pollTimer = null;
|
||
const currentTask = ref(null);
|
||
|
||
// Stop Execution相关
|
||
const isExecuting = ref(false);
|
||
const isStopping = ref(false);
|
||
const currentTaskId = ref(null);
|
||
|
||
// 轮询任务状态
|
||
const pollTaskStatus = async (taskId, stageName) => {
|
||
try {
|
||
const response = await fetch(`/api/task/${taskId}`);
|
||
const data = await response.json();
|
||
|
||
if (!data) return;
|
||
|
||
// 更新消息显示
|
||
if (data.message) {
|
||
// 找到最后一条assistant消息并更新
|
||
const lastMsg = messages.value.filter(m => m.role === 'assistant').pop();
|
||
if (lastMsg && lastMsg.content.includes('执行阶段') || lastMsg.content.includes('等待')) {
|
||
lastMsg.content = `⏳ ${data.message}`;
|
||
}
|
||
}
|
||
|
||
// 更新阶段状态
|
||
const stageIdx = stages.findIndex(s => s.name === stageName);
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = data.status === 'running' ? 'running' :
|
||
data.status === 'success' ? 'success' :
|
||
data.status === 'failed' ? 'failed' : 'pending';
|
||
}
|
||
|
||
if (data.status === 'success') {
|
||
// 任务完成
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
}
|
||
|
||
// Stop计时器
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
const elapsed = executeStartTime ? Math.round((Date.now() - executeStartTime) / 1000) : 0;
|
||
const elapsedStr = elapsed > 60 ? `${Math.floor(elapsed/60)}分${elapsed%60}s` : `${elapsed}s`;
|
||
|
||
addMessage('assistant', `✅ ${stageName} 完成! (耗时: ${elapsedStr})`);
|
||
|
||
// 更新阶段结果 - 如果有分stage的文件,则更新所有stage的文件
|
||
if (stageIdx >= 0 && data.result) {
|
||
const stageFiles = data.result.stage_files;
|
||
if (stageFiles) {
|
||
// 按stage分类更新各stage的文件
|
||
stages.forEach((s, idx) => {
|
||
if (stageFiles[s.name]) {
|
||
stages[idx].images = stageFiles[s.name].images || [];
|
||
stages[idx].files = stageFiles[s.name].files || [];
|
||
if (stages[idx].images.length > 0 || stages[idx].files.length > 0) {
|
||
stages[idx].status = 'success';
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
// 兼容旧版本
|
||
stages[stageIdx].images = data.result.images || [];
|
||
stages[stageIdx].files = data.result.files || [];
|
||
}
|
||
}
|
||
|
||
refreshResults();
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTask.value = null;
|
||
currentTaskId.value = null;
|
||
|
||
// 显示结果对话框
|
||
resultDialogParams.value = {
|
||
stage: stageName,
|
||
prefix: confirmParams.prefix,
|
||
cpu: confirmParams.cpu,
|
||
elapsed: elapsedStr
|
||
};
|
||
resultDialogVisible.value = true;
|
||
|
||
} else if (data.status === 'failed') {
|
||
// 任务失败
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
}
|
||
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
const errMsg = data.result?.error || data.message || '未知错误';
|
||
addMessage('assistant', `❌ 错误: ${errMsg}`);
|
||
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'failed';
|
||
}
|
||
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTask.value = null;
|
||
currentTaskId.value = null;
|
||
}
|
||
// 其他状态(pending/running)继续轮询
|
||
|
||
} catch (err) {
|
||
console.error('轮询任务状态失败:', err);
|
||
}
|
||
};
|
||
|
||
// Stop Execution任务
|
||
const stopExecution = async () => {
|
||
if (!currentTask.value && !currentTaskId.value) {
|
||
ElMessage.warning('没有正在执行的任务');
|
||
return;
|
||
}
|
||
|
||
const taskId = currentTask.value?.id || currentTaskId.value;
|
||
if (!taskId) {
|
||
ElMessage.warning('无法获取任务ID');
|
||
return;
|
||
}
|
||
|
||
isStopping.value = true;
|
||
|
||
try {
|
||
const response = await fetch(`/api/agent/cancel/${taskId}`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
ElMessage.success('任务已Stop');
|
||
addMessage('assistant', '⏹ 任务已Stop');
|
||
|
||
// 清理状态
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
}
|
||
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
// 更新阶段状态
|
||
if (currentTask.value?.stage) {
|
||
const stageIdx = stages.findIndex(s => s.name === currentTask.value.stage);
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'failed';
|
||
}
|
||
}
|
||
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTask.value = null;
|
||
currentTaskId.value = null;
|
||
} else {
|
||
ElMessage.error(data.message || 'Stop任务失败');
|
||
}
|
||
} catch (err) {
|
||
console.error('Stop任务失败:', err);
|
||
ElMessage.error('Stop任务失败: ' + err.message);
|
||
} finally {
|
||
isStopping.value = false;
|
||
}
|
||
};
|
||
|
||
const doExecute = async () => {
|
||
console.log('[DEBUG] doExecute called, closing dialog');
|
||
confirmDialogVisible.value = false;
|
||
clearAutoExecuteTimer(); // 清除自动执行倒计时
|
||
console.log('[DEBUG] doExecute: confirmDialogVisible =', confirmDialogVisible.value);
|
||
executing.value = true;
|
||
isExecuting.value = true;
|
||
executeStartTime = Date.now();
|
||
executeElapsed.value = '0.0';
|
||
|
||
// 启动计时器,每0.1s更新
|
||
executeTimer = setInterval(() => {
|
||
if (executeStartTime) {
|
||
const elapsed = (Date.now() - executeStartTime) / 1000;
|
||
executeElapsed.value = elapsed.toFixed(1);
|
||
}
|
||
}, 100);
|
||
|
||
addMessage('assistant', `⚙️ 执行阶段: ${confirmParams.stage}...`);
|
||
|
||
// RNA-seq 分步骤执行(现在统一使用 /api/execute)
|
||
// 注意:rnaseq_qc 已经通过 /api/execute 支持
|
||
// rnaseq_align/quant/de/enrich 现在也使用 /api/execute
|
||
|
||
// 更新阶段状态为运行中
|
||
const stageIdx = stages.findIndex(s => s.name === confirmParams.stage);
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'running';
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/execute', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
stage: confirmParams.stage,
|
||
job_id: jobInfo.job_id, // 添加当前job_id
|
||
prefix: confirmParams.prefix,
|
||
cpu: confirmParams.cpu,
|
||
idEvalue: confirmParams.idEvalue,
|
||
idCoverage: confirmParams.idCoverage,
|
||
blastEvalue: confirmParams.blastEvalue,
|
||
requiredPfams: confirmParams.requiredPfams,
|
||
refSpecies: confirmParams.refSpecies,
|
||
mafftParams: confirmParams.mafftParams,
|
||
iqtreeParams: confirmParams.iqtreeParams,
|
||
nmotifs: confirmParams.nmotifs,
|
||
minw: confirmParams.minw,
|
||
maxw: confirmParams.maxw,
|
||
domEvalue: confirmParams.domEvalue,
|
||
domCoverage: confirmParams.domCoverage,
|
||
idTransform: confirmParams.idTransform,
|
||
geoId: confirmParams.geoId,
|
||
// RNA-seq参数
|
||
rnaseqMode: confirmParams.rnaseqMode,
|
||
speciesName: confirmParams.speciesName,
|
||
// RNA-seq QC 参数
|
||
rnaseqDataMode: confirmParams.rnaseqDataMode,
|
||
rnaseqDataPath: confirmParams.rnaseqDataPath,
|
||
// PPI 参数
|
||
ppiSpecies: confirmParams.ppiSpecies,
|
||
ppiScore: confirmParams.ppiScore,
|
||
// KaKs 参数
|
||
kaksNeutralRate: confirmParams.kaksNeutralRate,
|
||
// Genome Prepare 参数
|
||
genomePrepareType: confirmParams.genomePrepareType
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
// 立即失败
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
const errMsg = data.error || '未知错误';
|
||
addMessage('assistant', `❌ 错误: ${errMsg}`);
|
||
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'failed';
|
||
}
|
||
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTaskId.value = null;
|
||
return;
|
||
}
|
||
|
||
// 检查是否是异步任务
|
||
if (data.async) {
|
||
// 异步任务 - 启动轮询
|
||
currentTask.value = {
|
||
id: data.task_id,
|
||
stage: confirmParams.stage
|
||
};
|
||
currentTaskId.value = data.task_id;
|
||
|
||
addMessage('assistant', `⏳ ${data.message || '任务已提交,正在后台执行...'}`);
|
||
|
||
// 立即查询一次状态
|
||
await pollTaskStatus(data.task_id, confirmParams.stage);
|
||
|
||
// 启动轮询定时器(每5s查询一次)
|
||
pollTimer = setInterval(() => {
|
||
pollTaskStatus(data.task_id, confirmParams.stage);
|
||
}, 5000);
|
||
|
||
} else {
|
||
// 同步任务 - 直接处理结果
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
const elapsed = executeStartTime ? Math.round((Date.now() - executeStartTime) / 1000) : 0;
|
||
const elapsedStr = elapsed > 60 ? `${Math.floor(elapsed/60)}分${elapsed%60}s` : `${elapsed}s`;
|
||
|
||
resultDialogParams.value = {
|
||
stage: confirmParams.stage,
|
||
prefix: confirmParams.prefix,
|
||
cpu: confirmParams.cpu,
|
||
elapsed: elapsedStr
|
||
};
|
||
|
||
addMessage('assistant', `✅ ${confirmParams.stage} 完成! (耗时: ${elapsedStr})`);
|
||
logContent.value = data.log || '';
|
||
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'success';
|
||
stages[stageIdx].images = data.images || [];
|
||
stages[stageIdx].files = data.files || [];
|
||
}
|
||
|
||
refreshResults();
|
||
resultDialogVisible.value = true;
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTaskId.value = null;
|
||
}
|
||
|
||
} catch (err) {
|
||
addMessage('assistant', `❌ 请求失败: ${err.message}`);
|
||
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
|
||
if (stageIdx >= 0) {
|
||
stages[stageIdx].status = 'failed';
|
||
}
|
||
|
||
executing.value = false;
|
||
isExecuting.value = false;
|
||
currentTaskId.value = null;
|
||
}
|
||
};
|
||
|
||
// 上传成功
|
||
const handleUploadSuccess = (response) => {
|
||
ElMessage.success('文件上传成功');
|
||
refreshInfo();
|
||
};
|
||
const handleUploadError = (err) => {
|
||
ElMessage.error('上传失败: ' + err.message);
|
||
};
|
||
|
||
// Download Examples文件
|
||
const handleDownloadExample = async (type) => {
|
||
const examples = {
|
||
samples: `# RNA-seq 完整分析配置
|
||
# 结合 QC 和 DE 阶段的配置
|
||
|
||
# QC阶段:样本路径配置
|
||
samples:
|
||
SRR6847695:
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847695/SRR6847695_1.fastq
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847695/SRR6847695_2.fastq
|
||
SRR6847696:
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847696/SRR6847696_1.fastq
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847696/SRR6847696_2.fastq
|
||
SRR6847697:
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847697/SRR6847697_1.fastq
|
||
- /home/xia-lab/wutx/00test/bbav2/rnaseq/SRR6847697/SRR6847697_2.fastq
|
||
|
||
# DE阶段:分组和比较配置
|
||
groups:
|
||
SRR6847695: treat
|
||
SRR6847696: treat
|
||
SRR6847697: control
|
||
|
||
compare:
|
||
- ["treat", "control"]`,
|
||
geneID: `Ft.SPL.00001
|
||
Ft.SPL.00002
|
||
Ft.SPL.00003`,
|
||
sample: `X2 X2 Control
|
||
X3 X3 Treatment`,
|
||
fpkm: `geneID X2 X3
|
||
Ft.SPL.00001 10.5 2.3
|
||
Ft.SPL.00002 8.2 1.9
|
||
Ft.SPL.00003 15.3 3.1`,
|
||
annotation_config: `# 功能注释技能配置 (API模式)
|
||
# 使用: python skills/annotation/run_api.py --input protein.fa --output ./annotation --config annotation_config.yaml
|
||
|
||
skill:
|
||
name: annotation-api
|
||
version: "1.0.0"
|
||
description: "功能注释分析 - eggNOG API(在线模式)"
|
||
|
||
input:
|
||
protein_fasta: null # 蛋白序列FASTAFile Path
|
||
|
||
parameters:
|
||
api:
|
||
delay: 1.0 # API请求间隔(s),建议1.0-2.0避免服务器限流
|
||
timeout: 60 # 请求超时(s)
|
||
db: "euk" # 数据库类型: euk(真核)/bact(细菌)/arch(古菌)/viruses
|
||
min_seq_length: 10 # 最小序列长度
|
||
max_seq_length: 50000 # 最大序列长度
|
||
|
||
output:
|
||
format: ["gene2go"] # Output格式: gene2go
|
||
include_reproducibility: true # 包含可复现性文件
|
||
|
||
dependencies:
|
||
conda:
|
||
channels:
|
||
- conda-forge
|
||
packages:
|
||
- python=3.9
|
||
- requests=2.31.0
|
||
- pyyaml=6.0.1`
|
||
};
|
||
const content = examples[type];
|
||
if (!content) return;
|
||
|
||
// 创建Download
|
||
const blob = new Blob([content], { type: 'text/plain' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `${type}.yaml`;
|
||
if (type === 'geneID') a.download = 'geneID';
|
||
if (type === 'sample') a.download = 'sample.txt';
|
||
if (type === 'fpkm') a.download = 'expression.fpkm.txt';
|
||
if (type === 'annotation_config') a.download = 'annotation_config.yaml';
|
||
if (type === 'samples') a.download = 'samples.yaml';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
ElMessage.success('示例文件已Download:' + a.download);
|
||
};
|
||
|
||
// 从路径加载
|
||
const loadFromPath = async () => {
|
||
if (!inputPath.value) {
|
||
ElMessage.warning('请InputFile Path');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/load_from_path', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
job_id: jobInfo.job_id,
|
||
path: inputPath.value
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
ElMessage.success(`已加载 ${data.count} files`);
|
||
// Clear路径Input
|
||
inputPath.value = '';
|
||
} else {
|
||
ElMessage.error(data.message || '加载失败');
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error(`加载失败: ${e.message}`);
|
||
}
|
||
};
|
||
|
||
// Agent Mode Methods
|
||
const onModelChange = (model) => {
|
||
const names = {
|
||
kimi: '🧠 Kimi',
|
||
glm: '🔬 GLM-4',
|
||
minimax: '⚡ MiniMax',
|
||
llama3: '🦙 Llama3',
|
||
qwen2: '🤖 Qwen2',
|
||
ollama: '📍 Ollama'
|
||
};
|
||
ElMessage.success(`Switched to ${names[model] || model}`);
|
||
};
|
||
|
||
const clearAgentSession = () => {
|
||
agentSession.messages = [];
|
||
agentSession.currentTask = null;
|
||
ElMessage.success('对话历史已Clear');
|
||
};
|
||
|
||
const getTaskTypeColor = (type) => {
|
||
const colors = { knowledge_query: 'info', complex_analysis: 'warning', code_generation: 'success' };
|
||
return colors[type] || 'info';
|
||
};
|
||
|
||
const getStageLabel = (stage) => {
|
||
const labels = {
|
||
identify: 'Gene Family Identification', phylogeny: 'Phylogenetic Analysis', motif: 'Motif Analysis',
|
||
domain: '结构域分析', structure: 'Gene Structure分析', promoter: '启动子分析',
|
||
synteny: 'Synteny Analysis', expression: '表达分析', pep: '蛋白理化性质',
|
||
ppi: 'Protein Interaction', kaks: 'Ka/Ks分析', combo: 'Combined Plot'
|
||
};
|
||
return labels[stage] || stage;
|
||
};
|
||
|
||
const formatMessage = (content) => {
|
||
return content
|
||
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
|
||
.replace(/\*(.+?)\*/g, '<i>$1</i>')
|
||
.replace(/`(.+?)`/g, '<code style="background: #f5f7fa; padding: 2px 4px; border-radius: 3px;">$1</code>')
|
||
.replace(/\n/g, '<br>');
|
||
};
|
||
|
||
// 添加Agent消息到会话
|
||
const addAgentMessage = (role, content, intent = null) => {
|
||
const now = new Date();
|
||
agentSession.messages.push({
|
||
role,
|
||
content,
|
||
time: now.toLocaleTimeString(),
|
||
intent
|
||
});
|
||
if (intent && intentHistory) {
|
||
intentHistory.value.push(intent);
|
||
}
|
||
};
|
||
|
||
// Clear错误日志
|
||
const clearErrorLog = () => {
|
||
logContent.value = '';
|
||
ElMessage.success('错误日志已Clear');
|
||
};
|
||
|
||
// 自动滚动
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
const chatArea = document.querySelector('.agent-chat-history');
|
||
if (chatArea) {
|
||
chatArea.scrollTop = chatArea.scrollHeight;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 降级到简单模式的辅助函数(定义在使用之前)
|
||
const fallbackToSimpleMode = (intent) => {
|
||
const stages = intent.stages || [intent.stage || 'identify'];
|
||
|
||
// 添加AI回复
|
||
addAgentMessage('assistant', intent.message || `我将帮您执行 ${stages.join(', ')} 分析。`, {
|
||
stage: stages[0],
|
||
stages: stages,
|
||
taskType: 'batch_analysis',
|
||
params: intent.params,
|
||
confidence: intent.confidence || 0.7
|
||
});
|
||
|
||
// 存储批量执行信息
|
||
batchExecuteStages.value = stages;
|
||
batchExecuteIndex.value = 0;
|
||
batchExecuteParams.value = {
|
||
...intent.params,
|
||
stages: stages
|
||
};
|
||
|
||
// 显示批量执行确认对话框
|
||
batchExecuteDialogVisible.value = true;
|
||
addAgentMessage('assistant', `⚙️ 准备批量执行 ${stages.length} 个阶段: ${stages.join(', ')}`);
|
||
|
||
// 启动15秒倒计时
|
||
startBatchExecuteTimer();
|
||
};
|
||
|
||
// 处理自然语言Input
|
||
const handleNlInput = async () => {
|
||
if (!nlInput.value.trim()) {
|
||
ElMessage.warning('请Input内容');
|
||
return;
|
||
}
|
||
|
||
const userInput = nlInput.value.trim();
|
||
nlLoading.value = true;
|
||
|
||
// V2: RAG模式 - 文献问答
|
||
if (ragMode.value) {
|
||
try {
|
||
addMessage('user', userInput);
|
||
ragHistory.value.push({ role: 'user', content: userInput, time: new Date().toLocaleTimeString() });
|
||
|
||
// 调用RAG API (gene-family-mas-v2 端口8000)
|
||
const response = await fetch('http://localhost:7867/api/v1/rag/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
query: userInput,
|
||
mode: 'substring',
|
||
top_k: 5
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
const answer = data.response || data.answer || '未找到相关信息';
|
||
|
||
addMessage('assistant', answer);
|
||
ragHistory.value.push({ role: 'assistant', content: answer, time: new Date().toLocaleTimeString() });
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => {
|
||
if (chatArea.value) {
|
||
chatArea.value.scrollTop = chatArea.value.scrollHeight;
|
||
}
|
||
}, 100);
|
||
|
||
} catch (err) {
|
||
ElMessage.error('RAG请求失败: ' + err.message);
|
||
addMessage('assistant', '抱歉,文献检索服务暂时不可用。');
|
||
} finally {
|
||
nlInput.value = '';
|
||
nlLoading.value = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Agent模式 - 技能执行
|
||
try {
|
||
// 添加用户消息到Agent会话
|
||
addAgentMessage('user', userInput);
|
||
|
||
const response = await fetch('/api/agent/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: userInput,
|
||
model: selectedModel.value,
|
||
session_id: sessionId.value,
|
||
job_id: jobInfo.job_id, // 传入当前 job_id
|
||
history: agentSession.messages.slice(-10) // 保留最近10条上下文
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.intent) {
|
||
// 更新自动检测的模式
|
||
autoDetectedMode.value = data.intent.taskType === 'faq' ? 'faq' : 'skill';
|
||
|
||
// 判断使用哪种模式:自动检测优先,但考虑用户偏好
|
||
const isFaqQuery = data.intent.taskType === 'faq' || !data.intent.stage;
|
||
const useRagMode = isFaqQuery || (preferenceMode.value && !data.intent.stage);
|
||
|
||
if (useRagMode) {
|
||
// FAQ查询或偏好为Literature:使用RAG回答
|
||
autoDetectedMode.value = 'faq';
|
||
|
||
// 调用RAG API获取回答
|
||
try {
|
||
const ragResponse = await fetch('http://localhost:8000/api/v1/rag/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
query: userInput,
|
||
mode: 'substring',
|
||
top_k: 5,
|
||
use_llm: true,
|
||
include_sources: true
|
||
})
|
||
});
|
||
|
||
const ragData = await ragResponse.json();
|
||
const answer = ragData.response || 'No relevant information found.';
|
||
|
||
addAgentMessage('assistant', answer, {
|
||
stage: null,
|
||
taskType: 'faq',
|
||
params: {},
|
||
confidence: data.intent.confidence
|
||
});
|
||
} catch (ragErr) {
|
||
// RAG失败,使用意图识别的消息
|
||
addAgentMessage('assistant', data.intent.message || 'Answer based on knowledge base.', {
|
||
stage: null,
|
||
taskType: 'faq',
|
||
params: {},
|
||
confidence: data.intent.confidence
|
||
});
|
||
}
|
||
console.log('[DEBUG] FAQ/Literature mode: displaying knowledge response');
|
||
} else {
|
||
// 分析任务:使用V2规划API获取详细执行计划
|
||
autoDetectedMode.value = 'skill';
|
||
|
||
// 调用规划API获取详细计划
|
||
console.log('[DEBUG] Calling /api/agent/plan for detailed planning...');
|
||
try {
|
||
const planResponse = await fetch('/api/agent/plan', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: userInput,
|
||
session_id: sessionId.value,
|
||
history: agentSession.messages.slice(-5)
|
||
})
|
||
});
|
||
|
||
const planData = await planResponse.json();
|
||
console.log('[DEBUG] Plan API response:', planData);
|
||
|
||
if (planData.success && planData.plan) {
|
||
const plan = planData.plan;
|
||
const stages = plan.stages || [];
|
||
const stagesDetail = plan.stages_detail || [];
|
||
|
||
// 显示智能规划结果
|
||
let planMessage = `📋 分析计划 (${plan.mode || 'v2'})\n\n`;
|
||
planMessage += `基因家族: ${plan.gene_family}\n`;
|
||
planMessage += `物种: ${plan.species}\n`;
|
||
planMessage += `预估时间: ${plan.estimated_time}\n`;
|
||
planMessage += `执行策略: ${plan.execution_strategy === 'parallel' ? '并行执行' : '串行执行'}\n\n`;
|
||
planMessage += `分析阶段 (${plan.total_stages}个):\n`;
|
||
|
||
stagesDetail.forEach((stage, idx) => {
|
||
const deps = stage.dependencies?.length > 0
|
||
? `(依赖: ${stage.dependencies.join(', ')})`
|
||
: '';
|
||
const parallel = stage.can_parallel ? ' [可并行]' : '';
|
||
planMessage += `${idx + 1}. ${stage.name} ${deps}${parallel}\n`;
|
||
});
|
||
|
||
addAgentMessage('assistant', planMessage, {
|
||
stage: stages[0],
|
||
stages: stages,
|
||
taskType: 'batch_analysis',
|
||
params: plan.params,
|
||
plan: plan,
|
||
confidence: planData.intent?.confidence || 0.9
|
||
});
|
||
|
||
// 存储批量执行信息
|
||
batchExecuteStages.value = stages;
|
||
batchExecuteIndex.value = 0;
|
||
batchExecuteParams.value = {
|
||
...plan.params,
|
||
stages: stages,
|
||
plan: plan
|
||
};
|
||
|
||
// 显示批量执行确认对话框
|
||
batchExecuteDialogVisible.value = true;
|
||
addAgentMessage('assistant', `⚙️ 准备执行 ${stages.length} 个分析阶段,请确认参数`);
|
||
|
||
// 启动15秒倒计时
|
||
startBatchExecuteTimer();
|
||
return;
|
||
} else {
|
||
// 规划失败,降级到简单模式
|
||
console.log('[DEBUG] Plan API failed, falling back to simple mode');
|
||
fallbackToSimpleMode(data.intent);
|
||
}
|
||
} catch (planErr) {
|
||
console.error('[DEBUG] Plan API error:', planErr);
|
||
// 降级到简单模式
|
||
fallbackToSimpleMode(data.intent);
|
||
}
|
||
}
|
||
} else {
|
||
addAgentMessage('assistant', data.message || '抱歉,我没有理解你的意思。请尝试更详细地描述。');
|
||
}
|
||
} catch (err) {
|
||
ElMessage.error('请求失败: ' + err.message);
|
||
addAgentMessage('assistant', '抱歉,服务暂时不可用,请稍后重试。');
|
||
} finally {
|
||
nlInput.value = '';
|
||
nlLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// Refresh结果
|
||
const refreshResults = async () => {
|
||
try {
|
||
const response = await fetch('/api/results');
|
||
const data = await response.json();
|
||
stages.forEach(stage => {
|
||
stage.images = data[stage.name]?.images || [];
|
||
stage.files = data[stage.name]?.files || [];
|
||
stage.elapsed_time = data[stage.name]?.elapsed_time || 0;
|
||
if (stage.images.length > 0) stage.status = 'success';
|
||
});
|
||
// 同时RefreshMCScanX HTML文件列表
|
||
await refreshMcscanxHtml();
|
||
} catch (err) { console.error(err); }
|
||
};
|
||
|
||
// 打开图片预览
|
||
const openPreview = (img) => {
|
||
previewImg.value = img;
|
||
previewDialogVisible.value = true;
|
||
};
|
||
|
||
// 重新绘图
|
||
const redrawImage = () => {
|
||
ElMessage.info('重新绘图功能开发中...');
|
||
};
|
||
|
||
// 组件卸载时清理定时器
|
||
const cleanup = () => {
|
||
if (executeTimer) {
|
||
clearInterval(executeTimer);
|
||
executeTimer = null;
|
||
}
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
refreshInfo();
|
||
refreshResults();
|
||
fetchAgentTasks();
|
||
startAgentTaskPolling();
|
||
});
|
||
|
||
// 使用 Vue 3 的 onUnmounted
|
||
const { onUnmounted } = Vue;
|
||
onUnmounted(() => {
|
||
cleanup();
|
||
stopAgentTaskPolling();
|
||
});
|
||
|
||
return {
|
||
copyPath,
|
||
nlInput,
|
||
nlLoading,
|
||
handleNlInput,
|
||
// Agent Mode
|
||
selectedModel,
|
||
modelDisplayName,
|
||
agentSession,
|
||
onModelChange,
|
||
clearAgentSession,
|
||
getTaskTypeColor,
|
||
getStageLabel,
|
||
formatMessage,
|
||
addAgentMessage,
|
||
selectedStage,
|
||
params,
|
||
inputPath,
|
||
jobInfo,
|
||
messages,
|
||
logContent,
|
||
chatArea,
|
||
confirmDialogVisible,
|
||
confirmParams,
|
||
executing,
|
||
executeElapsed,
|
||
autoExecute,
|
||
autoExecuteCountdown,
|
||
clearAutoExecuteTimer,
|
||
// 批量执行
|
||
batchExecuteDialogVisible,
|
||
batchExecuteStages,
|
||
batchExecuteIndex,
|
||
batchExecuteCountdown,
|
||
isBatchExecuting,
|
||
shouldStopBatchExecute,
|
||
currentBatchStage,
|
||
startBatchExecute,
|
||
cancelBatchExecute,
|
||
stopBatchExecute,
|
||
// 依赖驱动并行执行
|
||
stageDependencies,
|
||
stageExecutionStatus,
|
||
runningStages,
|
||
maxParallelStages,
|
||
// 阶段确认对话框
|
||
stageConfirmDialogVisible,
|
||
stageConfirmCountdown,
|
||
stageConfirmCurrent,
|
||
stageConfirmTotal,
|
||
stageConfirmQueue,
|
||
processStageConfirmQueue,
|
||
confirmStageExecute,
|
||
skipStageExecute,
|
||
executeStagesWithDependencies,
|
||
executeStageWithConfirmation,
|
||
stages,
|
||
// 异步任务轮询
|
||
currentTask,
|
||
// Stop Execution相关
|
||
isExecuting,
|
||
isStopping,
|
||
currentTaskId,
|
||
stopExecution,
|
||
// 新版结果区域变量
|
||
activeStages,
|
||
selectedResultStage,
|
||
selectedFile,
|
||
selectedImage,
|
||
selectedImageUrl,
|
||
selectedImageSize,
|
||
zoomLevel,
|
||
previewContentRef,
|
||
// 新版结果区域方法
|
||
isImageFile,
|
||
selectFile,
|
||
zoomIn,
|
||
zoomOut,
|
||
resetZoom,
|
||
toggleFullscreen,
|
||
downloadImage,
|
||
onImageLoad,
|
||
onImageError,
|
||
onZoomChange,
|
||
getFileName,
|
||
selectImageFile,
|
||
// MCScanX HTML结果
|
||
mcscanxHtmlFiles,
|
||
openMcscanxHtml,
|
||
// Agent Tasks
|
||
agentTasks,
|
||
intentHistory,
|
||
uploadedFiles,
|
||
formatTime,
|
||
showTaskDetail,
|
||
// 原有
|
||
previewDialogVisible,
|
||
previewImg,
|
||
previewParams,
|
||
resultDialogVisible,
|
||
resultDialogParams,
|
||
formatSize,
|
||
onStageChange,
|
||
showConfirm,
|
||
doExecute,
|
||
handleUploadSuccess,
|
||
handleUploadError,
|
||
handleDownloadExample,
|
||
loadFromPath,
|
||
refreshResults,
|
||
openPreview,
|
||
redrawImage,
|
||
createNewJob,
|
||
// Working DirectorySelect
|
||
existingJobs,
|
||
jobSelectorVisible,
|
||
selectedJobId,
|
||
showJobSelector,
|
||
selectJob,
|
||
// V2: RAG相关
|
||
ragMode,
|
||
userId,
|
||
sessionId,
|
||
ragHistory,
|
||
toggleRagMode,
|
||
clearRagSession,
|
||
// 偏好模式和自动检测
|
||
preferenceMode,
|
||
autoDetectedMode,
|
||
togglePreferenceMode,
|
||
// 错误日志
|
||
clearErrorLog,
|
||
// 会话管理
|
||
apiBaseUrl,
|
||
currentConversationId,
|
||
conversations,
|
||
filteredConversations,
|
||
createNewConversation,
|
||
selectConversation,
|
||
// 新会话选择器
|
||
recentConversations,
|
||
moreConversations
|
||
};
|
||
}
|
||
});
|
||
|
||
// 注册Agent状态组件
|
||
app.component('agent-status-component', AgentStatusComponent);
|
||
|
||
// 注册ConversationSidebar组件
|
||
if (typeof ConversationSidebar !== 'undefined') {
|
||
app.component('conversation-sidebar', ConversationSidebar);
|
||
console.log('ConversationSidebar registered successfully');
|
||
} else {
|
||
console.warn('ConversationSidebar not found, using fallback');
|
||
// 注册一个空的占位组件
|
||
app.component('conversation-sidebar', {
|
||
template: '<div style="width: 60px; background: #202123; height: 100%;"></div>'
|
||
});
|
||
}
|
||
|
||
// 将 jobInfo 暴露给 Agent 组件使用
|
||
window.parentJobInfo = null;
|
||
|
||
app.use(ElementPlus);
|
||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||
app.component(key, component);
|
||
}
|
||
app.mount('#app');
|
||
|
||
// 隐藏加载提示,显示应用
|
||
document.getElementById('loading-msg').style.display = 'none';
|
||
document.getElementById('app').style.display = 'block';
|
||
console.log('App mounted successfully');
|
||
</script>
|
||
</body>
|
||
</html>
|