Files
chief-agent-ui/src/components/Agent.vue
2026-07-02 23:18:02 +08:00

269 lines
6.8 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.

<template>
<div class="agent-container">
<main class="message-container" ref="msgContainer">
<div v-if="agentStore.chatMessages.length === 0" class="empty-state">
<el-avatar :size="72" :src="aiAvatar" class="empty-avatar" />
<h2>你好我是智能家庭助手</h2>
<p>有什么可以帮你的</p>
<div class="quick-actions">
<el-button v-for="q in quickQuestions" :key="q" type="primary" round @click="askQuick(q)">{{ q }}</el-button>
</div>
</div>
<div v-for="(item, index) in agentStore.chatMessages" :key="index" class="msg-row" :class="item.role">
<el-avatar :size="36" :src="item.role === 'ai' ? aiAvatar : userAvatar" class="msg-avatar"/>
<div class="msg-body">
<div class="msg-bubble" :class="item.role">
<MdPreview v-if="item.role === 'ai'" :modelValue="item.content" class="md-preview"/>
<template v-else><span class="user-text">{{ item.content }}</span></template>
</div>
<span class="msg-time">{{ item.role === 'ai' ? 'AI' : '你' }}</span>
</div>
</div>
<!-- 加载动画 -->
<div v-if="agentStore.loading" class="msg-row ai">
<el-avatar :size="36" :src="aiAvatar" class="msg-avatar" />
<div class="msg-body">
<div class="msg-bubble ai typing">
<span class="dot" />
<span class="dot" />
<span class="dot" />
</div>
</div>
</div>
</main>
<!-- 底部输入区 -->
<footer class="input-bar">
<div class="input-wrap">
<el-input v-model="agentStore.inputMessage" placeholder="输入你的问题…"
@keyup.enter="sendMessage" clearable :disabled="agentStore.loading"/>
<el-button type="primary" circle :loading="agentStore.loading"
:disabled="!agentStore.inputMessage.trim()" @click="sendMessage">
<template #icon>
<el-icon><Promotion /></el-icon>
</template>
</el-button>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { Promotion } from "@element-plus/icons-vue";
import { MdPreview } from "md-editor-v3";
import "md-editor-v3/lib/preview.css";
import { useAgentStore } from "../stores/agent.ts";
import { nextTick, ref } from "vue";
import aiAvatar from '../assets/agent.svg'
import userAvatar from '../assets/user.svg'
const agentStore = useAgentStore();
const msgContainer = ref<HTMLElement>();
const quickQuestions = [
"帮我分析下这个月的账单",
"帮我查询下最近花费最大的一笔是什么?",
"帮我查询下今年存了多少钱?"
];
const scrollToBottom = () => {
nextTick(() => {
const el = msgContainer.value;
if (el) el.scrollTop = el.scrollHeight;
});
};
const sendMessage = async () => {
if (!agentStore.inputMessage.trim() || agentStore.loading) return;
const userMsg = agentStore.inputMessage;
agentStore.inputMessage = "";
agentStore.chatMessages.push({ role: "user", content: userMsg });
agentStore.loading = true;
scrollToBottom();
try {
const response = await fetch("/agent-api/query/bill", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMsg }),
});
agentStore.chatMessages.push({ role: "ai", content: "" });
const reader = response.body!.getReader();
const decoder = new TextDecoder("utf-8");
let aiText = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
aiText += decoder.decode(value, { stream: true });
agentStore.chatMessages[agentStore.chatMessages.length - 1].content = aiText;
scrollToBottom();
}
} finally {
agentStore.loading = false;
scrollToBottom();
}
};
const askQuick = (q: string) => {
agentStore.inputMessage = q;
sendMessage();
};
const clearChat = () => {
agentStore.chatMessages = [];
};
</script>
<style lang="scss">
.agent-container {
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
.message-container {
background: #f5f5f5;
border-radius: 12px;
flex: 1;
overflow-y: auto;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 16px;
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #999;
h2 {
margin: 0;
font-size: 20px;
color: #333;
}
p {
margin: 0;
font-size: 14px;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
justify-content: center;
}
}
.msg-row {
display: flex;
gap: 10px;
align-items: flex-start;
max-width: 85%;
.msg-body {
display: flex;
flex-direction: column;
gap: 4px;
.msg-bubble {
padding: 10px 14px;
line-height: 1.7;
word-break: break-word;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
.md-preview {
background: transparent;
padding: 0;
font-size: 14px;
}
.user-text {
white-space: pre-wrap;
}
&.ai {
background: #fff;
color: #333;
}
&.user {
background: linear-gradient(135deg, #409eff 0%, #337ecc 100%);
color: #fff;
}
}
.msg-time {
font-size: 11px;
color: #bbb;
padding-left: 4px;
}
.typing {
display: flex;
align-items: center;
gap: 5px;
padding: 14px 18px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #bbb;
animation: blink 1.2s infinite both;
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.4s; }
}
}
@keyframes blink {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
}
.msg-avatar {
flex-shrink: 0;
margin-top: 2px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
&.user {
flex-direction: row-reverse;
align-self: flex-end;
.msg-bubble {
border-radius: 18px 18px 4px 18px;
}
}
&.ai {
align-self: flex-start;
.msg-bubble {
border-radius: 18px 18px 18px 4px;
}
}
}
}
.input-bar {
background: #fff;
border-top: 1px solid #e8e8e8;
.input-wrap {
display: flex;
gap: 10px;
align-items: center;
}
}
}
</style>