Skip to content

Commit

Permalink
feat: 数据持久化,头像支持自定义 (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
liou666 committed Apr 20, 2023
1 parent c8699b2 commit 09c66c1
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 61 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ VITE_SERVE_PROXY=xxx
# Aruze tts Key (required for tts)
VITE_SCRIPTION_KEY=xxx

# Aruze tts Region (required for tts , e.g eastasia1)
VITE_REGION=xxx
# # Aruze tts Region (required for tts , e.g eastasia1) 已经不需要了,直接在界面中指定
# VITE_REGION=xxx

# Aruze translate Key (required for translate)
VITE_TRANSLATE_KEY=xxx
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {}

declare module '@vue/runtime-core' {
export interface GlobalComponents {
Avatar: typeof import('./src/components/Avatar.vue')['default']
Button: typeof import('./src/components/Button.vue')['default']
Card: typeof import('./src/pages/Home/components/Card.vue')['default']
Content: typeof import('./src/pages/Home/Content.vue')['default']
Expand Down
2 changes: 1 addition & 1 deletion electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ ipcMain.on('open-settings-window', (event) => {
y: 200,
frame: true,
titleBarStyle: 'default',
modal: true, // 模态窗口,会阻塞父窗口 (macOS 不支持)
// modal: true, // 模态窗口,会阻塞父窗口 (macOS 不支持)
parent: win!,
resizable: false,
fullscreenable: false,
Expand Down
2 changes: 2 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGlobalSetting: typeof import('./hooks/useGlobalSetting')['default']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
Expand Down Expand Up @@ -456,6 +457,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGlobalSetting: UnwrapRef<typeof import('./hooks/useGlobalSetting')['default']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
Expand Down
36 changes: 36 additions & 0 deletions src/components/Avatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
const { maxSize = 2, imageUrl } = defineProps<{ maxSize?: number; imageUrl: string }>()
const emit = defineEmits<{
(event: 'change', value: string): void
(event: 'update:imageUrl', value: string): void
}>()
const inputFileElement = ref<HTMLInputElement | null>(null)
const fileChange = (event: Event) => {
const maxFileSize = maxSize * 1024 * 1024
const file = (event.target as HTMLInputElement).files![0]
const acceptImageType = ['image/png', 'image/jpeg']
if (!file || !acceptImageType.includes(file.type)) {
alert('仅支持上传png、jpg格式的图片')
return
}
if (file.size > maxFileSize) {
alert(`图片大小不能超过${maxSize}MB`)
return
}
const reader = new FileReader()
reader.onload = function () {
emit('update:imageUrl', reader.result as string)
}
reader.readAsDataURL(file)
}
</script>

<template>
<img object-fill w-14 h-14 rounded-full :src="imageUrl" alt="" @click="inputFileElement?.click()">
<input ref="inputFileElement" type="file" name="avatar" class="hidden" accept=".jpg,.png" @change="fileChange($event)">
</template>
1 change: 1 addition & 0 deletions src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export const IS_ALWAYS_RECOGNITION = 'isAlwaysRecognition'
export const OPEN_MODEL = 'openModel'
export const CHAT_API_NAME = 'chatApiName'
export const CHAT_REMEMBER_COUNT = 'chatRememberCount'
export const SELF_AVATAR_URL = 'selfAvatarUrl'
37 changes: 37 additions & 0 deletions src/hooks/useGlobalSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AZURE_KEY, AZURE_REGION, AZURE_TRANSLATE_KEY, CHAT_API_NAME, CHAT_REMEMBER_COUNT, OPEN_KEY, OPEN_MODEL, OPEN_PROXY, SELF_AVATAR_URL } from '@/constant'

import { getAvatarUrl } from '@/utils'

const defaultOpenKey = import.meta.env.VITE_OPENAI_API_KEY
const defaultOpenProxy = import.meta.env.VITE_SERVE_PROXY
const defaultAzureRegion = import.meta.env.VITE_REGION
const defaultAzureKey = import.meta.env.VITE_SCRIPTION_KEY
const defaultAzureTranslateKey = import.meta.env.VITE_TRANSLATE_KEY

export const useGlobalSetting = () => {
console.log('useGlobalSetting')

const openKey = useLocalStorage(OPEN_KEY, defaultOpenKey)
const openProxy = useLocalStorage(OPEN_PROXY, defaultOpenProxy)
const azureRegion = useLocalStorage(AZURE_REGION, defaultAzureRegion)
const azureKey = useLocalStorage(AZURE_KEY, defaultAzureKey)
const azureTranslateKey = useLocalStorage(AZURE_TRANSLATE_KEY, defaultAzureTranslateKey)
const openModel = useLocalStorage(OPEN_MODEL, 'gpt-3.5-turbo')
const selfAvatar = useLocalStorage(SELF_AVATAR_URL, getAvatarUrl('self.png'))
const chatApiName = useLocalStorage(CHAT_API_NAME, 'openai')
const chatRememberCount = useLocalStorage(CHAT_REMEMBER_COUNT, '10')

return {
openKey,
openProxy,
openModel,
azureRegion,
azureKey,
azureTranslateKey,
selfAvatar,
chatApiName,
chatRememberCount,
}
}

export default useGlobalSetting
11 changes: 6 additions & 5 deletions src/hooks/useSpeechService.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { VoiceInfo } from 'microsoft-cognitiveservices-speech-sdk'
import {
AudioConfig,
ResultReason,
SpeakerAudioDestination,
SpeechConfig,
SpeechRecognizer,
SpeechSynthesizer,
} from 'microsoft-cognitiveservices-speech-sdk'

export const useSpeechService = (subscriptionKey: string, region: string, langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-CN', 'zh-HK', 'ko-KR', 'de-DE']) => {
export const useSpeechService = (langs = <const>['fr-FR', 'ja-JP', 'en-US', 'zh-CN', 'zh-HK', 'ko-KR', 'de-DE']) => {
const { azureKey, azureRegion } = useGlobalSetting()
const languages = ref(langs)
const language = ref<typeof langs[number]>(langs[0])
const languageMap = ref<Partial<Record<typeof langs[number], VoiceInfo[]>>>({})
const voiceName = ref('en-US-JennyMultilingualNeural')

const speechConfig = ref(SpeechConfig.fromSubscription(subscriptionKey, region))
const speechConfig = ref(SpeechConfig.fromSubscription(azureKey.value, azureRegion.value))
const isRecognizing = ref(false) // 语音识别中
const isSynthesizing = ref(false) // 语音合成中
const isPlaying = ref(false) // 语音播放中
Expand All @@ -31,11 +31,11 @@ export const useSpeechService = (subscriptionKey: string, region: string, langs
// 引入变量,触发 SpeechSynthesizer 实例的重新创建
const count = ref(0)

watch([language, voiceName, count], ([lang, voice]) => {
watch([language, voiceName, count, azureKey, azureRegion], ([lang, voice]) => {
speechConfig.value = SpeechConfig.fromSubscription(azureKey.value, azureRegion.value)
speechConfig.value.speechRecognitionLanguage = lang
speechConfig.value.speechSynthesisLanguage = lang
speechConfig.value.speechSynthesisVoiceName = voice

// 通过playback结束事件来判断播放结束
const player = new SpeakerAudioDestination()
player.onAudioStart = function (_) {
Expand All @@ -48,6 +48,7 @@ export const useSpeechService = (subscriptionKey: string, region: string, langs
isPlayend.value = true
console.log('playback finished')
}

const audioConfig = AudioConfig.fromDefaultMicrophoneInput()
const audioConfiga = AudioConfig.fromSpeakerOutput(player)
recognizer.value = new SpeechRecognizer(speechConfig.value, audioConfig)
Expand Down
17 changes: 8 additions & 9 deletions src/pages/Home/components/Content.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import Button from '@/components/Button.vue'
import { generatTranslate, generateText } from '@/server/api'
import { getAvatarUrl, getAzureTranslateKey, getOpenAzureKey, getOpenAzureRegion, getOpenKey, getOpenProxy, verifyOpenKey } from '@/utils'
import { getAzureTranslateKey, verifyOpenKey } from '@/utils'
import { useConversationStore } from '@/stores'
interface Translates {
Expand All @@ -13,8 +13,9 @@ interface Translates {
// hooks
const store = useConversationStore()
const { el, scrollToBottom } = useScroll()
const { selfAvatar, openKey, openProxy } = useGlobalSetting()
const {
language,
voiceName,
Expand All @@ -26,7 +27,7 @@ const {
stopRecognizeSpeech,
ssmlToSpeak,
isSynthesizing,
} = useSpeechService(getOpenAzureKey(), getOpenAzureRegion(), store.allLanguage as any)
} = useSpeechService(store.allLanguage as any)
// states
const message = ref('') // input message
Expand Down Expand Up @@ -60,7 +61,7 @@ watch(currentKey, () => {
const fetchResponse = async (key: string) => {
let res
try {
res = await generateText(currentChatMessages.value, key, getOpenProxy())
res = await generateText(currentChatMessages.value, key, openProxy.value)
}
catch (error: any) {
return alert('[Error] 网络请求超时, 请检查网络或代理')
Expand All @@ -71,9 +72,7 @@ const fetchResponse = async (key: string) => {
}
const onSubmit = async () => {
const key = getOpenKey()
if (!verifyOpenKey(key)) return alert('请输入正确的API-KEY')
if (!verifyOpenKey(openKey.value)) return alert('请输入正确的API-KEY')
if (!message.value) return
store.changeConversations([
Expand All @@ -83,7 +82,7 @@ const onSubmit = async () => {
message.value = ''
store.changeLoading(true)
const content = await fetchResponse(key)
const content = await fetchResponse(openKey.value)
if (content) {
store.changeConversations([
...currentChatMessages.value,
Expand Down Expand Up @@ -149,7 +148,7 @@ const translate = async (text: string, i: number) => {
center-y odd:flex-row-reverse
>
<div class="w-10 h-10">
<img w-full h-full object-fill rounded-full :src="item.role === 'user' ? getAvatarUrl('self.png') : currentAvatar" alt="">
<img w-full h-full object-fill rounded-full :src="item.role === 'user' ? selfAvatar : currentAvatar" alt="">
</div>

<div style="flex-basis:fit-content" mx-2>
Expand Down
34 changes: 4 additions & 30 deletions src/pages/Home/components/NewChat.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<script setup lang="ts">
import { v4 as uuid } from 'uuid'
import type { VoiceInfo } from 'microsoft-cognitiveservices-speech-sdk'
import Avatar from '@/components/Avatar.vue'
import { supportLanguageMap } from '@/config'
import { useConversationStore } from '@/stores'
import { getAvatarUrl } from '@/utils'
const { allVoices } = defineProps<{ allVoices: VoiceInfo[] }>()
const emits = defineEmits(['close'])
const modules = import.meta.glob(['../assets/avatars/*', '!../assets/avatars/self.png'])
const avatarList = ref<string[]>(Object.keys(modules).map(path => path.replace('../assets/avatars/', '')))
const modules = import.meta.glob(['../../../assets/avatars/*', '!../../../assets/avatars/self.png'])
const avatarList = ref<string[]>(Object.keys(modules).map(path => path.replace('../../../assets/avatars/', '')))
const currentAvatarIndex = ref(Math.random() * avatarList.value.length | 0)
const inputFileElement = ref<HTMLInputElement | null>(null)
const store = useConversationStore()
Expand Down Expand Up @@ -58,30 +58,6 @@ const addChat = (event: any) => {
const changeAvatar = () => {
currentAvatarIndex.value = avatarList.value.length - 1 === currentAvatarIndex.value ? 0 : currentAvatarIndex.value + 1
}
const fileChange = (event: Event) => {
const baseSize = 2
const maxFileSize = baseSize * 1024 * 1024 // 2MB
const file = (event.target as HTMLInputElement).files![0]
const acceptImageType = ['image/png', 'image/jpeg']
if (!file || !acceptImageType.includes(file.type)) {
alert('仅支持上传png、jpg格式的图片')
return
}
if (file.size > maxFileSize) {
alert(`图片大小不能超过${baseSize}MB`)
return
}
const reader = new FileReader()
reader.onload = function () {
imageUrl.value = reader.result as string
}
reader.readAsDataURL(file)
}
</script>

<script>
Expand All @@ -95,9 +71,7 @@ const fileChange = (event: Event) => {
</div> -->

<div flex>
<img object-fill w-14 h-14 rounded-full :src="imageUrl" alt="" @click="inputFileElement?.click()">
<input ref="inputFileElement" type="file" name="avatar" class="hidden" accept=".jpg,.png" @change="fileChange($event)">
<!-- <img w-14 h-14 rounded-full :src="getAvatarUrl(avatarList[currentAvatarIndex])" alt="" @click="changeAvatar()"> -->
<Avatar v-model:image-url="imageUrl" />
</div>
<div flex>
<label center-y justify-end mr-2 for="">姓名</label>
Expand Down
9 changes: 4 additions & 5 deletions src/pages/Home/components/Tool.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<script setup lang="ts">
import { ipcRenderer } from 'electron'
import Setting from '../../Setting/Setting.vue'
import NewChat from './NewChat.vue'
import { getOpenAzureKey, getOpenAzureRegion } from '@/utils'
import Setting from '@/pages/Setting/Setting.vue'
const isDark = useDark()
const toggleDark = useToggle(isDark)
const addVisible = ref(false)
const settingVisible = ref(false)
const { allVoices } = useSpeechService(getOpenAzureKey(), getOpenAzureRegion())
const isDark = useDark()
const toggleDark = useToggle(isDark)
const { allVoices } = useSpeechService()
</script>

<template>
Expand Down
31 changes: 23 additions & 8 deletions src/pages/Setting/components/OpenSetting.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<script setup lang="ts">
import Avatar from '@/components/Avatar.vue'
import { openaiModels } from '@/config'
import { CHAT_API_NAME, CHAT_REMEMBER_COUNT, OPEN_KEY, OPEN_MODEL, OPEN_PROXY } from '@/constant'
const openKey = useLocalStorage(OPEN_KEY, '')
const proxy = useLocalStorage(OPEN_PROXY, '')
const openModel = useLocalStorage(OPEN_MODEL, 'gpt-3.5-turbo')
const chatApiName = useLocalStorage(CHAT_API_NAME, 'openAI')
const chatRememberCount = useLocalStorage(CHAT_REMEMBER_COUNT, '10')
const { openKey, openProxy, openModel, chatApiName, chatRememberCount, selfAvatar } = useGlobalSetting()
</script>

<template>
Expand Down Expand Up @@ -48,7 +45,7 @@ const chatRememberCount = useLocalStorage(CHAT_REMEMBER_COUNT, '10')
<label mr-1 for="">OpenAI API 代理地址</label>
</div>
<input
v-model="proxy"
v-model="openProxy"
placeholder="https://api.openai.com"
>
</div>
Expand All @@ -66,7 +63,7 @@ const chatRememberCount = useLocalStorage(CHAT_REMEMBER_COUNT, '10')
</section>

<section class="main-section">
<div m-2 pr-2>
<div m-2 p2>
<div center-y>
<label mr-1 my-1 for="">联系上下文次数</label>
<el-tooltip
Expand All @@ -85,6 +82,24 @@ const chatRememberCount = useLocalStorage(CHAT_REMEMBER_COUNT, '10')
</div>
</div>
</section>

<section class="main-section">
<div m-2 p2>
<div center-y>
<label mr-1 my-1 for="">头像</label>
<el-tooltip
effect="dark"
content="点击图片更换喜欢的头像"
placement="bottom"
>
<i icon-btn i-carbon:information-square />
</el-tooltip>
<span ml-auto>
<Avatar v-model:image-url="selfAvatar" />
</span>
</div>
</div>
</section>
</div>
</template>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Setting/components/TTSSetting.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const isAlwaysRecognition = useLocalStorage(IS_ALWAYS_RECOGNITION, false)
</section>

<section class="main-section">
<div center-y justify-between m-2 pr-2>
<div class="section-item">
<div center-y>
<label mr-1 for="">沉浸式对话模式</label>
<el-tooltip
Expand Down

0 comments on commit 09c66c1

Please sign in to comment.