当前位置: 首页 > news >正文

做网站有2个前提条件_一个是网站化学试剂网站建设

做网站有2个前提条件_一个是网站,化学试剂网站建设,网站建设实施规范,淘宝详情页做的比较好的网站AI 对话实现打字机效果 需求需求: 要做一个AI对话聊天的页面 就和正常的chatGPT、Deepseek一样,AI回复的问题需要有打字机效果,历史聊天记录不需要打字机效果仅限于最后一条回答实现打字机效果。效果图 后面补上 实现方式 通过setInterval将数…

AI 对话实现打字机效果

需求

需求: 要做一个AI对话聊天的页面 就和正常的chatGPT、Deepseek一样,AI回复的问题需要有打字机效果,历史聊天记录不需要打字机效果仅限于最后一条回答实现打字机效果。

效果图

后面补上

实现方式

  1. 通过setInterval将数据一个字一个字的打印出来(仅限于AI回复的最后一次回答)

代码

<template>
<div :style="chatContainerHeight" class="chat-box"><div v-for="(message, index) in messages" :key="index" class="message" :class="{'user-message': message.isUser, 'ai-message': !message.isUser}"><div v-if="!message.isUser" class="avatar"><img :src="message.isUser ? userAvatar : aiAvatar" alt="avatar" /></div><div class="message-content"><p v-if="message.deepThinking" style="color: #1ae46a;margin-bottom: -15px">深度思考</p><p>{{ message.text }}</p><p :style="customStyle(message)">{{ message.createdAt }}</p></div><div v-if="message.isUser" class="avatar"><img :src="message.isUser ? userAvatar : aiAvatar" alt="avatar" /></div></div></div><div class="chat-footer"><div class="input-area"><el-input ref="inputRef" v-model="newMessage" type="text" placeholder="请输入您想问的问题" class="inputDeep" maxlength="3000" show-word-limit @keydown.enter="sendMessage" /><el-button :disabled="newMessage==='' || isDisabled" :type="newMessage===''? 'info' : 'primary'" @click="sendMessage">发送</el-button><div v-if="!messages.length" class="footer-tips"><span>限制体验次数为</span><span style="color: #ff6a00">{{count}}</span></div><div v-else class="footer-tips"><span>体验模型将会消耗Tokens,费用以实际发生为准</span><span style="color: #ff6a00">{{count}}</span></div></div></div><template>
<script setup>const messages = ref([]);	
// 聊天记录高度
const chatContainerHeight = computed(()=>{if(!messages.value.length){return {height: `${window.innerHeight - 390 }px`,}}else{return {minHeight: "400px",maxHeight: `${window.innerHeight - 390 }px`,}}
})// 动态样式
const customStyle = (message) =>{if(message.isUser){return {padding: "0 10px",color: "#ccc",textAlign: "right"}}else{return {padding: "0 10px",color: "#ccc",textAlign: "left"}}
}
function typeEffect(text, callback, doneCallback) {let currentIndex = 0;let resultText = '';// 每隔 100 毫秒,更新一次字符const interval = setInterval(() => {resultText += text[currentIndex];callback(resultText); // 回调函数,更新显示的文本currentIndex++;// 当所有字符都显示完毕时,清除定时器if (currentIndex === text.length) {clearInterval(interval);if(doneCallback) doneCallback(); // 调用传入的doneCallback}}, 10); // 每 100 毫秒更新一个字符
}
const sendMessage = () => {if (newMessage.value?.trim() === '') return;if (isDisabled.value) return;isDisabled.value = true;params.messages[0].content = newMessage.value;const userMessage = {avatar: 'https://tse1-mm.cn.bing.net/th/id/OIP-C.Knh5i_ceDHm_cwzEcKFJ2gAAAA?w=208&h=208&c=7&r=0&o=7&pid=1.7&rm=3', // 你可以根据需要修改头像text: newMessage.value,isUser: true,createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}messages.value.push(userMessage);// 保存到本地存储saveMessagesToStorage(messages.value);// 接口是sse形式请求头Accept必须是text/event-streamsseChat(params).then(res => {const jsonResponse = res.replace(/^data:/, ''); // 去掉 'data:' 前缀const result = JSON.parse(jsonResponse);const tentParams = {...result.choices[0].message,isUser: false,text: "",createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),deepThinking: status.value // 深度思考}const textToDisplay = result.choices[0].message?.content;messages.value.push(tentParams);// 逐字显示typeEffect(textToDisplay, (newText) => {// 更新 messages.value 的 text 字段const lastMessage = messages.value[messages.value.length - 1];if (lastMessage && lastMessage.isUser === false) {// 使用 Vue 的响应式方法更新 text,确保视图更新lastMessage.text = newText; // 每次更新 text// 强制重新赋值数组,以便 Vue 识别变更messages.value = [...messages.value]; // 这里是通过赋新数组来强制视图更新saveMessagesToStorage(messages.value);}}, ()=> {// 后端返回的数据全部打自己效果完成之后执行这里console.log("执行完毕")isDisabled.value = false;});})// 清空输入的值newMessage.value = ''; // 发送后清空输入框inputRef.value?.focus(); // 晴空内容后自动获取焦点
};
</script>

完整版代码

在这里插入代码片
<script setup>
import { ref } from 'vue';
import recommendModel from './recommendedModel.vue';
import {sseChat, searchQuery} from "~/apis/model-market/index";const emit = defineEmits(["changeRecommendedRadio"]);
const route = useRoute();
const props = defineProps({});
const isDisabled = ref(false);
// 是否深度思考
const status = defineModel("status");
// 外层的下拉模型
const modelType = defineModel("modelType");
const inputRef = ref(null);
const count = ref(10);
const recommendedList = ref([]);
const params = reactive({id: null,model: "public/LLM-Research/Meta-Llama-3-8B-Instruct",reasoning_effort: "",messages: [{role: "user",content: "",}]
});watch(()=>status.value, newVal=>{params.reasoning_effort = newVal ? "low": ""console.log(route.query, "route");
})// 推荐的模型 具体看选择哪个 默认为空对象
const modelVal = ref({});
const userAvatar = "https://tse1-mm.cn.bing.net/th/id/OIP-C.Knh5i_ceDHm_cwzEcKFJ2gAAAA?w=208&h=208&c=7&r=0&o=7&pid=1.7&rm=3"
const aiAvatar = "https://tse1-mm.cn.bing.net/th/id/OIP-C.bWLvtF_jhkcQdIyd8fH2JQAAAA?w=208&h=208&c=7&r=0&o=7&pid=1.7&rm=3"
const innerHeight = computed(()=>{return {height: `${window.innerHeight - 235 }px`}
})
// 聊天记录高度
const chatContainerHeight = computed(()=>{if(!messages.value.length){return {height: `${window.innerHeight - 390 }px`,}}else{return {minHeight: "400px",maxHeight: `${window.innerHeight - 390 }px`,}}
})
// 加载本地存储的聊天记录
const loadMessagesFromStorage = () => {const storedMessages = localStorage.getItem('chatMessages');if (storedMessages) {return JSON.parse(storedMessages);}return [];
};
const messages = ref(loadMessagesFromStorage());const newMessage = ref('');function typeEffect(text, callback, doneCallback) {let currentIndex = 0;let resultText = '';// 每隔 100 毫秒,更新一次字符const interval = setInterval(() => {resultText += text[currentIndex];callback(resultText); // 回调函数,更新显示的文本currentIndex++;// 当所有字符都显示完毕时,清除定时器if (currentIndex === text.length) {clearInterval(interval);if(doneCallback) doneCallback(); // 调用传入的doneCallback}}, 10); // 每 100 毫秒更新一个字符
}
onMounted(async ()=>{const params = {pageNum: 1,pageSize: 1000000000,name: "",typeIds: [],providerId: [],contextLength: [],};const res = await searchQuery(params)const result = res.data.list.filter(item=>item?.isRecommend);recommendedList.value = result.length > 3 ? result.slice(0,3) : result.slice(0, result.length - 1);console.log(route.fullPath, "route")
})
const sendMessage = () => {if (newMessage.value?.trim() === '') return;if (isDisabled.value) return;isDisabled.value = true;params.messages[0].content = newMessage.value;const userMessage = {avatar: 'https://tse1-mm.cn.bing.net/th/id/OIP-C.Knh5i_ceDHm_cwzEcKFJ2gAAAA?w=208&h=208&c=7&r=0&o=7&pid=1.7&rm=3', // 你可以根据需要修改头像text: newMessage.value,isUser: true,createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}messages.value.push(userMessage);// 保存到本地存储saveMessagesToStorage(messages.value);sseChat(params).then(res => {const jsonResponse = res.replace(/^data:/, ''); // 去掉 'data:' 前缀const result = JSON.parse(jsonResponse);const tentParams = {...result.choices[0].message,isUser: false,text: "",createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),deepThinking: status.value // 深度思考}const textToDisplay = result.choices[0].message?.content;messages.value.push(tentParams);// 逐字显示typeEffect(textToDisplay, (newText) => {// 更新 messages.value 的 text 字段const lastMessage = messages.value[messages.value.length - 1];if (lastMessage && lastMessage.isUser === false) {// 使用 Vue 的响应式方法更新 text,确保视图更新lastMessage.text = newText; // 每次更新 text// 强制重新赋值数组,以便 Vue 识别变更messages.value = [...messages.value]; // 这里是通过赋新数组来强制视图更新saveMessagesToStorage(messages.value);}}, ()=> {isDisabled.value = false;});})// 清空输入的值newMessage.value = ''; // 发送后清空输入框inputRef.value?.focus(); // 晴空内容后自动获取焦点
};
// 保存聊天记录到本地存储
const saveMessagesToStorage = (messages) => {localStorage.setItem(`${params.model}${params.id}`, JSON.stringify(messages));
};// 清空聊天记录
const clearChatHistory = () => {messages.value = [];modelVal.value = {};isDisabled.value = false;saveMessagesToStorage(messages.value);
};
const handleChangeRadio = val => {modelVal.value = val;params.model = val.nameparams.id = val.idemit("changeRecommendedRadio", val);
}
// 动态样式
const customStyle = (message) =>{if(message.isUser){return {padding: "0 10px",color: "#ccc",textAlign: "right"}}else{return {padding: "0 10px",color: "#ccc",textAlign: "left"}}
}
defineExpose({clearChatHistory
})</script><template><div :style="innerHeight" class="chat-container"><div :style="chatContainerHeight" class="chat-box"><div v-for="(message, index) in messages" :key="index" class="message" :class="{'user-message': message.isUser, 'ai-message': !message.isUser}"><div v-if="!message.isUser" class="avatar"><img :src="message.isUser ? userAvatar : aiAvatar" alt="avatar" /></div><div class="message-content"><p v-if="message.deepThinking" style="color: #1ae46a;margin-bottom: -15px">深度思考</p><p>{{ message.text }}</p><p :style="customStyle(message)">{{ message.createdAt }}</p></div><div v-if="message.isUser" class="avatar"><img :src="message.isUser ? userAvatar : aiAvatar" alt="avatar" /></div></div></div><div v-if="modelVal.name" class="tip-center"><div v-if="!messages.length">已选择{{modelVal?.name}}开启模型体验吧</div></div><template v-if="route.fullPath !== '/workbench/workbench/instanceDetail'"><div v-if="!messages.length" class="message-default"><!--    <div v-if="!modelType" class="message-default">--><recommendModel :list="recommendedList" @change="handleChangeRadio" style="margin-bottom: 20px"/><p v-show="!modelVal.name" style="text-align: center;font-size: 16px"><strong>请先选择模型,在开始体验</strong></p></div></template><div class="chat-footer"><div class="input-area"><el-input ref="inputRef" v-model="newMessage" type="text" placeholder="请输入您想问的问题" class="inputDeep" maxlength="3000" show-word-limit @keydown.enter="sendMessage" /><el-button :disabled="newMessage==='' || isDisabled" :type="newMessage===''? 'info' : 'primary'" @click="sendMessage">发送</el-button><div v-if="!messages.length" class="footer-tips"><span>限制体验次数为</span><span style="color: #ff6a00">{{count}}</span></div><div v-else class="footer-tips"><span>体验模型将会消耗Tokens,费用以实际发生为准</span><span style="color: #ff6a00">{{count}}</span></div></div></div></div>
</template><style lang="scss" scoped>
.tip-center{position: absolute;top: 45%;left: 50%;transform: translate(-50%,-50%);
}
.chat-container {margin: 0 auto;padding: 10px;//background-color: #f5f5f5;border-radius: 8px;position: relative;
}.chat-box {//max-height: 500px;overflow-y: auto;margin-bottom: 10px;
}.message {display: flex;align-items: flex-start;margin-bottom: 10px;width: 100%;
}.avatar img {width: 40px;height: 40px;border-radius: 50%;margin-right: 10px;
}.user-message .message-content {background-color: #f5f5f5;border-radius: 10px 10px 0 10px;margin-left: auto; /* 用户消息靠右 */width: auto; /* 内容宽度自适应 */max-width: 85%; /* 设置最大宽度 */word-wrap: break-word;
}.ai-message .message-content {background-color: #f5f5f5;border-radius: 10px 10px 10px 0;margin-right: auto; /* AI消息靠左 */width: auto; /* 内容宽度自适应 */max-width: 85%; /* 设置最大宽度 */word-wrap: break-word;
}
.message-content {//background-color: #fff;background-color: #f1f2f4;/* padding: 10px; */border-radius: 10px;max-width: 100%;word-wrap: break-word;/* min-height: 40px; *//* line-height: 25px; */p{padding: 10px;margin: 0;}
}
.chat-footer{width: 100%;position: absolute;bottom: 10px;left: 0;
}
.footer-tips{position: absolute;top: -30px;left: 0;
}
.input-area {width: 80%;padding: 20px 10px;box-sizing: border-box;display: flex;align-items: center;gap: 10px;//background-color: pink;position: relative;background-color: #fff;border: solid 1px #d7d7d7;border-radius: 8px;margin: 0 auto;
}.inputDeep {// text:deep(.el-input__wrapper) {box-shadow: 0 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;cursor: default;.el-input__inner {cursor: default !important;}}// textarea:deep(.el-textarea__inner) {box-shadow: 0 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;resize: none;cursor: default;}
}
.input-area input {width: 100%;padding: 10px;border: 1px solid #ddd;border-radius: 20px;
}.input-area button {padding: 10px 15px;//background-color: #007bff;color: white;border: none;border-radius: 20px;cursor: pointer;
}.input-area button:hover {//background-color: #0056b3;
}// 初始模型样式
.message-default{position: absolute;//bottom: 100px;bottom: 10px;left: 50%;transform: translate(-50%,-50%);
}
</style>
http://www.lakalapos1.cn/news/615/

相关文章:

  • 小学网站建设实施方案个人养老金制度相关细则
  • 域名服务商网站企业网站建设排名资讯
  • 网站建设费 账务处理网站流量分析怎么做
  • 全球建筑设计网站平板电脑可以做网站吗
  • 重庆市建设工程造价管理总网站怎么做门淘宝网站
  • 网站空间在哪里买网站建设的原则有哪些内容
  • 电子商务网站建设和推广 范文wordpress 摄影 主题
  • 广州海珠网站开发价格搜搜提交网站
  • 建设校园标准信息服务网站论文年利润三十万小作坊
  • 广州网站建设教程asp网站怎么改成中英双语
  • ueditor是做网站的吗域名防红跳转网址生成
  • 上海部道网站 建设安徽网络建站
  • 制作京东一样的网站网站建设公司面临的问题
  • h5做的网站有哪些微信公众平台怎么做微网站吗
  • 做内贸的网站淘宝网建设网站意义
  • 河南专业网站建设公司哪家好如何在建设教育协会网站注册考试
  • 进网站后台加什么个人网站用备案吗
  • 做网站分几种电商网站设计周志
  • 裕华建设集团网站小企业门户网站建设
  • 网站描述样本h5页面制作软件教程
  • 建销售网站需要多少钱网站建设首页模板
  • 百度建网站多少钱公司公众网站微信平台建设方案
  • 酒店网站设计模板教人怎么做网页的网站
  • 为什么网站只能显示ip地址_不能显示域名 wordpress4399小游戏网页在线玩
  • 有关网站建设的app个人网页设计首页
  • 保世基官方网站建设万界随机购物系统
  • ps制作个性字网站手工制作盲盒
  • 福建漳州网站建设价格wordpress postline
  • 网站优化网络品牌网站建设小蝌蚪
  • 长春科技网站建设网站建设商城模板下载