Files
PlantAI-dev/apps/web/vue3/index-with-sidebar_ml.html

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>