Archer 34602b25df
4.6.8-alpha (#804)
* perf: redirect request and err log replace

perf: dataset openapi

feat: session

fix: retry input error

feat: 468 doc

sub page

feat: standard sub

perf: rerank tip

perf: rerank tip

perf: api sdk

perf: openapi

sub plan

perf: sub ui

fix: ts

* perf: init log

* fix: variable select

* sub page

* icon

* perf: llm model config

* perf: menu ux

* perf: system store

* perf: publish app name

* fix: init data

* perf: flow edit ux

* fix: value type format and ux

* fix prompt editor default value (#13)

* fix prompt editor default value

* fix prompt editor update when not focus

* add key with variable

---------

Co-authored-by: Archer <545436317@qq.com>

* fix: value type

* doc

* i18n

* import path

* home page

* perf: mongo session running

* fix: ts

* perf: use toast

* perf: flow edit

* perf: sse response

* slider ui

* fetch error

* fix prompt editor rerender when not focus by key defaultvalue (#14)

* perf: prompt editor

* feat: dataset search concat

* perf: doc

* fix:ts

* perf: doc

* fix json editor onblur value (#15)

* faq

* vector model default config

* ipv6

---------

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
2024-02-01 21:57:41 +08:00

386 lines
11 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Flex,
ModalFooter,
ModalBody,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useTheme,
Link,
Input,
MenuList,
MenuItem,
MenuButton,
Menu
} from '@chakra-ui/react';
import {
getOpenApiKeys,
createAOpenApiKey,
delOpenApiById,
putOpenApiKey
} from '@/web/support/openapi/api';
import type { EditApiKeyProps } from '@/global/support/openapi/api.d';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/web/common/hooks/useLoading';
import dayjs from 'dayjs';
import { AddIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@/components/MyModal';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/web/common/hooks/useRequest';
import MyTooltip from '@/components/MyTooltip';
import { getDocPath } from '@/web/common/system/doc';
import MyMenu from '@/components/MyMenu';
type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
name: '',
limit: {
credit: -1
}
};
const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const { copyData } = useCopyData();
const { feConfigs } = useSystemStore();
const [baseUrl, setBaseUrl] = useState('https://fastgpt.in/api');
const [editData, setEditData] = useState<EditProps>();
const [apiKey, setApiKey] = useState('');
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys', appId], () => getOpenApiKeys({ appId }));
useEffect(() => {
setBaseUrl(feConfigs?.customApiDomain || `${location.origin}/api`);
}, []);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
<Box flex={1}>
<Flex alignItems={'flex-end'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
{t('support.openapi.Api manager')}
</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/development/openapi')}
target={'_blank'}
ml={1}
color={'primary.500'}
>
{t('common.Read document')}
</Link>
)}
</Flex>
<Box fontSize={'sm'} color={'myGray.600'}>
{tips}
</Box>
</Box>
<Flex
mt={[2, 0]}
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
userSelect={'none'}
onClick={() => copyData(baseUrl, t('support.openapi.Copy success'))}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
{t('support.openapi.Api baseurl')}
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Box mt={[2, 0]} textAlign={'right'}>
<Button
ml={3}
leftIcon={<AddIcon fontSize={'md'} />}
variant={'whitePrimary'}
onClick={() =>
setEditData({
...defaultEditData,
appId
})
}
>
{t('common.New Create')}
</Button>
</Box>
</Box>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
<Th>{t('Name')}</Th>
<Th>Api Key</Th>
<Th>{t('support.openapi.Usage')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('support.openapi.Max usage')}</Th>
<Th>{t('common.Expired Time')}</Th>
</>
)}
<Th>{t('common.Create Time')}</Th>
<Th>{t('common.Last use time')}</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => (
<Tr key={_id}>
<Td>{name}</Td>
<Td>{apiKey}</Td>
<Td>{usage}</Td>
{feConfigs?.isPlus && (
<>
<Td>
{limit?.credit && limit?.credit > -1
? `${limit?.credit}`
: t('common.Unlimited')}
</Td>
<Td whiteSpace={'pre-wrap'}>
{limit?.expiredTime
? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td whiteSpace={'pre-wrap'}>{dayjs(createTime).format('YYYY/MM/DD\nHH:mm:ss')}</Td>
<Td whiteSpace={'pre-wrap'}>
{lastUsedTime
? dayjs(lastUsedTime).format('YYYY/MM/DD\nHH:mm:ss')
: t('common.Un used')}
</Td>
<Td>
<MyMenu
offset={[-50, 5]}
Button={
<MyIcon
name={'more'}
w={'14px'}
p={2}
_hover={{ bg: 'myWhite.600 ' }}
cursor={'pointer'}
borderRadius={'md'}
/>
}
menuList={[
{
label: t('common.Edit'),
icon: 'edit',
onClick: () =>
setEditData({
_id,
name,
limit,
appId
})
},
{
label: t('common.Delete'),
icon: 'delete',
onClick: () => onclickRemove(_id)
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Loading loading={isGetting || isDeleting} fixed={false} />
</TableContainer>
{!!editData && (
<EditKeyModal
defaultData={editData}
onClose={() => setEditData(undefined)}
onCreate={(id) => {
setApiKey(id);
refetch();
setEditData(undefined);
}}
onEdit={() => {
refetch();
setEditData(undefined);
}}
/>
)}
<MyModal
isOpen={!!apiKey}
w={['400px', '600px']}
iconSrc="/imgs/modal/key.svg"
title={
<Box>
<Box fontWeight={'bold'} fontSize={'xl'}>
{t('support.openapi.New api key')}
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
{t('support.openapi.New api key tip')}
</Box>
</Box>
}
onClose={() => setApiKey('')}
>
<ModalBody pt={5}>
<Flex
bg={'myGray.100'}
px={3}
py={2}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
cursor={'pointer'}
borderRadius={'md'}
onClick={() => copyData(apiKey)}
>
<Box flex={1}>{apiKey}</Box>
<MyIcon ml={1} name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant="whiteBase" onClick={() => setApiKey('')}>
{t('common.OK')}
</Button>
</ModalFooter>
</MyModal>
</Flex>
);
};
export default React.memo(ApiKeyTable);
// edit link modal
function EditKeyModal({
defaultData,
onClose,
onCreate,
onEdit
}: {
defaultData: EditProps;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
}) {
const { t } = useTranslation();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { feConfigs } = useSystemStore();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: EditProps) => createAOpenApiKey(e),
errorToast: '创建链接异常',
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: EditProps) => {
//@ts-ignore
return putOpenApiKey(e);
},
errorToast: '更新链接异常',
onSuccess: onEdit
});
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/key.svg"
title={isEdit ? t('outlink.Edit API Key') : t('outlink.Create API Key')}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 90px'}>{t('Name')}:</Box>
<Input
placeholder={t('openapi.key alias') || 'key alias'}
maxLength={20}
{...register('name', {
required: t('common.Name is empty') || 'Name is empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.credit', {
min: -1,
max: 1000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Expired Time')}:
</Flex>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}