LoginSignup
4
2

More than 1 year has passed since last update.

Vue2 × Vuetifyでチャットアプリつくーる(Powered by ChatGPT)

Last updated at Posted at 2023-05-07

はじめに

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

4.Vue2を選択
image.png

5.一旦実行

cd chatapp
npm run serve

6.http://localhost:8080/にアクセス

image.png

7.Vuetifyをインストール

vue add vuetify

8.Vuetify 2 - Vue CLI (recommended) を選択
image.png

9.一旦実行

npm run serve

image.png

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ポータルからコピペ)

image.png

.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資料です

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2