Files
PlantAI-dev/apps/web/vue3/static/conversation-sidebar.js

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 };
}