最近在了解关于vue3的一些技术架构,也在寻找一些对接ai的实现私有化开源框架、组件库相关的内容。 然后网上冲浪时刚好看到了Meatachat。大概看了一下官方文档,感觉接入挺简单的。于是便打算尝试一下,正好基于本地的一个vue3 demo代码。中间经历一些小波折,最终还是完整运行出来了,效果和官方展示的差不多。但是不是直接拉取官方的代码跑的。
1.MateChat
MateChat 是一款基于前端开发的智能化聊天组件,它提供了丰富的 API 和可定制化的 UI,帮助开发者快速集成聊天功能到各类应用中。通过引入对应依赖,能快速通过组件化调用的方式构建出ai对话的页面。来达到广泛应用于各种需要聊天功能的场景。
官网的文档很简洁,毕竟只是构建一个Ai聊天对话。简洁易用,封装性好才是根本。因此,这种简单直接的文档对接方式,恰恰就满足了很大一部分开发者的需求。
2.Demo演示效果
基于本地的vue3-demo运行的效果,经过调试优化,最终达到和官方展示的效果一样,并且是适配移动端的。
Ai智能对话聊天:实现本地ai智能对话,支持手动更改主题色亮色和暗色,适配移动端
3.自主对接开发
按照文档的说法,对话聊天页面构建的核心依赖是matechat,通过openai依赖来实现ai对话接口请求,组件库用的devui。综上,所以对接流程如下:
2.1依赖安装
基本及相关依赖引入如下,如果后面还想涉及到主题的更改,还需要安装 npm i devui-theme;后面,想实现智能对话,这里需要引入openai,npm i openai。基于我测试用的vue3-Demo,一共安装了下面几个依赖。
npm i vue-devui @matechat/core @devui-design/icons
2.2 依赖引入
安装好对应依赖后,就是引入项目中,lz的Demo是基于vue3+elementPlus+Sass+ts+pinia的,在main.ts文件中进行引入即可。
//引入matechat组件及样式
import MateChat from '@matechat/core';
import DevUI from 'vue-devui';
import 'devui-theme/styles-var/devui-var.scss'
import 'vue-devui/style.css';
import '@devui-design/icons/icomoon/devui-icon.css';
createApp(App).use(DevUI).use(MateChat).mount('#app');
2.3 示例使用
这里想看到如官方展示的示例效果,所以也就没有一个一个组件去尝试了,直接把官方提供的一个简单的对话界面搭建示例代码拿过来运行看效果,然后再本地调试优化,最终就实现了如官方所展示的效果。官方的代码就不粘贴了,这里直接粘贴调试后的Demo代码。
Tips:这里对接的openai是阿里云的qwen模型,直接替换你自己的sk就行。当然,你也可以对接其他的模型。
index.vue
<template>
<McLayout class="container" :style="theme?'background:#292A2E':'background:#fff'">
<McHeader :title="'MateChat'" :logoImg="'https://matechat.gitcode.com/logo.svg'">
<template #operationArea>
<div class="operations">
<d-popover content="MateChat AI聊天对话在线">
<i class="icon-helping" style="font-size: 16px;"></i>
</d-popover>
<d-switch v-model="theme" @change="themeChange" color="#292A2E" size="sm">
<template #checkedContent>
<i class="icon-dark"></i>
</template>
<template #uncheckedContent>
<i class="icon-light"></i>
</template>
</d-switch>
</div>
</template>
</McHeader>
<McLayoutContent
v-if="startPage"
style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px"
>
<McIntroduction
:logoImg="'https://matechat.gitcode.com/logo2x.svg'"
:title="'MateChat'"
:subTitle="'Hi,欢迎使用 MateChat'"
></McIntroduction>
<McPrompt
:list="introPrompt.list"
:direction="introPrompt.direction"
class="intro-prompt"
@itemClick="onSubmit($event.label)"
></McPrompt>
</McLayoutContent>
<McLayoutContent class="content-container" v-else>
<div ref="conversationRef" class="content-container">
<template v-for="(msg, idx) in messages" :key="idx">
<McBubble
v-if="msg.from === 'user'"
:content="msg.content"
:align="'right'"
:avatarConfig="{ imgSrc: 'https://matechat.gitcode.com/png/demo/userAvatar.svg' }"
>
</McBubble>
<McBubble :loading="msg.loading" bordered v-else :avatarConfig="{ imgSrc: 'https://matechat.gitcode.com/logo.svg' }">
<McMarkdownCard :theme="theme?'dark':'light'" :content="msg.content"></McMarkdownCard>
</McBubble >
</template>
</div>
</McLayoutContent>
<div class="shortcut" style="display: flex; align-items: center; gap: 8px">
<McPrompt
v-if="!startPage"
:list="simplePrompt"
:direction="'horizontal'"
style="flex: 1"
@itemClick="onSubmit($event.label)"
></McPrompt>
<Button
style="margin-left: auto"
icon="add"
shape="circle"
title="新建对话"
size="sm"
@click="newConversation"
/>
</div>
<McLayoutSender>
<McInput :value="inputValue" :maxLength="2000" @change="(e) => (inputValue = e)" @submit="onSubmit">
<template #extra>
<div class="input-foot-wrapper">
<div class="input-foot-left">
<span class="input-foot-dividing-line"></span>
<span class="input-foot-maxlength">{{ inputValue.length }}/2000</span>
</div>
<div class="input-foot-right">
<Button icon="op-clearup" shape="round" :disabled="!inputValue" @click="inputValue = ''">清空输入</Button>
</div>
</div>
</template>
</McInput>
</McLayoutSender>
</McLayout>
</template>
<script setup lang="ts">
import { ref,nextTick,onMounted} from 'vue';
import { Button } from 'vue-devui/button';
import 'vue-devui/button/style.css';
import OpenAI from 'openai';
import { ThemeServiceInit, infinityTheme,galaxyTheme } from 'devui-theme';
// 使用无限主题
const themeService = ThemeServiceInit({ infinityTheme }, 'infinityTheme');
onMounted(()=>{
console.log('onMounted');
})
const theme = ref(false);
const themeChange = (val:boolean)=>{
val?themeService?.applyTheme(galaxyTheme):themeService?.applyTheme(infinityTheme)
}
const introPrompt = {
direction: 'horizontal',
list: [
{
value: 'quickSort',
label: '帮我写一个快速排序',
iconConfig: { name: 'icon-info-o', color: '#5e7ce0' },
desc: '使用 js 实现一个快速排序',
},
{
value: 'helpMd',
label: '你可以帮我做些什么?',
iconConfig: { name: 'icon-star', color: 'rgb(255, 215, 0)' },
desc: '了解当前大模型可以帮你做的事',
},
{
value: 'bindProjectSpace',
label: '怎么绑定项目空间',
iconConfig: { name: 'icon-priority', color: '#3ac295' },
desc: '如何绑定云空间中的项目',
},
],
};
const simplePrompt = [
{
value: 'quickSort',
iconConfig: { name: 'icon-info-o', color: '#5e7ce0' },
label: '帮我写一个快速排序',
},
{
value: 'helpMd',
iconConfig: { name: 'icon-star', color: 'rgb(255, 215, 0)' },
label: '你可以帮我做些什么?',
},
];
const startPage = ref(true);
const inputValue = ref('');
const messages = ref<any[]>([
{
from: 'user',
content: '你好',
},
{
from: 'model',
content: '你好,我是 MateChat',
id: 'init-msg',
},
]);
const newConversation = () => {
startPage.value = true;
messages.value = [];
}
const client = ref<OpenAI>();
client.value = new OpenAI({
apiKey: 'sk-xxxxx', // 模型APIKey
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', // 模型API地址
dangerouslyAllowBrowser: true,
});
const onSubmit = (evt:any) => {
inputValue.value = '';
startPage.value = false;
// 用户发送消息
messages.value.push({
from: 'user',
content: evt,
avatarConfig: { name: 'user' },
});
fetchData(evt);
};
const conversationRef = ref();
const fetchData = async (ques:any) => {
messages.value.push({
from: 'model',
content: '',
avatarConfig: { name: 'model' },
id: '',
loading: true,
});
const completion = await client.value!.chat.completions.create({
model: 'qwen-long', // 替换为自己的model名称
messages: [{ role: 'user', content: ques }],
stream: true, // 为 true 则开启接口的流式返回
});
messages.value[messages.value.length - 1].loading = false;
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || '';
const chatId = chunk.id;
messages.value[messages.value.length - 1].content += content;
messages.value[messages.value.length - 1].id = chatId;
nextTick(() => {
conversationRef.value?.scrollTo({
top: conversationRef.value.scrollHeight,
behavior: 'smooth',
});
});
}
};
</script>
<style lang="scss">
@import "index.scss";
</style>
index.scss
.container {
width: 98%;
margin: 5px auto;
height: calc(100vh - 10px);
padding: 20px;
gap: 8px;
border: 1px solid #ddd;
border-radius: 16px;
}
.content-container {
display: flex;
flex-direction: column;
gap: 8px;
overflow: auto;
}
.operations{
display: flex;
align-items: center;
gap: 4px;
}
.input-foot-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
margin-right: 8px;
.input-foot-left {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 12px;
color: #252b3a;
cursor: pointer;
}
.input-foot-dividing-line {
width: 1px;
height: 14px;
background-color: #d7d8da;
}
.input-foot-maxlength {
font-size: 12px;
color: #71757f;
}
}
.input-foot-right {
& > *:not(:first-child) {
margin-left: 8px;
}
}
}
4.代码分享及总结
在调试的过程中,发现主题的更改有点问题,按照官方的方式进行调试。结果总是差强人意。部分变色,部分不变色的问题让人头大。索性直接通过方法手动调整了。看了官方提的issue也有人反馈这个问题。各种潜在的问题预示着这个项目还有很大优化空间,希望能越来越好吧。
本来不太想提供Demo代码的,因为构建的ai对话聊天页面,实际上就是一个vue页面,按照我提供的代码应该也能看到效果。不过,我试过引入其他的vue3项目中,展现效果和Demo中的有一点点偏差,也懒得再去调试了。索性直接把整个Demo提供出来吧。
Demo下载地址:在我上传的csdn资源文件中。