Spaces:
Build error
Build error
Commit
·
3424203
1
Parent(s):
a81cbf3
Upload 135 files
Browse files- .env +2 -2
- .eslintignore +1 -1
- docker-compose/docker-compose.yml +1 -1
- docker-compose/nginx/nginx.conf +1 -1
- kubernetes/README.md +1 -1
- kubernetes/deploy.yaml +1 -1
- kubernetes/ingress.yaml +1 -1
- package-lock.json +0 -0
- package.json +1 -1
- pnpm-lock.yaml +1 -1
- public/favicon.ico +0 -0
- public/favicon.svg +1 -24
- public/pwa-192x192.png +0 -0
- public/pwa-512x512.png +0 -0
- service/.env.example +1 -3
- service/package.json +1 -1
- service/pnpm-lock.yaml +0 -0
- src/api/index.ts +1 -1
- src/components/common/PromptStore/index.vue +1 -1
- src/components/common/Setting/Advanced.vue +2 -2
- src/components/common/Setting/General.vue +5 -2
- src/components/common/Setting/index.vue +1 -1
- src/router/permission.ts +1 -1
- src/store/helper.ts +3 -0
- src/store/index.ts +1 -3
- src/store/modules/app/helper.ts +2 -2
- src/store/modules/app/index.ts +1 -1
- src/store/modules/auth/index.ts +1 -1
- src/store/modules/chat/index.ts +6 -1
- src/store/modules/settings/helper.ts +1 -1
- src/store/modules/settings/index.ts +3 -3
- src/store/modules/user/helper.ts +3 -3
- src/styles/lib/highlight.less +1 -1
- src/typings/env.d.ts +1 -1
- src/utils/functions/debounce.ts +1 -1
- src/utils/request/index.ts +1 -1
- src/utils/storage/index.ts +57 -1
- src/views/chat/components/Header/index.vue +8 -8
- src/views/chat/components/Message/Text.vue +2 -5
- src/views/chat/components/Message/index.vue +1 -1
- src/views/chat/components/Message/style.less +61 -1
- src/views/chat/index.vue +5 -5
- src/views/chat/layout/sider/List.vue +1 -1
- src/views/chat/layout/sider/index.vue +28 -6
- start.cmd +1 -1
- vite.config.ts +1 -1
.env
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
# Glob API URL
|
| 2 |
VITE_GLOB_API_URL=/api
|
| 3 |
|
| 4 |
-
VITE_APP_API_BASE_URL=http://
|
| 5 |
|
| 6 |
# Whether long replies are supported, which may result in higher API fees
|
| 7 |
VITE_GLOB_OPEN_LONG_REPLY=false
|
| 8 |
|
| 9 |
# When you want to use PWA
|
| 10 |
-
VITE_GLOB_APP_PWA=false
|
|
|
|
| 1 |
# Glob API URL
|
| 2 |
VITE_GLOB_API_URL=/api
|
| 3 |
|
| 4 |
+
VITE_APP_API_BASE_URL=http://127.0.0.1:3002/
|
| 5 |
|
| 6 |
# Whether long replies are supported, which may result in higher API fees
|
| 7 |
VITE_GLOB_OPEN_LONG_REPLY=false
|
| 8 |
|
| 9 |
# When you want to use PWA
|
| 10 |
+
VITE_GLOB_APP_PWA=false
|
.eslintignore
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
docker-compose
|
| 2 |
-
kubernetes
|
|
|
|
| 1 |
docker-compose
|
| 2 |
+
kubernetes
|
docker-compose/docker-compose.yml
CHANGED
|
@@ -44,4 +44,4 @@ services:
|
|
| 44 |
- ./nginx/html:/usr/share/nginx/html
|
| 45 |
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
| 46 |
links:
|
| 47 |
-
- app
|
|
|
|
| 44 |
- ./nginx/html:/usr/share/nginx/html
|
| 45 |
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
| 46 |
links:
|
| 47 |
+
- app
|
docker-compose/nginx/nginx.conf
CHANGED
|
@@ -24,4 +24,4 @@ server {
|
|
| 24 |
proxy_set_header X-Real-IP $remote_addr;
|
| 25 |
proxy_set_header REMOTE-HOST $remote_addr;
|
| 26 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 27 |
-
}
|
|
|
|
| 24 |
proxy_set_header X-Real-IP $remote_addr;
|
| 25 |
proxy_set_header REMOTE-HOST $remote_addr;
|
| 26 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 27 |
+
}
|
kubernetes/README.md
CHANGED
|
@@ -6,4 +6,4 @@ kubectl apply -f deploy.yaml
|
|
| 6 |
### 如果需要Ingress域名接入
|
| 7 |
```
|
| 8 |
kubectl apply -f ingress.yaml
|
| 9 |
-
```
|
|
|
|
| 6 |
### 如果需要Ingress域名接入
|
| 7 |
```
|
| 8 |
kubectl apply -f ingress.yaml
|
| 9 |
+
```
|
kubernetes/deploy.yaml
CHANGED
|
@@ -63,4 +63,4 @@ spec:
|
|
| 63 |
targetPort: 3002
|
| 64 |
selector:
|
| 65 |
app: chatgpt-web
|
| 66 |
-
type: ClusterIP
|
|
|
|
| 63 |
targetPort: 3002
|
| 64 |
selector:
|
| 65 |
app: chatgpt-web
|
| 66 |
+
type: ClusterIP
|
kubernetes/ingress.yaml
CHANGED
|
@@ -18,4 +18,4 @@ spec:
|
|
| 18 |
path: /
|
| 19 |
pathType: ImplementationSpecific
|
| 20 |
tls:
|
| 21 |
-
- secretName: chatgpt-web-tls
|
|
|
|
| 18 |
path: /
|
| 19 |
pathType: ImplementationSpecific
|
| 20 |
tls:
|
| 21 |
+
- secretName: chatgpt-web-tls
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "chatgpt-web",
|
| 3 |
-
"version": "2.11.
|
| 4 |
"private": false,
|
| 5 |
"description": "ChatGPT Web",
|
| 6 |
"author": "ChenZhaoYu <[email protected]>",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "chatgpt-web",
|
| 3 |
+
"version": "2.11.1",
|
| 4 |
"private": false,
|
| 5 |
"description": "ChatGPT Web",
|
| 6 |
"author": "ChenZhaoYu <[email protected]>",
|
pnpm-lock.yaml
CHANGED
|
@@ -6900,4 +6900,4 @@ packages:
|
|
| 6900 | |
| 6901 |
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
| 6902 |
engines: {node: '>=10'}
|
| 6903 |
-
dev: true
|
|
|
|
| 6900 | |
| 6901 |
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
| 6902 |
engines: {node: '>=10'}
|
| 6903 |
+
dev: true
|
public/favicon.ico
CHANGED
|
|
|
|
public/favicon.svg
CHANGED
|
|
|
|
public/pwa-192x192.png
CHANGED
|
|
public/pwa-512x512.png
CHANGED
|
|
service/.env.example
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
# OpenAI API Key - https://platform.openai.com/overview
|
| 2 |
OPENAI_API_KEY=
|
| 3 |
|
| 4 |
-
# OpenAI API Key ARR - https://platform.openai.com/overview
|
| 5 |
-
OPENAI_API_KEY_ARR=
|
| 6 |
-
|
| 7 |
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
|
| 8 |
OPENAI_ACCESS_TOKEN=
|
| 9 |
|
|
@@ -44,3 +41,4 @@ SOCKS_PROXY_PASSWORD=
|
|
| 44 |
|
| 45 |
# HTTPS PROXY
|
| 46 |
HTTPS_PROXY=
|
|
|
|
|
|
| 1 |
# OpenAI API Key - https://platform.openai.com/overview
|
| 2 |
OPENAI_API_KEY=
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
|
| 5 |
OPENAI_ACCESS_TOKEN=
|
| 6 |
|
|
|
|
| 41 |
|
| 42 |
# HTTPS PROXY
|
| 43 |
HTTPS_PROXY=
|
| 44 |
+
|
service/package.json
CHANGED
|
@@ -44,4 +44,4 @@
|
|
| 44 |
"tsup": "^6.6.3",
|
| 45 |
"typescript": "^4.9.5"
|
| 46 |
}
|
| 47 |
-
}
|
|
|
|
| 44 |
"tsup": "^6.6.3",
|
| 45 |
"typescript": "^4.9.5"
|
| 46 |
}
|
| 47 |
+
}
|
service/pnpm-lock.yaml
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/api/index.ts
CHANGED
|
@@ -63,4 +63,4 @@ export function fetchVerify<T>(token: string) {
|
|
| 63 |
url: '/verify',
|
| 64 |
data: { token },
|
| 65 |
})
|
| 66 |
-
}
|
|
|
|
| 63 |
url: '/verify',
|
| 64 |
data: { token },
|
| 65 |
})
|
| 66 |
+
}
|
src/components/common/PromptStore/index.vue
CHANGED
|
@@ -477,4 +477,4 @@ const dataSource = computed(() => {
|
|
| 477 |
</NButton>
|
| 478 |
</NSpace>
|
| 479 |
</NModal>
|
| 480 |
-
</template>
|
|
|
|
| 477 |
</NButton>
|
| 478 |
</NSpace>
|
| 479 |
</NModal>
|
| 480 |
+
</template>
|
src/components/common/Setting/Advanced.vue
CHANGED
|
@@ -42,7 +42,7 @@ function handleReset() {
|
|
| 42 |
<div class="flex items-center space-x-4">
|
| 43 |
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.temperature') }} </span>
|
| 44 |
<div class="flex-1">
|
| 45 |
-
<NSlider v-model:value="temperature" :max="
|
| 46 |
</div>
|
| 47 |
<span>{{ temperature }}</span>
|
| 48 |
<NButton size="tiny" text type="primary" @click="updateSettings({ temperature })">
|
|
@@ -67,4 +67,4 @@ function handleReset() {
|
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
-
</template>
|
|
|
|
| 42 |
<div class="flex items-center space-x-4">
|
| 43 |
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.temperature') }} </span>
|
| 44 |
<div class="flex-1">
|
| 45 |
+
<NSlider v-model:value="temperature" :max="2" :min="0" :step="0.1" />
|
| 46 |
</div>
|
| 47 |
<span>{{ temperature }}</span>
|
| 48 |
<NButton size="tiny" text type="primary" @click="updateSettings({ temperature })">
|
|
|
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
+
</template>
|
src/components/common/Setting/General.vue
CHANGED
|
@@ -54,8 +54,11 @@ const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
|
| 54 |
]
|
| 55 |
|
| 56 |
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
| 57 |
-
{ label: '
|
|
|
|
| 58 |
{ label: 'English', key: 'en-US', value: 'en-US' },
|
|
|
|
|
|
|
| 59 |
]
|
| 60 |
|
| 61 |
function updateUserInfo(options: Partial<UserInfo>) {
|
|
@@ -219,4 +222,4 @@ function handleImportButtonClick(): void {
|
|
| 219 |
</div>
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
-
</template>
|
|
|
|
| 54 |
]
|
| 55 |
|
| 56 |
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
| 57 |
+
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
|
| 58 |
+
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
|
| 59 |
{ label: 'English', key: 'en-US', value: 'en-US' },
|
| 60 |
+
{ label: '한국어', key: 'ko-KR', value: 'ko-KR' },
|
| 61 |
+
{ label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
|
| 62 |
]
|
| 63 |
|
| 64 |
function updateUserInfo(options: Partial<UserInfo>) {
|
|
|
|
| 222 |
</div>
|
| 223 |
</div>
|
| 224 |
</div>
|
| 225 |
+
</template>
|
src/components/common/Setting/index.vue
CHANGED
|
@@ -67,4 +67,4 @@ const show = computed({
|
|
| 67 |
</NTabs>
|
| 68 |
</div>
|
| 69 |
</NModal>
|
| 70 |
-
</template>
|
|
|
|
| 67 |
</NTabs>
|
| 68 |
</div>
|
| 69 |
</NModal>
|
| 70 |
+
</template>
|
src/router/permission.ts
CHANGED
|
@@ -25,4 +25,4 @@ export function setupPageGuard(router: Router) {
|
|
| 25 |
next()
|
| 26 |
}
|
| 27 |
})
|
| 28 |
-
}
|
|
|
|
| 25 |
next()
|
| 26 |
}
|
| 27 |
})
|
| 28 |
+
}
|
src/store/helper.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createPinia } from 'pinia'
|
| 2 |
+
|
| 3 |
+
export const store = createPinia()
|
src/store/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
import type { App } from 'vue'
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
export const store = createPinia()
|
| 5 |
|
| 6 |
export function setupStore(app: App) {
|
| 7 |
app.use(store)
|
|
|
|
| 1 |
import type { App } from 'vue'
|
| 2 |
+
import { store } from './helper'
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export function setupStore(app: App) {
|
| 5 |
app.use(store)
|
src/store/modules/app/helper.ts
CHANGED
|
@@ -4,7 +4,7 @@ const LOCAL_NAME = 'appSetting'
|
|
| 4 |
|
| 5 |
export type Theme = 'light' | 'dark' | 'auto'
|
| 6 |
|
| 7 |
-
export type Language = '
|
| 8 |
|
| 9 |
export interface AppState {
|
| 10 |
siderCollapsed: boolean
|
|
@@ -13,7 +13,7 @@ export interface AppState {
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export function defaultSetting(): AppState {
|
| 16 |
-
return { siderCollapsed: false, theme: 'light', language: '
|
| 17 |
}
|
| 18 |
|
| 19 |
export function getLocalSetting(): AppState {
|
|
|
|
| 4 |
|
| 5 |
export type Theme = 'light' | 'dark' | 'auto'
|
| 6 |
|
| 7 |
+
export type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ko-KR' | 'ru-RU'
|
| 8 |
|
| 9 |
export interface AppState {
|
| 10 |
siderCollapsed: boolean
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export function defaultSetting(): AppState {
|
| 16 |
+
return { siderCollapsed: false, theme: 'light', language: 'zh-CN' }
|
| 17 |
}
|
| 18 |
|
| 19 |
export function getLocalSetting(): AppState {
|
src/store/modules/app/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import type { AppState, Language, Theme } from './helper'
|
| 3 |
import { getLocalSetting, setLocalSetting } from './helper'
|
| 4 |
-
import { store } from '@/store'
|
| 5 |
|
| 6 |
export const useAppStore = defineStore('app-store', {
|
| 7 |
state: (): AppState => getLocalSetting(),
|
|
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import type { AppState, Language, Theme } from './helper'
|
| 3 |
import { getLocalSetting, setLocalSetting } from './helper'
|
| 4 |
+
import { store } from '@/store/helper'
|
| 5 |
|
| 6 |
export const useAppStore = defineStore('app-store', {
|
| 7 |
state: (): AppState => getLocalSetting(),
|
src/store/modules/auth/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import { getToken, removeToken, setToken } from './helper'
|
| 3 |
-
import { store } from '@/store'
|
| 4 |
import { fetchSession } from '@/api'
|
| 5 |
|
| 6 |
interface SessionResponse {
|
|
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import { getToken, removeToken, setToken } from './helper'
|
| 3 |
+
import { store } from '@/store/helper'
|
| 4 |
import { fetchSession } from '@/api'
|
| 5 |
|
| 6 |
interface SessionResponse {
|
src/store/modules/chat/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
-
import { getLocalState, setLocalState } from './helper'
|
| 3 |
import { router } from '@/router'
|
| 4 |
|
| 5 |
export const useChatStore = defineStore('chat-store', {
|
|
@@ -182,6 +182,11 @@ export const useChatStore = defineStore('chat-store', {
|
|
| 182 |
}
|
| 183 |
},
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
async reloadRoute(uuid?: number) {
|
| 186 |
this.recordState()
|
| 187 |
await router.push({ name: 'Chat', params: { uuid } })
|
|
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
+
import { defaultState, getLocalState, setLocalState } from './helper'
|
| 3 |
import { router } from '@/router'
|
| 4 |
|
| 5 |
export const useChatStore = defineStore('chat-store', {
|
|
|
|
| 182 |
}
|
| 183 |
},
|
| 184 |
|
| 185 |
+
clearHistory() {
|
| 186 |
+
this.$state = { ...defaultState() }
|
| 187 |
+
this.recordState()
|
| 188 |
+
},
|
| 189 |
+
|
| 190 |
async reloadRoute(uuid?: number) {
|
| 191 |
this.recordState()
|
| 192 |
await router.push({ name: 'Chat', params: { uuid } })
|
src/store/modules/settings/helper.ts
CHANGED
|
@@ -27,4 +27,4 @@ export function setLocalState(setting: SettingsState): void {
|
|
| 27 |
|
| 28 |
export function removeLocalState() {
|
| 29 |
ss.remove(LOCAL_NAME)
|
| 30 |
-
}
|
|
|
|
| 27 |
|
| 28 |
export function removeLocalState() {
|
| 29 |
ss.remove(LOCAL_NAME)
|
| 30 |
+
}
|
src/store/modules/settings/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import type { SettingsState } from './helper'
|
| 3 |
-
import { defaultSetting, getLocalState, setLocalState } from './helper'
|
| 4 |
|
| 5 |
export const useSettingStore = defineStore('setting-store', {
|
| 6 |
state: (): SettingsState => getLocalState(),
|
|
@@ -12,11 +12,11 @@ export const useSettingStore = defineStore('setting-store', {
|
|
| 12 |
|
| 13 |
resetSetting() {
|
| 14 |
this.$state = defaultSetting()
|
| 15 |
-
|
| 16 |
},
|
| 17 |
|
| 18 |
recordState() {
|
| 19 |
setLocalState(this.$state)
|
| 20 |
},
|
| 21 |
},
|
| 22 |
-
})
|
|
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
import type { SettingsState } from './helper'
|
| 3 |
+
import { defaultSetting, getLocalState, removeLocalState, setLocalState } from './helper'
|
| 4 |
|
| 5 |
export const useSettingStore = defineStore('setting-store', {
|
| 6 |
state: (): SettingsState => getLocalState(),
|
|
|
|
| 12 |
|
| 13 |
resetSetting() {
|
| 14 |
this.$state = defaultSetting()
|
| 15 |
+
removeLocalState()
|
| 16 |
},
|
| 17 |
|
| 18 |
recordState() {
|
| 19 |
setLocalState(this.$state)
|
| 20 |
},
|
| 21 |
},
|
| 22 |
+
})
|
src/store/modules/user/helper.ts
CHANGED
|
@@ -15,9 +15,9 @@ export interface UserState {
|
|
| 15 |
export function defaultSetting(): UserState {
|
| 16 |
return {
|
| 17 |
userInfo: {
|
| 18 |
-
avatar: 'https://
|
| 19 |
-
name: '
|
| 20 |
-
description: '
|
| 21 |
},
|
| 22 |
}
|
| 23 |
}
|
|
|
|
| 15 |
export function defaultSetting(): UserState {
|
| 16 |
return {
|
| 17 |
userInfo: {
|
| 18 |
+
avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg',
|
| 19 |
+
name: 'ChenZhaoYu',
|
| 20 |
+
description: 'Star on <a href="https://github.com/Chanzhaoyu/chatgpt-bot" class="text-blue-500" target="_blank" >GitHub</a>',
|
| 21 |
},
|
| 22 |
}
|
| 23 |
}
|
src/styles/lib/highlight.less
CHANGED
|
@@ -203,4 +203,4 @@ html {
|
|
| 203 |
.hljs-link {
|
| 204 |
text-decoration: underline
|
| 205 |
}
|
| 206 |
-
}
|
|
|
|
| 203 |
.hljs-link {
|
| 204 |
text-decoration: underline
|
| 205 |
}
|
| 206 |
+
}
|
src/typings/env.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ interface ImportMetaEnv {
|
|
| 5 |
readonly VITE_APP_API_BASE_URL: string;
|
| 6 |
readonly VITE_GLOB_OPEN_LONG_REPLY: string;
|
| 7 |
readonly VITE_GLOB_APP_PWA: string;
|
| 8 |
-
}
|
|
|
|
| 5 |
readonly VITE_APP_API_BASE_URL: string;
|
| 6 |
readonly VITE_GLOB_OPEN_LONG_REPLY: string;
|
| 7 |
readonly VITE_GLOB_APP_PWA: string;
|
| 8 |
+
}
|
src/utils/functions/debounce.ts
CHANGED
|
@@ -15,4 +15,4 @@ export function debounce<T extends unknown[]>(
|
|
| 15 |
clearTimeout(timeoutId)
|
| 16 |
timeoutId = setTimeout(later, wait)
|
| 17 |
}
|
| 18 |
-
}
|
|
|
|
| 15 |
clearTimeout(timeoutId)
|
| 16 |
timeoutId = setTimeout(later, wait)
|
| 17 |
}
|
| 18 |
+
}
|
src/utils/request/index.ts
CHANGED
|
@@ -81,4 +81,4 @@ export function post<T = any>(
|
|
| 81 |
})
|
| 82 |
}
|
| 83 |
|
| 84 |
-
export default post
|
|
|
|
| 81 |
})
|
| 82 |
}
|
| 83 |
|
| 84 |
+
export default post
|
src/utils/storage/index.ts
CHANGED
|
@@ -1 +1,57 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface StorageData<T = any> {
|
| 2 |
+
data: T
|
| 3 |
+
expire: number | null
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export function createLocalStorage(options?: { expire?: number | null }) {
|
| 7 |
+
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
|
| 8 |
+
|
| 9 |
+
const { expire } = Object.assign({ expire: DEFAULT_CACHE_TIME }, options)
|
| 10 |
+
|
| 11 |
+
function set<T = any>(key: string, data: T) {
|
| 12 |
+
const storageData: StorageData<T> = {
|
| 13 |
+
data,
|
| 14 |
+
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const json = JSON.stringify(storageData)
|
| 18 |
+
window.localStorage.setItem(key, json)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function get(key: string) {
|
| 22 |
+
const json = window.localStorage.getItem(key)
|
| 23 |
+
if (json) {
|
| 24 |
+
let storageData: StorageData | null = null
|
| 25 |
+
|
| 26 |
+
try {
|
| 27 |
+
storageData = JSON.parse(json)
|
| 28 |
+
}
|
| 29 |
+
catch {
|
| 30 |
+
// Prevent failure
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if (storageData) {
|
| 34 |
+
const { data, expire } = storageData
|
| 35 |
+
if (expire === null || expire >= Date.now())
|
| 36 |
+
return data
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
remove(key)
|
| 40 |
+
return null
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function remove(key: string) {
|
| 45 |
+
window.localStorage.removeItem(key)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function clear() {
|
| 49 |
+
window.localStorage.clear()
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return { set, get, remove, clear }
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export const ls = createLocalStorage()
|
| 56 |
+
|
| 57 |
+
export const ss = createLocalStorage({ expire: null })
|
src/views/chat/components/Header/index.vue
CHANGED
|
@@ -9,7 +9,7 @@ interface Props {
|
|
| 9 |
|
| 10 |
interface Emit {
|
| 11 |
(ev: 'export'): void
|
| 12 |
-
(ev: '
|
| 13 |
}
|
| 14 |
|
| 15 |
defineProps<Props>()
|
|
@@ -36,8 +36,8 @@ function handleExport() {
|
|
| 36 |
emit('export')
|
| 37 |
}
|
| 38 |
|
| 39 |
-
function
|
| 40 |
-
emit('
|
| 41 |
}
|
| 42 |
</script>
|
| 43 |
|
|
@@ -62,16 +62,16 @@ function toggleUsingContext() {
|
|
| 62 |
{{ currentChatHistory?.title ?? '' }}
|
| 63 |
</h1>
|
| 64 |
<div class="flex items-center space-x-2">
|
| 65 |
-
<HoverButton @click="toggleUsingContext">
|
| 66 |
-
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
|
| 67 |
-
<SvgIcon icon="ri:chat-history-line" />
|
| 68 |
-
</span>
|
| 69 |
-
</HoverButton>
|
| 70 |
<HoverButton @click="handleExport">
|
| 71 |
<span class="text-xl text-[#4f555e] dark:text-white">
|
| 72 |
<SvgIcon icon="ri:download-2-line" />
|
| 73 |
</span>
|
| 74 |
</HoverButton>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
</header>
|
|
|
|
| 9 |
|
| 10 |
interface Emit {
|
| 11 |
(ev: 'export'): void
|
| 12 |
+
(ev: 'handleClear'): void
|
| 13 |
}
|
| 14 |
|
| 15 |
defineProps<Props>()
|
|
|
|
| 36 |
emit('export')
|
| 37 |
}
|
| 38 |
|
| 39 |
+
function handleClear() {
|
| 40 |
+
emit('handleClear')
|
| 41 |
}
|
| 42 |
</script>
|
| 43 |
|
|
|
|
| 62 |
{{ currentChatHistory?.title ?? '' }}
|
| 63 |
</h1>
|
| 64 |
<div class="flex items-center space-x-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
<HoverButton @click="handleExport">
|
| 66 |
<span class="text-xl text-[#4f555e] dark:text-white">
|
| 67 |
<SvgIcon icon="ri:download-2-line" />
|
| 68 |
</span>
|
| 69 |
</HoverButton>
|
| 70 |
+
<HoverButton @click="handleClear">
|
| 71 |
+
<span class="text-xl text-[#4f555e] dark:text-white">
|
| 72 |
+
<SvgIcon icon="ri:delete-bin-line" />
|
| 73 |
+
</span>
|
| 74 |
+
</HoverButton>
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
</header>
|
src/views/chat/components/Message/Text.vue
CHANGED
|
@@ -107,17 +107,14 @@ onUnmounted(() => {
|
|
| 107 |
<div class="text-black" :class="wrapClass">
|
| 108 |
<div ref="textRef" class="leading-relaxed break-words">
|
| 109 |
<div v-if="!inversion">
|
| 110 |
-
<div v-if="!asRawText" class="markdown-body" v-html="text" />
|
| 111 |
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
| 112 |
</div>
|
| 113 |
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
| 114 |
-
<template v-if="loading">
|
| 115 |
-
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
|
| 116 |
-
</template>
|
| 117 |
</div>
|
| 118 |
</div>
|
| 119 |
</template>
|
| 120 |
|
| 121 |
<style lang="less">
|
| 122 |
@import url(./style.less);
|
| 123 |
-
</style>
|
|
|
|
| 107 |
<div class="text-black" :class="wrapClass">
|
| 108 |
<div ref="textRef" class="leading-relaxed break-words">
|
| 109 |
<div v-if="!inversion">
|
| 110 |
+
<div v-if="!asRawText" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
|
| 111 |
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
| 112 |
</div>
|
| 113 |
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
|
|
|
|
|
|
|
|
|
| 114 |
</div>
|
| 115 |
</div>
|
| 116 |
</template>
|
| 117 |
|
| 118 |
<style lang="less">
|
| 119 |
@import url(./style.less);
|
| 120 |
+
</style>
|
src/views/chat/components/Message/index.vue
CHANGED
|
@@ -142,4 +142,4 @@ async function handleCopy() {
|
|
| 142 |
</div>
|
| 143 |
</div>
|
| 144 |
</div>
|
| 145 |
-
</template>
|
|
|
|
| 142 |
</div>
|
| 143 |
</div>
|
| 144 |
</div>
|
| 145 |
+
</template>
|
src/views/chat/components/Message/style.less
CHANGED
|
@@ -57,10 +57,60 @@
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
html.dark {
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
.message-reply {
|
| 65 |
.whitespace-pre-wrap {
|
| 66 |
white-space: pre-wrap;
|
|
@@ -72,4 +122,14 @@ html.dark {
|
|
| 72 |
pre {
|
| 73 |
background-color: #282c34;
|
| 74 |
}
|
| 75 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
| 60 |
+
|
| 61 |
+
&.markdown-body-generate>dd:last-child:after,
|
| 62 |
+
&.markdown-body-generate>dl:last-child:after,
|
| 63 |
+
&.markdown-body-generate>dt:last-child:after,
|
| 64 |
+
&.markdown-body-generate>h1:last-child:after,
|
| 65 |
+
&.markdown-body-generate>h2:last-child:after,
|
| 66 |
+
&.markdown-body-generate>h3:last-child:after,
|
| 67 |
+
&.markdown-body-generate>h4:last-child:after,
|
| 68 |
+
&.markdown-body-generate>h5:last-child:after,
|
| 69 |
+
&.markdown-body-generate>h6:last-child:after,
|
| 70 |
+
&.markdown-body-generate>li:last-child:after,
|
| 71 |
+
&.markdown-body-generate>ol:last-child li:last-child:after,
|
| 72 |
+
&.markdown-body-generate>p:last-child:after,
|
| 73 |
+
&.markdown-body-generate>pre:last-child code:after,
|
| 74 |
+
&.markdown-body-generate>td:last-child:after,
|
| 75 |
+
&.markdown-body-generate>ul:last-child li:last-child:after {
|
| 76 |
+
animation: blink 1s steps(5, start) infinite;
|
| 77 |
+
color: #000;
|
| 78 |
+
content: '_';
|
| 79 |
+
font-weight: 700;
|
| 80 |
+
margin-left: 3px;
|
| 81 |
+
vertical-align: baseline;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
@keyframes blink {
|
| 85 |
+
to {
|
| 86 |
+
visibility: hidden;
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
}
|
| 90 |
|
| 91 |
html.dark {
|
| 92 |
|
| 93 |
+
.markdown-body {
|
| 94 |
+
|
| 95 |
+
&.markdown-body-generate>dd:last-child:after,
|
| 96 |
+
&.markdown-body-generate>dl:last-child:after,
|
| 97 |
+
&.markdown-body-generate>dt:last-child:after,
|
| 98 |
+
&.markdown-body-generate>h1:last-child:after,
|
| 99 |
+
&.markdown-body-generate>h2:last-child:after,
|
| 100 |
+
&.markdown-body-generate>h3:last-child:after,
|
| 101 |
+
&.markdown-body-generate>h4:last-child:after,
|
| 102 |
+
&.markdown-body-generate>h5:last-child:after,
|
| 103 |
+
&.markdown-body-generate>h6:last-child:after,
|
| 104 |
+
&.markdown-body-generate>li:last-child:after,
|
| 105 |
+
&.markdown-body-generate>ol:last-child li:last-child:after,
|
| 106 |
+
&.markdown-body-generate>p:last-child:after,
|
| 107 |
+
&.markdown-body-generate>pre:last-child code:after,
|
| 108 |
+
&.markdown-body-generate>td:last-child:after,
|
| 109 |
+
&.markdown-body-generate>ul:last-child li:last-child:after {
|
| 110 |
+
color: #65a665;
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
.message-reply {
|
| 115 |
.whitespace-pre-wrap {
|
| 116 |
white-space: pre-wrap;
|
|
|
|
| 122 |
pre {
|
| 123 |
background-color: #282c34;
|
| 124 |
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
@media screen and (max-width: 533px) {
|
| 128 |
+
.markdown-body .code-block-wrapper {
|
| 129 |
+
padding: unset;
|
| 130 |
+
|
| 131 |
+
code {
|
| 132 |
+
padding: 24px 16px 16px 16px;
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
src/views/chat/index.vue
CHANGED
|
@@ -93,7 +93,7 @@ async function onConversation() {
|
|
| 93 |
+uuid,
|
| 94 |
{
|
| 95 |
dateTime: new Date().toLocaleString(),
|
| 96 |
-
text: '',
|
| 97 |
loading: true,
|
| 98 |
inversion: false,
|
| 99 |
error: false,
|
|
@@ -469,7 +469,7 @@ onUnmounted(() => {
|
|
| 469 |
v-if="isMobile"
|
| 470 |
:using-context="usingContext"
|
| 471 |
@export="handleExport"
|
| 472 |
-
@
|
| 473 |
/>
|
| 474 |
<main class="flex-1 overflow-hidden">
|
| 475 |
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
|
|
@@ -513,7 +513,7 @@ onUnmounted(() => {
|
|
| 513 |
<footer :class="footerClass">
|
| 514 |
<div class="w-full max-w-screen-xl m-auto">
|
| 515 |
<div class="flex items-center justify-between space-x-2">
|
| 516 |
-
<HoverButton @click="handleClear">
|
| 517 |
<span class="text-xl text-[#4f555e] dark:text-white">
|
| 518 |
<SvgIcon icon="ri:delete-bin-line" />
|
| 519 |
</span>
|
|
@@ -523,7 +523,7 @@ onUnmounted(() => {
|
|
| 523 |
<SvgIcon icon="ri:download-2-line" />
|
| 524 |
</span>
|
| 525 |
</HoverButton>
|
| 526 |
-
<HoverButton
|
| 527 |
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
|
| 528 |
<SvgIcon icon="ri:chat-history-line" />
|
| 529 |
</span>
|
|
@@ -554,4 +554,4 @@ onUnmounted(() => {
|
|
| 554 |
</div>
|
| 555 |
</footer>
|
| 556 |
</div>
|
| 557 |
-
</template>
|
|
|
|
| 93 |
+uuid,
|
| 94 |
{
|
| 95 |
dateTime: new Date().toLocaleString(),
|
| 96 |
+
text: '思考中',
|
| 97 |
loading: true,
|
| 98 |
inversion: false,
|
| 99 |
error: false,
|
|
|
|
| 469 |
v-if="isMobile"
|
| 470 |
:using-context="usingContext"
|
| 471 |
@export="handleExport"
|
| 472 |
+
@handle-clear="handleClear"
|
| 473 |
/>
|
| 474 |
<main class="flex-1 overflow-hidden">
|
| 475 |
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
|
|
|
|
| 513 |
<footer :class="footerClass">
|
| 514 |
<div class="w-full max-w-screen-xl m-auto">
|
| 515 |
<div class="flex items-center justify-between space-x-2">
|
| 516 |
+
<HoverButton v-if="!isMobile" @click="handleClear">
|
| 517 |
<span class="text-xl text-[#4f555e] dark:text-white">
|
| 518 |
<SvgIcon icon="ri:delete-bin-line" />
|
| 519 |
</span>
|
|
|
|
| 523 |
<SvgIcon icon="ri:download-2-line" />
|
| 524 |
</span>
|
| 525 |
</HoverButton>
|
| 526 |
+
<HoverButton @click="toggleUsingContext">
|
| 527 |
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
|
| 528 |
<SvgIcon icon="ri:chat-history-line" />
|
| 529 |
</span>
|
|
|
|
| 554 |
</div>
|
| 555 |
</footer>
|
| 556 |
</div>
|
| 557 |
+
</template>
|
src/views/chat/layout/sider/List.vue
CHANGED
|
@@ -102,4 +102,4 @@ function isActive(uuid: number) {
|
|
| 102 |
</template>
|
| 103 |
</div>
|
| 104 |
</NScrollbar>
|
| 105 |
-
</template>
|
|
|
|
| 102 |
</template>
|
| 103 |
</div>
|
| 104 |
</NScrollbar>
|
| 105 |
+
</template>
|
src/views/chat/layout/sider/index.vue
CHANGED
|
@@ -1,16 +1,19 @@
|
|
| 1 |
<script setup lang='ts'>
|
| 2 |
import type { CSSProperties } from 'vue'
|
| 3 |
import { computed, ref, watch } from 'vue'
|
| 4 |
-
import { NButton, NLayoutSider } from 'naive-ui'
|
| 5 |
import List from './List.vue'
|
| 6 |
import Footer from './Footer.vue'
|
| 7 |
import { useAppStore, useChatStore } from '@/store'
|
| 8 |
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
| 9 |
-
import { PromptStore } from '@/components/common'
|
|
|
|
| 10 |
|
| 11 |
const appStore = useAppStore()
|
| 12 |
const chatStore = useChatStore()
|
| 13 |
|
|
|
|
|
|
|
| 14 |
const { isMobile } = useBasicLayout()
|
| 15 |
const show = ref(false)
|
| 16 |
|
|
@@ -26,6 +29,20 @@ function handleUpdateCollapsed() {
|
|
| 26 |
appStore.setSiderCollapsed(!collapsed.value)
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
const getMobileClass = computed<CSSProperties>(() => {
|
| 30 |
if (isMobile.value) {
|
| 31 |
return {
|
|
@@ -79,9 +96,14 @@ watch(
|
|
| 79 |
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
|
| 80 |
<List />
|
| 81 |
</div>
|
| 82 |
-
<div class="p-4">
|
| 83 |
-
<
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
</NButton>
|
| 86 |
</div>
|
| 87 |
</main>
|
|
@@ -92,4 +114,4 @@ watch(
|
|
| 92 |
<div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
|
| 93 |
</template>
|
| 94 |
<PromptStore v-model:visible="show" />
|
| 95 |
-
</template>
|
|
|
|
| 1 |
<script setup lang='ts'>
|
| 2 |
import type { CSSProperties } from 'vue'
|
| 3 |
import { computed, ref, watch } from 'vue'
|
| 4 |
+
import { NButton, NLayoutSider, useDialog } from 'naive-ui'
|
| 5 |
import List from './List.vue'
|
| 6 |
import Footer from './Footer.vue'
|
| 7 |
import { useAppStore, useChatStore } from '@/store'
|
| 8 |
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
| 9 |
+
import { PromptStore, SvgIcon } from '@/components/common'
|
| 10 |
+
import { t } from '@/locales'
|
| 11 |
|
| 12 |
const appStore = useAppStore()
|
| 13 |
const chatStore = useChatStore()
|
| 14 |
|
| 15 |
+
const dialog = useDialog()
|
| 16 |
+
|
| 17 |
const { isMobile } = useBasicLayout()
|
| 18 |
const show = ref(false)
|
| 19 |
|
|
|
|
| 29 |
appStore.setSiderCollapsed(!collapsed.value)
|
| 30 |
}
|
| 31 |
|
| 32 |
+
function handleClearAll() {
|
| 33 |
+
dialog.warning({
|
| 34 |
+
title: t('chat.deleteMessage'),
|
| 35 |
+
content: t('chat.clearHistoryConfirm'),
|
| 36 |
+
positiveText: t('common.yes'),
|
| 37 |
+
negativeText: t('common.no'),
|
| 38 |
+
onPositiveClick: () => {
|
| 39 |
+
chatStore.clearHistory()
|
| 40 |
+
if (isMobile.value)
|
| 41 |
+
appStore.setSiderCollapsed(true)
|
| 42 |
+
},
|
| 43 |
+
})
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
const getMobileClass = computed<CSSProperties>(() => {
|
| 47 |
if (isMobile.value) {
|
| 48 |
return {
|
|
|
|
| 96 |
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
|
| 97 |
<List />
|
| 98 |
</div>
|
| 99 |
+
<div class="flex items-center p-4 space-x-4">
|
| 100 |
+
<div class="flex-1">
|
| 101 |
+
<NButton block @click="show = true">
|
| 102 |
+
{{ $t('store.siderButton') }}
|
| 103 |
+
</NButton>
|
| 104 |
+
</div>
|
| 105 |
+
<NButton @click="handleClearAll">
|
| 106 |
+
<SvgIcon icon="ri:close-circle-line" />
|
| 107 |
</NButton>
|
| 108 |
</div>
|
| 109 |
</main>
|
|
|
|
| 114 |
<div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
|
| 115 |
</template>
|
| 116 |
<PromptStore v-model:visible="show" />
|
| 117 |
+
</template>
|
start.cmd
CHANGED
|
@@ -6,4 +6,4 @@ echo "Start service complete!"
|
|
| 6 |
cd ..
|
| 7 |
echo "" > front.log
|
| 8 |
start pnpm dev > front.log &
|
| 9 |
-
echo "Start front complete!"
|
|
|
|
| 6 |
cd ..
|
| 7 |
echo "" > front.log
|
| 8 |
start pnpm dev > front.log &
|
| 9 |
+
echo "Start front complete!"
|
vite.config.ts
CHANGED
|
@@ -51,4 +51,4 @@ export default defineConfig((env) => {
|
|
| 51 |
},
|
| 52 |
},
|
| 53 |
}
|
| 54 |
-
})
|
|
|
|
| 51 |
},
|
| 52 |
},
|
| 53 |
}
|
| 54 |
+
})
|