web/src/views/AIAgentView.vue
2025-05-11 18:14:43 +08:00

1255 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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.

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