900 lines
21 KiB
Vue
900 lines
21 KiB
Vue
<template>
|
|
<div class="chat-view">
|
|
<!-- Left Sidebar - ChatGPT Style -->
|
|
<aside class="chat-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
|
<div class="sidebar-header">
|
|
<button class="collapse-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
|
|
<el-icon><Fold v-if="!sidebarCollapsed" /><Expand v-else /></el-icon>
|
|
<span v-if="!sidebarCollapsed">{{ sidebarCollapsed ? '展开' : '收起' }}</span>
|
|
</button>
|
|
<button class="new-chat-btn" @click="handleNewChat">
|
|
<el-icon><Plus /></el-icon>
|
|
<span v-if="!sidebarCollapsed">新建对话</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="!sidebarCollapsed" class="conversations-list">
|
|
<div class="list-section">
|
|
<div class="section-title">今天</div>
|
|
<div
|
|
v-for="conv in todayConversations"
|
|
:key="conv.id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConversationId === conv.id }"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<el-icon><ChatDotRound /></el-icon>
|
|
<span class="conv-title">{{ conv.title }}</span>
|
|
<div class="conv-actions">
|
|
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-section" v-if="olderConversations.length > 0">
|
|
<div class="section-title">历史记录</div>
|
|
<div
|
|
v-for="conv in olderConversations"
|
|
:key="conv.id"
|
|
class="conversation-item"
|
|
:class="{ active: currentConversationId === conv.id }"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<el-icon><ChatDotRound /></el-icon>
|
|
<span class="conv-title">{{ conv.title }}</span>
|
|
<div class="conv-actions">
|
|
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Chat Area -->
|
|
<div class="chat-main">
|
|
<!-- Header -->
|
|
<header class="chat-header">
|
|
<el-dropdown trigger="click" @command="handleAgentChange" class="agent-dropdown">
|
|
<div class="header-title">
|
|
<span class="agent-icon">{{ currentAgent.icon }}</span>
|
|
<span>{{ currentAgent.name }}</span>
|
|
<el-icon><ArrowDown /></el-icon>
|
|
</div>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item
|
|
v-for="agent in agents"
|
|
:key="agent.id"
|
|
:command="agent.id"
|
|
:class="{ 'is-active': agent.id === currentAgent.id }"
|
|
>
|
|
<span class="dropdown-agent-icon">{{ agent.icon }}</span>
|
|
<span>{{ agent.name }}</span>
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</header>
|
|
|
|
<!-- Chat Content -->
|
|
<div class="chat-content" ref="chatContentRef">
|
|
<!-- Welcome Message -->
|
|
<div v-if="messages.length === 0" class="welcome-section">
|
|
<div class="ai-avatar">
|
|
<div class="avatar-icon" :style="{ background: currentAgent.color }">
|
|
{{ currentAgent.icon }}
|
|
</div>
|
|
</div>
|
|
<p class="welcome-text">
|
|
{{ currentAgent.description }}
|
|
</p>
|
|
<h2 class="welcome-title">{{ welcomeTitle }}</h2>
|
|
|
|
<!-- Suggestion Cards -->
|
|
<div class="suggestion-cards">
|
|
<div
|
|
v-for="(suggestion, index) in currentSuggestions"
|
|
:key="index"
|
|
class="suggestion-card"
|
|
@click="handleSuggestionClick(suggestion)"
|
|
>
|
|
<div class="card-icon" :style="{ background: currentAgent.color }">
|
|
<component :is="cardIcons[index % cardIcons.length]" />
|
|
</div>
|
|
<p class="card-text">{{ suggestion }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat Messages -->
|
|
<div v-else class="messages-container">
|
|
<div
|
|
v-for="message in messages"
|
|
:key="message.id"
|
|
class="message"
|
|
:class="message.role"
|
|
>
|
|
<div class="message-avatar">
|
|
<img v-if="message.role === 'assistant'" src="/logo.jpg" alt="AI" class="ai-avatar-small" />
|
|
<div v-else class="user-avatar-small">👤</div>
|
|
</div>
|
|
<div class="message-content">
|
|
<div class="message-text">{{ message.content }}</div>
|
|
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading indicator -->
|
|
<div v-if="isLoading" class="message assistant">
|
|
<div class="message-avatar">
|
|
<img src="/logo.jpg" alt="AI" class="ai-avatar-small" />
|
|
</div>
|
|
<div class="message-content">
|
|
<div class="typing-indicator">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input Area -->
|
|
<div class="input-area">
|
|
<div class="input-wrapper">
|
|
<textarea
|
|
v-model="inputText"
|
|
placeholder="请输入内容..."
|
|
@keydown.enter.prevent="handleSend"
|
|
rows="1"
|
|
ref="textareaRef"
|
|
></textarea>
|
|
<div class="input-actions">
|
|
<div class="action-buttons">
|
|
<button class="action-btn" title="附件">
|
|
<el-icon><Paperclip /></el-icon>
|
|
</button>
|
|
<button class="action-btn" title="表情">
|
|
<el-icon><Star /></el-icon>
|
|
</button>
|
|
<button class="action-btn" title="图片">
|
|
<el-icon><Picture /></el-icon>
|
|
</button>
|
|
<button class="action-btn" title="更多">
|
|
<el-icon><MoreFilled /></el-icon>
|
|
</button>
|
|
<button class="action-btn" title="截图">
|
|
<el-icon><CameraFilled /></el-icon>
|
|
</button>
|
|
</div>
|
|
<div class="send-actions">
|
|
<button class="action-btn" title="语音">
|
|
<el-icon><Microphone /></el-icon>
|
|
</button>
|
|
<button class="send-btn" @click="handleSend" :disabled="!inputText.trim()">
|
|
<el-icon><Promotion /></el-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, nextTick, watch, computed } from 'vue'
|
|
import {
|
|
ArrowDown,
|
|
Paperclip,
|
|
Star,
|
|
Picture,
|
|
MoreFilled,
|
|
CameraFilled,
|
|
Microphone,
|
|
Promotion,
|
|
Mute,
|
|
OfficeBuilding,
|
|
Warning,
|
|
Cloudy,
|
|
Plus,
|
|
Fold,
|
|
Expand,
|
|
ChatDotRound,
|
|
Delete
|
|
} from '@element-plus/icons-vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useChatStore } from '@/stores/chat'
|
|
import { useAgentStore } from '@/stores/agent'
|
|
|
|
const route = useRoute()
|
|
const chatStore = useChatStore()
|
|
const agentStore = useAgentStore()
|
|
|
|
// 智能体相关
|
|
const agents = computed(() => agentStore.agents)
|
|
const currentAgent = computed(() => agentStore.currentAgent)
|
|
|
|
const inputText = ref('')
|
|
const isLoading = ref(false)
|
|
const chatContentRef = ref(null)
|
|
const textareaRef = ref(null)
|
|
const sidebarCollapsed = ref(false)
|
|
const currentConversationId = ref(null)
|
|
|
|
// 对话历史记录
|
|
const conversations = ref([
|
|
{ id: 1, title: '城市生命线关键设施咨询', date: new Date(), messages: [] },
|
|
{ id: 2, title: '消防安全隐患处理方案', date: new Date(), messages: [] },
|
|
{ id: 3, title: '排水系统优化建议', date: new Date(Date.now() - 86400000), messages: [] },
|
|
{ id: 4, title: '应急预案讨论', date: new Date(Date.now() - 172800000), messages: [] }
|
|
])
|
|
|
|
// 今天的对话
|
|
const todayConversations = computed(() => {
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
return conversations.value.filter(conv => new Date(conv.date) >= today)
|
|
})
|
|
|
|
// 历史对话
|
|
const olderConversations = computed(() => {
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
return conversations.value.filter(conv => new Date(conv.date) < today)
|
|
})
|
|
|
|
const messages = ref([])
|
|
|
|
// 各智能体的建议内容
|
|
const agentSuggestions = {
|
|
default: [
|
|
'城市生命线关键设施有哪些?',
|
|
'消防安全隐患常见问题以及处理措施有哪些?',
|
|
'如何平衡排水能力和生态环境保护?'
|
|
],
|
|
xiaohongshu: [
|
|
'帮我写一篇关于美食探店的小红书文案',
|
|
'生成一篇旅行打卡的种草笔记',
|
|
'写一篇护肤心得分享文案'
|
|
],
|
|
contract: [
|
|
'帮我审核这份合同的风险点',
|
|
'分析合同中的关键条款',
|
|
'生成一份标准服务合同模板'
|
|
],
|
|
video: [
|
|
'帮我写一个产品介绍短视频脚本',
|
|
'生成一个美食探店的视频文案',
|
|
'写一个知识科普类短视频脚本'
|
|
],
|
|
email: [
|
|
'帮我写一封商务合作邀请邮件',
|
|
'生成一封会议通知邮件',
|
|
'写一封工作周报汇报邮件'
|
|
],
|
|
translate: [
|
|
'将这段中文翻译成英文',
|
|
'帮我翻译这份技术文档',
|
|
'将这段日文翻译成中文'
|
|
]
|
|
}
|
|
|
|
// 各智能体的欢迎标题
|
|
const agentWelcomeTitles = {
|
|
default: '今天需要我帮你做点什么吗?',
|
|
xiaohongshu: '想要创作什么样的爆款文案?',
|
|
contract: '需要我帮你处理什么合同?',
|
|
video: '想要创作什么样的短视频?',
|
|
email: '需要我帮你写什么邮件?',
|
|
translate: '需要翻译什么内容?'
|
|
}
|
|
|
|
// 当前智能体的建议
|
|
const currentSuggestions = computed(() => {
|
|
return agentSuggestions[currentAgent.value.id] || agentSuggestions.default
|
|
})
|
|
|
|
// 当前欢迎标题
|
|
const welcomeTitle = computed(() => {
|
|
return agentWelcomeTitles[currentAgent.value.id] || agentWelcomeTitles.default
|
|
})
|
|
|
|
const cardIcons = [OfficeBuilding, Warning, Cloudy]
|
|
|
|
const formatTime = (timestamp) => {
|
|
if (!timestamp) return ''
|
|
const date = new Date(timestamp)
|
|
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
const scrollToBottom = async () => {
|
|
await nextTick()
|
|
if (chatContentRef.value) {
|
|
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
|
|
}
|
|
}
|
|
|
|
const handleSuggestionClick = async (suggestion) => {
|
|
inputText.value = suggestion
|
|
await handleSend()
|
|
}
|
|
|
|
// 新建对话
|
|
const handleNewChat = () => {
|
|
const newConv = {
|
|
id: Date.now(),
|
|
title: '新对话',
|
|
date: new Date(),
|
|
messages: []
|
|
}
|
|
conversations.value.unshift(newConv)
|
|
currentConversationId.value = newConv.id
|
|
messages.value = []
|
|
}
|
|
|
|
// 选择对话
|
|
const selectConversation = (conv) => {
|
|
currentConversationId.value = conv.id
|
|
messages.value = conv.messages || []
|
|
}
|
|
|
|
// 删除对话
|
|
const deleteConversation = (id) => {
|
|
const index = conversations.value.findIndex(c => c.id === id)
|
|
if (index > -1) {
|
|
conversations.value.splice(index, 1)
|
|
if (currentConversationId.value === id) {
|
|
currentConversationId.value = null
|
|
messages.value = []
|
|
}
|
|
}
|
|
}
|
|
|
|
// 切换智能体
|
|
const handleAgentChange = (agentId) => {
|
|
agentStore.setCurrentAgent(agentId)
|
|
// 切换智能体时清空对话
|
|
messages.value = []
|
|
currentConversationId.value = null
|
|
}
|
|
|
|
const handleSend = async () => {
|
|
const text = inputText.value.trim()
|
|
if (!text || isLoading.value) return
|
|
|
|
// Add user message
|
|
const userMessage = {
|
|
id: Date.now().toString(),
|
|
content: text,
|
|
role: 'user',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
messages.value.push(userMessage)
|
|
inputText.value = ''
|
|
|
|
await scrollToBottom()
|
|
|
|
// Show loading
|
|
isLoading.value = true
|
|
|
|
try {
|
|
const response = await chatStore.sendMessage(text)
|
|
|
|
// Add assistant message
|
|
const assistantMessage = {
|
|
id: response.id || Date.now().toString() + '_ai',
|
|
content: response.content,
|
|
role: 'assistant',
|
|
timestamp: response.timestamp || new Date().toISOString()
|
|
}
|
|
messages.value.push(assistantMessage)
|
|
} catch (error) {
|
|
console.error('Send message error:', error)
|
|
// Add error message
|
|
messages.value.push({
|
|
id: Date.now().toString() + '_error',
|
|
content: '抱歉,发送失败,请稍后重试。',
|
|
role: 'assistant',
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
} finally {
|
|
isLoading.value = false
|
|
await scrollToBottom()
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// 处理路由参数中的智能体ID
|
|
const agentId = route.query.agentId
|
|
if (agentId) {
|
|
agentStore.setCurrentAgent(agentId)
|
|
}
|
|
|
|
try {
|
|
const data = await chatStore.getSuggestions()
|
|
if (data && data.length > 0) {
|
|
suggestions.value = data
|
|
}
|
|
} catch (error) {
|
|
console.log('Using default suggestions')
|
|
}
|
|
})
|
|
|
|
// 监听路由变化
|
|
watch(() => route.query.agentId, (newAgentId) => {
|
|
if (newAgentId) {
|
|
agentStore.setCurrentAgent(newAgentId)
|
|
messages.value = []
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.chat-view {
|
|
display: flex;
|
|
flex-direction: row;
|
|
height: 100%;
|
|
background: #fff;
|
|
position: relative;
|
|
}
|
|
|
|
// 左侧侧边栏样式 - ChatGPT Style
|
|
.chat-sidebar {
|
|
width: 260px;
|
|
background: #f7f7f8;
|
|
border-right: 1px solid #e5e7eb;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: width 0.3s ease;
|
|
|
|
&.collapsed {
|
|
width: 60px;
|
|
|
|
.new-chat-btn {
|
|
padding: 10px;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.new-chat-btn {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 10px 16px;
|
|
background: #fff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background: #f3f4f6;
|
|
border-color: #7c3aed;
|
|
color: #7c3aed;
|
|
}
|
|
}
|
|
|
|
.collapse-toggle {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 10px 16px;
|
|
background: #fff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background: #f3f4f6;
|
|
color: #7c3aed;
|
|
}
|
|
}
|
|
|
|
.conversations-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 12px 8px;
|
|
}
|
|
|
|
.list-section {
|
|
margin-bottom: 16px;
|
|
|
|
.section-title {
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.conversation-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: #374151;
|
|
|
|
&:hover {
|
|
background: #e5e7eb;
|
|
|
|
.conv-actions {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
&.active {
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
.conv-title {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.conv-actions {
|
|
opacity: 0;
|
|
display: flex;
|
|
gap: 4px;
|
|
transition: opacity 0.2s;
|
|
|
|
.action-icon {
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
color: #6b7280;
|
|
|
|
&:hover {
|
|
background: #d1d5db;
|
|
color: #ef4444;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 主聊天区域
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.chat-header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
.agent-dropdown {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.header-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
cursor: pointer;
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background: #f3f4f6;
|
|
color: #7c3aed;
|
|
}
|
|
|
|
.agent-icon {
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.dropdown-agent-icon {
|
|
margin-right: 8px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
:deep(.el-dropdown-menu__item.is-active) {
|
|
background: rgba(124, 58, 237, 0.1);
|
|
color: #7c3aed;
|
|
}
|
|
|
|
.chat-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 40px 80px;
|
|
}
|
|
|
|
.welcome-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 400px;
|
|
|
|
.ai-avatar {
|
|
width: 88px;
|
|
height: 88px;
|
|
margin-bottom: 24px;
|
|
|
|
.avatar-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 16px;
|
|
object-fit: contain;
|
|
background: #f3f4f6;
|
|
padding: 8px;
|
|
}
|
|
|
|
.avatar-icon {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 40px;
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
.welcome-text {
|
|
color: #6b7280;
|
|
font-size: 14px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.welcome-title {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
margin-bottom: 40px;
|
|
}
|
|
}
|
|
|
|
.suggestion-cards {
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
max-width: 800px;
|
|
|
|
.suggestion-card {
|
|
width: 220px;
|
|
padding: 20px;
|
|
background: #f9fafb;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
border-color: #7c3aed;
|
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.card-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 20px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.card-text {
|
|
color: #374151;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
}
|
|
}
|
|
|
|
.messages-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
|
|
.message {
|
|
display: flex;
|
|
gap: 12px;
|
|
|
|
&.user {
|
|
flex-direction: row-reverse;
|
|
|
|
.message-content {
|
|
align-items: flex-end;
|
|
|
|
.message-text {
|
|
background: #7c3aed;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
|
|
.message-avatar {
|
|
flex-shrink: 0;
|
|
|
|
.ai-avatar-small {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
object-fit: contain;
|
|
background: #f3f4f6;
|
|
padding: 4px;
|
|
}
|
|
|
|
.user-avatar-small {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: #e5e7eb;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
|
|
.message-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
max-width: 70%;
|
|
|
|
.message-text {
|
|
padding: 12px 16px;
|
|
background: #f3f4f6;
|
|
border-radius: 12px;
|
|
color: #1f2937;
|
|
line-height: 1.6;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.message-time {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
padding: 0 4px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.typing-indicator {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 16px;
|
|
|
|
span {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #9ca3af;
|
|
border-radius: 50%;
|
|
animation: typing 1.4s infinite ease-in-out both;
|
|
|
|
&:nth-child(1) { animation-delay: -0.32s; }
|
|
&:nth-child(2) { animation-delay: -0.16s; }
|
|
}
|
|
}
|
|
|
|
@keyframes typing {
|
|
0%, 80%, 100% {
|
|
transform: scale(0.6);
|
|
opacity: 0.5;
|
|
}
|
|
40% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.input-area {
|
|
padding: 20px 80px 30px;
|
|
background: #fff;
|
|
|
|
.input-wrapper {
|
|
background: #f9fafb;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
|
|
textarea {
|
|
width: 100%;
|
|
border: none;
|
|
background: transparent;
|
|
resize: none;
|
|
outline: none;
|
|
font-size: 14px;
|
|
color: #1f2937;
|
|
min-height: 24px;
|
|
max-height: 120px;
|
|
|
|
&::placeholder {
|
|
color: #9ca3af;
|
|
}
|
|
}
|
|
|
|
.input-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 12px;
|
|
|
|
.action-buttons, .send-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #6b7280;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background: #e5e7eb;
|
|
color: #374151;
|
|
}
|
|
}
|
|
|
|
.send-btn {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: #7c3aed;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
transition: all 0.2s;
|
|
|
|
&:hover:not(:disabled) {
|
|
background: #5b21b6;
|
|
}
|
|
|
|
&:disabled {
|
|
background: #d1d5db;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|