269 lines
6.8 KiB
Vue
269 lines
6.8 KiB
Vue
<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>
|