Files

6499 lines
327 KiB
Plaintext
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PlantBioAgents - Plant Bioinformation Analysis Platform</title>
<!-- Element Plus CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.3.14/dist/index.css">
<link rel="stylesheet" href="/static/conversation-sidebar.css">
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>
<!-- Element Plus -->
<script src="https://unpkg.com/element-plus@2.3.14/dist/index.full.js"></script>
<!-- Element Plus Icons -->
<script src="https://unpkg.com/@element-plus/icons-vue@2.1.0/dist/index.iife.js"></script>
<!-- Conversation Sidebar -->
<script src="/static/conversation-sidebar.js"></script>
<style>
/* ========== Global Border Radius Override - Square Style ========== */
/* Override Element Plus default border radius */
:root {
--el-border-radius-base: 0px;
--el-border-radius-small: 0px;
--el-border-radius-round: 0px;
}
.el-button { border-radius: 0px !important; }
.el-input__wrapper { border-radius: 0px !important; }
.el-input__inner { border-radius: 0px !important; }
.el-select .el-input__wrapper { border-radius: 0px !important; }
.el-dialog { border-radius: 0px !important; }
.el-message-box { border-radius: 0px !important; }
.el-card { border-radius: 0px !important; }
.el-tag { border-radius: 0px !important; }
.el-badge__content { border-radius: 0px !important; }
.el-dropdown-menu { border-radius: 0px !important; }
.el-popover { border-radius: 0px !important; }
.el-tooltip__popper { border-radius: 0px !important; }
.el-collapse-item__header { border-radius: 0px !important; }
.el-collapse-item__wrap { border-radius: 0px !important; }
.el-tabs--border-card { border-radius: 0px !important; }
.el-tabs__nav { border-radius: 0px !important; }
.el-switch__core { border-radius: 0px !important; }
.el-slider__runway { border-radius: 0px !important; }
.el-slider__bar { border-radius: 0px !important; }
.el-slider__button { border-radius: 0px !important; }
.el-checkbox__inner { border-radius: 0px !important; }
.el-radio__inner { border-radius: 0px !important; }
.el-textarea__inner { border-radius: 0px !important; }
.el-table { border-radius: 0px !important; }
.el-table__header-wrapper { border-radius: 0px !important; }
.el-pagination .btn-prev, .el-pagination .btn-next { border-radius: 0px !important; }
.el-pagination .el-pager li { border-radius: 0px !important; }
.el-upload--picture-card { border-radius: 0px !important; }
.el-upload-list__item { border-radius: 0px !important; }
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: 0px; 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: 0px; 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: 0px;
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: 0px; padding: 10px; min-height: 120px; resize: vertical; max-height: 400px; }
.chat-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 0px; 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: 0px;
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: 0px; }
.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: 0px; 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: 0px; }
/* 新版结果区域样式 */
.result-file-section { max-height: 250px; overflow-y: auto; margin-bottom: 10px; border: 1px solid #eee; border-radius: 0px; }
.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: 0px; 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: 0px;
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: 0px;
}
.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: 0px;
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;
}
/* ========== RAG Agent Status Styles ========== */
.agent-status-panel .status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.agent-status-panel .status-badge {
padding: 4px 12px;
border-radius: 0px;
font-size: 12px;
}
.agent-status-panel .status-badge.idle { background: #909399; color: white; }
.agent-status-panel .status-badge.analyzing { background: #409EFF; color: white; }
.agent-status-panel .status-badge.retrieving { background: #E6A23C; color: white; }
.agent-status-panel .status-badge.reflecting { background: #67C23A; color: white; }
.agent-status-panel .status-badge.generating { background: #8E44AD; color: white; }
.agent-status-panel .status-badge.completed { background: #67C23A; color: white; }
.agent-status-panel .status-badge.error { background: #F56C6C; color: white; }
.agent-status-panel .rag-status {
background: white;
padding: 15px;
border-radius: 0px;
margin-bottom: 15px;
}
.agent-status-panel .query-text {
font-weight: bold;
margin-bottom: 10px;
color: #303133;
}
.agent-status-panel .retrieval-steps {
display: flex;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
.agent-status-panel .step {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 12px;
border-radius: 0px;
background: #f0f0f0;
font-size: 12px;
}
.agent-status-panel .step.pending { opacity: 0.5; }
.agent-status-panel .step.running { background: #ecf5ff; color: #409EFF; }
.agent-status-panel .step.completed { background: #f0f9eb; color: #67C23A; }
.agent-status-panel .reflection-panel,
.agent-status-panel .multihop-panel {
margin-top: 15px;
padding: 10px;
background: #fdf6ec;
border-radius: 0px;
font-size: 12px;
}
.agent-status-panel .reflection-title,
.agent-status-panel .multihop-title {
font-weight: bold;
margin-bottom: 5px;
}
.agent-status-panel .kg-visualization {
background: white;
padding: 15px;
border-radius: 0px;
margin-bottom: 15px;
}
.agent-status-panel .kg-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.agent-status-panel .kg-chart {
height: 300px;
background: #f5f7fa;
border-radius: 0px;
}
.agent-status-panel .kg-stats {
margin-top: 10px;
font-size: 12px;
color: #666;
}
.agent-status-panel .results-panel {
background: white;
padding: 15px;
border-radius: 0px;
margin-bottom: 15px;
}
.agent-status-panel .result-item {
margin-bottom: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 0px;
}
.agent-status-panel .result-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 12px;
}
.agent-status-panel .result-doc { color: #409EFF; }
.agent-status-panel .result-score { color: #67C23A; }
.agent-status-panel .result-content {
font-size: 12px;
color: #666;
line-height: 1.5;
}
.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: 0px;
resize: none;
font-family: inherit;
}
.agent-status-panel .agent-input button {
padding: 8px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 0px;
cursor: pointer;
}
.agent-status-panel .agent-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* ========== 动态消息内容样式系统 ========== */
/* 基础消息气泡 - 自适应padding */
.agent-status-panel .message-bubble {
transition: all 0.2s ease;
}
/* 短内容:紧凑模式 */
.agent-status-panel .message-bubble.short-content {
padding: 6px 10px;
}
.agent-status-panel .message-bubble.short-content .message-text {
font-size: 13px;
line-height: 1.4;
}
/* 中等内容:标准模式 */
.agent-status-panel .message-bubble.medium-content {
padding: 10px 14px;
}
.agent-status-panel .message-bubble.medium-content .message-text {
font-size: 14px;
line-height: 1.5;
}
/* 结构化内容(RAG结果):紧凑分段 */
.agent-status-panel .message-bubble.structured-content {
padding: 8px 12px;
}
.agent-status-panel .message-bubble.structured-content .message-text {
font-size: 13px;
line-height: 1.4;
}
/* RAG结构化内容专用样式 */
.rag-content-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
}
.rag-header {
font-size: 12px;
color: #67C23A;
font-weight: 600;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.rag-section {
background: #f9f9f9;
border-left: 3px solid #4CAF50;
border-radius: 0 0px 0px 0;
padding: 8px 10px;
margin: 0;
}
.rag-section-title {
font-weight: 600;
color: #333;
font-size: 13px;
margin: 0 0 4px 0;
display: flex;
align-items: center;
gap: 6px;
}
.rag-section-title::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background: #4CAF50;
border-radius: 50%;
}
.rag-section-content {
font-size: 12px;
color: #555;
line-height: 1.4;
margin: 0;
}
.rag-section-content p {
margin: 0 0 4px 0;
}
.rag-section-content p:last-child {
margin-bottom: 0;
}
/* 标签区域紧凑布局 */
.rag-tags-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
align-items: center;
}
.rag-tag {
font-size: 10px;
padding: 2px 6px;
background: #e8f5e9;
color: #2e7d32;
border-radius: 3px;
border: 1px solid #c8e6c9;
white-space: nowrap;
}
.rag-source {
font-size: 10px;
color: #909399;
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
/* 引用来源链接 */
.rag-source-link {
color: #409EFF;
text-decoration: none;
cursor: pointer;
}
.rag-source-link:hover {
text-decoration: underline;
}
/* 底部提示信息 */
.rag-footer {
font-size: 11px;
color: #909399;
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed #e4e7ed;
display: flex;
align-items: center;
gap: 6px;
}
/* 长内容:展开/折叠支持 */
.agent-status-panel .message-bubble.long-content {
padding: 12px 16px;
}
.agent-status-panel .message-bubble.long-content .message-text {
font-size: 14px;
line-height: 1.6;
}
/* 消息间距动态调整 */
.agent-message {
transition: margin 0.2s ease;
}
.agent-message.compact {
margin-bottom: 8px;
}
.agent-message.normal {
margin-bottom: 12px;
}
.agent-message.spacious {
margin-bottom: 16px;
}
/* ========== RAG历史消息专用样式 ========== */
.rag-history-panel {
margin-top: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 0px;
max-height: 400px;
overflow-y: auto;
}
.rag-message {
border-radius: 0px;
transition: all 0.2s ease;
}
.rag-message.user {
background: #ecf5ff;
text-align: right;
padding: 6px 10px;
margin-bottom: 6px;
}
.rag-message.assistant {
background: white;
border: 1px solid #e4e7ed;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.rag-message.assistant.compact {
margin-bottom: 6px;
}
.rag-message.assistant.normal {
margin-bottom: 8px;
}
.rag-message.assistant.spacious {
margin-bottom: 12px;
}
/* ========== 执行日志/RAG消息专用样式 ========== */
.execution-log-item.assistant {
background: white;
border: 1px solid #e4e7ed;
border-radius: 0px;
padding: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.execution-log-item.assistant.compact {
margin-bottom: 6px;
padding: 6px;
}
.execution-log-item.assistant.normal {
margin-bottom: 8px;
padding: 8px;
}
.execution-log-item.assistant.spacious {
margin-bottom: 12px;
padding: 10px;
}
.execution-log-item.assistant .log-content.structured-content {
/* 结构化内容使用rag样式 */
}
.execution-log-item.assistant .log-content.short-content {
font-size: 13px;
}
.execution-log-item.assistant .log-content.medium-content {
font-size: 14px;
}
.execution-log-item.assistant .log-content.long-content {
font-size: 14px;
line-height: 1.6;
}
.rag-message-time {
font-size: 11px;
color: #999;
padding: 4px 8px;
}
.rag-user-content {
font-size: 13px;
color: #303133;
padding: 0 8px 6px 8px;
}
.rag-ai-content {
padding: 8px;
}
.rag-ai-content.short-content {
padding: 6px 8px;
}
.rag-ai-content.medium-content {
padding: 8px 10px;
}
.rag-ai-content.structured-content {
padding: 6px 8px;
}
.rag-ai-content.long-content {
padding: 10px 12px;
}
.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: 0px;
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: 0px;
overflow: hidden;
}
/* 执行日志条目 - Zoom Out宽度 */
.execution-log-item {
padding: 6px 10px;
margin-bottom: 6px;
background: white;
border-radius: 0px;
border-left: 3px solid #409EFF;
font-size: 12px;
line-height: 1.4;
max-width: 50%;
}
/* RAG模式下执行日志更宽 */
.execution-log-item.assistant {
max-width: 95%;
border-left: 3px solid #67C23A;
}
.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: 0px;
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">
<div style="display: flex; align-items: baseline; gap: 12px;">
<h1 style="margin: 0; font-size: 28px; font-weight: 600;">🌱 PlantBioAgents</h1>
<span style="font-size: 15px; opacity: 0.85; font-weight: 400;">A Multi-Agent Plant Bioinformation Analysis Platform integrating Agentic RetrievalAugmented Generation (RAG)</span>
</div>
</div>
<div class="main-container">
<!-- 左侧:控制区 -->
<div class="left-panel">
<!-- Analysis StageSelect -->
<div class="btn-section">
<h4>▶ Analysis Stage</h4>
<el-select v-model="selectedStage" placeholder="SelectAnalysis Stage" style="width:100%" @change="onStageChange">
<el-option label="===== 数据准备 =====" value="prepare_header" disabled></el-option>
<el-option label="Genome Prepare - Genome数据预处理" value="genome_prepare"></el-option>
<el-option label="===== 核心分析 =====" value="core_header" disabled></el-option>
<el-option label="Identify - Gene Family Identification" value="identify"></el-option>
<el-option label="Phylogeny - Phylogenetic Analysis" value="phylogeny"></el-option>
<el-option label="PEP - Protein Property Analysis" value="pep"></el-option>
<el-option label="Motif - Motif Analysis" value="motif"></el-option>
<el-option label="Domain - Protein Domain" value="domain"></el-option>
<el-option label="Structure - Gene Structure" value="structure"></el-option>
<el-option label="Promoter - Cis-acting Elements" value="promoter"></el-option>
<el-option label="Synteny - Synteny Analysis(Circos)" value="synteny"></el-option>
<el-option label="Karyotype - Karyotype Analysis" value="karyotype"></el-option>
<el-option label="Interspecies Synteny - 种间Synteny Analysis" value="interspecies_synteny"></el-option>
<el-option label="PPI - Protein Interaction" value="ppi"></el-option>
<el-option label="KaKs - KaKs Analysis" value="kaks"></el-option>
<el-option label="Expression (使用FPKM) - Differential Expression" value="expression"></el-option>
<el-option label="RNA-seq (Raw Data) - Complete Pipeline from FASTQ" value="rnaseq"></el-option>
<el-option label="Combo - Combined Plot(Tree+Motif+Structure+Domain)" value="combo"></el-option>
<el-option label="Run All - Complete Pipeline" value="all"></el-option>
<!-- RNA-seq拆分选项 -->
<el-option label="===== RNA-Seq 分析 =====" value="rnaseq_header" disabled></el-option>
<el-option label="RNA-Seq QC - Quality Control" value="rnaseq_qc"></el-option>
<el-option label="RNA-Seq Align - Alignment" value="rnaseq_align"></el-option>
<el-option label="RNA-Seq Quant - Quantification" value="rnaseq_quant"></el-option>
<el-option label="RNA-Seq DE - 差异表达" value="rnaseq_de"></el-option>
<el-option label="Annotation - 背景注释(生成gene2go/gene2ko)" value="annotation"></el-option>
<el-option label="RNA-Seq GO Enrich - GO富集分析" value="rnaseq_go_enrich"></el-option>
<el-option label="RNA-Seq KEGG Enrich - KEGG富集分析" value="rnaseq_kegg_enrich"></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;">
<!-- Prefix?????config.xlsx???????????? -->
<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: 0px; color: white; flex-shrink: 0;">
<!-- 标题行 -->
<div style="font-size: 18px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center;">
<el-icon style="margin-right: 8px; font-size: 20px;"><chat-dot-round /></el-icon>
Agent Assistant / Execution Log
</div>
<!-- 模型选择和模式切换行 -->
<div style="font-size: 13px; opacity: 0.95; display: flex; gap: 15px; align-items: center; justify-content: space-between;">
<div style="display: flex; gap: 12px; align-items: center;">
<!-- 模型Select器 -->
<el-select v-model="selectedModel" size="small" style="width: 140px;" popper-class="model-select-dropdown" @change="onModelChange">
<el-option key="kimi" label="🧠 Kimi (Default)" value="kimi"></el-option>
<el-option key="glm" label="🔬 GLM-4" value="glm"></el-option>
<el-option key="minimax" label="⚡ MiniMax" value="minimax"></el-option>
<el-option key="llama3" label="🦙 Llama3 (Local)" value="llama3"></el-option>
<el-option key="qwen2" label="🤖 Qwen2 (Local)" value="qwen2"></el-option>
<el-option key="ollama" label="📍 Ollama (Local)" value="ollama"></el-option>
</el-select>
<span v-if="agentSession.messages.length > 0">Rounds: {{ Math.floor(agentSession.messages.length / 2) }}</span>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<!-- 偏好模式切换按钮 -->
<span style="font-size: 11px; opacity: 0.8; margin-right: 5px;">Preference:</span>
<el-switch
v-model="preferenceMode"
active-text="📚 Literature"
inactive-text="🔧 Skill"
@change="togglePreferenceMode"
style="--el-switch-on-color: #67C23A; --el-switch-off-color: #409EFF;">
</el-switch>
<el-button v-if="agentSession.messages.length > 0" size="small" type="info" plain @click="clearAgentSession">
<el-icon><delete /></el-icon> Clear
</el-button>
</div>
</div>
</div>
<!-- 会话选择器 - 红框区域 -->
<div style="background: white; border: 1px solid #e4e7ed; border-radius: 0px; padding: 10px 15px; margin-bottom: 10px; display: flex; align-items: center; gap: 15px; flex-shrink: 0;">
<span style="font-size: 13px; color: #606266; white-space: nowrap;">💬 当前会话:</span>
<!-- 最近会话快捷按钮 -->
<div style="display: flex; gap: 8px; flex: 1; overflow-x: auto;">
<el-button
v-for="conv in recentConversations"
:key="conv.conversation_id"
:type="currentConversationId === conv.conversation_id ? 'primary' : 'default'"
size="small"
@click="selectConversation(conv)"
style="white-space: nowrap;">
{{ conv.title }}
<el-tag v-if="conv.gene_family" size="small" type="success" style="margin-left: 5px;">{{ conv.gene_family }}</el-tag>
</el-button>
</div>
<!-- 更多会话下拉框 -->
<el-dropdown @command="selectConversation" trigger="click">
<el-button type="default" size="small">
更多会话 <el-icon><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="conv in moreConversations"
:key="conv.conversation_id"
:command="conv">
<div style="display: flex; align-items: center; justify-content: space-between; width: 200px;">
<span>{{ conv.title }}</span>
<el-tag v-if="conv.gene_family" size="small" type="success">{{ conv.gene_family }}</el-tag>
</div>
</el-dropdown-item>
<el-dropdown-item divided @click="createNewConversation">
<el-icon><plus /></el-icon> 新建会话
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 新建会话按钮 -->
<el-button type="primary" size="small" @click="createNewConversation">
<el-icon><plus /></el-icon> 新建
</el-button>
</div>
<!-- 合并后的对话和执行日志区域 -->
<div class="agent-chat-history" ref="agentChatArea" style="flex: 1; overflow-y: auto; margin-bottom: 8px; border: 1px solid #e4e7ed; border-radius: 0px; 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, getMessageSpacingClass(msg.content)]"
style="animation: fadeIn 0.3s ease;">
<!-- 用户消息 - 动态样式 -->
<div v-if="msg.role === 'user'" :class="['agent-message', getMessageSpacingClass(msg.content)]" style="display: flex; justify-content: flex-end;">
<div :class="['message-bubble', getContentClass(msg.content)]" style="max-width: 80%; background: #409EFF; color: white; border-radius: 0px 12px 2px 12px; box-shadow: 0 2px 8px rgba(64,158,255,0.3);">
<div class="message-text">{{ msg.content }}</div>
<div style="font-size: 11px; opacity: 0.8; margin-top: 4px; 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 :class="['message-bubble', getContentClass(msg.content)]"
style="background: white; border-radius: 0px 12px 12px 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border: 1px solid #e4e7ed;">
<div class="message-text" style="color: #303133;" v-html="msg.isHtml ? msg.content : formatMessage(msg.content, true)"></div>
<div style="font-size: 11px; color: #909399; margin-top: 6px;">{{ msg.time }}</div>
</div>
<div v-if="msg.intent" class="intent-card" style="margin-top: 6px; padding: 8px 10px; background: #f0f9eb; border-radius: 0px; border-left: 3px solid #67C23A;">
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;">
<el-icon style="color: #67C23A;"><check /></el-icon>
<span style="font-weight: 600; color: #67C23A; font-size: 12px;">意图识别成功</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: 11px; color: #606266;">
<b>Analysis Stage:</b> {{ getStageLabel(msg.intent.stage) }}
</div>
</div>
</div>
</div>
</div>
<!-- 执行日志/RAG消息 - 动态样式 -->
<div v-for="(msg, idx) in messages" :key="'log_'+idx"
:class="['execution-log-item', msg.role, getMessageSpacingClass(msg.content)]">
<div class="log-time">{{ msg.time }}</div>
<!-- RAG AI消息使用动态格式化 -->
<div v-if="ragMode && msg.role === 'assistant'"
:class="['log-content', getContentClass(msg.content)]"
v-html="formatMessage(msg.content, true)"></div>
<!-- 其他消息保持原样 -->
<div v-else 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" class="rag-history-panel">
<div v-for="(msg, idx) in ragHistory.slice(-5)" :key="idx"
:class="['rag-message', msg.role, getMessageSpacingClass(msg.content)]">
<div class="rag-message-time">{{ msg.time }}</div>
<!-- 用户消息:简单显示 -->
<div v-if="msg.role === 'user'" class="rag-user-content">{{ msg.content }}</div>
<!-- AI消息:使用动态格式化 -->
<div v-else :class="['rag-ai-content', getContentClass(msg.content)]" v-html="formatMessage(msg.content, true)"></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: 0px;">
<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: 0px; 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: 0px; 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; margin-left: -10px; margin-right: -10px; border: 2px solid #dcdfe6; border-radius: 0px; padding: 10px; background: #f5f7fa; max-height: 300px; 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: 0px; 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">
<!-- Prefix?????config.xlsx???????????? -->
<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: 0px; 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: 0px; 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: 0px; 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: 0px; 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: 0px; 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: 0px; 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 class="param-row">
<el-select v-model="confirmParams.rnaseqForegroundMode" style="width:100%">
<template #prepend>前景基因</template>
<el-option label="自动判定(推荐):优先基因家族,缺失时回退全基因组" value="auto"></el-option>
<el-option label="基因家族 foreground:仅分析 final_members.id 中的家族成员" value="family"></el-option>
<el-option label="全基因组 foreground:使用所有差异表达基因" value="global"></el-option>
</el-select>
</div>
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 0px; 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="['rnaseq_enrich', 'rnaseq_go_enrich', 'rnaseq_kegg_enrich'].includes(confirmParams.stage)" class="param-group">
<h5>RNA-seq Enrich 参数 (Enrichment Analysis - Step 5)</h5>
<div class="param-row">
<el-select v-model="confirmParams.enrichType" style="width:100%">
<template #prepend>富集类型</template>
<el-option label="GO 富集" value="go"></el-option>
<el-option label="KEGG 富集" value="kegg"></el-option>
<el-option label="GO + KEGG" value="both"></el-option>
</el-select>
</div>
<div class="param-row">
<el-select v-model="confirmParams.rnaseqForegroundMode" style="width:100%">
<template #prepend>前景基因</template>
<el-option label="自动判定(推荐):优先 family_DE_genes,缺失时回退 DE_genes" value="auto"></el-option>
<el-option label="基因家族 foreground:使用 family_DE_genes" value="family"></el-option>
<el-option label="全基因组 foreground:使用全局 DE_genes" value="global"></el-option>
</el-select>
</div>
<div class="param-row" style="margin-top:10px">
<el-select v-model="confirmParams.enrichBackgroundSource" style="width:100%">
<template #prepend>背景来源</template>
<el-option label="自动选择:优先 gene2go,否则内置OrgDb" value="auto"></el-option>
<el-option label="使用已上传 gene2go 文件" value="gene2go"></el-option>
<el-option label="使用内置模式物种 OrgDb" value="orgdb"></el-option>
<el-option label="内置 eggNOG 自动注释 + 富集(后台长任务,数小时到半天)" value="annotation"></el-option>
</el-select>
</div>
<div class="param-row" style="margin-top:10px" v-if="confirmParams.enrichBackgroundSource !== 'gene2go' && confirmParams.enrichBackgroundSource !== 'annotation'">
<el-select v-model="confirmParams.enrichSpecies" placeholder="请选择模式物种" style="width:100%">
<template #prepend>OrgDb物种</template>
<el-option label="拟南芥 Arabidopsis (ath)" value="ath"></el-option>
<el-option label="人 Human (hsa)" value="hsa"></el-option>
<el-option label="小鼠 Mouse (mmu)" value="mmu"></el-option>
<el-option label="大鼠 Rat (rno)" value="rno"></el-option>
<el-option label="果蝇 Fly (dme)" value="dme"></el-option>
<el-option label="酵母 Yeast (sce)" value="sce"></el-option>
</el-select>
</div>
<div class="param-row" style="margin-top:10px" v-if="confirmParams.enrichBackgroundSource !== 'gene2go' && confirmParams.enrichBackgroundSource !== 'annotation'">
<el-input v-model="confirmParams.enrichKeyType" placeholder="auto" style="width:100%">
<template #prepend>keyType</template>
</el-input>
</div>
<div class="param-row" style="margin-top:10px" v-if="confirmParams.enrichType === 'kegg' || confirmParams.enrichType === 'both'">
<el-input v-model="confirmParams.keggOrganism" placeholder="如 ath / hsa / osa" style="width:100%">
<template #prepend>KEGG organism</template>
</el-input>
</div>
<div style="margin-top: 10px; padding: 10px; background: #fdf6ec; border-radius: 0px; font-size: 12px;" v-if="confirmParams.enrichBackgroundSource === 'annotation'">
<div style="color: #303133; margin-bottom: 8px;"><b>⚠️ 自动注释说明:</b></div>
<div style="color: #606266; line-height: 1.8;">
• 系统会先使用目标物种蛋白序列生成 gene2go 背景文件<br>
• 小型/中型蛋白组通常需要数小时;几百 MB 级别数据可能需要半天左右<br>
• gene2go 生成后会继续执行 RNA-seq 富集分析
</div>
</div>
<div style="margin-top: 10px; padding: 10px; background: #e6f7ff; border-radius: 0px; font-size: 12px;">
<div style="color: #303133; margin-bottom: 8px;"><b>✅ 自动使用上一步Output:</b></div>
<div style="color: #606266; line-height: 1.8;">
• 基因家族模式使用 family_DE_genes;全基因组模式使用 DE_genes<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: 0px; 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: 0px; 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: 0px; 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 v-if="confirmParams.stage === 'interspecies_synteny'" class="param-group">
<h5>Interspecies species selection</h5>
<div v-if="interspeciesSpeciesLoading" style="color:#909399; font-size:12px; padding: 8px 0;">Loading species from config.xlsx...</div>
<div v-else>
<div style="margin-bottom: 8px; color:#606266; font-size:12px;">Targets are selected by default. References prefer At/Ath; otherwise the first Ref=True species in config is selected.</div>
<div class="param-row" style="display:block; margin-bottom: 10px;">
<div style="font-weight:600; margin-bottom:6px;">Target species (Ref=False)</div>
<el-checkbox-group v-model="confirmParams.targetSpecies">
<el-checkbox v-for="sp in interspeciesSpeciesOptions.targets" :key="'target-' + sp" :label="sp">{{ sp }}</el-checkbox>
</el-checkbox-group>
<div v-if="!interspeciesSpeciesOptions.targets.length" style="color:#F56C6C; font-size:12px;">No target species found</div>
</div>
<div class="param-row" style="display:block;">
<div style="font-weight:600; margin-bottom:6px;">Reference species (Ref=True)</div>
<el-checkbox-group v-model="confirmParams.refSpeciesList">
<el-checkbox v-for="sp in interspeciesSpeciesOptions.refs" :key="'ref-' + sp" :label="sp">{{ sp }}</el-checkbox>
</el-checkbox-group>
<div v-if="!interspeciesSpeciesOptions.refs.length" style="color:#F56C6C; font-size:12px;">No reference species found</div>
</div>
<div style="margin-top:10px; padding:8px; background:#f6f8fa; color:#606266; font-size:12px;">
Current selection: targets {{ confirmParams.targetSpecies.join(', ') || 'none' }}; references {{ confirmParams.refSpeciesList.join(', ') || 'none' }}. Auto-run uses this selection after the countdown.
</div>
</div>
</div>
<div style="margin-top: 15px; padding: 10px; background: #f0f9eb; border-radius: 0px; 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: 0px;">
<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 || getStageLabel(stage.id)) : getStageLabel(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: 0px; 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 || getStageLabel(batchExecuteStages[batchExecuteIndex].id)) : getStageLabel(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="syntenyChoiceDialogVisible"
title="请选择共线性分析类型"
width="560px"
:close-on-click-modal="false">
<div style="display: grid; gap: 12px;">
<div
v-for="option in syntenyChoiceOptions"
:key="option.id"
style="padding: 14px; border: 1px solid #dcdfe6; border-radius: 6px; background: #fff; cursor: pointer;"
@click="confirmSyntenyChoice(option)">
<div style="font-weight: 600; color: #303133; margin-bottom: 6px;">{{ option.label }}</div>
<div style="font-size: 13px; color: #606266; line-height: 1.6;">{{ option.description }}</div>
<div style="margin-top: 8px;">
<el-tag
v-for="stage in option.stages"
:key="stage"
size="small"
style="margin-right: 6px;">
{{ getStageLabel(stage) }}
</el-tag>
</div>
</div>
</div>
<template #footer>
<el-button @click="cancelSyntenyChoice">取消</el-button>
</template>
</el-dialog>
<!-- 诊断修复确认对话框 -->
<el-dialog
v-model="diagnosisConfirmDialogVisible"
title="🔧 参数修复确认"
width="600px"
:close-on-click-modal="false">
<div class="param-dialog">
<div class="param-group">
<h5>⚠️ 检测到问题</h5>
<div style="padding: 10px; background: #fdf6ec; border-radius: 0px; margin-bottom: 15px;">
<div style="color: #E6A23C; font-weight: bold; margin-bottom: 8px;">
{{ diagnosisFixData.errorType }}
</div>
<div style="color: #606266; font-size: 14px;">
{{ diagnosisFixData.description }}
</div>
</div>
</div>
<div class="param-group">
<h5>💡 建议修复方案</h5>
<div style="padding: 10px; background: #f0f9ff; border-radius: 0px; margin-bottom: 15px;">
<div style="color: #409EFF; font-size: 14px; margin-bottom: 10px;">
{{ diagnosisFixData.reason }}
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<div style="flex: 1;">
<div style="font-size: 12px; color: #909399; margin-bottom: 4px;">当前参数</div>
<el-tag type="info" size="small">{{ diagnosisFixData.currentParamsDisplay }}</el-tag>
</div>
<div style="font-size: 20px; color: #409EFF;">→</div>
<div style="flex: 1;">
<div style="font-size: 12px; color: #909399; margin-bottom: 4px;">建议参数</div>
<el-tag type="success" size="small">{{ diagnosisFixData.suggestedParamsDisplay }}</el-tag>
</div>
</div>
</div>
</div>
<div style="text-align: center; padding: 10px; background: #f5f7fa; border-radius: 0px;">
<span style="color: #606266; font-size: 14px;">
⏱️ {{ diagnosisConfirmCountdown }}秒后自动应用修复方案
</span>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<span v-if="diagnosisConfirmCountdown > 0" style="color: #E6A23C; margin-right: 10px;">
⚡ {{ diagnosisConfirmCountdown }}秒后自动执行
</span>
<el-button @click="rejectDiagnosisFix">
❌ 跳过修复
</el-button>
<el-button type="primary" @click="confirmDiagnosisFix">
✅ 应用修复
</el-button>
</span>
</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}: ${getStageLabel(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: 0px;">
<div style="color: #67C23A; font-weight: bold;">
📋 当前阶段: {{ getStageLabel(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">
<!-- Prefix?????config.xlsx???????????? -->
<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: 0px; font-size: 12px; color: #606266;">
💡 基因ID将自动从 final/{{ confirmParams.prefix || "Multi.<GeneFamily>" }}.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: 0px; 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 v-if="selectedStage === 'rnaseq_de'" style="display: grid; gap: 10px;">
<div class="param-row">
<el-select v-model="confirmParams.rnaseqForegroundMode" style="width:100%">
<template #prepend>差异表达范围</template>
<el-option label="自动判定:有 final_members.id 时做基因家族,否则做全基因组" value="auto"></el-option>
<el-option label="基因家族差异表达:只输出 family_DE_genes" value="family"></el-option>
<el-option label="全基因组差异表达:输出全局 DE_genes" value="global"></el-option>
</el-select>
</div>
<div style="padding: 10px; background: #e6f7ff; border-radius: 0px; color: #606266; font-size: 12px; line-height: 1.8;">
当前阶段会复用 RNA-seq 定量输出和 samples.yaml。若选择基因家族差异表达,需要已有 final_members.id;若选择全基因组差异表达,不依赖基因家族成员列表。
</div>
</div>
<div v-else-if="['rnaseq_enrich', 'rnaseq_go_enrich', 'rnaseq_kegg_enrich'].includes(selectedStage)" style="display: grid; gap: 10px;">
<div class="param-row">
<el-select v-model="confirmParams.enrichType" style="width:100%">
<template #prepend>富集类型</template>
<el-option label="GO 富集" value="go"></el-option>
<el-option label="KEGG 富集" value="kegg"></el-option>
<el-option label="GO + KEGG" value="both"></el-option>
</el-select>
</div>
<div class="param-row">
<el-select v-model="confirmParams.rnaseqForegroundMode" style="width:100%">
<template #prepend>前景基因</template>
<el-option label="自动判定:优先 family_DE_genes,缺失时回退全局 DE_genes" value="auto"></el-option>
<el-option label="基因家族 foreground:使用 family_DE_genes" value="family"></el-option>
<el-option label="全基因组 foreground:使用全局 DE_genes" value="global"></el-option>
</el-select>
</div>
<div class="param-row">
<el-select v-model="confirmParams.enrichBackgroundSource" style="width:100%">
<template #prepend>背景来源</template>
<el-option label="自动选择:优先已上传背景,其次内置模式物种" value="auto"></el-option>
<el-option label="使用已上传 gene2go/gene2ko 背景文件" value="gene2go"></el-option>
<el-option label="使用内置模式物种 OrgDb(仅 GO)" value="orgdb"></el-option>
<el-option label="内置 eggNOG 自动注释 + 继续富集(长任务)" value="annotation"></el-option>
</el-select>
</div>
<div class="param-row" v-if="confirmParams.enrichBackgroundSource !== 'gene2go' && confirmParams.enrichBackgroundSource !== 'annotation'">
<el-select v-model="confirmParams.enrichSpecies" placeholder="内置物种代码" style="width:100%">
<template #prepend>OrgDb物种</template>
<el-option label="拟南芥 Arabidopsis (ath)" value="ath"></el-option>
<el-option label="人 Human (hsa)" value="hsa"></el-option>
<el-option label="小鼠 Mouse (mmu)" value="mmu"></el-option>
<el-option label="大鼠 Rat (rno)" value="rno"></el-option>
<el-option label="果蝇 Fly (dme)" value="dme"></el-option>
<el-option label="酵母 Yeast (sce)" value="sce"></el-option>
</el-select>
</div>
<div class="param-row" v-if="confirmParams.enrichType === 'kegg' || confirmParams.enrichType === 'both'">
<el-input v-model="confirmParams.keggOrganism" placeholder="如 ath / hsa / osa">
<template #prepend>KEGG organism</template>
</el-input>
</div>
<div style="padding: 10px; background: #fdf6ec; border-radius: 0px; color: #606266; font-size: 12px; line-height: 1.8;" v-if="confirmParams.enrichBackgroundSource === 'annotation'">
自动注释会先用目标物种蛋白序列生成 gene2go/gene2ko 背景文件,再继续执行富集。小型/中型蛋白组通常需要数小时,几百 MB 级数据可能需要半天左右。
</div>
</div>
<div v-else style="padding: 10px; background: #fdf6ec; border-radius: 0px; color: #E6A23C; font-size: 12px;">
⚠️ RNA-seq分析需要预先上传样本数据。如未上传,此阶段将跳过。
</div>
</div>
<div style="margin-top: 15px; padding: 10px; background: #f0f9eb; border-radius: 0px; 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: 0px;">
<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>
<!-- 深度解读弹窗 -->
<el-dialog
v-model="interpretationDialogVisible"
title=" "
width="80%"
:close-on-click-modal="false"
class="interpretation-dialog"
>
<div v-if="interpretationReport" class="report-container">
<!-- 报告头部 -->
<div class="report-header">
<div class="report-title">🔬 {{ interpretationReport.metadata.title }}</div>
<div class="report-meta">
分析流程: {{ interpretationReport.metadata.included_stages.join(' → ') }} |
生成时间: {{ formatTime(interpretationReport.metadata.generated_at) }}
</div>
</div>
<!-- 流程可视化 -->
<div class="section-title">📈 阶段性进展</div>
<div class="flow-visualization">
<template v-for="(stage, index) in interpretationReport.stage_flow.stages" :key="stage.id">
<div class="flow-stage">
<div class="flow-stage-icon">{{ index === interpretationReport.stage_flow.stages.length - 1 ? '🎯' : '✅' }}</div>
<div class="flow-stage-name">{{ stage.name }}</div>
<div class="flow-stage-result">{{ stage.key_result }}</div>
</div>
<div v-if="index < interpretationReport.stage_flow.stages.length - 1" class="flow-arrow">→</div>
</template>
</div>
<!-- 阶段关联分析 -->
<div class="section-title" v-if="interpretationReport.stage_connections.length > 0">🔗 阶段间关联分析</div>
<div v-for="(conn, idx) in interpretationReport.stage_connections" :key="idx" class="connection-card">
<div class="connection-header">{{ conn.from }} → {{ conn.to }}</div>
<div>{{ conn.description }}</div>
<div style="margin-top: 8px; color: #409eff; font-size: 13px;">
💡 {{ conn.insight }}
</div>
</div>
<!-- 关键发现 -->
<div class="section-title">💡 阶段性科学发现</div>
<div v-for="finding in interpretationReport.key_findings" :key="finding.id" class="finding-item">
<div class="finding-title">{{ finding.id }}. {{ finding.title }}</div>
<div class="finding-desc">{{ finding.description }}</div>
<div class="finding-evidence">📋 证据: {{ finding.evidence }}</div>
</div>
<!-- 生物学故事 -->
<div class="section-title">🧬 生物学故事</div>
<div class="story-box">
{{ interpretationReport.biological_story }}
</div>
<!-- 下一步建议 -->
<div class="section-title">📋 下一步建议</div>
<div v-for="(step, idx) in interpretationReport.next_steps" :key="idx" class="next-step-item">
<div :class="['next-step-priority', 'priority-' + step.priority]">
{{ step.priority === 'high' ? '高优先级' : '中优先级' }}
</div>
<div class="next-step-content">
<div class="next-step-title">{{ step.stage }}: {{ step.description }}</div>
<div class="next-step-desc">{{ step.rationale }}</div>
</div>
</div>
</div>
<!-- 弹窗底部按钮 -->
<template #footer>
<el-button @click="downloadInterpretationReport('markdown')">
📄 Markdown
</el-button>
<el-button @click="downloadInterpretationReport('json')">
📊 JSON
</el-button>
<el-button type="primary" @click="downloadInterpretationReport('pdf')">
📥 下载PDF报告
</el-button>
<el-button type="success" @click="copyInterpretationToClipboard">
📋 复制到剪贴板
</el-button>
<el-button @click="interpretationDialogVisible = false">
关闭
</el-button>
</template>
</el-dialog>
</div>
<!-- Agent状态显示组件模板 - 集成gene-family-mas-v2版本 -->
<template id="agent-status-component-template">
<div class="agent-status-panel">
<!-- Agent状态显示 -->
<div class="status-header">
<h4>🤖 Agent状态</h4>
<span :class="['status-badge', status]">{{ statusText }}</span>
</div>
<!-- RAG查询状态 -->
<div class="rag-status" v-if="ragQuery">
<div class="query-info">
<div class="query-text">{{ ragQuery }}</div>
<div class="query-progress">
<el-progress :percentage="progress" :status="progressStatus"></el-progress>
</div>
</div>
<!-- 检索步骤 -->
<div class="retrieval-steps">
<div v-for="(step, idx) in steps" :key="idx"
:class="['step', step.status]">
<span class="step-icon">{{ step.icon }}</span>
<span class="step-name">{{ step.name }}</span>
<span class="step-time" v-if="step.time">{{ step.time }}ms</span>
</div>
</div>
<!-- Self-RAG反思状态 -->
<div class="reflection-panel" v-if="reflection">
<div class="reflection-title">📝 Self-RAG反思</div>
<div class="reflection-content">
<div>迭代: {{ reflection.iteration }}/{{ reflection.maxIterations }}</div>
<div>原因: {{ reflection.reason }}</div>
<div v-if="reflection.rewrittenQuery">
重写查询: {{ reflection.rewrittenQuery }}
</div>
</div>
</div>
<!-- MultiHopRAG状态 -->
<div class="multihop-panel" v-if="multiHop">
<div class="multihop-title">🔀 MultiHopRAG</div>
<div class="multihop-content">
<div>跳数: {{ multiHop.hops }}</div>
<div>来源: {{ multiHop.sources.join(', ') }}</div>
</div>
</div>
</div>
<!-- 知识图谱可视化 -->
<div class="kg-visualization" v-if="showKG">
<div class="kg-header">
<h5>🧬 知识图谱</h5>
<el-button size="small" @click="refreshKG">刷新</el-button>
</div>
<div id="kg-chart" class="kg-chart"></div>
<div class="kg-stats">
节点: {{ kgStats.nodes }} | 关系: {{ kgStats.edges }}
</div>
</div>
<!-- 结果展示 -->
<div class="results-panel" v-if="results.length > 0">
<h5>📄 检索结果</h5>
<div v-for="(result, idx) in results" :key="idx" class="result-item">
<div class="result-header">
<span class="result-doc">{{ result.doc_id }}</span>
<span class="result-score">Score: {{ result.score.toFixed(3) }}</span>
</div>
<div class="result-content">{{ result.content }}</div>
</div>
</div>
<!-- 原有Agent消息显示 - 动态样式 -->
<div class="agent-messages" ref="messageContainer" v-if="messages.length > 0">
<div v-for="(msg, index) in messages" :key="index"
:class="['message', msg.role, getMessageSpacingClass(msg.content)]">
<div class="message-role">{{ getRoleLabel(msg.role) }}</div>
<div :class="['message-content', getContentClass(msg.content)]" v-html="formatMessage(msg.content, msg.role === 'assistant')"></div>
<div class="message-time">{{ formatTime(msg.timestamp) }}</div>
</div>
</div>
<div class="agent-input" v-if="showInput">
<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, ElMessageBox } = ElementPlus;
// ========== Agent Status Component - 集成RAG Agent状态 ==========
const AgentStatusComponent = {
template: '#agent-status-component-template',
data() {
return {
messages: [],
userInput: '',
isProcessing: false,
currentMode: '待机',
sessionId: '',
tasks: [],
currentTask: null,
pollingInterval: null,
// RAG Agent状态
status: 'idle',
ragQuery: '',
progress: 0,
steps: [
{ name: '查询分析', icon: '🔍', status: 'pending', time: null },
{ name: '文档检索', icon: '📚', status: 'pending', time: null },
{ name: '结果评估', icon: '✓', status: 'pending', time: null },
{ name: '生成回答', icon: '✍', status: 'pending', time: null }
],
reflection: null,
multiHop: null,
results: [],
showKG: false,
showInput: true,
kgStats: { nodes: 0, edges: 0 },
kgChart: null
};
},
computed: {
statusText() {
const statusMap = {
'idle': '待机',
'analyzing': '分析中',
'retrieving': '检索中',
'reflecting': '反思中',
'generating': '生成中',
'completed': '完成',
'error': '错误'
};
return statusMap[this.status] || this.status;
},
progressStatus() {
if (this.status === 'error') return 'exception';
if (this.status === 'completed') return 'success';
return '';
}
},
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.status = 'analyzing';
this.progress = 10;
this.updateStep(0, 'running');
// 添加用户消息
this.addMessage('user', message);
try {
// 步骤1: 查询分析
await this.simulateStep(0, 500);
this.status = 'retrieving';
this.progress = 30;
// 调用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',
prefix: window.parentParams ? window.parentParams.prefix : '',
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);
}
},
// RAG Agent方法
async simulateStep(stepIndex, duration) {
await new Promise(resolve => setTimeout(resolve, duration));
},
updateStep(index, status, time = null) {
this.steps[index].status = status;
if (time !== null) {
this.steps[index].time = time;
}
},
async searchDocuments() {
const startTime = Date.now();
try {
const response = await fetch('/api/v1/rag/advanced/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: this.ragQuery,
use_agentic: true,
enable_self_rag: true,
enable_multi_hop: true
})
});
const data = await response.json();
return {
results: data.sources || [],
time: Date.now() - startTime
};
} catch (error) {
console.error('检索失败:', error);
return { results: [], time: Date.now() - startTime };
}
},
async evaluateResults() {
const startTime = Date.now();
await new Promise(resolve => setTimeout(resolve, 300));
return {
iteration: 1,
maxIterations: 3,
reason: '检索结果充分',
needsRewrite: false,
rewrittenQuery: null,
time: Date.now() - startTime
};
},
async multiHopSearch(query) {
const startTime = Date.now();
await new Promise(resolve => setTimeout(resolve, 600));
return {
hops: 2,
sources: ['Doc1', 'Doc2', 'Doc3'],
results: [],
time: Date.now() - startTime
};
},
initKGChart() {
if (typeof echarts !== 'undefined') {
const chartDom = document.getElementById('kg-chart');
if (chartDom) {
this.kgChart = echarts.init(chartDom);
this.loadKGData();
}
}
},
async loadKGData() {
try {
const response = await fetch('/api/v1/kg/graph');
const data = await response.json();
this.kgStats = {
nodes: data.nodes?.length || 0,
edges: data.edges?.length || 0
};
this.renderKG(data.nodes || [], data.edges || []);
} catch (error) {
console.error('加载知识图谱失败:', error);
}
},
renderKG(nodes, edges) {
if (!this.kgChart) return;
const option = {
tooltip: {},
series: [{
type: 'graph',
layout: 'force',
data: nodes.map(n => ({
id: n.id,
name: n.name || n.id,
symbolSize: n.degree || 20,
category: n.category || 'default'
})),
links: edges.map(e => ({
source: e.source,
target: e.target,
label: { show: true, formatter: e.relation }
})),
roam: true,
label: { show: true },
force: { repulsion: 100 }
}]
};
this.kgChart.setOption(option);
},
refreshKG() {
this.loadKGData();
},
updateKG() {
this.loadKGData();
},
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, isAI = false) {
if (!content) return '';
// 检测是否是结构化内容
const isStructured = isAI && this.detectStructuredContent(content);
let processed = content
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
if (isStructured) {
// 结构化内容使用智能格式化
processed = this.formatStructuredContent(processed);
} else {
processed = processed.replace(/\n/g, '<br>');
}
return processed;
},
detectStructuredContent(content) {
const patterns = [
/研究总结|基于.*文献|基于.*篇文献/i,
/^\d+\.\s+.+$/m,
/转录因子|研究发现|研究结论/i,
/DNA BINDING|TRANSCRIPTION|GENE FAMILY/i,
/Arabidopsis|SPL.*BMC|doi\.org|PMID/i
];
return patterns.some(p => p.test(content));
},
formatStructuredContent(content) {
let html = content;
// 1. 提取头部信息
html = html.replace(/^(基于\s*\d+\s*篇文献[^\n]*)/i,
'<div class="rag-header">📚 $1</div>');
// 2. 处理编号段落
html = html.replace(/(\d+)\.\s*([^\n<]+)([\n<]|$)/g,
'<div class="rag-section">' +
'<div class="rag-section-title">$1. $2</div>');
// 3. 处理段落内容
html = html.replace(/<div class="rag-section">([\s\S]*?)(?=<div class="rag-section">|<div class="rag-tags|$)/g,
(match, content) => {
const tagPattern = /([A-Z][A-Z\s]*[A-Z])(?=,|$|<)/g;
const tags = [];
let cleanContent = content.replace(tagPattern, (match) => {
tags.push(match.trim());
return '';
});
const sourcePattern = /(Arabidopsis[^,]*|doi[^\s]*|PMID[^\s]*)/i;
const sourceMatch = cleanContent.match(sourcePattern);
const source = sourceMatch ? sourceMatch[1] : '';
cleanContent = cleanContent.replace(sourcePattern, '');
cleanContent = cleanContent.replace(/[,,]\s*$/, '').trim();
let tagsHtml = '';
if (tags.length > 0) {
tagsHtml = '<div class="rag-tags-row">' +
tags.map(t => `<span class="rag-tag">${t}</span>`).join('') +
(source ? `<span class="rag-source">📄 ${source}</span>` : '') +
'</div>';
}
return `<div class="rag-section">` +
(cleanContent ? `<div class="rag-section-content">${cleanContent}</div>` : '') +
tagsHtml +
`</div>`;
});
// 4. 处理底部提示
html = html.replace(/([💡📝].*知识图谱.*关联.*)/i,
'<div class="rag-footer">$1</div>');
// 5. 包裹整个内容
if (!html.includes('rag-content-wrapper')) {
html = `<div class="rag-content-wrapper">${html}</div>`;
}
return html;
},
getMessageSpacingClass(content) {
if (!content) return 'compact';
const length = content.length;
const lines = content.split('\n').length;
if (length < 100 && lines <= 2) return 'compact';
if (length > 1000 || lines > 20) return 'spacious';
return 'normal';
},
getContentClass(content) {
if (!content) return 'medium-content';
const length = content.length;
const lines = content.split('\n').length;
const isStructured = this.detectStructuredContent(content);
if (isStructured) return 'structured-content';
if (length < 150 && lines <= 3) return 'short-content';
if (length > 800 || lines > 15) return 'long-content';
return 'medium-content';
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}
};
// 调试:确认Vue加载成功
console.log('Vue loaded:', typeof Vue);
console.log('ElementPlus loaded:', typeof ElementPlus);
const app = createApp({
setup() {
// ========== 会话管理 ==========
const conversations = ref([]); // 从API加载,不再硬编码
const currentConversationId = ref('');
// 加载会话列表
const loadConversations = async () => {
try {
const response = await fetch('/api/conversations/list/default');
const data = await response.json();
conversations.value = data.conversations || [];
if (conversations.value.length > 0 && !currentConversationId.value) {
currentConversationId.value = conversations.value[0].conversation_id;
}
console.log('[会话] 加载了', conversations.value.length, '个会话');
} catch (e) {
console.error('[会话] 加载失败:', e);
}
};
// 最近会话(显示为快捷按钮)
const recentConversations = computed(() => {
return conversations.value.slice(0, 3);
});
// 更多会话(显示在下拉框中)
const moreConversations = computed(() => {
return conversations.value.slice(3);
});
// 创建新会话(调用API)
const createNewConversation = async () => {
try {
const response = await fetch('/api/conversations/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: 'default',
title: '新会话 ' + new Date().toLocaleTimeString(),
job_id: null, // 新建会话不绑定 job
input_dir: null,
output_dir: null,
working_dir: null
})
});
const data = await response.json();
if (data.conversation_id) {
currentConversationId.value = data.conversation_id;
// 清空当前会话消息
agentSession.messages = [];
console.log('[会话] 清空消息,新建会话:', data.conversation_id);
// 清空 jobInfo
jobInfo.job_id = null;
jobInfo.input_dir = '';
jobInfo.output_dir = '';
jobInfo.working_dir = '';
console.log('[会话] 清空 jobInfo');
// 清空分析结果区域
stages.forEach(stage => {
stage.images = [];
stage.files = [];
stage.status = 'pending';
stage.elapsed_time = 0;
});
console.log('[会话] 清空 stages');
await loadConversations(); // 刷新列表
ElementPlus.ElMessage.success('新会话已创建,请选择或创建 Working Directory');
}
} catch (e) {
console.error('[会话] 创建失败:', e);
ElementPlus.ElMessage.error('创建会话失败');
}
};
const selectConversation = async (conv) => {
currentConversationId.value = conv.conversation_id;
ElementPlus.ElMessage.info('切换到: ' + conv.title);
// 恢复关联的 job_id
if (conv.job_id) {
jobInfo.job_id = conv.job_id;
jobInfo.input_dir = conv.input_dir || '';
jobInfo.output_dir = conv.output_dir || '';
jobInfo.working_dir = conv.working_dir || '';
console.log('[会话] 恢复 job:', conv.job_id);
// 先清空 stages 状态
stages.forEach(stage => {
stage.images = [];
stage.files = [];
stage.status = 'pending';
stage.elapsed_time = 0;
});
console.log('[会话] 已清空 stages 状态');
// 再刷新该 job 的分析结果
await refreshResults(conv.job_id);
}
// 加载会话历史消息
try {
const response = await fetch(`/api/conversations/${conv.conversation_id}/messages`);
const data = await response.json();
if (data.messages) {
// 清空当前Agent会话消息
agentSession.messages = [];
// 加载历史消息
data.messages.forEach(msg => {
agentSession.messages.push({
role: msg.role,
content: msg.content,
time: new Date(msg.timestamp).toLocaleTimeString(),
intent: msg.metadata?.intent || null
});
});
console.log('[会话] 加载了', data.messages.length, '条历史消息');
}
} catch (e) {
console.error('[会话] 加载历史消息失败:', e);
}
};
// 会话列表过滤(用于侧边栏)
const filteredConversations = computed(() => {
return conversations.value;
});
// Analysis StageSelect
const selectedStage = ref('');
// Basic Parameters
const params = reactive({
prefix: '',
cpu: 8
});
window.parentParams = params;
// Input路径
const inputPath = ref('');
// 任务信息
const jobInfo = reactive({
job_id: 'default_job',
input_dir: '',
working_dir: '',
output_dir: ''
});
// 对话消息
const messages = ref([]);
const logContent = ref('');
const chatArea = ref(null);
// 自然语言Input
const nlInput = ref('');
const nlLoading = ref(false);
// V2: RAG对话记忆
const ragMode = ref(false); // 是否RAG模式
const preferenceMode = ref(false); // 偏好模式: false=Skill(默认), true=Literature
const autoDetectedMode = ref('skill'); // 自动检测的模式: 'skill' 或 'faq'
// Agent Mode - Model Selection
const selectedModel = ref('kimi');
const modelDisplayName = computed(() => {
const names = {
kimi: '🧠 Kimi',
glm: '🔬 GLM-4',
minimax: '⚡ MiniMax',
llama3: '🦙 Llama3',
qwen2: '🤖 Qwen2',
ollama: '📍 Ollama'
};
return names[selectedModel.value] || '🧠 Kimi';
});
// Agent Session State
const agentSession = reactive({
messages: [],
currentTask: null,
tasks: []
});
// 深度解读功能状态
const interpretationDialogVisible = ref(false);
const interpretationReport = ref(null);
const currentInterpretStage = ref({ id: '', name: '' });
// 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:7868/api'); // API基础URL - 前端服务同时提供API
// 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 diagnosisConfirmDialogVisible = ref(false);
const diagnosisConfirmCountdown = ref(30); // 30秒倒计时
let diagnosisConfirmTimer = null;
const diagnosisFixData = ref({
errorType: '',
description: '',
currentParams: {},
suggestedParams: {},
currentParamsDisplay: '',
suggestedParamsDisplay: '',
reason: '',
sessionId: '',
jobId: ''
});
// 批量执行相关
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 syntenyChoiceDialogVisible = ref(false);
const syntenyChoiceQuery = ref('');
const syntenyChoiceBaseIntent = ref({});
const syntenyChoiceOptions = 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'],
// 第三阶段:依赖identify + phylogeny
'structure': ['identify', 'phylogeny'],
'motif': ['identify', 'phylogeny'],
'domain': ['identify', 'phylogeny'],
'promoter': ['identify', 'phylogeny'],
'expression': ['identify', 'phylogeny'],
'enrichment': ['identify', 'phylogeny'],
'pep': ['identify'],
'msa': ['identify'],
// 共线性相关
'synteny': ['identify'],
'karyotype': ['identify', 'synteny'],
'interspecies_synteny': ['identify', 'phylogeny'],
// 同源基因和PPI
'ortholog': ['identify', 'phylogeny'],
'ppi': ['identify', 'phylogeny', 'ortholog'],
// Ka/Ks
'kaks': ['identify', 'phylogeny', 'interspecies_synteny'],
// 组合图
'combo': ['identify', 'phylogeny', 'motif', 'structure', 'domain'],
// RNA-seq 独立流程(严格按顺序执行)
'rnaseq_qc': [],
'rnaseq_align': ['rnaseq_qc'],
'rnaseq_quant': ['rnaseq_align'],
'rnaseq_de': ['rnaseq_quant'],
'annotation': [],
'rnaseq_go_enrich': ['rnaseq_de'],
'rnaseq_kegg_enrich': ['rnaseq_de'],
'rnaseq_enrich': ['rnaseq_de']
};
// 阶段执行状态跟踪
const stageExecutionStatus = ref({}); // { stageName: 'pending'|'running'|'completed'|'failed' }
const runningStages = ref(new Set()); // 当前正在执行的阶段
const maxParallelStages = 4; // 最大并行数
const confirmParams = reactive({
stage: '',
prefix: '',
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: '',
rnaseqForegroundMode: 'auto',
enrichBackgroundSource: 'auto',
enrichSpecies: '',
enrichKeyType: 'auto',
enrichType: 'go',
keggOrganism: '',
// RNA-seq QC 参数
rnaseqDataMode: 'upload',
rnaseqDataPath: '',
// PPI 参数
ppiSpecies: 'ath',
ppiScore: 700,
// KaKs 参数
kaksNeutralRate: 7.08e-9,
// Genome Prepare 参数
genomePrepareType: 'auto',
targetSpecies: [],
refSpeciesList: []
});
// 执行状态
geoId: ''
// 执行状态
const interspeciesSpeciesOptions = reactive({
targets: [],
refs: [],
default_targets: [],
default_refs: []
});
const interspeciesSpeciesLoading = ref(false);
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️⃣ 种间Synteny', 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: 'annotation', label: '1️⃣9️⃣ Annotation', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'rnaseq_go_enrich', label: '2️⃣0️⃣ RNA-seq GO Enrich', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'rnaseq_kegg_enrich', label: '2️⃣1️⃣ RNA-seq KEGG Enrich', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'rnaseq_enrich', label: '2️⃣2️⃣ 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);
// 同时启动诊断确认轮询
startDiagnosisConfirmPolling();
};
const stopAgentTaskPolling = () => {
if (agentTaskTimer) clearInterval(agentTaskTimer);
// 同时停止诊断确认轮询
stopDiagnosisConfirmPolling();
};
// 诊断确认状态轮询
const startDiagnosisConfirmPolling = () => {
if (diagnosisConfirmTimer) clearInterval(diagnosisConfirmTimer);
diagnosisConfirmTimer = setInterval(checkDiagnosisConfirmation, 2000);
};
const stopDiagnosisConfirmPolling = () => {
if (diagnosisConfirmTimer) clearInterval(diagnosisConfirmTimer);
diagnosisConfirmTimer = null;
};
// 检查是否有等待确认的修复方案
const checkDiagnosisConfirmation = async () => {
if (!sessionId.value) return;
try {
const response = await fetch(`${apiBaseUrl.value}/agent/pending_confirmation/${sessionId.value}?job_id=${jobInfo.job_id || ''}`);
if (!response.ok) return;
const data = await response.json();
if (data.status === "waiting" && data.confirmation) {
// 有等待确认的修复方案,显示确认对话框
if (!diagnosisConfirmDialogVisible.value) {
showDiagnosisConfirm(data.confirmation);
}
}
} catch (err) {
// 静默失败,不影响正常轮询
console.debug('[Diagnosis Polling] Error:', err);
}
};
// 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 = '';
// 更新当前会话的 job_id
if (currentConversationId.value) {
try {
await fetch('/api/conversations/link-to-job', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: currentConversationId.value,
job_id: data.job_id
})
});
console.log('[会话] 已绑定 job:', data.job_id);
await loadConversations(); // 刷新列表
} catch (e) {
console.error('[会话] 绑定 job 失败:', e);
}
}
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;
// 更新当前会话的 job_id
if (currentConversationId.value) {
try {
await fetch('/api/conversations/link-to-job', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: currentConversationId.value,
job_id: data.job_id
})
});
console.log('[会话] 已绑定 job:', data.job_id);
await loadConversations();
} catch (e) {
console.error('[会话] 绑定 job 失败:', e);
}
}
ElMessage.success('已切换到Working Directory: ' + data.job_id);
// Refresh结果展示
await refreshResults();
} else {
ElMessage.error(data.error || '切换失败');
}
} catch (err) {
ElMessage.error('切换失败: ' + err.message);
}
};
// 阶段Select变化
const onStageChange = (val) => {
console.log('Select阶段:', val);
if (val && val.startsWith('rnaseq')) {
// RNA-seq阶段特殊处理
loadRnaseqData();
}
};
// ========== 会话管理方法 ==========
const onSelectConversation = (conv) => {
console.log('Select会话:', conv);
currentConversation.value = conv;
if (conv) {
// 加载会话消息
loadConversationMessages(conv.conversation_id);
// 更新标题
document.title = conv.title + ' - BotanicalBaseAgent';
}
};
const onNewConversation = (conv) => {
console.log('新建会话:', conv);
currentConversation.value = conv;
// 清空当前消息
messages.value = [];
agentSession.messages = [];
// 添加欢迎消息
const welcomeMsg = {
role: 'assistant',
content: '您好!我是BotanicalBaseAgent,专注于基因家族分析。请告诉我您想分析哪个基因家族?',
timestamp: new Date().toISOString()
};
messages.value.push(welcomeMsg);
agentSession.messages.push(welcomeMsg);
};
const onSidebarToggle = (collapsed) => {
sidebarCollapsed.value = collapsed;
console.log('侧边栏状态:', collapsed ? '折叠' : '展开');
};
// 加载会话消息
const loadConversationMessages = async (conversationId) => {
try {
const response = await fetch(`${apiBaseUrl.value}/conversations/${conversationId}/messages`);
if (response.ok) {
const data = await response.json();
messages.value = data.messages || [];
agentSession.messages = data.messages || [];
}
} catch (e) {
console.error('加载会话消息失败:', e);
}
};
// 加载RNA-seq数据
const loadRnaseqData = async () => {
try {
const response = await fetch('/api/rnaseq/samples');
const data = await response.json();
if (data.success) {
rnaseqSamples.value = data.samples;
}
} catch (e) {
console.error('加载RNA-seq样本失败:', e);
}
};
const rnaseqSamples = ref([]);
// 显示确认对话框
const loadInterspeciesSpeciesOptions = async () => {
interspeciesSpeciesLoading.value = true;
try {
const jobId = jobInfo.job_id || '';
const response = await fetch(`/api/interspecies/species-options?job_id=${encodeURIComponent(jobId)}`);
const data = await response.json();
if (!data.success) throw new Error(data.message || 'Failed to load species options');
interspeciesSpeciesOptions.targets = data.targets || [];
interspeciesSpeciesOptions.refs = data.refs || [];
interspeciesSpeciesOptions.default_targets = data.default_targets || [];
interspeciesSpeciesOptions.default_refs = data.default_refs || [];
confirmParams.targetSpecies = [...interspeciesSpeciesOptions.default_targets];
confirmParams.refSpeciesList = [...interspeciesSpeciesOptions.default_refs];
} catch (err) {
console.error('[DEBUG] Failed to load interspecies species options:', err);
ElMessage.error(`Failed to load species options: ${err.message || err}`);
interspeciesSpeciesOptions.targets = [];
interspeciesSpeciesOptions.refs = [];
confirmParams.targetSpecies = [];
confirmParams.refSpeciesList = [];
} finally {
interspeciesSpeciesLoading.value = false;
}
};
const showConfirm = async () => {
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 = "";
confirmParams.cpu = params.cpu;
if (selectedStage.value === 'rnaseq_go_enrich') {
confirmParams.enrichType = 'go';
} else if (selectedStage.value === 'rnaseq_kegg_enrich') {
confirmParams.enrichType = 'kegg';
}
if (['rnaseq_enrich', 'rnaseq_go_enrich', 'rnaseq_kegg_enrich'].includes(selectedStage.value)) {
confirmParams.rnaseqForegroundMode = 'auto';
}
if (selectedStage.value === 'interspecies_synteny') {
await loadInterspeciesSpeciesOptions();
} else {
confirmParams.targetSpecies = [];
confirmParams.refSpeciesList = [];
}
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 = confirmParams.stage === 'interspecies_synteny' ? 20 : 15;
clearAutoExecuteTimer();
autoExecuteTimer = setInterval(() => {
autoExecuteCountdown.value--;
if (autoExecuteCountdown.value <= 0) {
clearAutoExecuteTimer();
if (confirmDialogVisible.value && !executing.value) {
console.log(`[DEBUG] Auto executing after ${confirmParams.stage === 'interspecies_synteny' ? 20 : 15}s countdown`);
doExecute();
}
}
}, 1000);
};
// 清除自动执行倒计时
const clearAutoExecuteTimer = () => {
if (autoExecuteTimer) {
clearInterval(autoExecuteTimer);
autoExecuteTimer = null;
}
autoExecute.value = false;
};
// 诊断修复确认相关函数
const startDiagnosisConfirmTimer = () => {
diagnosisConfirmCountdown.value = 30;
clearDiagnosisConfirmTimer();
diagnosisConfirmTimer = setInterval(() => {
diagnosisConfirmCountdown.value--;
if (diagnosisConfirmCountdown.value <= 0) {
clearDiagnosisConfirmTimer();
if (diagnosisConfirmDialogVisible.value) {
console.log('[DEBUG] Auto applying diagnosis fix after 30s');
ElMessage.warning('⏱️ 倒计时结束,自动应用修复方案...');
confirmDiagnosisFix();
}
}
}, 1000);
};
const clearDiagnosisConfirmTimer = () => {
if (diagnosisConfirmTimer) {
clearInterval(diagnosisConfirmTimer);
diagnosisConfirmTimer = null;
}
};
const showDiagnosisConfirm = (fixData) => {
diagnosisFixData.value = {
errorType: fixData.error_type || '执行错误',
description: fixData.description || '检测到执行问题',
currentParams: fixData.current_params || {},
suggestedParams: fixData.suggested_params || {},
currentParamsDisplay: fixData.current_params_display || JSON.stringify(fixData.current_params || {}),
suggestedParamsDisplay: fixData.suggested_params_display || JSON.stringify(fixData.suggested_params || {}),
reason: fixData.reason || '建议调整参数以解决问题',
sessionId: fixData.session_id || sessionId.value,
jobId: fixData.job_id || currentJobId.value
};
diagnosisConfirmDialogVisible.value = true;
startDiagnosisConfirmTimer();
};
const confirmDiagnosisFix = async () => {
clearDiagnosisConfirmTimer();
diagnosisConfirmDialogVisible.value = false;
try {
const response = await fetch(`${apiBaseUrl.value}/agent/confirm_fix`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: diagnosisFixData.value.sessionId,
job_id: diagnosisFixData.value.jobId,
confirmed: true,
fix_data: diagnosisFixData.value.suggestedParams
})
});
if (response.ok) {
ElMessage.success('✅ 已应用修复方案,继续执行...');
} else {
ElMessage.error('❌ 应用修复失败');
}
} catch (error) {
console.error('[Diagnosis Confirm] Error:', error);
ElMessage.error('❌ 网络错误,请重试');
}
};
const rejectDiagnosisFix = async () => {
clearDiagnosisConfirmTimer();
diagnosisConfirmDialogVisible.value = false;
try {
const response = await fetch(`${apiBaseUrl.value}/agent/confirm_fix`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: diagnosisFixData.value.sessionId,
job_id: diagnosisFixData.value.jobId,
confirmed: false
})
});
ElMessage.info('⏭️ 已跳过修复,继续下一阶段...');
} catch (error) {
console.error('[Diagnosis Reject] Error:', error);
}
};
// 批量执行相关函数
const startBatchExecuteTimer = () => {
batchExecuteCountdown.value = 30; // 增加到30秒,给用户更多时间确认
clearBatchExecuteTimer();
// 添加明显的提示消息
ElMessage.info(`📋 批量执行确认对话框已打开,${batchExecuteCountdown.value}秒后自动开始执行,请点击确认或取消`);
batchExecuteTimer = setInterval(() => {
batchExecuteCountdown.value--;
if (batchExecuteCountdown.value <= 0) {
clearBatchExecuteTimer();
if (batchExecuteDialogVisible.value && !isBatchExecuting.value) {
console.log('[DEBUG] Auto starting batch execute after 30s');
ElMessage.warning('⏱️ 倒计时结束,自动开始执行...');
startBatchExecute();
}
}
}, 1000);
};
const clearBatchExecuteTimer = () => {
if (batchExecuteTimer) {
clearInterval(batchExecuteTimer);
batchExecuteTimer = null;
}
};
const startBatchExecute = async () => {
clearBatchExecuteTimer();
batchExecuteDialogVisible.value = false;
isBatchExecuting.value = true;
shouldStopBatchExecute.value = false;
const stages = batchExecuteStages.value;
addAgentMessage('assistant', `🚀 开始依赖驱动的并行执行,共 ${stages.length} 个阶段...`);
// 初始化阶段状态
stageExecutionStatus.value = {};
// 检查已完成的阶段(通过manifest文件)
const checkCompletedStages = async () => {
const allStages = ['identify', 'phylogeny', 'motif', 'domain', 'structure', 'promoter',
'synteny', 'karyotype', 'interspecies_synteny', 'ppi', 'kaks', 'pep',
'expression', 'rnaseq_qc', 'rnaseq_align', 'rnaseq_quant', 'rnaseq_de',
'annotation', 'rnaseq_go_enrich', 'rnaseq_kegg_enrich', 'rnaseq_enrich'];
const jobId = jobInfo.job_id;
const prefix = encodeURIComponent(params.prefix || '');
for (const stage of allStages) {
try {
const response = await fetch(`/api/stage-status?job_id=${jobId}&stage=${stage}&prefix=${prefix}`);
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;
}
// 【调试日志】打印当前状态
console.log('[DEBUG-STATE] checkAndExecute called');
console.log('[DEBUG-STATE] stageExecutionStatus:', JSON.parse(JSON.stringify(stageExecutionStatus.value)));
console.log('[DEBUG-STATE] runningStages:', Array.from(runningStages.value));
// 获取可执行的阶段(依赖已完成,且未在执行/完成)
const executableStages = stages.filter(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
const status = stageExecutionStatus.value[stageId];
const deps = stageDependencies[stageId] || [];
const depsCompleted = deps.every(dep => stageExecutionStatus.value[dep] === 'completed');
console.log(`[DEBUG-CHECK] Stage ${stageId}: status=${status}, deps=${JSON.stringify(deps)}, depsCompleted=${depsCompleted}`);
if (status !== 'pending') return false;
return depsCompleted;
});
console.log('[DEBUG-STATE] executableStages:', executableStages.map(s => typeof s === 'object' ? s.id : s));
// 检查是否全部完成
const allCompleted = stages.every(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
return stageExecutionStatus.value[stageId] === 'completed' ||
stageExecutionStatus.value[stageId] === 'failed';
});
if (allCompleted) {
console.log('[DEBUG-STATE] All stages completed, resolving');
resolve();
return;
}
// 并行执行可执行的阶段(限制最大并行数)
const availableSlots = maxParallelStages - runningStages.value.size;
const stagesToExecute = executableStages.slice(0, availableSlots);
console.log('[DEBUG-STATE] availableSlots:', availableSlots, 'stagesToExecute:', stagesToExecute.map(s => typeof s === 'object' ? s.id : s));
if (stagesToExecute.length > 0) {
console.log('[DEBUG] Executing stages:', stagesToExecute);
// 并行启动所有可执行阶段
const executePromises = stagesToExecute.map(stage =>
executeStageWithConfirmation(stage, stages.findIndex(s => {
const sid = typeof s === 'object' ? s.id : s;
const tid = typeof stage === 'object' ? stage.id : stage;
return sid === tid;
}) + 1, 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, currentIndex, totalStages) => {
// stage可能是对象或字符串,统一处理
const stageId = typeof stage === 'object' ? stage.id : stage;
const stageName = typeof stage === 'object' ? stage.name : stage;
console.log(`[DEBUG-STAGE] executeStageWithConfirmation START: ${stageId}`);
runningStages.value.add(stageId);
stageExecutionStatus.value[stageId] = 'running';
currentBatchStage.value = stageId;
console.log(`[DEBUG-STAGE] ${stageId}: added to runningStages, status set to running`);
console.log(`[DEBUG-STAGE] runningStages:`, Array.from(runningStages.value));
try {
// 显示阶段确认对话框(带15秒倒计时)
console.log(`[DEBUG-STAGE] ${stageId}: showing confirm dialog`);
const shouldExecute = await showStageConfirmDialog(stage, currentIndex, totalStages);
console.log(`[DEBUG-STAGE] ${stageId}: confirm dialog result: ${shouldExecute}`);
if (!shouldExecute) {
console.log(`[DEBUG-STAGE] ${stageId}: user skipped`);
addAgentMessage('assistant', `⏭️ 跳过阶段: ${stageName}`);
stageExecutionStatus.value[stageId] = 'failed'; // 标记为失败以便下游知道
runningStages.value.delete(stageId);
console.log(`[DEBUG-STAGE] ${stageId}: status set to failed, removed from runningStages`);
return;
}
if (shouldStopBatchExecute.value) {
console.log(`[DEBUG-STAGE] ${stageId}: batch stopped`);
stageExecutionStatus.value[stageId] = 'failed';
runningStages.value.delete(stageId);
console.log(`[DEBUG-STAGE] ${stageId}: status set to failed, removed from runningStages`);
return;
}
addAgentMessage('assistant', `⚙️ 执行阶段: ${stageName}...`);
// 执行阶段
console.log(`[DEBUG-STAGE] ${stageId}: calling executeSingleStageWithParams`);
await executeSingleStageWithParams(stageId);
console.log(`[DEBUG-STAGE] ${stageId}: executeSingleStageWithParams returned`);
stageExecutionStatus.value[stageId] = 'completed';
console.log(`[DEBUG-STAGE] ${stageId}: status set to completed`);
// 添加阶段完成消息,包含深度解读按钮
const completionMsg = `✅ 阶段 ${stageName} 完成`;
addAgentMessage('assistant', completionMsg);
// 添加深度解读按钮(阶段完成后显示)
addInterpretationButton(stageId, stageName);
// 刷新结果展示
await refreshResults();
} catch (err) {
console.log(`[DEBUG-STAGE] ${stageId}: caught error: ${err.message}`);
const shouldRetryWithFallback = await maybePromptEnrichBackgroundFallback(err.message, stageId);
if (shouldRetryWithFallback) {
try {
addAgentMessage('assistant', `🔄 阶段 ${getStageLabel(stageId)} 重新提交(自动注释背景)...`);
await executeSingleStageWithParams(stageId);
stageExecutionStatus.value[stageId] = 'completed';
addAgentMessage('assistant', `✅ 阶段 ${stageName} 完成`);
await refreshResults();
return;
} catch (retryErr) {
stageExecutionStatus.value[stageId] = 'failed';
addAgentMessage('assistant', `❌ 阶段 ${stageName} 失败: ${retryErr.message}`);
console.error('[ERROR] Stage retry failed:', stageId, retryErr);
}
} else {
stageExecutionStatus.value[stageId] = 'failed';
addAgentMessage('assistant', `❌ 阶段 ${stageName} 失败: ${err.message}`);
console.error('[ERROR] Stage failed:', stageId, err);
}
} finally {
runningStages.value.delete(stageId);
console.log(`[DEBUG-STAGE] ${stageId}: FINALLY - removed from runningStages`);
console.log(`[DEBUG-STAGE] ${stageId}: final status:`, stageExecutionStatus.value[stageId]);
console.log(`[DEBUG-STAGE] runningStages after:`, Array.from(runningStages.value));
}
};
// 显示阶段确认对话框(队列机制,支持并行阶段的动态确认)
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 stageObj = typeof request.stage === 'object' ? request.stage : {};
const stageId = typeof request.stage === 'object' ? request.stage.id : request.stage;
const stageName = typeof request.stage === 'object' ? request.stage.name : request.stage;
const stageParams = stageObj.params || {};
// 设置当前阶段参数
selectedStage.value = stageId;
confirmParams.stage = stageId;
confirmParams.prefix = batchExecuteParams.value.params?.prefix || '';
confirmParams.cpu = batchExecuteParams.value.params?.cpu || 8;
// 特殊处理:ppi 自动设置基因ID文件
if (stageId === 'ppi') {
confirmParams.ppiSpecies = 'ath';
confirmParams.ppiScore = 700;
}
if (stageId === 'rnaseq_enrich' || stageId === 'rnaseq_go_enrich' || stageId === 'rnaseq_kegg_enrich') {
// 富集阶段必须重新默认到 auto,避免沿用上一个 rnaseq_de 弹窗中的 family/global。
// 用户可在当前富集弹窗中显式改为 family 或 global。
confirmParams.rnaseqForegroundMode = stageParams.rnaseqForegroundMode || 'auto';
confirmParams.enrichBackgroundSource = stageParams.enrichBackgroundSource || confirmParams.enrichBackgroundSource || 'auto';
confirmParams.enrichSpecies = stageParams.enrichSpecies || confirmParams.enrichSpecies || '';
confirmParams.enrichKeyType = stageParams.enrichKeyType || confirmParams.enrichKeyType || 'auto';
confirmParams.enrichType = stageParams.enrichType || confirmParams.enrichType || 'go';
if (stageId === 'rnaseq_go_enrich') confirmParams.enrichType = 'go';
if (stageId === 'rnaseq_kegg_enrich') confirmParams.enrichType = 'kegg';
confirmParams.keggOrganism = stageParams.keggOrganism || confirmParams.keggOrganism || '';
}
if (stageId === 'rnaseq_de') {
confirmParams.rnaseqForegroundMode = stageParams.rnaseqForegroundMode || confirmParams.rnaseqForegroundMode || 'auto';
}
// 显示确认对话框
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) => {
console.log(`[DEBUG-EXEC] executeSingleStageWithParams START: ${stageName}`);
const backendStageName = (
stageName === 'rnaseq_go_enrich' || stageName === 'rnaseq_kegg_enrich'
) ? 'rnaseq_enrich' : stageName;
const requestBody = {
stage: backendStageName,
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 || '';
requestBody.rnaseqForegroundMode = confirmParams.rnaseqForegroundMode || 'auto';
}
if (stageName === 'rnaseq_enrich' || stageName === 'rnaseq_go_enrich' || stageName === 'rnaseq_kegg_enrich') {
if (stageName === 'rnaseq_go_enrich') {
requestBody.enrichType = 'go';
} else if (stageName === 'rnaseq_kegg_enrich') {
requestBody.enrichType = 'kegg';
}
requestBody.enrichBackgroundSource = confirmParams.enrichBackgroundSource || 'auto';
requestBody.enrichSpecies = confirmParams.enrichSpecies || '';
requestBody.enrichKeyType = confirmParams.enrichKeyType || 'auto';
requestBody.enrichType = requestBody.enrichType || confirmParams.enrichType || 'go';
requestBody.keggOrganism = confirmParams.keggOrganism || '';
}
console.log('[DEBUG-EXEC] Executing stage with params:', stageName, requestBody);
if (stageName === 'interspecies_synteny') {
requestBody.targetSpecies = confirmParams.targetSpecies || [];
requestBody.refSpeciesList = confirmParams.refSpeciesList || [];
}
const response = await fetch('/api/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
console.log(`[DEBUG-EXEC] ${stageName} /api/execute response:`, data);
if (data.success) {
if (data.task_id) {
// 异步任务,轮询状态
console.log(`[DEBUG-EXEC] ${stageName}: async task, task_id=${data.task_id}`);
const result = await pollTaskUntilComplete(data.task_id, stageName);
console.log(`[DEBUG-EXEC] ${stageName}: pollTaskUntilComplete returned`);
return result;
} else if (data.async === false) {
// 同步执行完成,直接返回结果
console.log(`[DEBUG-EXEC] ${stageName}: sync completed`);
return data;
} else {
throw new Error(data.message || '执行失败');
}
} else {
throw new Error(data.message || data.error || '执行失败');
}
};
// 执行单个阶段(内部函数)
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) {
if (data.task_id) {
// 异步任务,轮询状态
await pollTaskUntilComplete(data.task_id, stageName);
resolve(data);
} else if (data.async === false) {
// 同步执行完成
resolve(data);
} else {
reject(new Error(data.message || '执行失败'));
}
} else {
reject(new Error(data.message || data.error || '执行失败'));
}
} catch (err) {
reject(err);
}
};
doExecuteInternal();
});
};
const maybePromptEnrichBackgroundFallback = async (errMsg, stageName) => {
if (!['rnaseq_enrich', 'rnaseq_go_enrich', 'rnaseq_kegg_enrich'].includes(stageName)) return false;
const text = String(errMsg || '');
const foregroundMissing = (
text.includes('前景依赖缺失') ||
text.includes('family_DE_genes') ||
text.includes('global DE genes') ||
text.includes('全局 DE_genes') ||
text.includes('No family-specific DE genes') ||
text.includes('No global DE genes')
);
if (foregroundMissing) return false;
const backgroundError = (
text.includes('缺少背景注释文件') ||
text.includes('缺少 gene2ko 背景文件') ||
text.includes('Background annotation is required') ||
text.includes('你选择了 gene2go 背景文件') ||
text.includes('KEGG enrichment requires gene2ko')
);
if (!backgroundError) return false;
try {
await ElMessageBox.confirm(
`${text}\n\n是否切换为“自动注释 + 继续富集”?`,
'RNA-seq 富集背景缺失',
{
confirmButtonText: '自动注释并继续',
cancelButtonText: '我先去上传背景文件',
type: 'warning',
distinguishCancelAndClose: true
}
);
} catch (_err) {
return false;
}
confirmParams.enrichBackgroundSource = 'annotation';
addMessage('assistant', '🔄 已切换为自动注释模式,正在重新提交富集任务...');
return true;
};
// 【关键修复】检查 manifest 文件是否真的存在
const checkManifestExists = async (stageName, prefixOverride = null) => {
try {
const mappedStage = (
stageName === 'rnaseq_go_enrich' || stageName === 'rnaseq_kegg_enrich'
) ? 'rnaseq_enrich' : stageName;
const prefix = prefixOverride || confirmParams.prefix || params.prefix || '';
const jobId = jobInfo.job_id;
const encodedPrefix = encodeURIComponent(prefix);
const response = await fetch(`/api/stage-status?job_id=${jobId}&stage=${mappedStage}&prefix=${encodedPrefix}`);
const data = await response.json();
console.log(`[DEBUG-MANIFEST] checkManifestExists ${stageName}=>${mappedStage}:`, data.completed);
return data.completed === true;
} catch (e) {
console.log(`[DEBUG-MANIFEST] checkManifestExists ${stageName} error:`, e);
return false;
}
};
const pollTaskUntilComplete = async (taskId, stageName, prefixOverride = null) => {
console.log(`[DEBUG-POLL] pollTaskUntilComplete START: taskId=${taskId}, stageName=${stageName}`);
return new Promise((resolve, reject) => {
let pollCount = 0;
const checkStatus = async () => {
pollCount++;
console.log(`[DEBUG-POLL] checkStatus #${pollCount}: taskId=${taskId}`);
try {
const response = await fetch(`/api/task/${taskId}`);
const data = await response.json();
console.log(`[DEBUG-POLL] taskId=${taskId} response:`, data);
// 后端返回: completed, failed, running, pending
// 【关键修复】验证 manifest 文件是否真的存在
if (data.status === 'completed' || data.status === 'success') {
const manifestValid = await checkManifestExists(stageName, prefixOverride);
if (manifestValid) {
console.log(`[DEBUG-POLL] taskId=${taskId} COMPLETED (manifest verified)`);
resolve(data);
} else {
// API 返回 success 但 manifest 不存在,说明执行失败
console.log(`[DEBUG-POLL] taskId=${taskId} FAILED: manifest not found`);
reject(new Error(`${stageName} 执行失败:未找到输出文件`));
}
} else if (data.status === 'failed') {
console.log(`[DEBUG-POLL] taskId=${taskId} FAILED: ${data.message}`);
reject(new Error(data.message || '任务失败'));
} else {
// 继续轮询 (running 或 pending)
console.log(`[DEBUG-POLL] taskId=${taskId} status: ${data.status}, continuing poll in 2s...`);
setTimeout(checkStatus, 2000);
}
} catch (err) {
console.log(`[DEBUG-POLL] taskId=${taskId} ERROR:`, 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.result?.message || data.message || '未知错误';
const shouldRetry = await maybePromptEnrichBackgroundFallback(errMsg, stageName);
if (shouldRetry) {
executing.value = false;
isExecuting.value = false;
currentTask.value = null;
currentTaskId.value = null;
await doExecute();
return;
}
addMessage('assistant', `❌ 错误: ${errMsg}`);
// 设置日志内容(用于显示执行记录区域)
logContent.value = data.log || 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 backendStage = (
confirmParams.stage === 'rnaseq_go_enrich' || confirmParams.stage === 'rnaseq_kegg_enrich'
) ? 'rnaseq_enrich' : confirmParams.stage;
const response = await fetch('/api/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
stage: backendStage,
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,
rnaseqForegroundMode: confirmParams.rnaseqForegroundMode,
enrichBackgroundSource: confirmParams.enrichBackgroundSource,
enrichSpecies: confirmParams.enrichSpecies,
enrichKeyType: confirmParams.enrichKeyType,
enrichType: confirmParams.stage === 'rnaseq_go_enrich'
? 'go'
: (confirmParams.stage === 'rnaseq_kegg_enrich' ? 'kegg' : confirmParams.enrichType),
keggOrganism: confirmParams.keggOrganism,
// 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,
targetSpecies: confirmParams.targetSpecies,
refSpeciesList: confirmParams.refSpeciesList
})
});
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: `Multi.SPL.00001
Multi.SPL.00002
Multi.SPL.00003`,
sample: `X2 X2 Control
X3 X3 Treatment`,
fpkm: `geneID X2 X3
Multi.SPL.00001 10.5 2.3
Multi.SPL.00002 8.2 1.9
Multi.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',
annotation: '背景注释',
rnaseq_qc: 'RNA-seq质控',
rnaseq_align: 'RNA-seq比对',
rnaseq_quant: 'RNA-seq定量',
rnaseq_de: '差异表达分析',
rnaseq_enrich: 'RNA-seq富集分析',
rnaseq_go_enrich: 'RNA-seq GO富集分析',
rnaseq_kegg_enrich: 'RNA-seq KEGG富集分析'
};
return labels[stage] || stage;
};
// ========== 智能内容格式化 ==========
const formatMessage = (content, isAI = false) => {
if (!content) return '';
// 检测内容类型
const isStructured = isAI && detectStructuredContent(content);
let processed = 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>');
if (isStructured) {
// 结构化内容:智能处理RAG返回结果
processed = formatStructuredContent(processed);
} else {
// 普通内容:智能压缩多余换行
processed = processed
.replace(/\n{4,}/g, '\n\n') // 4个以上换行压缩为2个
.replace(/\n/g, '<br>');
}
return processed;
};
// 检测是否是结构化内容(RAG结果等)
const detectStructuredContent = (content) => {
// 检测特征:研究总结、编号列表、文献引用等
const patterns = [
/研究总结|基于.*文献|基于.*篇文献/i,
/^\d+\.\s+.+$/m, // 1. 2. 3. 编号
/转录因子|研究发现|研究结论/i,
/DNA BINDING|TRANSCRIPTION|GENE FAMILY/i, // 标签
/Arabidopsis|SPL.*BMC|doi\.org|PMID/i // 文献来源
];
return patterns.some(p => p.test(content));
};
// 格式化结构化内容(RAG结果)
const formatStructuredContent = (content) => {
let html = content;
// 1. 提取头部信息(如"基于10篇文献...")
html = html.replace(/^(基于\s*\d+\s*篇文献[^\n]*)/i,
'<div class="rag-header">📚 $1</div>');
// 2. 处理编号段落(1. 2. 3.)为rag-section
html = html.replace(/(\d+)\.\s*([^\n<]+)([\n<]|$)/g,
'<div class="rag-section">' +
'<div class="rag-section-title">$1. $2</div>');
// 3. 处理段落内容(直到下一个编号或标签行)
// 将连续的非编号文本作为段落内容
html = html.replace(/<div class="rag-section">([\s\S]*?)(?=<div class="rag-section">|<div class="rag-tags|$)/g,
(match, content) => {
// 提取标签行(如 DNA BINDING, TRANSCRIPTION FACTOR)
const tagPattern = /([A-Z][A-Z\s]*[A-Z])(?=,|$|<)/g;
const tags = [];
let cleanContent = content.replace(tagPattern, (match) => {
tags.push(match.trim());
return '';
});
// 提取来源信息
const sourcePattern = /(Arabidopsis[^,]*|doi[^\s]*|PMID[^\s]*)/i;
const sourceMatch = cleanContent.match(sourcePattern);
const source = sourceMatch ? sourceMatch[1] : '';
cleanContent = cleanContent.replace(sourcePattern, '');
// 清理多余字符
cleanContent = cleanContent.replace(/[,,]\s*$/, '').trim();
// 构建标签HTML
let tagsHtml = '';
if (tags.length > 0) {
tagsHtml = '<div class="rag-tags-row">' +
tags.map(t => `<span class="rag-tag">${t}</span>`).join('') +
(source ? `<span class="rag-source">📄 ${source}</span>` : '') +
'</div>';
}
return `<div class="rag-section">` +
(cleanContent ? `<div class="rag-section-content">${cleanContent}</div>` : '') +
tagsHtml +
`</div>`;
});
// 4. 处理底部提示(如"点击知识图谱查看更多关联")
html = html.replace(/([💡📝].*知识图谱.*关联.*)/i,
'<div class="rag-footer">$1</div>');
// 5. 包裹整个内容
if (!html.includes('rag-content-wrapper')) {
html = `<div class="rag-content-wrapper">${html}</div>`;
}
return html;
};
// ========== 动态样式类判断 ==========
// 根据内容判断消息间距类
const getMessageSpacingClass = (content) => {
if (!content) return 'compact';
const length = content.length;
const lines = content.split('\n').length;
if (length < 100 && lines <= 2) return 'compact';
if (length > 1000 || lines > 20) return 'spacious';
return 'normal';
};
// 根据内容判断气泡样式类
const getContentClass = (content) => {
if (!content) return 'medium-content';
const length = content.length;
const lines = content.split('\n').length;
const isStructured = detectStructuredContent(content);
if (isStructured) return 'structured-content';
if (length < 150 && lines <= 3) return 'short-content';
if (length > 800 || lines > 15) return 'long-content';
return 'medium-content';
};
// 添加Agent消息到会话
const addAgentMessage = async (role, content, intent = null) => {
const now = new Date();
agentSession.messages.push({
role,
content,
time: now.toLocaleTimeString(),
intent
});
if (intent && intentHistory) {
intentHistory.value.push(intent);
}
// 持久化到后端(如果有当前会话ID)
if (currentConversationId.value) {
try {
await fetch('/api/conversations/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: currentConversationId.value,
role: role,
content: content,
metadata: { intent }
})
});
} catch (e) {
console.error('[会话] 消息持久化失败:', e);
}
}
};
// Clear错误日志
const clearErrorLog = () => {
logContent.value = '';
ElMessage.success('错误日志已Clear');
};
// ============ 深度解读功能 ============
// 添加深度解读按钮到消息
const addInterpretationButton = (stageId, stageName) => {
const now = new Date();
const buttonId = `interpret-btn-${stageId}-${Date.now()}`;
agentSession.messages.push({
role: 'assistant',
content: `<div style="margin: 8px 0;">
<button
type="button"
class="interpret-btn"
data-stage-id="${stageId}"
data-stage-name="${stageName}"
id="${buttonId}"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 6px 12px; border-radius: 0px; cursor: pointer; font-size: 13px;">
🔍 深度解读 (${stageName})
</button>
<span style="color: #909399; font-size: 12px; margin-left: 8px;">
查看阶段性综合报告
</span>
</div>`,
time: now.toLocaleTimeString(),
isHtml: true,
stageId: stageId
});
};
// 事件委托:处理深度解读按钮点击
const handleInterpretButtonClick = (e) => {
const btn = e.target.closest('.interpret-btn');
if (btn) {
const stageId = btn.dataset.stageId;
const stageName = btn.dataset.stageName;
if (stageId && stageName) {
showInterpretationModal(stageId, stageName);
}
}
};
// 显示深度解读弹窗
const showInterpretationModal = async (stageId, stageName) => {
console.log('[深度解读] 请求解读:', stageId, stageName);
try {
const res = await fetch('/api/agent/interpret', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId.value || 'demo_session',
stage: stageId,
include_previous: true,
mode: 'comprehensive'
})
});
const data = await res.json();
if (data.success) {
// 打开解读弹窗
interpretationDialogVisible.value = true;
interpretationReport.value = data.report;
currentInterpretStage.value = { id: stageId, name: stageName };
} else {
ElMessage.error(data.message || '生成解读失败');
}
} catch (err) {
console.error('[深度解读] 请求失败:', err);
ElMessage.error('请求解读服务失败');
}
};
// 下载解读报告
const downloadInterpretationReport = async (format) => {
if (!interpretationReport.value) return;
const stage = currentInterpretStage.value;
const session = sessionId.value || 'demo_session';
try {
const res = await fetch(`/api/agent/report/${session}/download?format=${format}&stage=${stage.id}`);
if (format === 'json') {
const data = await res.json();
console.log('报告数据:', data);
ElMessage.success('JSON数据已加载到控制台');
} else {
// 下载文件
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${session}_${stage.id}.${format === 'markdown' ? 'md' : format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
ElMessage.success(`${format.toUpperCase()}报告已下载`);
}
} catch (err) {
console.error('[下载报告] 失败:', err);
ElMessage.error('下载报告失败');
}
};
// 复制解读内容到剪贴板
const copyInterpretationToClipboard = () => {
if (!interpretationReport.value) return;
const story = interpretationReport.value.biological_story || '';
navigator.clipboard.writeText(story).then(() => {
ElMessage.success('生物学故事已复制到剪贴板');
}).catch(() => {
ElMessage.error('复制失败');
});
};
// 自动滚动
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
});
// 存储批量执行信息
if (isBatchExecuting.value) {
startBackgroundBatchExecution(stages, {
stages,
stages_detail: stages.map(stage => ({
id: getStageId(stage),
name: getStageLabel(stage),
params: intent.params || {}
})),
params: intent.params || {},
prefix: intent.params?.prefix || params.prefix || ''
}, { intent });
return true;
}
batchExecuteStages.value = stages;
batchExecuteIndex.value = 0;
batchExecuteParams.value = {
...intent.params,
stages: stages
};
// 显示批量执行确认对话框
batchExecuteDialogVisible.value = true;
addAgentMessage('assistant', `⚙️ 准备批量执行 ${stages.length} 个阶段: ${stages.join(', ')}`);
// 启动15秒倒计时
startBatchExecuteTimer();
};
const showSyntenyChoice = (intent, userInput) => {
syntenyChoiceQuery.value = userInput;
syntenyChoiceBaseIntent.value = intent || {};
syntenyChoiceOptions.value = intent.options || [
{
id: 'synteny',
label: '种内共线性分析',
description: '分析同一物种基因组内的共线性关系',
stages: ['identify', 'synteny']
},
{
id: 'interspecies_synteny',
label: '种间共线性分析',
description: '比较不同物种之间的共线性关系',
stages: ['identify', 'phylogeny', 'interspecies_synteny']
}
];
syntenyChoiceDialogVisible.value = true;
addAgentMessage('assistant', intent.clarification_question || '请选择要执行哪一种共线性分析。', {
taskType: 'clarification',
options: syntenyChoiceOptions.value
});
};
const runPlanForIntent = async (userInput, intent) => {
const planResponse = await fetch('/api/agent/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userInput,
session_id: sessionId.value,
job_id: jobInfo.job_id,
prefix: params.prefix || '',
history: agentSession.messages.slice(-5),
intent: intent
})
});
const planData = await planResponse.json();
console.log('[DEBUG] Plan API response:', planData);
if (!planData.success || !planData.plan) {
const errorMsg = planData.message || '规划失败';
const planHint = /缺少|输入文件|upload|上传|file/i.test(errorMsg)
? '\n\n请检查必要输入文件是否已上传。'
: '\n\n规划阶段未能生成可执行流程,请查看后端日志中的具体原因。';
ElMessage.error(`❌ ${errorMsg}`);
addAgentMessage('assistant', `❌ 无法执行分析:${errorMsg}${planHint}`, {
taskType: 'error'
});
return false;
}
const plan = planData.plan;
const completedSet = new Set(planData.completed_stages || []);
let stages = (plan.stages || []).filter(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
return !completedSet.has(stageId);
});
let stagesDetail = (plan.stages_detail || []).filter(stage => !completedSet.has(stage.id || stage.name));
if (stages.length === 0) {
addAgentMessage('assistant', plan.description || '所有相关分析阶段已完成,无需重复执行。', {
taskType: 'analysis_completed',
plan: plan
});
return true;
}
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 += `分析阶段 (${stages.length}个):\n`;
stagesDetail.forEach((stage, idx) => {
const deps = stage.dependencies?.length > 0
? `(依赖: ${stage.dependencies.join(', ')})`
: '';
const parallel = stage.can_parallel ? ' [可并行]' : '';
const stageId = stage.id || stage.name;
const isCompleted = planData.completed_stages && planData.completed_stages.includes(stageId);
const statusTag = isCompleted ? ' ✅已完成' : ' ⏳待执行';
planMessage += `${idx + 1}. ${stage.name} ${deps}${parallel}${statusTag}\n`;
});
addAgentMessage('assistant', planMessage, {
stage: stages[0],
stages: stages,
taskType: 'batch_analysis',
params: plan.params,
plan: plan,
confidence: planData.intent?.confidence || 0.9
});
if (isBatchExecuting.value) {
batchExecuteDialogVisible.value = false;
startBackgroundBatchExecution(stages, plan, planData);
return true;
}
batchExecuteStages.value = stages;
batchExecuteIndex.value = 0;
batchExecuteParams.value = {
...plan.params,
stages: stages,
plan: plan
};
batchExecuteDialogVisible.value = true;
addAgentMessage('assistant', `⚙️ 准备执行 ${stages.length} 个分析阶段,请确认参数`);
startBatchExecuteTimer();
return true;
};
const confirmSyntenyChoice = async (option) => {
syntenyChoiceDialogVisible.value = false;
nlLoading.value = true;
const baseIntent = syntenyChoiceBaseIntent.value || {};
const selectedIntent = {
...baseIntent,
stages: option.stages,
stage: option.stages[0],
primary_stage: option.id,
description: option.description,
needs_clarification: false,
clarification_type: null,
taskType: 'analysis'
};
addAgentMessage('assistant', `已选择:${option.label}`);
try {
await runPlanForIntent(syntenyChoiceQuery.value || option.label, selectedIntent);
} catch (err) {
console.error('[DEBUG] Synteny choice plan error:', err);
ElMessage.error(`❌ ${err.message || '规划请求失败'}`);
addAgentMessage('assistant', `❌ 服务暂时不可用:${err.message || '规划请求失败'}\n\n请稍后重试。`, {
taskType: 'error'
});
} finally {
nlInput.value = '';
nlLoading.value = false;
}
};
const cancelSyntenyChoice = () => {
syntenyChoiceDialogVisible.value = false;
nlLoading.value = false;
nlInput.value = '';
};
const getStageId = (stage) => typeof stage === 'object' ? stage.id : stage;
const getStageName = (stage) => typeof stage === 'object' ? (stage.name || stage.id) : getStageLabel(stage);
const fetchStageCompleted = async (stageId, prefixValue = null) => {
try {
const jobId = jobInfo.job_id;
const encodedPrefix = encodeURIComponent(prefixValue || params.prefix || '');
const response = await fetch(`/api/stage-status?job_id=${jobId}&stage=${stageId}&prefix=${encodedPrefix}`);
const data = await response.json();
return data.completed === true;
} catch (e) {
console.log(`[BACKGROUND] stage-status check failed for ${stageId}:`, e);
return false;
}
};
const executeStageDirect = async (stageId, stageParams = {}) => {
const backendStageId = (
stageId === 'rnaseq_go_enrich' || stageId === 'rnaseq_kegg_enrich'
) ? 'rnaseq_enrich' : stageId;
const requestBody = {
stage: backendStageId,
job_id: jobInfo.job_id,
prefix: stageParams.prefix || params.prefix || '',
cpu: stageParams.cpu || 8
};
if (stageId === 'ppi') {
requestBody.ppiSpecies = stageParams.ppiSpecies || confirmParams.ppiSpecies || 'ath';
requestBody.ppiScore = stageParams.ppiScore || confirmParams.ppiScore || 700;
}
if (stageId === 'kaks') {
requestBody.kaksNeutralRate = stageParams.kaksNeutralRate || confirmParams.kaksNeutralRate || 7.08e-9;
}
if (stageId === 'expression') {
requestBody.geoId = stageParams.geoId || confirmParams.geoId || '';
requestBody.idTransform = stageParams.idTransform || confirmParams.idTransform || 'remove_T01';
}
if (stageId.startsWith('rnaseq')) {
requestBody.rnaseqMode = stageParams.rnaseqMode || confirmParams.rnaseqMode || 'upload';
requestBody.speciesName = stageParams.speciesName || confirmParams.speciesName || '';
requestBody.rnaseqForegroundMode = stageParams.rnaseqForegroundMode || confirmParams.rnaseqForegroundMode || 'auto';
}
if (stageId === 'rnaseq_enrich' || stageId === 'rnaseq_go_enrich' || stageId === 'rnaseq_kegg_enrich') {
requestBody.rnaseqForegroundMode = stageParams.rnaseqForegroundMode || 'auto';
if (stageId === 'rnaseq_go_enrich') {
requestBody.enrichType = 'go';
} else if (stageId === 'rnaseq_kegg_enrich') {
requestBody.enrichType = 'kegg';
}
requestBody.enrichBackgroundSource = stageParams.enrichBackgroundSource || confirmParams.enrichBackgroundSource || 'auto';
requestBody.enrichSpecies = stageParams.enrichSpecies || confirmParams.enrichSpecies || '';
requestBody.enrichKeyType = stageParams.enrichKeyType || confirmParams.enrichKeyType || 'auto';
requestBody.enrichType = requestBody.enrichType || stageParams.enrichType || confirmParams.enrichType || 'go';
requestBody.keggOrganism = stageParams.keggOrganism || confirmParams.keggOrganism || '';
}
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) {
throw new Error(data.message || data.error || '执行失败');
}
if (data.task_id) {
return await pollTaskUntilComplete(data.task_id, stageId, requestBody.prefix);
}
if (data.async === false) {
return data;
}
throw new Error(data.message || '执行失败');
};
const startBackgroundBatchExecution = async (stages, plan, planData = {}) => {
const batchId = `bg_${Date.now()}`;
const stageIds = stages.map(getStageId).filter(Boolean);
const stageMap = {};
stages.forEach(stage => {
stageMap[getStageId(stage)] = stage;
});
const localStatus = {};
const prefixValue = plan.prefix || params.prefix || '';
addAgentMessage('assistant', `📌 当前已有前台批次执行中,新任务已转为后台执行:${stageIds.map(getStageLabel).join(', ')}`, {
taskType: 'background_batch',
batchId,
stages: stageIds
});
const knownStages = new Set([
...Object.keys(stageDependencies),
...stageIds,
'identify', 'phylogeny', 'ortholog'
]);
for (const stageId of knownStages) {
localStatus[stageId] = await fetchStageCompleted(stageId, prefixValue) ? 'completed' : 'pending';
}
stageIds.forEach(stageId => {
if (localStatus[stageId] !== 'completed') {
localStatus[stageId] = 'pending';
}
});
let completed = 0;
let failed = 0;
while (true) {
const pending = stageIds.filter(stageId => localStatus[stageId] === 'pending');
if (pending.length === 0) break;
const executable = pending.filter(stageId => {
const deps = stageDependencies[stageId] || [];
return deps.every(dep => localStatus[dep] === 'completed');
});
if (executable.length === 0) {
const blocked = pending.filter(stageId => {
const deps = stageDependencies[stageId] || [];
return deps.some(dep => localStatus[dep] === 'failed');
});
if (blocked.length > 0) {
blocked.forEach(stageId => {
localStatus[stageId] = 'failed';
failed++;
});
addAgentMessage('assistant', `⚠️ 后台批次中以下阶段因依赖失败被跳过: ${blocked.map(getStageLabel).join(', ')}`);
continue;
}
addAgentMessage('assistant', `⚠️ 后台批次无法继续:${pending.map(getStageLabel).join(', ')} 的依赖尚未完成`);
pending.forEach(stageId => {
localStatus[stageId] = 'failed';
failed++;
});
break;
}
await Promise.all(executable.map(async (stageId) => {
localStatus[stageId] = 'running';
const stage = stageMap[stageId] || stageId;
const stageName = getStageName(stage);
addAgentMessage('assistant', `⚙️ 后台执行阶段: ${stageName}...`);
try {
const stageParams = (plan.stages_detail || []).find(s => s.id === stageId)?.params || plan.params || {};
await executeStageDirect(stageId, stageParams);
localStatus[stageId] = 'completed';
completed++;
addAgentMessage('assistant', `✅ 后台阶段 ${stageName} 完成`);
} catch (err) {
localStatus[stageId] = 'failed';
failed++;
addAgentMessage('assistant', `❌ 后台阶段 ${stageName} 失败: ${err.message}`);
}
}));
}
addAgentMessage('assistant', `📦 后台批次完成:成功 ${completed}/${stageIds.length},失败 ${failed}/${stageIds.length}`);
await refreshResults();
};
// 处理自然语言Input
const handleNlInput = async () => {
if (!nlInput.value.trim()) {
ElMessage.warning('请Input内容');
return;
}
const userInput = nlInput.value.trim();
nlLoading.value = true;
// V2: RAG模式 - 文献问答
if (ragMode.value) {
try {
addMessage('user', userInput);
ragHistory.value.push({ role: 'user', content: userInput, time: new Date().toLocaleTimeString() });
// 调用RAG API (gene-family-mas-v2 端口8000)
const response = await fetch('http://localhost:7867/api/v1/rag/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: userInput,
mode: 'substring',
top_k: 5
})
});
const data = await response.json();
const answer = data.response || data.answer || '未找到相关信息';
addMessage('assistant', answer);
ragHistory.value.push({ role: 'assistant', content: answer, time: new Date().toLocaleTimeString() });
// 滚动到底部
setTimeout(() => {
if (chatArea.value) {
chatArea.value.scrollTop = chatArea.value.scrollHeight;
}
}, 100);
} catch (err) {
ElMessage.error('RAG请求失败: ' + err.message);
addMessage('assistant', '抱歉,文献检索服务暂时不可用。');
} finally {
nlInput.value = '';
nlLoading.value = false;
}
return;
}
// Agent模式 - 技能执行
try {
// 检查是否有绑定的 job
if (!jobInfo.job_id || jobInfo.job_id === 'default_job') {
ElMessage.warning('请先选择或创建 Working Directory');
nlLoading.value = false;
return;
}
// 如果没有当前会话,先自动创建一个
if (!currentConversationId.value) {
await createNewConversation();
console.log('[会话] 自动创建会话:', currentConversationId.value);
}
// 添加用户消息到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';
if (data.intent.needs_clarification || data.intent.taskType === 'clarification') {
autoDetectedMode.value = 'skill';
showSyntenyChoice(data.intent, userInput);
return;
}
// 判断使用哪种模式:自动检测优先,但考虑用户偏好
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';
// 调用 bbav5 后端 RAG API(由后端转发到 gene-family-mas-v2)
try {
const ragResponse = await fetch('/api/agent/rag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: userInput,
mode: 'rag',
top_k: 5
})
});
const ragData = await ragResponse.json();
const answer = ragData.success ? ragData.answer : (ragData.message || 'No relevant information found.');
addAgentMessage('assistant', answer, {
stage: null,
taskType: 'faq',
params: {},
confidence: data.intent.confidence,
sources: ragData.sources || []
});
} 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...');
console.log('[DEBUG] Passing intent from chat:', data.intent);
try {
const planResponse = await fetch('/api/agent/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userInput,
session_id: sessionId.value,
job_id: jobInfo.job_id, // 传递当前 job_id 用于查询已完成阶段
prefix: params.prefix || '',
history: agentSession.messages.slice(-5),
intent: data.intent // 传递 chat 返回的 intent,避免重复调用 IntentAgentV2
})
});
const planData = await planResponse.json();
console.log('[DEBUG] Plan API response:', planData);
if (planData.success && planData.plan) {
const plan = planData.plan;
const completedSet = new Set(planData.completed_stages || []);
let stages = (plan.stages || []).filter(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
return !completedSet.has(stageId);
});
let stagesDetail = (plan.stages_detail || []).filter(stage => !completedSet.has(stage.id || stage.name));
if (stages.length === 0) {
addAgentMessage('assistant', plan.description || '所有相关分析阶段已完成,无需重复执行。', {
taskType: 'analysis_completed',
plan: plan
});
nlLoading.value = false;
nlInput.value = '';
return;
}
// 显示智能规划结果
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 += `分析阶段 (${stages.length}个):\n`;
stagesDetail.forEach((stage, idx) => {
const deps = stage.dependencies?.length > 0
? `(依赖: ${stage.dependencies.join(', ')})`
: '';
const parallel = stage.can_parallel ? ' [可并行]' : '';
// 检查是否已完成
const stageId = stage.id || stage.name;
const isCompleted = planData.completed_stages && planData.completed_stages.includes(stageId);
const statusTag = isCompleted ? ' ✅已完成' : ' ⏳待执行';
planMessage += `${idx + 1}. ${stage.name} ${deps}${parallel}${statusTag}\n`;
});
addAgentMessage('assistant', planMessage, {
stage: stages[0],
stages: stages,
taskType: 'batch_analysis',
params: plan.params,
plan: plan,
confidence: planData.intent?.confidence || 0.9
});
if (isBatchExecuting.value) {
startBackgroundBatchExecution(stages, plan, planData);
nlLoading.value = false;
nlInput.value = '';
return;
}
// 存储批量执行信息
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 {
// 规划失败(如PreFlight验证失败),显示错误并停止
const errorMsg = planData.message || "规划失败";
console.log('[DEBUG] Plan API failed:', errorMsg);
ElMessage.error(`❌ ${errorMsg}`);
const planHint = /缺少|输入文件|upload|上传|file/i.test(errorMsg)
? '\n\n请检查必要输入文件是否已上传。'
: '\n\n规划阶段未能生成可执行流程,请查看后端日志中的具体原因。';
addAgentMessage('assistant', `❌ 无法执行分析:${errorMsg}${planHint}`, {
taskType: 'error'
});
nlLoading.value = false;
nlInput.value = '';
return;
}
} catch (planErr) {
console.error('[DEBUG] Plan API error:', planErr);
// API调用异常,显示错误并停止
const errorMsg = planErr.message || "规划请求失败";
ElMessage.error(`❌ ${errorMsg}`);
addAgentMessage('assistant', `❌ 服务暂时不可用:${errorMsg}\n\n请稍后重试。`, {
taskType: 'error'
});
nlLoading.value = false;
nlInput.value = '';
return;
}
}
} else {
addAgentMessage('assistant', data.message || '抱歉,我没有理解你的意思。请尝试更详细地描述。');
}
} catch (err) {
ElMessage.error('请求失败: ' + err.message);
addAgentMessage('assistant', '抱歉,服务暂时不可用,请稍后重试。');
} finally {
nlInput.value = '';
nlLoading.value = false;
}
};
// Refresh结果
const refreshResults = async (jobId = null) => {
try {
const url = jobId ? `/api/results?job_id=${jobId}` : '/api/results';
const response = await fetch(url);
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.files.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();
loadConversations(); // 加载会话列表
// 添加深度解读按钮的事件委托监听
document.addEventListener('click', handleInterpretButtonClick);
});
// 使用 Vue 3 的 onUnmounted
const { onUnmounted } = Vue;
onUnmounted(() => {
cleanup();
stopAgentTaskPolling();
// 移除深度解读按钮的事件监听
document.removeEventListener('click', handleInterpretButtonClick);
});
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,
interspeciesSpeciesOptions,
interspeciesSpeciesLoading,
executing,
executeElapsed,
autoExecute,
autoExecuteCountdown,
clearAutoExecuteTimer,
// 批量执行
batchExecuteDialogVisible,
batchExecuteStages,
batchExecuteIndex,
batchExecuteCountdown,
isBatchExecuting,
shouldStopBatchExecute,
currentBatchStage,
startBatchExecute,
cancelBatchExecute,
stopBatchExecute,
syntenyChoiceDialogVisible,
syntenyChoiceOptions,
confirmSyntenyChoice,
cancelSyntenyChoice,
// 依赖驱动并行执行
stageDependencies,
stageExecutionStatus,
runningStages,
maxParallelStages,
// 阶段确认对话框
stageConfirmDialogVisible,
stageConfirmCountdown,
stageConfirmCurrent,
stageConfirmTotal,
stageConfirmQueue,
processStageConfirmQueue,
confirmStageExecute,
skipStageExecute,
executeStagesWithDependencies,
executeStageWithConfirmation,
stages,
// 异步任务轮询
currentTask,
// Stop Execution相关
isExecuting,
isStopping,
currentTaskId,
stopExecution,
// 新版结果区域变量
activeStages,
selectedResultStage,
selectedFile,
selectedImage,
selectedImageUrl,
selectedImageSize,
zoomLevel,
previewContentRef,
// 新版结果区域方法
isImageFile,
selectFile,
zoomIn,
zoomOut,
resetZoom,
toggleFullscreen,
downloadImage,
onImageLoad,
onImageError,
onZoomChange,
getFileName,
selectImageFile,
// MCScanX HTML结果
mcscanxHtmlFiles,
openMcscanxHtml,
// Agent Tasks
agentTasks,
intentHistory,
uploadedFiles,
formatTime,
showTaskDetail,
// 原有
previewDialogVisible,
previewImg,
previewParams,
resultDialogVisible,
resultDialogParams,
formatSize,
onStageChange,
showConfirm,
doExecute,
handleUploadSuccess,
handleUploadError,
handleDownloadExample,
loadFromPath,
refreshResults,
openPreview,
redrawImage,
createNewJob,
// Working DirectorySelect
existingJobs,
jobSelectorVisible,
selectedJobId,
showJobSelector,
selectJob,
// V2: RAG相关
ragMode,
userId,
sessionId,
ragHistory,
toggleRagMode,
clearRagSession,
// 偏好模式和自动检测
preferenceMode,
autoDetectedMode,
togglePreferenceMode,
// 错误日志
clearErrorLog,
// 会话管理
apiBaseUrl,
currentConversationId,
conversations,
filteredConversations,
createNewConversation,
selectConversation,
// 新会话选择器
recentConversations,
moreConversations,
// 深度解读功能
interpretationDialogVisible,
interpretationReport,
currentInterpretStage,
addInterpretationButton,
downloadInterpretationReport,
copyInterpretationToClipboard,
// 诊断修复确认
diagnosisConfirmDialogVisible,
diagnosisConfirmCountdown,
diagnosisFixData,
startDiagnosisConfirmTimer,
clearDiagnosisConfirmTimer,
showDiagnosisConfirm,
confirmDiagnosisFix,
rejectDiagnosisFix,
startDiagnosisConfirmPolling,
stopDiagnosisConfirmPolling,
checkDiagnosisConfirmation,
// 动态样式系统
getMessageSpacingClass,
getContentClass,
detectStructuredContent,
formatStructuredContent
};
}
});
// 注册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>
<!-- 深度解读弹窗样式 -->
<style>
.interpretation-dialog .el-dialog__body { padding: 0; max-height: 70vh; overflow-y: auto; }
.report-container { padding: 20px; }
.report-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; margin: -20px -20px 20px -20px; }
.report-title { font-size: 20px; font-weight: bold; margin-bottom: 10px; }
.report-meta { font-size: 14px; opacity: 0.9; }
.flow-visualization { display: flex; align-items: center; justify-content: center; padding: 20px; background: #f8f9fa; border-radius: 0px; margin: 20px 0; }
.flow-stage { text-align: center; padding: 10px 20px; }
.flow-stage-icon { font-size: 24px; margin-bottom: 5px; }
.flow-stage-name { font-size: 14px; font-weight: bold; }
.flow-stage-result { font-size: 12px; color: #666; margin-top: 5px; }
.flow-arrow { font-size: 24px; color: #409eff; margin: 0 10px; }
.section-title { font-size: 18px; font-weight: bold; margin: 30px 0 15px 0; padding-bottom: 10px; border-bottom: 2px solid #409eff; }
.connection-card { background: #f0f9ff; border-left: 4px solid #409eff; padding: 15px; margin: 15px 0; border-radius: 0px; }
.connection-header { font-weight: bold; color: #409eff; margin-bottom: 10px; }
.finding-item { background: white; border: 1px solid #e4e7ed; padding: 15px; margin: 10px 0; border-radius: 0px; }
.finding-title { font-weight: bold; color: #303133; margin-bottom: 8px; }
.finding-desc { color: #606266; font-size: 14px; line-height: 1.6; }
.finding-evidence { color: #909399; font-size: 12px; margin-top: 8px; font-style: italic; }
.story-box { background: #fdf6ec; border: 1px solid #f5dab1; padding: 20px; border-radius: 0px; line-height: 1.8; color: #5d4a2a; }
.next-step-item { display: flex; align-items: flex-start; padding: 12px; background: white; border: 1px solid #e4e7ed; margin: 8px 0; border-radius: 0px; }
.next-step-priority { padding: 2px 8px; border-radius: 0px; font-size: 12px; margin-right: 12px; }
.priority-high { background: #fef0f0; color: #f56c6c; }
.priority-medium { background: #fdf6ec; color: #e6a23c; }
.next-step-content { flex: 1; }
.next-step-title { font-weight: bold; margin-bottom: 4px; }
.next-step-desc { font-size: 13px; color: #606266; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 15px 20px; border-top: 1px solid #e4e7ed; }
</style>
</body>
</html>