mirror of
https://github.com/labring/FastGPT.git
synced 2025-08-01 03:48:24 +00:00
Optimize the project structure and introduce DDD design (#394)
This commit is contained in:
27
projects/app/src/web/common/api/bill.ts
Normal file
27
projects/app/src/web/common/api/bill.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import type { CreateTrainingBillType } from '@/global/common/api/billReq.d';
|
||||
import type { PaySchema } from '@/types/mongoSchema';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { UserBillType } from '@/types/user';
|
||||
|
||||
export const getUserBills = (data: RequestPaging) =>
|
||||
POST<PagingData<UserBillType>>(`/user/getBill`, data);
|
||||
|
||||
export const postCreateTrainingBill = (data: CreateTrainingBillType) =>
|
||||
POST<string>(`/common/bill/createTrainingBill`, data);
|
||||
|
||||
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);
|
||||
|
||||
export const getPayCode = (amount: number) =>
|
||||
GET<{
|
||||
codeUrl: string;
|
||||
payId: string;
|
||||
}>(`/plusApi/user/pay/getPayCode`, { amount });
|
||||
|
||||
export const checkPayResult = (payId: string) =>
|
||||
GET<number>(`/plusApi/user/pay/checkPayResult`, { payId }).then(() => {
|
||||
try {
|
||||
GET('/user/account/paySuccess');
|
||||
} catch (error) {}
|
||||
return 'success';
|
||||
});
|
115
projects/app/src/web/common/api/fetch.ts
Normal file
115
projects/app/src/web/common/api/fetch.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { sseResponseEventEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { parseStreamChunk, SSEParseData } from '@/utils/sse';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { StartChatFnProps } from '@/components/ChatBox';
|
||||
import { getToken } from '@/utils/user';
|
||||
|
||||
type StreamFetchProps = {
|
||||
url?: string;
|
||||
data: Record<string, any>;
|
||||
onMessage: StartChatFnProps['generatingMessage'];
|
||||
abortSignal: AbortController;
|
||||
};
|
||||
export const streamFetch = ({
|
||||
url = '/api/v1/chat/completions',
|
||||
data,
|
||||
onMessage,
|
||||
abortSignal
|
||||
}: StreamFetchProps) =>
|
||||
new Promise<{
|
||||
responseText: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType[];
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await window.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token: getToken()
|
||||
},
|
||||
signal: abortSignal.signal,
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
detail: true,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response?.body) {
|
||||
throw new Error('Request Error');
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
// response data
|
||||
let responseText = '';
|
||||
let errMsg = '';
|
||||
let responseData: ChatHistoryItemResType[] = [];
|
||||
|
||||
const parseData = new SSEParseData();
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
if (response.status === 200 && !errMsg) {
|
||||
return resolve({
|
||||
responseText,
|
||||
responseData
|
||||
});
|
||||
} else {
|
||||
return reject({
|
||||
message: errMsg || '响应过程出现异常~',
|
||||
responseText
|
||||
});
|
||||
}
|
||||
}
|
||||
const chunkResponse = parseStreamChunk(value);
|
||||
|
||||
chunkResponse.forEach((item) => {
|
||||
// parse json data
|
||||
const { eventName, data } = parseData.parse(item);
|
||||
|
||||
if (!eventName || !data) return;
|
||||
|
||||
if (eventName === sseResponseEventEnum.answer && data !== '[DONE]') {
|
||||
const answer: string = data?.choices?.[0]?.delta?.content || '';
|
||||
onMessage({ text: answer });
|
||||
responseText += answer;
|
||||
} else if (
|
||||
eventName === sseResponseEventEnum.moduleStatus &&
|
||||
data?.name &&
|
||||
data?.status
|
||||
) {
|
||||
onMessage(data);
|
||||
} else if (
|
||||
eventName === sseResponseEventEnum.appStreamResponse &&
|
||||
Array.isArray(data)
|
||||
) {
|
||||
responseData = data;
|
||||
} else if (eventName === sseResponseEventEnum.error) {
|
||||
errMsg = getErrText(data, '流响应错误');
|
||||
}
|
||||
});
|
||||
read();
|
||||
} catch (err: any) {
|
||||
if (err?.message === 'The user aborted a request.') {
|
||||
return resolve({
|
||||
responseText,
|
||||
responseData
|
||||
});
|
||||
}
|
||||
reject({
|
||||
responseText,
|
||||
message: getErrText(err, '请求异常')
|
||||
});
|
||||
}
|
||||
};
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err, 'fetch error');
|
||||
|
||||
reject(getErrText(err, '请求异常'));
|
||||
}
|
||||
});
|
6
projects/app/src/web/common/api/plugin.ts
Normal file
6
projects/app/src/web/common/api/plugin.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { GET, POST, PUT, DELETE } from './request';
|
||||
|
||||
import type { FetchResultItem } from '@/global/common/api/pluginRes.d';
|
||||
|
||||
export const postFetchUrls = (urlList: string[]) =>
|
||||
POST<FetchResultItem[]>(`/plugins/urlFetch`, { urlList });
|
140
projects/app/src/web/common/api/request.ts
Normal file
140
projects/app/src/web/common/api/request.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import axios, {
|
||||
Method,
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosProgressEvent
|
||||
} from 'axios';
|
||||
import { clearToken, getToken } from '@/utils/user';
|
||||
import { TOKEN_ERROR_CODE } from '@/service/errorCode';
|
||||
|
||||
interface ConfigType {
|
||||
headers?: { [key: string]: string };
|
||||
hold?: boolean;
|
||||
timeout?: number;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
cancelToken?: AbortController;
|
||||
}
|
||||
interface ResponseDataType {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求开始
|
||||
*/
|
||||
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
|
||||
if (config.headers) {
|
||||
config.headers.token = getToken();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功,检查请求头
|
||||
*/
|
||||
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* 响应数据检查
|
||||
*/
|
||||
function checkRes(data: ResponseDataType) {
|
||||
if (data === undefined) {
|
||||
console.log('error->', data, 'data is empty');
|
||||
return Promise.reject('服务器异常');
|
||||
} else if (data.code < 200 || data.code >= 400) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应错误
|
||||
*/
|
||||
function responseError(err: any) {
|
||||
console.log('error->', '请求错误', err);
|
||||
|
||||
if (!err) {
|
||||
return Promise.reject({ message: '未知错误' });
|
||||
}
|
||||
if (typeof err === 'string') {
|
||||
return Promise.reject({ message: err });
|
||||
}
|
||||
// 有报错响应
|
||||
if (err?.code in TOKEN_ERROR_CODE) {
|
||||
clearToken();
|
||||
window.location.replace(
|
||||
`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`
|
||||
);
|
||||
return Promise.reject({ message: 'token过期,重新登录' });
|
||||
}
|
||||
if (err?.response?.data) {
|
||||
return Promise.reject(err?.response?.data);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
/* 创建请求实例 */
|
||||
const instance = axios.create({
|
||||
timeout: 60000, // 超时时间
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
/* 请求拦截 */
|
||||
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err));
|
||||
/* 响应拦截 */
|
||||
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
|
||||
|
||||
function request(
|
||||
url: string,
|
||||
data: any,
|
||||
{ cancelToken, ...config }: ConfigType,
|
||||
method: Method
|
||||
): any {
|
||||
/* 去空 */
|
||||
for (const key in data) {
|
||||
if (data[key] === null || data[key] === undefined) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
.request({
|
||||
baseURL: '/api',
|
||||
url,
|
||||
method,
|
||||
data: ['POST', 'PUT'].includes(method) ? data : null,
|
||||
params: !['POST', 'PUT'].includes(method) ? data : null,
|
||||
signal: cancelToken?.signal,
|
||||
...config // 用户自定义配置,可以覆盖前面的配置
|
||||
})
|
||||
.then((res) => checkRes(res.data))
|
||||
.catch((err) => responseError(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* api请求方式
|
||||
* @param {String} url
|
||||
* @param {Any} params
|
||||
* @param {Object} config
|
||||
* @returns
|
||||
*/
|
||||
export function GET<T>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, params, config, 'GET');
|
||||
}
|
||||
|
||||
export function POST<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'POST');
|
||||
}
|
||||
|
||||
export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'PUT');
|
||||
}
|
||||
|
||||
export function DELETE<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'DELETE');
|
||||
}
|
21
projects/app/src/web/common/api/system.ts
Normal file
21
projects/app/src/web/common/api/system.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
import type { InitDateResponse } from '@/global/common/api/systemRes';
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const getSystemInitData = () => GET<InitDateResponse>('/system/getInitData');
|
||||
|
||||
export const postUploadImg = (base64Img: string) =>
|
||||
POST<string>('/system/file/uploadImage', { base64Img });
|
||||
|
||||
export const postUploadFiles = (
|
||||
data: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST<string[]>('/system/file/upload', data, {
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const getFileViewUrl = (fileId: string) => GET<string>('/system/file/readUrl', { fileId });
|
80
projects/app/src/web/common/hooks/useConfirm.tsx
Normal file
80
projects/app/src/web/common/hooks/useConfirm.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
export const useConfirm = (props: { title?: string | null; content?: string | null }) => {
|
||||
const { t } = useTranslation();
|
||||
const { title = t('Warning'), content } = props;
|
||||
const [customContent, setCustomContent] = useState(content);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const cancelRef = useRef(null);
|
||||
const confirmCb = useRef<any>();
|
||||
const cancelCb = useRef<any>();
|
||||
|
||||
return {
|
||||
openConfirm: useCallback(
|
||||
(confirm?: any, cancel?: any, customContent?: string) => {
|
||||
confirmCb.current = confirm;
|
||||
cancelCb.current = cancel;
|
||||
|
||||
customContent && setCustomContent(customContent);
|
||||
|
||||
return onOpen;
|
||||
},
|
||||
[onOpen]
|
||||
),
|
||||
ConfirmModal: useCallback(
|
||||
() => (
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
autoFocus={false}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent maxW={'min(90vw,400px)'}>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{title}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>{customContent}</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={4}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof confirmCb.current === 'function' && confirmCb.current();
|
||||
}}
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
),
|
||||
[customContent, isOpen, onClose, t, title]
|
||||
)
|
||||
};
|
||||
};
|
39
projects/app/src/web/common/hooks/useCopyData.tsx
Normal file
39
projects/app/src/web/common/hooks/useCopyData.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
/**
|
||||
* copy text data
|
||||
*/
|
||||
export const useCopyData = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
return {
|
||||
copyData: async (
|
||||
data: string,
|
||||
title: string | null = t('common.Copy Successful'),
|
||||
duration = 1000
|
||||
) => {
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(data);
|
||||
} else {
|
||||
throw new Error('');
|
||||
}
|
||||
} catch (error) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = data;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
toast({
|
||||
title,
|
||||
status: 'success',
|
||||
duration
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
77
projects/app/src/web/common/hooks/useEditTitle.tsx
Normal file
77
projects/app/src/web/common/hooks/useEditTitle.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { ModalFooter, ModalBody, Input, useDisclosure, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
|
||||
export const useEditTitle = ({
|
||||
title,
|
||||
placeholder = ''
|
||||
}: {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const onSuccessCb = useRef<(content: string) => void | Promise<void>>();
|
||||
const onErrorCb = useRef<(err: any) => void>();
|
||||
const defaultValue = useRef('');
|
||||
|
||||
const onOpenModal = useCallback(
|
||||
({
|
||||
defaultVal,
|
||||
onSuccess,
|
||||
onError
|
||||
}: {
|
||||
defaultVal: string;
|
||||
onSuccess: (content: string) => any;
|
||||
onError?: (err: any) => void;
|
||||
}) => {
|
||||
onOpen();
|
||||
onSuccessCb.current = onSuccess;
|
||||
onErrorCb.current = onError;
|
||||
defaultValue.current = defaultVal;
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const onclickConfirm = useCallback(async () => {
|
||||
if (!inputRef.current) return;
|
||||
try {
|
||||
const val = inputRef.current.value;
|
||||
await onSuccessCb.current?.(val);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
onErrorCb.current?.(err);
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const EditModal = useCallback(
|
||||
() => (
|
||||
<MyModal isOpen={isOpen} onClose={onClose} title={title}>
|
||||
<ModalBody>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={defaultValue.current}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
maxLength={20}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'base'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onclickConfirm}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
),
|
||||
[isOpen, onClose, onclickConfirm, placeholder, title]
|
||||
);
|
||||
|
||||
return {
|
||||
onOpenModal,
|
||||
EditModal
|
||||
};
|
||||
};
|
31
projects/app/src/web/common/hooks/useLoading.tsx
Normal file
31
projects/app/src/web/common/hooks/useLoading.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import LoadingComponent from '@/components/Loading';
|
||||
|
||||
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
||||
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
||||
|
||||
const Loading = useCallback(
|
||||
({
|
||||
loading,
|
||||
fixed = true,
|
||||
text = '',
|
||||
zIndex
|
||||
}: {
|
||||
loading?: boolean;
|
||||
fixed?: boolean;
|
||||
text?: string;
|
||||
zIndex?: number;
|
||||
}): JSX.Element | null => {
|
||||
return isLoading || loading ? (
|
||||
<LoadingComponent fixed={fixed} text={text} zIndex={zIndex} />
|
||||
) : null;
|
||||
},
|
||||
[isLoading]
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
Loading
|
||||
};
|
||||
};
|
15
projects/app/src/web/common/hooks/useMarkdown.ts
Normal file
15
projects/app/src/web/common/hooks/useMarkdown.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export const getMd = async (url: string) => {
|
||||
const response = await fetch(`/docs/${url}`);
|
||||
const textContent = await response.text();
|
||||
return textContent;
|
||||
};
|
||||
|
||||
export const useMarkdown = ({ url }: { url: string }) => {
|
||||
const { data = '' } = useQuery([url], () => getMd(url));
|
||||
|
||||
return {
|
||||
data
|
||||
};
|
||||
};
|
188
projects/app/src/web/common/hooks/usePagination.tsx
Normal file
188
projects/app/src/web/common/hooks/usePagination.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useRef, useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import type { PagingData } from '@/types/index.d';
|
||||
import { IconButton, Flex, Box, Input } from '@chakra-ui/react';
|
||||
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from './useToast';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
const thresholdVal = 100;
|
||||
|
||||
export function usePagination<T = any>({
|
||||
api,
|
||||
pageSize = 10,
|
||||
params = {},
|
||||
defaultRequest = true,
|
||||
type = 'button',
|
||||
onChange
|
||||
}: {
|
||||
api: (data: any) => any;
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
defaultRequest?: boolean;
|
||||
type?: 'button' | 'scroll';
|
||||
onChange?: (pageNum: number) => void;
|
||||
}) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const { toast } = useToast();
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const maxPage = useMemo(() => Math.ceil(total / pageSize) || 1, [pageSize, total]);
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationFn: async (num: number = pageNum) => {
|
||||
try {
|
||||
const res: PagingData<T> = await api({
|
||||
pageNum: num,
|
||||
pageSize,
|
||||
...params
|
||||
});
|
||||
setPageNum(num);
|
||||
res.total !== undefined && setTotal(res.total);
|
||||
setData(res.data);
|
||||
onChange && onChange(num);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '获取数据异常',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const Pagination = useCallback(() => {
|
||||
return (
|
||||
<Flex alignItems={'center'} justifyContent={'end'}>
|
||||
<IconButton
|
||||
isDisabled={pageNum === 1}
|
||||
icon={<ArrowBackIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => mutate(pageNum - 1)}
|
||||
/>
|
||||
<Flex mx={2} alignItems={'center'}>
|
||||
<Input
|
||||
defaultValue={pageNum}
|
||||
w={'50px'}
|
||||
size={'xs'}
|
||||
type={'number'}
|
||||
min={1}
|
||||
max={maxPage}
|
||||
onBlur={(e) => {
|
||||
const val = +e.target.value;
|
||||
if (val === pageNum) return;
|
||||
if (val >= maxPage) {
|
||||
mutate(maxPage);
|
||||
} else if (val < 1) {
|
||||
mutate(1);
|
||||
} else {
|
||||
mutate(+e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// @ts-ignore
|
||||
const val = +e.target.value;
|
||||
if (val && e.keyCode === 13) {
|
||||
if (val === pageNum) return;
|
||||
if (val >= maxPage) {
|
||||
mutate(maxPage);
|
||||
} else if (val < 1) {
|
||||
mutate(1);
|
||||
} else {
|
||||
mutate(val);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box mx={2}>/</Box>
|
||||
{maxPage}
|
||||
</Flex>
|
||||
<IconButton
|
||||
isDisabled={pageNum === maxPage}
|
||||
icon={<ArrowForwardIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
isLoading={isLoading}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
onClick={() => mutate(pageNum + 1)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}, [isLoading, maxPage, mutate, pageNum]);
|
||||
|
||||
const ScrollData = useCallback(
|
||||
({ children, ...props }: { children: React.ReactNode }) => {
|
||||
const loadText = useMemo(() => {
|
||||
if (isLoading) return '请求中……';
|
||||
if (total <= data.length) return '已加载全部';
|
||||
return '点击加载更多';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box {...props} ref={elementRef} overflow={'overlay'}>
|
||||
{children}
|
||||
<Box
|
||||
mt={2}
|
||||
fontSize={'xs'}
|
||||
color={'blackAlpha.500'}
|
||||
textAlign={'center'}
|
||||
cursor={loadText === '点击加载更多' ? 'pointer' : 'default'}
|
||||
onClick={() => {
|
||||
if (loadText !== '点击加载更多') return;
|
||||
mutate(pageNum + 1);
|
||||
}}
|
||||
>
|
||||
{loadText}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
[data.length, isLoading, mutate, pageNum, total]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current || type !== 'scroll') return;
|
||||
|
||||
const scrolling = throttle((e: Event) => {
|
||||
const element = e.target as HTMLDivElement;
|
||||
if (!element) return;
|
||||
// 当前滚动位置
|
||||
const scrollTop = element.scrollTop;
|
||||
// 可视高度
|
||||
const clientHeight = element.clientHeight;
|
||||
// 内容总高度
|
||||
const scrollHeight = element.scrollHeight;
|
||||
// 判断是否滚动到底部
|
||||
if (scrollTop + clientHeight + thresholdVal >= scrollHeight) {
|
||||
mutate(pageNum + 1);
|
||||
}
|
||||
}, 100);
|
||||
elementRef.current.addEventListener('scroll', scrolling);
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
elementRef.current?.removeEventListener('scroll', scrolling);
|
||||
};
|
||||
}, [elementRef, mutate, pageNum, type]);
|
||||
|
||||
useEffect(() => {
|
||||
defaultRequest && mutate(1);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
data,
|
||||
isLoading,
|
||||
Pagination,
|
||||
ScrollData,
|
||||
getData: mutate
|
||||
};
|
||||
}
|
36
projects/app/src/web/common/hooks/useRequest.tsx
Normal file
36
projects/app/src/web/common/hooks/useRequest.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props extends UseMutationOptions<any, any, any, any> {
|
||||
successToast?: string | null;
|
||||
errorToast?: string | null;
|
||||
}
|
||||
|
||||
export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const mutation = useMutation<unknown, unknown, any, unknown>({
|
||||
...props,
|
||||
onSuccess(res, variables: void, context: unknown) {
|
||||
onSuccess?.(res, variables, context);
|
||||
successToast &&
|
||||
toast({
|
||||
title: successToast,
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
onError(err: any, variables: void, context: unknown) {
|
||||
onError?.(err, variables, context);
|
||||
errorToast &&
|
||||
toast({
|
||||
title: t(getErrText(err, errorToast)),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
44
projects/app/src/web/common/hooks/useSelectFile.tsx
Normal file
44
projects/app/src/web/common/hooks/useSelectFile.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useToast } from './useToast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { fileType = '*', multiple = false } = props || {};
|
||||
const { toast } = useToast();
|
||||
const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
|
||||
const File = useCallback(
|
||||
({ onSelect }: { onSelect: (e: File[]) => void }) => (
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input
|
||||
ref={SelectFileDom}
|
||||
type="file"
|
||||
accept={fileType}
|
||||
multiple={multiple}
|
||||
onChange={(e) => {
|
||||
if (!e.target.files || e.target.files?.length === 0) return;
|
||||
if (e.target.files.length > 10) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('file.Select a maximum of 10 files')
|
||||
});
|
||||
}
|
||||
onSelect(Array.from(e.target.files));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
[fileType, multiple, t, toast]
|
||||
);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
SelectFileDom.current && SelectFileDom.current.click();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
File,
|
||||
onOpen
|
||||
};
|
||||
};
|
13
projects/app/src/web/common/hooks/useToast.ts
Normal file
13
projects/app/src/web/common/hooks/useToast.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useToast as uToast, UseToastOptions } from '@chakra-ui/react';
|
||||
|
||||
export const useToast = (props?: UseToastOptions) => {
|
||||
const toast = uToast({
|
||||
position: 'top',
|
||||
duration: 2000,
|
||||
...props
|
||||
});
|
||||
|
||||
return {
|
||||
toast
|
||||
};
|
||||
};
|
81
projects/app/src/web/common/store/global.ts
Normal file
81
projects/app/src/web/common/store/global.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import axios from 'axios';
|
||||
import { OAuthEnum } from '@/constants/user';
|
||||
|
||||
type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string };
|
||||
|
||||
type State = {
|
||||
lastRoute: string;
|
||||
setLastRoute: (e: string) => void;
|
||||
loginStore?: LoginStoreType;
|
||||
setLoginStore: (e: LoginStoreType) => void;
|
||||
loading: boolean;
|
||||
setLoading: (val: boolean) => null;
|
||||
screenWidth: number;
|
||||
setScreenWidth: (val: number) => void;
|
||||
isPc?: boolean;
|
||||
initIsPc(val: boolean): void;
|
||||
gitStar: number;
|
||||
loadGitStar: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const useGlobalStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
lastRoute: '/app/list',
|
||||
setLastRoute(e) {
|
||||
set((state) => {
|
||||
state.lastRoute = e;
|
||||
});
|
||||
},
|
||||
loginStore: undefined,
|
||||
setLoginStore(e) {
|
||||
set((state) => {
|
||||
state.loginStore = e;
|
||||
});
|
||||
},
|
||||
loading: false,
|
||||
setLoading: (val: boolean) => {
|
||||
set((state) => {
|
||||
state.loading = val;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
screenWidth: 600,
|
||||
setScreenWidth(val: number) {
|
||||
set((state) => {
|
||||
state.screenWidth = val;
|
||||
state.isPc = val < 900 ? false : true;
|
||||
});
|
||||
},
|
||||
isPc: undefined,
|
||||
initIsPc(val: boolean) {
|
||||
if (get().isPc !== undefined) return;
|
||||
|
||||
set((state) => {
|
||||
state.isPc = val;
|
||||
});
|
||||
},
|
||||
gitStar: 3700,
|
||||
async loadGitStar() {
|
||||
try {
|
||||
const { data: git } = await axios.get('https://api.github.com/repos/labring/FastGPT');
|
||||
|
||||
set((state) => {
|
||||
state.gitStar = git.stargazers_count;
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'globalStore',
|
||||
partialize: (state) => ({
|
||||
loginStore: state.loginStore
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
40
projects/app/src/web/common/store/static.ts
Normal file
40
projects/app/src/web/common/store/static.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
type QAModelItemType,
|
||||
type ChatModelItemType,
|
||||
type VectorModelItemType
|
||||
} from '@/types/model';
|
||||
import type { InitDateResponse } from '@/global/common/api/systemRes';
|
||||
import { getSystemInitData } from '@/web/common/api/system';
|
||||
import { delay } from '@/utils/tools';
|
||||
import { FeConfigsType } from '@/types';
|
||||
|
||||
export let systemVersion = '0.0.0';
|
||||
export let chatModelList: ChatModelItemType[] = [];
|
||||
export let qaModel: QAModelItemType = {
|
||||
model: 'gpt-3.5-turbo-16k',
|
||||
name: 'GPT35-16k',
|
||||
maxToken: 16000,
|
||||
price: 0
|
||||
};
|
||||
export let vectorModelList: VectorModelItemType[] = [];
|
||||
export let feConfigs: FeConfigsType = {};
|
||||
|
||||
let retryTimes = 3;
|
||||
|
||||
export const clientInitData = async (): Promise<InitDateResponse> => {
|
||||
try {
|
||||
const res = await getSystemInitData();
|
||||
|
||||
chatModelList = res.chatModels;
|
||||
qaModel = res.qaModel;
|
||||
vectorModelList = res.vectorModels;
|
||||
feConfigs = res.feConfigs;
|
||||
systemVersion = res.systemVersion;
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
retryTimes--;
|
||||
await delay(500);
|
||||
return clientInitData();
|
||||
}
|
||||
};
|
18
projects/app/src/web/common/utils/eventbus.ts
Normal file
18
projects/app/src/web/common/utils/eventbus.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export enum EventNameEnum {
|
||||
guideClick = 'guideClick'
|
||||
}
|
||||
type EventNameType = `${EventNameEnum}`;
|
||||
|
||||
export const eventBus = {
|
||||
list: new Map<EventNameType, Function>(),
|
||||
on: function (name: EventNameType, fn: Function) {
|
||||
this.list.set(name, fn);
|
||||
},
|
||||
emit: function (name: EventNameType, data: Record<string, any> = {}) {
|
||||
const fn = this.list.get(name);
|
||||
fn && fn(data);
|
||||
},
|
||||
off: function (name: EventNameType) {
|
||||
this.list.delete(name);
|
||||
}
|
||||
};
|
262
projects/app/src/web/common/utils/file.ts
Normal file
262
projects/app/src/web/common/utils/file.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import mammoth from 'mammoth';
|
||||
import Papa from 'papaparse';
|
||||
import { postUploadImg, postUploadFiles, getFileViewUrl } from '@/web/common/api/system';
|
||||
|
||||
/**
|
||||
* upload file to mongo gridfs
|
||||
*/
|
||||
export const uploadFiles = (
|
||||
files: File[],
|
||||
metadata: Record<string, any> = {},
|
||||
percentListen?: (percent: number) => void
|
||||
) => {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
files.forEach((file) => {
|
||||
form.append('file', file, encodeURIComponent(file.name));
|
||||
});
|
||||
return postUploadFiles(form, (e) => {
|
||||
if (!e.total) return;
|
||||
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
percentListen && percentListen(percent);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取 txt 文件内容
|
||||
*/
|
||||
export const readTxtContent = (file: File) => {
|
||||
return new Promise((resolve: (_: string) => void, reject) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
console.log('error txt read:', err);
|
||||
reject('读取 txt 文件失败');
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
reject('浏览器不支持文件内容读取');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取 pdf 内容
|
||||
*/
|
||||
export const readPdfContent = (file: File) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
const pdfjsLib = window['pdfjs-dist/build/pdf'];
|
||||
pdfjsLib.workerSrc = '/js/pdf.worker.js';
|
||||
|
||||
const readPDFPage = async (doc: any, pageNo: number) => {
|
||||
const page = await doc.getPage(pageNo);
|
||||
const tokenizedText = await page.getTextContent();
|
||||
|
||||
const pageText = tokenizedText.items
|
||||
.map((token: any) => token.str)
|
||||
.filter((item: string) => item)
|
||||
.join('');
|
||||
return pageText;
|
||||
};
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.onload = async (event) => {
|
||||
if (!event?.target?.result) return reject('解析 PDF 失败');
|
||||
try {
|
||||
const doc = await pdfjsLib.getDocument(event.target.result).promise;
|
||||
const pageTextPromises = [];
|
||||
for (let pageNo = 1; pageNo <= doc.numPages; pageNo++) {
|
||||
pageTextPromises.push(readPDFPage(doc, pageNo));
|
||||
}
|
||||
const pageTexts = await Promise.all(pageTextPromises);
|
||||
resolve(pageTexts.join('\n'));
|
||||
} catch (err) {
|
||||
console.log(err, 'pdf load error');
|
||||
reject('解析 PDF 失败');
|
||||
}
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
console.log(err, 'pdf load error');
|
||||
reject('解析 PDF 失败');
|
||||
};
|
||||
} catch (error) {
|
||||
reject('浏览器不支持文件内容读取');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 读取doc
|
||||
*/
|
||||
export const readDocContent = (file: File) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.onload = async ({ target }) => {
|
||||
if (!target?.result) return reject('读取 doc 文件失败');
|
||||
try {
|
||||
const res = await mammoth.extractRawText({
|
||||
arrayBuffer: target.result as ArrayBuffer
|
||||
});
|
||||
resolve(res?.value);
|
||||
} catch (error) {
|
||||
window.umami?.track('wordReadError', {
|
||||
err: error?.toString()
|
||||
});
|
||||
console.log('error doc read:', error);
|
||||
|
||||
reject('读取 doc 文件失败, 请转换成 PDF');
|
||||
}
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
window.umami?.track('wordReadError', {
|
||||
err: err?.toString()
|
||||
});
|
||||
console.log('error doc read:', err);
|
||||
|
||||
reject('读取 doc 文件失败');
|
||||
};
|
||||
} catch (error) {
|
||||
reject('浏览器不支持文件内容读取');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 读取csv
|
||||
*/
|
||||
export const readCsvContent = async (file: File) => {
|
||||
try {
|
||||
const textArr = await readTxtContent(file);
|
||||
const csvArr = Papa.parse(textArr).data as string[][];
|
||||
if (csvArr.length === 0) {
|
||||
throw new Error('csv 解析失败');
|
||||
}
|
||||
return {
|
||||
header: csvArr.shift() as string[],
|
||||
data: csvArr.map((item) => item)
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject('解析 csv 文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* file download
|
||||
*/
|
||||
export const fileDownload = ({
|
||||
text,
|
||||
type,
|
||||
filename
|
||||
}: {
|
||||
text: string;
|
||||
type: string;
|
||||
filename: string;
|
||||
}) => {
|
||||
// 导出为文件
|
||||
const blob = new Blob([`\uFEFF${text}`], { type: `${type};charset=utf-8;` });
|
||||
|
||||
// 创建下载链接
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
downloadLink.download = filename;
|
||||
|
||||
// 添加链接到页面并触发下载
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
export async function getFileAndOpen(fileId: string) {
|
||||
const url = await getFileViewUrl(fileId);
|
||||
const asPath = `${location.origin}${url}`;
|
||||
window.open(asPath, '_blank');
|
||||
}
|
||||
|
||||
export const fileToBase64 = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* compress image. response base64
|
||||
* @param maxSize The max size of the compressed image
|
||||
*/
|
||||
export const compressImg = ({
|
||||
file,
|
||||
maxW = 200,
|
||||
maxH = 200,
|
||||
maxSize = 1024 * 100
|
||||
}: {
|
||||
file: File;
|
||||
maxW?: number;
|
||||
maxH?: number;
|
||||
maxSize?: number;
|
||||
}) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = async () => {
|
||||
const img = new Image();
|
||||
// @ts-ignore
|
||||
img.src = reader.result;
|
||||
img.onload = async () => {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > maxW) {
|
||||
height *= maxW / width;
|
||||
width = maxW;
|
||||
}
|
||||
} else {
|
||||
if (height > maxH) {
|
||||
width *= maxH / height;
|
||||
height = maxH;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return reject('压缩图片异常');
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const compressedDataUrl = canvas.toDataURL(file.type, 0.8);
|
||||
// 移除 canvas 元素
|
||||
canvas.remove();
|
||||
|
||||
if (compressedDataUrl.length > maxSize) {
|
||||
return reject('图片太大了');
|
||||
}
|
||||
|
||||
const src = await (async () => {
|
||||
try {
|
||||
const src = await postUploadImg(compressedDataUrl);
|
||||
return src;
|
||||
} catch (error) {
|
||||
return compressedDataUrl;
|
||||
}
|
||||
})();
|
||||
|
||||
resolve(src);
|
||||
};
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
console.log(err);
|
||||
reject('压缩图片异常');
|
||||
};
|
||||
});
|
37
projects/app/src/web/common/utils/i18n.ts
Normal file
37
projects/app/src/web/common/utils/i18n.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export const LANG_KEY = 'NEXT_LOCALE_LANG';
|
||||
export enum LangEnum {
|
||||
'zh' = 'zh',
|
||||
'en' = 'en'
|
||||
}
|
||||
export const langMap = {
|
||||
[LangEnum.en]: {
|
||||
label: 'English',
|
||||
icon: 'language_en'
|
||||
},
|
||||
[LangEnum.zh]: {
|
||||
label: '简体中文',
|
||||
icon: 'language_zh'
|
||||
}
|
||||
};
|
||||
|
||||
export const setLangStore = (value: `${LangEnum}`) => {
|
||||
return Cookies.set(LANG_KEY, value, { expires: 7, sameSite: 'None', secure: true });
|
||||
};
|
||||
|
||||
export const getLangStore = () => {
|
||||
return (Cookies.get(LANG_KEY) as `${LangEnum}`) || LangEnum.zh;
|
||||
};
|
||||
|
||||
export const serviceSideProps = (content: any) => {
|
||||
const acceptLanguage = (content.req.headers['accept-language'] as string) || '';
|
||||
const acceptLanguageList = acceptLanguage.split(/,|;/g);
|
||||
// @ts-ignore
|
||||
const firstLang = acceptLanguageList.find((lang) => langMap[lang]);
|
||||
|
||||
const language = content.req.cookies[LANG_KEY] || firstLang || 'zh';
|
||||
|
||||
return serverSideTranslations(language, undefined, null, content.locales);
|
||||
};
|
129
projects/app/src/web/common/utils/voice.ts
Normal file
129
projects/app/src/web/common/utils/voice.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
export const useAudioPlay = (props?: { ttsUrl?: string }) => {
|
||||
const { ttsUrl } = props || {};
|
||||
const { toast } = useToast();
|
||||
const [audio, setAudio] = useState<HTMLAudioElement>();
|
||||
const [audioLoading, setAudioLoading] = useState(false);
|
||||
const [audioPlaying, setAudioPlaying] = useState(false);
|
||||
|
||||
const hasAudio = useMemo(() => {
|
||||
if (ttsUrl) return true;
|
||||
const voices = window.speechSynthesis?.getVoices?.() || []; // 获取语言包
|
||||
const voice = voices.find((item) => {
|
||||
return item.lang === 'zh-CN';
|
||||
});
|
||||
return !!voice;
|
||||
}, [ttsUrl]);
|
||||
|
||||
const playAudio = useCallback(
|
||||
async (text: string) => {
|
||||
text = text.replace(/\\n/g, '\n');
|
||||
try {
|
||||
if (audio && ttsUrl) {
|
||||
setAudioLoading(true);
|
||||
const response = await fetch(ttsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text
|
||||
})
|
||||
}).then((res) => res.blob());
|
||||
|
||||
const audioUrl = URL.createObjectURL(response);
|
||||
audio.src = audioUrl;
|
||||
audio.play();
|
||||
} else {
|
||||
// window speech
|
||||
window.speechSynthesis?.cancel();
|
||||
const msg = new SpeechSynthesisUtterance(text);
|
||||
const voices = window.speechSynthesis?.getVoices?.() || []; // 获取语言包
|
||||
const voice = voices.find((item) => {
|
||||
return item.lang === 'zh-CN';
|
||||
});
|
||||
if (voice) {
|
||||
msg.onstart = () => {
|
||||
setAudioPlaying(true);
|
||||
};
|
||||
msg.onend = () => {
|
||||
setAudioPlaying(false);
|
||||
msg.onstart = null;
|
||||
msg.onend = null;
|
||||
};
|
||||
msg.voice = voice;
|
||||
window.speechSynthesis?.speak(msg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(error, '语音播报异常')
|
||||
});
|
||||
}
|
||||
setAudioLoading(false);
|
||||
},
|
||||
[audio, toast, ttsUrl]
|
||||
);
|
||||
|
||||
const cancelAudio = useCallback(() => {
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
}
|
||||
window.speechSynthesis?.cancel();
|
||||
setAudioPlaying(false);
|
||||
}, [audio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ttsUrl) {
|
||||
setAudio(new Audio());
|
||||
} else {
|
||||
setAudio(undefined);
|
||||
}
|
||||
}, [ttsUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (audio) {
|
||||
audio.onplay = () => {
|
||||
setAudioPlaying(true);
|
||||
};
|
||||
audio.onended = () => {
|
||||
setAudioPlaying(false);
|
||||
};
|
||||
audio.onerror = () => {
|
||||
setAudioPlaying(false);
|
||||
};
|
||||
}
|
||||
const listen = () => {
|
||||
cancelAudio();
|
||||
};
|
||||
window.addEventListener('beforeunload', listen);
|
||||
return () => {
|
||||
if (audio) {
|
||||
audio.onplay = null;
|
||||
audio.onended = null;
|
||||
audio.onerror = null;
|
||||
}
|
||||
cancelAudio();
|
||||
window.removeEventListener('beforeunload', listen);
|
||||
};
|
||||
}, [audio, cancelAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setAudio(undefined);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audioPlaying,
|
||||
audioLoading,
|
||||
hasAudio,
|
||||
playAudio,
|
||||
cancelAudio
|
||||
};
|
||||
};
|
5
projects/app/src/web/core/api/ai.ts
Normal file
5
projects/app/src/web/core/api/ai.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import type { CreateQuestionGuideParams } from '@/global/core/api/aiReq.d';
|
||||
|
||||
export const postQuestionGuide = (data: CreateQuestionGuideParams, cancelToken: AbortController) =>
|
||||
POST<string[]>('/core/ai/agent/createQuestionGuide', data, { cancelToken });
|
57
projects/app/src/web/core/api/app.ts
Normal file
57
projects/app/src/web/core/api/app.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import type { AppListItemType, AppUpdateParams } from '@/types/app';
|
||||
import { RequestPaging } from '@/types/index';
|
||||
import { addDays } from 'date-fns';
|
||||
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
|
||||
import type { CreateAppParams } from '@/types/app';
|
||||
|
||||
/**
|
||||
* 获取模型列表
|
||||
*/
|
||||
export const getMyModels = () => GET<AppListItemType[]>('/app/myApps');
|
||||
|
||||
/**
|
||||
* 创建一个模型
|
||||
*/
|
||||
export const postCreateApp = (data: CreateAppParams) => POST<string>('/app/create', data);
|
||||
|
||||
/**
|
||||
* 根据 ID 删除模型
|
||||
*/
|
||||
export const delModelById = (id: string) => DELETE(`/app/del?appId=${id}`);
|
||||
|
||||
/**
|
||||
* 根据 ID 获取模型
|
||||
*/
|
||||
export const getModelById = (id: string) => GET<AppSchema>(`/app/detail?appId=${id}`);
|
||||
|
||||
/**
|
||||
* 根据 ID 更新模型
|
||||
*/
|
||||
export const putAppById = (id: string, data: AppUpdateParams) =>
|
||||
PUT(`/app/update?appId=${id}`, data);
|
||||
|
||||
/* 共享市场 */
|
||||
/**
|
||||
* 获取共享市场模型
|
||||
*/
|
||||
export const getShareModelList = (data: { searchText?: string } & RequestPaging) =>
|
||||
POST(`/app/share/getModels`, data);
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏模型
|
||||
*/
|
||||
export const triggerModelCollection = (appId: string) =>
|
||||
POST<number>(`/app/share/collection?appId=${appId}`);
|
||||
|
||||
// ====================== data
|
||||
export const getAppTotalUsage = (data: { appId: string }) =>
|
||||
POST<{ date: String; total: number }[]>(`/app/data/totalUsage`, {
|
||||
...data,
|
||||
start: addDays(new Date(), -13),
|
||||
end: addDays(new Date(), 1)
|
||||
}).then((res) => (res.length === 0 ? [{ date: new Date(), total: 0 }] : res));
|
||||
|
||||
// =================== chat logs
|
||||
export const getAppChatLogs = (data: GetAppChatLogsParams) => POST(`/app/getChatLogs`, data);
|
45
projects/app/src/web/core/api/chat.ts
Normal file
45
projects/app/src/web/core/api/chat.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import type { ChatHistoryItemType } from '@/types/chat';
|
||||
import type { InitChatResponse } from '@/global/core/api/chatRes.d';
|
||||
import type { RequestPaging } from '@/types';
|
||||
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
|
||||
import type { AdminUpdateFeedbackParams } from '@/global/core/api/chatReq.d';
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
*/
|
||||
export const getInitChatSiteInfo = (data: { appId: string; chatId?: string }) =>
|
||||
GET<InitChatResponse>(`/chat/init`, data);
|
||||
|
||||
/**
|
||||
* 获取历史记录
|
||||
*/
|
||||
export const getChatHistory = (data: RequestPaging & { appId?: string }) =>
|
||||
POST<ChatHistoryItemType[]>('/chat/history/getHistory', data);
|
||||
|
||||
/**
|
||||
* 删除一条历史记录
|
||||
*/
|
||||
export const delChatHistoryById = (chatId: string) => DELETE(`/chat/removeHistory`, { chatId });
|
||||
/**
|
||||
* clear all history by appid
|
||||
*/
|
||||
export const clearChatHistoryByAppId = (appId: string) => DELETE(`/chat/removeHistory`, { appId });
|
||||
|
||||
/**
|
||||
* 删除一句对话
|
||||
*/
|
||||
export const delChatRecordById = (data: { chatId: string; contentId: string }) =>
|
||||
DELETE(`/chat/delChatRecordByContentId`, data);
|
||||
|
||||
/**
|
||||
* 修改历史记录: 标题/置顶
|
||||
*/
|
||||
export const putChatHistory = (data: UpdateHistoryProps) =>
|
||||
PUT('/chat/history/updateChatHistory', data);
|
||||
|
||||
export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) =>
|
||||
POST('/chat/feedback/userUpdate', data);
|
||||
|
||||
export const adminUpdateChatFeedback = (data: AdminUpdateFeedbackParams) =>
|
||||
POST('/chat/feedback/adminUpdate', data);
|
123
projects/app/src/web/core/api/dataset.ts
Normal file
123
projects/app/src/web/core/api/dataset.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import type { DatasetItemType, DatasetsItemType, DatasetPathItemType } from '@/types/core/dataset';
|
||||
import type {
|
||||
DatasetUpdateParams,
|
||||
CreateDatasetParams,
|
||||
SearchTestProps,
|
||||
GetFileListProps,
|
||||
UpdateFileProps,
|
||||
MarkFileUsedProps,
|
||||
PushDataProps,
|
||||
UpdateDatasetDataPrams,
|
||||
GetDatasetDataListProps
|
||||
} from '@/global/core/api/datasetReq.d';
|
||||
import type { SearchTestResponseType, PushDataResponse } from '@/global/core/api/datasetRes.d';
|
||||
import { KbTypeEnum } from '@/constants/dataset';
|
||||
import type { DatasetFileItemType } from '@/types/core/dataset/file';
|
||||
import type { GSFileInfoType } from '@/types/common/file';
|
||||
import type { QuoteItemType } from '@/types/chat';
|
||||
import { getToken } from '@/utils/user';
|
||||
import download from 'downloadjs';
|
||||
import type { DatasetDataItemType } from '@/types/core/dataset/data';
|
||||
|
||||
/* ======================== dataset ======================= */
|
||||
export const getDatasets = (data: { parentId?: string; type?: `${KbTypeEnum}` }) =>
|
||||
GET<DatasetsItemType[]>(`/core/dataset/list`, data);
|
||||
|
||||
/**
|
||||
* get type=dataset list
|
||||
*/
|
||||
export const getAllDataset = () => GET<DatasetsItemType[]>(`/core/dataset/allDataset`);
|
||||
|
||||
export const getDatasetPaths = (parentId?: string) =>
|
||||
GET<DatasetPathItemType[]>('/core/dataset/paths', { parentId });
|
||||
|
||||
export const getDatasetById = (id: string) => GET<DatasetItemType>(`/core/dataset/detail?id=${id}`);
|
||||
|
||||
export const postCreateDataset = (data: CreateDatasetParams) =>
|
||||
POST<string>(`/core/dataset/create`, data);
|
||||
|
||||
export const putDatasetById = (data: DatasetUpdateParams) => PUT(`/core/dataset/update`, data);
|
||||
|
||||
export const delDatasetById = (id: string) => DELETE(`/core/dataset/delete?id=${id}`);
|
||||
|
||||
export const postSearchText = (data: SearchTestProps) =>
|
||||
POST<SearchTestResponseType>(`/core/dataset/searchTest`, data);
|
||||
|
||||
/* ============================= file ==================================== */
|
||||
export const getDatasetFiles = (data: GetFileListProps) =>
|
||||
POST<DatasetFileItemType[]>(`/core/dataset/file/list`, data);
|
||||
export const delDatasetFileById = (params: { fileId: string; kbId: string }) =>
|
||||
DELETE(`/core/dataset/file/delById`, params);
|
||||
export const getFileInfoById = (fileId: string) =>
|
||||
GET<GSFileInfoType>(`/core/dataset/file/detail`, { fileId });
|
||||
export const delDatasetEmptyFiles = (kbId: string) =>
|
||||
DELETE(`/core/dataset/file/delEmptyFiles`, { kbId });
|
||||
|
||||
export const updateDatasetFile = (data: UpdateFileProps) => PUT(`/core/dataset/file/update`, data);
|
||||
|
||||
export const putMarkFilesUsed = (data: MarkFileUsedProps) =>
|
||||
PUT(`/core/dataset/file/markUsed`, data);
|
||||
|
||||
/* =============================== data ==================================== */
|
||||
|
||||
/* kb data */
|
||||
export const getDatasetDataList = (data: GetDatasetDataListProps) =>
|
||||
POST(`/core/dataset/data/getDataList`, data);
|
||||
|
||||
/**
|
||||
* export and download data
|
||||
*/
|
||||
export const exportDatasetData = (data: { kbId: string }) =>
|
||||
fetch(`/api/core/dataset/data/exportAll?kbId=${data.kbId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
token: getToken()
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data?.message || 'Export failed');
|
||||
}
|
||||
return res.blob();
|
||||
})
|
||||
.then((blob) => download(blob, 'dataset.csv', 'text/csv'));
|
||||
|
||||
/**
|
||||
* 获取模型正在拆分数据的数量
|
||||
*/
|
||||
export const getTrainingData = (data: { kbId: string; init: boolean }) =>
|
||||
POST<{
|
||||
qaListLen: number;
|
||||
vectorListLen: number;
|
||||
}>(`/core/dataset/data/getTrainingData`, data);
|
||||
|
||||
/* get length of system training queue */
|
||||
export const getTrainingQueueLen = () => GET<number>(`/core/dataset/data/getQueueLen`);
|
||||
|
||||
export const getDatasetDataItemById = (dataId: string) =>
|
||||
GET<QuoteItemType>(`/core/dataset/data/getDataById`, { dataId });
|
||||
|
||||
/**
|
||||
* push data to training queue
|
||||
*/
|
||||
export const postChunks2Dataset = (data: PushDataProps) =>
|
||||
POST<PushDataResponse>(`/core/dataset/data/pushData`, data);
|
||||
|
||||
/**
|
||||
* insert one data to dataset (immediately insert)
|
||||
*/
|
||||
export const postData2Dataset = (data: { kbId: string; data: DatasetDataItemType }) =>
|
||||
POST<string>(`/core/dataset/data/insertData`, data);
|
||||
|
||||
/**
|
||||
* 更新一条数据
|
||||
*/
|
||||
export const putDatasetDataById = (data: UpdateDatasetDataPrams) =>
|
||||
PUT('/core/dataset/data/updateData', data);
|
||||
/**
|
||||
* 删除一条知识库数据
|
||||
*/
|
||||
export const delOneDatasetDataById = (dataId: string) =>
|
||||
DELETE(`/core/dataset/data/delDataById?dataId=${dataId}`);
|
119
projects/app/src/web/core/store/chat.ts
Normal file
119
projects/app/src/web/core/store/chat.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
import { ChatHistoryItemType } from '@/types/chat';
|
||||
import type { InitChatResponse } from '@/global/core/api/chatRes.d';
|
||||
import { delChatHistoryById, getChatHistory, clearChatHistoryByAppId } from '@/web/core/api/chat';
|
||||
|
||||
type State = {
|
||||
history: ChatHistoryItemType[];
|
||||
loadHistory: (data: { appId?: string }) => Promise<null>;
|
||||
delHistory(history: string): Promise<void>;
|
||||
clearHistory(appId: string): Promise<void>;
|
||||
updateHistory: (history: ChatHistoryItemType) => void;
|
||||
chatData: InitChatResponse;
|
||||
setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
|
||||
lastChatAppId: string;
|
||||
setLastChatAppId: (id: string) => void;
|
||||
lastChatId: string;
|
||||
setLastChatId: (id: string) => void;
|
||||
};
|
||||
|
||||
const defaultChatData: InitChatResponse = {
|
||||
chatId: '',
|
||||
appId: '',
|
||||
app: {
|
||||
name: 'FastGPT',
|
||||
avatar: '/icon/logo.svg',
|
||||
intro: '',
|
||||
canUse: false
|
||||
},
|
||||
title: '新对话',
|
||||
variables: {},
|
||||
history: []
|
||||
};
|
||||
|
||||
export const useChatStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
lastChatAppId: '',
|
||||
setLastChatAppId(id: string) {
|
||||
set((state) => {
|
||||
state.lastChatAppId = id;
|
||||
});
|
||||
},
|
||||
lastChatId: '',
|
||||
setLastChatId(id: string) {
|
||||
set((state) => {
|
||||
state.lastChatId = id;
|
||||
});
|
||||
},
|
||||
history: [],
|
||||
async loadHistory({ appId }) {
|
||||
const oneHistory = get().history[0];
|
||||
if (oneHistory && oneHistory.appId === appId) return null;
|
||||
const data = await getChatHistory({
|
||||
appId,
|
||||
pageNum: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
set((state) => {
|
||||
state.history = data;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
async delHistory(chatId) {
|
||||
set((state) => {
|
||||
state.history = state.history.filter((item) => item.chatId !== chatId);
|
||||
});
|
||||
await delChatHistoryById(chatId);
|
||||
},
|
||||
async clearHistory(appId) {
|
||||
set((state) => {
|
||||
state.history = [];
|
||||
});
|
||||
await clearChatHistoryByAppId(appId);
|
||||
},
|
||||
updateHistory(history) {
|
||||
const index = get().history.findIndex((item) => item.chatId === history.chatId);
|
||||
set((state) => {
|
||||
const newHistory = (() => {
|
||||
if (index > -1) {
|
||||
return [
|
||||
history,
|
||||
...get().history.slice(0, index),
|
||||
...get().history.slice(index + 1)
|
||||
];
|
||||
} else {
|
||||
return [history, ...state.history];
|
||||
}
|
||||
})();
|
||||
|
||||
state.history = newHistory;
|
||||
});
|
||||
},
|
||||
chatData: defaultChatData,
|
||||
setChatData(e = defaultChatData) {
|
||||
if (typeof e === 'function') {
|
||||
set((state) => {
|
||||
state.chatData = e(state.chatData);
|
||||
});
|
||||
} else {
|
||||
set((state) => {
|
||||
state.chatData = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'chatStore',
|
||||
partialize: (state) => ({
|
||||
lastChatAppId: state.lastChatAppId,
|
||||
lastChatId: state.lastChatId
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
110
projects/app/src/web/core/store/dataset.ts
Normal file
110
projects/app/src/web/core/store/dataset.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { SearchTestItemType } from '@/types/core/dataset';
|
||||
import type { DatasetItemType, DatasetsItemType } from '@/types/core/dataset';
|
||||
import { getAllDataset, getDatasets, getDatasetById, putDatasetById } from '@/web/core/api/dataset';
|
||||
import { defaultKbDetail } from '@/constants/dataset';
|
||||
import type { DatasetUpdateParams } from '@/global/core/api/datasetReq.d';
|
||||
|
||||
type State = {
|
||||
allDatasets: DatasetsItemType[];
|
||||
loadAllDatasets: () => Promise<DatasetsItemType[]>;
|
||||
myKbList: DatasetsItemType[];
|
||||
loadKbList: (parentId?: string) => Promise<any>;
|
||||
setKbList(val: DatasetsItemType[]): void;
|
||||
kbDetail: DatasetItemType;
|
||||
getKbDetail: (id: string, init?: boolean) => Promise<DatasetItemType>;
|
||||
updateDataset: (data: DatasetUpdateParams) => Promise<any>;
|
||||
|
||||
kbTestList: SearchTestItemType[];
|
||||
pushKbTestItem: (data: SearchTestItemType) => void;
|
||||
delKbTestItemById: (id: string) => void;
|
||||
updateKbItemById: (data: SearchTestItemType) => void;
|
||||
};
|
||||
|
||||
export const useDatasetStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
allDatasets: [],
|
||||
async loadAllDatasets() {
|
||||
const res = await getAllDataset();
|
||||
set((state) => {
|
||||
state.allDatasets = res;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
myKbList: [],
|
||||
async loadKbList(parentId = '') {
|
||||
const res = await getDatasets({ parentId });
|
||||
set((state) => {
|
||||
state.myKbList = res;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
setKbList(val) {
|
||||
set((state) => {
|
||||
state.myKbList = val;
|
||||
});
|
||||
},
|
||||
kbDetail: defaultKbDetail,
|
||||
async getKbDetail(id: string, init = false) {
|
||||
if (id === get().kbDetail._id && !init) return get().kbDetail;
|
||||
|
||||
const data = await getDatasetById(id);
|
||||
|
||||
set((state) => {
|
||||
state.kbDetail = data;
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
async updateDataset(data) {
|
||||
if (get().kbDetail._id === data.id) {
|
||||
set((state) => {
|
||||
state.kbDetail = {
|
||||
...state.kbDetail,
|
||||
...data
|
||||
};
|
||||
});
|
||||
}
|
||||
set((state) => {
|
||||
state.myKbList = state.myKbList = state.myKbList.map((item) =>
|
||||
item._id === data.id
|
||||
? {
|
||||
...item,
|
||||
...data,
|
||||
tags: data.tags?.split(' ') || []
|
||||
}
|
||||
: item
|
||||
);
|
||||
});
|
||||
await putDatasetById(data);
|
||||
},
|
||||
kbTestList: [],
|
||||
pushKbTestItem(data) {
|
||||
set((state) => {
|
||||
state.kbTestList = [data, ...state.kbTestList].slice(0, 500);
|
||||
});
|
||||
},
|
||||
delKbTestItemById(id) {
|
||||
set((state) => {
|
||||
state.kbTestList = state.kbTestList.filter((item) => item.id !== id);
|
||||
});
|
||||
},
|
||||
updateKbItemById(data: SearchTestItemType) {
|
||||
set((state) => {
|
||||
state.kbTestList = state.kbTestList.map((item) => (item.id === data.id ? data : item));
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'kbStore',
|
||||
partialize: (state) => ({
|
||||
kbTestList: state.kbTestList
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
137
projects/app/src/web/core/store/shareChat.ts
Normal file
137
projects/app/src/web/core/store/shareChat.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
import type { ChatSiteItemType, ShareChatHistoryItemType, ShareChatType } from '@/types/chat';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
|
||||
type State = {
|
||||
shareChatData: ShareChatType;
|
||||
setShareChatData: (e: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
|
||||
shareChatHistory: ShareChatHistoryItemType[];
|
||||
saveChatResponse: (e: {
|
||||
chatId: string;
|
||||
prompts: ChatSiteItemType[];
|
||||
variables: Record<string, any>;
|
||||
shareId: string;
|
||||
}) => void;
|
||||
delOneShareHistoryByChatId: (chatId: string) => void;
|
||||
delShareChatHistoryItemById: (e: { chatId: string; contentId?: string; index: number }) => void;
|
||||
delManyShareChatHistoryByShareId: (shareId?: string) => void;
|
||||
};
|
||||
|
||||
export const defaultHistory: ShareChatHistoryItemType = {
|
||||
chatId: `${Date.now()}`,
|
||||
updateTime: new Date(),
|
||||
title: '新对话',
|
||||
shareId: '',
|
||||
chats: []
|
||||
};
|
||||
const defaultShareChatData: ShareChatType = {
|
||||
userAvatar: HUMAN_ICON,
|
||||
app: {
|
||||
name: '',
|
||||
avatar: '/icon/logo.svg',
|
||||
intro: ''
|
||||
},
|
||||
history: defaultHistory
|
||||
};
|
||||
|
||||
export const useShareChatStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
shareChatData: defaultShareChatData,
|
||||
setShareChatData(e) {
|
||||
const val = (() => {
|
||||
if (typeof e === 'function') {
|
||||
return e(get().shareChatData);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
})();
|
||||
set((state) => {
|
||||
state.shareChatData = val;
|
||||
// update history
|
||||
state.shareChatHistory = state.shareChatHistory.map((item) =>
|
||||
item.chatId === val.history.chatId ? val.history : item
|
||||
);
|
||||
});
|
||||
},
|
||||
shareChatHistory: [],
|
||||
saveChatResponse({ chatId, prompts, variables, shareId }) {
|
||||
const chatHistory = get().shareChatHistory.find((item) => item.chatId === chatId);
|
||||
|
||||
const historyList = (() => {
|
||||
if (chatHistory) {
|
||||
return get().shareChatHistory.map((item) =>
|
||||
item.chatId === chatId
|
||||
? {
|
||||
...item,
|
||||
title: prompts[prompts.length - 2]?.value,
|
||||
updateTime: new Date(),
|
||||
chats: chatHistory.chats.concat(prompts).slice(-50),
|
||||
variables
|
||||
}
|
||||
: item
|
||||
);
|
||||
}
|
||||
return get().shareChatHistory.concat({
|
||||
chatId,
|
||||
shareId,
|
||||
title: prompts[prompts.length - 2]?.value,
|
||||
updateTime: new Date(),
|
||||
chats: prompts,
|
||||
variables
|
||||
});
|
||||
})();
|
||||
|
||||
// @ts-ignore
|
||||
historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
|
||||
|
||||
set((state) => {
|
||||
state.shareChatHistory = historyList.slice(0, 100);
|
||||
});
|
||||
},
|
||||
delOneShareHistoryByChatId(chatId: string) {
|
||||
set((state) => {
|
||||
state.shareChatHistory = state.shareChatHistory.filter(
|
||||
(item) => item.chatId !== chatId
|
||||
);
|
||||
});
|
||||
},
|
||||
delShareChatHistoryItemById({ chatId, contentId }) {
|
||||
set((state) => {
|
||||
// update history store
|
||||
const newHistoryList = state.shareChatHistory.map((item) =>
|
||||
item.chatId === chatId
|
||||
? {
|
||||
...item,
|
||||
chats: item.chats.filter((item) => item.dataId !== contentId)
|
||||
}
|
||||
: item
|
||||
);
|
||||
state.shareChatHistory = newHistoryList;
|
||||
});
|
||||
},
|
||||
delManyShareChatHistoryByShareId(shareId?: string) {
|
||||
set((state) => {
|
||||
if (shareId) {
|
||||
state.shareChatHistory = state.shareChatHistory.filter(
|
||||
(item) => item.shareId !== shareId
|
||||
);
|
||||
} else {
|
||||
state.shareChatHistory = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'shareChatStore',
|
||||
partialize: (state) => ({
|
||||
shareChatHistory: state.shareChatHistory
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
53
projects/app/src/web/core/utils/dataset.ts
Normal file
53
projects/app/src/web/core/utils/dataset.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { postCreateTrainingBill } from '@/web/common/api/bill';
|
||||
import { postChunks2Dataset } from '@/web/core/api/dataset';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import type { DatasetDataItemType } from '@/types/core/dataset/data';
|
||||
import { delay } from '@/utils/tools';
|
||||
|
||||
export async function chunksUpload({
|
||||
kbId,
|
||||
mode,
|
||||
chunks,
|
||||
prompt,
|
||||
rate = 150,
|
||||
onUploading
|
||||
}: {
|
||||
kbId: string;
|
||||
mode: `${TrainingModeEnum}`;
|
||||
chunks: DatasetDataItemType[];
|
||||
prompt?: string;
|
||||
rate?: number;
|
||||
onUploading?: (insertLen: number, total: number) => void;
|
||||
}) {
|
||||
// create training bill
|
||||
const billId = await postCreateTrainingBill({ name: 'dataset.Training Name' });
|
||||
|
||||
async function upload(data: DatasetDataItemType[]) {
|
||||
return postChunks2Dataset({
|
||||
kbId,
|
||||
data,
|
||||
mode,
|
||||
prompt,
|
||||
billId
|
||||
});
|
||||
}
|
||||
|
||||
let successInsert = 0;
|
||||
let retryTimes = 10;
|
||||
for (let i = 0; i < chunks.length; i += rate) {
|
||||
try {
|
||||
const { insertLen } = await upload(chunks.slice(i, i + rate));
|
||||
onUploading && onUploading(i + rate, chunks.length);
|
||||
successInsert += insertLen;
|
||||
} catch (error) {
|
||||
if (retryTimes === 0) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
await delay(1000);
|
||||
retryTimes--;
|
||||
i -= rate;
|
||||
}
|
||||
}
|
||||
|
||||
return { insertLen: successInsert };
|
||||
}
|
24
projects/app/src/web/styles/default.scss
Normal file
24
projects/app/src/web/styles/default.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
#nprogress .bar {
|
||||
background: '#1237b3' !important; //自定义颜色
|
||||
}
|
||||
|
||||
.textEllipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.textEllipsis3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.grecaptcha-badge {
|
||||
display: none !important;
|
||||
}
|
||||
.textlg {
|
||||
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
6
projects/app/src/web/styles/reactflow.scss
Normal file
6
projects/app/src/web/styles/reactflow.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.react-flow__panel {
|
||||
display: none;
|
||||
}
|
||||
.react-flow__panel.react-flow__attribution {
|
||||
display: none !important;
|
||||
}
|
93
projects/app/src/web/styles/reset.scss
Normal file
93
projects/app/src/web/styles/reset.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
@import './reactflow.scss';
|
||||
@import './default.scss';
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
hr,
|
||||
p,
|
||||
blockquote,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
pre,
|
||||
form,
|
||||
fieldset,
|
||||
legend,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
th,
|
||||
td,
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(189, 193, 197, 0.7);
|
||||
border-radius: 2px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(189, 193, 197, 1);
|
||||
}
|
||||
|
||||
div {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
transition: background 1s;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(189, 193, 197, 0.7) !important;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(189, 193, 197, 1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
-webkit-focus-ring-color: rgba(0, 0, 0, 0);
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#__next {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
body {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
302
projects/app/src/web/styles/theme.ts
Normal file
302
projects/app/src/web/styles/theme.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { extendTheme, defineStyleConfig, ComponentStyleConfig } from '@chakra-ui/react';
|
||||
import { modalAnatomy, switchAnatomy, selectAnatomy, numberInputAnatomy } from '@chakra-ui/anatomy';
|
||||
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
|
||||
|
||||
const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(
|
||||
modalAnatomy.keys
|
||||
);
|
||||
const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle } =
|
||||
createMultiStyleConfigHelpers(switchAnatomy.keys);
|
||||
const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
|
||||
createMultiStyleConfigHelpers(selectAnatomy.keys);
|
||||
const { definePartsStyle: numInputPart, defineMultiStyleConfig: numInputMultiStyle } =
|
||||
createMultiStyleConfigHelpers(numberInputAnatomy.keys);
|
||||
|
||||
// 按键
|
||||
const Button = defineStyleConfig({
|
||||
baseStyle: {
|
||||
_active: {
|
||||
transform: 'scale(0.98)'
|
||||
}
|
||||
},
|
||||
sizes: {
|
||||
xs: {
|
||||
fontSize: 'xs',
|
||||
px: 3,
|
||||
py: 0,
|
||||
fontWeight: 'normal',
|
||||
height: '22px',
|
||||
borderRadius: '2px'
|
||||
},
|
||||
sm: {
|
||||
fontSize: 'sm',
|
||||
px: 4,
|
||||
py: 0,
|
||||
fontWeight: 'normal',
|
||||
height: '26px',
|
||||
borderRadius: '2px'
|
||||
},
|
||||
md: {
|
||||
fontSize: 'md',
|
||||
px: 6,
|
||||
py: 0,
|
||||
height: '32px',
|
||||
fontWeight: 'normal',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
lg: {
|
||||
fontSize: 'lg',
|
||||
px: 8,
|
||||
py: 0,
|
||||
height: '42px',
|
||||
fontWeight: 'normal',
|
||||
borderRadius: '8px'
|
||||
}
|
||||
},
|
||||
variants: {
|
||||
primary: {
|
||||
backgroundImage:
|
||||
'linear-gradient(to bottom right, #2152d9 0%,#3370ff 40%, #4e83fd 100%) !important',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
_hover: {
|
||||
filter: 'brightness(115%)'
|
||||
},
|
||||
_disabled: {
|
||||
bg: '#3370ff !important'
|
||||
}
|
||||
},
|
||||
base: {
|
||||
color: 'myGray.900',
|
||||
border: '1px solid',
|
||||
borderColor: 'myGray.200',
|
||||
bg: 'transparent',
|
||||
transition: 'background 0.3s',
|
||||
_hover: {
|
||||
color: 'myBlue.600',
|
||||
bg: 'myWhite.400'
|
||||
},
|
||||
_active: {
|
||||
color: 'myBlue.700'
|
||||
},
|
||||
_disabled: { bg: 'myGray.100 !important', color: 'myGray.700 !important' }
|
||||
}
|
||||
},
|
||||
defaultProps: {
|
||||
size: 'md',
|
||||
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 NumberInput = numInputMultiStyle({
|
||||
variants: {
|
||||
outline: numInputPart({
|
||||
field: {
|
||||
bg: 'myWhite.300',
|
||||
border: '1px solid',
|
||||
borderRadius: 'base',
|
||||
borderColor: 'myGray.200',
|
||||
_focus: {
|
||||
borderColor: 'myBlue.600 !important',
|
||||
boxShadow: '0px 0px 4px #A8DBFF !important',
|
||||
bg: 'transparent'
|
||||
},
|
||||
_disabled: {
|
||||
color: 'myGray.400 !important',
|
||||
bg: 'myWhite.300 !important'
|
||||
}
|
||||
},
|
||||
stepper: {
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
color: 'myGray.600',
|
||||
_active: {
|
||||
color: 'myBlue.600'
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
defaultProps: {
|
||||
variant: 'outline'
|
||||
}
|
||||
});
|
||||
|
||||
const Textarea: ComponentStyleConfig = {
|
||||
variants: {
|
||||
outline: {
|
||||
border: '1px solid',
|
||||
borderRadius: 'base',
|
||||
borderColor: 'myGray.200',
|
||||
_hover: {
|
||||
borderColor: ''
|
||||
},
|
||||
_focus: {
|
||||
borderColor: 'myBlue.600',
|
||||
boxShadow: '0px 0px 4px #A8DBFF',
|
||||
bg: 'white'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// 全局主题
|
||||
export const theme = extendTheme({
|
||||
styles: {
|
||||
global: {
|
||||
'html, body': {
|
||||
color: 'myGray.900',
|
||||
fontSize: 'md',
|
||||
fontWeight: 400,
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
a: {
|
||||
color: 'myBlue.700'
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
myWhite: {
|
||||
100: '#FEFEFE',
|
||||
200: '#FDFDFE',
|
||||
300: '#FBFBFC',
|
||||
400: '#F8FAFB',
|
||||
500: '#F6F8F9',
|
||||
600: '#F4F6F8',
|
||||
700: '#C3C5C6',
|
||||
800: '#929495',
|
||||
900: '#626263',
|
||||
1000: '#313132'
|
||||
},
|
||||
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'
|
||||
},
|
||||
myRead: {
|
||||
600: '#ff4d4f'
|
||||
}
|
||||
},
|
||||
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 #DAE0E2',
|
||||
lg: '1px solid #D0E0E2'
|
||||
},
|
||||
shadows: {
|
||||
sm: '0 0 5px rgba(0,0,0,0.1)',
|
||||
md: '0 0 8px rgba(0,0,0,0.1)',
|
||||
base: '0 0 10px rgba(0,0,0,0.15)',
|
||||
lg: '0 0 10px rgba(0,0,0,0.2)'
|
||||
},
|
||||
breakpoints: {
|
||||
sm: '900px',
|
||||
md: '1200px',
|
||||
lg: '1500px',
|
||||
xl: '1800px',
|
||||
'2xl': '2100px'
|
||||
},
|
||||
lgColor: {
|
||||
activeBlueGradient: 'linear-gradient(to bottom right, #d6e8ff 0%, #f0f7ff 100%)',
|
||||
hoverBlueGradient: 'linear-gradient(to top left, #d6e8ff 0%, #f0f7ff 100%)',
|
||||
primary: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 40%, #4e83fd 100%)',
|
||||
primary2: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 30%,#4e83fd 80%, #85b1ff 100%)'
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
Switch,
|
||||
Select,
|
||||
NumberInput
|
||||
}
|
||||
});
|
26
projects/app/src/web/support/api/openapi.ts
Normal file
26
projects/app/src/web/support/api/openapi.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { GET, POST, DELETE } from '@/web/common/api/request';
|
||||
import type { EditApiKeyProps, GetApiKeyProps } from '@/global/support/api/openapiReq.d';
|
||||
import type { OpenApiSchema } from '@/types/support/openapi';
|
||||
|
||||
/**
|
||||
* crete a api key
|
||||
*/
|
||||
export const createAOpenApiKey = (data: EditApiKeyProps) =>
|
||||
POST<string>('/support/openapi/postKey', data);
|
||||
|
||||
/**
|
||||
* update a api key
|
||||
*/
|
||||
export const putOpenApiKey = (data: EditApiKeyProps & { _id: string }) =>
|
||||
POST<string>('/support/openapi/putKey', data);
|
||||
|
||||
/**
|
||||
* get api keys
|
||||
*/
|
||||
export const getOpenApiKeys = (params?: GetApiKeyProps) =>
|
||||
GET<OpenApiSchema[]>('/support/openapi/getKeys', params);
|
||||
|
||||
/**
|
||||
* delete api by id
|
||||
*/
|
||||
export const delOpenApiById = (id: string) => DELETE(`/support/openapi/delKey?id=${id}`);
|
34
projects/app/src/web/support/api/outLink.ts
Normal file
34
projects/app/src/web/support/api/outLink.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { GET, POST, DELETE } from '@/web/common/api/request';
|
||||
import type { InitShareChatResponse } from '@/global/support/api/outLinkRes.d';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
import type { OutLinkSchema } from '@/types/support/outLink';
|
||||
|
||||
/**
|
||||
* 初始化分享聊天
|
||||
*/
|
||||
export const initShareChatInfo = (data: { shareId: string; authToken?: string }) =>
|
||||
GET<InitShareChatResponse>(`/support/outLink/init`, data);
|
||||
|
||||
/**
|
||||
* create a shareChat
|
||||
*/
|
||||
export const createShareChat = (
|
||||
data: OutLinkEditType & {
|
||||
appId: string;
|
||||
type: OutLinkSchema['type'];
|
||||
}
|
||||
) => POST<string>(`/support/outLink/create`, data);
|
||||
|
||||
export const putShareChat = (data: OutLinkEditType) =>
|
||||
POST<string>(`/support/outLink/update`, data);
|
||||
|
||||
/**
|
||||
* get shareChat
|
||||
*/
|
||||
export const getShareChatList = (appId: string) =>
|
||||
GET<OutLinkSchema[]>(`/support/outLink/list`, { appId });
|
||||
|
||||
/**
|
||||
* delete a shareChat
|
||||
*/
|
||||
export const delShareChatById = (id: string) => DELETE(`/support/outLink/delete?id=${id}`);
|
88
projects/app/src/web/support/api/user.ts
Normal file
88
projects/app/src/web/support/api/user.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { GET, POST, PUT } from '@/web/common/api/request';
|
||||
import { createHashPassword } from '@/utils/tools';
|
||||
import type { ResLogin, PromotionRecordType } from '@/global/support/api/userRes.d';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { UserType, UserUpdateParams } from '@/types/user';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { informSchema } from '@/types/mongoSchema';
|
||||
import { OAuthEnum } from '@/constants/user';
|
||||
|
||||
export const sendAuthCode = (data: {
|
||||
username: string;
|
||||
type: `${UserAuthTypeEnum}`;
|
||||
googleToken: string;
|
||||
}) => POST(`/plusApi/user/inform/sendAuthCode`, data);
|
||||
|
||||
export const getTokenLogin = () => GET<UserType>('/user/account/tokenLogin');
|
||||
export const oauthLogin = (params: {
|
||||
type: `${OAuthEnum}`;
|
||||
code: string;
|
||||
callbackUrl: string;
|
||||
inviterId?: string;
|
||||
}) => POST<ResLogin>('/plusApi/user/account/login/oauth', params);
|
||||
|
||||
export const postRegister = ({
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
inviterId
|
||||
}: {
|
||||
username: string;
|
||||
code: string;
|
||||
password: string;
|
||||
inviterId?: string;
|
||||
}) =>
|
||||
POST<ResLogin>(`/plusApi/user/account/register/emailAndPhone`, {
|
||||
username,
|
||||
code,
|
||||
inviterId,
|
||||
password: createHashPassword(password)
|
||||
});
|
||||
|
||||
export const postFindPassword = ({
|
||||
username,
|
||||
code,
|
||||
password
|
||||
}: {
|
||||
username: string;
|
||||
code: string;
|
||||
password: string;
|
||||
}) =>
|
||||
POST<ResLogin>(`/plusApi/user/account/password/updateByCode`, {
|
||||
username,
|
||||
code,
|
||||
password: createHashPassword(password)
|
||||
});
|
||||
|
||||
export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw: string }) =>
|
||||
POST('/user/account/updatePasswordByOld', {
|
||||
oldPsw: createHashPassword(oldPsw),
|
||||
newPsw: createHashPassword(newPsw)
|
||||
});
|
||||
|
||||
export const postLogin = ({ username, password }: { username: string; password: string }) =>
|
||||
POST<ResLogin>('/user/account/loginByPassword', {
|
||||
username,
|
||||
password: createHashPassword(password)
|
||||
});
|
||||
|
||||
export const loginOut = () => GET('/user/account/loginout');
|
||||
|
||||
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/account/update', data);
|
||||
|
||||
export const getInforms = (data: RequestPaging) =>
|
||||
POST<PagingData<informSchema>>(`/user/inform/list`, data);
|
||||
|
||||
export const getUnreadCount = () => GET<number>(`/user/inform/countUnread`);
|
||||
export const readInform = (id: string) => GET(`/user/inform/read`, { id });
|
||||
|
||||
/* get promotion init data */
|
||||
export const getPromotionInitData = () =>
|
||||
GET<{
|
||||
invitedAmount: number;
|
||||
earningsAmount: number;
|
||||
}>('/user/promotion/getPromotionData');
|
||||
|
||||
/* promotion records */
|
||||
export const getPromotionRecords = (data: RequestPaging) =>
|
||||
POST<PromotionRecordType>(`/user/promotion/getPromotions`, data);
|
81
projects/app/src/web/support/hooks/useSendCode.ts
Normal file
81
projects/app/src/web/support/hooks/useSendCode.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { sendAuthCode } from '@/web/support/api/user';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { feConfigs } from '@/web/common/store/static';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
let timer: any;
|
||||
|
||||
export const useSendCode = () => {
|
||||
const { toast } = useToast();
|
||||
const [codeSending, setCodeSending] = useState(false);
|
||||
const [codeCountDown, setCodeCountDown] = useState(0);
|
||||
const sendCodeText = useMemo(() => {
|
||||
if (codeCountDown >= 10) {
|
||||
return `${codeCountDown}s后重新获取`;
|
||||
}
|
||||
if (codeCountDown > 0) {
|
||||
return `0${codeCountDown}s后重新获取`;
|
||||
}
|
||||
return '获取验证码';
|
||||
}, [codeCountDown]);
|
||||
|
||||
const sendCode = useCallback(
|
||||
async ({ username, type }: { username: string; type: `${UserAuthTypeEnum}` }) => {
|
||||
setCodeSending(true);
|
||||
try {
|
||||
await sendAuthCode({
|
||||
username,
|
||||
type,
|
||||
googleToken: await getClientToken(feConfigs.googleClientVerKey)
|
||||
});
|
||||
setCodeCountDown(60);
|
||||
timer = setInterval(() => {
|
||||
setCodeCountDown((val) => {
|
||||
if (val <= 0) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
return val - 1;
|
||||
});
|
||||
}, 1000);
|
||||
toast({
|
||||
title: '验证码已发送',
|
||||
status: 'success',
|
||||
position: 'top'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: getErrText(error, '验证码发送异常'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setCodeSending(false);
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return {
|
||||
codeSending,
|
||||
sendCode,
|
||||
sendCodeText,
|
||||
codeCountDown
|
||||
};
|
||||
};
|
||||
|
||||
export function getClientToken(googleClientVerKey?: string) {
|
||||
if (!googleClientVerKey || typeof window.grecaptcha === 'undefined' || !window.grecaptcha?.ready)
|
||||
return '';
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
window.grecaptcha.ready(async () => {
|
||||
try {
|
||||
const token = await window.grecaptcha.execute(googleClientVerKey, {
|
||||
action: 'submit'
|
||||
});
|
||||
resolve(token);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
109
projects/app/src/web/support/store/user.ts
Normal file
109
projects/app/src/web/support/store/user.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { UserType, UserUpdateParams } from '@/types/user';
|
||||
import { getMyModels, getModelById, putAppById } from '@/web/core/api/app';
|
||||
import { formatPrice } from '@fastgpt/common/bill/index';
|
||||
import { getTokenLogin, putUserInfo } from '@/web/support/api/user';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
import { AppListItemType, AppUpdateParams } from '@/types/app';
|
||||
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
type State = {
|
||||
userInfo: UserType | null;
|
||||
initUserInfo: () => Promise<UserType>;
|
||||
setUserInfo: (user: UserType | null) => void;
|
||||
updateUserInfo: (user: UserUpdateParams) => Promise<void>;
|
||||
myApps: AppListItemType[];
|
||||
myCollectionApps: AppListItemType[];
|
||||
loadMyApps: (init?: boolean) => Promise<AppListItemType[]>;
|
||||
appDetail: AppSchema;
|
||||
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
|
||||
updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
|
||||
clearAppModules(): void;
|
||||
};
|
||||
|
||||
export const useUserStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
userInfo: null,
|
||||
async initUserInfo() {
|
||||
const res = await getTokenLogin();
|
||||
get().setUserInfo(res);
|
||||
return res;
|
||||
},
|
||||
setUserInfo(user: UserType | null) {
|
||||
set((state) => {
|
||||
state.userInfo = user
|
||||
? {
|
||||
...user,
|
||||
balance: formatPrice(user.balance)
|
||||
}
|
||||
: null;
|
||||
});
|
||||
},
|
||||
async updateUserInfo(user: UserUpdateParams) {
|
||||
const oldInfo = (get().userInfo ? { ...get().userInfo } : null) as UserType | null;
|
||||
set((state) => {
|
||||
if (!state.userInfo) return;
|
||||
state.userInfo = {
|
||||
...state.userInfo,
|
||||
...user
|
||||
};
|
||||
});
|
||||
try {
|
||||
await putUserInfo(user);
|
||||
} catch (error) {
|
||||
set((state) => {
|
||||
state.userInfo = oldInfo;
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
myApps: [],
|
||||
myCollectionApps: [],
|
||||
async loadMyApps(init = true) {
|
||||
if (get().myApps.length > 0 && !init) return [];
|
||||
const res = await getMyModels();
|
||||
set((state) => {
|
||||
state.myApps = res;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
appDetail: defaultApp,
|
||||
async loadAppDetail(id: string, init = false) {
|
||||
if (id === get().appDetail._id && !init) return get().appDetail;
|
||||
|
||||
const res = await getModelById(id);
|
||||
set((state) => {
|
||||
state.appDetail = res;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
async updateAppDetail(appId: string, data: AppUpdateParams) {
|
||||
await putAppById(appId, data);
|
||||
set((state) => {
|
||||
state.appDetail = {
|
||||
...state.appDetail,
|
||||
...data
|
||||
};
|
||||
});
|
||||
},
|
||||
clearAppModules() {
|
||||
set((state) => {
|
||||
state.appDetail = {
|
||||
...state.appDetail,
|
||||
modules: []
|
||||
};
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'userStore',
|
||||
partialize: (state) => ({})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
Reference in New Issue
Block a user