diff --git a/README.md b/README.md index f9638807b..7576f4b69 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,20 @@ events { } http { + resolver 8.8.8.8; + proxy_ssl_server_name on; + access_log off; server_names_hash_bucket_size 512; - client_header_buffer_size 32k; - large_client_header_buffers 4 32k; + client_header_buffer_size 64k; + large_client_header_buffers 4 64k; client_max_body_size 50M; + proxy_connect_timeout 240s; + proxy_read_timeout 240s; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + gzip on; gzip_min_length 1k; gzip_buffers 4 8k; @@ -215,7 +223,7 @@ services: - /root/fast-gpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh - /etc/localtime:/etc/localtime:ro mongodb: - image: mongo:4.0.1 + image: mongo:6.0.4 container_name: mongo restart: always ports: diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index bae160c0c..9c46795c3 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.conf @@ -7,11 +7,19 @@ events { } http { + resolver 8.8.8.8; + proxy_ssl_server_name on; + access_log off; server_names_hash_bucket_size 512; - client_header_buffer_size 32k; - large_client_header_buffers 4 32k; + client_header_buffer_size 64k; + large_client_header_buffers 4 64k; client_max_body_size 50M; + + proxy_connect_timeout 240s; + proxy_read_timeout 240s; + proxy_buffer_size 128k; + proxy_buffers 4 256k; gzip on; gzip_min_length 1k; diff --git a/package.json b/package.json index e010c98bc..000ed0323 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tanstack/react-query": "^4.24.10", "@types/nprogress": "^0.2.0", "axios": "^1.3.3", + "cookie": "^0.5.0", "crypto": "^1.0.1", "dayjs": "^1.11.7", "eventsource-parser": "^0.1.0", @@ -60,6 +61,7 @@ }, "devDependencies": { "@svgr/webpack": "^6.5.1", + "@types/cookie": "^0.5.1", "@types/formidable": "^2.0.5", "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "^4.14.191", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 348796876..60b4cd872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: '@next/font': 13.1.6 '@svgr/webpack': ^6.5.1 '@tanstack/react-query': ^4.24.10 + '@types/cookie': ^0.5.1 '@types/formidable': ^2.0.5 '@types/jsonwebtoken': ^9.0.1 '@types/lodash': ^4.14.191 @@ -26,6 +27,7 @@ specifiers: '@types/react-syntax-highlighter': ^15.5.6 '@types/tunnel': ^0.0.3 axios: ^1.3.3 + cookie: ^0.5.0 crypto: ^1.0.1 dayjs: ^1.11.7 eslint: 8.34.0 @@ -80,6 +82,7 @@ dependencies: '@tanstack/react-query': registry.npmmirror.com/@tanstack/react-query/4.24.10_biqbaboplfbrettd7655fr4n2y '@types/nprogress': registry.npmmirror.com/@types/nprogress/0.2.0 axios: registry.npmmirror.com/axios/1.3.3 + cookie: 0.5.0 crypto: registry.npmmirror.com/crypto/1.0.1 dayjs: registry.npmmirror.com/dayjs/1.11.7 eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0 @@ -116,6 +119,7 @@ dependencies: devDependencies: '@svgr/webpack': registry.npmmirror.com/@svgr/webpack/6.5.1 + '@types/cookie': 0.5.1 '@types/formidable': registry.npmmirror.com/@types/formidable/2.0.5 '@types/jsonwebtoken': registry.npmmirror.com/@types/jsonwebtoken/9.0.1 '@types/lodash': registry.npmmirror.com/@types/lodash/4.14.191 @@ -286,6 +290,15 @@ packages: dev: false optional: true + /@types/cookie/0.5.1: + resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} + dev: true + + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} diff --git a/public/docs/chatProblem.md b/public/docs/chatProblem.md index baa8c22f4..38dab0236 100644 --- a/public/docs/chatProblem.md +++ b/public/docs/chatProblem.md @@ -1,10 +1,14 @@ ### 常见问题 + **请求次数太多了** 一般是因为自己的 openai 账号异常。请先检查自己的账号是否正常使用。 **内容长度** -chatgpt 上下文最长 4096 tokens, 上下文超长时会报错。 +chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部分会被遗忘。 **删除和复制** 电脑端:聊天内容右侧有复制和删除的图标。 移动端:点击对话头像,可以选择复制或删除该条内容。 **代理出错** -服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/) \ No newline at end of file +服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/) +**其他问题** +请 WX 联系: fastgpt123 +![FastGpt](/imgs/wx300.jpg) diff --git a/src/api/chat.ts b/src/api/chat.ts index 04daf14a0..e7eb088db 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -1,18 +1,19 @@ import { GET, POST, DELETE } from './request'; -import type { ChatItemType } from '@/types/chat'; +import type { ChatItemType, HistoryItemType } from '@/types/chat'; import type { InitChatResponse } from './response/chat'; +import { RequestPaging } from '../types/index'; /** * 获取初始化聊天内容 */ -export const getInitChatSiteInfo = (modelId: string, chatId: '' | string) => +export const getInitChatSiteInfo = (modelId: '' | string, chatId: '' | string) => GET(`/chat/init?modelId=${modelId}&chatId=${chatId}`); /** * 获取历史记录 */ -export const getChatHistory = () => - GET<{ _id: string; title: string; modelId: string }[]>('/chat/getHistory'); +export const getChatHistory = (data: RequestPaging) => + POST('/chat/getHistory', data); /** * 删除一条历史记录 diff --git a/src/api/fetch.ts b/src/api/fetch.ts index 0c496c3cd..77ecd53aa 100644 --- a/src/api/fetch.ts +++ b/src/api/fetch.ts @@ -1,4 +1,3 @@ -import { getToken } from '../utils/user'; import { SYSTEM_PROMPT_HEADER, NEW_CHATID_HEADER } from '@/constants/chat'; interface StreamFetchProps { @@ -14,8 +13,7 @@ export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchPr const res = await fetch(url, { method: 'POST', headers: { - 'Content-Type': 'application/json', - Authorization: getToken() || '' + 'Content-Type': 'application/json' }, body: JSON.stringify(data), signal: abortSignal.signal diff --git a/src/api/model.ts b/src/api/model.ts index 05e0b1ca4..eac5469c4 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,13 +1,14 @@ import { GET, POST, DELETE, PUT } from './request'; import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema'; -import { ModelUpdateParams, ShareModelItem } from '@/types/model'; +import type { ModelUpdateParams, ShareModelItem } from '@/types/model'; import { RequestPaging } from '../types/index'; import { Obj2Query } from '@/utils/tools'; +import type { ModelListResponse } from './response/model'; /** * 获取模型列表 */ -export const getMyModels = () => GET('/model/list'); +export const getMyModels = () => GET('/model/list'); /** * 创建一个模型 @@ -100,7 +101,7 @@ export const delOneModelData = (dataId: string) => export const getShareModelList = (data: { searchText?: string } & RequestPaging) => POST(`/model/share/getModels`, data); /** - * 获取收藏的模型 + * 获取我收藏的模型 */ export const getCollectionModels = () => GET(`/model/share/getCollection`); /** diff --git a/src/api/request.ts b/src/api/request.ts index 95059b135..ec1f3c412 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -1,5 +1,5 @@ import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; -import { getToken, clearToken } from '@/utils/user'; +import { clearToken } from '@/utils/user'; import { TOKEN_ERROR_CODE } from '@/service/errorCode'; interface ConfigType { @@ -17,7 +17,7 @@ interface ResponseDataType { */ function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { if (config.headers) { - config.headers.Authorization = getToken(); + // config.headers.Authorization = getToken(); } return config; diff --git a/src/api/response/chat.d.ts b/src/api/response/chat.d.ts index bc8906dc8..c017a1a43 100644 --- a/src/api/response/chat.d.ts +++ b/src/api/response/chat.d.ts @@ -1,12 +1,15 @@ import type { ChatPopulate, ModelSchema } from '@/types/mongoSchema'; import type { ChatItemType } from '@/types/chat'; -export type InitChatResponse = { +export interface InitChatResponse { chatId: string; modelId: string; - name: string; - avatar: string; - intro: string; + model: { + name: string; + avatar: string; + intro: string; + canUse: boolean; + }; chatModel: ModelSchema['chat']['chatModel']; // 对话模型名 history: ChatItemType[]; -}; +} diff --git a/src/api/response/model.d.ts b/src/api/response/model.d.ts new file mode 100644 index 000000000..e79617f5c --- /dev/null +++ b/src/api/response/model.d.ts @@ -0,0 +1,6 @@ +import { ModelListItemType } from '@/types/model'; + +export type ModelListResponse = { + myModels: ModelListItemType[]; + myCollectionModels: ModelListItemType[]; +}; diff --git a/src/api/response/user.d.ts b/src/api/response/user.d.ts index 3bf8ab10c..c135fbb5a 100644 --- a/src/api/response/user.d.ts +++ b/src/api/response/user.d.ts @@ -1,7 +1,6 @@ import type { UserType } from '@/types/user'; import type { PromotionRecordSchema } from '@/types/mongoSchema'; export interface ResLogin { - token: string; user: UserType; } diff --git a/src/components/Icon/icons/back.svg b/src/components/Icon/icons/back.svg new file mode 100644 index 000000000..311943fbf --- /dev/null +++ b/src/components/Icon/icons/back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/board.svg b/src/components/Icon/icons/board.svg deleted file mode 100644 index 74845c417..000000000 --- a/src/components/Icon/icons/board.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/chat.svg b/src/components/Icon/icons/chat.svg new file mode 100644 index 000000000..c83e51a52 --- /dev/null +++ b/src/components/Icon/icons/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/closeSolid.svg b/src/components/Icon/icons/closeSolid.svg new file mode 100644 index 000000000..882daca04 --- /dev/null +++ b/src/components/Icon/icons/closeSolid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/dbModel.svg b/src/components/Icon/icons/dbModel.svg deleted file mode 100644 index b0fe76aa6..000000000 --- a/src/components/Icon/icons/dbModel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/develop.svg b/src/components/Icon/icons/develop.svg index 442958b0f..7eb7b4bb6 100644 --- a/src/components/Icon/icons/develop.svg +++ b/src/components/Icon/icons/develop.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Icon/icons/empty.svg b/src/components/Icon/icons/empty.svg new file mode 100644 index 000000000..d770ec957 --- /dev/null +++ b/src/components/Icon/icons/empty.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Icon/icons/export.svg b/src/components/Icon/icons/export.svg deleted file mode 100644 index 08bf9a3da..000000000 --- a/src/components/Icon/icons/export.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/history.svg b/src/components/Icon/icons/history.svg deleted file mode 100644 index 6f9bbaa66..000000000 --- a/src/components/Icon/icons/history.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/home.svg b/src/components/Icon/icons/home.svg deleted file mode 100644 index c8a412051..000000000 --- a/src/components/Icon/icons/home.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/menu.svg b/src/components/Icon/icons/menu.svg deleted file mode 100644 index 335d82561..000000000 --- a/src/components/Icon/icons/menu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/model.svg b/src/components/Icon/icons/model.svg index f817a33ef..f91cc4083 100644 --- a/src/components/Icon/icons/model.svg +++ b/src/components/Icon/icons/model.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Icon/icons/more.svg b/src/components/Icon/icons/more.svg new file mode 100644 index 000000000..fac518301 --- /dev/null +++ b/src/components/Icon/icons/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/out.svg b/src/components/Icon/icons/out.svg new file mode 100644 index 000000000..8b89944f5 --- /dev/null +++ b/src/components/Icon/icons/out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/pay.svg b/src/components/Icon/icons/pay.svg deleted file mode 100644 index eaf4cdd51..000000000 --- a/src/components/Icon/icons/pay.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/phoneTabbar/chat.svg b/src/components/Icon/icons/phoneTabbar/chat.svg new file mode 100644 index 000000000..73595a4e8 --- /dev/null +++ b/src/components/Icon/icons/phoneTabbar/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/phoneTabbar/me.svg b/src/components/Icon/icons/phoneTabbar/me.svg new file mode 100644 index 000000000..f0023ebed --- /dev/null +++ b/src/components/Icon/icons/phoneTabbar/me.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/phoneTabbar/model.svg b/src/components/Icon/icons/phoneTabbar/model.svg new file mode 100644 index 000000000..4f4f4d354 --- /dev/null +++ b/src/components/Icon/icons/phoneTabbar/model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/phoneTabbar/more.svg b/src/components/Icon/icons/phoneTabbar/more.svg new file mode 100644 index 000000000..b5164ac6a --- /dev/null +++ b/src/components/Icon/icons/phoneTabbar/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/promotion.svg b/src/components/Icon/icons/promotion.svg index b132c3ae4..b71596c89 100644 --- a/src/components/Icon/icons/promotion.svg +++ b/src/components/Icon/icons/promotion.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Icon/icons/share.svg b/src/components/Icon/icons/share.svg deleted file mode 100644 index 2e7899bfd..000000000 --- a/src/components/Icon/icons/share.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Icon/icons/shareMarket.svg b/src/components/Icon/icons/shareMarket.svg index c75ddb304..de4916b4c 100644 --- a/src/components/Icon/icons/shareMarket.svg +++ b/src/components/Icon/icons/shareMarket.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Icon/icons/user.svg b/src/components/Icon/icons/user.svg index b0068fb0d..1c681a441 100644 --- a/src/components/Icon/icons/user.svg +++ b/src/components/Icon/icons/user.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Icon/icons/wx.svg b/src/components/Icon/icons/wx.svg new file mode 100644 index 000000000..f220f0415 --- /dev/null +++ b/src/components/Icon/icons/wx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 9a849bf32..271b52784 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -4,25 +4,28 @@ import { Icon } from '@chakra-ui/react'; const map = { model: require('./icons/model.svg').default, - share: require('./icons/share.svg').default, - home: require('./icons/home.svg').default, - menu: require('./icons/menu.svg').default, - pay: require('./icons/pay.svg').default, copy: require('./icons/copy.svg').default, chatSend: require('./icons/chatSend.svg').default, - board: require('./icons/board.svg').default, develop: require('./icons/develop.svg').default, user: require('./icons/user.svg').default, promotion: require('./icons/promotion.svg').default, delete: require('./icons/delete.svg').default, withdraw: require('./icons/withdraw.svg').default, - dbModel: require('./icons/dbModel.svg').default, - history: require('./icons/history.svg').default, stop: require('./icons/stop.svg').default, shareMarket: require('./icons/shareMarket.svg').default, collectionLight: require('./icons/collectionLight.svg').default, collectionSolid: require('./icons/collectionSolid.svg').default, - export: require('./icons/export.svg').default + chat: require('./icons/chat.svg').default, + empty: require('./icons/empty.svg').default, + back: require('./icons/back.svg').default, + more: require('./icons/more.svg').default, + tabbarChat: require('./icons/phoneTabbar/chat.svg').default, + tabbarModel: require('./icons/phoneTabbar/model.svg').default, + tabbarMore: require('./icons/phoneTabbar/more.svg').default, + tabbarMe: require('./icons/phoneTabbar/me.svg').default, + closeSolid: require('./icons/closeSolid.svg').default, + wx: require('./icons/wx.svg').default, + out: require('./icons/out.svg').default }; export type IconName = keyof typeof map; diff --git a/src/components/Layout/auth.tsx b/src/components/Layout/auth.tsx index 09b5977c5..b36bcfd35 100644 --- a/src/components/Layout/auth.tsx +++ b/src/components/Layout/auth.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { useRouter } from 'next/router'; import { useToast } from '@chakra-ui/react'; import { useUserStore } from '@/store/user'; -import { useGlobalStore } from '@/store/global'; import { useQuery } from '@tanstack/react-query'; const unAuthPage: { [key: string]: boolean } = { @@ -19,15 +18,13 @@ const Auth = ({ children }: { children: JSX.Element }) => { status: 'warning' }); const { userInfo, initUserInfo } = useUserStore(); - const { setLoading } = useGlobalStore(); useQuery( - [router.pathname, userInfo], + [router.pathname], () => { if (unAuthPage[router.pathname] === true || userInfo) { - return setLoading(false); + return null; } else { - setLoading(true); return initUserInfo(); } }, @@ -38,9 +35,6 @@ const Auth = ({ children }: { children: JSX.Element }) => { `/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}` ); toast(); - }, - onSettled() { - setLoading(false); } } ); diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 6fdfcc35c..085198336 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Box, useColorMode, Flex } from '@chakra-ui/react'; import Navbar from './navbar'; import NavbarPhone from './navbarPhone'; @@ -8,53 +8,12 @@ import { useLoading } from '@/hooks/useLoading'; import Auth from './auth'; import { useGlobalStore } from '@/store/global'; -const unShowLayoutRoute: { [key: string]: boolean } = { - '/login': true, - '/chat': true +const pcUnShowLayoutRoute: Record = { + '/login': true }; -const navbarList = [ - { - label: '介绍', - icon: 'board', - link: '/', - activeLink: ['/'] - }, - { - label: '共享', - icon: 'shareMarket', - link: '/model/share', - activeLink: ['/model/share'] - }, - { - label: '模型', - icon: 'model', - link: '/model/list', - activeLink: ['/model/list', '/model/detail'] - }, - - { - label: '账号', - icon: 'user', - link: '/number/setting', - activeLink: ['/number/setting'] - }, - { - label: '邀请', - icon: 'promotion', - link: '/promotion', - activeLink: ['/promotion'] - }, - { - label: '开发', - icon: 'develop', - link: '/openapi', - activeLink: ['/openapi'] - } -]; - -const Layout = ({ children }: { children: JSX.Element }) => { - const { isPc } = useScreen(); +const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => { + const { isPc } = useScreen({ defaultIsPc: isPcDevice }); const router = useRouter(); const { colorMode, setColorMode } = useColorMode(); const { Loading } = useLoading({ defaultLoading: true }); @@ -66,40 +25,60 @@ const Layout = ({ children }: { children: JSX.Element }) => { } }, [colorMode, router.pathname, setColorMode]); + const RenderPc = useCallback( + () => + pcUnShowLayoutRoute[router.pathname] ? ( + {children} + ) : ( + <> + + + + + {children} + + + ), + [children, router.pathname] + ); + + const RenderPhone = useCallback(() => { + const phoneUnShowLayoutRoute: Record = { + '/login': true + }; + + const isChatPage = + router.pathname === '/chat' && Object.values(router.query).join('').length !== 0; + + if (phoneUnShowLayoutRoute[router.pathname] || isChatPage) { + return {children}; + } + return ( + + + {children} + + + + + + ); + }, [children, router]); + return ( <> - {!unShowLayoutRoute[router.pathname] ? ( - - {isPc ? ( - <> - - - - - - {children} - - - - ) : ( - - - - - - {children} - - - )} - - ) : ( - - <>{children} - - )} + + {isPc ? : } + {loading && } ); }; export default Layout; + +Layout.getInitialProps = ({ req }: any) => { + return { + isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent) + }; +}; diff --git a/src/components/Layout/navbar.tsx b/src/components/Layout/navbar.tsx index b904d97d5..b6d0e5da3 100644 --- a/src/components/Layout/navbar.tsx +++ b/src/components/Layout/navbar.tsx @@ -1,78 +1,130 @@ -import React from 'react'; -import { Box, Flex } from '@chakra-ui/react'; -import Image from 'next/image'; +import React, { useMemo } from 'react'; +import { Box, Flex, Image, Tooltip } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import MyIcon from '../Icon'; +import { useUserStore } from '@/store/user'; +import { useChatStore } from '@/store/chat'; + export enum NavbarTypeEnum { normal = 'normal', small = 'small' } -const Navbar = ({ - navbarList -}: { - navbarList: { - label: string; - icon: string; - link: string; - activeLink: string[]; - }[]; -}) => { +const Navbar = () => { const router = useRouter(); + const { userInfo, lastModelId } = useUserStore(); + const { lastChatModelId, lastChatId } = useChatStore(); + const navbarList = useMemo( + () => [ + { + label: '模型', + icon: 'model', + link: `/model?modelId=${lastModelId}`, + activeLink: ['/model'] + }, + { + label: '聊天', + icon: 'chat', + link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`, + activeLink: ['/chat'] + }, + + { + label: '共享', + icon: 'shareMarket', + link: '/model/share', + activeLink: ['/model/share'] + }, + { + label: '邀请', + icon: 'promotion', + link: '/promotion', + activeLink: ['/promotion'] + }, + { + label: '开发', + icon: 'develop', + link: '/openapi', + activeLink: ['/openapi'] + }, + { + label: '账号', + icon: 'user', + link: '/number', + activeLink: ['/number'] + } + ], + [lastChatId, lastChatModelId, lastModelId] + ); return ( {/* logo */} - - + router.push('/number')} + > + {/* 导航列表 */} {navbarList.map((item) => ( - { - if (item.link === router.pathname) return; - router.push(item.link, undefined, { - shallow: true - }); - }} - cursor={'pointer'} - fontSize={'sm'} - w={'60px'} - h={'70px'} - borderRadius={'sm'} - {...(item.activeLink.includes(router.pathname) - ? { - color: '#2B6CB0', - backgroundColor: '#BEE3F8' - } - : { - color: '#4A5568', - backgroundColor: 'transparent' - })} + placement={'right'} + openDelay={100} + gutter={-10} > - - {item.label} - + { + if (item.link === router.asPath) return; + router.push(item.link, undefined, { + shallow: true + }); + }} + cursor={'pointer'} + w={'60px'} + h={'45px'} + _hover={{ + color: '#ffffff' + }} + {...(item.activeLink.includes(router.pathname) + ? { + color: '#ffffff ', + backgroundImage: 'linear-gradient(270deg,#4e83fd,#3370ff)' + } + : { + color: '#9096a5', + backgroundColor: 'transparent' + })} + > + + + ))} diff --git a/src/components/Layout/navbarPhone.tsx b/src/components/Layout/navbarPhone.tsx index 0f7e6806c..7cccb3571 100644 --- a/src/components/Layout/navbarPhone.tsx +++ b/src/components/Layout/navbarPhone.tsx @@ -1,32 +1,41 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useRouter } from 'next/router'; import MyIcon from '../Icon'; -import { - Flex, - Drawer, - DrawerBody, - DrawerFooter, - DrawerOverlay, - DrawerContent, - Box, - useDisclosure, - Button, - Image -} from '@chakra-ui/react'; +import { Flex } from '@chakra-ui/react'; +import { useChatStore } from '@/store/chat'; -const NavbarPhone = ({ - navbarList -}: { - navbarList: { - label: string; - icon: string; - link: string; - activeLink: string[]; - }[]; -}) => { +const NavbarPhone = () => { const router = useRouter(); - - const { isOpen, onClose, onOpen } = useDisclosure(); + const { lastChatModelId, lastChatId } = useChatStore(); + const navbarList = useMemo( + () => [ + { + label: '模型', + icon: 'tabbarModel', + link: `/model`, + activeLink: ['/model'] + }, + { + label: '聊天', + icon: 'tabbarChat', + link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`, + activeLink: ['/chat'] + }, + { + label: '发现', + icon: 'tabbarMore', + link: '/tools', + activeLink: ['/tools'] + }, + { + label: '我', + icon: 'tabbarMe', + link: '/number', + activeLink: ['/number'] + } + ], + [lastChatId, lastChatModelId] + ); return ( <> @@ -36,61 +45,51 @@ const NavbarPhone = ({ justifyContent={'space-between'} backgroundColor={'white'} position={'relative'} - px={7} + px={10} > - - - + {navbarList.map((item) => ( + { + if (item.link === router.asPath) return; + router.push(item.link); + }} + > + + + ))} - - - - - - - - {navbarList.map((item) => ( - { - if (item.link === router.pathname) return; - router.push(item.link); - onClose(); - }} - cursor={'pointer'} - h={'60px'} - borderRadius={'md'} - {...(item.activeLink.includes(router.pathname) - ? { - color: '#2B6CB0', - backgroundColor: '#BEE3F8' - } - : { - color: '#4A5568', - backgroundColor: 'transparent' - })} - > - - {item.label} - - ))} - - - - - - - ); }; diff --git a/src/components/Radio/index.tsx b/src/components/Radio/index.tsx index acc1263d7..6a78860a4 100644 --- a/src/components/Radio/index.tsx +++ b/src/components/Radio/index.tsx @@ -28,16 +28,16 @@ const Radio = ({ list, value, onChange, ...props }: Props) => { ...(value === item.value ? { border: '5px solid', - borderColor: 'blue.500' + borderColor: 'myBlue.700' } : { border: '2px solid', - borderColor: 'gray.200' + borderColor: 'myGray.200' }) }} _hover={{ _before: { - borderColor: 'blue.400' + borderColor: 'myBlue.600' } }} onClick={() => onChange(item.value)} diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx index f2c9f8a5e..1316a34c7 100644 --- a/src/components/Slider/index.tsx +++ b/src/components/Slider/index.tsx @@ -52,7 +52,7 @@ const MySlider = ({ mt={3} fontSize={'sm'} transform={'translateX(-50%)'} - {...(activeVal === item.value ? { color: 'blue.500', fontWeight: 'bold' } : {})} + {...(activeVal === item.value ? { color: 'myBlue.500', fontWeight: 'bold' } : {})} > {item.label} @@ -74,7 +74,7 @@ const MySlider = ({ > - + ); }; diff --git a/src/components/WxConcat/index.tsx b/src/components/WxConcat/index.tsx index 29086bbb6..861911118 100644 --- a/src/components/WxConcat/index.tsx +++ b/src/components/WxConcat/index.tsx @@ -9,23 +9,23 @@ import { ModalFooter, ModalBody, ModalCloseButton, - useColorModeValue + useColorModeValue, + Image } from '@chakra-ui/react'; -import Image from 'next/image'; const WxConcat = ({ onClose }: { onClose: () => void }) => { return ( - + wx交流群 diff --git a/src/constants/theme.ts b/src/constants/theme.ts index 3c3c1cb76..652177dc1 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -1,10 +1,16 @@ -import { extendTheme, defineStyleConfig } from '@chakra-ui/react'; +import { extendTheme, defineStyleConfig, ComponentStyleConfig } from '@chakra-ui/react'; // @ts-ignore -import { modalAnatomy as parts } from '@chakra-ui/anatomy'; +import { modalAnatomy, switchAnatomy, selectAnatomy } from '@chakra-ui/anatomy'; // @ts-ignore import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; -const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts.keys); +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers( + modalAnatomy.keys +); +const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle } = + createMultiStyleConfigHelpers(switchAnatomy.keys); +const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } = + createMultiStyleConfigHelpers(selectAnatomy.keys); // modal 弹窗 const ModalTheme = defineMultiStyleConfig({ @@ -17,46 +23,141 @@ const ModalTheme = defineMultiStyleConfig({ // 按键 const Button = defineStyleConfig({ - baseStyle: {}, + baseStyle: { + _active: { + transform: 'scale(0.98)' + } + }, sizes: { - sm: { + xs: { fontSize: 'xs', px: 3, py: 0, fontWeight: 'normal', + height: '22px', + borderRadius: '2px' + }, + sm: { + fontSize: 'sm', + px: 3, + py: 0, + fontWeight: 'normal', height: '26px', - lineHeight: '26px' + borderRadius: '2px' }, md: { - fontSize: 'sm', + fontSize: 'md', px: 6, py: 0, - height: '34px', - lineHeight: '34px', - fontWeight: 'normal' + height: '32px', + fontWeight: 'normal', + borderRadius: '4px' }, lg: { - fontSize: 'md', + fontSize: 'lg', px: 8, py: 0, height: '42px', - lineHeight: '42px', - fontWeight: 'normal' + fontWeight: 'normal', + borderRadius: '8px' } }, variants: { - white: { - color: '#fff', - backgroundColor: 'transparent', - border: '1px solid #ffffff', + primary: { + background: 'myBlue.700 !important', + color: 'white', _hover: { - backgroundColor: 'rgba(255,255,255,0.1)' + filter: 'brightness(110%)' + } + }, + base: { + color: 'myGray.900', + border: '1px solid', + borderColor: 'myGray.200', + bg: 'transparent', + _hover: { + color: 'myBlue.600' + }, + _active: { + color: 'myBlue.700' } } }, defaultProps: { size: 'md', - colorScheme: 'blue' + variant: 'primary' + } +}); + +const Input: ComponentStyleConfig = { + baseStyle: {}, + variants: { + outline: { + field: { + backgroundColor: 'transparent', + border: '1px solid', + borderRadius: 'base', + borderColor: 'myGray.200', + _focus: { + borderColor: 'myBlue.600', + boxShadow: '0px 0px 4px #A8DBFF', + bg: 'white' + }, + _disabled: { + color: 'myGray.400', + bg: 'myWhite.300' + } + } + } + }, + defaultProps: { + size: 'md', + variant: 'outline' + } +}; + +const Textarea: ComponentStyleConfig = { + variants: { + outline: { + border: '1px solid', + borderRadius: 'base', + borderColor: 'myGray.200', + _focus: { + borderColor: 'myBlue.600', + boxShadow: '0px 0px 4px #A8DBFF' + } + } + }, + + defaultProps: { + size: 'md', + variant: 'outline' + } +}; + +const Switch = switchMultiStyle({ + baseStyle: switchPart({ + track: { + bg: 'myGray.100', + _checked: { + bg: 'myBlue.700' + } + } + }) +}); + +const Select = selectMultiStyle({ + variants: { + outline: selectPart({ + field: { + borderColor: 'myGray.100', + + _focusWithin: { + boxShadow: '0px 0px 4px #A8DBFF', + borderColor: 'myBlue.600' + } + } + }) } }); @@ -65,31 +166,57 @@ export const theme = extendTheme({ styles: { global: { 'html, body': { - color: 'blackAlpha.800', + color: 'myGray.900', + fontSize: 'md', + fontWeight: 400, height: '100%', - maxHeight: '100vh', - overflowY: 'hidden' + overflow: 'hidden' } } }, - fontSizes: { - xs: '0.8rem', - sm: '0.9rem', - md: '1rem', - lg: '1.125rem', - xl: '1.25rem', - '2xl': '1.5rem', - '3xl': '1.875rem', - '4xl': '2.25rem', - '5xl': '3rem', - '6xl': '3.75rem', - '7xl': '4.5rem', - '8xl': '6rem', - '9xl': '8rem' + colors: { + myGray: { + 100: '#EFF0F1', + 200: '#DEE0E2', + 300: '#BDC1C5', + 400: '#9CA2A8', + 500: '#7B838B', + 600: '#5A646E', + 700: '#485058', + 800: '#363C42', + 900: '#24282C', + 1000: '#121416' + }, + myBlue: { + 100: '#f0f7ff', + 200: '#EBF7FD', + 300: '#d6e8ff', + 400: '#adceff', + 500: '#85b1ff', + 600: '#4e83fd', + 700: '#3370ff', + 800: '#2152d9', + 900: '#1237b3', + 1000: '#07228c' + } }, fonts: { body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"' }, + fontSizes: { + xs: '10px', + sm: '12px', + md: '14px', + lg: '16px', + xl: '16px', + '2xl': '18px', + '3xl': '20px' + }, + borders: { + sm: '1px solid #EFF0F1', + base: '1px solid #DEE0E2', + md: '1px solid #BDC1C5' + }, breakpoints: { sm: '900px', md: '1200px', @@ -99,6 +226,10 @@ export const theme = extendTheme({ }, components: { Modal: ModalTheme, - Button + Button, + Input, + Textarea, + Switch, + Select } }); diff --git a/src/hooks/useConfirm.tsx b/src/hooks/useConfirm.tsx index 1986c2e54..02616c28b 100644 --- a/src/hooks/useConfirm.tsx +++ b/src/hooks/useConfirm.tsx @@ -39,7 +39,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont + {models.length > 1 && ( + + + + )} + + )} + + {/* chat history */} + + {history.map((item) => ( + { + if (item._id === chatId) return; + if (isPc) { + router.replace(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + } else { + router.push(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + } + }} + onContextMenu={(e) => onclickContextMenu(e, item)} + > + + + + + {item.title} + + + {formatTimeToChatTime(item.updateTime)} + + + + {item.latestChat || '……'} + + + {/* phone quick delete */} + {!isPc && ( + { + e.stopPropagation(); + setIsLoading(true); + try { + await onclickDelHistory(item._id); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }} + /> + )} + + ))} + {!isLoadingHistory && history.length === 0 && ( + + + + 还没有聊天记录 + + + )} + + {/* context menu */} + {contextMenuData && ( + + + + + { + setIsLoading(true); + try { + await onclickDelHistory(contextMenuData.history._id); + if (contextMenuData.history._id === chatId) { + router.replace(`/chat?modelId=${modelId}`); + } + } catch (error) { + console.log(error); + } + setIsLoading(false); + }} + > + 删除记录 + + onclickExportChat('html')}>导出HTML格式 + onclickExportChat('pdf')}>导出PDF格式 + onclickExportChat('md')}>导出Markdown格式 + + + + )} + + + + ); +}; + +export default PcSliderBar; diff --git a/src/pages/chat/components/ModelList.tsx b/src/pages/chat/components/ModelList.tsx new file mode 100644 index 000000000..12386d25f --- /dev/null +++ b/src/pages/chat/components/ModelList.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Box, Flex, Image } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import { ModelListItemType } from '@/types/model'; + +const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId: string }) => { + const router = useRouter(); + + return ( + + {models.map((item) => ( + + { + if (item._id === modelId) return; + router.replace(`/chat?modelId=${item._id}`); + }} + > + + + + {item.name} + + + {item.systemPrompt || '这个模型没有提示词~'} + + + + + ))} + + ); +}; + +export default ModelList; diff --git a/src/pages/chat/components/PhoneSliderBar.tsx b/src/pages/chat/components/PhoneSliderBar.tsx new file mode 100644 index 000000000..3aadb0eb0 --- /dev/null +++ b/src/pages/chat/components/PhoneSliderBar.tsx @@ -0,0 +1,194 @@ +import React, { useMemo } from 'react'; +import { AddIcon, ChatIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Flex, + Divider, + useDisclosure, + useColorMode, + useColorModeValue, + Image +} from '@chakra-ui/react'; +import { useUserStore } from '@/store/user'; +import { useQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import MyIcon from '@/components/Icon'; +import WxConcat from '@/components/WxConcat'; +import { delChatHistoryById } from '@/api/chat'; +import { useChatStore } from '@/store/chat'; + +const PhoneSliderBar = ({ + chatId, + modelId, + onClose +}: { + chatId: string; + modelId: string; + onClose: () => void; +}) => { + const router = useRouter(); + const { colorMode, toggleColorMode } = useColorMode(); + const { myModels, myCollectionModels, loadMyModels } = useUserStore(); + const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure(); + + const models = useMemo( + () => [...myModels, ...myCollectionModels], + [myCollectionModels, myModels] + ); + useQuery(['loadModels'], () => loadMyModels(false)); + + const { history, loadHistory } = useChatStore(); + useQuery(['loadingHistory'], () => loadHistory({ pageNum: 1 })); + + const RenderButton = ({ + onClick, + children + }: { + onClick: () => void; + children: JSX.Element | string; + }) => ( + + + {children} + + + ); + + return ( + + + AI助手 + {/* 新对话 */} + + + {/* 我的模型 & 历史记录 折叠框*/} + + + {models.map((item) => ( + { + if (item._id === modelId) return; + router.replace(`/chat?modelId=${item._id}`); + onClose(); + }} + > + {''} + + {item.name} + + + ))} + + + <> + 历史记录 + {history.map((item) => ( + { + if (item._id === chatId) return; + router.replace(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + onClose(); + }} + > + + + {item.title} + + + { + e.stopPropagation(); + console.log(111); + await delChatHistoryById(item._id); + loadHistory({ pageNum: 1, init: true }); + if (item._id === chatId) { + router.replace(`/chat?modelId=${modelId}`); + } + }} + /> + + + ))} + + + + + + router.push('/')}> + <> + + 退出聊天 + + + + <> + + 交流群 + + + + {/* wx 联系 */} + {isOpenWx && } + + ); +}; + +export default PhoneSliderBar; diff --git a/src/pages/chat/components/SlideBar.tsx b/src/pages/chat/components/SlideBar.tsx deleted file mode 100644 index 495a76e0d..000000000 --- a/src/pages/chat/components/SlideBar.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import React, { useRef, useEffect, useMemo, useCallback } from 'react'; -import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons'; -import { - Box, - Button, - Accordion, - AccordionItem, - AccordionButton, - AccordionPanel, - AccordionIcon, - Flex, - Divider, - IconButton, - useDisclosure, - useColorMode, - useColorModeValue, - Menu, - MenuButton, - MenuList, - MenuItem -} from '@chakra-ui/react'; -import { useUserStore } from '@/store/user'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { useRouter } from 'next/router'; -import { getToken } from '@/utils/user'; -import MyIcon from '@/components/Icon'; -import WxConcat from '@/components/WxConcat'; -import { getChatHistory, delChatHistoryById } from '@/api/chat'; -import { getCollectionModels } from '@/api/model'; -import type { ChatSiteItemType } from '../index'; -import { fileDownload } from '@/utils/file'; -import { htmlTemplate } from '@/constants/common'; - -const SlideBar = ({ - chatId, - modelId, - history, - resetChat, - onClose -}: { - chatId: string; - modelId: string; - history: ChatSiteItemType[]; - resetChat: (modelId?: string, chatId?: string) => void; - onClose: () => void; -}) => { - const router = useRouter(); - const { colorMode, toggleColorMode } = useColorMode(); - const { myModels, getMyModels } = useUserStore(); - const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure(); - const preChatId = useRef('chatId'); // 用于校验上一次chatId的情况,判断是否需要刷新历史记录 - - const { isSuccess, refetch: fetchMyModels } = useQuery(['getMyModels'], getMyModels, { - cacheTime: 5 * 60 * 1000, - enabled: false - }); - - const { data: collectionModels = [], refetch: fetchCollectionModels } = useQuery( - [getCollectionModels], - getCollectionModels, - { - cacheTime: 5 * 60 * 1000, - enabled: false - } - ); - - const models = useMemo(() => { - const myModelList = myModels.map((item) => ({ - id: item._id, - name: item.name, - icon: 'model' as any - })); - const collectionList = collectionModels - .map((item) => ({ - id: item._id, - name: item.name, - icon: 'collectionSolid' as any - })) - .filter((model) => !myModelList.find((item) => item.id === model.id)); - - return myModelList.concat(collectionList); - }, [collectionModels, myModels]); - - const { data: chatHistory = [], mutate: loadChatHistory } = useMutation({ - mutationFn: getChatHistory - }); - - // update history - useEffect(() => { - if (chatId && preChatId.current === '') { - loadChatHistory(); - } - preChatId.current = chatId; - }, [chatId, loadChatHistory]); - - // init history - useEffect(() => { - setTimeout(() => { - fetchMyModels(); - fetchCollectionModels(); - loadChatHistory(); - }, 1000); - }, [fetchCollectionModels, fetchMyModels, loadChatHistory]); - - /** - * export md - */ - const onclickExportMd = useCallback(() => { - fileDownload({ - text: history.map((item) => item.value).join('\n'), - type: 'text/markdown', - filename: 'chat.md' - }); - }, [history]); - - const getHistoryHtml = useCallback(() => { - const historyDom = document.getElementById('history'); - if (!historyDom) return; - const dom = Array.from(historyDom.children).map((child, i) => { - const avatar = ``; - - const chatContent = child.querySelector('.markdown'); - - if (!chatContent) { - return ''; - } - - const chatContentClone = chatContent.cloneNode(true) as HTMLDivElement; - - const codeHeader = chatContentClone.querySelectorAll('.code-header'); - codeHeader.forEach((childElement: any) => { - childElement.remove(); - }); - - return `
- ${avatar} - ${chatContentClone.outerHTML} -
`; - }); - const html = htmlTemplate.replace('{{CHAT_CONTENT}}', dom.join('\n')); - return html; - }, []); - - const onclickExportHtml = useCallback(() => { - const html = getHistoryHtml(); - html && - fileDownload({ - text: html, - type: 'text/html', - filename: '聊天记录.html' - }); - }, [getHistoryHtml]); - - const onclickExportPdf = useCallback(() => { - const html = getHistoryHtml(); - - html && - // @ts-ignore - html2pdf(html, { - margin: 0, - filename: `聊天记录.pdf` - }); - }, [getHistoryHtml]); - - const RenderHistory = () => ( - <> - {chatHistory.map((item) => ( - { - if (item._id === chatId) return; - preChatId.current = 'chatId'; - resetChat(item.modelId, item._id); - onClose(); - }} - > - - - {item.title} - - - } - variant={'unstyled'} - aria-label={'edit'} - size={'xs'} - onClick={async (e) => { - e.stopPropagation(); - - await delChatHistoryById(item._id); - loadChatHistory(); - if (item._id === chatId) { - resetChat(); - } - }} - /> - - - ))} - - ); - - const RenderButton = ({ - onClick, - children - }: { - onClick: () => void; - children: JSX.Element | string; - }) => ( - - - {children} - - - ); - - return ( - - {/* 新对话 */} - {getToken() && ( - - )} - {/* 我的模型 & 历史记录 折叠框*/} - - {isSuccess && ( - <> - - {models.map((item) => ( - { - if (item.id === modelId) return; - resetChat(item.id); - onClose(); - }} - > - - - {item.name} - - - ))} - - - )} - - - - - 历史记录 - - - - - - - - - - - - - {history.length > 0 && ( - - - - 导出聊天 - - - HTML格式 - PDF格式 - Markdown格式 - - - )} - - router.push('/')}> - <> - - 首页 - - - - router.push('/number/setting')}> - <> - - 充值 - - - - - - 交流群 - - : } - aria-label={''} - variant={'outline'} - w={'16px'} - colorScheme={'white'} - _hover={{ - backgroundColor: 'rgba(255,255,255,0.2)' - }} - onClick={toggleColorMode} - /> - - - {/* wx 联系 */} - {isOpenWx && } - - ); -}; - -export default SlideBar; diff --git a/src/pages/chat/index.module.scss b/src/pages/chat/index.module.scss index f039eccdc..4dea32235 100644 --- a/src/pages/chat/index.module.scss +++ b/src/pages/chat/index.module.scss @@ -9,3 +9,18 @@ transform: scale(1.2); } } + +.newChat { + .modelList { + height: 0; + border-radius: 6px; + overflow: hidden; + } + &:hover { + .modelList { + height: 50vh; + box-shadow: 0 0 5px rgba($color: #000000, $alpha: 0.05); + border: 1px solid #dee0e2; + } + } +} diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 04dc078ac..1be11422b 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -1,16 +1,16 @@ import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; import { useRouter } from 'next/router'; -import { getInitChatSiteInfo, delChatRecordByIndex, postSaveChat } from '@/api/chat'; -import type { InitChatResponse } from '@/api/response/chat'; -import type { ChatItemType } from '@/types/chat'; +import { + getInitChatSiteInfo, + delChatRecordByIndex, + postSaveChat, + delChatHistoryById +} from '@/api/chat'; +import type { ChatSiteItemType, ExportChatType } from '@/types/chat'; import { Textarea, Box, Flex, - useDisclosure, - Drawer, - DrawerOverlay, - DrawerContent, useColorModeValue, Menu, MenuButton, @@ -22,38 +22,46 @@ import { ModalOverlay, ModalContent, ModalBody, - ModalCloseButton + ModalCloseButton, + useDisclosure, + Drawer, + DrawerOverlay, + DrawerContent } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; import { useScreen } from '@/hooks/useScreen'; import { useQuery } from '@tanstack/react-query'; -import { OpenAiChatEnum } from '@/constants/model'; import dynamic from 'next/dynamic'; -import { useGlobalStore } from '@/store/global'; import { useCopyData } from '@/utils/tools'; import { streamFetch } from '@/api/fetch'; import MyIcon from '@/components/Icon'; import { throttle } from 'lodash'; import { Types } from 'mongoose'; import Markdown from '@/components/Markdown'; -import { HUMAN_ICON, LOGO_ICON } from '@/constants/chat'; +import { LOGO_ICON } from '@/constants/chat'; +import { useChatStore } from '@/store/chat'; +import { useLoading } from '@/hooks/useLoading'; +import { fileDownload } from '@/utils/file'; +import { htmlTemplate } from '@/constants/common'; +import { useUserStore } from '@/store/user'; -const SlideBar = dynamic(() => import('./components/SlideBar')); +const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar')); +const History = dynamic(() => import('./components/History')); const Empty = dynamic(() => import('./components/Empty')); import styles from './index.module.scss'; const textareaMinH = '22px'; -export type ChatSiteItemType = { - status: 'loading' | 'finish'; -} & ChatItemType; - -interface ChatType extends InitChatResponse { - history: ChatSiteItemType[]; -} - -const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { +const Chat = ({ + modelId, + chatId, + isPcDevice +}: { + modelId: string; + chatId: string; + isPcDevice: boolean; +}) => { const router = useRouter(); const ChatBox = useRef(null); @@ -61,31 +69,34 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { // 中断请求 const controller = useRef(new AbortController()); - const isResetPage = useRef(false); - - const [chatData, setChatData] = useState({ - chatId, - modelId, - name: '', - avatar: '/icon/logo.png', - intro: '', - chatModel: OpenAiChatEnum.GPT35, - history: [] - }); // 聊天框整体数据 + const isLeavePage = useRef(false); const [inputVal, setInputVal] = useState(''); // user input prompt const [showSystemPrompt, setShowSystemPrompt] = useState(''); + const { + lastChatModelId, + setLastChatModelId, + lastChatId, + setLastChatId, + loadHistory, + chatData, + setChatData, + forbidLoadChatData, + setForbidLoadChatData + } = useChatStore(); + const isChatting = useMemo( () => chatData.history[chatData.history.length - 1]?.status === 'loading', [chatData.history] ); - const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { toast } = useToast(); const { copyData } = useCopyData(); - const { isPc, media } = useScreen(); - const { setLoading } = useGlobalStore(); + const { isPc } = useScreen({ defaultIsPc: isPcDevice }); + const { Loading, setIsLoading } = useLoading(); + const { userInfo } = useUserStore(); + const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); // 滚动到底部 const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => { @@ -122,83 +133,13 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { }, 100); }, []); - // 获取对话信息 - const loadChatInfo = useCallback( - async ({ - modelId, - chatId, - isLoading = false, - isScroll = false - }: { - modelId: string; - chatId: string; - isLoading?: boolean; - isScroll?: boolean; - }) => { - isLoading && setLoading(true); - try { - const res = await getInitChatSiteInfo(modelId, chatId); - - setChatData({ - ...res, - history: res.history.map((item) => ({ - ...item, - status: 'finish' - })) - }); - if (isScroll && res.history.length > 0) { - setTimeout(() => { - scrollToBottom('auto'); - }, 1000); - } - } catch (e: any) { - toast({ - title: e?.message || '获取对话信息异常,请检查地址', - status: 'error', - isClosable: true, - duration: 5000 - }); - router.back(); - } - setLoading(false); - return null; - }, - [router, scrollToBottom, setLoading, toast] - ); - - // 重载新的对话 - const resetChat = useCallback( - async (modelId = chatData.modelId, chatId = '') => { - // 强制中断流 - isResetPage.current = true; - controller.current?.abort(); - - try { - router.replace(`/chat?modelId=${modelId}&chatId=${chatId}`); - loadChatInfo({ - modelId, - chatId, - isLoading: true, - isScroll: true - }); - } catch (error: any) { - toast({ - title: error?.message || '生成新对话失败', - status: 'warning' - }); - } - onCloseSlider(); - }, - [chatData.modelId, loadChatInfo, onCloseSlider, router, toast] - ); - // gpt 对话 const gptChatPrompt = useCallback( async (prompts: ChatSiteItemType[]) => { // create abort obj const abortSignal = new AbortController(); controller.current = abortSignal; - isResetPage.current = false; + isLeavePage.current = false; const prompt = { obj: prompts[0].obj, @@ -230,7 +171,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { }); // 重置了页面,说明退出了当前聊天, 不缓存任何内容 - if (isResetPage.current) { + if (isLeavePage.current) { return; } @@ -255,7 +196,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { ] }); if (newChatId) { + setForbidLoadChatData(true); router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`); + loadHistory({ pageNum: 1, init: true }); } } catch (err) { toast({ @@ -280,7 +223,16 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { }) })); }, - [chatId, generatingMessage, modelId, router, toast] + [ + chatId, + setForbidLoadChatData, + generatingMessage, + loadHistory, + modelId, + router, + setChatData, + toast + ] ); /** @@ -351,12 +303,21 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { history: newChatList.slice(0, newChatList.length - 2) })); } - }, [isChatting, inputVal, chatData.history, resetInputVal, toast, scrollToBottom, gptChatPrompt]); + }, [ + isChatting, + inputVal, + chatData.history, + setChatData, + resetInputVal, + toast, + scrollToBottom, + gptChatPrompt + ]); // 删除一句话 const delChatRecord = useCallback( async (index: number, id: string) => { - setLoading(true); + setIsLoading(true); try { // 删除数据库最后一句 await delChatRecordByIndex(chatId, id); @@ -368,9 +329,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { } catch (err) { console.log(err); } - setLoading(false); + setIsLoading(false); }, - [chatId, setLoading] + [chatId, setChatData, setIsLoading] ); // 复制内容 @@ -382,20 +343,172 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { [copyData] ); - // 初始化聊天框 - useQuery(['init'], () => - loadChatInfo({ - modelId, - chatId, - isLoading: true, - isScroll: true - }) + // export chat data + const onclickExportChat = useCallback( + (type: ExportChatType) => { + const getHistoryHtml = () => { + const historyDom = document.getElementById('history'); + if (!historyDom) return; + + const dom = Array.from(historyDom.children).map((child, i) => { + const avatar = ``; + + const chatContent = child.querySelector('.markdown'); + + if (!chatContent) { + return ''; + } + + const chatContentClone = chatContent.cloneNode(true) as HTMLDivElement; + + const codeHeader = chatContentClone.querySelectorAll('.code-header'); + codeHeader.forEach((childElement: any) => { + childElement.remove(); + }); + + return `
+ ${avatar} + ${chatContentClone.outerHTML} +
`; + }); + + const html = htmlTemplate.replace('{{CHAT_CONTENT}}', dom.join('\n')); + return html; + }; + + const map: Record void> = { + md: () => { + fileDownload({ + text: chatData.history.map((item) => item.value).join('\n\n'), + type: 'text/markdown', + filename: 'chat.md' + }); + }, + html: () => { + const html = getHistoryHtml(); + html && + fileDownload({ + text: html, + type: 'text/html', + filename: '聊天记录.html' + }); + }, + pdf: () => { + const html = getHistoryHtml(); + + html && + // @ts-ignore + html2pdf(html, { + margin: 0, + filename: `聊天记录.pdf` + }); + } + }; + + map[type](); + }, + [chatData.history] ); - // 更新流中断对象 + // delete history and reload history + const onclickDelHistory = useCallback( + async (historyId: string) => { + await delChatHistoryById(historyId); + loadHistory({ pageNum: 1, init: true }); + }, + [loadHistory] + ); + + // 获取对话信息 + const loadChatInfo = useCallback( + async ({ + modelId, + chatId, + isLoading = false + }: { + modelId: string; + chatId: string; + isLoading?: boolean; + }) => { + isLoading && setIsLoading(true); + try { + const res = await getInitChatSiteInfo(modelId, chatId); + + setChatData({ + ...res, + history: res.history.map((item) => ({ + ...item, + status: 'finish' + })) + }); + if (res.history.length > 0) { + setTimeout(() => { + scrollToBottom('auto'); + }, 300); + } + + // 空 modelId 请求, 重定向到新的 model 聊天 + if (res.modelId !== modelId) { + setForbidLoadChatData(true); + router.replace(`/chat?modelId=${res.modelId}`); + } + } catch (e: any) { + // reset all chat tore + setLastChatModelId(''); + setLastChatId(''); + setChatData(); + loadHistory({ pageNum: 1, init: true }); + router.replace('/chat'); + } + setIsLoading(false); + return null; + }, + [ + router, + loadHistory, + setForbidLoadChatData, + scrollToBottom, + setChatData, + setIsLoading, + setLastChatId, + setLastChatModelId + ] + ); + // 初始化聊天框 + const { isLoading } = useQuery(['init', modelId, chatId], () => { + // pc: redirect to latest model chat + if (!modelId && lastChatModelId) { + router.replace(`/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`); + return null; + } + + // store id + modelId && setLastChatModelId(modelId); + setLastChatId(chatId); + + // focus scroll bottom + chatId && scrollToBottom('auto'); + + /* get mode and chat into ↓ */ + + // phone: history page + if (!isPc && Object.keys(router.query).length === 0) return null; + if (forbidLoadChatData) { + setForbidLoadChatData(false); + return null; + } + + return loadChatInfo({ + modelId, + chatId + }); + }); + useEffect(() => { return () => { - isResetPage.current = true; + isLeavePage.current = true; controller.current?.abort(); }; }, []); @@ -403,239 +516,286 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { return ( - {isPc ? ( - - + ) : ( - + - - - - {chatData?.name} + + {chatData.model.name} + + + + + + router.replace(`/chat?modelId=${modelId}`)}> + 新对话 + + { + try { + setIsLoading(true); + await onclickDelHistory(chatData.chatId); + router.replace(`/chat`); + } catch (err) { + console.log(err); + } + setIsLoading(false); + }} + > + 删除记录 + + onclickExportChat('html')}>导出HTML格式 + onclickExportChat('pdf')}>导出PDF格式 + onclickExportChat('md')}>导出Markdown格式 + + - + )} - - {/* 聊天内容 */} - - {chatData.history.map((item, index) => ( - - - - - avatar - - - onclickCopy(item.value)}>复制 - delChatRecord(index, item._id)}>删除该行 - - - - {item.obj === 'AI' ? ( - <> - + {chatData.history.map((item, index) => ( + + + + + avatar - {item.systemPrompt && ( - + + + {chatData.model.canUse && ( + router.push(`/model?modelId=${chatData.modelId}`)}> + 模型详情 + )} - - ) : ( - - {item.value} - - )} - - {isPc && ( - - + onclickCopy(item.value)}>复制 + delChatRecord(index, item._id)}>删除该行 + + + + {item.obj === 'AI' ? ( + <> + + {item.systemPrompt && ( + + )} + + ) : ( + + {item.value} + + )} + + {isPc && ( + + + onclickCopy(item.value)} + /> + onclickCopy(item.value)} + color={'blackAlpha.700'} + _hover={{ + color: 'red.600' + }} + onClick={() => delChatRecord(index, item._id)} /> - - delChatRecord(index, item._id)} - /> - - )} - - - ))} - {chatData.history.length === 0 && ( - - )} -
- {/* 发送区 */} - - - {/* 输入框 */} -