736 lines
20 KiB
Vue
736 lines
20 KiB
Vue
<template>
|
||
<view class="chat-container">
|
||
<!-- 聊天消息区域 -->
|
||
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
|
||
@scroll="onScroll">
|
||
<view class="" style="padding: 10px 0;">
|
||
|
||
<view v-for="(msg, index) in messages" :key="index"
|
||
:class="['message', msg.sender === 'me' ? 'sent' : 'received']">
|
||
<image v-if="msg.sender !== 'me'" :src="botAvatar" class="avatar"></image>
|
||
|
||
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '']">
|
||
<!-- <text class="message-type-tag" v-if="msg.type !== 'normal'">
|
||
{{ msg.type === 'thinking' ? '思考中' : msg.type === 'answer' ? '回答' : '' }}
|
||
</text> -->
|
||
<view class="message-text" style="color: #999999;">{{ msg.thinkContent }}</view>
|
||
<view class="message-text" v-if="msg.answerType===2">{{ msg.showAnswer}}</view>
|
||
<view class="message-text" v-if="msg.answerType===1">
|
||
<view>
|
||
【穿搭建议】:
|
||
<view>
|
||
🧥上身:
|
||
<view>
|
||
{{`${msg.showAnswer.outfit.top.color+msg.showAnswer.outfit.top.style}👔,${msg.showAnswer.outfit.top.detail_description},${msg.showAnswer.outfit.top.reason}`}}
|
||
</view>
|
||
</view>
|
||
<view style="margin-top: 10px;">
|
||
🩳下身:
|
||
<view>
|
||
{{`${msg.showAnswer.outfit.bottom.color+msg.showAnswer.outfit.bottom.style},${msg.showAnswer.outfit.bottom.detail_description},${msg.showAnswer.outfit.bottom.reason}😎`}}
|
||
</view>
|
||
</view>
|
||
<view style="margin-top: 10px;">
|
||
🥾鞋子:
|
||
<view>
|
||
{{`${msg.showAnswer.outfit.shoes.color+msg.showAnswer.outfit.shoes.style},${msg.showAnswer.outfit.shoes.detail_description},${msg.showAnswer.outfit.shoes.reason}🏃🏻`}}
|
||
</view>
|
||
</view>
|
||
<view style="margin-top: 10px;">👒配饰:
|
||
<view v-for="(item,index) of msg.showAnswer.outfit.accessories">
|
||
<view>
|
||
{{`${index+1}.`+item.color+item.type+','+item.detail_description+','+item.reason}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view style="margin-top: 10px;">
|
||
💅化妆建议:
|
||
<view>
|
||
<view class="">
|
||
{{msg.showAnswer.makeup_suggestion.style+','+msg.showAnswer.makeup_suggestion.description}}
|
||
</view>
|
||
<view style="margin-top: 10px;">
|
||
🧩产品推荐:
|
||
<view v-for="(item,index) of msg.showAnswer.makeup_suggestion.products">
|
||
<view class="">
|
||
{{`${index+1}.`+item.name+','+item.detail_description+','+item.reason}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view style="margin-top: 10px;">
|
||
📝风格推荐:
|
||
<view class="">{{msg.showAnswer.styling_tips.color_coordination}}</view>
|
||
<view class="">{{msg.showAnswer.styling_tips.occasion_adaptation}}</view>
|
||
<view v-for="(item,index) of msg.showAnswer.styling_tips.detail_enhancements">
|
||
<view class="">{{`${index+1}.${item}`}}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="typing-indicator" v-if="msg.isTyping">
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
<text class="typing-text">对方正在输入...</text>
|
||
</view>
|
||
</view>
|
||
|
||
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
|
||
</view>
|
||
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 输入区域 -->
|
||
<view class="chat-bottom">
|
||
<view class="" style="padding: 10px;">
|
||
<view class="aiTag" v-if="aiName==='生活咨询'" @click="sendMessageCeshi(0)">
|
||
最近雨季来了,家里总是很潮湿,有什么简单有效的除湿防霉小妙招吗?
|
||
</view>
|
||
<view class="aiTag" v-if="aiName==='穿搭顾问'" @click="sendMessageCeshi(1)">
|
||
我下周要参加一个商务休闲风格的客户午餐会(夏天,户外庭院),男,身高175cm,体型偏瘦,肤色偏白,有什么搭配建议吗?
|
||
</view>
|
||
<view class="aiTag" v-if="aiName==='膳食管家'" @click="sendMessageCeshi(2)">
|
||
我想开始减脂,能为我设计一份适合上班族、操作简单、营养均衡的晚餐食谱吗?
|
||
</view>
|
||
</view>
|
||
<view class="chat-input">
|
||
<view class="" style="flex: 1;">
|
||
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
|
||
confirm-type="send" />
|
||
</view>
|
||
<view class="" style="width: 100px;">
|
||
<u-button type="primary" :disabled="!inputMsg.trim()||isAnswering" @click="sendMessage"
|
||
shape="circle">
|
||
发送
|
||
</u-button>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
WS_BASE_URL,
|
||
IMAGE_BASE_URL
|
||
} from '@/utils/config';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
inputMsg: '',
|
||
scrollTop: 0,
|
||
userAvatar: require('../../static/imgs/index/nav.png'),
|
||
botAvatar: require('../../static/imgs/ai/chuandaguwen.png'),
|
||
messages: [],
|
||
socketTask: null, // 微信小程序的 WebSocket 任务对象
|
||
isConnected: false,
|
||
reconnectAttempts: 0,
|
||
maxReconnectAttempts: 5,
|
||
reconnectDelay: 3000,
|
||
userId: null,
|
||
userName: '默认用户',
|
||
apiKey: '',
|
||
serviceUrl: '',
|
||
currentAnswer: '',
|
||
isAnswering: false,
|
||
isHandleClose: false,
|
||
connectNum: 0,
|
||
aiName: '',
|
||
aiDefaultList: [
|
||
'最近雨季来了,家里总是很潮湿,有什么简单有效的除湿防霉小妙招吗?',
|
||
'我下周要参加一个商务休闲风格的客户午餐会(夏天,户外庭院),男,身高175cm,体型偏瘦,肤色偏白,有什么搭配建议吗?',
|
||
'我想开始减脂,能为我设计一份适合上班族、操作简单、营养均衡的晚餐食谱吗?'
|
||
]
|
||
}
|
||
},
|
||
|
||
onLoad(options) {
|
||
this.serviceUrl = options.serviceUrl || '';
|
||
this.apiKey = options.apiKey || '';
|
||
this.botAvatar = options.icon || this.botAvatar;
|
||
this.aiName = options.name
|
||
const userInfo = wx.getStorageSync('userInfo') || {};
|
||
this.userId = userInfo.id || Date.now().toString();
|
||
this.userName = userInfo.name || '默认用户';
|
||
this.userAvatar = userInfo.avatar ? `${IMAGE_BASE_URL+userInfo.avatar}` : this.userAvatar;
|
||
|
||
uni.setNavigationBarTitle({
|
||
title: options.name || 'AI助手'
|
||
});
|
||
|
||
this.initWebSocket();
|
||
},
|
||
|
||
onUnload() {
|
||
this.closeWebSocket();
|
||
},
|
||
|
||
methods: {
|
||
// 初始化WebSocket连接
|
||
initWebSocket() {
|
||
if (this.isConnected) return;
|
||
console.log(WS_BASE_URL,'aiName===');
|
||
// 微信小程序中使用 wx.connectSocket
|
||
this.socketTask = wx.connectSocket({
|
||
url: `${WS_BASE_URL}/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
|
||
success: () => {
|
||
console.log('WebSocket连接创建成功');
|
||
},
|
||
fail: (err) => {
|
||
console.error('WebSocket连接创建失败', err);
|
||
this.handleReconnect();
|
||
}
|
||
});
|
||
|
||
// 监听 WebSocket 事件
|
||
this.socketTask.onOpen(() => {
|
||
console.log('WebSocket连接已打开');
|
||
this.isConnected = true;
|
||
// this.reconnectAttempts = 0;
|
||
if (this.reconnectAttempts === 0) {
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: '已连接,请输入您的问题',
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
}
|
||
|
||
});
|
||
|
||
this.socketTask.onMessage((res) => {
|
||
// console.log('收到WebSocket消息:', res.data);
|
||
this.processBotMessage(res.data);
|
||
});
|
||
|
||
this.socketTask.onError((err) => {
|
||
console.error('WebSocket发生错误:', err);
|
||
this.isConnected = false;
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: '连接出错,请稍后再试',
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
this.handleReconnect();
|
||
});
|
||
|
||
this.socketTask.onClose(() => {
|
||
console.log('WebSocket连接已关闭');
|
||
this.isConnected = false;
|
||
if (!this.isHandleClose) {
|
||
this.handleReconnect();
|
||
}
|
||
});
|
||
},
|
||
|
||
processBotMessage(data) {
|
||
try {
|
||
// 解析数据
|
||
const messageData = typeof data === 'string' ? JSON.parse(data) : data;
|
||
|
||
if (messageData.event === 'text_chunk') {
|
||
// 处理分块文本
|
||
this.handleTextChunk(messageData);
|
||
} else if (messageData.event === 'workflow_finished') {
|
||
this.currentAnswer = ''
|
||
// 处理完整节点完成
|
||
this.handleNodeFinished(messageData);
|
||
}
|
||
} catch (error) {
|
||
console.error('处理机器人消息出错:', error);
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: '处理消息时出错: ' + error.message,
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
}
|
||
},
|
||
|
||
// 处理文本分块
|
||
handleTextChunk(chunkData) {
|
||
const text = chunkData.data.text;
|
||
if (!text || text.trim() === '') return;
|
||
let isThinking = true
|
||
if (text.includes('<think>')) {
|
||
this.messages[this.messages.length - 1].type = 'thinking'
|
||
}
|
||
if (text.includes('</think>')) {
|
||
this.messages[this.messages.length - 1].type = 'answer'
|
||
}
|
||
|
||
// 判断是否是思考内容
|
||
// const isThinking = text.includes('<think>') ||(this.messages[this.messages.length - 1]?.type === 'thinking');
|
||
console.log(text, this.messages[this.messages.length - 1].type);
|
||
if (this.messages[this.messages.length - 1].type == 'thinking') {
|
||
this.handleThinkingContent(text);
|
||
}
|
||
if (this.messages[this.messages.length - 1].type == 'answer') {
|
||
this.handleAnswerContent(text);
|
||
}
|
||
|
||
|
||
},
|
||
|
||
// 处理思考内容
|
||
handleThinkingContent(text) {
|
||
// 提取思考内容,去除标签
|
||
const thinkContent = text.replace(/<\/?think>/g, '').trim();
|
||
if (!thinkContent) return;
|
||
|
||
// 查找最后一条思考消息
|
||
// const lastThinkMsgIndex = this.messages.findLastIndex(
|
||
// msg => msg.type === 'thinking'
|
||
// );
|
||
|
||
// if (lastThinkMsgIndex >= 0) {
|
||
// // 追加到现有思考消息
|
||
// this.messages[lastThinkMsgIndex].thinkContent += thinkContent;
|
||
// this.$forceUpdate();
|
||
// } else {
|
||
// // 创建新的思考消息
|
||
// this.addMessage('bot', thinkContent, 'thinking');
|
||
// }
|
||
this.messages[this.messages.length - 1].thinkContent += thinkContent
|
||
this.$forceUpdate();
|
||
this.scrollToBottom();
|
||
},
|
||
|
||
// 处理回答内容
|
||
handleAnswerContent(text) {
|
||
// 提取回答内容,去除标签
|
||
const answerContent = text.replace(/<\/?think>/g, '');
|
||
if (!answerContent) return;
|
||
|
||
// 查找最后一条回答消息
|
||
// const lastAnswerMsgIndex = this.messages.findLastIndex(
|
||
// msg => msg.type === 'answer' || msg.type === 'normal'
|
||
// );
|
||
|
||
// if (lastAnswerMsgIndex >= 0 && this.messages[lastAnswerMsgIndex].type === 'answer') {
|
||
// // 追加到现有回答消息
|
||
// this.messages[lastAnswerMsgIndex].answerContent += answerContent;
|
||
// this.$forceUpdate();
|
||
|
||
// } else {
|
||
// // 创建新的回答消息
|
||
// this.addMessage('bot', answerContent, 'answer');
|
||
// }
|
||
this.messages[this.messages.length - 1].answerContent += answerContent
|
||
// this.$forceUpdate();
|
||
this.scrollToBottom();
|
||
},
|
||
|
||
// 处理节点完成
|
||
handleNodeFinished(nodeData) {
|
||
// 标记最后一条消息为完成
|
||
if (this.messages.length > 0) {
|
||
const lastMsg = this.messages[this.messages.length - 1];
|
||
lastMsg.isComplete = true;
|
||
lastMsg.isTyping = false
|
||
// 如果最后一条是思考消息,我们不需要额外处理
|
||
if (lastMsg.type === 'thinking') {
|
||
return;
|
||
}
|
||
if (lastMsg.answerContent.includes('outfit')) {
|
||
const ls = lastMsg.answerContent.replace(/\n/g, '')
|
||
try {
|
||
console.log(lastMsg.answerContent)
|
||
const obj = JSON.parse(lastMsg.answerContent)
|
||
console.log(obj)
|
||
lastMsg.answerContent = obj
|
||
} catch (error) {
|
||
lastMsg.answerContent = '哎呀,AI开小差了,请重新提问'
|
||
}
|
||
|
||
lastMsg.answerType = 1 //1代表返回了正式的穿搭建议,2代表非正式的问答
|
||
}
|
||
lastMsg.showAnswer = lastMsg.answerContent
|
||
this.isAnswering = false
|
||
// console.log(JSON.parse(lastMsg.answerContent));
|
||
}
|
||
|
||
// 只有当没有找到回答内容时才添加完整输出
|
||
// if (nodeData.data.outputs?.text) {
|
||
// const fullText = nodeData.data.outputs.text;
|
||
// const answerContent = fullText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||
|
||
// // 检查是否已经有相同内容的消息
|
||
// const hasSameContent = this.messages.some(
|
||
// msg => msg.content.includes(answerContent)
|
||
// );
|
||
|
||
// if (answerContent && !hasSameContent) {
|
||
// this.addMessage('bot', answerContent, 'answer');
|
||
// }
|
||
// }
|
||
|
||
this.scrollToBottom();
|
||
},
|
||
|
||
|
||
// 修改后的 addMessage 方法
|
||
addMessage(params) {
|
||
// const message = {
|
||
// sender: sender === 'bot' ? 'other' : 'me',
|
||
// thinkContent,
|
||
// answerContent,
|
||
// showAnswer,
|
||
// type, // 'thinking', 'answer', 或 'normal'
|
||
// isTyping,
|
||
// isComplete: true, // 默认完整,流式消息会设为false
|
||
// timestamp: Date.now(),
|
||
// answerType,
|
||
// };
|
||
const message = {
|
||
...params,
|
||
sender: params.sender === 'bot' ? 'other' : 'me',
|
||
isComplete: true, // 默认完整,流式消息会设为false
|
||
timestamp: Date.now(),
|
||
}
|
||
// 如果是流式消息,标记为未完成
|
||
if (message.type === 'thinking' || message.type === 'answer') {
|
||
message.isComplete = false;
|
||
}
|
||
|
||
this.messages.push(message);
|
||
this.scrollToBottom();
|
||
},
|
||
// 发送消息
|
||
sendMessage() {
|
||
const content = this.inputMsg.trim();
|
||
if (!content || !this.isConnected) return;
|
||
const params = {
|
||
sender: 'me',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: content,
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
this.isAnswering = true
|
||
// 微信小程序中使用 socketTask.send
|
||
this.socketTask.send({
|
||
data: content,
|
||
success: () => {
|
||
console.log('消息发送成功');
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '思考中...',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: true,
|
||
showAnswer: '',
|
||
answerType: 2
|
||
}
|
||
// 添加机器人正在输入的提示
|
||
this.addMessage(params);
|
||
},
|
||
fail: (err) => {
|
||
console.error('消息发送失败', err);
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: true,
|
||
showAnswer: '消息发送失败,请重试',
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
}
|
||
});
|
||
|
||
this.inputMsg = '';
|
||
},
|
||
sendMessageCeshi(index) {
|
||
const content = this.aiDefaultList[index];
|
||
if (!content || !this.isConnected) return;
|
||
const params = {
|
||
sender: 'me',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: content,
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
this.isAnswering = true
|
||
// 微信小程序中使用 socketTask.send
|
||
this.socketTask.send({
|
||
data: content,
|
||
success: () => {
|
||
console.log('消息发送成功');
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '思考中...',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: true,
|
||
showAnswer: '',
|
||
answerType: 2
|
||
}
|
||
// 添加机器人正在输入的提示
|
||
this.addMessage(params);
|
||
},
|
||
fail: (err) => {
|
||
console.error('消息发送失败', err);
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: true,
|
||
showAnswer: '消息发送失败,请重试',
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
}
|
||
});
|
||
|
||
this.inputMsg = '';
|
||
},
|
||
// 处理重连逻辑
|
||
handleReconnect() {
|
||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||
console.log('已达到最大重连次数');
|
||
const params = {
|
||
sender: 'bot',
|
||
thinkContent: '',
|
||
answerContent: '',
|
||
type: 'normal',
|
||
isTyping: false,
|
||
showAnswer: '连接已断开,请刷新页面重试',
|
||
answerType: 2
|
||
}
|
||
this.addMessage(params);
|
||
return;
|
||
}
|
||
|
||
this.reconnectAttempts++;
|
||
console.log(`尝试重新连接,第${this.reconnectAttempts}次`);
|
||
|
||
setTimeout(() => {
|
||
this.initWebSocket();
|
||
}, this.reconnectDelay);
|
||
},
|
||
|
||
// 关闭WebSocket连接
|
||
closeWebSocket() {
|
||
if (this.socketTask) {
|
||
// 移除所有回调引用
|
||
this.socketTask.onClose = null;
|
||
this.socketTask.onError = null;
|
||
this.socketTask.onMessage = null;
|
||
this.socketTask.close();
|
||
this.socketTask = null;
|
||
this.isHandleClose = true
|
||
}
|
||
this.isConnected = false;
|
||
},
|
||
|
||
// 滚动到底部
|
||
scrollToBottom() {
|
||
this.$nextTick(() => {
|
||
this.scrollTop = 99999; // 足够大的值确保滚动到底部
|
||
});
|
||
},
|
||
|
||
onScroll(e) {
|
||
// 可以在这里实现加载历史消息
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.chat-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
/* padding: 15px; */
|
||
background-color: #e5ddd5;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.message {
|
||
display: flex;
|
||
margin: 10px;
|
||
}
|
||
|
||
.sent {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.received {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.aiTag {
|
||
padding: 5px;
|
||
font-size: 14px;
|
||
background: #f0f0f0;
|
||
border-radius: 10px;
|
||
color: #666;
|
||
}
|
||
|
||
.avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.message-bubble {
|
||
max-width: 70%;
|
||
padding: 10px 15px;
|
||
border-radius: 18px;
|
||
background-color: white;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.me {
|
||
background-color: #dcf8c6;
|
||
}
|
||
|
||
.message-text {
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
color: #333333;
|
||
}
|
||
|
||
.chat-bottom {
|
||
background-color: white;
|
||
border-top: 1px solid #ddd;
|
||
}
|
||
|
||
.chat-input {
|
||
background-color: white;
|
||
border-top: 1px solid #ddd;
|
||
display: flex;
|
||
padding: 10px;
|
||
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
padding: 8px 15px;
|
||
border-radius: 20px;
|
||
background-color: #f0f0f0;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.send-btn {
|
||
padding: 0 20px;
|
||
border-radius: 20px;
|
||
background-color: #07C160;
|
||
color: white;
|
||
}
|
||
|
||
.send-btn:disabled {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* 对方正在输入提示 */
|
||
.typing-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background-color: #7f8c8d;
|
||
margin: 0 3px;
|
||
animation: typing 1.4s infinite ease-in-out;
|
||
}
|
||
|
||
.dot:nth-child(1) {
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.dot:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.dot:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
.typing-text {
|
||
font-size: 12px;
|
||
margin-left: 8px;
|
||
color: #555;
|
||
}
|
||
|
||
@keyframes typing {
|
||
|
||
0%,
|
||
60%,
|
||
100% {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
30% {
|
||
transform: translateY(-5px);
|
||
}
|
||
}
|
||
|
||
/* 思考消息样式 */
|
||
.message-bubble.thinking {
|
||
background-color: #f0f0f0;
|
||
border-left: 4px solid #999;
|
||
font-style: italic;
|
||
color: #666;
|
||
}
|
||
|
||
/* 回答消息样式 */
|
||
.message-bubble.answer {
|
||
background-color: #f8f8f8;
|
||
border-left: 4px solid #07C160;
|
||
}
|
||
|
||
/* 错误消息样式 */
|
||
.message-bubble.error {
|
||
background-color: #ffeeee;
|
||
border-left: 4px solid #ff4d4f;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
/* 消息类型标签 */
|
||
.message-type-tag {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 4px;
|
||
font-weight: bold;
|
||
}
|
||
</style> |