feat: app detail
This commit is contained in:
53
client/src/pages/api/app/data/tokenUsage.ts
Normal file
53
client/src/pages/api/app/data/tokenUsage.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Bill } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { ChatHistoryItemType } from '@/types/chat';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
/* get one app chat history content number. */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { appId, start, end } = req.body as { appId: string; start: Date; end: Date };
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const result = await Bill.aggregate([
|
||||
{
|
||||
$match: {
|
||||
appId: new Types.ObjectId(appId),
|
||||
userId: new Types.ObjectId(userId),
|
||||
time: { $gte: new Date(start) }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $year: '$time' },
|
||||
month: { $month: '$time' },
|
||||
day: { $dayOfMonth: '$time' }
|
||||
},
|
||||
tokenLen: { $sum: '$tokenLen' } // 对tokenLen的值求和
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } },
|
||||
tokenLen: 1
|
||||
}
|
||||
},
|
||||
{ $sort: { date: 1 } }
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: result
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
// 验证是否是该用户的 app
|
||||
await authApp({
|
||||
appId,
|
||||
userId
|
||||
|
||||
@@ -6,22 +6,22 @@ import { Types } from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, historyId } = req.query as {
|
||||
chatId: string;
|
||||
const { historyId, contentId } = req.query as {
|
||||
historyId: string;
|
||||
contentId: string;
|
||||
};
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (!chatId || !historyId) {
|
||||
if (!historyId || !contentId) {
|
||||
throw new Error('params is error');
|
||||
}
|
||||
|
||||
const history = await Chat.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_id: new Types.ObjectId(chatId),
|
||||
_id: new Types.ObjectId(historyId),
|
||||
userId: new Types.ObjectId(userId)
|
||||
}
|
||||
},
|
||||
@@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
'content._id': new Types.ObjectId(historyId)
|
||||
'content._id': new Types.ObjectId(contentId)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ import { Types } from 'mongoose';
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let {
|
||||
chatId,
|
||||
historyId,
|
||||
contentId,
|
||||
quoteId,
|
||||
sourceText = ''
|
||||
} = req.query as {
|
||||
chatId: string;
|
||||
historyId: string;
|
||||
contentId: string;
|
||||
quoteId: string;
|
||||
sourceText: string;
|
||||
};
|
||||
@@ -21,15 +21,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (!chatId || !historyId || !quoteId) {
|
||||
if (!contentId || !historyId || !quoteId) {
|
||||
throw new Error('params is error');
|
||||
}
|
||||
|
||||
await Chat.updateOne(
|
||||
{
|
||||
_id: new Types.ObjectId(chatId),
|
||||
_id: new Types.ObjectId(historyId),
|
||||
userId: new Types.ObjectId(userId),
|
||||
'content._id': new Types.ObjectId(historyId)
|
||||
'content._id': new Types.ObjectId(contentId)
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
|
||||
@@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } =
|
||||
await (async () => {
|
||||
if (historyId) {
|
||||
// auth chatId
|
||||
// auth historyId
|
||||
const chat = await Chat.findOne({
|
||||
_id: historyId,
|
||||
userId
|
||||
|
||||
@@ -53,14 +53,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
throw new Error('params is error');
|
||||
}
|
||||
|
||||
// auth model
|
||||
// auth app
|
||||
const { app } = await authApp({
|
||||
appId,
|
||||
userId
|
||||
});
|
||||
|
||||
const result = await appKbSearch({
|
||||
model: app,
|
||||
app,
|
||||
userId,
|
||||
fixedQuote: [],
|
||||
prompt: prompts[prompts.length - 1],
|
||||
@@ -81,21 +81,21 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
});
|
||||
|
||||
export async function appKbSearch({
|
||||
model,
|
||||
app,
|
||||
userId,
|
||||
fixedQuote = [],
|
||||
prompt,
|
||||
similarity = 0.8,
|
||||
limit = 5
|
||||
}: {
|
||||
model: AppSchema;
|
||||
app: AppSchema;
|
||||
userId: string;
|
||||
fixedQuote?: QuoteItemType[];
|
||||
prompt: ChatItemType;
|
||||
similarity: number;
|
||||
limit: number;
|
||||
}): Promise<Response> {
|
||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||
const modelConstantsData = ChatModelMap[app.chat.chatModel];
|
||||
|
||||
// get vector
|
||||
const promptVector = await openaiEmbedding({
|
||||
@@ -107,7 +107,7 @@ export async function appKbSearch({
|
||||
const res: any = await PgClient.query(
|
||||
`BEGIN;
|
||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||
select id,q,a,source from modelData where kb_id IN (${model.chat.relatedKbs
|
||||
select id,q,a,source from modelData where kb_id IN (${app.chat.relatedKbs
|
||||
.map((item) => `'${item}'`)
|
||||
.join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${
|
||||
promptVector[0]
|
||||
@@ -133,32 +133,32 @@ export async function appKbSearch({
|
||||
});
|
||||
|
||||
// 计算固定提示词的 token 数量
|
||||
const userSystemPrompt = model.chat.systemPrompt // user system prompt
|
||||
const userSystemPrompt = app.chat.systemPrompt // user system prompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: model.chat.systemPrompt
|
||||
value: app.chat.systemPrompt
|
||||
}
|
||||
]
|
||||
: [];
|
||||
const userLimitPrompt = [
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: model.chat.limitPrompt
|
||||
? model.chat.limitPrompt
|
||||
: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。`
|
||||
value: app.chat.limitPrompt
|
||||
? app.chat.limitPrompt
|
||||
: `知识库是关于 ${app.name} 的内容,参考知识库回答问题。与 "${app.name}" 无关内容,直接回复: "我不知道"。`
|
||||
}
|
||||
];
|
||||
|
||||
const fixedSystemTokens = modelToolMap.countTokens({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
messages: [...userSystemPrompt, ...userLimitPrompt]
|
||||
});
|
||||
|
||||
// filter part quote by maxToken
|
||||
const sliceResult = modelToolMap
|
||||
.tokenSlice({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens,
|
||||
messages: filterSearch.map((item, i) => ({
|
||||
obj: ChatRoleEnum.System,
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function extract({ agents, history = [], userChatInput, description
|
||||
agents.forEach((item) => {
|
||||
properties[item.key] = {
|
||||
type: 'string',
|
||||
description: item.desc
|
||||
description: item.value
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Types } from 'mongoose';
|
||||
import { moduleFetch } from '@/service/api/request';
|
||||
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
|
||||
import { FlowInputItemTypeEnum } from '@/constants/flow';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
||||
type FastGptWebChatProps = {
|
||||
@@ -168,6 +170,16 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
chatModel: 'gpt-3.5-turbo',
|
||||
userId,
|
||||
appId,
|
||||
textLen: 1,
|
||||
tokens: 100,
|
||||
type: BillTypeEnum.chat
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (stream) {
|
||||
res.status(500);
|
||||
|
||||
@@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
kbId: id
|
||||
});
|
||||
|
||||
// delete related model
|
||||
// delete related app
|
||||
await App.updateMany(
|
||||
{
|
||||
userId
|
||||
|
||||
198
client/src/pages/app/detail/components/Charts/TokenUsage.tsx
Normal file
198
client/src/pages/app/detail/components/Charts/TokenUsage.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { getTokenUsage } from '@/api/app';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const map = {
|
||||
blue: {
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(3, 190, 232, 0.42)' // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(0, 182, 240, 0)'
|
||||
}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
lineColor: '#36ADEF'
|
||||
},
|
||||
deepBlue: {
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(47, 112, 237, 0.42)' // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(94, 159, 235, 0)'
|
||||
}
|
||||
],
|
||||
global: false
|
||||
},
|
||||
lineColor: '#3293EC'
|
||||
},
|
||||
purple: {
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(211, 190, 255, 0.42)' // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(52, 60, 255, 0)'
|
||||
}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
lineColor: '#8172D8'
|
||||
},
|
||||
green: {
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(4, 209, 148, 0.42)' // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(19, 217, 181, 0)'
|
||||
}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
lineColor: '#00A9A6',
|
||||
max: 100
|
||||
}
|
||||
};
|
||||
|
||||
const TokenUsage = ({ appId }: { appId: string }) => {
|
||||
const { screenWidth } = useGlobalStore();
|
||||
|
||||
const Dom = useRef<HTMLDivElement>(null);
|
||||
const myChart = useRef<echarts.ECharts>();
|
||||
const { data = [] } = useQuery(['init'], () => getTokenUsage({ appId }));
|
||||
|
||||
const option = useMemo(
|
||||
() => ({
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
show: false,
|
||||
boundaryGap: false,
|
||||
data: data.map((item) => item.date)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: false,
|
||||
splitNumber: 5,
|
||||
max: Math.max(...data.map((item) => item.tokenLen)),
|
||||
min: 0
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
left: 5,
|
||||
right: 5,
|
||||
top: 5,
|
||||
bottom: 5
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
formatter: (e: any[]) => {
|
||||
const data = e[0];
|
||||
if (!data) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div>${dayjs(data.axisValue).format('YYYY/MM/DD')}</div>
|
||||
<div>${((e[0]?.value || 0) / 1000).toFixed(2)}k Tokens</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: data.map((item) => item.tokenLen),
|
||||
type: 'line',
|
||||
showSymbol: true,
|
||||
animationDuration: 300,
|
||||
animationEasingUpdate: 'linear',
|
||||
areaStyle: {
|
||||
color: map['blue'].backgroundColor
|
||||
},
|
||||
lineStyle: {
|
||||
width: '1',
|
||||
color: map['blue'].lineColor
|
||||
},
|
||||
itemStyle: {
|
||||
width: 1.5,
|
||||
color: map['blue'].lineColor
|
||||
},
|
||||
emphasis: {
|
||||
// highlight
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
[data]
|
||||
);
|
||||
|
||||
// init chart
|
||||
useEffect(() => {
|
||||
if (!Dom.current || myChart?.current?.getOption()) return;
|
||||
myChart.current = echarts.init(Dom.current);
|
||||
myChart.current && myChart.current.setOption(option);
|
||||
}, [Dom]);
|
||||
|
||||
// data changed, update
|
||||
useEffect(() => {
|
||||
if (!myChart.current || !myChart?.current?.getOption()) return;
|
||||
myChart.current.setOption(option);
|
||||
}, [data, option]);
|
||||
|
||||
// limit changed, update
|
||||
useEffect(() => {
|
||||
if (!myChart.current || !myChart?.current?.getOption()) return;
|
||||
myChart.current.setOption(option);
|
||||
}, [option]);
|
||||
|
||||
// resize chart
|
||||
useEffect(() => {
|
||||
if (!myChart.current || !myChart.current.getOption()) return;
|
||||
myChart.current.resize();
|
||||
}, [screenWidth]);
|
||||
|
||||
return <div ref={Dom} style={{ width: '100%', height: '100%' }} />;
|
||||
};
|
||||
|
||||
export default React.memo(TokenUsage);
|
||||
194
client/src/pages/app/detail/components/InfoModal.tsx
Normal file
194
client/src/pages/app/detail/components/InfoModal.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { delModelById, putAppById } from '@/api/app';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const InfoModal = ({
|
||||
defaultApp,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
defaultApp: AppSchema;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: defaultApp
|
||||
});
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
// 提交保存模型修改
|
||||
const saveSubmitSuccess = useCallback(
|
||||
async (data: AppSchema) => {
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
await putAppById(data._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro,
|
||||
chat: data.chat,
|
||||
share: data.share
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setBtnLoading(false);
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
// 提交保存表单失败
|
||||
const saveSubmitError = useCallback(() => {
|
||||
// deep search message
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj) return '提交表单错误';
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
const saveUpdateModel = useCallback(
|
||||
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
|
||||
[handleSubmit, saveSubmitError, saveSubmitSuccess]
|
||||
);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const src = await compressImg({
|
||||
file,
|
||||
maxW: 100,
|
||||
maxH: 100
|
||||
});
|
||||
setValue('avatar', src);
|
||||
setRefresh((state) => !state);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, '头像选择异常'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'min(90vw,470px)'}>
|
||||
<ModalHeader>应用信息设置</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Box>头像 & 名称</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
w={['26px', '34px']}
|
||||
h={['26px', '34px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'lg'}
|
||||
mr={4}
|
||||
title={'点击切换头像'}
|
||||
onClick={() => onOpenSelectFile()}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={'给应用设置一个名称'}
|
||||
{...register('name', {
|
||||
required: '展示名称不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Box mt={6} mb={1}>
|
||||
应用介绍
|
||||
</Box>
|
||||
<Box color={'myGray.500'} mb={2} fontSize={'sm'}>
|
||||
该介绍主要用于记忆和在应用市场展示
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
placeholder={'给你的 AI 应用一个介绍'}
|
||||
bg={'myWhite.600'}
|
||||
{...register('intro')}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={btnLoading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await saveUpdateModel();
|
||||
onSuccess();
|
||||
onClose();
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoModal;
|
||||
@@ -1,97 +1,34 @@
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react';
|
||||
import { Box, Flex, Button, Grid, useTheme, BoxProps, IconButton } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { delModelById, putAppById } from '@/api/app';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { delModelById } from '@/api/app';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const InfoModal = dynamic(() => import('./InfoModal'));
|
||||
const TokenUsage = dynamic(() => import('./Charts/TokenUsage'));
|
||||
const AppEdit = dynamic(() => import('./edit'));
|
||||
import styles from '../../list/index.module.scss';
|
||||
|
||||
const Settings = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { userInfo, appDetail, myApps, loadAppDetail, setLastModelId } = useUserStore();
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
const { appDetail, loadAppDetail } = useUserStore();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该应用?'
|
||||
});
|
||||
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: appDetail
|
||||
});
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
|
||||
// 提交保存模型修改
|
||||
const saveSubmitSuccess = useCallback(
|
||||
async (data: AppSchema) => {
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
await putAppById(data._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro,
|
||||
chat: data.chat,
|
||||
share: data.share
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setBtnLoading(false);
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
// 提交保存表单失败
|
||||
const saveSubmitError = useCallback(() => {
|
||||
// deep search message
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj) return '提交表单错误';
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
const saveUpdateModel = useCallback(
|
||||
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
|
||||
[handleSubmit, saveSubmitError, saveSubmitSuccess]
|
||||
);
|
||||
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
/* 点击删除 */
|
||||
const handleDelModel = useCallback(async () => {
|
||||
@@ -113,156 +50,126 @@ const Settings = ({ appId }: { appId: string }) => {
|
||||
setIsLoading(false);
|
||||
}, [appDetail, setIsLoading, toast, router]);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const src = await compressImg({
|
||||
file,
|
||||
maxW: 100,
|
||||
maxH: 100
|
||||
});
|
||||
setValue('avatar', src);
|
||||
setRefresh((state) => !state);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, '头像选择异常'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, toast]
|
||||
);
|
||||
|
||||
// load model data
|
||||
const { isLoading } = useQuery([appId], () => loadAppDetail(appId, true), {
|
||||
onSuccess(res) {
|
||||
res && reset(res);
|
||||
appId && setLastModelId(appId);
|
||||
setRefresh(!refresh);
|
||||
},
|
||||
// load app data
|
||||
const { isLoading, refetch } = useQuery([appId], () => loadAppDetail(appId, true), {
|
||||
onError(err: any) {
|
||||
toast({
|
||||
title: err?.message || '获取应用异常',
|
||||
status: 'error'
|
||||
});
|
||||
setLastModelId('');
|
||||
router.replace('/model');
|
||||
router.replace('/app/list');
|
||||
},
|
||||
onSettled() {
|
||||
router.prefetch(`/chat?appId=${appId}`);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
pt={[0, 5]}
|
||||
pb={3}
|
||||
px={[5, '25px', '50px']}
|
||||
fontSize={['sm', 'lg']}
|
||||
position={'relative'}
|
||||
maxW={['auto', '800px']}
|
||||
>
|
||||
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
|
||||
基本信息
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
头像
|
||||
</Box>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
w={['32px', '40px']}
|
||||
h={['32px', '40px']}
|
||||
cursor={isOwner ? 'pointer' : 'default'}
|
||||
title={'点击切换头像'}
|
||||
onClick={() => isOwner && onOpenSelectFile()}
|
||||
/>
|
||||
</Flex>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
名称
|
||||
<Flex h={'100%'} flexDirection={'column'} position={'relative'}>
|
||||
<Box w={'100%'} pt={[0, 7]} px={[2, 5, 8]}>
|
||||
<Grid gridTemplateColumns={['1fr', 'repeat(2,1fr)']} gridGap={[2, 4, 6]}>
|
||||
<Box>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
概览
|
||||
</Box>
|
||||
<Box
|
||||
border={theme.borders.sm}
|
||||
borderRadius={'lg'}
|
||||
px={5}
|
||||
py={4}
|
||||
bg={'rgba(235,245,255,0.4)'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex alignItems={'center'} py={2}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
_hover={{
|
||||
bg: 'myGray.100',
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box className={styles.intro} py={3} wordBreak={'break-all'} color={'myGray.600'}>
|
||||
{appDetail.intro || '快来给应用一个介绍~'}
|
||||
</Box>
|
||||
<Flex>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
mx={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
|
||||
onClick={() => {
|
||||
router.replace({
|
||||
query: {
|
||||
appId,
|
||||
currentTab: 'share'
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
分享
|
||||
</Button>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
|
||||
onClick={() => setSettingAppInfo(appDetail)}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Input
|
||||
isDisabled={!isOwner}
|
||||
{...register('name', {
|
||||
required: '展示名称不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<Flex mt={5} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
介绍
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
placeholder={'给你的 AI 应用一个介绍'}
|
||||
{...register('intro')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
近 7 日 Tokens 消耗
|
||||
</Box>
|
||||
<Box h={'150px'}>
|
||||
<TokenUsage appId={appId} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} position={'relative'}>
|
||||
<AppEdit
|
||||
app={appDetail}
|
||||
onFullScreen={(val) => setFullScreen(val)}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider mt={5} />
|
||||
{settingAppInfo && (
|
||||
<InfoModal
|
||||
defaultApp={settingAppInfo}
|
||||
onClose={() => setSettingAppInfo(undefined)}
|
||||
onSuccess={refetch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
|
||||
<Button
|
||||
mr={3}
|
||||
w={'120px'}
|
||||
size={['sm', 'md']}
|
||||
isLoading={btnLoading}
|
||||
isDisabled={!isOwner}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await saveUpdateModel();
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
error;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isOwner ? '保存' : '仅读,无法修改'}
|
||||
</Button>
|
||||
<Button
|
||||
mr={3}
|
||||
w={'100px'}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
color={'myBlue.600'}
|
||||
borderColor={'myBlue.600'}
|
||||
isLoading={btnLoading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
router.prefetch('/chat');
|
||||
await saveUpdateModel();
|
||||
} catch (error) {}
|
||||
router.push(`/chat?appId=${appId}`);
|
||||
}}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
variant={'base'}
|
||||
size={['sm', 'md']}
|
||||
isLoading={btnLoading}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
<ConfirmChild />
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={[0, 5]} px={5} minH={'50vh'}>
|
||||
<Box position={'relative'} pt={[0, 5, 8]} px={[5, 8]} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录聊天窗口
|
||||
@@ -150,33 +150,37 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||
<Td>
|
||||
<Flex>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
<MyTooltip label={'复制分享地址'}>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={'删除链接'}>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -28,7 +28,7 @@ const ModuleStoreList = ({
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
@@ -41,48 +41,50 @@ const ModuleStoreList = ({
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
h={isOpen ? '90%' : '0'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 100px)' : '0'}
|
||||
w={isOpen ? '360px' : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
px={'15px'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||
<Box w={'330px'} py={4} px={5} fontSize={'xl'} fontWeight={'bold'}>
|
||||
系统模块
|
||||
</Box>
|
||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||
{ModuleTemplates.map((item) =>
|
||||
item.list.map((item) => (
|
||||
<Flex
|
||||
key={item.name}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < 360) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} borderRadius={'0'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
||||
<Box w={'330px'} mx={'auto'}>
|
||||
{ModuleTemplates.map((item) =>
|
||||
item.list.map((item) => (
|
||||
<Flex
|
||||
key={item.name}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < 360) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} borderRadius={'0'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -75,12 +75,12 @@ const nodeTypes = {
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
type Props = { app: AppSchema; onBack: () => void };
|
||||
type Props = { app: AppSchema; fullScreen: boolean; onFullScreen: (val: boolean) => void };
|
||||
|
||||
const AppEdit = ({ app, onBack }: Props) => {
|
||||
const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
||||
const theme = useTheme();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||
const theme = useTheme();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
@@ -91,6 +91,14 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
} = useDisclosure();
|
||||
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
|
||||
|
||||
const onFixView = useCallback(() => {
|
||||
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
|
||||
|
||||
setTimeout(() => {
|
||||
btn && btn.click();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
@@ -258,23 +266,57 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
}, [app, initData]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
<>
|
||||
{/* header */}
|
||||
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={onBack}
|
||||
/>
|
||||
<Box ml={5} fontSize={'xl'} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
<Flex
|
||||
py={3}
|
||||
px={[2, 5, 8]}
|
||||
borderBottom={theme.borders.base}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{fullScreen ? (
|
||||
<>
|
||||
<MyTooltip label={'取消全屏'} offset={[10, 10]}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
icon={<MyIcon name={'fullScreenLight'} w={'16px'} />}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={() => {
|
||||
onFullScreen(false);
|
||||
onFixView();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box ml={5} fontSize={['lg', '2xl']} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box fontSize={['lg', '2xl']} flex={1}>
|
||||
应用编排
|
||||
</Box>
|
||||
<MyTooltip label={'全屏'}>
|
||||
<IconButton
|
||||
mr={6}
|
||||
icon={<MyIcon name={'fullScreenLight'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'fullScreenLight'}
|
||||
onClick={() => {
|
||||
onFullScreen(true);
|
||||
onFixView();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</>
|
||||
)}
|
||||
{testModules ? (
|
||||
<IconButton
|
||||
mr={6}
|
||||
@@ -361,11 +403,7 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls
|
||||
position={'bottom-center'}
|
||||
style={{ display: 'flex' }}
|
||||
showInteractive={false}
|
||||
/>
|
||||
<Controls position={'bottom-right'} style={{ display: 'flex' }} showInteractive={false} />
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
||||
@@ -376,14 +414,26 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
onClose={() => setTestModules(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Flow = (data: Props) => (
|
||||
<ReactFlowProvider>
|
||||
<AppEdit {...data} />
|
||||
</ReactFlowProvider>
|
||||
<Box
|
||||
h={'100%'}
|
||||
position={data.fullScreen ? 'fixed' : 'relative'}
|
||||
zIndex={999}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
{!!data.app._id && <AppEdit {...data} />}
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default React.memo(Flow);
|
||||
|
||||
@@ -12,9 +12,6 @@ import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
|
||||
const EditApp = dynamic(() => import('./components/edit'), {
|
||||
ssr: false
|
||||
});
|
||||
const Share = dynamic(() => import('./components/Share'), {
|
||||
ssr: false
|
||||
});
|
||||
@@ -24,7 +21,6 @@ const API = dynamic(() => import('./components/API'), {
|
||||
|
||||
enum TabEnum {
|
||||
'settings' = 'settings',
|
||||
'edit' = 'edit',
|
||||
'share' = 'share',
|
||||
'API' = 'API'
|
||||
}
|
||||
@@ -33,17 +29,11 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const { appId } = router.query as { appId: string };
|
||||
const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore();
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
const { appDetail = defaultApp } = useUserStore();
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: `${TabEnum}`) => {
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
appId,
|
||||
currentTab: tab
|
||||
@@ -56,28 +46,23 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{ label: '概览', id: TabEnum.settings, icon: 'overviewLight' },
|
||||
...(isOwner ? [{ label: '高级设置', id: TabEnum.edit, icon: 'settingLight' }] : []),
|
||||
{ label: '链接分享', id: TabEnum.share, icon: 'shareLight' },
|
||||
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chatLight' }
|
||||
],
|
||||
[isOwner]
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = '内容已修改,确认离开页面吗?';
|
||||
};
|
||||
// useEffect(() => {
|
||||
// window.onbeforeunload = (e) => {
|
||||
// e.preventDefault();
|
||||
// e.returnValue = '内容已修改,确认离开页面吗?';
|
||||
// };
|
||||
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAppDetail(appId);
|
||||
}, [appId, loadAppDetail]);
|
||||
// return () => {
|
||||
// window.onbeforeunload = null;
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -156,11 +141,6 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
{currentTab === TabEnum.settings && <Settings appId={appId} />}
|
||||
{currentTab === TabEnum.edit && (
|
||||
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.API && <API appId={appId} />}
|
||||
{currentTab === TabEnum.share && <Share appId={appId} />}
|
||||
</Box>
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useRouter } from 'next/router';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
type HistoryItemType = {
|
||||
id: string;
|
||||
@@ -20,6 +22,7 @@ type HistoryItemType = {
|
||||
};
|
||||
|
||||
const ChatHistorySlider = ({
|
||||
appId,
|
||||
appName,
|
||||
appAvatar,
|
||||
history,
|
||||
@@ -29,6 +32,7 @@ const ChatHistorySlider = ({
|
||||
onSetHistoryTop,
|
||||
onCloseSlider
|
||||
}: {
|
||||
appId?: string;
|
||||
appName: string;
|
||||
appAvatar: string;
|
||||
history: HistoryItemType[];
|
||||
@@ -39,6 +43,7 @@ const ChatHistorySlider = ({
|
||||
onCloseSlider: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const concatHistory = useMemo<HistoryItemType[]>(
|
||||
@@ -57,12 +62,27 @@ const ChatHistorySlider = ({
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{isPc && (
|
||||
<Flex pt={5} pb={2} px={[2, 5]} alignItems={'center'}>
|
||||
<Avatar src={appAvatar} />
|
||||
<Box ml={2} fontWeight={'bold'} className={'textEllipsis'}>
|
||||
{appName}
|
||||
</Box>
|
||||
</Flex>
|
||||
<MyTooltip label={appId ? '应用详情' : ''} offset={[0, 0]}>
|
||||
<Flex
|
||||
pt={5}
|
||||
pb={2}
|
||||
px={[2, 5]}
|
||||
alignItems={'center'}
|
||||
cursor={appId ? 'pointer' : 'default'}
|
||||
onClick={() =>
|
||||
appId &&
|
||||
router.push({
|
||||
pathname: '/app/detail',
|
||||
query: { appId }
|
||||
})
|
||||
}
|
||||
>
|
||||
<Avatar src={appAvatar} />
|
||||
<Box ml={2} fontWeight={'bold'} className={'textEllipsis'}>
|
||||
{appName}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{/* 新对话 */}
|
||||
<Box w={'100%'} px={[2, 5]} h={'36px'} my={5}>
|
||||
|
||||
@@ -21,11 +21,11 @@ import { getErrText } from '@/utils/tools';
|
||||
|
||||
const QuoteModal = ({
|
||||
historyId,
|
||||
chatId,
|
||||
contentId,
|
||||
onClose
|
||||
}: {
|
||||
historyId: string;
|
||||
chatId: string;
|
||||
contentId: string;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -41,7 +41,7 @@ const QuoteModal = ({
|
||||
data: quote = [],
|
||||
refetch,
|
||||
isLoading
|
||||
} = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, chatId }));
|
||||
} = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, contentId }));
|
||||
|
||||
/**
|
||||
* update kbData, update mongo status and reload quotes
|
||||
@@ -51,7 +51,7 @@ const QuoteModal = ({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await updateHistoryQuote({
|
||||
chatId,
|
||||
contentId,
|
||||
historyId,
|
||||
quoteId,
|
||||
sourceText
|
||||
@@ -66,7 +66,7 @@ const QuoteModal = ({
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[chatId, historyId, refetch, setIsLoading, toast]
|
||||
[contentId, historyId, refetch, setIsLoading, toast]
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ const SliderApps = ({ appId }: { appId: string }) => {
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => router.replace('/app/list')}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
|
||||
@@ -54,8 +54,8 @@ const Chat = () => {
|
||||
const {
|
||||
lastChatAppId,
|
||||
setLastChatAppId,
|
||||
lastChatId,
|
||||
setLastChatId,
|
||||
lastHistoryId,
|
||||
setLastHistoryId,
|
||||
history,
|
||||
loadHistory,
|
||||
updateHistory,
|
||||
@@ -192,13 +192,13 @@ const Chat = () => {
|
||||
} catch (e: any) {
|
||||
// reset all chat tore
|
||||
setLastChatAppId('');
|
||||
setLastChatId('');
|
||||
setLastHistoryId('');
|
||||
router.replace('/chat');
|
||||
}
|
||||
setIsLoading(false);
|
||||
return null;
|
||||
},
|
||||
[setIsLoading, setChatData, router, setLastChatAppId, setLastChatId]
|
||||
[setIsLoading, setChatData, router, setLastChatAppId, setLastHistoryId]
|
||||
);
|
||||
// 初始化聊天框
|
||||
useQuery(['init', appId, historyId], () => {
|
||||
@@ -207,7 +207,7 @@ const Chat = () => {
|
||||
router.replace({
|
||||
query: {
|
||||
appId: lastChatAppId,
|
||||
historyId: lastChatId
|
||||
historyId: lastHistoryId
|
||||
}
|
||||
});
|
||||
return null;
|
||||
@@ -215,7 +215,7 @@ const Chat = () => {
|
||||
|
||||
// store id
|
||||
appId && setLastChatAppId(appId);
|
||||
setLastChatId(historyId);
|
||||
setLastHistoryId(historyId);
|
||||
|
||||
if (forbidRefresh.current) {
|
||||
forbidRefresh.current = false;
|
||||
@@ -254,6 +254,7 @@ const Chat = () => {
|
||||
);
|
||||
})(
|
||||
<ChatHistorySlider
|
||||
appId={appId}
|
||||
appName={chatData.app.name}
|
||||
appAvatar={chatData.app.avatar}
|
||||
activeHistoryId={historyId}
|
||||
|
||||
@@ -17,14 +17,13 @@ const Login = () => {
|
||||
const { lastRoute = '' } = router.query as { lastRoute: string };
|
||||
const { isPc } = useGlobalStore();
|
||||
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
|
||||
const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore();
|
||||
const { setLastChatId, setLastChatAppId } = useChatStore();
|
||||
const { setUserInfo, loadKbList, setLastKbId } = useUserStore();
|
||||
const { setLastHistoryId, setLastChatAppId } = useChatStore();
|
||||
|
||||
const loginSuccess = useCallback(
|
||||
(res: ResLogin) => {
|
||||
// init store
|
||||
setLastChatId('');
|
||||
setLastModelId('');
|
||||
setLastHistoryId('');
|
||||
setLastChatAppId('');
|
||||
setLastKbId('');
|
||||
loadKbList(true);
|
||||
@@ -34,16 +33,7 @@ const Login = () => {
|
||||
router.push(lastRoute ? decodeURIComponent(lastRoute) : '/model');
|
||||
}, 100);
|
||||
},
|
||||
[
|
||||
lastRoute,
|
||||
loadKbList,
|
||||
router,
|
||||
setLastChatId,
|
||||
setLastChatAppId,
|
||||
setLastKbId,
|
||||
setLastModelId,
|
||||
setUserInfo
|
||||
]
|
||||
[lastRoute, loadKbList, router, setLastHistoryId, setLastChatAppId, setLastKbId, setUserInfo]
|
||||
);
|
||||
|
||||
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {
|
||||
|
||||
@@ -69,19 +69,17 @@ const BillTable = () => {
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex w={'100%'} mt={4} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Box ml={2}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex w={'100%'} mt={4} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Box ml={2}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user