281 lines
7.4 KiB
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>
|