374 lines
9.6 KiB
JavaScript
Executable File
374 lines
9.6 KiB
JavaScript
Executable File
/**
|
|
* Conversation Sidebar Component
|
|
* Conversation Sidebar component
|
|
*/
|
|
|
|
const ConversationSidebar = {
|
|
template: `
|
|
<div class="conversation-sidebar" :class="{ collapsed: isCollapsed }">
|
|
<!-- Header: New Chat button -->
|
|
<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>
|
|
|
|
<!-- Search box -->
|
|
<div class="search-box" v-if="!isCollapsed">
|
|
<el-input
|
|
v-model="searchQuery"
|
|
placeholder="Search conversations..."
|
|
prefix-icon="search"
|
|
clearable
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Conversation list -->
|
|
<div class="conversation-list" v-if="!isCollapsed">
|
|
<!-- Todays conversations -->
|
|
<div class="list-section" v-if="todayConversations.length > 0">
|
|
<div class="section-title">Today</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 }} messages</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>Rename
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="archiveConversation(conv)">
|
|
<el-icon><folder /></el-icon>Archive
|
|
</el-dropdown-item>
|
|
<el-dropdown-item divided @click="deleteConversation(conv)" type="danger">
|
|
<el-icon><delete /></el-icon>Delete
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Yesterdays conversations -->
|
|
<div class="list-section" v-if="yesterdayConversations.length > 0">
|
|
<div class="section-title">Yesterday</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>
|
|
|
|
<!-- Older conversations -->
|
|
<div class="list-section" v-if="olderConversations.length > 0">
|
|
<div class="section-title">Past 7 days</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>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="filteredConversations.length === 0" class="empty-state">
|
|
<el-icon><chat-line-round /></el-icon>
|
|
<p>No conversations yet</p>
|
|
<p class="hint">Click "New Chat" to start gene family analysis</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- footer: user info -->
|
|
<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,
|
|
// Icons
|
|
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();
|
|
// Auto-refresh
|
|
this.refreshInterval = setInterval(() => {
|
|
this.loadConversations();
|
|
}, 30000); // 30 seconds refresh interval
|
|
},
|
|
|
|
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);
|
|
// Use mock data
|
|
this.conversations = this.getMockConversations();
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
getMockConversations() {
|
|
// Mock data for testing
|
|
return [
|
|
{
|
|
conversation_id: 'conv_001',
|
|
title: 'At.WRKY gene family analysis',
|
|
gene_family: 'WRKY',
|
|
species: 'At',
|
|
message_count: 12,
|
|
last_message: 'Phylogenetic tree done',
|
|
updated_at: new Date().toISOString()
|
|
},
|
|
{
|
|
conversation_id: 'conv_002',
|
|
title: 'Os.TCP evolution analysis',
|
|
gene_family: 'TCP',
|
|
species: 'Os',
|
|
message_count: 5,
|
|
last_message: 'Building tree...',
|
|
updated_at: new Date(Date.now() - 3600000).toISOString()
|
|
},
|
|
{
|
|
conversation_id: 'conv_003',
|
|
title: 'HMMER config consultation',
|
|
message_count: 8,
|
|
last_message: 'E-value 1e-5 recommended',
|
|
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 Chat ' + 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);
|
|
// Create local conversation
|
|
const newConv = {
|
|
conversation_id: 'conv_' + Date.now(),
|
|
title: 'New Chat',
|
|
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('Enter new title:', 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; // local update
|
|
}
|
|
}
|
|
},
|
|
|
|
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(`Delete conversation "${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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Export component
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { ConversationSidebar };
|
|
}
|