fischer-agentkit/src/agentkit/server/frontend/src/views/ChatView.vue

281 lines
7.4 KiB
Vue

<template>
<div class="chat-view">
<ChatSidebar
:conversations="chatStore.conversations"
:current-id="chatStore.currentConversationId"
@create="chatStore.createConversation"
@select="chatStore.selectConversation"
/>
<div class="chat-view__main">
<div v-if="!chatStore.currentConversationId" class="chat-view__empty">
<a-empty description="选择一个对话或创建新对话开始聊天">
<a-button type="primary" @click="chatStore.createConversation">
<template #icon><PlusOutlined /></template>
新建对话
</a-button>
</a-empty>
</div>
<template v-else>
<ExpertTeamView />
<BoardStatusView />
<div class="chat-view__messages" ref="messagesContainer">
<div v-if="chatStore.currentMessages.length === 0" class="chat-view__welcome">
<div class="chat-view__welcome-inner">
<div class="chat-view__welcome-logo">
<RobotOutlined class="chat-view__welcome-icon" />
</div>
<h2 class="chat-view__welcome-title">Fischer AgentKit</h2>
<p class="chat-view__welcome-subtitle">企业级 AI 智能体平台</p>
<div class="chat-view__welcome-hints">
<div class="chat-view__hint" v-for="hint in welcomeHints" :key="hint">
<ThunderboltOutlined class="chat-view__hint-icon" />
<span>{{ hint }}</span>
</div>
</div>
</div>
</div>
<ChatMessage
v-for="msg in chatStore.currentMessages"
:key="msg.id"
:message="msg"
/>
<!-- Streaming steps -->
<div v-if="chatStore.streamingSteps.length > 0" class="chat-view__steps">
<a-typography-text type="secondary">
<LoadingOutlined /> 处理中...
</a-typography-text>
<div v-for="(step, idx) in chatStore.streamingSteps" :key="idx" class="chat-view__step">
<RightOutlined class="chat-view__step-icon" />
<span>{{ step }}</span>
</div>
</div>
</div>
<ChatInput
:disabled="chatStore.isLoading"
@send="handleSend"
/>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { Empty as AEmpty, Button as AButton, Typography as ATypography } from 'ant-design-vue'
import {
PlusOutlined,
RobotOutlined,
LoadingOutlined,
RightOutlined,
ThunderboltOutlined,
} from '@ant-design/icons-vue'
import { useChatStore } from '@/stores/chat'
import ChatSidebar from '@/components/chat/ChatSidebar.vue'
import ChatMessage from '@/components/chat/ChatMessage.vue'
import ChatInput from '@/components/chat/ChatInput.vue'
import ExpertTeamView from '@/components/chat/ExpertTeamView.vue'
import BoardStatusView from '@/components/chat/BoardStatusView.vue'
const ATypographyText = ATypography.Text
const chatStore = useChatStore()
const messagesContainer = ref<HTMLElement | null>(null)
const welcomeHints = [
'智能路由 — 自动匹配最优技能',
'工具调用 — 读写文件、执行命令',
'流式响应 — 实时查看推理过程',
'私董会 — 输入 @board 召集专家团讨论',
]
onMounted(async () => {
await chatStore.loadConversations()
chatStore.connectWebSocket()
// Auto-select or create a conversation so user can start chatting immediately
if (chatStore.conversations.length > 0 && !chatStore.currentConversationId) {
chatStore.selectConversation(chatStore.conversations[0].id)
} else if (chatStore.conversations.length === 0) {
chatStore.createConversation()
}
})
onUnmounted(() => {
chatStore.disconnectWebSocket()
})
// Auto-scroll to bottom when new messages arrive
watch(
() => chatStore.currentMessages.length,
async () => {
await nextTick()
scrollToBottom()
}
)
watch(
() => chatStore.streamingSteps.length,
async () => {
await nextTick()
scrollToBottom()
}
)
function scrollToBottom(): void {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
function handleSend(message: string, model?: string): void {
if (chatStore.isWsConnected) {
chatStore.sendWsMessage(message, undefined, model)
} else {
chatStore.sendMessage(message)
}
}
</script>
<style scoped>
.chat-view {
display: flex;
height: 100%;
overflow: hidden;
}
.chat-view__main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg-secondary);
}
.chat-view__empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.chat-view__messages {
flex: 1;
overflow-y: auto;
padding: var(--space-4) 0;
}
.chat-view__welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-placeholder);
}
.chat-view__welcome-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
animation: welcomeFadeIn 0.6s ease-out;
}
@keyframes welcomeFadeIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-view__welcome-logo {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-brand);
border-radius: var(--radius-xl);
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.25);
animation: welcomeLogoIn 0.5s ease-out;
}
@keyframes welcomeLogoIn {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
.chat-view__welcome-icon {
font-size: 32px;
color: var(--text-inverse) !important;
-webkit-background-clip: unset;
-webkit-text-fill-color: var(--text-inverse) !important;
background-clip: unset;
}
.chat-view__welcome-title {
color: var(--text-primary);
font-size: var(--font-xl);
font-weight: var(--font-weight-bold);
margin: 0;
animation: welcomeFadeIn 0.6s ease-out 0.1s both;
}
.chat-view__welcome-subtitle {
font-size: var(--font-base);
color: var(--text-tertiary);
margin: 0;
animation: welcomeFadeIn 0.6s ease-out 0.2s both;
}
.chat-view__welcome-hints {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: var(--space-4);
}
.chat-view__hint {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--font-sm);
color: var(--text-tertiary);
padding: var(--space-2) var(--space-3);
background: var(--bg-primary);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
animation: welcomeFadeIn 0.5s ease-out both;
}
.chat-view__hint:nth-child(1) { animation-delay: 0.3s; }
.chat-view__hint:nth-child(2) { animation-delay: 0.4s; }
.chat-view__hint:nth-child(3) { animation-delay: 0.5s; }
.chat-view__hint:nth-child(4) { animation-delay: 0.6s; }
.chat-view__hint-icon {
font-size: 14px;
color: var(--color-primary);
}
.chat-view__steps {
padding: var(--space-3) var(--space-4);
margin: 0 var(--space-4);
background: var(--bg-primary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.chat-view__step {
display: flex;
align-items: center;
gap: var(--space-1);
padding: 2px 0;
font-size: var(--font-sm);
color: var(--text-secondary);
}
.chat-view__step-icon {
font-size: 10px;
color: var(--color-primary);
}
</style>