392 lines
16 KiB
HTML
Executable File
392 lines
16 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<script src="/static/i18n.js"></script>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BotanicalBaseAgent</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.3.14/dist/index.full.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@element-plus/icons-vue@2.1.0/dist/index.iife.js"></script>
|
|
<link rel="stylesheet" href="conversation-sidebar.css">
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
|
background: #f5f7fa;
|
|
}
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 15px 20px;
|
|
}
|
|
.header h1 { margin: 0; font-size: 22px; }
|
|
.main-container {
|
|
display: flex;
|
|
height: calc(100vh - 60px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 左侧控制面板 */
|
|
.left-panel {
|
|
width: 280px;
|
|
background: white;
|
|
border-right: 1px solid #e4e7ed;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 15px;
|
|
gap: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 中间内容区 */
|
|
.center-panel {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 15px;
|
|
gap: 15px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 右侧结果区 */
|
|
.right-panel {
|
|
width: 350px;
|
|
background: white;
|
|
border-left: 1px solid #e4e7ed;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.panel-section {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
color: #409EFF;
|
|
margin-bottom: 12px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 2px solid #409EFF;
|
|
}
|
|
|
|
.chat-area {
|
|
flex: 1;
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.chat-message {
|
|
margin-bottom: 15px;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
max-width: 80%;
|
|
}
|
|
|
|
.chat-message.user {
|
|
background: #ecf5ff;
|
|
margin-left: auto;
|
|
text-align: right;
|
|
}
|
|
|
|
.chat-message.assistant {
|
|
background: #f0f9eb;
|
|
}
|
|
|
|
.input-area {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.stage-tag {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
margin: 4px;
|
|
}
|
|
|
|
.stage-tag.success { background: #67C23A; color: white; }
|
|
.stage-tag.running { background: #E6A23C; color: white; }
|
|
.stage-tag.pending { background: #909399; color: white; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<h1>{{ t('gf-analysis-system') }}</h1>
|
|
</div>
|
|
|
|
<div class="main-container">
|
|
<!-- 左侧:会话侧边栏 + 控制面板 -->
|
|
<div class="left-panel">
|
|
<!-- 会话侧边栏 -->
|
|
<div class="conversation-sidebar" style="width: 100%; margin: -15px; margin-bottom: 0;">
|
|
<div class="sidebar-header">
|
|
<button class="new-chat-btn" @click="createNewConversation" style="flex: 1;">
|
|
<el-icon><plus /></el-icon>
|
|
<span>New Chat</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="search-box" style="padding: 12px;">
|
|
<el-input
|
|
v-model="searchQuery"
|
|
:placeholder="t('search-conv')"
|
|
prefix-icon="search"
|
|
clearable
|
|
size="small">
|
|
</el-input>
|
|
</div>
|
|
|
|
<div class="conversation-list" style="max-height: 300px;">
|
|
<div class="list-section">
|
|
<div class="section-title">{{ t('conv-list') }}</div>
|
|
<div v-for="conv in filteredConversations"
|
|
:key="conv.id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConv?.id === conv.id }"
|
|
@click="selectConversation(conv)">
|
|
<div class="item-icon">
|
|
<el-icon><chat-dot-round /></el-icon>
|
|
</div>
|
|
<div class="item-content">
|
|
<div class="item-title">{{ conv.title }}</div>
|
|
<div class="item-meta">
|
|
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
|
|
<span class="message-count">{{ conv.message_count }}{{ t('msg-count') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 分析控制 -->
|
|
<div class="panel-section">
|
|
<div class="panel-title">{{ t('analysis-stage') }}</div>
|
|
<el-select v-model="selectedStage" :placeholder="t('select-stage')" style="width: 100%;">
|
|
<el-option :label="t('identify-label')" value="identify"></el-option>
|
|
<el-option :label="t('phylogeny-label')" value="phylogeny"></el-option>
|
|
<el-option :label="t('motif-label')" value="motif"></el-option>
|
|
<el-option :label="t('domain-label')" value="domain"></el-option>
|
|
<el-option :label="t('expression-label')" value="expression"></el-option>
|
|
</el-select>
|
|
<el-button type="primary" style="width: 100%; margin-top: 10px;" @click="executeAnalysis">
|
|
{{ t('execute-analysis') }}
|
|
</el-button>
|
|
</div>
|
|
|
|
<!-- 参数设置 -->
|
|
<div class="panel-section">
|
|
<div class="panel-title">{{ t('param-settings') }}</div>
|
|
<el-form :model="params" label-width="80px" size="small">
|
|
<el-form-item :label="t('prefix')">
|
|
<el-input v-model="params.prefix" :placeholder="t('prefix-placeholder')"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="CPU">
|
|
<el-input-number v-model="params.cpu" :min="1" :max="32" style="width: 100%;"></el-input-number>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 中间:对话区 -->
|
|
<div class="center-panel">
|
|
<div class="chat-area">
|
|
<div v-for="(msg, index) in messages" :key="index"
|
|
class="chat-message" :class="msg.role">
|
|
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">
|
|
{{ msg.role === 'user' ? t('user') : t('ai-assistant') }} · {{ msg.time }}
|
|
</div>
|
|
<div>{{ msg.content }}</div>
|
|
</div>
|
|
<div v-if="messages.length === 0" style="text-align: center; color: #999; padding: 40px;">
|
|
<el-icon :size="48" style="display: block; margin-bottom: 10px;"><chat-line-round /></el-icon>
|
|
<p>{{ t('start-new-chat') }}</p>
|
|
<p style="font-size: 12px;">{{ t('or-continue') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-area">
|
|
<el-input
|
|
v-model="userInput"
|
|
type="textarea"
|
|
:rows="3"
|
|
:placeholder="t('describe-need')"
|
|
@keyup.enter.ctrl="sendMessage">
|
|
</el-input>
|
|
<el-button type="primary" size="large" @click="sendMessage" :disabled="!userInput.trim()">
|
|
{{ t('send') }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:结果展示 -->
|
|
<div class="right-panel">
|
|
<div class="panel-title">📊 Analysis Results</div>
|
|
|
|
<div v-for="stage in stages" :key="stage.name" style="margin-bottom: 15px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-weight: 500;">{{ stage.label }}</span>
|
|
<span class="stage-tag" :class="stage.status">{{ stage.statusText }}</span>
|
|
</div>
|
|
<el-progress :percentage="stage.progress" :status="stage.status"></el-progress>
|
|
</div>
|
|
|
|
<el-divider></el-divider>
|
|
|
|
<div style="text-align: center; color: #999; padding: 20px;">
|
|
<el-icon :size="48" style="display: block; margin-bottom: 10px;"><picture /></el-icon>
|
|
<p>{{ t('results-here') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, reactive, computed } = Vue;
|
|
|
|
const app = createApp({
|
|
setup() {
|
|
const t = window.t;
|
|
document.title = 'BotanicalBaseAgent - ' + t('gf-analysis');
|
|
// 会话管理
|
|
const conversations = ref([
|
|
{ id: 1, title: 'Ft.SPL基因家族分析', gene_family: 'SPL', message_count: 12 },
|
|
{ id: 2, title: 'Os.TCP进化分析', gene_family: 'TCP', message_count: 5 },
|
|
{ id: 3, title: 'HMMER参数配置咨询', gene_family: null, message_count: 8 },
|
|
]);
|
|
const currentConv = ref(null);
|
|
const searchQuery = ref('');
|
|
|
|
const filteredConversations = computed(() => {
|
|
if (!searchQuery.value) return conversations.value;
|
|
const query = searchQuery.value.toLowerCase();
|
|
return conversations.value.filter(conv =>
|
|
conv.title.toLowerCase().includes(query) ||
|
|
(conv.gene_family && conv.gene_family.toLowerCase().includes(query))
|
|
);
|
|
});
|
|
|
|
const createNewConversation = () => {
|
|
const newConv = {
|
|
id: Date.now(),
|
|
title: t('new-chat') + ' ' + new Date().toLocaleTimeString(),
|
|
gene_family: null,
|
|
message_count: 0
|
|
};
|
|
conversations.value.unshift(newConv);
|
|
selectConversation(newConv);
|
|
};
|
|
|
|
const selectConversation = (conv) => {
|
|
currentConv.value = conv;
|
|
// 清空消息并添加欢迎语
|
|
messages.value = [{
|
|
role: 'assistant',
|
|
content: t('initial-welcome').replace('{title}', conv.title),
|
|
time: new Date().toLocaleTimeString()
|
|
}];
|
|
};
|
|
|
|
// 分析参数
|
|
const selectedStage = ref('');
|
|
const params = reactive({
|
|
prefix: 'Ft.SPL',
|
|
cpu: 8
|
|
});
|
|
|
|
// 消息
|
|
const messages = ref([]);
|
|
const userInput = ref('');
|
|
|
|
const sendMessage = () => {
|
|
if (!userInput.value.trim()) return;
|
|
|
|
// 添加用户消息
|
|
messages.value.push({
|
|
role: 'user',
|
|
content: userInput.value,
|
|
time: new Date().toLocaleTimeString()
|
|
});
|
|
|
|
// 更新当前会话消息数
|
|
if (currentConv.value) {
|
|
currentConv.value.message_count++;
|
|
}
|
|
|
|
// 模拟AI回复
|
|
setTimeout(() => {
|
|
messages.value.push({
|
|
role: 'assistant',
|
|
content: `Received: "${userInput.value}". I will help analyze the gene family. Confirm parameters and click "${t('execute-analysis')}".`,
|
|
time: new Date().toLocaleTimeString()
|
|
});
|
|
if (currentConv.value) {
|
|
currentConv.value.message_count++;
|
|
}
|
|
}, 500);
|
|
|
|
userInput.value = '';
|
|
};
|
|
|
|
// 分析阶段
|
|
const stages = ref([
|
|
{ name: 'identify', label: 'Identify', status: 'success', statusText: t('completed'), progress: 100 },
|
|
{ name: 'phylogeny', label: 'Phylogeny', status: 'running', statusText: t('running'), progress: 60 },
|
|
{ name: 'motif', label: 'Motif', status: 'pending', statusText: t('pending'), progress: 0 },
|
|
{ name: 'domain', label: 'Domain', status: 'pending', statusText: t('pending'), progress: 0 },
|
|
]);
|
|
|
|
const executeAnalysis = () => {
|
|
if (!selectedStage.value) {
|
|
ElementPlus.ElMessage.warning(t('msg-select-stage-first'));
|
|
return;
|
|
}
|
|
ElementPlus.ElMessage.success(t('msg-executing') + selectedStage.value + t('msg-analysis-text'));
|
|
};
|
|
|
|
return {
|
|
t,
|
|
conversations,
|
|
currentConv,
|
|
searchQuery,
|
|
filteredConversations,
|
|
createNewConversation,
|
|
selectConversation,
|
|
selectedStage,
|
|
params,
|
|
messages,
|
|
userInput,
|
|
sendMessage,
|
|
stages,
|
|
executeAnalysis
|
|
};
|
|
}
|
|
});
|
|
|
|
app.use(ElementPlus);
|
|
// 注册 i18n 翻译函数到 Vue 模板作用域
|
|
app.config.globalProperties.t = window.t || ((key) => key);
|
|
|
|
// 注册图标
|
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
app.component(key, component);
|
|
}
|
|
|
|
app.mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|