From d31bdf0ee081f28aeb611c0d09c76c32e5786707 Mon Sep 17 00:00:00 2001
From: archer <545436317@qq.com>
Date: Sun, 14 May 2023 21:37:26 +0800
Subject: [PATCH] feat: share chat page
---
src/api/chat.ts | 31 +-
src/api/response/chat.d.ts | 10 +
src/components/Layout/index.tsx | 8 +-
src/components/Loading/index.tsx | 2 +-
src/constants/model.ts | 7 +
src/pages/_error.tsx | 11 +-
src/pages/api/chat/shareChat/chat.ts | 130 +++
src/pages/api/chat/shareChat/create.ts | 39 +
src/pages/api/chat/shareChat/delete.ts | 29 +
src/pages/api/chat/shareChat/init.ts | 59 ++
src/pages/api/chat/shareChat/list.ts | 43 +
src/pages/chat/components/Empty.tsx | 32 +-
src/pages/chat/components/ShareHistory.tsx | 203 ++++
src/pages/chat/share.tsx | 930 ++++++++++++++++++
.../detail/components/ModelEditForm.tsx | 296 +++++-
src/service/events/pushBill.ts | 19 +-
src/service/models/shareChat.ts | 38 +
src/service/mongo.ts | 1 +
src/service/utils/auth.ts | 51 +-
src/store/chat.ts | 141 ++-
src/types/chat.d.ts | 15 +-
src/types/model.d.ts | 6 +
src/types/mongoSchema.d.ts | 11 +
23 files changed, 2047 insertions(+), 65 deletions(-)
create mode 100644 src/pages/api/chat/shareChat/chat.ts
create mode 100644 src/pages/api/chat/shareChat/create.ts
create mode 100644 src/pages/api/chat/shareChat/delete.ts
create mode 100644 src/pages/api/chat/shareChat/init.ts
create mode 100644 src/pages/api/chat/shareChat/list.ts
create mode 100644 src/pages/chat/components/ShareHistory.tsx
create mode 100644 src/pages/chat/share.tsx
create mode 100644 src/service/models/shareChat.ts
diff --git a/src/api/chat.ts b/src/api/chat.ts
index 25bb230a4..93c755663 100644
--- a/src/api/chat.ts
+++ b/src/api/chat.ts
@@ -1,7 +1,10 @@
import { GET, POST, DELETE } from './request';
import type { ChatItemType, HistoryItemType } from '@/types/chat';
-import type { InitChatResponse } from './response/chat';
+import type { InitChatResponse, InitShareChatResponse } from './response/chat';
import { RequestPaging } from '../types/index';
+import type { ShareChatSchema } from '@/types/mongoSchema';
+import type { ShareChatEditType } from '@/types/model';
+import { Obj2Query } from '@/utils/tools';
/**
* 获取初始化聊天内容
@@ -35,3 +38,29 @@ export const postSaveChat = (data: {
*/
export const delChatRecordByIndex = (chatId: string, contentId: string) =>
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
+
+/**
+ * create a shareChat
+ */
+export const createShareChat = (
+ data: ShareChatEditType & {
+ modelId: string;
+ }
+) => POST(`/chat/shareChat/create`, data);
+
+/**
+ * get shareChat
+ */
+export const getShareChatList = (modelId: string) =>
+ GET(`/chat/shareChat/list?modelId=${modelId}`);
+
+/**
+ * delete a shareChat
+ */
+export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`);
+
+/**
+ * 初始化分享聊天
+ */
+export const initShareChatInfo = (data: { shareId: string; password: string }) =>
+ GET(`/chat/shareChat/init?${Obj2Query(data)}`);
diff --git a/src/api/response/chat.d.ts b/src/api/response/chat.d.ts
index c017a1a43..c7d8c344e 100644
--- a/src/api/response/chat.d.ts
+++ b/src/api/response/chat.d.ts
@@ -13,3 +13,13 @@ export interface InitChatResponse {
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
history: ChatItemType[];
}
+
+export interface InitShareChatResponse {
+ maxContext: number;
+ model: {
+ name: string;
+ avatar: string;
+ intro: string;
+ };
+ chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
+}
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 949947bb5..fea9ae15c 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -10,11 +10,13 @@ import NavbarPhone from './navbarPhone';
const pcUnShowLayoutRoute: Record = {
'/': true,
- '/login': true
+ '/login': true,
+ '/chat/share': true
};
const phoneUnShowLayoutRoute: Record = {
'/': true,
- '/login': true
+ '/login': true,
+ '/chat/share': true
};
const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => {
@@ -67,7 +69,7 @@ const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: b
)}
- {loading && }
+
>
);
};
diff --git a/src/components/Loading/index.tsx b/src/components/Loading/index.tsx
index 1352013fd..ff9489d18 100644
--- a/src/components/Loading/index.tsx
+++ b/src/components/Loading/index.tsx
@@ -5,7 +5,7 @@ const Loading = ({ fixed = true }: { fixed?: boolean }) => {
return (
- {statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}
-
- );
+function Error({ errStr }: { errStr: string }) {
+ return {errStr}
;
}
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
- const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
console.log(err);
- return { statusCode };
+ return { errStr: JSON.stringify(err) };
};
export default Error;
diff --git a/src/pages/api/chat/shareChat/chat.ts b/src/pages/api/chat/shareChat/chat.ts
new file mode 100644
index 000000000..3014c565c
--- /dev/null
+++ b/src/pages/api/chat/shareChat/chat.ts
@@ -0,0 +1,130 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { connectToDatabase } from '@/service/mongo';
+import { authShareChat } from '@/service/utils/auth';
+import { modelServiceToolMap } from '@/service/utils/chat';
+import { ChatItemSimpleType } from '@/types/chat';
+import { jsonRes } from '@/service/response';
+import { PassThrough } from 'stream';
+import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
+import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
+import { resStreamResponse } from '@/service/utils/chat';
+import { searchKb } from '@/service/plugins/searchKb';
+import { ChatRoleEnum } from '@/constants/chat';
+
+/* 发送提示词 */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ let step = 0; // step=1 时,表示开始了流响应
+ const stream = new PassThrough();
+ stream.on('error', () => {
+ console.log('error: ', 'stream error');
+ stream.destroy();
+ });
+ res.on('close', () => {
+ stream.destroy();
+ });
+ res.on('error', () => {
+ console.log('error: ', 'request error');
+ stream.destroy();
+ });
+
+ try {
+ const { shareId, password, historyId, prompts } = req.body as {
+ prompts: ChatItemSimpleType[];
+ password: string;
+ shareId: string;
+ historyId: string;
+ };
+
+ if (!historyId || !prompts) {
+ throw new Error('分享链接无效');
+ }
+
+ await connectToDatabase();
+ let startTime = Date.now();
+
+ const { model, showModelDetail, userOpenAiKey, systemAuthKey, userId } = await authShareChat({
+ shareId,
+ password
+ });
+
+ const modelConstantsData = ChatModelMap[model.chat.chatModel];
+
+ // 使用了知识库搜索
+ if (model.chat.useKb) {
+ const { code, searchPrompts } = await searchKb({
+ userOpenAiKey,
+ prompts,
+ similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
+ model,
+ userId
+ });
+
+ // search result is empty
+ if (code === 201) {
+ return res.send(searchPrompts[0]?.value);
+ }
+
+ prompts.splice(prompts.length - 3, 0, ...searchPrompts);
+ } else {
+ // 没有用知识库搜索,仅用系统提示词
+ model.chat.systemPrompt &&
+ prompts.splice(prompts.length - 3, 0, {
+ obj: ChatRoleEnum.System,
+ value: model.chat.systemPrompt
+ });
+ }
+
+ // 计算温度
+ const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
+ 2
+ );
+
+ // 发出请求
+ const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
+ apiKey: userOpenAiKey || systemAuthKey,
+ temperature: +temperature,
+ messages: prompts,
+ stream: true,
+ res,
+ chatId: historyId
+ });
+
+ console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
+
+ step = 1;
+
+ const { totalTokens, finishMessages } = await resStreamResponse({
+ model: model.chat.chatModel,
+ res,
+ stream,
+ chatResponse: streamResponse,
+ prompts,
+ systemPrompt: ''
+ });
+
+ /* bill */
+ pushChatBill({
+ isPay: !userOpenAiKey,
+ chatModel: model.chat.chatModel,
+ userId,
+ textLen: finishMessages.map((item) => item.value).join('').length,
+ tokens: totalTokens
+ });
+ updateShareChatBill({
+ shareId,
+ tokens: totalTokens
+ });
+ } catch (err: any) {
+ if (step === 1) {
+ // 直接结束流
+ console.log('error,结束');
+ stream.destroy();
+ } else {
+ res.status(500);
+ jsonRes(res, {
+ code: 500,
+ error: err
+ });
+ }
+ }
+}
diff --git a/src/pages/api/chat/shareChat/create.ts b/src/pages/api/chat/shareChat/create.ts
new file mode 100644
index 000000000..4d4617d16
--- /dev/null
+++ b/src/pages/api/chat/shareChat/create.ts
@@ -0,0 +1,39 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { jsonRes } from '@/service/response';
+import { connectToDatabase, ShareChat } from '@/service/mongo';
+import { authModel, authToken } from '@/service/utils/auth';
+import type { ShareChatEditType } from '@/types/model';
+
+/* create a shareChat */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const { modelId, name, maxContext, password } = req.body as ShareChatEditType & {
+ modelId: string;
+ };
+
+ await connectToDatabase();
+
+ const userId = await authToken(req);
+ await authModel({
+ modelId,
+ userId
+ });
+
+ const { _id } = await ShareChat.create({
+ userId,
+ modelId,
+ name,
+ password,
+ maxContext
+ });
+
+ jsonRes(res, {
+ data: _id
+ });
+ } catch (err) {
+ jsonRes(res, {
+ code: 500,
+ error: err
+ });
+ }
+}
diff --git a/src/pages/api/chat/shareChat/delete.ts b/src/pages/api/chat/shareChat/delete.ts
new file mode 100644
index 000000000..736d7f761
--- /dev/null
+++ b/src/pages/api/chat/shareChat/delete.ts
@@ -0,0 +1,29 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { jsonRes } from '@/service/response';
+import { connectToDatabase, ShareChat } from '@/service/mongo';
+import { authToken } from '@/service/utils/auth';
+
+/* delete a shareChat by shareChatId */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const { id } = req.query as {
+ id: string;
+ };
+
+ await connectToDatabase();
+
+ const userId = await authToken(req);
+
+ await ShareChat.findOneAndRemove({
+ _id: id,
+ userId
+ });
+
+ jsonRes(res);
+ } catch (err) {
+ jsonRes(res, {
+ code: 500,
+ error: err
+ });
+ }
+}
diff --git a/src/pages/api/chat/shareChat/init.ts b/src/pages/api/chat/shareChat/init.ts
new file mode 100644
index 000000000..548f65500
--- /dev/null
+++ b/src/pages/api/chat/shareChat/init.ts
@@ -0,0 +1,59 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { jsonRes } from '@/service/response';
+import { connectToDatabase, ShareChat } from '@/service/mongo';
+import type { InitShareChatResponse } from '@/api/response/chat';
+import { authModel } from '@/service/utils/auth';
+import { hashPassword } from '@/service/utils/tools';
+
+/* 初始化我的聊天框,需要身份验证 */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ let { shareId, password = '' } = req.query as {
+ shareId: string;
+ password: string;
+ };
+
+ if (!shareId) {
+ throw new Error('params is error');
+ }
+
+ await connectToDatabase();
+
+ // get shareChat
+ const shareChat = await ShareChat.findById(shareId);
+
+ if (!shareChat) {
+ throw new Error('分享链接已失效');
+ }
+
+ if (shareChat.password !== hashPassword(password)) {
+ return jsonRes(res, {
+ code: 501,
+ message: '密码不正确'
+ });
+ }
+
+ // 校验使用权限
+ const { model } = await authModel({
+ modelId: shareChat.modelId,
+ userId: String(shareChat.userId)
+ });
+
+ jsonRes(res, {
+ data: {
+ maxContext: shareChat.maxContext,
+ model: {
+ name: model.name,
+ avatar: model.avatar,
+ intro: model.share.intro
+ },
+ chatModel: model.chat.chatModel
+ }
+ });
+ } catch (err) {
+ jsonRes(res, {
+ code: 500,
+ error: err
+ });
+ }
+}
diff --git a/src/pages/api/chat/shareChat/list.ts b/src/pages/api/chat/shareChat/list.ts
new file mode 100644
index 000000000..2a3894e8b
--- /dev/null
+++ b/src/pages/api/chat/shareChat/list.ts
@@ -0,0 +1,43 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { jsonRes } from '@/service/response';
+import { connectToDatabase, ShareChat } from '@/service/mongo';
+import { authToken } from '@/service/utils/auth';
+import { hashPassword } from '@/service/utils/tools';
+
+/* get shareChat list by modelId */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const { modelId } = req.query as {
+ modelId: string;
+ };
+
+ await connectToDatabase();
+
+ const userId = await authToken(req);
+
+ const data = await ShareChat.find({
+ modelId,
+ userId
+ }).sort({
+ _id: -1
+ });
+
+ const blankPassword = hashPassword('');
+
+ jsonRes(res, {
+ data: data.map((item) => ({
+ _id: item._id,
+ name: item.name,
+ password: item.password === blankPassword ? '' : '1',
+ tokens: item.tokens,
+ maxContext: item.maxContext,
+ lastTime: item.lastTime
+ }))
+ });
+ } catch (err) {
+ jsonRes(res, {
+ code: 500,
+ error: err
+ });
+ }
+}
diff --git a/src/pages/chat/components/Empty.tsx b/src/pages/chat/components/Empty.tsx
index ccde333a5..9a686d3dc 100644
--- a/src/pages/chat/components/Empty.tsx
+++ b/src/pages/chat/components/Empty.tsx
@@ -26,21 +26,23 @@ const Empty = ({
alignItems={'center'}
justifyContent={'center'}
>
-
-
-
-
- {name}
-
-
- {intro}
-
+ {name && (
+
+
+
+
+ {name}
+
+
+ {intro}
+
+ )}
{/* version intro */}
diff --git a/src/pages/chat/components/ShareHistory.tsx b/src/pages/chat/components/ShareHistory.tsx
new file mode 100644
index 000000000..3c1c960bc
--- /dev/null
+++ b/src/pages/chat/components/ShareHistory.tsx
@@ -0,0 +1,203 @@
+import React, { useCallback, useRef, useState } from 'react';
+import type { MouseEvent } from 'react';
+import { AddIcon } from '@chakra-ui/icons';
+import {
+ Box,
+ Button,
+ Flex,
+ useTheme,
+ Menu,
+ MenuList,
+ MenuItem,
+ useOutsideClick
+} from '@chakra-ui/react';
+import { ChatIcon } from '@chakra-ui/icons';
+import { useRouter } from 'next/router';
+import { formatTimeToChatTime } from '@/utils/tools';
+import MyIcon from '@/components/Icon';
+import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
+import { useChatStore } from '@/store/chat';
+import { useScreen } from '@/hooks/useScreen';
+
+import styles from '../index.module.scss';
+
+const PcSliderBar = ({
+ isPcDevice,
+ onclickDelHistory,
+ onclickExportChat,
+ onCloseSlider
+}: {
+ isPcDevice: boolean;
+ onclickDelHistory: (historyId: string) => void;
+ onclickExportChat: (type: ExportChatType) => void;
+ onCloseSlider: () => void;
+}) => {
+ const router = useRouter();
+ const { shareId = '', historyId = '' } = router.query as { shareId: string; historyId: string };
+ const theme = useTheme();
+ const { isPc } = useScreen({ defaultIsPc: isPcDevice });
+
+ const ContextMenuRef = useRef(null);
+
+ const [contextMenuData, setContextMenuData] = useState<{
+ left: number;
+ top: number;
+ history: ShareChatHistoryItemType;
+ }>();
+
+ const { shareChatHistory } = useChatStore();
+
+ // close contextMenu
+ useOutsideClick({
+ ref: ContextMenuRef,
+ handler: () =>
+ setTimeout(() => {
+ setContextMenuData(undefined);
+ })
+ });
+
+ const onclickContextMenu = useCallback(
+ (e: MouseEvent, history: ShareChatHistoryItemType) => {
+ e.preventDefault(); // 阻止默认右键菜单
+
+ if (!isPc) return;
+
+ setContextMenuData({
+ left: e.clientX + 15,
+ top: e.clientY + 10,
+ history
+ });
+ },
+ [isPc]
+ );
+
+ const replaceChatPage = useCallback(
+ ({ hId = '', shareId }: { hId?: string; shareId: string }) => {
+ if (hId === historyId) return;
+
+ router.replace(`/chat/share?shareId=${shareId}&historyId=${hId}`);
+ !isPc && onCloseSlider();
+ },
+ [historyId, isPc, onCloseSlider, router]
+ );
+
+ return (
+
+ {/* 新对话 */}
+
+ }
+ onClick={() => replaceChatPage({ shareId })}
+ >
+ 新对话
+
+
+
+ {/* chat history */}
+
+ {shareChatHistory.map((item) => (
+ replaceChatPage({ hId: item._id, shareId: item.shareId })}
+ onContextMenu={(e) => onclickContextMenu(e, item)}
+ >
+
+
+
+
+ {item.title}
+
+
+ {formatTimeToChatTime(item.updateTime)}
+
+
+
+ {item.latestChat || '……'}
+
+
+ {/* phone quick delete */}
+ {!isPc && (
+ {
+ e.stopPropagation();
+ onclickDelHistory(item._id);
+ item._id === historyId && replaceChatPage({ shareId: item.shareId });
+ }}
+ />
+ )}
+
+ ))}
+ {shareChatHistory.length === 0 && (
+
+
+
+ 还没有聊天记录
+
+
+ )}
+
+ {/* context menu */}
+ {contextMenuData && (
+
+
+
+
+ )}
+
+ );
+};
+
+export default PcSliderBar;
diff --git a/src/pages/chat/share.tsx b/src/pages/chat/share.tsx
new file mode 100644
index 000000000..0b30c6cca
--- /dev/null
+++ b/src/pages/chat/share.tsx
@@ -0,0 +1,930 @@
+import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react';
+import { useRouter } from 'next/router';
+import { initShareChatInfo } from '@/api/chat';
+import type { ChatSiteItemType, ExportChatType } from '@/types/chat';
+import {
+ Textarea,
+ Box,
+ Flex,
+ useColorModeValue,
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ Image,
+ Button,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+ Drawer,
+ DrawerOverlay,
+ DrawerContent,
+ Card,
+ Tooltip,
+ useOutsideClick,
+ useTheme,
+ Input,
+ ModalFooter,
+ ModalHeader
+} from '@chakra-ui/react';
+import { useToast } from '@/hooks/useToast';
+import { useScreen } from '@/hooks/useScreen';
+import { useQuery } from '@tanstack/react-query';
+import dynamic from 'next/dynamic';
+import { useCopyData, voiceBroadcast } 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 { 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';
+import Loading from '@/components/Loading';
+
+const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
+ loading: () => ,
+ ssr: false
+});
+const Empty = dynamic(() => import('./components/Empty'), {
+ loading: () => ,
+ ssr: false
+});
+
+import styles from './index.module.scss';
+
+const textareaMinH = '22px';
+
+const Chat = ({
+ shareId,
+ historyId,
+ isPcDevice
+}: {
+ shareId: string;
+ historyId: string;
+ isPcDevice: boolean;
+}) => {
+ const hasVoiceApi = !!window.speechSynthesis;
+ const router = useRouter();
+ const theme = useTheme();
+
+ const ChatBox = useRef(null);
+ const TextareaDom = useRef(null);
+ const ContextMenuRef = useRef(null);
+ const PhoneContextShow = useRef(false);
+
+ // 中断请求
+ const controller = useRef(new AbortController());
+ const isLeavePage = useRef(false);
+
+ const [inputVal, setInputVal] = useState(''); // user input prompt
+ const [showSystemPrompt, setShowSystemPrompt] = useState('');
+ const [messageContextMenuData, setMessageContextMenuData] = useState<{
+ // message messageContextMenuData
+ left: number;
+ top: number;
+ message: ChatSiteItemType;
+ }>();
+ const [foldSliderBar, setFoldSlideBar] = useState(false);
+
+ const {
+ password,
+ setPassword,
+ shareChatHistory,
+ delShareHistoryById,
+ setShareChatHistory,
+ shareChatData,
+ setShareChatData,
+ delShareChatHistoryItemById,
+ delShareChatHistory
+ } = useChatStore();
+
+ const isChatting = useMemo(
+ () => shareChatData.history[shareChatData.history.length - 1]?.status === 'loading',
+ [shareChatData.history]
+ );
+
+ const { toast } = useToast();
+ const { copyData } = useCopyData();
+ const { isPc } = useScreen({ defaultIsPc: isPcDevice });
+ const { Loading, setIsLoading } = useLoading();
+ const { userInfo } = useUserStore();
+ const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
+ const {
+ isOpen: isOpenPassword,
+ onClose: onClosePassword,
+ onOpen: onOpenPassword
+ } = useDisclosure();
+
+ // close contextMenu
+ useOutsideClick({
+ ref: ContextMenuRef,
+ handler: () => {
+ // 移动端长按后会将其设置为true,松手时候也会触发一次,松手的时候需要忽略一次。
+ if (PhoneContextShow.current) {
+ PhoneContextShow.current = false;
+ } else {
+ messageContextMenuData &&
+ setTimeout(() => {
+ setMessageContextMenuData(undefined);
+ window.getSelection?.()?.empty?.();
+ window.getSelection?.()?.removeAllRanges?.();
+ document?.getSelection()?.empty();
+ });
+ }
+ }
+ });
+
+ // 滚动到底部
+ const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
+ if (!ChatBox.current) return;
+ ChatBox.current.scrollTo({
+ top: ChatBox.current.scrollHeight,
+ behavior
+ });
+ }, []);
+
+ // 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const generatingMessage = useCallback(
+ throttle(() => {
+ if (!ChatBox.current) return;
+ const isBottom =
+ ChatBox.current.scrollTop + ChatBox.current.clientHeight + 150 >=
+ ChatBox.current.scrollHeight;
+
+ isBottom && scrollToBottom('auto');
+ }, 100),
+ []
+ );
+
+ // 重置输入内容
+ const resetInputVal = useCallback((val: string) => {
+ setInputVal(val);
+ setTimeout(() => {
+ /* 回到最小高度 */
+ if (TextareaDom.current) {
+ TextareaDom.current.style.height =
+ val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
+ }
+ }, 100);
+ }, []);
+
+ // gpt 对话
+ const gptChatPrompt = useCallback(
+ async (prompts: ChatSiteItemType[]) => {
+ // create abort obj
+ const abortSignal = new AbortController();
+ controller.current = abortSignal;
+ isLeavePage.current = false;
+
+ const formatPrompts = prompts.map((item) => ({
+ obj: item.obj,
+ value: item.value
+ }));
+
+ // 流请求,获取数据
+ const { responseText, systemPrompt } = await streamFetch({
+ url: '/api/chat/shareChat/chat',
+ data: {
+ prompts: formatPrompts.slice(-shareChatData.maxContext - 1, -1),
+ password,
+ shareId,
+ historyId
+ },
+ onMessage: (text: string) => {
+ setShareChatData((state) => ({
+ ...state,
+ history: state.history.map((item, index) => {
+ if (index !== state.history.length - 1) return item;
+ return {
+ ...item,
+ value: item.value + text
+ };
+ })
+ }));
+ generatingMessage();
+ },
+ abortSignal
+ });
+
+ // 重置了页面,说明退出了当前聊天, 不缓存任何内容
+ if (isLeavePage.current) {
+ return;
+ }
+
+ let responseHistory: ChatSiteItemType[] = [];
+
+ // 设置聊天内容为完成状态
+ setShareChatData((state) => {
+ responseHistory = state.history.map((item, index) => {
+ if (index !== state.history.length - 1) return item;
+ return {
+ ...item,
+ status: 'finish',
+ systemPrompt
+ };
+ });
+
+ return {
+ ...state,
+ history: responseHistory
+ };
+ });
+
+ setShareChatHistory({
+ historyId,
+ shareId,
+ title: formatPrompts[formatPrompts.length - 2].value,
+ latestChat: responseText,
+ chats: responseHistory
+ });
+
+ setTimeout(() => {
+ generatingMessage();
+ }, 100);
+ },
+ [
+ generatingMessage,
+ historyId,
+ password,
+ setShareChatData,
+ setShareChatHistory,
+ shareChatData.maxContext,
+ shareId
+ ]
+ );
+
+ /**
+ * 发送一个内容
+ */
+ const sendPrompt = useCallback(async () => {
+ if (isChatting) {
+ toast({
+ title: '正在聊天中...请等待结束',
+ status: 'warning'
+ });
+ return;
+ }
+ const storeInput = inputVal;
+ // 去除空行
+ const val = inputVal.trim().replace(/\n\s*/g, '\n');
+
+ if (!val) {
+ toast({
+ title: '内容为空',
+ status: 'warning'
+ });
+ return;
+ }
+
+ const newChatList: ChatSiteItemType[] = [
+ ...shareChatData.history,
+ {
+ _id: String(new Types.ObjectId()),
+ obj: 'Human',
+ value: val,
+ status: 'finish'
+ },
+ {
+ _id: String(new Types.ObjectId()),
+ obj: 'AI',
+ value: '',
+ status: 'loading'
+ }
+ ];
+
+ // 插入内容
+ setShareChatData((state) => ({
+ ...state,
+ history: newChatList
+ }));
+
+ // 清空输入内容
+ resetInputVal('');
+ setTimeout(() => {
+ scrollToBottom();
+ }, 100);
+
+ try {
+ await gptChatPrompt(newChatList);
+ } catch (err: any) {
+ toast({
+ title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
+ status: 'warning',
+ duration: 5000,
+ isClosable: true
+ });
+
+ resetInputVal(storeInput);
+
+ setShareChatData((state) => ({
+ ...state,
+ history: newChatList.slice(0, newChatList.length - 2)
+ }));
+ }
+ }, [
+ isChatting,
+ inputVal,
+ shareChatData.history,
+ setShareChatData,
+ resetInputVal,
+ toast,
+ scrollToBottom,
+ gptChatPrompt
+ ]);
+
+ // 复制内容
+ const onclickCopy = useCallback(
+ (value: string) => {
+ const val = value.replace(/\n+/g, '\n');
+ copyData(val);
+ },
+ [copyData]
+ );
+
+ // 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: shareChatData.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]();
+ },
+ [shareChatData.history]
+ );
+
+ // onclick chat message context
+ const onclickContextMenu = useCallback(
+ (e: MouseEvent, message: ChatSiteItemType) => {
+ e.preventDefault(); // 阻止默认右键菜单
+
+ // select all text
+ const range = document.createRange();
+ range.selectNodeContents(e.currentTarget as HTMLDivElement);
+ window.getSelection()?.removeAllRanges();
+ window.getSelection()?.addRange(range);
+
+ navigator.vibrate?.(50); // 震动 50 毫秒
+
+ if (!isPcDevice) {
+ PhoneContextShow.current = true;
+ }
+
+ setMessageContextMenuData({
+ left: e.clientX - 20,
+ top: e.clientY,
+ message
+ });
+
+ return false;
+ },
+ [isPcDevice]
+ );
+
+ // 获取对话信息
+ const loadChatInfo = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const res = await initShareChatInfo({
+ shareId,
+ password
+ });
+
+ setShareChatData({
+ ...res,
+ history: shareChatHistory.find((item) => item._id === historyId)?.chats || []
+ });
+
+ onClosePassword();
+
+ setTimeout(() => {
+ scrollToBottom();
+ }, 500);
+ } catch (e: any) {
+ toast({
+ status: 'error',
+ title: typeof e === 'string' ? e : e?.message || '初始化异常'
+ });
+ if (e?.code === 501) {
+ onOpenPassword();
+ } else {
+ delShareChatHistory(shareId);
+ router.replace(`/chat/share`);
+ }
+ }
+ setIsLoading(false);
+ return null;
+ }, [
+ setIsLoading,
+ shareId,
+ password,
+ setShareChatData,
+ shareChatHistory,
+ onClosePassword,
+ historyId,
+ scrollToBottom,
+ toast,
+ onOpenPassword,
+ delShareChatHistory,
+ router
+ ]);
+
+ // 初始化聊天框
+ useQuery(['init', historyId], () => {
+ if (!shareId) {
+ return null;
+ }
+
+ if (!historyId) {
+ router.replace(`/chat/share?shareId=${shareId}&historyId=${new Types.ObjectId()}`);
+ return null;
+ }
+
+ return loadChatInfo();
+ });
+
+ // abort stream
+ useEffect(() => {
+ return () => {
+ window.speechSynthesis?.cancel();
+ isLeavePage.current = true;
+ controller.current?.abort();
+ };
+ }, [shareId, historyId]);
+
+ // context menu component
+ const RenderContextMenu = useCallback(
+ ({ history, index }: { history: ChatSiteItemType; index: number }) => (
+
+
+ {hasVoiceApi && (
+
+ )}
+
+
+
+ ),
+ [delShareChatHistoryItemById, hasVoiceApi, historyId, onclickCopy, theme.borders.base]
+ );
+
+ return (
+
+ {/* pc always show history. */}
+ {isPc && (
+ div': { visibility: 'visible', opacity: 1 }
+ }}
+ >
+ setFoldSlideBar(!foldSliderBar)}
+ >
+
+
+
+
+
+
+ )}
+
+ {/* 聊天内容 */}
+
+ {/* chat header */}
+
+ {!isPc && (
+
+ )}
+
+ {shareChatData.model.name}
+ {shareChatData.history.length > 0 ? ` (${shareChatData.history.length})` : ''}
+
+ {shareChatData.history.length > 0 ? (
+
+ ) : (
+
+ )}
+
+ {/* chat content box */}
+
+
+ {shareChatData.history.map((item, index) => (
+
+ {item.obj === 'Human' && }
+ {/* avatar */}
+
+ {/* message */}
+
+ {item.obj === 'AI' ? (
+
+ onclickContextMenu(e, item)}
+ >
+
+ {item.systemPrompt && (
+
+ )}
+
+
+ ) : (
+
+ onclickContextMenu(e, item)}
+ >
+ {item.value}
+
+
+ )}
+
+
+ ))}
+ {shareChatData.history.length === 0 && }
+
+
+ {/* 发送区 */}
+
+
+ {/* 输入框 */}
+
+
+
+
+
+
+ {/* phone slider */}
+ {!isPc && (
+
+
+
+
+
+
+ )}
+ {/* system prompt show modal */}
+ {
+ setShowSystemPrompt('')}>
+
+
+
+
+ {showSystemPrompt}
+
+
+
+ }
+ {/* context menu */}
+ {messageContextMenuData && (
+
+
+
+
+ )}
+ {/* password input */}
+ {
+
+
+
+
+ 安全密码
+
+
+ 密码:
+ setPassword(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ }
+
+ );
+};
+
+Chat.getInitialProps = ({ query, req }: any) => {
+ return {
+ shareId: query?.shareId || '',
+ historyId: query?.historyId || '',
+ isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
+ };
+};
+
+export default Chat;
diff --git a/src/pages/model/components/detail/components/ModelEditForm.tsx b/src/pages/model/components/detail/components/ModelEditForm.tsx
index a3db5a061..922e44efb 100644
--- a/src/pages/model/components/detail/components/ModelEditForm.tsx
+++ b/src/pages/model/components/detail/components/ModelEditForm.tsx
@@ -14,13 +14,30 @@ import {
Tooltip,
Button,
Select,
- Grid,
Switch,
- Image
+ Image,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+ Table,
+ Thead,
+ Tbody,
+ Tfoot,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ IconButton
} from '@chakra-ui/react';
+import { DeleteIcon } from '@chakra-ui/icons';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
-import { UseFormReturn } from 'react-hook-form';
+import { useForm, UseFormReturn } from 'react-hook-form';
import { ChatModelMap, ModelVectorSearchModeMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
@@ -28,6 +45,13 @@ import { useSelectFile } from '@/hooks/useSelectFile';
import { useToast } from '@/hooks/useToast';
import { compressImg } from '@/utils/file';
import { useQuery } from '@tanstack/react-query';
+import { getShareChatList, createShareChat, delShareChatById } from '@/api/chat';
+import { useRouter } from 'next/router';
+import { defaultShareChat } from '@/constants/model';
+import type { ShareChatEditType } from '@/types/model';
+import { formatTimeToChatTime, useCopyData } from '@/utils/tools';
+import MyIcon from '@/components/Icon';
+import { useGlobalStore } from '@/store/global';
const ModelEditForm = ({
formHooks,
@@ -38,16 +62,34 @@ const ModelEditForm = ({
isOwner: boolean;
handleDelModel: () => void;
}) => {
+ const { toast } = useToast();
+ const { modelId } = useRouter().query as { modelId: string };
+ const { setLoading } = useGlobalStore();
+ const [refresh, setRefresh] = useState(false);
+
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该AI助手?'
});
+ const { copyData } = useCopyData();
const { register, setValue, getValues } = formHooks;
- const [refresh, setRefresh] = useState(false);
+ const {
+ register: registerShareChat,
+ getValues: getShareChatValues,
+ setValue: setShareChatValues,
+ handleSubmit: submitShareChat,
+ reset: resetShareChat
+ } = useForm({
+ defaultValues: defaultShareChat
+ });
+ const {
+ isOpen: isOpenCreateShareChat,
+ onOpen: onOpenCreateShareChat,
+ onClose: onCloseCreateShareChat
+ } = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
- const { toast } = useToast();
const onSelectFile = useCallback(
async (e: File[]) => {
@@ -71,10 +113,54 @@ const ModelEditForm = ({
[setValue, toast]
);
- const { data: chatModelList = [] } = useQuery(['init'], getChatModelList);
+ const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
+
+ const { data: shareChatList = [], refetch: refetchShareChatList } = useQuery(
+ ['initShareChatList', modelId],
+ () => getShareChatList(modelId)
+ );
+
+ const onclickCreateShareChat = useCallback(
+ async (e: ShareChatEditType) => {
+ try {
+ setLoading(true);
+ const id = await createShareChat({
+ ...e,
+ modelId
+ });
+ onCloseCreateShareChat();
+ refetchShareChatList();
+
+ const url = `你可以与 ${getValues('name')} 进行对话。
+对话地址为:${location.origin}/chat/share?shareId=${id}
+${e.password ? `密码为: ${e.password}` : ''}`;
+ copyData(url, '已复制分享地址');
+
+ resetShareChat(defaultShareChat);
+ } catch (error) {
+ console.log(error);
+ }
+ setLoading(false);
+ },
+ [
+ copyData,
+ getValues,
+ modelId,
+ onCloseCreateShareChat,
+ refetchShareChatList,
+ resetShareChat,
+ setLoading
+ ]
+ );
+
+ const formatTokens = (tokens: number) => {
+ if (tokens < 10000) return tokens;
+ return `${(tokens / 10000).toFixed(2)}万`;
+ };
return (
<>
+ {/* basic info */}
基本信息
@@ -160,6 +246,7 @@ const ModelEditForm = ({
)}
+ {/* model effect */}
模型效果
@@ -246,13 +333,16 @@ const ModelEditForm = ({
{isOwner && (
-
- 分享设置
-
-
+ <>
+ {/* model share setting */}
+
+ 分享设置
- 模型分享:
+ 模型分享:
+
+
+
{
@@ -260,9 +350,12 @@ const ModelEditForm = ({
setRefresh(!refresh);
}}
/>
-
+
分享模型细节:
+
+
+
{
@@ -282,25 +375,170 @@ const ModelEditForm = ({
/>
-
- Tips
-
-
- 开启模型分享后,你的模型将会出现在共享市场,可供 FastGpt
- 所有用户使用。用户使用时不会消耗你的 tokens,而是消耗使用者的 tokens。
-
- 开启分享详情后,其他用户可以查看该模型的特有数据:温度、提示词和数据集。
+
+ {/* shareChat */}
+
+
+
+ 免登录聊天窗口
+
+
+
+ (Beta)
-
-
-
+
+
+
+
+
+
+ 名称 |
+ 密码 |
+ 最大上下文 |
+ tokens消耗 |
+ 最后使用时间 |
+ |
+
+
+
+ {shareChatList.map((item) => (
+
+ {item.name} |
+ {item.password === '1' ? '已开启' : '未使用'} |
+ {item.maxContext} |
+ {formatTokens(item.tokens)} |
+ {item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'} |
+
+
+ {
+ const url = `${location.origin}/chat/share?shareId=${item._id}`;
+ copyData(url, '已复制分享地址');
+ }}
+ />
+ {
+ setLoading(true);
+ try {
+ await delShareChatById(item._id);
+ refetchShareChatList();
+ } catch (error) {
+ console.log(error);
+ }
+ setLoading(false);
+ }}
+ />
+
+ |
+
+ ))}
+
+
+
+
+ >
)}
+ {/* create shareChat modal */}
+
+
+
+ 创建免登录窗口
+
+
+
+
+
+ 名称:
+
+
+
+
+
+
+
+ 密码:
+
+
+
+
+ 密码不会再次展示,请记住你的密码
+
+
+
+
+
+ 最长上下文(组)
+
+ {
+ setShareChatValues('maxContext', e);
+ setRefresh(!refresh);
+ }}
+ >
+
+ {getShareChatValues('maxContext')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
diff --git a/src/service/events/pushBill.ts b/src/service/events/pushBill.ts
index 3d26c6ad8..d11290ae3 100644
--- a/src/service/events/pushBill.ts
+++ b/src/service/events/pushBill.ts
@@ -1,4 +1,4 @@
-import { connectToDatabase, Bill, User } from '../mongo';
+import { connectToDatabase, Bill, User, ShareChat } from '../mongo';
import { ChatModelMap, OpenAiChatEnum, ChatModelType, embeddingModel } from '@/constants/model';
import { BillTypeEnum } from '@/constants/user';
@@ -55,6 +55,23 @@ export const pushChatBill = async ({
}
};
+export const updateShareChatBill = async ({
+ shareId,
+ tokens
+}: {
+ shareId: string;
+ tokens: number;
+}) => {
+ try {
+ await ShareChat.findByIdAndUpdate(shareId, {
+ $inc: { tokens },
+ lastTime: new Date()
+ });
+ } catch (error) {
+ console.log('update shareChat error', error);
+ }
+};
+
export const pushSplitDataBill = async ({
isPay,
userId,
diff --git a/src/service/models/shareChat.ts b/src/service/models/shareChat.ts
new file mode 100644
index 000000000..b2792d6e6
--- /dev/null
+++ b/src/service/models/shareChat.ts
@@ -0,0 +1,38 @@
+import { Schema, model, models, Model } from 'mongoose';
+import { ShareChatSchema as ShareChatSchemaType } from '@/types/mongoSchema';
+import { hashPassword } from '@/service/utils/tools';
+
+const ShareChatSchema = new Schema({
+ userId: {
+ type: Schema.Types.ObjectId,
+ ref: 'user',
+ required: true
+ },
+ modelId: {
+ type: Schema.Types.ObjectId,
+ ref: 'model',
+ required: true
+ },
+ name: {
+ type: String,
+ required: true
+ },
+ password: {
+ type: String,
+ set: (val: string) => hashPassword(val)
+ },
+ tokens: {
+ type: Number,
+ default: 0
+ },
+ maxContext: {
+ type: Number,
+ default: 20
+ },
+ lastTime: {
+ type: Date
+ }
+});
+
+export const ShareChat: Model =
+ models['shareChat'] || model('shareChat', ShareChatSchema);
diff --git a/src/service/mongo.ts b/src/service/mongo.ts
index 2645eced3..42f9dc54b 100644
--- a/src/service/mongo.ts
+++ b/src/service/mongo.ts
@@ -51,3 +51,4 @@ export * from './models/splitData';
export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';
+export * from './models/shareChat';
diff --git a/src/service/utils/auth.ts b/src/service/utils/auth.ts
index 894633bd3..543089a71 100644
--- a/src/service/utils/auth.ts
+++ b/src/service/utils/auth.ts
@@ -1,7 +1,7 @@
import type { NextApiRequest } from 'next';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
-import { Chat, Model, OpenApi, User } from '../mongo';
+import { Chat, Model, OpenApi, User, ShareChat } from '../mongo';
import type { ModelSchema } from '@/types/mongoSchema';
import type { ChatItemSimpleType } from '@/types/chat';
import mongoose from 'mongoose';
@@ -9,6 +9,7 @@ import { ClaudeEnum, defaultModel } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { ERROR_ENUM } from '../errorCode';
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
+import { hashPassword } from '@/service/utils/tools';
/* 校验 token */
export const authToken = (req: NextApiRequest): Promise => {
@@ -113,8 +114,8 @@ export const authModel = async ({
1. authOwner=true or authUser = true , just owner can use
2. authUser = false and share, anyone can use
*/
- if ((authOwner || (authUser && !model.share.isShare)) && userId !== String(model.userId)) {
- return Promise.reject(ERROR_ENUM.unAuthModel);
+ if (authOwner || (authUser && !model.share.isShare)) {
+ if (userId !== String(model.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
}
// do not share detail info
@@ -183,6 +184,50 @@ export const authChat = async ({
showModelDetail
};
};
+export const authShareChat = async ({
+ shareId,
+ password
+}: {
+ shareId: string;
+ password: string;
+}) => {
+ // get shareChat
+ const shareChat = await ShareChat.findById(shareId);
+
+ if (!shareChat) {
+ return Promise.reject('分享链接已失效');
+ }
+
+ if (shareChat.password !== hashPassword(password)) {
+ return Promise.reject({
+ code: 501,
+ message: '密码不正确'
+ });
+ }
+
+ const modelId = String(shareChat.modelId);
+ const userId = String(shareChat.userId);
+
+ // 获取 model 数据
+ const { model, showModelDetail } = await authModel({
+ modelId,
+ userId
+ });
+
+ // 获取 user 的 apiKey
+ const { userOpenAiKey, systemAuthKey } = await getApiKey({
+ model: model.chat.chatModel,
+ userId
+ });
+
+ return {
+ userOpenAiKey,
+ systemAuthKey,
+ userId,
+ model,
+ showModelDetail
+ };
+};
/* 校验 open api key */
export const authOpenApiKey = async (req: NextApiRequest) => {
diff --git a/src/store/chat.ts b/src/store/chat.ts
index a7bf83eeb..a908de111 100644
--- a/src/store/chat.ts
+++ b/src/store/chat.ts
@@ -3,9 +3,23 @@ import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { OpenAiChatEnum } from '@/constants/model';
-import { HistoryItemType, ChatType } from '@/types/chat';
+import {
+ ChatSiteItemType,
+ HistoryItemType,
+ ShareChatHistoryItemType,
+ ChatType,
+ ShareChatType
+} from '@/types/chat';
import { getChatHistory } from '@/api/chat';
+type SetShareChatHistoryItem = {
+ historyId: string;
+ shareId: string;
+ title: string;
+ latestChat: string;
+ chats: ChatSiteItemType[];
+};
+
type State = {
history: HistoryItemType[];
loadHistory: (data: { pageNum: number; init?: boolean }) => Promise;
@@ -17,6 +31,16 @@ type State = {
setLastChatModelId: (id: string) => void;
lastChatId: string;
setLastChatId: (id: string) => void;
+
+ shareChatData: ShareChatType;
+ setShareChatData: (e?: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
+ password: string;
+ setPassword: (val: string) => void;
+ shareChatHistory: ShareChatHistoryItemType[];
+ setShareChatHistory: (e: SetShareChatHistoryItem) => void;
+ delShareHistoryById: (historyId: string) => void;
+ delShareChatHistoryItemById: (historyId: string, index: number) => void;
+ delShareChatHistory: (shareId?: string) => void;
};
const defaultChatData = {
@@ -31,6 +55,16 @@ const defaultChatData = {
chatModel: OpenAiChatEnum.GPT35,
history: []
};
+const defaultShareChatData: ShareChatType = {
+ maxContext: 5,
+ model: {
+ name: '',
+ avatar: '/icon/logo.png',
+ intro: ''
+ },
+ chatModel: 'gpt-3.5-turbo',
+ history: []
+};
export const useChatStore = create()(
devtools(
@@ -77,13 +111,114 @@ export const useChatStore = create()(
state.chatData = e;
});
}
+ },
+ shareChatData: defaultShareChatData,
+ setShareChatData(
+ e: ShareChatType | ((e: ShareChatType) => ShareChatType) = defaultShareChatData
+ ) {
+ if (typeof e === 'function') {
+ set((state) => {
+ state.shareChatData = e(state.shareChatData);
+ });
+ } else {
+ set((state) => {
+ state.shareChatData = e;
+ });
+ }
+ },
+ password: '',
+ setPassword(val: string) {
+ set((state) => {
+ state.password = val;
+ });
+ },
+ shareChatHistory: [],
+ setShareChatHistory({
+ historyId,
+ shareId,
+ title,
+ latestChat,
+ chats = []
+ }: SetShareChatHistoryItem) {
+ set((state) => {
+ const history = state.shareChatHistory.find((item) => item._id === historyId);
+ let historyList: ShareChatHistoryItemType[] = [];
+ if (history) {
+ historyList = state.shareChatHistory.map((item) =>
+ item._id === historyId
+ ? {
+ ...item,
+ title,
+ latestChat,
+ updateTime: new Date(),
+ chats
+ }
+ : item
+ );
+ } else {
+ historyList = [
+ ...state.shareChatHistory,
+ {
+ _id: historyId,
+ shareId,
+ title,
+ latestChat,
+ updateTime: new Date(),
+ chats
+ }
+ ];
+ }
+
+ // @ts-ignore
+ historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
+
+ state.shareChatHistory = historyList.slice(0, 30);
+ });
+ },
+ delShareHistoryById(historyId: string) {
+ set((state) => {
+ state.shareChatHistory = state.shareChatHistory.filter(
+ (item) => item._id !== historyId
+ );
+ });
+ },
+ delShareChatHistoryItemById(historyId: string, index: number) {
+ set((state) => {
+ // update history store
+ const newHistoryList = state.shareChatHistory.map((item) =>
+ item._id === historyId
+ ? {
+ ...item,
+ chats: [...item.chats.slice(0, index), ...item.chats.slice(index + 1)]
+ }
+ : item
+ );
+ state.shareChatHistory = newHistoryList;
+
+ // update chatData
+ state.shareChatData.history =
+ newHistoryList.find((item) => item._id === historyId)?.chats || [];
+ });
+ },
+ delShareChatHistory(shareId?: string) {
+ set((state) => {
+ if (shareId) {
+ state.shareChatHistory = state.shareChatHistory.filter(
+ (item) => item.shareId !== shareId
+ );
+ } else {
+ state.shareChatHistory = [];
+ }
+ });
}
})),
{
- name: 'globalStore',
+ name: 'chatStore',
partialize: (state) => ({
lastChatModelId: state.lastChatModelId,
- lastChatId: state.lastChatId
+ lastChatId: state.lastChatId,
+ password: state.password,
+ shareChatHistory: state.shareChatHistory
})
}
)
diff --git a/src/types/chat.d.ts b/src/types/chat.d.ts
index 4ca1f5aee..32053123a 100644
--- a/src/types/chat.d.ts
+++ b/src/types/chat.d.ts
@@ -1,5 +1,5 @@
import { ChatRoleEnum } from '@/constants/chat';
-import type { InitChatResponse } from '@/api/response/chat';
+import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
export type ExportChatType = 'md' | 'pdf' | 'html';
@@ -20,6 +20,10 @@ export interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
+export interface ShareChatType extends InitShareChatResponse {
+ history: ChatSiteItemType[];
+}
+
export type HistoryItemType = {
_id: string;
updateTime: Date;
@@ -27,3 +31,12 @@ export type HistoryItemType = {
title: string;
latestChat: string;
};
+
+export type ShareChatHistoryItemType = {
+ _id: string;
+ shareId: string;
+ updateTime: Date;
+ title: string;
+ latestChat: string;
+ chats: ChatSiteItemType[];
+};
diff --git a/src/types/model.d.ts b/src/types/model.d.ts
index 11458980d..c772363e4 100644
--- a/src/types/model.d.ts
+++ b/src/types/model.d.ts
@@ -33,3 +33,9 @@ export interface ShareModelItem {
share: ModelSchema['share'];
isCollection: boolean;
}
+
+export type ShareChatEditType = {
+ name: string;
+ password: string;
+ maxContext: number;
+};
diff --git a/src/types/mongoSchema.d.ts b/src/types/mongoSchema.d.ts
index ae63f27c0..a2d9160b8 100644
--- a/src/types/mongoSchema.d.ts
+++ b/src/types/mongoSchema.d.ts
@@ -137,3 +137,14 @@ export interface PromotionRecordSchema {
createTime: Date; // 记录时间
amount: number;
}
+
+export interface ShareChatSchema {
+ _id: string;
+ userId: string;
+ modelId: string;
+ password: string;
+ name: string;
+ tokens: number;
+ maxContext: number;
+ lastTime: Date;
+}