feat: version 2.9.1 (#207)

* feat: i18n

* chore: format

* feat: 补充遗漏翻译

* chore: update deps

* feat: 复制代码块[#196][#197]

* chore: version 2.9.1
This commit is contained in:
Redon 2023-03-02 21:27:20 +08:00 committed by GitHub
parent 21cf1bdd9e
commit f19998d59b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 589 additions and 334 deletions

11
.vscode/settings.json vendored
View File

@ -45,5 +45,14 @@
"VITE", "VITE",
"vueuse", "vueuse",
"Zhao" "Zhao"
] ],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested"
} }

View File

@ -1,3 +1,19 @@
## v2.9.1
`2023-03-02`
### Feature
- 代码块添加当前代码语言显示和复制功能[#197][#196]
- 完善多语言,现在可以切换中英文显示
## Enhancement
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
### BugFix
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.0 ## v2.9.0
`2023-03-02` `2023-03-02`

View File

@ -69,9 +69,9 @@ API_REVERSE_PROXY=
[✓] 对代码等消息类型的格式化美化处理 [✓] 对代码等消息类型的格式化美化处理
[] 界面多语言 [] 界面多语言
[] 界面主题 [] 界面主题
[✗] More... [✗] More...
@ -174,7 +174,7 @@ version: '3'
services: services:
app: app:
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull tag 镜像即可
ports: ports:
- 3002:3002 - 3002:3002
environment: environment:

View File

@ -18,9 +18,9 @@ services:
build: nginx build: nginx
image: chatgpt/nginx image: chatgpt/nginx
ports: ports:
- "80:80" - '80:80'
expose: expose:
- "80" - '80'
volumes: volumes:
- ./nginx/html/:/etc/nginx/html/ - ./nginx/html/:/etc/nginx/html/
links: links:

View File

@ -1,6 +1,6 @@
{ {
"name": "chatgpt-web", "name": "chatgpt-web",
"version": "2.9.0", "version": "2.9.1",
"private": false, "private": false,
"description": "ChatGPT Web", "description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>", "author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
@ -27,34 +27,34 @@
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"marked": "^4.2.12", "marked": "^4.2.12",
"naive-ui": "^2.34.3", "naive-ui": "^2.34.3",
"pinia": "^2.0.30", "pinia": "^2.0.32",
"vue": "^3.2.47", "vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.35.2", "@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4", "@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4", "@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0", "@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/marked": "^4.0.8", "@types/marked": "^4.0.8",
"@types/node": "^18.14.0", "@types/node": "^18.14.4",
"@types/web-bluetooth": "^0.0.16",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"axios": "^1.3.3", "axios": "^1.3.4",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"eslint": "^8.34.0", "eslint": "^8.35.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"less": "^4.1.3", "less": "^4.1.3",
"lint-staged": "^13.1.2", "lint-staged": "^13.1.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"rimraf": "^4.1.2", "rimraf": "^4.1.3",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "~4.9.5", "typescript": "~4.9.5",
"vite": "^4.1.2", "vite": "^4.1.4",
"vue-tsc": "^1.1.4" "vue-tsc": "^1.2.0"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,vue}": [ "*.{ts,tsx,vue}": [

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
}, },
"dependencies": { "dependencies": {
"chatgpt": "^5.0.1", "chatgpt": "^5.0.4",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"esno": "^0.16.3", "esno": "^0.16.3",
"express": "^4.18.2", "express": "^4.18.2",
@ -35,7 +35,7 @@
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.35.3", "@antfu/eslint-config": "^0.35.3",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/node": "^18.14.3", "@types/node": "^18.14.4",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"rimraf": "^4.1.3", "rimraf": "^4.1.3",
"tsup": "^6.6.3", "tsup": "^6.6.3",

View File

@ -3,8 +3,8 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@antfu/eslint-config': ^0.35.3 '@antfu/eslint-config': ^0.35.3
'@types/express': ^4.17.17 '@types/express': ^4.17.17
'@types/node': ^18.14.3 '@types/node': ^18.14.4
chatgpt: ^5.0.1 chatgpt: ^5.0.4
dotenv: ^16.0.3 dotenv: ^16.0.3
eslint: ^8.35.0 eslint: ^8.35.0
esno: ^0.16.3 esno: ^0.16.3
@ -17,7 +17,7 @@ specifiers:
typescript: ^4.9.5 typescript: ^4.9.5
dependencies: dependencies:
chatgpt: 5.0.1 chatgpt: 5.0.4
dotenv: 16.0.3 dotenv: 16.0.3
esno: 0.16.3 esno: 0.16.3
express: 4.18.2 express: 4.18.2
@ -28,7 +28,7 @@ dependencies:
devDependencies: devDependencies:
'@antfu/eslint-config': 0.35.3_ycpbpc6yetojsgtrx3mwntkhsu '@antfu/eslint-config': 0.35.3_ycpbpc6yetojsgtrx3mwntkhsu
'@types/express': 4.17.17 '@types/express': 4.17.17
'@types/node': 18.14.3 '@types/node': 18.14.4
eslint: 8.35.0 eslint: 8.35.0
rimraf: 4.1.3 rimraf: 4.1.3
tsup: 6.6.3_typescript@4.9.5 tsup: 6.6.3_typescript@4.9.5
@ -428,19 +428,19 @@ packages:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies: dependencies:
'@types/connect': 3.4.35 '@types/connect': 3.4.35
'@types/node': 18.14.3 '@types/node': 18.14.4
dev: true dev: true
/@types/connect/3.4.35: /@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies: dependencies:
'@types/node': 18.14.3 '@types/node': 18.14.4
dev: true dev: true
/@types/express-serve-static-core/4.17.33: /@types/express-serve-static-core/4.17.33:
resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
dependencies: dependencies:
'@types/node': 18.14.3 '@types/node': 18.14.4
'@types/qs': 6.9.7 '@types/qs': 6.9.7
'@types/range-parser': 1.2.4 '@types/range-parser': 1.2.4
dev: true dev: true
@ -472,8 +472,8 @@ packages:
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
dev: true dev: true
/@types/node/18.14.3: /@types/node/18.14.4:
resolution: {integrity: sha512-1y36CC5iL5CMyKALzwX9cwwxcWIxvIBe3gzs4GrXWXEQ8klQnCZ2U/WDGiNrXHmQcUhnaun17XG9TEIDlGj2RA==} resolution: {integrity: sha512-VhCw7I7qO2X49+jaKcAUwi3rR+hbxT5VcYF493+Z5kMLI0DL568b7JI4IDJaxWFH0D/xwmGJNoXisyX+w7GH/g==}
dev: true dev: true
/@types/normalize-package-data/2.4.1: /@types/normalize-package-data/2.4.1:
@ -495,7 +495,7 @@ packages:
resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
dependencies: dependencies:
'@types/mime': 3.0.1 '@types/mime': 3.0.1
'@types/node': 18.14.3 '@types/node': 18.14.4
dev: true dev: true
/@types/unist/2.0.6: /@types/unist/2.0.6:
@ -896,8 +896,8 @@ packages:
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
dev: true dev: true
/chatgpt/5.0.1: /chatgpt/5.0.4:
resolution: {integrity: sha512-Wy+/2XL0FobiJFaQ6N5WnhRCnOwrUJCpoVCn67qqhiWrM1QW6lgmpvtDDKKDyvj7D1MLMjc2xB/kK8aT27mL/w==} resolution: {integrity: sha512-qkppO2IDYDJC1eaXfqupXdZcOPNqtBkToRcvr9CAGM1rdsKfBDpWLTx4Y6OMNH02sgWu48aJB//0lO1M17K58w==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
dependencies: dependencies:

View File

@ -47,16 +47,16 @@ onMounted(() => {
> >
Github Github
</a> </a>
免费并且没有任何形式分付费行为 免费且基于 MIT 协议没有任何形式的付费行为
</p> </p>
<p> <p>
如果你觉得此项目对你有帮助请在 Github 帮我点个 Star 或者给予一点赞助谢谢 如果你觉得此项目对你有帮助请在 Github 帮我点个 Star 或者给予一点赞助谢谢
</p> </p>
</div> </div>
<p>API方式{{ config?.apiModel ?? '-' }}</p> <p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p>反向代理{{ config?.reverseProxy ?? '-' }}</p> <p>{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}</p>
<p>超时时间{{ config?.timeoutMs ?? '-' }}</p> <p>{{ $t("setting.timeout") }}{{ config?.timeoutMs ?? '-' }}</p>
<p>Socks代理{{ config?.socksProxy ?? '-' }}</p> <p>{{ $t("setting.socks") }}{{ config?.socksProxy ?? '-' }}</p>
</div> </div>
</NSpin> </NSpin>
</template> </template>

View File

@ -5,6 +5,7 @@ import type { Language, Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common' import { SvgIcon } from '@/components/common'
import { useAppStore, useUserStore } from '@/store' import { useAppStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper' import type { UserInfo } from '@/store/modules/user/helper'
import { t } from '@/locales'
interface Emit { interface Emit {
(event: 'update'): void (event: 'update'): void
@ -61,12 +62,12 @@ const languageOptions: { label: string; key: Language; value: Language }[] = [
function updateUserInfo(options: Partial<UserInfo>) { function updateUserInfo(options: Partial<UserInfo>) {
userStore.updateUserInfo(options) userStore.updateUserInfo(options)
ms.success('Update success') ms.success(t('common.success'))
} }
function handleReset() { function handleReset() {
userStore.resetUserInfo() userStore.resetUserInfo()
ms.success('Reset success') ms.success(t('common.success'))
emit('update') emit('update')
} }
</script> </script>
@ -75,40 +76,40 @@ function handleReset() {
<div class="p-4 space-y-5 min-h-[200px]"> <div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Avatar Link</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1"> <div class="flex-1">
<NInput v-model:value="avatar" placeholder="" /> <NInput v-model:value="avatar" placeholder="" />
</div> </div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })"> <NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
Save {{ $t('common.save') }}
</NButton> </NButton>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Name</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]"> <div class="w-[200px]">
<NInput v-model:value="name" placeholder="" /> <NInput v-model:value="name" placeholder="" />
</div> </div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })"> <NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
Save {{ $t('common.save') }}
</NButton> </NButton>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Description</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
<div class="flex-1"> <div class="flex-1">
<NInput v-model:value="description" placeholder="" /> <NInput v-model:value="description" placeholder="" />
</div> </div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })"> <NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
Save {{ $t('common.save') }}
</NButton> </NButton>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Reset UserInfo</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
<NButton text type="primary" @click="handleReset"> <NButton text type="primary" @click="handleReset">
Reset {{ $t('common.reset') }}
</NButton> </NButton>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Theme</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<template v-for="item of themeOptions" :key="item.key"> <template v-for="item of themeOptions" :key="item.key">
<a <a
@ -124,7 +125,7 @@ function handleReset() {
</div> </div>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">Language</span> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<template v-for="item of languageOptions" :key="item.key"> <template v-for="item of languageOptions" :key="item.key">
<a <a

View File

@ -5,6 +5,10 @@ import General from './General.vue'
import About from './About.vue' import About from './About.vue'
import { SvgIcon } from '@/components/common' import { SvgIcon } from '@/components/common'
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
interface Props { interface Props {
visible: boolean visible: boolean
} }
@ -13,10 +17,6 @@ interface Emit {
(e: 'update:visible', visible: boolean): void (e: 'update:visible', visible: boolean): void
} }
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const active = ref('General') const active = ref('General')
const reload = ref(false) const reload = ref(false)
@ -39,13 +39,13 @@ function handleReload() {
</script> </script>
<template> <template>
<NModal v-model:show="show"> <NModal v-model:show="show" :auto-focus="false">
<NCard role="dialog" aria-modal="true" :bordered="false" style="width: 100%; max-width: 640px"> <NCard role="dialog" aria-modal="true" :bordered="false" style="width: 100%; max-width: 640px">
<NTabs v-model:value="active" type="line" animated> <NTabs v-model:value="active" type="line" animated>
<NTabPane name="General" tab="General"> <NTabPane name="General" tab="General">
<template #tab> <template #tab>
<SvgIcon class="text-lg" icon="ri:file-user-line" /> <SvgIcon class="text-lg" icon="ri:file-user-line" />
<span class="ml-2">General</span> <span class="ml-2">{{ $t('setting.general') }}</span>
</template> </template>
<div class="min-h-[100px]"> <div class="min-h-[100px]">
<General v-if="!reload" @update="handleReload" /> <General v-if="!reload" @update="handleReload" />
@ -54,7 +54,7 @@ function handleReload() {
<NTabPane name="Config" tab="Config"> <NTabPane name="Config" tab="Config">
<template #tab> <template #tab>
<SvgIcon class="text-lg" icon="ri:list-settings-line" /> <SvgIcon class="text-lg" icon="ri:list-settings-line" />
<span class="ml-2">Config</span> <span class="ml-2">{{ $t('setting.config') }}</span>
</template> </template>
<About /> <About />
</NTabPane> </NTabPane>

View File

@ -1,15 +1,23 @@
import { computed } from 'vue' import { computed } from 'vue'
import { enUS, zhCN } from 'naive-ui' import { enUS, zhCN } from 'naive-ui'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { setLocale } from '@/locales'
export function useLanguage() { export function useLanguage() {
const appStore = useAppStore() const appStore = useAppStore()
const language = computed(() => { const language = computed(() => {
if (appStore.language === 'zh-CN') switch (appStore.language) {
return zhCN case 'en-US':
else setLocale('en-US')
return enUS return enUS
case 'zh-CN':
setLocale('zh-CN')
return zhCN
default:
setLocale('zh-CN')
return enUS
}
}) })
return { language } return { language }

41
src/locales/en-US.ts Normal file
View File

@ -0,0 +1,41 @@
export default {
common: {
delete: 'Delete',
save: 'Save',
reset: 'Reset',
yes: 'Yes',
no: 'No',
noData: 'No Data',
wrong: 'Something went wrong, please try again later.',
success: 'Success',
failed: 'Failed',
},
chat: {
placeholder: 'Ask me anything...(Shift + Enter = line break)',
placeholderMobile: 'Ask me anything...',
copy: 'Copy',
copied: 'Copied',
copyCode: 'Copy Code',
clearChat: 'Clear Chat',
clearChatConfirm: 'Are you sure to clear this chat?',
deleteMessage: 'Delete Message',
deleteMessageConfirm: 'Are you sure to delete this message?',
deleteHistoryConfirm: 'Are you sure to clear this history?',
},
setting: {
setting: 'Setting',
general: 'General',
config: 'Config',
avatarLink: 'Avatar Link',
name: 'Name',
description: 'Description',
resetUserInfo: 'Reset UserInfo',
theme: 'Theme',
language: 'Language',
api: 'API',
reverseProxy: 'Reverse Proxy',
timeout: 'Timeout',
socks: 'Socks',
},
}

34
src/locales/index.ts Normal file
View File

@ -0,0 +1,34 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import en from './en-US'
import cn from './zh-CN'
import { useAppStoreWithOut } from '@/store/modules/app'
import type { Language } from '@/store/modules/app/helper'
const appStore = useAppStoreWithOut()
const defaultLocale = appStore.language || 'zh-CN'
const i18n = createI18n({
locale: defaultLocale,
fallbackLocale: 'en-US',
allowComposition: true,
messages: {
'en-US': en,
'zh-CN': cn,
},
})
export function t(key: string) {
return i18n.global.t(key)
}
export function setLocale(locale: Language) {
i18n.global.locale = locale
}
export function setupI18n(app: App) {
app.use(i18n)
}
export default i18n

41
src/locales/zh-CN.ts Normal file
View File

@ -0,0 +1,41 @@
export default {
common: {
delete: '删除',
save: '保存',
reset: '重置',
yes: '是',
no: '否',
noData: '暂无数据',
wrong: '好像出错了,请稍后再试。',
success: '操作成功',
failed: '操作失败',
},
chat: {
placeholder: '来说点什么...Shift + Enter = 换行)',
placeholderMobile: '来说点什么...',
copy: '复制',
copied: '复制成功',
copyCode: '复制代码',
clearChat: '清空会话',
clearChatConfirm: '是否清空会话?',
deleteMessage: '删除消息',
deleteMessageConfirm: '是否删除此消息?',
deleteHistoryConfirm: '确定删除此记录?',
},
setting: {
setting: '设置',
general: '总览',
config: '配置',
avatarLink: '头像链接',
name: '名称',
description: '描述',
resetUserInfo: '重置用户信息',
theme: '主题',
language: '语言',
api: 'API',
reverseProxy: '反向代理',
timeout: '超时',
socks: 'Socks',
},
}

View File

@ -1,9 +1,10 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import { setupDirectives } from './directives' import { setupDirectives } from './directives'
import { setupAssets } from '@/plugins' import { setupI18n } from './locales'
import { setupStore } from '@/store' import { setupAssets } from './plugins'
import { setupRouter } from '@/router' import { setupStore } from './store'
import { setupRouter } from './router'
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
@ -13,6 +14,8 @@ async function bootstrap() {
setupDirectives(app) setupDirectives(app)
setupI18n(app)
await setupRouter(app) await setupRouter(app)
app.mount('#app') app.mount('#app')

View File

@ -1,8 +1,9 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
export const store = createPinia()
export function setupStore(app: App) { export function setupStore(app: App) {
const store = createPinia()
app.use(store) app.use(store)
} }

View File

@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { AppState, Language, Theme } from './helper' import type { AppState, Language, Theme } from './helper'
import { getLocalSetting, setLocalSetting } from './helper' import { getLocalSetting, setLocalSetting } from './helper'
import { store } from '@/store'
export const useAppStore = defineStore('app-store', { export const useAppStore = defineStore('app-store', {
state: (): AppState => getLocalSetting(), state: (): AppState => getLocalSetting(),
@ -27,3 +28,7 @@ export const useAppStore = defineStore('app-store', {
}, },
}, },
}) })
export function useAppStoreWithOut() {
return useAppStore(store)
}

View File

@ -4,6 +4,7 @@ import { marked } from 'marked'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { encodeHTML } from '@/utils/format' import { encodeHTML } from '@/utils/format'
import { t } from '@/locales'
interface Props { interface Props {
inversion?: boolean inversion?: boolean
@ -26,8 +27,10 @@ renderer.html = (html) => {
renderer.code = (code, language) => { renderer.code = (code, language) => {
const validLang = !!(language && hljs.getLanguage(language)) const validLang = !!(language && hljs.getLanguage(language))
if (validLang) if (validLang) {
return `<pre><code class="hljs ${language}">${hljs.highlight(language, code).value}</code></pre>` const lang = language ?? ''
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${language}">${hljs.highlight(lang, code).value}</code></pre>`
}
return `<pre style="background: none">${hljs.highlightAuto(code).value}</pre>` return `<pre style="background: none">${hljs.highlightAuto(code).value}</pre>`
} }

View File

@ -1,11 +1,12 @@
<script setup lang='ts'> <script setup lang='ts'>
import { ref } from 'vue' import { ref } from 'vue'
import { NDropdown, useMessage } from 'naive-ui' import { NDropdown } from 'naive-ui'
import AvatarComponent from './Avatar.vue' import AvatarComponent from './Avatar.vue'
import TextComponent from './Text.vue' import TextComponent from './Text.vue'
import { SvgIcon } from '@/components/common' import { SvgIcon } from '@/components/common'
import { copyText } from '@/utils/format' import { copyText } from '@/utils/format'
import { useIconRender } from '@/hooks/useIconRender' import { useIconRender } from '@/hooks/useIconRender'
import { t } from '@/locales'
interface Props { interface Props {
dateTime?: string dateTime?: string
@ -24,25 +25,18 @@ const props = defineProps<Props>()
const emit = defineEmits<Emit>() const emit = defineEmits<Emit>()
const ms = useMessage()
const { iconRender } = useIconRender() const { iconRender } = useIconRender()
const textRef = ref<HTMLElement>() const textRef = ref<HTMLElement>()
const options = [ const options = [
{ {
label: 'Copy Raw', label: t('chat.copy'),
key: 'copyRaw', key: 'copyText',
icon: iconRender({ icon: 'ri:file-copy-2-line' }), icon: iconRender({ icon: 'ri:file-copy-2-line' }),
}, },
{ {
label: 'Copy Text', label: t('common.delete'),
key: 'copyText',
icon: iconRender({ icon: 'ri:file-copy-line' }),
},
{
label: 'Delete',
key: 'delete', key: 'delete',
icon: iconRender({ icon: 'ri:delete-bin-line' }), icon: iconRender({ icon: 'ri:delete-bin-line' }),
}, },
@ -50,15 +44,8 @@ const options = [
function handleSelect(key: 'copyRaw' | 'copyText' | 'delete') { function handleSelect(key: 'copyRaw' | 'copyText' | 'delete') {
switch (key) { switch (key) {
case 'copyRaw':
if (textRef.value && (textRef.value as any).textRef) {
copyText({ text: (textRef.value as any).textRef.innerText })
ms.success('Copied Raw')
}
return
case 'copyText': case 'copyText':
copyText({ text: props.text ?? '', origin: false }) copyText({ text: props.text ?? '' })
ms.success('Copied Text')
return return
case 'delete': case 'delete':
emit('delete') emit('delete')

View File

@ -24,9 +24,37 @@
background-color: #fff; background-color: #fff;
} }
code.hljs{ code.hljs {
padding: 0; padding: 0;
} }
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy{
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
} }
html.dark { html.dark {

View File

@ -0,0 +1,18 @@
import { onMounted } from 'vue'
export function useCopyCode() {
function copyCodeBlock() {
const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper')
codeBlockWrapper.forEach((wrapper) => {
const copyBtn = wrapper.querySelector('.code-block-header__copy')
const codeBlock = wrapper.querySelector('.code-block-body')
if (copyBtn && codeBlock) {
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent ?? '')
})
}
})
}
onMounted(() => copyCodeBlock())
}

View File

@ -1,23 +1,25 @@
<script setup lang='ts'> <script setup lang='ts'>
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { NButton, NInput, useDialog, useMessage } from 'naive-ui' import { NButton, NInput, useDialog } from 'naive-ui'
import { Message } from './components' import { Message } from './components'
import { useScroll } from './hooks/useScroll' import { useScroll } from './hooks/useScroll'
import { useChat } from './hooks/useChat' import { useChat } from './hooks/useChat'
import { useCopyCode } from './hooks/useCopyCode'
import { HoverButton, SvgIcon } from '@/components/common' import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useChatStore } from '@/store' import { useChatStore } from '@/store'
import { fetchChatAPIProcess } from '@/api' import { fetchChatAPIProcess } from '@/api'
import { t } from '@/locales'
let controller = new AbortController() let controller = new AbortController()
const route = useRoute() const route = useRoute()
const dialog = useDialog() const dialog = useDialog()
const ms = useMessage()
const chatStore = useChatStore() const chatStore = useChatStore()
useCopyCode()
const { isMobile } = useBasicLayout() const { isMobile } = useBasicLayout()
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat() const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
@ -118,7 +120,7 @@ async function onConversation() {
}) })
} }
catch (error: any) { catch (error: any) {
const errorMessage = error?.message ?? 'Something went wrong, please try again later.' const errorMessage = error?.message ?? t('common.wrong')
if (error.message === 'canceled') { if (error.message === 'canceled') {
updateChatSome( updateChatSome(
@ -245,7 +247,7 @@ async function onRegenerate(index: number) {
return return
} }
const errorMessage = error?.message ?? 'Something went wrong, please try again later.' const errorMessage = error?.message ?? t('common.wrong')
updateChat( updateChat(
+uuid, +uuid,
@ -271,13 +273,12 @@ function handleDelete(index: number) {
return return
dialog.warning({ dialog.warning({
title: 'Delete Message', title: t('chat.deleteMessage'),
content: 'Are you sure to delete this message?', content: t('chat.deleteMessageConfirm'),
positiveText: 'Yes', positiveText: t('common.yes'),
negativeText: 'No', negativeText: t('common.no'),
onPositiveClick: () => { onPositiveClick: () => {
chatStore.deleteChatByUuid(+uuid, index) chatStore.deleteChatByUuid(+uuid, index)
ms.success('Message deleted successfully.')
}, },
}) })
} }
@ -287,10 +288,10 @@ function handleClear() {
return return
dialog.warning({ dialog.warning({
title: 'Clear Chat', title: t('chat.clearChat'),
content: 'Are you sure to clear this chat?', content: t('chat.clearChatConfirm'),
positiveText: 'Yes', positiveText: t('common.yes'),
negativeText: 'No', negativeText: t('common.no'),
onPositiveClick: () => { onPositiveClick: () => {
chatStore.clearChatByUuid(+uuid) chatStore.clearChatByUuid(+uuid)
}, },
@ -315,8 +316,8 @@ function handleStop() {
const placeholder = computed(() => { const placeholder = computed(() => {
if (isMobile.value) if (isMobile.value)
return 'Ask me anything...' return t('chat.placeholderMobile')
return 'Ask me anything... (Shift + Enter = line break)' return t('chat.placeholder')
}) })
const buttonDisabled = computed(() => { const buttonDisabled = computed(() => {

View File

@ -11,7 +11,7 @@ const show = ref(false)
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800"> <footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800">
<UserAvatar /> <UserAvatar />
<HoverButton tooltip="Setting" @click="show = true"> <HoverButton :tooltip="$t('setting.setting')" @click="show = true">
<span class="text-xl text-[#4f555e] dark:text-white"> <span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:settings-4-line" /> <SvgIcon icon="ri:settings-4-line" />
</span> </span>

View File

@ -49,7 +49,7 @@ function isActive(uuid: number) {
<template v-if="!dataSources.length"> <template v-if="!dataSources.length">
<div class="flex flex-col items-center mt-4 text-center text-neutral-300"> <div class="flex flex-col items-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" /> <SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" />
<span>No history</span> <span>{{ $t('common.noData') }}</span>
</div> </div>
</template> </template>
<template v-else> <template v-else>
@ -87,7 +87,7 @@ function isActive(uuid: number) {
<SvgIcon icon="ri:delete-bin-line" /> <SvgIcon icon="ri:delete-bin-line" />
</button> </button>
</template> </template>
Are you sure to clear this history? {{ $t('chat.deleteHistoryConfirm') }}
</NPopconfirm> </NPopconfirm>
</template> </template>
</div> </div>

View File

@ -13,10 +13,11 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"strictNullChecks": true, "strictNullChecks": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": ["vite/client", "node", "naive-ui/volar", "web-bluetooth"] "types": ["vite/client", "node", "naive-ui/volar"]
}, },
"exclude": ["node_modules", "dist", "service"] "exclude": ["node_modules", "dist", "service"]
} }