ideas-generator/admin/src/pages/Chat.vue
DJP 574e390be1 Update brand color from orange to yellow across entire application
- 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>
2025-09-09 11:50:10 -04:00

1342 lines
No EOL
34 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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