はじめに
Vue2とVuetifyでチャットアプリを作りましょう
開発環境
- Windows 11 PC
- Node.js 18.15.0
- Vue CLI 5.0.8
- Vue 2.7.14
- Vuetify 2.6.15
- Azure OpenAI Service
実装
1.Node.jsのインストール
2.Node.js command promptを開き、Vueのインストール
npm install -g @vue/cli
3.プロジェクトを作成(プロジェクト名は小文字)
vue create chatapp
5.一旦実行
cd chatapp
npm run serve
7.Vuetifyをインストール
vue add vuetify
8.Vuetify 2 - Vue CLI (recommended) を選択
9.一旦実行
npm run serve
10.vue-clipboard2、clipboard、markdown-it、axiosのインストール
npm install vue-clipboard2 clipboard markdown-it axios
11.App.vueを編集
App.vue
<template>
<v-app>
<v-row no-gutters>
<v-container>
<v-row justify="center">
<v-col cols="12" sm="10" md="8">
<v-card class="mx-auto my-6" max-width="800">
<v-card-text ref="conversation" style="
min-height: 400px;
max-height: 450px;
overflow-y: scroll;
padding-bottom: 4;
">
<v-list>
<v-list-item v-for="(message, index) in messages" :key="index">
<v-list-item-avatar>
<v-icon v-if="message.isUser">mdi-account-circle</v-icon>
<v-icon v-else>mdi-robot</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-card class="my-1" :class="{
'blue lighten-5': message.isUser,
'grey lighten-4': !message.isUser,
}">
<v-card-text>
<template v-for="(msg, idx) in parseText(message.text)">
<v-card-text v-if="!msg.isCode" :key="'msg' + idx" v-html="md.render(msg.text)"></v-card-text>
<v-card-text v-else :key="'code' + idx" class="code">
<v-card-text style="position: relative; padding-right: 4px">
<pre style="
white-space: pre-wrap;
border-radius: 4px;
overflow: auto;
">{{ msg.text }}</pre>
<v-btn class="mt-2" color="primary" style="position: absolute; top: 0; right: 0"
@click="copyToClipboard(msg.text)">Copy</v-btn>
</v-card-text>
</v-card-text>
</template>
</v-card-text>
</v-card>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card-text>
<v-divider></v-divider>
<v-select v-model="selectedDeployment" :items="deployments" item-value="name" item-text="displayName"
label="Deployment" hide-details solo clearable></v-select>
<v-textarea v-model="inputText" :disabled="sending" label="Type a message" solo class="input-textarea"
style="width: 100%; height: 100px" @keydown.enter.prevent="sendWithLineBreak"></v-textarea>
<v-row justify="end" no-gutters>
<v-col cols="auto">
<v-btn @click="sendMessage" :loading="sending" color="primary" icon><v-icon>mdi-send</v-icon></v-btn>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
</v-container>
</v-row>
</v-app>
</template>
<script>
import Vue from "vue";
import VueClipboard from "vue-clipboard2";
import ClipboardJS from "clipboard";
import MarkdownIt from "markdown-it";
import axios from "axios";
Vue.use(VueClipboard);
export default {
mounted() {
this.clipboard = new ClipboardJS(".copy-to-clipboard");
this.clipboard.on("success", this.onClipboardSuccess);
},
data() {
return {
json_data: {
messages: [
{ role: "system", content: "あなたはプロのAIアシスタントです。" },
],
},
messages: [],
inputText: "",
sending: false,
md: new MarkdownIt(),
selectedDeployment: "VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT35",
deployments: [
{
name: "VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT35",
displayName: "gpt-3.5-turbo",
},
{ name: "VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT4", displayName: "gpt-4" },
{
name: "VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT432K",
displayName: "gpt-4-32k",
},
],
drawer: true,
selectedMessageId: null,
selectedMessageTitle: "",
conversationContainers: [],
};
},
created() {
this.messages.push({
text: "こんにちは。なにかお手伝いできることはございますか?",
isUser: false,
});
},
methods: {
copyToClipboard(code) {
this.$copyText(code)
.then(() => {
this.$toast.success("Code copied to clipboard!");
})
.catch(() => {
this.$toast.error("Failed to copy code!");
});
},
parseText(text) {
const codeRegex = /```([\w]+)?\n?([\s\S]+?)\n?```/g;
const codeBlocks = [];
let match;
let lastIndex = 0;
while ((match = codeRegex.exec(text)) !== null) {
const [block, lang, code] = match;
if (match.index > lastIndex) {
const prevText = text.substring(lastIndex, match.index);
codeBlocks.push({ isCode: false, text: prevText });
}
codeBlocks.push({ isCode: true, text: code.trim(), lang });
lastIndex = match.index + block.length;
}
const lastText = text.substring(lastIndex);
codeBlocks.push({ isCode: false, text: lastText });
return codeBlocks;
},
sendWithLineBreak() {
this.inputText += "\n";
},
sendMessage() {
if (!this.inputText) {
return;
}
this.sending = true;
const messages = [{ text: this.inputText, isUser: true }];
this.messages.push(...messages);
const url = `${process.env.VUE_APP_OPENAI_BASE_URL}/openai/deployments/${process.env[this.selectedDeployment]
}/chat/completions?api-version=${process.env.VUE_APP_OPENAI_API_VERSION}`;
const headers = {
headers: {
"Content-Type": "application/json",
"api-key": process.env.VUE_APP_OPENAI_API_KEY,
},
};
this.json_data.messages.push({ role: "user", content: this.inputText });
this.inputText = "";
axios
.post(url, this.json_data, headers)
.then((response) => {
console.log(response.data);
this.messages.push({
text: response.data.choices[0].message.content,
isUser: false,
});
this.json_data.messages.push({
role: "assistant",
content: response.data.choices[0].message.content,
});
this.sending = false;
this.$refs.conversation.scrollTop =
this.$refs.conversation.scrollHeight;
this.$nextTick(() => {
this.$refs.conversation.scrollTop =
this.$refs.conversation.scrollHeight;
});
})
.catch((error) => {
if (error.response.status === 429) {
this.messages.push({
text: "ただいま、大変混み合っております。しばらく待ってから再度お試しください。",
isUser: false,
});
this.sending = false;
this.$nextTick(() => {
this.$refs.conversation.scrollTop =
this.$refs.conversation.scrollHeight;
});
} else {
this.sending = false;
console.log(error);
}
});
this.$nextTick(() => {
this.$refs.conversation.scrollTop =
this.$refs.conversation.scrollHeight;
});
},
},
};
</script>
<style>
.code {
background-color: #f0f0f0;
padding: 10px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.4;
border-radius: 4px;
overflow: auto;
}
</style>
<style scoped>
.input-textarea textarea {
height: auto !important;
max-height: 150px;
overflow-y: auto;
}
</style>
12.main.jsを編集
main.js
import Vue from 'vue'
import App from './App.vue'
import Vuetify from 'vuetify';
import vuetify from './plugins/vuetify'
import 'vuetify/dist/vuetify.min.css'
Vue.use(Vuetify)
Vue.config.productionTip = false
new Vue({
vuetify,
render: h => h(App)
}).$mount('#app')
13..envを作成(VUE_APP_OPENAI_BASE_URLとVUE_APP_OPENAI_API_KEYはAzureポータルからコピペ)
.env
VUE_APP_OPENAI_BASE_URL=https://xxxx.openai.azure.com/
VUE_APP_OPENAI_API_KEY=xxxx
VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT432K=gpt-4-32k
VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT4=gpt-4
VUE_APP_OPENAI_DEPLOYMENT_NAME_GPT35=gpt-35-turbo
VUE_APP_OPENAI_API_VERSION=2023-03-15-preview
14.実行
npm run serve
お疲れ様でした。
追記
AIミーティング 2023/05/10 #ChatGPT #GPT4 #PaLM のLT資料です