1255 lines
28 KiB
Vue
1255 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { ref, nextTick, computed, onMounted, watch } from 'vue'
|
||
import { useUserStore } from '../stores/user'
|
||
|
||
// 定义Agent类型
|
||
interface Agent {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
icon?: string
|
||
hello_prompt?: string
|
||
}
|
||
|
||
// 定义API返回的Agent类型
|
||
interface AgentResponse {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
hello_prompt: string
|
||
}
|
||
|
||
// 获取用户状态
|
||
const userStore = useUserStore()
|
||
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||
|
||
// 显示访问限制提示
|
||
const showAccessDeniedAlert = ref(false)
|
||
|
||
// Agent列表
|
||
const agents = ref<Agent[]>([])
|
||
const isLoadingAgents = ref(false)
|
||
|
||
// 当前选中的Agent
|
||
const selectedAgent = ref<Agent | null>(null)
|
||
|
||
// 根据环境选择API基础URL
|
||
const apiBaseUrl =
|
||
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
|
||
|
||
// 获取Agent列表
|
||
const fetchAgents = async () => {
|
||
if (!isAuthenticated.value) return
|
||
|
||
isLoadingAgents.value = true
|
||
try {
|
||
const headers: Record<string, string> = {}
|
||
if (userStore.authHeader) {
|
||
headers['Authorization'] = userStore.authHeader
|
||
}
|
||
|
||
const response = await fetch(`${apiBaseUrl}/agent/list`, {
|
||
method: 'GET',
|
||
headers,
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
agents.value = data.map((agent: AgentResponse) => ({
|
||
...agent,
|
||
}))
|
||
|
||
if (agents.value.length > 0 && !selectedAgent.value) {
|
||
selectedAgent.value = agents.value[0]
|
||
addInitialGreeting(agents.value[0])
|
||
}
|
||
} catch (error) {
|
||
console.error('获取Agent列表失败:', error)
|
||
} finally {
|
||
isLoadingAgents.value = false
|
||
}
|
||
}
|
||
|
||
// 当用户登录状态改变时获取Agent列表
|
||
onMounted(() => {
|
||
if (isAuthenticated.value) {
|
||
fetchAgents()
|
||
}
|
||
})
|
||
|
||
interface ChatMessage {
|
||
role: 'user' | 'assistant' | 'thought'
|
||
content: string
|
||
files: Array<{
|
||
type: string
|
||
url: string
|
||
}>
|
||
thought?: {
|
||
thought: string
|
||
observation: string
|
||
tool: string
|
||
tool_input: string
|
||
}
|
||
tools?: string[]
|
||
}
|
||
|
||
const userInput = ref('')
|
||
const chatHistory = ref<ChatMessage[]>([])
|
||
const isLoading = ref(false)
|
||
const messagesContainer = ref<HTMLElement | null>(null)
|
||
const currentResponseText = ref('')
|
||
|
||
// 当用户不是VIP时,显示提示
|
||
onMounted(() => {
|
||
if (isAuthenticated.value) {
|
||
fetchAgents()
|
||
}
|
||
})
|
||
|
||
const closeAlert = () => {
|
||
showAccessDeniedAlert.value = false
|
||
}
|
||
|
||
const scrollToBottom = async () => {
|
||
await nextTick()
|
||
if (messagesContainer.value) {
|
||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||
}
|
||
}
|
||
|
||
// 添加打字机效果的状态
|
||
const typingText = ref('')
|
||
const isTyping = ref(false)
|
||
const typingMessageIndex = ref(-1)
|
||
|
||
// 打字机效果函数
|
||
const typeMessage = async (message: string, messageIndex: number) => {
|
||
isTyping.value = true
|
||
typingMessageIndex.value = messageIndex
|
||
typingText.value = ''
|
||
chatHistory.value[messageIndex].content = ''
|
||
|
||
for (let i = 0; i < message.length; i++) {
|
||
typingText.value += message[i]
|
||
chatHistory.value[messageIndex].content = typingText.value
|
||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||
}
|
||
|
||
isTyping.value = false
|
||
typingMessageIndex.value = -1
|
||
chatHistory.value[messageIndex].content = message
|
||
return message
|
||
}
|
||
|
||
// 添加初始问候语
|
||
const addInitialGreeting = async (agent: Agent) => {
|
||
const message: ChatMessage = {
|
||
role: 'assistant',
|
||
content: '',
|
||
files: [],
|
||
}
|
||
chatHistory.value = [message]
|
||
|
||
const greetingText =
|
||
agent.hello_prompt ||
|
||
'你好!我是AI Agent,可以回答你的任何关于Web3的问题。请告诉我你想了解什么?'
|
||
await typeMessage(greetingText, 0)
|
||
}
|
||
|
||
// 监听选中的Agent变化
|
||
watch(selectedAgent, (newAgent) => {
|
||
if (newAgent && chatHistory.value.length === 0) {
|
||
addInitialGreeting(newAgent).catch(console.error)
|
||
}
|
||
})
|
||
|
||
const showConfirmDialog = ref(false)
|
||
const confirmDialogTitle = ref('')
|
||
const confirmDialogMessage = ref('')
|
||
const confirmCallback = ref<(() => void) | null>(null)
|
||
|
||
const showConfirm = (title: string, message: string, callback: () => void) => {
|
||
confirmDialogTitle.value = title
|
||
confirmDialogMessage.value = message
|
||
confirmCallback.value = callback
|
||
showConfirmDialog.value = true
|
||
}
|
||
|
||
// 选择Agent
|
||
const selectAgent = (agent: Agent) => {
|
||
if (!selectedAgent.value || agent.id !== selectedAgent.value.id) {
|
||
// 直接切换到新的 Agent
|
||
selectedAgent.value = agent
|
||
chatHistory.value = []
|
||
addInitialGreeting(agent).catch(console.error)
|
||
} else {
|
||
// 点击当前 Agent,显示确认框
|
||
showConfirm('重新开始对话', '是否重新开启新会话?', () => {
|
||
chatHistory.value = []
|
||
addInitialGreeting(agent).catch(console.error)
|
||
showConfirmDialog.value = false
|
||
})
|
||
}
|
||
}
|
||
|
||
const sendMessage = async () => {
|
||
if (!userInput.value.trim() || isLoading.value || !isAuthenticated.value || !selectedAgent.value)
|
||
return
|
||
|
||
// 重置当前响应文本
|
||
currentResponseText.value = ''
|
||
|
||
// 添加用户消息到历史记录
|
||
chatHistory.value.push({
|
||
role: 'user',
|
||
content: userInput.value,
|
||
files: [],
|
||
})
|
||
|
||
// 滚动到底部以显示用户消息
|
||
await scrollToBottom()
|
||
|
||
const currentInput = userInput.value
|
||
userInput.value = ''
|
||
isLoading.value = true
|
||
|
||
// 添加临时助手消息用于流式显示
|
||
const tempMessageIndex = chatHistory.value.length
|
||
chatHistory.value.push({
|
||
role: 'assistant',
|
||
content: 'AI 正在思考...',
|
||
files: [],
|
||
tools: [],
|
||
})
|
||
|
||
await scrollToBottom()
|
||
|
||
try {
|
||
// 准备请求头,包含认证信息
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
|
||
if (userStore.authHeader) {
|
||
headers['Authorization'] = userStore.authHeader
|
||
}
|
||
|
||
// 调用流式接口
|
||
const response = await fetch(`${apiBaseUrl}/agent/chat`, {
|
||
method: 'POST',
|
||
headers,
|
||
body: JSON.stringify({
|
||
user_prompt: currentInput,
|
||
agent_id: selectedAgent.value.id,
|
||
}),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text()
|
||
console.error('API响应错误:', response.status, errorText)
|
||
throw new Error(`服务器响应错误: ${response.status} - ${errorText || '未知错误'}`)
|
||
}
|
||
|
||
const reader = response.body?.getReader()
|
||
if (!reader) {
|
||
throw new Error('无法获取响应流')
|
||
}
|
||
|
||
const decoder = new TextDecoder()
|
||
let buffer = '' // 用于存储不完整的JSON数据
|
||
|
||
// 处理流式响应
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
const chunk = decoder.decode(value, { stream: true })
|
||
buffer += chunk
|
||
|
||
// 处理可能的多行数据
|
||
const lines = buffer.split('\n')
|
||
// 保留最后一行(可能不完整)
|
||
buffer = lines.pop() || ''
|
||
|
||
for (const line of lines) {
|
||
if (!line.trim() || !line.startsWith('data: ')) continue
|
||
|
||
try {
|
||
const jsonStr = line.slice(6) // 移除 "data: " 前缀
|
||
console.log('收到的JSON数据:', jsonStr) // 调试日志
|
||
const data = JSON.parse(jsonStr)
|
||
|
||
switch (data.event) {
|
||
case 'agent_message':
|
||
if (data.answer) {
|
||
currentResponseText.value += data.answer
|
||
chatHistory.value[tempMessageIndex].content = currentResponseText.value
|
||
await scrollToBottom()
|
||
}
|
||
break
|
||
|
||
case 'agent_thought':
|
||
if (data.tool) {
|
||
const thoughtText = `Agent 正在调用 ${data.tool} 工具`
|
||
chatHistory.value[tempMessageIndex].content = currentResponseText.value
|
||
? `${thoughtText}\n\n${currentResponseText.value}`
|
||
: thoughtText
|
||
await scrollToBottom()
|
||
}
|
||
break
|
||
|
||
case 'error':
|
||
const errorMessage = data.error || '未知错误'
|
||
chatHistory.value[tempMessageIndex].content =
|
||
`抱歉,处理您的请求时出错了:${errorMessage}`
|
||
isLoading.value = false
|
||
await scrollToBottom()
|
||
break
|
||
|
||
case 'message_end':
|
||
if (currentResponseText.value.trim()) {
|
||
chatHistory.value[tempMessageIndex].content = currentResponseText.value
|
||
}
|
||
break
|
||
}
|
||
} catch (e) {
|
||
console.error('解析JSON数据出错:', e, '原始数据:', line)
|
||
if (line.includes('<!DOCTYPE html>') || line.includes('<html>')) {
|
||
chatHistory.value[tempMessageIndex].content =
|
||
'抱歉,服务器返回了错误的响应格式,请稍后再试。'
|
||
} else {
|
||
chatHistory.value[tempMessageIndex].content = '抱歉,解析响应数据时出错了,请稍后再试。'
|
||
}
|
||
await scrollToBottom()
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('调用API出错:', error)
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
chatHistory.value[tempMessageIndex].content = `抱歉,请求出错了:${errorMessage}`
|
||
await scrollToBottom()
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 在 script 部分添加图标渲染函数
|
||
const getAgentIcon = (agent: Agent) => {
|
||
// 根据 agent 类型返回对应的 SVG 图标
|
||
return `<svg class="agent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
${getIconPath(agent)}
|
||
</svg>`
|
||
}
|
||
|
||
const getIconPath = (agent: Agent) => {
|
||
// 根据 agent id 或其他属性返回不同的图标路径
|
||
switch (agent.id) {
|
||
case 'market_analysis':
|
||
return '<path d="M7 12l5-5 5 5M7 17l5-5 5 5"/>'
|
||
case 'trading_strategy':
|
||
return '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7"/>'
|
||
default:
|
||
return '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line"/>'
|
||
}
|
||
}
|
||
|
||
// 在 script 部分修改获取首字母的函数
|
||
const getInitial = (nickname: string) => {
|
||
if (!nickname) return '?'
|
||
return nickname.trim()[0].toUpperCase()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="ai-agent-view">
|
||
<!-- 访问限制提示 -->
|
||
<div v-if="showAccessDeniedAlert" class="access-denied-alert">
|
||
<div class="alert-content">
|
||
<span>AI Agent功能仅对VIP及超级VIP用户开放</span>
|
||
<button class="close-alert" @click="closeAlert">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- <h1 class="page-title">AI Agent</h1>
|
||
<p class="page-description">为您提供全面区块链信息查询和专业分析</p> -->
|
||
|
||
<!-- 未登录状态显示登录提示 -->
|
||
<div v-if="!isAuthenticated" class="login-prompt">
|
||
<div class="login-prompt-content">
|
||
<h2>需要登录</h2>
|
||
<p>请登录或注册账号以使用AI Agent功能</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已登录用户显示聊天界面 -->
|
||
<div v-else class="main-container">
|
||
<!-- 右侧聊天界面 -->
|
||
<div class="chat-container">
|
||
<div class="chat-window">
|
||
<div class="messages-container" ref="messagesContainer">
|
||
<div v-for="(message, index) in chatHistory" :key="index" class="message-wrapper">
|
||
<div
|
||
:class="[
|
||
'message',
|
||
message.role === 'user'
|
||
? 'user-message'
|
||
: message.role === 'thought'
|
||
? 'thought-message'
|
||
: 'ai-message',
|
||
]"
|
||
>
|
||
<div v-if="message.role === 'assistant'" class="avatar ai-avatar">
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path
|
||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||
></path>
|
||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||
</svg>
|
||
</div>
|
||
<div v-if="message.role === 'user'" class="avatar user-avatar">
|
||
{{ getInitial(userStore.userInfo?.nickname || '') }}
|
||
</div>
|
||
<div
|
||
class="message-content"
|
||
:class="{
|
||
typing:
|
||
isTyping && typingMessageIndex === index && message.role === 'assistant',
|
||
}"
|
||
>
|
||
{{ message.content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Agent 选择器移到这里 -->
|
||
<div class="agent-selector">
|
||
<div class="agent-list">
|
||
<div v-if="isLoadingAgents" class="agent-loading">加载中...</div>
|
||
<template v-else>
|
||
<button
|
||
v-for="agent in agents"
|
||
:key="agent.id"
|
||
class="agent-item"
|
||
:class="{ active: selectedAgent?.id === agent.id }"
|
||
@click="selectAgent(agent)"
|
||
>
|
||
<span class="agent-icon" v-html="getAgentIcon(agent)"></span>
|
||
<span class="agent-name">{{ agent.name }}</span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-container">
|
||
<div class="input-wrapper">
|
||
<textarea
|
||
v-model="userInput"
|
||
class="message-input"
|
||
placeholder="请输入您的问题..."
|
||
@keydown.enter.prevent="sendMessage"
|
||
:disabled="isLoading || !isAuthenticated || !selectedAgent"
|
||
></textarea>
|
||
<button
|
||
class="send-button"
|
||
@click="sendMessage"
|
||
:disabled="isLoading || !isAuthenticated || !selectedAgent"
|
||
>
|
||
<span class="send-icon">➤</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加确认对话框 -->
|
||
<div v-if="showConfirmDialog" class="confirm-popup">
|
||
<div class="confirm-popup-content">
|
||
<p>{{ confirmDialogMessage }}</p>
|
||
<div class="confirm-actions">
|
||
<button
|
||
@click="
|
||
() => {
|
||
confirmCallback?.()
|
||
showConfirmDialog = false
|
||
}
|
||
"
|
||
>
|
||
确认
|
||
</button>
|
||
<button @click="showConfirmDialog = false">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.ai-agent-view {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background-color: var(--color-bg-primary);
|
||
}
|
||
|
||
/* 访问限制提示样式 */
|
||
.access-denied-alert {
|
||
position: fixed;
|
||
top: calc(var(--header-height) + 20px);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background-color: rgba(255, 152, 0, 0.95);
|
||
color: #000;
|
||
padding: 12px 20px;
|
||
border-radius: var(--border-radius);
|
||
z-index: 1000;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
animation: slideDown 0.3s ease-out;
|
||
max-width: 90%;
|
||
width: auto;
|
||
}
|
||
|
||
.alert-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.close-alert {
|
||
background: none;
|
||
border: none;
|
||
color: #000;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
transform: translate(-50%, -20px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translate(-50%, 0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 2.2rem;
|
||
font-weight: 700;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.page-description {
|
||
color: var(--color-text-secondary);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
/* 登录提示样式 */
|
||
.login-prompt {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 3rem 1rem;
|
||
width: 100%;
|
||
height: calc(80vh - 150px);
|
||
}
|
||
|
||
.login-prompt-content {
|
||
background-color: var(--color-bg-elevated);
|
||
border-radius: var(--border-radius);
|
||
padding: 2rem;
|
||
text-align: center;
|
||
max-width: 500px;
|
||
width: 100%;
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.login-prompt-content h2 {
|
||
font-size: 1.8rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.login-prompt-content p {
|
||
color: var(--color-text-secondary);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.login-prompt-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.login-button,
|
||
.register-button {
|
||
padding: 0.8rem 2rem;
|
||
border-radius: var(--border-radius);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.login-button {
|
||
background-color: transparent;
|
||
border: 1px solid var(--color-border);
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.login-button:hover {
|
||
background-color: var(--color-bg-elevated);
|
||
border-color: var(--color-border-hover);
|
||
}
|
||
|
||
.register-button {
|
||
background-color: #3355ff;
|
||
color: white;
|
||
border: 1px solid #3355ff;
|
||
}
|
||
|
||
.register-button:hover {
|
||
background-color: #2244ee;
|
||
}
|
||
|
||
/* 聊天界面样式 */
|
||
.main-container {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
background-color: var(--color-bg-primary);
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.chat-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
background-color: var(--color-bg-primary);
|
||
height: 100%;
|
||
}
|
||
|
||
.chat-window {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: var(--color-bg-primary);
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.messages-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 2rem 0.5rem;
|
||
padding-bottom: 2rem;
|
||
scroll-behavior: smooth;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.message-wrapper {
|
||
margin: 0.75rem 0;
|
||
display: flex;
|
||
width: 100%;
|
||
}
|
||
|
||
.user-message {
|
||
justify-content: flex-end;
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.ai-message,
|
||
.thought-message {
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.user-avatar {
|
||
background-color: var(--color-accent);
|
||
color: white;
|
||
font-size: 1.1rem;
|
||
font-weight: 500;
|
||
border: none;
|
||
}
|
||
|
||
.ai-avatar {
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.ai-avatar svg {
|
||
width: 24px;
|
||
height: 24px;
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.message {
|
||
display: flex;
|
||
padding: 0;
|
||
}
|
||
|
||
.message-content {
|
||
display: inline-block;
|
||
max-width: 800px;
|
||
min-width: 4em;
|
||
word-break: break-word;
|
||
overflow-wrap: break-word;
|
||
white-space: pre-wrap;
|
||
box-sizing: border-box;
|
||
background-clip: padding-box;
|
||
line-height: 1.6;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.message-content.typing :deep(.markdown-body) {
|
||
display: inline;
|
||
}
|
||
|
||
.message-content :deep(p) {
|
||
margin: 0;
|
||
line-height: 1.4;
|
||
display: inline;
|
||
}
|
||
|
||
.message-content :deep(p:not(:last-child)) {
|
||
display: block;
|
||
margin-bottom: 0.75em;
|
||
}
|
||
|
||
/* 确保markdown内容正确显示 */
|
||
:deep(.markdown-body) {
|
||
color: inherit;
|
||
line-height: inherit;
|
||
}
|
||
|
||
.user-message .message-content {
|
||
background-color: var(--color-accent);
|
||
color: white;
|
||
border-radius: 0 1rem 1rem 1rem;
|
||
padding: 1.25rem;
|
||
}
|
||
|
||
.ai-message .message-content {
|
||
background-color: var(--color-bg-secondary);
|
||
color: var(--color-text-primary);
|
||
border-radius: 1rem 0 1rem 1rem;
|
||
border: 1px solid var(--color-border);
|
||
padding: 1.25rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.thought-message .message-content {
|
||
background-color: rgba(var(--color-accent-rgb), 0.08);
|
||
color: var(--color-text-secondary);
|
||
border-radius: 0.75rem;
|
||
font-style: italic;
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid rgba(var(--color-accent-rgb), 0.15);
|
||
font-size: 0.95rem;
|
||
max-width: 600px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 调整思考消息的间距 */
|
||
.thought-message + .ai-message {
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
/* 确保思考消息和AI回复之间的视觉关联 */
|
||
.thought-message {
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.message-content {
|
||
max-width: 90%;
|
||
}
|
||
|
||
.avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
.avatar svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.thought-message .message-content {
|
||
max-width: 85%;
|
||
font-size: 0.9rem;
|
||
padding: 0.6rem 0.8rem;
|
||
}
|
||
}
|
||
|
||
.input-container {
|
||
padding: 0.5rem 0.5rem 2rem 0.5rem;
|
||
background-color: var(--color-bg-primary);
|
||
width: 100%;
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 0.5rem;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.message-input {
|
||
flex: 1;
|
||
height: 3rem;
|
||
min-height: 3rem;
|
||
max-height: 6rem;
|
||
padding: 0.75rem 3rem 0.75rem 1rem;
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--border-radius);
|
||
color: var(--color-text-primary);
|
||
font-size: 1rem;
|
||
line-height: 1.4;
|
||
resize: none;
|
||
outline: none;
|
||
transition: all 0.2s ease;
|
||
overflow-y: hidden;
|
||
display: block;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.message-input::placeholder {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.message-input:focus {
|
||
border-color: var(--color-accent);
|
||
}
|
||
|
||
.message-input:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.send-button {
|
||
position: absolute;
|
||
right: 0.75rem;
|
||
bottom: 0.5rem;
|
||
width: 2rem;
|
||
height: 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--color-accent);
|
||
border: none;
|
||
border-radius: 50%;
|
||
color: white;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.send-button:hover:not(:disabled) {
|
||
background-color: var(--color-accent-hover);
|
||
}
|
||
|
||
.send-button:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.send-icon {
|
||
font-size: 1rem;
|
||
transform: rotate(270deg);
|
||
}
|
||
|
||
:deep(.markdown-body) {
|
||
color: inherit;
|
||
line-height: inherit;
|
||
}
|
||
|
||
:deep(.markdown-body p) {
|
||
margin: 0;
|
||
line-height: inherit;
|
||
display: inline;
|
||
}
|
||
|
||
:deep(.markdown-body p:not(:last-child)) {
|
||
display: block;
|
||
margin-bottom: 0.75em;
|
||
}
|
||
|
||
/* 修改 Agent 选择器样式 */
|
||
.agent-selector {
|
||
background-color: #ffffff;
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.agent-list {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 0.75rem;
|
||
padding: 0.75rem;
|
||
overflow-x: auto;
|
||
scrollbar-width: none; /* Firefox */
|
||
-ms-overflow-style: none; /* IE and Edge */
|
||
}
|
||
|
||
.agent-list::-webkit-scrollbar {
|
||
display: none; /* Chrome, Safari and Opera */
|
||
}
|
||
|
||
.agent-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
padding: 0.75rem 1rem;
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid rgba(51, 85, 255, 0.15);
|
||
border-radius: 8px;
|
||
color: var(--color-text-primary);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
font-size: 0.95rem;
|
||
font-weight: 500;
|
||
min-width: 120px;
|
||
width: fit-content;
|
||
justify-content: flex-start;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.agent-item:hover {
|
||
border-color: var(--color-accent);
|
||
background-color: rgba(51, 85, 255, 0.04);
|
||
}
|
||
|
||
.agent-item.active {
|
||
background-color: var(--color-accent);
|
||
border-color: var(--color-accent);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.agent-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
flex-shrink: 0;
|
||
stroke: currentColor;
|
||
}
|
||
|
||
.agent-item.active .agent-icon {
|
||
stroke: #ffffff;
|
||
}
|
||
|
||
.agent-name {
|
||
text-align: left;
|
||
}
|
||
|
||
.agent-loading {
|
||
padding: 0.75rem;
|
||
text-align: center;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.agent-list {
|
||
padding: 0.5rem;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.agent-item {
|
||
padding: 0.6rem 1rem;
|
||
font-size: 0.9rem;
|
||
min-width: 100px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.messages-container {
|
||
padding: 0.75rem 0.5rem;
|
||
padding-bottom: calc(8rem + 60px); /* 为 Agent 选择器预留空间 */
|
||
}
|
||
|
||
.input-container {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 0.75rem;
|
||
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
|
||
background-color: var(--color-bg-primary);
|
||
border-top: 1px solid var(--color-border);
|
||
z-index: 10;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
margin: 0;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.input-wrapper {
|
||
max-width: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.message-input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.send-button {
|
||
position: absolute;
|
||
right: 0.75rem;
|
||
bottom: 0.5rem;
|
||
}
|
||
|
||
.agent-selector {
|
||
position: fixed;
|
||
bottom: calc(4rem + env(safe-area-inset-bottom));
|
||
left: 0;
|
||
right: 0;
|
||
background-color: var(--color-bg-primary);
|
||
border-top: 1px solid var(--color-border);
|
||
z-index: 11; /* 确保比输入框的z-index高 */
|
||
padding: 0;
|
||
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.agent-list {
|
||
padding: 0.5rem;
|
||
gap: 0.5rem;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
margin: 0;
|
||
background-color: var(--color-bg-primary);
|
||
}
|
||
|
||
.agent-item {
|
||
padding: 0.5rem 0.75rem;
|
||
font-size: 0.9rem;
|
||
min-width: auto;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.message-content {
|
||
padding: 1rem;
|
||
font-size: 0.95rem;
|
||
}
|
||
}
|
||
|
||
.message-content {
|
||
overflow: auto;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.message-content pre,
|
||
.message-content table {
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.message-content ul,
|
||
.message-content ol {
|
||
/* margin: 0.5em 0; */
|
||
padding-left: 1.5em;
|
||
box-sizing: border-box;
|
||
max-width: 100%;
|
||
overflow-wrap: break-word;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.message-content li {
|
||
/* margin: 0.25em 0; */
|
||
word-break: break-word;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.user-message .markdown-body {
|
||
color: white;
|
||
}
|
||
|
||
.user-message :deep(.markdown-body) {
|
||
color: inherit;
|
||
}
|
||
|
||
.user-message :deep(.markdown-body pre) {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.user-message :deep(.markdown-body blockquote) {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
border-left-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.user-message :deep(.markdown-body a) {
|
||
color: inherit;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.user-message :deep(.markdown-body code) {
|
||
color: inherit;
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* 添加确认对话框样式 */
|
||
.confirm-popup {
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 1000;
|
||
animation: slideDown 0.3s ease-out;
|
||
}
|
||
|
||
.confirm-popup-content {
|
||
background-color: #ffffff;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
min-width: 300px;
|
||
max-width: 90vw;
|
||
}
|
||
|
||
.confirm-popup-content p {
|
||
margin: 0;
|
||
color: #000000;
|
||
font-size: 0.95rem;
|
||
flex: 1;
|
||
}
|
||
|
||
.confirm-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.confirm-actions button {
|
||
padding: 0.4rem 0.8rem;
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: none;
|
||
background: none;
|
||
}
|
||
|
||
.confirm-actions button:first-child {
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.confirm-actions button:first-child:hover {
|
||
background-color: rgba(51, 85, 255, 0.1);
|
||
}
|
||
|
||
.confirm-actions button:last-child {
|
||
color: #666666;
|
||
}
|
||
|
||
.confirm-actions button:last-child:hover {
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
transform: translate(-50%, -100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translate(-50%, 0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.confirm-popup {
|
||
top: 10px;
|
||
width: calc(100% - 20px);
|
||
}
|
||
|
||
.confirm-popup-content {
|
||
padding: 0.8rem 1rem;
|
||
min-width: auto;
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
/* 调整用户头像样式以更好地显示中文字符 */
|
||
.user-avatar {
|
||
font-size: 1.1rem;
|
||
font-weight: 500;
|
||
color: white;
|
||
background-color: var(--color-accent);
|
||
border: none;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.user-avatar {
|
||
font-size: 1rem;
|
||
}
|
||
}
|
||
</style>
|