v4.6 -1 (#459)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { sseResponseEventEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { TaskResponseKeyEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { parseStreamChunk, SSEParseData } from '@/utils/sse';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d';
|
||||
import { StartChatFnProps } from '@/components/ChatBox';
|
||||
import { getToken } from '@/web/support/user/auth';
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { GET, POST } from '@/web/common/api/request';
|
||||
import type { CreateTrainingBillType } from '@fastgpt/global/common/bill/types/billReq.d';
|
||||
import type { PaySchema } from '@fastgpt/global/support/wallet/type.d';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { UserBillType } from '@/types/user';
|
||||
import { delay } from '@/utils/tools';
|
||||
|
||||
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/support/user/pay/getPayCode`, { amount });
|
||||
|
||||
export const checkPayResult = (payId: string) =>
|
||||
GET<number>(`/plusApi/support/user/pay/checkPayResult`, { payId }).then(() => {
|
||||
async function startQueue() {
|
||||
try {
|
||||
await GET('/user/account/paySuccess');
|
||||
} catch (error) {
|
||||
await delay(1000);
|
||||
startQueue();
|
||||
}
|
||||
}
|
||||
startQueue();
|
||||
return 'success';
|
||||
});
|
||||
17
projects/app/src/web/common/file/api.ts
Normal file
17
projects/app/src/web/common/file/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const postUploadImg = (base64Img: string) =>
|
||||
POST<string>('/common/file/uploadImage', { base64Img });
|
||||
|
||||
export const postUploadFiles = (
|
||||
data: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST<string[]>('/common/file/upload', data, {
|
||||
timeout: 60000,
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
104
projects/app/src/web/common/file/controller.ts
Normal file
104
projects/app/src/web/common/file/controller.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { postUploadImg, postUploadFiles } from '@/web/common/file/api';
|
||||
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
|
||||
|
||||
/**
|
||||
* upload file to mongo gridfs
|
||||
*/
|
||||
export const uploadFiles = ({
|
||||
files,
|
||||
bucketName,
|
||||
metadata = {},
|
||||
percentListen
|
||||
}: {
|
||||
files: File[];
|
||||
bucketName: `${BucketNameEnum}`;
|
||||
metadata?: Record<string, any>;
|
||||
percentListen?: (percent: number) => void;
|
||||
}) => {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
form.append('bucketName', bucketName);
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* compress image. response base64
|
||||
* @param maxSize The max size of the compressed image
|
||||
*/
|
||||
export const compressImgAndUpload = ({
|
||||
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('压缩图片异常');
|
||||
};
|
||||
});
|
||||
@@ -1,27 +1,6 @@
|
||||
import mammoth from 'mammoth';
|
||||
import Papa from 'papaparse';
|
||||
import { postUploadImg, postUploadFiles, getFileViewUrl } from '@/web/common/system/api';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
};
|
||||
import { postUploadImg } from '@/web/common/file/api';
|
||||
|
||||
/**
|
||||
* 读取 txt 文件内容
|
||||
@@ -147,7 +126,7 @@ export const readCsvContent = async (file: File) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* file download
|
||||
* file download by text
|
||||
*/
|
||||
export const fileDownload = ({
|
||||
text,
|
||||
@@ -169,15 +148,9 @@ export const fileDownload = ({
|
||||
// 添加链接到页面并触发下载
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
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();
|
||||
@@ -186,77 +159,3 @@ export const fileToBase64 = (file: File) => {
|
||||
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('压缩图片异常');
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
export const useConfirm = (props: {
|
||||
export const useConfirm = (props?: {
|
||||
title?: string | null;
|
||||
content?: string | null;
|
||||
bg?: string;
|
||||
showCancel?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { title = t('Warning'), content, bg } = props;
|
||||
const { title = t('Warning'), content, bg, showCancel = true } = props || {};
|
||||
const [customContent, setCustomContent] = useState(content);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@@ -52,18 +53,23 @@ export const useConfirm = (props: {
|
||||
{title}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>{customContent}</AlertDialogBody>
|
||||
<AlertDialogBody whiteSpace={'pre-wrap'} py={0}>
|
||||
{customContent}
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
{showCancel && (
|
||||
<Button
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
{...(bg && { bg: `${bg} !important` })}
|
||||
ml={4}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useCopyData = () => {
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
document.body?.removeChild(textarea);
|
||||
}
|
||||
|
||||
toast({
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
|
||||
export const postTextCensor = (data: { text: string }) =>
|
||||
POST<{ code?: number; message: string }>('/common/censor/text_baidu', data)
|
||||
.then((res) => {
|
||||
if (res?.code === 5000) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err?.code === 5000) {
|
||||
return Promise.reject(err.message);
|
||||
}
|
||||
return Promise.resolve('');
|
||||
});
|
||||
@@ -1,22 +1,4 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/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, {
|
||||
timeout: 60000,
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const getFileViewUrl = (fileId: string) => GET<string>('/system/file/readUrl', { fileId });
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
defaultExtractModels,
|
||||
defaultQGModels,
|
||||
defaultVectorModels
|
||||
} from '@/constants/model';
|
||||
} from '@fastgpt/global/core/ai/model';
|
||||
|
||||
export let feConfigs: FeConfigsType = {};
|
||||
export let priceMd = '';
|
||||
|
||||
@@ -2,7 +2,7 @@ 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';
|
||||
import { OAuthEnum } from '@fastgpt/global/support/user/constant';
|
||||
|
||||
type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string };
|
||||
|
||||
|
||||
@@ -1,73 +1,85 @@
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { getChatItemSpeech } from '@/web/core/chat/api';
|
||||
import { AppTTSConfigType } from '@/types/app';
|
||||
import { TTSTypeEnum } from '@/constants/app';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
export const useAudioPlay = (props?: { ttsUrl?: string }) => {
|
||||
const { ttsUrl } = props || {};
|
||||
export const useAudioPlay = (props?: { ttsConfig?: AppTTSConfigType }) => {
|
||||
const { t } = useTranslation();
|
||||
const { ttsConfig } = props || {};
|
||||
const { toast } = useToast();
|
||||
const [audio, setAudio] = useState<HTMLAudioElement>();
|
||||
const [audioLoading, setAudioLoading] = useState(false);
|
||||
const [audioPlaying, setAudioPlaying] = useState(false);
|
||||
|
||||
// Check whether the voice is supported
|
||||
const hasAudio = useMemo(() => {
|
||||
if (ttsUrl) return true;
|
||||
if (ttsConfig?.type === TTSTypeEnum.none) return false;
|
||||
const voices = window.speechSynthesis?.getVoices?.() || []; // 获取语言包
|
||||
const voice = voices.find((item) => {
|
||||
return item.lang === 'zh-CN';
|
||||
});
|
||||
return !!voice;
|
||||
}, [ttsUrl]);
|
||||
}, [ttsConfig]);
|
||||
|
||||
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 playAudio = async ({
|
||||
text,
|
||||
chatItemId,
|
||||
buffer
|
||||
}: {
|
||||
text: string;
|
||||
chatItemId?: string;
|
||||
buffer?: Buffer;
|
||||
}) => {
|
||||
text = text.replace(/\\n/g, '\n');
|
||||
try {
|
||||
// tts play
|
||||
if (audio && ttsConfig && ttsConfig?.type === TTSTypeEnum.model) {
|
||||
setAudioLoading(true);
|
||||
const { data } = buffer
|
||||
? { data: buffer }
|
||||
: await getChatItemSpeech({ chatItemId, ttsConfig, input: text });
|
||||
|
||||
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, '语音播报异常')
|
||||
const arrayBuffer = new Uint8Array(data).buffer;
|
||||
|
||||
const audioUrl = URL.createObjectURL(new Blob([arrayBuffer], { type: 'audio/mp3' }));
|
||||
|
||||
audio.src = audioUrl;
|
||||
audio.play();
|
||||
setAudioLoading(false);
|
||||
|
||||
return data;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
setAudioLoading(false);
|
||||
},
|
||||
[audio, toast, ttsUrl]
|
||||
);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(error, t('core.chat.Audio Speech Error'))
|
||||
});
|
||||
}
|
||||
setAudioLoading(false);
|
||||
};
|
||||
|
||||
const cancelAudio = useCallback(() => {
|
||||
if (audio) {
|
||||
@@ -78,14 +90,12 @@ export const useAudioPlay = (props?: { ttsUrl?: string }) => {
|
||||
setAudioPlaying(false);
|
||||
}, [audio]);
|
||||
|
||||
// listen ttsUrl update
|
||||
useEffect(() => {
|
||||
if (ttsUrl) {
|
||||
setAudio(new Audio());
|
||||
} else {
|
||||
setAudio(undefined);
|
||||
}
|
||||
}, [ttsUrl]);
|
||||
setAudio(new Audio());
|
||||
}, []);
|
||||
|
||||
// listen audio status
|
||||
useEffect(() => {
|
||||
if (audio) {
|
||||
audio.onplay = () => {
|
||||
|
||||
Reference in New Issue
Block a user