- Change main brand color from #e6a335 to #ffc407 (new yellow) - Update hover/darker variant from #d1932b to #e6b006 - Update all RGBA color values to match new yellow (255, 196, 7) - Apply color changes across all components: - Navigation bar and branding - Buttons and interactive elements - Form inputs and focus states - Charts and data visualization - Status indicators and badges - Admin dashboard styling - Chat interface elements - Login and authentication pages Total updates: 40+ color references updated across 6 files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1342 lines
No EOL
34 KiB
Vue
1342 lines
No EOL
34 KiB
Vue
<template>
|
||
<div class="chat-page">
|
||
<div v-if="loading" class="loading">
|
||
Loading agents...
|
||
</div>
|
||
|
||
<div v-else-if="error" class="error">
|
||
{{ error }}
|
||
</div>
|
||
|
||
<div v-else class="main-layout">
|
||
<!-- Chat History Sidebar -->
|
||
<div class="chat-history-sidebar">
|
||
<div class="sidebar-header">
|
||
<h3>Chat History</h3>
|
||
<div class="header-actions">
|
||
<button v-if="!isSelecting" @click="startNewChat" class="new-chat-btn">
|
||
New Chat
|
||
</button>
|
||
<button v-if="!isSelecting && conversations.length > 0" @click="toggleSelectMode" class="select-btn">
|
||
Select
|
||
</button>
|
||
<div v-if="isSelecting" class="bulk-actions">
|
||
<button @click="selectAll" class="btn-small btn-outline">All</button>
|
||
<button @click="deleteSelected" :disabled="selectedConversations.length === 0" class="btn-small btn-danger">
|
||
Delete ({{ selectedConversations.length }})
|
||
</button>
|
||
<button @click="cancelSelection" class="btn-small btn-secondary">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="conversations-list">
|
||
<div v-if="conversations.length === 0" class="no-conversations">
|
||
No conversations yet
|
||
</div>
|
||
|
||
<div
|
||
v-for="conversation in conversations"
|
||
:key="conversation.id"
|
||
:class="['conversation-item', { active: currentConversationId === conversation.id, selected: selectedConversations.includes(conversation.id) }]"
|
||
>
|
||
<input
|
||
v-if="isSelecting"
|
||
type="checkbox"
|
||
:checked="selectedConversations.includes(conversation.id)"
|
||
@change="toggleConversationSelection(conversation.id)"
|
||
class="conversation-checkbox"
|
||
@click.stop
|
||
/>
|
||
<div class="conversation-content" @click="isSelecting ? toggleConversationSelection(conversation.id) : loadConversation(conversation.id)">
|
||
<div class="conversation-preview">
|
||
<div class="agent-name">{{ conversation.assistant?.name || 'Unknown Agent' }}</div>
|
||
<div class="conversation-title">{{ getConversationTitle(conversation) }}</div>
|
||
<div class="conversation-time">{{ formatConversationTime(conversation.lastMessageAt) }}</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
v-if="!isSelecting"
|
||
@click.stop="deleteConversation(conversation.id)"
|
||
class="delete-conversation-btn"
|
||
title="Delete conversation"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chat-container">
|
||
<!-- Agent Selection Dropdown -->
|
||
<div class="agent-selector card mb-4">
|
||
<div class="card-body">
|
||
<h3>Choose Your Agent</h3>
|
||
<select
|
||
v-model="selectedAgentKey"
|
||
@change="changeAgent"
|
||
class="agent-dropdown"
|
||
>
|
||
<option value="">Select an agent...</option>
|
||
<option
|
||
v-for="agent in agents"
|
||
:key="agent.key"
|
||
:value="agent.key"
|
||
>
|
||
{{ agent.name }} - {{ agent.description }}
|
||
<span v-if="agent.agentType === 'responses'"> (Advanced AI)</span>
|
||
</option>
|
||
</select>
|
||
|
||
<!-- Agent Capabilities Banner -->
|
||
<div v-if="selectedAgent" class="agent-capabilities">
|
||
<div class="agent-type-badge" :class="selectedAgent.agentType || 'chat'">
|
||
{{ selectedAgent.agentType === 'responses' ? 'Advanced AI Agent' : 'Standard Chat Agent' }}
|
||
</div>
|
||
|
||
<div v-if="selectedAgent.agentType === 'responses'" class="capabilities-list">
|
||
<div class="capability-item" v-if="selectedAgent.webSearchEnabled">
|
||
🌐 <span>Web Search</span>
|
||
</div>
|
||
<div class="capability-item" v-if="selectedAgent.fileSearchEnabled">
|
||
📚 <span>File Search</span>
|
||
</div>
|
||
<div class="capability-item" v-if="selectedAgent.codeInterpreterEnabled">
|
||
🐍 <span>Code Execution</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Messages -->
|
||
<div class="chat-messages card">
|
||
<div class="card-body">
|
||
<div v-if="messages.length === 0 && selectedAgent" class="empty-chat">
|
||
<div v-html="formatMessage(getStarterMessage())"></div>
|
||
</div>
|
||
|
||
<div v-else-if="!selectedAgent" class="no-agent">
|
||
<p>👆 Please select an agent from the dropdown above to start chatting!</p>
|
||
</div>
|
||
|
||
<div v-else class="messages-container" ref="messagesContainer">
|
||
<div
|
||
v-for="message in messages"
|
||
:key="message.id"
|
||
:class="['message', message.role]"
|
||
>
|
||
<div class="message-content">
|
||
<div class="message-text" v-html="formatMessage(message.content)"></div>
|
||
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isStreaming" class="message assistant streaming">
|
||
<div class="message-content">
|
||
<div class="message-text" v-html="formatMessage(streamingMessage)"></div>
|
||
<div class="typing-indicator">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- File Upload Section (only for responses agents) -->
|
||
<div v-if="selectedAgent && selectedAgent.agentType === 'responses'" class="file-upload-section">
|
||
<div class="upload-area" :class="{ 'drag-over': isDragOver }"
|
||
@drop="handleFileDrop"
|
||
@dragover.prevent="isDragOver = true"
|
||
@dragleave="isDragOver = false"
|
||
@dragenter.prevent>
|
||
<input
|
||
type="file"
|
||
ref="fileInput"
|
||
@change="handleFileSelect"
|
||
accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg,.gif"
|
||
multiple
|
||
style="display: none"
|
||
>
|
||
|
||
<div v-if="uploadedFiles.length === 0" class="upload-prompt">
|
||
<span class="upload-icon">📎</span>
|
||
Drop files or <button @click="$refs.fileInput.click()" class="upload-link">choose files</button>
|
||
<small style="display: block; margin-top: 0.25rem;">PDF, DOC, images, text</small>
|
||
</div>
|
||
|
||
<div v-else class="uploaded-files">
|
||
<div v-for="(file, index) in uploadedFiles" :key="index" class="uploaded-file">
|
||
<span class="file-icon">📄</span>
|
||
<span class="file-name">{{ file.name }}</span>
|
||
<button @click="removeFile(index)" class="remove-file-btn">×</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Input -->
|
||
<div class="chat-input-section">
|
||
<div class="chat-input-container">
|
||
<textarea
|
||
v-model="newMessage"
|
||
@keydown.enter.shift.exact="addNewline"
|
||
@keydown.enter.exact="sendMessage"
|
||
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
||
rows="1"
|
||
ref="messageInput"
|
||
:disabled="isSending"
|
||
></textarea>
|
||
<button
|
||
@click="sendMessage"
|
||
:disabled="!newMessage.trim() || isSending"
|
||
class="send-button"
|
||
>
|
||
<span v-if="isSending">⏳</span>
|
||
<span v-else>Send 🚀</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { agentsAPI, chatAPI } from '../services/api.js'
|
||
import { marked } from 'marked'
|
||
|
||
export default {
|
||
name: 'Chat',
|
||
props: ['agentKey', 'assistantKey'], // Keep assistantKey for backward compatibility
|
||
data() {
|
||
return {
|
||
agents: [],
|
||
selectedAgent: null,
|
||
selectedAgentKey: '',
|
||
selectedAssistantKey: '', // Keep for backward compatibility
|
||
messages: [],
|
||
newMessage: '',
|
||
conversationId: null,
|
||
currentConversationId: null,
|
||
conversations: [],
|
||
loading: true,
|
||
error: null,
|
||
isSending: false,
|
||
isStreaming: false,
|
||
streamingMessage: '',
|
||
userId: null,
|
||
conversationRefreshInterval: null,
|
||
isDragOver: false,
|
||
uploadedFiles: [],
|
||
isSelecting: false,
|
||
selectedConversations: []
|
||
}
|
||
},
|
||
async mounted() {
|
||
this.userId = this.getUserId()
|
||
if (!this.userId) {
|
||
this.$router.push('/login')
|
||
return
|
||
}
|
||
|
||
await this.loadAgents()
|
||
await this.loadConversations()
|
||
|
||
// Set up periodic conversation list refresh
|
||
this.conversationRefreshInterval = setInterval(() => {
|
||
if (this.userId) {
|
||
this.loadConversations()
|
||
}
|
||
}, 60000) // Refresh every 60 seconds
|
||
|
||
// Refresh conversations when window gains focus
|
||
this.onWindowFocus = () => {
|
||
if (this.userId) {
|
||
this.loadConversations()
|
||
}
|
||
}
|
||
window.addEventListener('focus', this.onWindowFocus)
|
||
|
||
const key = this.agentKey || this.assistantKey
|
||
if (key) {
|
||
this.selectedAgentKey = key
|
||
this.selectedAssistantKey = key // Keep for backward compatibility
|
||
await this.selectAgent(key)
|
||
}
|
||
},
|
||
beforeUnmount() {
|
||
if (this.conversationRefreshInterval) {
|
||
clearInterval(this.conversationRefreshInterval)
|
||
}
|
||
window.removeEventListener('focus', this.onWindowFocus)
|
||
},
|
||
watch: {
|
||
agentKey() {
|
||
const key = this.agentKey || this.assistantKey
|
||
if (key) {
|
||
this.selectedAgentKey = key
|
||
this.selectedAssistantKey = key
|
||
this.selectAgent(key)
|
||
}
|
||
},
|
||
assistantKey() {
|
||
const key = this.agentKey || this.assistantKey
|
||
if (key) {
|
||
this.selectedAgentKey = key
|
||
this.selectedAssistantKey = key
|
||
this.selectAgent(key)
|
||
}
|
||
},
|
||
messages: {
|
||
handler() {
|
||
this.$nextTick(() => {
|
||
this.scrollToBottom()
|
||
})
|
||
},
|
||
deep: true
|
||
},
|
||
userId: {
|
||
handler(newUserId) {
|
||
if (newUserId) {
|
||
this.loadConversations()
|
||
}
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
methods: {
|
||
getUserId() {
|
||
const currentUser = localStorage.getItem('currentUser')
|
||
if (!currentUser) {
|
||
this.$router.push('/login')
|
||
return null
|
||
}
|
||
|
||
const user = JSON.parse(currentUser)
|
||
return this.hashUserId(user.email)
|
||
},
|
||
|
||
hashUserId(email) {
|
||
// Create a consistent UUID-like string from email
|
||
let hash = 0
|
||
for (let i = 0; i < email.length; i++) {
|
||
hash = ((hash << 5) - hash + email.charCodeAt(i)) & 0xffffffff
|
||
}
|
||
|
||
// Convert to positive number and create UUID-like format
|
||
const positiveHash = Math.abs(hash).toString(16).padStart(8, '0')
|
||
return `${positiveHash.substr(0, 8)}-${positiveHash.substr(0, 4)}-4${positiveHash.substr(1, 3)}-8${positiveHash.substr(2, 3)}-${positiveHash.padEnd(12, '0').substr(0, 12)}`
|
||
},
|
||
|
||
|
||
async loadAgents() {
|
||
try {
|
||
this.loading = true
|
||
this.error = null
|
||
|
||
const data = await agentsAPI.getAll()
|
||
this.agents = data.agents || data.assistants // Support both old and new response format
|
||
} catch (error) {
|
||
this.error = 'Failed to load agents: ' + error.message
|
||
console.error('Error loading agents:', error)
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
async selectAgent(key) {
|
||
try {
|
||
this.selectedAgent = await agentsAPI.getByKey(key)
|
||
this.clearChat()
|
||
} catch (error) {
|
||
this.error = 'Failed to load agent: ' + error.message
|
||
console.error('Error loading agent:', error)
|
||
}
|
||
},
|
||
|
||
async changeAgent() {
|
||
if (this.selectedAgentKey) {
|
||
await this.selectAgent(this.selectedAgentKey)
|
||
this.$router.push(`/chat/${this.selectedAgentKey}`)
|
||
}
|
||
},
|
||
|
||
// Keep old methods for backward compatibility
|
||
async selectAssistant(key) { await this.selectAgent(key) },
|
||
async changeAssistant() { await this.changeAgent() },
|
||
|
||
getStarterMessage() {
|
||
if (this.selectedAgent?.starterMessage) {
|
||
return this.selectedAgent.starterMessage
|
||
}
|
||
|
||
// Fallback for agents without starter message
|
||
return `Hello! I'm ${this.selectedAgent?.name || 'your AI assistant'}. ${this.selectedAgent?.description || ''} How can I help you today?`
|
||
},
|
||
|
||
async sendMessage() {
|
||
if ((!this.newMessage.trim() && this.uploadedFiles.length === 0) || this.isSending || !this.selectedAgent) return
|
||
|
||
let messageContent = this.newMessage;
|
||
if (this.uploadedFiles.length > 0) {
|
||
const fileList = this.uploadedFiles.map(f => f.name).join(', ');
|
||
messageContent = this.newMessage ? `${this.newMessage}\n\n📎 Attached files: ${fileList}` : `📎 Attached files: ${fileList}`;
|
||
}
|
||
|
||
const userMessage = {
|
||
id: Date.now(),
|
||
role: 'user',
|
||
content: messageContent,
|
||
timestamp: new Date()
|
||
}
|
||
|
||
this.messages.push(userMessage)
|
||
const messageToSend = this.newMessage
|
||
this.newMessage = ''
|
||
this.isSending = true
|
||
this.isStreaming = true
|
||
this.streamingMessage = ''
|
||
|
||
try {
|
||
await chatAPI.sendStreamingMessage({
|
||
messages: [{ role: 'user', content: messageToSend }],
|
||
assistantKey: this.selectedAgent.key,
|
||
conversationId: this.conversationId,
|
||
userId: this.userId
|
||
}, (data) => {
|
||
if (data.content) {
|
||
this.streamingMessage += data.content
|
||
}
|
||
if (data.done) {
|
||
this.messages.push({
|
||
id: Date.now() + 1,
|
||
role: 'assistant',
|
||
content: this.streamingMessage,
|
||
timestamp: new Date()
|
||
})
|
||
this.conversationId = data.conversationId
|
||
this.currentConversationId = data.conversationId
|
||
this.isStreaming = false
|
||
this.streamingMessage = ''
|
||
// Clear uploaded files after successful send
|
||
this.clearFiles()
|
||
// Refresh conversations list immediately
|
||
this.loadConversations()
|
||
}
|
||
}, this.uploadedFiles)
|
||
} catch (error) {
|
||
console.error('Error sending message:', error)
|
||
this.messages.push({
|
||
id: Date.now() + 1,
|
||
role: 'assistant',
|
||
content: 'Sorry, I encountered an error. Please try again.',
|
||
timestamp: new Date()
|
||
})
|
||
this.isStreaming = false
|
||
this.streamingMessage = ''
|
||
} finally {
|
||
this.isSending = false
|
||
}
|
||
},
|
||
|
||
addNewline() {
|
||
this.newMessage += '\n'
|
||
},
|
||
|
||
clearChat() {
|
||
this.messages = []
|
||
this.conversationId = null
|
||
this.streamingMessage = ''
|
||
this.isStreaming = false
|
||
},
|
||
|
||
formatMessage(content) {
|
||
return marked(content || '', { breaks: true })
|
||
},
|
||
|
||
formatTime(timestamp) {
|
||
return new Date(timestamp).toLocaleTimeString()
|
||
},
|
||
|
||
scrollToBottom() {
|
||
const container = this.$refs.messagesContainer
|
||
if (container) {
|
||
container.scrollTop = container.scrollHeight
|
||
}
|
||
},
|
||
|
||
async loadConversations() {
|
||
try {
|
||
if (!this.userId) {
|
||
console.log('No userId available for loading conversations')
|
||
return
|
||
}
|
||
console.log(`Loading conversations for user: ${this.userId}`)
|
||
const data = await chatAPI.getConversations(this.userId)
|
||
const newConversations = data.conversations || []
|
||
console.log(`Loaded ${newConversations.length} conversations`)
|
||
this.conversations = newConversations
|
||
} catch (error) {
|
||
console.error('Error loading conversations:', error)
|
||
// Don't clear conversations on error, keep existing ones
|
||
}
|
||
},
|
||
|
||
async loadConversation(conversationId) {
|
||
try {
|
||
// Prevent double loading - check if we're already loading this conversation AND messages are loaded
|
||
if (this.currentConversationId === conversationId && this.messages.length > 0) {
|
||
return
|
||
}
|
||
|
||
// Set immediately to prevent concurrent calls
|
||
this.currentConversationId = conversationId
|
||
this.conversationId = conversationId
|
||
|
||
const data = await chatAPI.getConversationMessages(conversationId)
|
||
this.messages = data.messages.map(msg => ({
|
||
id: msg.id,
|
||
role: msg.role,
|
||
content: msg.content,
|
||
timestamp: new Date(msg.createdAt)
|
||
}))
|
||
|
||
// Find the agent for this conversation
|
||
const conversation = this.conversations.find(c => c.id === conversationId)
|
||
if (conversation && conversation.assistant) {
|
||
// Only load agent if it's different from current selection
|
||
if (this.selectedAgentKey !== conversation.assistant.key) {
|
||
this.selectedAgent = await agentsAPI.getByKey(conversation.assistant.key)
|
||
this.selectedAgentKey = conversation.assistant.key
|
||
}
|
||
// Update route without causing navigation if already on correct route
|
||
if (this.$route.params.agentKey !== conversation.assistant.key) {
|
||
this.$router.replace(`/chat/${conversation.assistant.key}`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading conversation:', error)
|
||
}
|
||
},
|
||
|
||
startNewChat() {
|
||
this.clearChat()
|
||
this.currentConversationId = null
|
||
this.$router.push('/chat')
|
||
},
|
||
|
||
getConversationTitle(conversation) {
|
||
// Use generated title if available, otherwise fallback to agent-based title
|
||
let title = conversation.title
|
||
|
||
if (!title) {
|
||
// For older conversations without generated titles
|
||
const agentName = conversation.assistant?.name || 'Unknown Agent'
|
||
title = `Chat with ${agentName}`
|
||
}
|
||
|
||
// Truncate long titles
|
||
return title.length > 50 ? title.substring(0, 50) + '...' : title
|
||
},
|
||
|
||
formatConversationTime(timestamp) {
|
||
if (!timestamp) return 'No date'
|
||
const date = new Date(timestamp)
|
||
if (isNaN(date.getTime())) return 'Invalid date'
|
||
|
||
const now = new Date()
|
||
const diffMs = now - date
|
||
const diffHours = diffMs / (1000 * 60 * 60)
|
||
|
||
if (diffHours < 24) {
|
||
// Today: show time only
|
||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||
} else {
|
||
// Older: show date and time
|
||
const dateStr = date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||
const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||
return `${dateStr} ${timeStr}`
|
||
}
|
||
},
|
||
|
||
async deleteConversation(conversationId) {
|
||
if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/chat/conversations/${conversationId}`, {
|
||
method: 'DELETE'
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete conversation')
|
||
}
|
||
|
||
// Remove conversation from local list
|
||
this.conversations = this.conversations.filter(c => c.id !== conversationId)
|
||
|
||
// If this was the current conversation, clear chat
|
||
if (this.currentConversationId === conversationId) {
|
||
this.clearChat()
|
||
this.currentConversationId = null
|
||
this.$router.push('/chat')
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting conversation:', error)
|
||
alert('Failed to delete conversation. Please try again.')
|
||
}
|
||
},
|
||
|
||
// File upload methods
|
||
handleFileSelect(event) {
|
||
const files = Array.from(event.target.files)
|
||
this.processFiles(files)
|
||
},
|
||
|
||
handleFileDrop(event) {
|
||
event.preventDefault()
|
||
this.isDragOver = false
|
||
const files = Array.from(event.dataTransfer.files)
|
||
this.processFiles(files)
|
||
},
|
||
|
||
processFiles(files) {
|
||
const maxSize = 10 * 1024 * 1024 // 10MB
|
||
const validFiles = files.filter(file => {
|
||
if (file.size > maxSize) {
|
||
alert(`File ${file.name} is too large. Maximum size is 10MB.`)
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
this.uploadedFiles = [...this.uploadedFiles, ...validFiles]
|
||
},
|
||
|
||
removeFile(index) {
|
||
this.uploadedFiles.splice(index, 1)
|
||
},
|
||
|
||
clearFiles() {
|
||
this.uploadedFiles = []
|
||
},
|
||
|
||
toggleSelectMode() {
|
||
this.isSelecting = !this.isSelecting
|
||
this.selectedConversations = []
|
||
},
|
||
|
||
toggleConversationSelection(conversationId) {
|
||
const index = this.selectedConversations.indexOf(conversationId)
|
||
if (index > -1) {
|
||
this.selectedConversations.splice(index, 1)
|
||
} else {
|
||
this.selectedConversations.push(conversationId)
|
||
}
|
||
},
|
||
|
||
selectAll() {
|
||
if (this.selectedConversations.length === this.conversations.length) {
|
||
this.selectedConversations = []
|
||
} else {
|
||
this.selectedConversations = this.conversations.map(c => c.id)
|
||
}
|
||
},
|
||
|
||
async deleteSelected() {
|
||
if (this.selectedConversations.length === 0) return
|
||
|
||
const count = this.selectedConversations.length
|
||
if (!confirm(`Are you sure you want to delete ${count} conversation${count > 1 ? 's' : ''}? This action cannot be undone.`)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const deletePromises = this.selectedConversations.map(id =>
|
||
fetch(`/api/chat/conversations/${id}`, { method: 'DELETE' })
|
||
)
|
||
await Promise.all(deletePromises)
|
||
|
||
await this.loadConversations()
|
||
this.selectedConversations = []
|
||
this.isSelecting = false
|
||
} catch (error) {
|
||
console.error('Error deleting conversations:', error)
|
||
alert('Error deleting some conversations. Please try again.')
|
||
}
|
||
},
|
||
|
||
cancelSelection() {
|
||
this.isSelecting = false
|
||
this.selectedConversations = []
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap');
|
||
|
||
* {
|
||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||
}
|
||
|
||
.main-layout {
|
||
display: flex;
|
||
height: 100vh;
|
||
width: 100%;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
position: fixed;
|
||
top: 60px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.chat-history-sidebar {
|
||
width: 280px;
|
||
min-width: 250px;
|
||
max-width: 320px;
|
||
background: #f8f9fa;
|
||
border-right: 1px solid #e5e7eb;
|
||
overflow-y: auto;
|
||
flex-shrink: 0;
|
||
resize: horizontal;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 0.75rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.sidebar-header h3 {
|
||
margin: 0;
|
||
color: #1f2937;
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 0.4rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.select-btn {
|
||
padding: 0.3rem 0.6rem;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
font-size: 0.7rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.select-btn:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
.bulk-actions {
|
||
display: flex;
|
||
gap: 0.3rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 0.2rem 0.4rem;
|
||
font-size: 0.65rem;
|
||
border-radius: 4px;
|
||
border: 1px solid;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-outline {
|
||
background: white;
|
||
color: #374151;
|
||
border-color: #d1d5db;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc2626;
|
||
color: white;
|
||
border-color: #dc2626;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6b7280;
|
||
color: white;
|
||
border-color: #6b7280;
|
||
}
|
||
|
||
.btn-small:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.new-chat-btn {
|
||
padding: 0.5rem 1rem;
|
||
background: #ffc407;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 0.68rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.new-chat-btn:hover {
|
||
background: #e6b006;
|
||
}
|
||
|
||
|
||
.conversations-list {
|
||
padding: 0.5rem 0;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.conversation-item {
|
||
display: flex;
|
||
align-items: center;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.conversation-item:hover {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.conversation-item.active {
|
||
background: #eef2ff;
|
||
border-right: 3px solid #ffc407;
|
||
}
|
||
|
||
.conversation-item.selected {
|
||
background: #fef3cd;
|
||
border-left: 3px solid #ffc407;
|
||
}
|
||
|
||
.conversation-checkbox {
|
||
margin: 0 0.5rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.conversation-content {
|
||
flex: 1;
|
||
padding: 0.5rem 0.75rem;
|
||
cursor: pointer;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.delete-conversation-btn {
|
||
padding: 0.5rem;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
color: #9ca3af;
|
||
border-radius: 4px;
|
||
margin-right: 0.5rem;
|
||
transition: all 0.2s;
|
||
opacity: 0;
|
||
flex-shrink: 0;
|
||
min-width: 2rem;
|
||
}
|
||
|
||
.conversation-item:hover .delete-conversation-btn {
|
||
opacity: 1;
|
||
}
|
||
|
||
.delete-conversation-btn:hover {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.conversation-preview .agent-name {
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
font-size: 0.7rem;
|
||
margin-bottom: 0.15rem;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.conversation-preview .conversation-title {
|
||
color: #6b7280;
|
||
font-size: 0.65rem;
|
||
margin-bottom: 0.15rem;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.conversation-preview .conversation-time {
|
||
color: #9ca3af;
|
||
font-size: 0.6rem;
|
||
}
|
||
|
||
.no-conversations {
|
||
padding: 2rem 1rem;
|
||
text-align: center;
|
||
color: #9ca3af;
|
||
font-style: italic;
|
||
}
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
flex: 1;
|
||
min-width: 400px;
|
||
max-width: none;
|
||
overflow: hidden;
|
||
position: relative;
|
||
padding-bottom: 200px;
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.chat-messages .card-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
}
|
||
|
||
.agent-selector {
|
||
flex-shrink: 0;
|
||
padding: 0.75rem;
|
||
background: white;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.agent-selector h3 {
|
||
margin-bottom: 1rem;
|
||
color: #1f2937;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.agent-dropdown {
|
||
width: 100%;
|
||
padding: 1rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
font-size: 0.8rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.agent-dropdown:focus {
|
||
outline: none;
|
||
border-color: #ffc407;
|
||
box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.1);
|
||
}
|
||
|
||
/* Keep old styles for backward compatibility */
|
||
.assistant-selector { @extend .agent-selector; }
|
||
.assistant-dropdown { @extend .agent-dropdown; }
|
||
|
||
.assistant-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.assistant-header .card-body {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.assistant-name {
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.assistant-description {
|
||
color: #6b7280;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.assistant-meta {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 0.6rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
}
|
||
|
||
.chat-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.chat-messages .card-body {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
.empty-chat {
|
||
text-align: center;
|
||
padding: 1rem;
|
||
color: #374151;
|
||
background: #f9fafb;
|
||
border-radius: 8px;
|
||
font-size: 0.75rem;
|
||
line-height: 1.4;
|
||
margin: 1rem;
|
||
}
|
||
|
||
.no-agent {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #6b7280;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* Keep old styles for backward compatibility */
|
||
.no-assistant { @extend .no-agent; }
|
||
|
||
.messages-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
min-height: 0;
|
||
}
|
||
|
||
.empty-chat, .no-agent {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.message {
|
||
display: flex;
|
||
max-width: 80%;
|
||
}
|
||
|
||
.message.user {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.message.user .message-content {
|
||
background: #ffc407;
|
||
color: white;
|
||
}
|
||
|
||
.message.assistant {
|
||
margin-right: auto;
|
||
}
|
||
|
||
.message.assistant .message-content {
|
||
background: #f3f4f6;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.message-content {
|
||
padding: 0.75rem;
|
||
border-radius: 12px;
|
||
position: relative;
|
||
font-size: 0.77rem;
|
||
}
|
||
|
||
.message-text {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.message-text :deep(p) {
|
||
margin: 0.35rem 0;
|
||
font-size: 0.77rem;
|
||
}
|
||
|
||
.message-text :deep(pre) {
|
||
background: rgba(0,0,0,0.1);
|
||
padding: 0.35rem;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 0.46rem;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.streaming .message-content {
|
||
background: #f9fafb;
|
||
border: 2px dashed #e5e7eb;
|
||
}
|
||
|
||
.typing-indicator {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.typing-indicator span {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #9ca3af;
|
||
animation: typing 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.typing-indicator span:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.typing-indicator span:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
@keyframes typing {
|
||
0%, 60%, 100% {
|
||
transform: translateY(0);
|
||
}
|
||
30% {
|
||
transform: translateY(-10px);
|
||
}
|
||
}
|
||
|
||
.chat-input-section {
|
||
position: fixed;
|
||
bottom: 0;
|
||
right: 0;
|
||
left: 280px;
|
||
padding: 0.75rem;
|
||
background: white;
|
||
border-top: 1px solid #e5e7eb;
|
||
z-index: 100;
|
||
}
|
||
|
||
.chat-input-container {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: end;
|
||
}
|
||
|
||
.chat-input-container textarea {
|
||
flex: 1;
|
||
padding: 1rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
font-size: 0.8rem;
|
||
resize: vertical;
|
||
min-height: 100px;
|
||
max-height: 300px;
|
||
}
|
||
|
||
.chat-input-container textarea:focus {
|
||
outline: none;
|
||
border-color: #ffc407;
|
||
}
|
||
|
||
.send-button {
|
||
padding: 1rem 1.5rem;
|
||
background: #ffc407;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.send-button:hover:not(:disabled) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(255, 196, 7, 0.4);
|
||
}
|
||
|
||
.send-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Agent Capabilities Styling */
|
||
.agent-capabilities {
|
||
margin-top: 0.5rem;
|
||
padding: 0.5rem 0.75rem;
|
||
background: #f8f9fa;
|
||
border-radius: 6px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.agent-type-badge {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.6rem;
|
||
border-radius: 12px;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.agent-type-badge.responses {
|
||
background: #e6f3ff;
|
||
color: #0066cc;
|
||
border: 1px solid #b3d9ff;
|
||
}
|
||
|
||
.agent-type-badge.chat {
|
||
background: #f0f9f0;
|
||
color: #006600;
|
||
border: 1px solid #ccffcc;
|
||
}
|
||
|
||
.capabilities-list {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.capability-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
font-size: 0.7rem;
|
||
color: #374151;
|
||
}
|
||
|
||
.capability-item span {
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* File Upload Styling */
|
||
.file-upload-section {
|
||
flex-shrink: 0;
|
||
padding: 0.5rem 0.75rem;
|
||
background: white;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 1px dashed #d1d5db;
|
||
border-radius: 8px;
|
||
padding: 0.75rem;
|
||
text-align: center;
|
||
background: #f9fafb;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.upload-area.drag-over {
|
||
border-color: #ffc407;
|
||
background: #fef9f0;
|
||
}
|
||
|
||
.upload-prompt {
|
||
color: #6b7280;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 1.2rem;
|
||
display: inline;
|
||
margin-right: 0.25rem;
|
||
}
|
||
|
||
.upload-link {
|
||
background: none;
|
||
border: none;
|
||
color: #ffc407;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
font-size: inherit;
|
||
}
|
||
|
||
.upload-link:hover {
|
||
color: #e6b006;
|
||
}
|
||
|
||
.uploaded-files {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.3rem;
|
||
}
|
||
|
||
.uploaded-file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
padding: 0.4rem 0.6rem;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.file-name {
|
||
flex: 1;
|
||
font-size: 0.75rem;
|
||
color: #374151;
|
||
text-align: left;
|
||
}
|
||
|
||
.remove-file-btn {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.remove-file-btn:hover {
|
||
background: #fecaca;
|
||
transform: scale(1.1);
|
||
}
|
||
</style> |