Phoenix 8 months ago
commit 435f6a734f

10
.env

@ -0,0 +1,10 @@
# Glob API URL
VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://114.218.158.24:9020
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false

19982
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,72 @@
{
"name": "chatgpt-web",
"version": "2.11.1",
"private": false,
"description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"vue"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"common:prepare": "husky install"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@vueuse/core": "^9.13.0",
"highlight.js": "^11.7.0",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"vite-plugin-qiankun": "^1.0.15",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.3.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^5.0.12",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
}
}

@ -0,0 +1,16 @@
import request from "@/utils/request/request";
export const getSessionList = (data) => {
return request({
url: '/chat/list',
method: 'post',
data
})
}
export const sessionDetail = (data) => {
return request({
url: '/chat/detail',
method: 'post',
data
})
}

@ -0,0 +1,64 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request'
import { useAuthStore, useSettingStore } from '@/store'
export function fetchChatAPI<T = any>(
prompt: string,
options?: { conversationId?: string; parentMessageId?: string },
signal?: GenericAbortSignal,
) {
return post<T>({
url: '/chat',
data: { prompt, options },
signal,
})
}
export function fetchChatConfig<T = any>() {
return post<T>({
url: '/config',
})
}
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
const settingStore = useSettingStore()
const authStore = useAuthStore()
let data: Record<string, any> = {
prompt: params.prompt,
options: params.options,
}
if (authStore.isChatGPTAPI) {
data = {
...data,
systemMessage: settingStore.systemMessage,
temperature: settingStore.temperature,
top_p: settingStore.top_p,
}
}
return post<T>({
url: '/chat-process',
data,
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchSession<T>() {
return post<T>({
url: '/session',
})
}
export function fetchVerify<T>(token: string) {
return post<T>({
url: '/verify',
data: { token },
})
}

@ -0,0 +1,7 @@
import type { Router } from 'vue-router'
export function setupPageGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
next()
})
}

@ -0,0 +1,25 @@
import axios, { type AxiosResponse } from 'axios'
import { useAuthStore } from '@/store'
const service = axios.create({
baseURL: 'http://114.218.158.24:9020',
})
service.interceptors.request.use(
(config) => {
// const token = useAuthStore().token
const token = "46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644e756eda7154e1af9e70d1c9d2f100823a26885ea6df3249fe619995cb79dc5dbd5ead32d43b955d6b3ce83129097bb21bb8169898f48692de4f966db140c71b85a2065acfc948561c465279fc05194a79a1115f3b00170944b6c4bd6c52ada909a075c55d18d76c2ed2175602421b34b27362a05c350733ed73382471df0a08950f7f1e812a610c17bdac82d82d54be38969f6b41201af79b8d36ef177c5b94b77a297c94f40d4036d2bd3a7ceac5cb29b191ba490d59fc77c794d533c2d078304c7642e8ae7d697ff83e4cc5dd96f685b7c30f18877f2c1672219ba649e17fdfee148c51f63f1875beb019586995f51fd1a51c55e3e62f09727a44181fa46ff"
if (token)
config.headers.Authorization =token
return config
}
)
service.interceptors.response.use(
(response: AxiosResponse): AxiosResponse => {
console.log(response,'response')
return response.data
}
)
export default service

@ -0,0 +1,16 @@
//对axios进行二次封装,将刚才下载好的axios导入进来
import axios from "axios";
const request = axios.create({
baseURL:'http://114.218.158.24:9020',
timeout:5000
});
request.interceptors.request.use((config)=>{
config.headers.Authorization = "46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644e756eda7154e1af9e70d1c9d2f100823a26885ea6df3249fe619995cb79dc5dbd5ead32d43b955d6b3ce83129097bb21bb8169898f48692de4f966db140c71b85a2065acfc948561c465279fc05194a79a1115f3b00170944b6c4bd6c52ada909a075c55d18d76c2ed2175602421b34b27362a05c350733ed73382471df0a08950f7f1e812a610c17bdac82d82d54be38969f6b41201af79b8d36ef177c5b94b77a297c94f40d4036d2bd3a7ceac5cb29b191ba490d59fc77c794d533c2d078304c7642e8ae7d697ff83e4cc5dd96f685b7c30f18877f2c1672219ba649e17fdfee148c51f63f1875beb019586995f51fd1a51c55e3e62f09727a44181fa46ff"
return config;
});
request.interceptors.response.use((res)=>{
return res.data;
});
export default request;

@ -0,0 +1,556 @@
<script setup lang='ts'>
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { NAutoComplete, NButton, NInput, useDialog, useMessage } from 'naive-ui'
import html2canvas from 'html2canvas'
import { Message } from './components'
import { useScroll } from './hooks/useScroll'
import { useChat } from './hooks/useChat'
import { useUsingContext } from './hooks/useUsingContext'
import HeaderComponent from './components/Header/index.vue'
import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useChatStore, usePromptStore } from '@/store'
import { fetchChatAPIProcess } from '@/api'
import { t } from '@/locales'
let controller = new AbortController()
const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
const route = useRoute()
const dialog = useDialog()
const ms = useMessage()
const chatStore = useChatStore()
const { isMobile } = useBasicLayout()
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
const { usingContext, toggleUsingContext } = useUsingContext()
const { uuid } = route.params as { uuid: string }
const dataSources = computed(() => chatStore.getChatByUuid(+uuid))
const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !!item.conversationOptions)))
const prompt = ref<string>('')
const loading = ref<boolean>(false)
const inputRef = ref<Ref | null>(null)
// PromptStore
const promptStore = usePromptStore()
// 使storeToRefsstore
const { promptList: promptTemplate } = storeToRefs<any>(promptStore)
// loading
dataSources.value.forEach((item, index) => {
if (item.loading)
updateChatSome(+uuid, index, { loading: false })
})
function handleSubmit() {
onConversation()
}
async function onConversation() {
let message = prompt.value
if (loading.value)
return
if (!message || message.trim() === '')
return
controller = new AbortController()
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: message,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
},
)
scrollToBottom()
loading.value = true
prompt.value = ''
let options: Chat.ConversationRequest = {}
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions
if (lastContext && usingContext.value)
options = { ...lastContext }
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: t('chat.thinking'),
loading: true,
inversion: false,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
try {
let lastText = ''
const fetchChatAPIOnce = async () => {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
options,
signal: controller.signal,
onDownloadProgress: ({ event }) => {
const xhr = event.target
const { responseText } = xhr
// Always process the final line
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
)
if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
options.parentMessageId = data.id
lastText = data.text
message = ''
return fetchChatAPIOnce()
}
scrollToBottomIfAtBottom()
}
catch (error) {
//
}
},
})
updateChatSome(+uuid, dataSources.value.length - 1, { loading: false })
}
await fetchChatAPIOnce()
}
catch (error: any) {
const errorMessage = error?.message ?? t('common.wrong')
if (error.message === 'canceled') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
loading: false,
},
)
scrollToBottomIfAtBottom()
return
}
const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
if (currentChat?.text && currentChat.text !== '') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
text: `${currentChat.text}\n[${errorMessage}]`,
error: false,
loading: false,
},
)
return
}
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottomIfAtBottom()
}
finally {
loading.value = false
}
}
async function onRegenerate(index: number) {
if (loading.value)
return
controller = new AbortController()
const { requestOptions } = dataSources.value[index]
let message = requestOptions?.prompt ?? ''
let options: Chat.ConversationRequest = {}
if (requestOptions.options)
options = { ...requestOptions.options }
loading.value = true
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: '',
inversion: false,
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
try {
let lastText = ''
const fetchChatAPIOnce = async () => {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
options,
signal: controller.signal,
onDownloadProgress: ({ event }) => {
const xhr = event.target
const { responseText } = xhr
// Always process the final line
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
)
if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
options.parentMessageId = data.id
lastText = data.text
message = ''
return fetchChatAPIOnce()
}
}
catch (error) {
//
}
},
})
updateChatSome(+uuid, index, { loading: false })
}
await fetchChatAPIOnce()
}
catch (error: any) {
if (error.message === 'canceled') {
updateChatSome(
+uuid,
index,
{
loading: false,
},
)
return
}
const errorMessage = error?.message ?? t('common.wrong')
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
}
finally {
loading.value = false
}
}
function handleExport() {
if (loading.value)
return
const d = dialog.warning({
title: t('chat.exportImage'),
content: t('chat.exportImageConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: async () => {
try {
d.loading = true
const ele = document.getElementById('image-wrapper')
const canvas = await html2canvas(ele as HTMLDivElement, {
useCORS: true,
})
const imgUrl = canvas.toDataURL('image/png')
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = imgUrl
tempLink.setAttribute('download', 'chat-shot.png')
if (typeof tempLink.download === 'undefined')
tempLink.setAttribute('target', '_blank')
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(imgUrl)
d.loading = false
ms.success(t('chat.exportSuccess'))
Promise.resolve()
}
catch (error: any) {
ms.error(t('chat.exportFailed'))
}
finally {
d.loading = false
}
},
})
}
function handleDelete(index: number) {
if (loading.value)
return
dialog.warning({
title: t('chat.deleteMessage'),
content: t('chat.deleteMessageConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: () => {
chatStore.deleteChatByUuid(+uuid, index)
},
})
}
function handleClear() {
if (loading.value)
return
dialog.warning({
title: t('chat.clearChat'),
content: t('chat.clearChatConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: () => {
chatStore.clearChatByUuid(+uuid)
},
})
}
function handleEnter(event: KeyboardEvent) {
if (!isMobile.value) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
else {
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault()
handleSubmit()
}
}
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
//
// 使valuevalue()
// key,renderOptionvaluerenderLabel
const searchOptions = computed(() => {
if (prompt.value.startsWith('/')) {
return promptTemplate.value.filter((item: { key: string }) => item.key.toLowerCase().includes(prompt.value.substring(1).toLowerCase())).map((obj: { value: any }) => {
return {
label: obj.value,
value: obj.value,
}
})
}
else {
return []
}
})
// valuekey
const renderOption = (option: { label: string }) => {
for (const i of promptTemplate.value) {
if (i.value === option.label)
return [i.key]
}
return []
}
const placeholder = computed(() => {
if (isMobile.value)
return t('chat.placeholderMobile')
return t('chat.placeholder')
})
const buttonDisabled = computed(() => {
return loading.value || !prompt.value || prompt.value.trim() === ''
})
const footerClass = computed(() => {
let classes = ['p-4']
if (isMobile.value)
classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden']
return classes
})
onMounted(() => {
scrollToBottom()
if (inputRef.value && !isMobile.value)
inputRef.value?.focus()
})
onUnmounted(() => {
if (loading.value)
controller.abort()
})
</script>
<template>
<div class="flex flex-col w-full h-full">
<HeaderComponent
v-if="isMobile"
:using-context="usingContext"
@export="handleExport"
@handle-clear="handleClear"
/>
<main class="flex-1 overflow-hidden">
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
<div
id="image-wrapper"
class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
:class="[isMobile ? 'p-2' : 'p-4']"
>
<template v-if="!dataSources.length">
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
<span>{{ t('chat.newChatTitle') }}</span>
</div>
</template>
<template v-else>
<div>
<Message
v-for="(item, index) of dataSources"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
@regenerate="onRegenerate(index)"
@delete="handleDelete(index)"
/>
<div class="sticky bottom-0 left-0 flex justify-center">
<NButton v-if="loading" type="warning" @click="handleStop">
<template #icon>
<SvgIcon icon="ri:stop-circle-line" />
</template>
{{ t('common.stopResponding') }}
</NButton>
</div>
</div>
</template>
</div>
</div>
</main>
<footer :class="footerClass">
<div class="w-full max-w-screen-xl m-auto">
<div class="flex items-center justify-between space-x-2">
<HoverButton v-if="!isMobile" @click="handleClear">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:delete-bin-line" />
</span>
</HoverButton>
<HoverButton v-if="!isMobile" @click="handleExport">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:download-2-line" />
</span>
</HoverButton>
<HoverButton @click="toggleUsingContext">
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
<SvgIcon icon="ri:chat-history-line" />
</span>
</HoverButton>
<NAutoComplete v-model:value="prompt" :options="searchOptions" :render-label="renderOption">
<template #default="{ handleInput, handleBlur, handleFocus }">
<NInput
ref="inputRef"
v-model:value="prompt"
type="textarea"
:placeholder="placeholder"
:autosize="{ minRows: 1, maxRows: isMobile ? 4 : 8 }"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@keypress="handleEnter"
/>
</template>
</NAutoComplete>
<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
<template #icon>
<span class="dark:text-black">
<SvgIcon icon="ri:send-plane-fill" />
</span>
</template>
</NButton>
</div>
</div>
</footer>
</div>
</template>

@ -0,0 +1,127 @@
<script setup >
import {computed, ref} from 'vue'
import { NInput, NPopconfirm, NScrollbar } from 'naive-ui'
import {getSessionList, sessionDetail} from '@/api/api'
import { SvgIcon } from '@/components/common'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { debounce } from '@/utils/functions/debounce'
const { isMobile } = useBasicLayout()
const appStore = useAppStore()
const chatStore = useChatStore()
const dataSources = computed(() => chatStore.history)
async function handleSelect({ listUuid }) {
currentListUuid.value=listUuid
getSessionDetail()
if (isActive(listUuid)){
return
}
if (chatStore.active)
chatStore.updateHistory(chatStore.active, { isEdit: false })
await chatStore.setActive(listUuid)
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
const dataList=ref([])
const currentListUuid=ref('')
const detailData=ref([])
const getSessionDetail =async () => {
const res= await sessionDetail({listUuid:currentListUuid.value})
if (res.code===0){
detailData.value=res.data
}
}
const getDataList = async () => {
const data = {
page: 1,
pageSize: 9999,
}
const res=await getSessionList(data).then((res)=>{
if (res.code===0){
dataList.value=res.data.data
console.log(dataList.value,'dataList.value')
}
})
}
getDataList()
function handleEdit({ listUuid }, isEdit, event) {
event?.stopPropagation()
chatStore.updateHistory(listUuid, { isEdit })
}
function handleDelete(index, event) {
event?.stopPropagation()
chatStore.deleteHistory(index)
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
const handleDeleteDebounce = debounce(handleDelete, 600)
function handleEnter({ listUuid }, isEdit, event) {
event?.stopPropagation()
if (event.key === 'Enter')
chatStore.updateHistory(listUuid, { isEdit })
}
function isActive(listUuid) {
return chatStore.active === listUuid
}
</script>
<template>
<NScrollbar class="px-4">
<div class="flex flex-col gap-2 text-sm">
<template v-if="!dataList.length">
<div class="flex flex-col items-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" />
<span>{{ $t('common.noData') }}</span>
</div>
</template>
<template v-else>
<div v-for="(item, index) of dataList" :key="item.listUuid">
<a
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group dark:border-neutral-800 dark:hover:bg-[#24272e]"
:class="isActive(item.listUuid) && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]', 'dark:bg-[#24272e]', 'dark:border-[#4b9e5f]', 'pr-14']"
@click="handleSelect(item)"
>
<span>
<SvgIcon icon="ri:message-3-line" />
</span>
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
<NInput
v-if="item.isEdit"
v-model:value="item.title" size="tiny"
@keypress="handleEnter(item, false, $event)"
/>
<span v-else>{{ item.title }}</span>
</div>
<div v-if="isActive(item.listUuid)" class="absolute z-10 flex visible right-1">
<template v-if="item.isEdit">
<button class="p-1" @click="handleEdit(item, false, $event)">
<SvgIcon icon="ri:save-line" />
</button>
</template>
<template v-else>
<button class="p-1">
<SvgIcon icon="ri:edit-line" @click="handleEdit(item, true, $event)" />
</button>
<NPopconfirm placement="bottom" @positive-click="handleDeleteDebounce(index, $event)">
<template #trigger>
<button class="p-1">
<SvgIcon icon="ri:delete-bin-line" />
</button>
</template>
{{ $t('chat.deleteHistoryConfirm') }}
</NPopconfirm>
</template>
</div>
</a>
</div>
</template>
</div>
</NScrollbar>
</template>

@ -0,0 +1,117 @@
<script setup lang='ts'>
import type { CSSProperties } from 'vue'
import { computed, ref, watch } from 'vue'
import { NButton, NLayoutSider, useDialog } from 'naive-ui'
import List from './List.vue'
import Footer from './Footer.vue'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { PromptStore, SvgIcon } from '@/components/common'
import { t } from '@/locales'
const appStore = useAppStore()
const chatStore = useChatStore()
const dialog = useDialog()
const { isMobile } = useBasicLayout()
const show = ref(false)
const collapsed = computed(() => appStore.siderCollapsed)
function handleAdd() {
chatStore.addHistory({ title: t('chat.newChatTitle'), uuid: Date.now(), isEdit: false })
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
function handleClearAll() {
dialog.warning({
title: t('chat.deleteMessage'),
content: t('chat.clearHistoryConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: () => {
chatStore.clearHistory()
if (isMobile.value)
appStore.setSiderCollapsed(true)
},
})
}
const getMobileClass = computed<CSSProperties>(() => {
if (isMobile.value) {
return {
position: 'fixed',
zIndex: 50,
}
}
return {}
})
const mobileSafeArea = computed(() => {
if (isMobile.value) {
return {
paddingBottom: 'env(safe-area-inset-bottom)',
}
}
return {}
})
watch(
isMobile,
(val) => {
appStore.setSiderCollapsed(val)
},
{
immediate: true,
flush: 'post',
},
)
</script>
<template>
<NLayoutSider
:collapsed="collapsed"
:collapsed-width="0"
:width="260"
:show-trigger="isMobile ? false : 'arrow-circle'"
collapse-mode="transform"
position="absolute"
bordered
:style="getMobileClass"
@update-collapsed="handleUpdateCollapsed"
>
<div class="flex flex-col h-full" :style="mobileSafeArea">
<main class="flex flex-col flex-1 min-h-0">
<div class="p-4">
<NButton dashed block @click="handleAdd">
{{ $t('chat.newChatButton') }}
</NButton>
</div>
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
<List />
</div>
<!-- <div class="flex items-center p-4 space-x-4">
<div class="flex-1">
<NButton block @click="show = true">
{{ $t('store.siderButton') }}
</NButton>
</div>
<NButton @click="handleClearAll">
<SvgIcon icon="ri:close-circle-line" />
</NButton>
</div>-->
</main>
<!-- <Footer />-->
</div>
</NLayoutSider>
<template v-if="isMobile">
<div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
</template>
<PromptStore v-model:visible="show" />
</template>

@ -0,0 +1,62 @@
import path from 'path'
import type { PluginOption } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
import qiankun from 'vite-plugin-qiankun'
function setupPlugins(env: ImportMetaEnv): PluginOption[] {
return [
vue(),
env.VITE_GLOB_APP_PWA === 'true' && VitePWA({
injectRegister: 'auto',
manifest: {
name: 'chatGPT',
short_name: 'chatGPT',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
],
},
}),
]
}
export default defineConfig((env) => {
const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv
return {
resolve: {
alias: {
'@': path.resolve(process.cwd(), 'src'),
},
},
plugins: [setupPlugins(viteEnv), qiankun('testapp', {
useDevMode: true,
})],
server: {
host: '0.0.0.0',
port: 1002,
open: false,
proxy: {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
rewrite: path => path.replace('/api/', '/'),
},
},
},
build: {
reportCompressedSize: false,
sourcemap: false,
commonjsOptions: {
ignoreTryCatch: false,
},
target: 'esnext',
lib: {
name: 'testapp',
entry: 'src/main.ts',
formats: ['umd'],
},
},
}
})
Loading…
Cancel
Save