update
This commit is contained in:
parent
f5f8395ef9
commit
507925efde
@ -5,7 +5,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: icrypto-web:1.0.8
|
||||
image: icrypto-web:1.0.9
|
||||
container_name: icrypto-web
|
||||
ports:
|
||||
- '6000:80'
|
||||
|
||||
@ -1,7 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, computed, onMounted } from 'vue'
|
||||
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)
|
||||
@ -11,17 +28,79 @@ const isVIP = computed(() => userInfo.value && userInfo.value.level >= 1)
|
||||
// 显示访问限制提示
|
||||
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,
|
||||
icon: '📊', // 可以根据agent类型设置不同的图标
|
||||
}))
|
||||
|
||||
// 如果有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
|
||||
}
|
||||
}
|
||||
|
||||
const userInput = ref('')
|
||||
const chatHistory = ref([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是AI Agent,可以回答你的任何关于Web3的问题。请告诉我你想了解什么?',
|
||||
},
|
||||
])
|
||||
const chatHistory = ref<ChatMessage[]>([])
|
||||
const isLoading = ref(false)
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
@ -48,13 +127,41 @@ const scrollToBottom = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加初始问候语
|
||||
const addInitialGreeting = (agent: Agent) => {
|
||||
chatHistory.value = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
agent.hello_prompt ||
|
||||
'你好!我是AI Agent,可以回答你的任何关于Web3的问题。请告诉我你想了解什么?',
|
||||
files: [],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 监听选中的Agent变化
|
||||
watch(selectedAgent, (newAgent) => {
|
||||
if (newAgent) {
|
||||
addInitialGreeting(newAgent)
|
||||
}
|
||||
})
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!userInput.value.trim() || isLoading.value || !isAuthenticated.value || !isVIP.value) return
|
||||
if (
|
||||
!userInput.value.trim() ||
|
||||
isLoading.value ||
|
||||
!isAuthenticated.value ||
|
||||
!isVIP.value ||
|
||||
!selectedAgent.value
|
||||
)
|
||||
return
|
||||
|
||||
// 添加用户消息到历史记录
|
||||
chatHistory.value.push({
|
||||
role: 'user',
|
||||
content: userInput.value,
|
||||
files: [],
|
||||
})
|
||||
|
||||
// 滚动到底部以显示用户消息
|
||||
@ -65,9 +172,11 @@ const sendMessage = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
// 添加临时助手消息用于流式显示
|
||||
const tempMessageIndex = chatHistory.value.length
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
content: 'AI 正在思考...',
|
||||
files: [],
|
||||
})
|
||||
|
||||
await scrollToBottom()
|
||||
@ -89,6 +198,7 @@ const sendMessage = async () => {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_prompt: currentInput,
|
||||
agent_id: selectedAgent.value.id,
|
||||
}),
|
||||
})
|
||||
|
||||
@ -110,20 +220,57 @@ const sendMessage = async () => {
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
responseText += chunk
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
// 更新最后一条消息(助手的回复)
|
||||
const lastIndex = chatHistory.value.length - 1
|
||||
chatHistory.value[lastIndex].content = responseText
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || !line.startsWith('data: ')) continue
|
||||
|
||||
// 滚动到底部以跟随新内容
|
||||
await scrollToBottom()
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6))
|
||||
|
||||
switch (data.event) {
|
||||
case 'agent_thought':
|
||||
// 忽略思考过程
|
||||
break
|
||||
|
||||
case 'agent_message':
|
||||
if (data.answer) {
|
||||
responseText += data.answer
|
||||
// 更新临时助手消息的内容
|
||||
chatHistory.value[tempMessageIndex].content = responseText
|
||||
await scrollToBottom()
|
||||
}
|
||||
break
|
||||
|
||||
case 'message_file':
|
||||
if (data.type === 'image') {
|
||||
// 找到最后一条助手消息来添加文件
|
||||
const lastIndex = chatHistory.value.length - 1
|
||||
if (chatHistory.value[lastIndex].role === 'assistant') {
|
||||
chatHistory.value[lastIndex].files.push({
|
||||
type: 'image',
|
||||
url: data.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'message_end':
|
||||
break
|
||||
|
||||
case 'tts_message':
|
||||
// 处理语音合成消息
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析响应数据出错:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用API出错:', error)
|
||||
// 更新最后一条消息显示错误
|
||||
const lastIndex = chatHistory.value.length - 1
|
||||
chatHistory.value[lastIndex].content = '抱歉,请求出错了。请稍后再试。'
|
||||
// 更新错误消息
|
||||
chatHistory.value[tempMessageIndex].content = '抱歉,请求出错了。请稍后再试。'
|
||||
await scrollToBottom()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@ -176,46 +323,106 @@ const sendMessage = async () => {
|
||||
</div>
|
||||
|
||||
<!-- 已登录且是VIP用户显示聊天界面 -->
|
||||
<div v-else class="chat-container">
|
||||
<div class="chat-header">
|
||||
<h2>Web3 AI Agent</h2>
|
||||
</div>
|
||||
|
||||
<div ref="messagesContainer" class="chat-messages">
|
||||
<div
|
||||
v-for="(message, index) in chatHistory"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="{
|
||||
'user-message': message.role === 'user',
|
||||
'ai-message': message.role === 'assistant',
|
||||
}"
|
||||
>
|
||||
<div class="message-content">
|
||||
<p v-for="(line, i) in message.content.split('\n')" :key="i">
|
||||
{{
|
||||
line ||
|
||||
(message.role === 'assistant' && isLoading && index === chatHistory.length - 1
|
||||
? '...'
|
||||
: line)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="main-container">
|
||||
<!-- 左侧Agent选择列表 -->
|
||||
<div class="agent-sidebar">
|
||||
<div class="agent-list">
|
||||
<div v-if="isLoadingAgents" class="agent-loading">加载中...</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
class="agent-item"
|
||||
:class="{ 'agent-item-active': selectedAgent?.id === agent.id }"
|
||||
@click="selectedAgent = agent"
|
||||
>
|
||||
<div class="agent-icon">{{ agent.icon }}</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">{{ agent.name }}</div>
|
||||
<div class="agent-description">{{ agent.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<input
|
||||
type="text"
|
||||
v-model="userInput"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="输入您的问题..."
|
||||
class="input-field"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
<button class="send-button" @click="sendMessage" :disabled="isLoading">
|
||||
{{ isLoading ? '加载中...' : '发送' }}
|
||||
</button>
|
||||
<!-- 右侧聊天界面 -->
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<div class="current-agent" v-if="selectedAgent">
|
||||
<span class="agent-icon">{{ selectedAgent.icon }}</span>
|
||||
<span class="agent-name">{{ selectedAgent.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="messagesContainer" class="chat-messages">
|
||||
<div
|
||||
v-for="(message, index) in chatHistory"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="{
|
||||
'user-message': message.role === 'user',
|
||||
'ai-message': message.role === 'assistant',
|
||||
'thought-message': message.role === 'thought',
|
||||
}"
|
||||
>
|
||||
<div class="message-content">
|
||||
<template v-if="message.role === 'thought'">
|
||||
<div class="thought-content">
|
||||
<div v-if="message.thought?.thought" class="thought-item">
|
||||
<span class="thought-label">思考:</span>
|
||||
<span class="thought-text">{{ message.thought.thought }}</span>
|
||||
</div>
|
||||
<div v-if="message.thought?.observation" class="thought-item">
|
||||
<span class="thought-label">观察:</span>
|
||||
<span class="thought-text">{{ message.thought.observation }}</span>
|
||||
</div>
|
||||
<div v-if="message.thought?.tool" class="thought-item">
|
||||
<span class="thought-label">工具:</span>
|
||||
<span class="thought-text">{{ message.thought.tool }}</span>
|
||||
</div>
|
||||
<div v-if="message.thought?.tool_input" class="thought-item">
|
||||
<span class="thought-label">输入:</span>
|
||||
<span class="thought-text">{{ message.thought.tool_input }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-for="(line, i) in message.content.split('\n')" :key="i">
|
||||
{{
|
||||
line ||
|
||||
(message.role === 'assistant' && isLoading && index === chatHistory.length - 1
|
||||
? '...'
|
||||
: line)
|
||||
}}
|
||||
</p>
|
||||
<!-- 显示图片文件 -->
|
||||
<div v-if="message.files && message.files.length > 0" class="message-files">
|
||||
<template
|
||||
v-for="(file, fileIndex) in message.files.filter((f) => f.type === 'image')"
|
||||
:key="fileIndex"
|
||||
>
|
||||
<img :src="file.url" :alt="'AI生成的图片'" class="message-image" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<input
|
||||
type="text"
|
||||
v-model="userInput"
|
||||
@keyup.enter="sendMessage"
|
||||
:placeholder="`向${selectedAgent?.name}提问...`"
|
||||
class="input-field"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
<button class="send-button" @click="sendMessage" :disabled="isLoading">
|
||||
{{ isLoading ? '加载中...' : '发送' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -412,26 +619,104 @@ const sendMessage = async () => {
|
||||
}
|
||||
|
||||
/* 聊天界面样式 */
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: calc(80vh - 150px);
|
||||
min-height: 500px;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
height: calc(80vh - 150px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.agent-sidebar {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-elevated);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.agent-item:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.agent-item-active {
|
||||
border-color: var(--color-accent);
|
||||
background-color: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.agent-description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-header h2 {
|
||||
.current-agent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.current-agent .agent-icon {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-agent .agent-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
@ -441,7 +726,6 @@ const sendMessage = async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 - 深色系 */
|
||||
@ -534,28 +818,78 @@ const sendMessage = async () => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.message-files {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
max-width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.thought-message {
|
||||
align-self: flex-start;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.thought-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.thought-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.thought-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
.thought-text {
|
||||
color: var(--color-text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.agent-loading {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
height: 70vh;
|
||||
.agent-sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.vip-prompt {
|
||||
height: 70vh;
|
||||
.agent-list {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.agent-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-prompt-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vip-level-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.agent-item {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user