feat: 优化 Chat 页面和聊天样式
- 新增 chat.css 聊天样式文件 - 优化 Chat.vue 页面交互 - 更新 chat.ts 聊天逻辑 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { useChat } from './chat/chat'
|
import { useChat } from './chat/chat'
|
||||||
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
||||||
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
||||||
@@ -188,6 +189,18 @@ const handleSelectModel = (model: any) => {
|
|||||||
showModelDropdown.value = false
|
showModelDropdown.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 选择智能体并创建会话
|
||||||
|
const selectAgentAndCreateSession = async (agent: any) => {
|
||||||
|
selectedAgent.value = agent
|
||||||
|
const session = await createSession()
|
||||||
|
if (session) {
|
||||||
|
messages.value = [
|
||||||
|
{ id: Date.now(), role: 'assistant', content: `你好!我是 ${agent.name},有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||||
|
]
|
||||||
|
await saveMessage('assistant', messages.value[0].content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除会话
|
// 删除会话
|
||||||
const handleDeleteSession = async (session: any) => {
|
const handleDeleteSession = async (session: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -236,15 +249,16 @@ const generateSessionTitle = async () => {
|
|||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!inputMessage.value.trim() || isLoading.value) return
|
if (!inputMessage.value.trim() || isLoading.value) return
|
||||||
|
|
||||||
|
// 如果没有会话,提示用户先选择智能体
|
||||||
|
if (!currentSessionId.value) {
|
||||||
|
ElMessage.warning('请先选择或创建一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const userContent = inputMessage.value.trim()
|
const userContent = inputMessage.value.trim()
|
||||||
inputMessage.value = ''
|
inputMessage.value = ''
|
||||||
resetInputHeight()
|
resetInputHeight()
|
||||||
|
|
||||||
if (!currentSessionId.value) {
|
|
||||||
const session = await createSession()
|
|
||||||
if (!session) return
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage = createUserMessage(userContent)
|
const userMessage = createUserMessage(userContent)
|
||||||
messages.value.push(userMessage)
|
messages.value.push(userMessage)
|
||||||
await saveMessage('user', userContent)
|
await saveMessage('user', userContent)
|
||||||
@@ -287,6 +301,7 @@ onUnmounted(() => {
|
|||||||
<div class="flex-1 flex flex-col bg-[#09090b]">
|
<div class="flex-1 flex flex-col bg-[#09090b]">
|
||||||
<!-- 顶部栏 -->
|
<!-- 顶部栏 -->
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
|
v-if="currentSessionId"
|
||||||
:selected-agent="selectedAgent"
|
:selected-agent="selectedAgent"
|
||||||
:chat-models="chatModels"
|
:chat-models="chatModels"
|
||||||
:selected-model="selectedModel"
|
:selected-model="selectedModel"
|
||||||
@@ -301,8 +316,42 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 消息区域 -->
|
<!-- 消息区域 -->
|
||||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
||||||
<!-- 空状态欢迎提示 -->
|
<!-- 无会话时显示引导界面 -->
|
||||||
<div v-if="messages.length === 0" class="h-full flex items-center justify-center">
|
<div v-if="!currentSessionId" class="h-full flex items-center justify-center empty-chat">
|
||||||
|
<div class="text-center" style="position: relative; z-index: 1;">
|
||||||
|
<div class="empty-logo">🧠</div>
|
||||||
|
<h2 class="empty-title">欢迎使用 X-Agents</h2>
|
||||||
|
<p class="empty-desc">与智能 AI 助手对话,获取专业解答与创意灵感</p>
|
||||||
|
<button @click="newChat" class="empty-btn">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>
|
||||||
|
开始新对话
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 推荐智能体 -->
|
||||||
|
<div v-if="chatAgents.length > 0" class="recommend-section">
|
||||||
|
<div class="recommend-title">或选择一个智能体</div>
|
||||||
|
<div class="recommend-cards">
|
||||||
|
<div
|
||||||
|
v-for="agent in chatAgents.slice(0, 4)"
|
||||||
|
:key="agent.id"
|
||||||
|
class="recommend-card"
|
||||||
|
@click="selectAgentAndCreateSession(agent)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="recommend-avatar"
|
||||||
|
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
||||||
|
>
|
||||||
|
{{ agent.avatar }}
|
||||||
|
</div>
|
||||||
|
<div class="recommend-name">{{ agent.name }}</div>
|
||||||
|
<div class="recommend-desc">{{ agent.description || 'AI 助手' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 有会话但无消息时显示欢迎提示 -->
|
||||||
|
<div v-else-if="messages.length === 0" class="h-full flex items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-5xl mb-4">{{ selectedAgent?.avatar || '🧠' }}</div>
|
<div class="text-5xl mb-4">{{ selectedAgent?.avatar || '🧠' }}</div>
|
||||||
<h2 class="text-xl font-semibold text-white mb-2">和 {{ selectedAgent?.name || 'AI' }} 开始对话</h2>
|
<h2 class="text-xl font-semibold text-white mb-2">和 {{ selectedAgent?.name || 'AI' }} 开始对话</h2>
|
||||||
@@ -320,8 +369,9 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 - 仅在有会话时显示 -->
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
v-if="currentSessionId"
|
||||||
v-model="inputMessage"
|
v-model="inputMessage"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
@send="sendMessage"
|
@send="sendMessage"
|
||||||
|
|||||||
@@ -49,3 +49,147 @@
|
|||||||
.agent-glow {
|
.agent-glow {
|
||||||
animation: pulse-glow 2s ease-in-out infinite;
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 空会话页面样式 */
|
||||||
|
.empty-chat {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-chat::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle at 30% 30%, rgba(249, 115, 22, 0.08) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 70% 70%, rgba(239, 68, 68, 0.06) 0%, transparent 50%);
|
||||||
|
animation: bgFloat 20s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bgFloat {
|
||||||
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||||
|
50% { transform: translate(-2%, -2%) rotate(1deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-logo {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(239, 68, 68, 0.1));
|
||||||
|
border-radius: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48px;
|
||||||
|
animation: logoFloat 3s ease-in-out infinite;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logoFloat {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.7) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn {
|
||||||
|
padding: 14px 32px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 8px 24px rgba(249, 115, 22, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 32px rgba(249, 115, 22, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 推荐智能体卡片 */
|
||||||
|
.recommend-section {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-card {
|
||||||
|
width: 160px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(249, 115, 22, 0.3);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-avatar {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export function useChat() {
|
|||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
})
|
})
|
||||||
|
|
||||||
currentSessionId.value = session.id
|
saveSessionId(session.id)
|
||||||
return session
|
return session
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@@ -508,18 +508,38 @@ export function useChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 选择历史对话
|
// 选择历史对话
|
||||||
|
// 保存会话 ID 到 localStorage
|
||||||
|
const saveSessionId = (sessionId: string) => {
|
||||||
|
localStorage.setItem('current_session_id', sessionId)
|
||||||
|
currentSessionId.value = sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 恢复会话
|
||||||
|
const restoreSession = async () => {
|
||||||
|
const savedSessionId = localStorage.getItem('current_session_id')
|
||||||
|
if (!savedSessionId) return
|
||||||
|
|
||||||
|
const session = chatSessions.value.find(s => s.id === savedSessionId)
|
||||||
|
if (session) {
|
||||||
|
await selectSession(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectSession = async (session: ChatSession) => {
|
const selectSession = async (session: ChatSession) => {
|
||||||
const agent = chatAgents.value.find(a => a.id === session.agent_id)
|
const agent = chatAgents.value.find(a => a.id === session.agent_id)
|
||||||
if (agent) {
|
if (agent) {
|
||||||
selectedAgent.value = agent
|
selectedAgent.value = agent
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSessionId.value = session.id
|
saveSessionId(session.id)
|
||||||
await fetchSessionMessages(session.id)
|
await fetchSessionMessages(session.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新建聊天 - 先打开智能体选择器
|
// 新建聊天 - 先打开智能体选择器
|
||||||
const newChat = () => {
|
const newChat = () => {
|
||||||
|
// 清除当前会话 ID(新建会话时会重新设置)
|
||||||
|
currentSessionId.value = null
|
||||||
|
localStorage.removeItem('current_session_id')
|
||||||
// 打开智能体选择器,让用户选择智能体
|
// 打开智能体选择器,让用户选择智能体
|
||||||
openAgentSelector('single')
|
openAgentSelector('single')
|
||||||
}
|
}
|
||||||
@@ -561,12 +581,15 @@ export function useChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
const init = () => {
|
const init = async () => {
|
||||||
fetchModels()
|
fetchModels()
|
||||||
fetchAgents()
|
fetchAgents()
|
||||||
fetchSessions()
|
await fetchSessions()
|
||||||
fetchGroups()
|
fetchGroups()
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
|
||||||
|
// 恢复之前选中的会话
|
||||||
|
await restoreSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
|
|||||||
Reference in New Issue
Block a user