374 lines
15 KiB
JavaScript
Executable File
374 lines
15 KiB
JavaScript
Executable File
/**
|
|
* Conversation Sidebar Component
|
|
* 类似ChatGPT的会话侧边栏组件
|
|
*/
|
|
|
|
const ConversationSidebar = {
|
|
template: `
|
|
<div class="conversation-sidebar" :class="{ collapsed: isCollapsed }">
|
|
<!-- 头部:New Chat按钮 -->
|
|
<div class="sidebar-header">
|
|
<button class="new-chat-btn" @click="createNewConversation">
|
|
<el-icon><plus /></el-icon>
|
|
<span v-if="!isCollapsed">New Chat</span>
|
|
</button>
|
|
<button class="toggle-btn" @click="toggleSidebar">
|
|
<el-icon><arrow-left v-if="!isCollapsed" /><arrow-right v-else /></el-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 搜索框 -->
|
|
<div class="search-box" v-if="!isCollapsed">
|
|
<el-input
|
|
v-model="searchQuery"
|
|
placeholder="搜索会话..."
|
|
prefix-icon="search"
|
|
clearable
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 会话列表 -->
|
|
<div class="conversation-list" v-if="!isCollapsed">
|
|
<!-- 今日会话 -->
|
|
<div class="list-section" v-if="todayConversations.length > 0">
|
|
<div class="section-title">今天</div>
|
|
<div
|
|
v-for="conv in todayConversations"
|
|
:key="conv.conversation_id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConversationId === conv.conversation_id }"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<div class="item-icon">
|
|
<el-icon><chat-dot-round /></el-icon>
|
|
</div>
|
|
<div class="item-content">
|
|
<div class="item-title">{{ conv.title }}</div>
|
|
<div class="item-meta">
|
|
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
|
|
<span class="message-count">{{ conv.message_count }}条消息</span>
|
|
</div>
|
|
<div class="item-preview">{{ conv.last_message }}</div>
|
|
</div>
|
|
<div class="item-actions" @click.stop>
|
|
<el-dropdown trigger="click">
|
|
<el-icon><more /></el-icon>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item @click="renameConversation(conv)">
|
|
<el-icon><edit /></el-icon>重命名
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="archiveConversation(conv)">
|
|
<el-icon><folder /></el-icon>归档
|
|
</el-dropdown-item>
|
|
<el-dropdown-item divided @click="deleteConversation(conv)" type="danger">
|
|
<el-icon><delete /></el-icon>删除
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 昨日会话 -->
|
|
<div class="list-section" v-if="yesterdayConversations.length > 0">
|
|
<div class="section-title">昨天</div>
|
|
<div
|
|
v-for="conv in yesterdayConversations"
|
|
:key="conv.conversation_id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConversationId === conv.conversation_id }"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<div class="item-icon">
|
|
<el-icon><chat-dot-round /></el-icon>
|
|
</div>
|
|
<div class="item-content">
|
|
<div class="item-title">{{ conv.title }}</div>
|
|
<div class="item-meta">
|
|
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
|
|
<span class="message-count">{{ conv.message_count }}条</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 更早会话 -->
|
|
<div class="list-section" v-if="olderConversations.length > 0">
|
|
<div class="section-title">过去7天</div>
|
|
<div
|
|
v-for="conv in olderConversations"
|
|
:key="conv.conversation_id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConversationId === conv.conversation_id }"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<div class="item-icon">
|
|
<el-icon><chat-dot-round /></el-icon>
|
|
</div>
|
|
<div class="item-content">
|
|
<div class="item-title">{{ conv.title }}</div>
|
|
<div class="item-meta">
|
|
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 空状态 -->
|
|
<div v-if="filteredConversations.length === 0" class="empty-state">
|
|
<el-icon><chat-line-round /></el-icon>
|
|
<p>暂无会话</p>
|
|
<p class="hint">点击上方"New Chat"开始新的基因家族分析</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 底部:用户信息 -->
|
|
<div class="sidebar-footer" v-if="!isCollapsed">
|
|
<div class="user-info">
|
|
<el-avatar :size="32" :icon="UserFilled" />
|
|
<span class="username">{{ userId }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
|
|
props: {
|
|
userId: {
|
|
type: String,
|
|
default: 'xia-lab'
|
|
},
|
|
apiBaseUrl: {
|
|
type: String,
|
|
default: 'http://localhost:8000/api'
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
isCollapsed: false,
|
|
searchQuery: '',
|
|
conversations: [],
|
|
currentConversationId: null,
|
|
loading: false,
|
|
// 图标
|
|
Plus: ElementPlusIconsVue.Plus,
|
|
ArrowLeft: ElementPlusIconsVue.ArrowLeft,
|
|
ArrowRight: ElementPlusIconsVue.ArrowRight,
|
|
ChatDotRound: ElementPlusIconsVue.ChatDotRound,
|
|
ChatLineRound: ElementPlusIconsVue.ChatLineRound,
|
|
More: ElementPlusIconsVue.More,
|
|
Edit: ElementPlusIconsVue.Edit,
|
|
Folder: ElementPlusIconsVue.Folder,
|
|
Delete: ElementPlusIconsVue.Delete,
|
|
UserFilled: ElementPlusIconsVue.UserFilled,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
filteredConversations() {
|
|
if (!this.searchQuery) return this.conversations;
|
|
const query = this.searchQuery.toLowerCase();
|
|
return this.conversations.filter(conv =>
|
|
conv.title.toLowerCase().includes(query) ||
|
|
(conv.gene_family && conv.gene_family.toLowerCase().includes(query)) ||
|
|
conv.last_message.toLowerCase().includes(query)
|
|
);
|
|
},
|
|
|
|
todayConversations() {
|
|
const today = new Date().toDateString();
|
|
return this.filteredConversations.filter(conv => {
|
|
const convDate = new Date(conv.updated_at).toDateString();
|
|
return convDate === today;
|
|
});
|
|
},
|
|
|
|
yesterdayConversations() {
|
|
const yesterday = new Date(Date.now() - 86400000).toDateString();
|
|
return this.filteredConversations.filter(conv => {
|
|
const convDate = new Date(conv.updated_at).toDateString();
|
|
return convDate === yesterday;
|
|
});
|
|
},
|
|
|
|
olderConversations() {
|
|
const yesterday = new Date(Date.now() - 86400000).toDateString();
|
|
return this.filteredConversations.filter(conv => {
|
|
const convDate = new Date(conv.updated_at).toDateString();
|
|
return convDate < yesterday;
|
|
});
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.loadConversations();
|
|
// 定时刷新
|
|
this.refreshInterval = setInterval(() => {
|
|
this.loadConversations();
|
|
}, 30000); // 30秒刷新一次
|
|
},
|
|
|
|
beforeUnmount() {
|
|
if (this.refreshInterval) {
|
|
clearInterval(this.refreshInterval);
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
async loadConversations() {
|
|
try {
|
|
this.loading = true;
|
|
const response = await fetch(`${this.apiBaseUrl}/conversations/list/${this.userId}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.conversations = data.conversations || [];
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load conversations:', error);
|
|
// 使用模拟数据
|
|
this.conversations = this.getMockConversations();
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
getMockConversations() {
|
|
// 模拟数据,用于测试
|
|
return [
|
|
{
|
|
conversation_id: 'conv_001',
|
|
title: 'Ft.SPL基因家族分析',
|
|
gene_family: 'SPL',
|
|
species: 'Ft',
|
|
message_count: 12,
|
|
last_message: '系统发育树已生成完成',
|
|
updated_at: new Date().toISOString()
|
|
},
|
|
{
|
|
conversation_id: 'conv_002',
|
|
title: 'Os.TCP进化分析',
|
|
gene_family: 'TCP',
|
|
species: 'Os',
|
|
message_count: 5,
|
|
last_message: '正在构建进化树...',
|
|
updated_at: new Date(Date.now() - 3600000).toISOString()
|
|
},
|
|
{
|
|
conversation_id: 'conv_003',
|
|
title: 'HMMER参数配置咨询',
|
|
message_count: 8,
|
|
last_message: 'E-value建议设置为1e-5',
|
|
updated_at: new Date(Date.now() - 86400000).toISOString()
|
|
}
|
|
];
|
|
},
|
|
|
|
toggleSidebar() {
|
|
this.isCollapsed = !this.isCollapsed;
|
|
this.$emit('toggle', this.isCollapsed);
|
|
},
|
|
|
|
async createNewConversation() {
|
|
try {
|
|
const response = await fetch(`${this.apiBaseUrl}/conversations/create`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
user_id: this.userId,
|
|
title: '新会话 ' + new Date().toLocaleTimeString()
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.currentConversationId = data.conversation_id;
|
|
this.$emit('new-conversation', data);
|
|
await this.loadConversations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create conversation:', error);
|
|
// 创建本地会话
|
|
const newConv = {
|
|
conversation_id: 'conv_' + Date.now(),
|
|
title: '新会话',
|
|
message_count: 0,
|
|
last_message: '',
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
this.conversations.unshift(newConv);
|
|
this.currentConversationId = newConv.conversation_id;
|
|
this.$emit('new-conversation', newConv);
|
|
}
|
|
},
|
|
|
|
selectConversation(conv) {
|
|
this.currentConversationId = conv.conversation_id;
|
|
this.$emit('select-conversation', conv);
|
|
},
|
|
|
|
async renameConversation(conv) {
|
|
const newTitle = prompt('请输入新标题:', conv.title);
|
|
if (newTitle && newTitle !== conv.title) {
|
|
try {
|
|
await fetch(`${this.apiBaseUrl}/conversations/update-title`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
conversation_id: conv.conversation_id,
|
|
title: newTitle
|
|
})
|
|
});
|
|
conv.title = newTitle;
|
|
} catch (error) {
|
|
console.error('Failed to rename:', error);
|
|
conv.title = newTitle; // 本地更新
|
|
}
|
|
}
|
|
},
|
|
|
|
async archiveConversation(conv) {
|
|
try {
|
|
await fetch(`${this.apiBaseUrl}/conversations/archive`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
conversation_id: conv.conversation_id
|
|
})
|
|
});
|
|
await this.loadConversations();
|
|
} catch (error) {
|
|
console.error('Failed to archive:', error);
|
|
}
|
|
},
|
|
|
|
async deleteConversation(conv) {
|
|
if (confirm(`确定要删除会话 "${conv.title}" 吗?`)) {
|
|
try {
|
|
await fetch(`${this.apiBaseUrl}/conversations/delete`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
conversation_id: conv.conversation_id
|
|
})
|
|
});
|
|
await this.loadConversations();
|
|
if (this.currentConversationId === conv.conversation_id) {
|
|
this.currentConversationId = null;
|
|
this.$emit('select-conversation', null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete:', error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 导出组件
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { ConversationSidebar };
|
|
}
|