在 Vue 3.0 使用 OpenAI API 實作 AI 翻譯工具

建立專案

建立專案。

1
2
npm create vite@latest gpt-translator -- --template vue
cd gpt-translator

安裝 ESLint 套件。

1
2
3
npm i @vue/eslint-config-airbnb \
eslint-import-resolver-typescript \
-D

在專案根目錄新增 .eslintrc.cjs 檔:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
extends: [
'@vue/airbnb',
],
settings: {
'import/resolver': {
typescript: {},
},
},
rules: {
},
};

安裝 Vuetify 框架。

1
npm i vuetify@^3.1.5

修改 main.js 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApp } from 'vue';
import './style.css';
import 'vuetify/styles';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import App from './App.vue';

const vuetify = createVuetify({
components,
directives,
});

createApp(App).use(vuetify).mount('#app');

修改 HelloWorld.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref } from 'vue';

defineProps({
msg: {
type: String,
default: '',
},
});

const count = ref(0);
</script>

<template>
<h1>{{ msg }}</h1>
<v-btn type="button" @click="count++">count is {{ count }}</v-btn>
</template>

<style scoped>
.read-the-docs {
color: #888;
}
</style>

實作

安裝 Axios 套件。

1
npm i axios

新增 api/index.js 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import axios from 'axios';

export const PARTICIPANT_AI = 'AI';
export const PARTICIPANT_HUMAN = 'Human';

export const FINISH_REASON_STOP = 'stop';
export const FINISH_REASON_LENGTH = 'length';

const newClient = (key) => axios.create({
baseURL: 'https://api.openai.com',
headers: {
Authorization: `Bearer ${key}`,
},
});

const createCompletion = (client) => ({
model = 'text-davinci-003',
prompt,
temperature = 0.9,
maxTokens = 160,
frequencyPenalty = 0,
presencePenalty = 0.6,
stop = [
` ${PARTICIPANT_AI}:`,
` ${PARTICIPANT_HUMAN}:`,
],
}) => client.post('/v1/completions', {
model,
prompt,
temperature: Number(temperature),
max_tokens: Number(maxTokens),
frequency_penalty: Number(frequencyPenalty),
presence_penalty: Number(presencePenalty),
stop,
});

export {
newClient,
createCompletion,
};

修改 App.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<v-app>
<v-main class="d-flex align-center bg-blue-grey-lighten-1">
<v-container>
<TheTranslator title="GPT Translator" />
</v-container>
</v-main>
</v-app>
</template>

<script setup>
import TheTranslator from './components/TheTranslator.vue';
</script>

新增 TheTranslator.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<script setup>
import { reactive, computed } from 'vue';
import {
createCompletion, newClient, PARTICIPANT_AI, PARTICIPANT_HUMAN,
} from '../api';

defineProps({
title: {
type: String,
default: '',
},
});

const data = reactive({
key: '',
question: '',
answer: '',
});

const prompt = computed(() => `${PARTICIPANT_HUMAN}: 請將以下內容翻譯成英文:「${data.question}」。\n${PARTICIPANT_AI}:`);

const translate = async () => {
const client = newClient(data.key);
const res = await createCompletion(client)({
prompt: prompt.value,
maxTokens: data.key.length * 4,
});
const { choices } = res.data;
const [choice] = choices;
const { text } = choice;
data.answer = text.trim();
};
</script>

<template>
<v-card>
<v-card-title>
{{ title }}
</v-card-title>
<v-card-item>
<v-textarea
v-model="data.question"
variant="outlined"
/>
<v-textarea
v-model="data.answer"
variant="outlined"
/>
<v-text-field
v-model="data.key"
label="Key"
type="password"
variant="outlined"
/>
</v-card-item>
<v-card-actions class="text-center">
<v-spacer />
<v-btn
@click="translate"
>
Translate
</v-btn>
</v-card-actions>
</v-card>
</template>

程式碼