This commit is contained in:
aaron 2025-05-08 00:44:24 +08:00
parent f5f8395ef9
commit 507925efde
2 changed files with 410 additions and 76 deletions

View File

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

View File

@ -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
}
// APIAgent
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)
// APIURL
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
//
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,9 +323,36 @@ const sendMessage = async () => {
</div>
<!-- 已登录且是VIP用户显示聊天界面 -->
<div v-else class="chat-container">
<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-container">
<div class="chat-header">
<h2>Web3 AI Agent</h2>
<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">
@ -189,9 +363,31 @@ const sendMessage = async () => {
: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 ||
@ -200,6 +396,16 @@ const sendMessage = async () => {
: 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>
@ -209,7 +415,7 @@ const sendMessage = async () => {
type="text"
v-model="userInput"
@keyup.enter="sendMessage"
placeholder="输入您的问题..."
:placeholder="`向${selectedAgent?.name}提问...`"
class="input-field"
:disabled="isLoading"
/>
@ -219,6 +425,7 @@ const sendMessage = async () => {
</div>
</div>
</div>
</div>
</template>
<style scoped>
@ -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>