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

6499 lines
327 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>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>