Files
PlantAI-dev/apps/web/vue3/index.html.backup.1776015080
2026-08-03 18:42:47 +08:00

4090 lines
203 KiB
Plaintext
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="conversation-sidebar-container">
<!-- 简化版侧边栏 - 直接内联 -->
<div class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
<button class="new-chat-btn" @click="createNewConversation">
<el-icon><plus /></el-icon>
<span v-if="!sidebarCollapsed">New Chat</span>
</button>
<button class="toggle-btn" @click="sidebarCollapsed = !sidebarCollapsed">
<el-icon><arrow-left v-if="!sidebarCollapsed" /><arrow-right v-else /></el-icon>
</button>
</div>
<div class="search-box" v-if="!sidebarCollapsed">
<el-input v-model="conversationSearch" placeholder="搜索会话..." prefix-icon="search" clearable size="small" />
</div>
<div class="conversation-list" v-if="!sidebarCollapsed">
<div class="list-section">
<div class="section-title">会话列表</div>
<div v-for="conv in filteredConversations" :key="conv.conversation_id"
class="conversation-item" :class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)">
<div class="item-icon"><el-icon><chat-dot-round /></el-icon></div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}条消息</span>
</div>
</div>
</div>
</div>
</div>
<div class="sidebar-footer" v-if="!sidebarCollapsed">
<div class="user-info">
<el-avatar :size="32" icon="user-filled" />
<span class="username">{{ userId }}</span>
</div>
</div>
</div>
</div>
<!-- 左侧:控制区 -->
<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 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>
&nbsp;&nbsp;• 参考Genome (*.fa/*.fasta)<br>
&nbsp;&nbsp;• 基因注释 (*.gtf/*.gff)<br>
&nbsp;&nbsp;• 样本配置文件 (samples.yaml)<br>
&nbsp;&nbsp;• 原始测序文件 (*_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/&lt;job&gt;/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() {
// 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';
});
// 会话列表过滤
const filteredConversations = computed(() => {
if (!conversationSearch.value) return conversations.value;
const query = conversationSearch.value.toLowerCase();
return conversations.value.filter(conv =>
conv.title.toLowerCase().includes(query) ||
(conv.gene_family && conv.gene_family.toLowerCase().includes(query))
);
});
// 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:8000/api'); // API基础URL
const currentConversation = ref(null); // 当前会话
const sidebarCollapsed = ref(false); // 侧边栏折叠状态
const conversationSearch = ref(''); // 会话搜索
const currentConversationId = ref(null); // 当前会话ID
// 会话列表(模拟数据)
const conversations = ref([
{
conversation_id: 'conv_001',
title: 'Ft.SPL基因家族分析',
gene_family: 'SPL',
message_count: 12,
last_message: '系统发育树已生成',
updated_at: new Date().toISOString()
},
{
conversation_id: 'conv_002',
title: 'Os.TCP进化分析',
gene_family: 'TCP',
message_count: 5,
last_message: '正在构建进化树...',
updated_at: new Date(Date.now() - 3600000).toISOString()
},
{
conversation_id: 'conv_003',
title: 'HMMER参数配置',
message_count: 8,
last_message: 'E-value建议1e-5',
updated_at: new Date(Date.now() - 86400000).toISOString()
}
]);
// 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 createNewConversation = () => {
const newConv = {
conversation_id: 'conv_' + Date.now(),
title: '新会话 ' + new Date().toLocaleTimeString(),
gene_family: '',
message_count: 0,
last_message: '',
updated_at: new Date().toISOString()
};
conversations.value.unshift(newConv);
currentConversationId.value = newConv.conversation_id;
ElMessage.success('新会话已创建');
};
// 选择会话
const selectConversation = (conv) => {
currentConversationId.value = conv.conversation_id;
ElMessage.info('切换到: ' + conv.title);
};
// 加载会话消息
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 = 15;
clearBatchExecuteTimer();
batchExecuteTimer = setInterval(() => {
batchExecuteCountdown.value--;
if (batchExecuteCountdown.value <= 0) {
clearBatchExecuteTimer();
if (batchExecuteDialogVisible.value && !isBatchExecuting.value) {
console.log('[DEBUG] Auto starting batch execute after 15s');
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:8000/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,
currentConversation,
sidebarCollapsed,
conversationSearch,
currentConversationId,
conversations,
filteredConversations,
createNewConversation,
selectConversation,
onSelectConversation,
onNewConversation,
onSidebarToggle
};
}
});
// 注册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>