V4.9.1 feature (#4206)

* fix: remove DefaultTeam (#4037)

* fix :Get application bound knowledge base information logical rewrite (#4057)

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* update package

* fix: import dataset step error;perf: ai proxy avatar (#4074)

* perf: pg config params

* perf: ai proxy avatar

* fix: import dataset step error

* feat: data input ux

* perf: app dataset rewite

* fix: 文本提取不支持arrayString,arrayNumber等jsonSchema (#4079)

* update doc ;perf: model test (#4098)

* perf: extract array

* update doc

* perf: model test

* perf: model test

* perf: think tag parse (#4102)

* chat quote reader (#3912)

* init chat quote full text reader

* linked structure

* dataset data linked

* optimize code

* fix ts build

* test finish

* delete log

* fix

* fix ts

* fix ts

* remove nextId

* initial scroll

* fix

* fix

* perf: chunk read   (#4109)

* package

* perf: chunk read

* feat: api dataset support pdf parse;fix: chunk reader auth (#4117)

* feat: api dataset support pdf parse

* fix: chunk reader auth

* feat: invitation link (#3979)

* feat: invitation link schema and apis

* feat: add invitation link

* feat: member status: active, leave, forbidden

* fix: expires show hours and minutes

* feat: invalid invitation link hint

* fix: typo

* chore: fix typo & i18n

* fix

* pref: fe

* feat: add ttl index for 30-day-clean-up

* perf: invite member code (#4118)

* perf: invite member code

* fix: ts

* fix: model test channel id;fix: quote reader (#4123)

* fix: model test channel id

* fix: quote reader

* fix chat quote reader (#4125)

* perf: model test;perf: sidebar trigger (#4127)

* fix: import dataset step error;perf: ai proxy avatar (#4074)

* perf: pg config params

* perf: ai proxy avatar

* fix: import dataset step error

* feat: data input ux

* perf: app dataset rewite

* perf: model test

* perf: sidebar trigger

* lock

* update nanoid version

* fix: select component ux

* fix: ts

* fix: vitest

* remove test

* fix: prompt toolcall ui (#4139)

* load log error adapt

* fix: prompt toolcall ui

* perf: commercial function tip

* update package

* pref: copy link (#4147)

* fix(i18n): namespace (#4143)

* hiden dataset source (#4152)

* hiden dataset source

* perf: reader

* chore: move all tests into a single folder (#4160)

* fix modal close scroll (#4162)

* fix modal close scroll

* update refresh

* feat: rerank modal select and weight (#4164)

* fix loadInitData refresh (#4169)

* fix

* fix

* form input number default & api dataset max token

* feat: mix search weight (#4170)

* feat: mix search weight

* feat: svg render

* fix: avatar error remove (#4173)

* fix: avatar error remove

* fix: index

* fix: guide

* fix: auth

* update package;fix: input data model ui (#4181)

* update package

* fix: ts

* update config

* update jieba package

* add type sign

* fix: input data ui

* fix: page title refresh (#4186)

* fix: ts

* update jieba package

* fix: page title refresh

* fix: remove member length check when opening invite create modal (#4193)

* add env to check internal ip (#4187)

* fix: ts

* update jieba package

* add env to check internal ip

* package

* fix: jieba

* reset package

* update config

* fix: jieba package

* init shell

* init version

* change team reload

* update jieba package (#4200)

* update jieba package

* package

* update package

* remove invalid code

* action

* package (#4201)

* package

* update package

* remove invalid code

* package

* remove i18n tip (#4202)

* doc (#4205)

* fix: i18n (#4208)

* fix: next config (#4207)

* reset package

* i18n

* update config

* i18n

* remove log

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>
Co-authored-by: shilin <39396378+shilin66@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-03-18 14:40:41 +08:00
committed by GitHub
parent 56793114d8
commit e75d81d05a
316 changed files with 10626 additions and 8464 deletions

View File

@@ -1,14 +1,12 @@
import React, { useMemo } from 'react';
import { Box, ButtonProps, Flex } from '@chakra-ui/react';
import { Box, ButtonProps } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
const TeamSelector = ({
@@ -21,7 +19,7 @@ const TeamSelector = ({
}) => {
const { t } = useTranslation();
const router = useRouter();
const { userInfo, initUserInfo } = useUserStore();
const { userInfo } = useUserStore();
const { setLoading } = useSystemStore();
const { data: myTeams = [] } = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
@@ -33,12 +31,11 @@ const TeamSelector = ({
async (teamId: string) => {
setLoading(true);
await putSwitchTeam(teamId);
return initUserInfo();
},
{
onFinally: () => {
router.reload();
setLoading(false);
onChange?.();
},
errorToast: t('common:user.team.Switch Team Failed')
}
@@ -46,48 +43,21 @@ const TeamSelector = ({
const teamList = useMemo(() => {
return myTeams.map((team) => ({
label: (
<Flex
key={team.teamId}
alignItems={'center'}
borderRadius={'md'}
cursor={'default'}
gap={3}
onClick={() => onSwitchTeam(team.teamId)}
_hover={{
cursor: 'pointer'
}}
>
<Avatar src={team.avatar} w={['1.25rem', '1.375rem']} />
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
{team.teamName}
</Box>
</Flex>
),
icon: team.avatar,
iconSize: '1.25rem',
label: team.teamName,
value: team.teamId
}));
}, [myTeams, onSwitchTeam]);
}, [myTeams]);
const formatTeamList = useMemo(() => {
return [
...(showManage
? [
{
label: (
<Flex
key={'manage'}
alignItems={'center'}
borderRadius={'md'}
cursor={'pointer'}
gap={3}
onClick={() => router.push('/account/team')}
>
<MyIcon name="common/setting" w={['1.25rem', '1.375rem']} />
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
{t('user:manage_team')}
</Box>
</Flex>
),
icon: 'common/setting',
iconSize: '1.25rem',
label: t('user:manage_team'),
value: 'manage',
showBorder: true
}
@@ -95,11 +65,24 @@ const TeamSelector = ({
: []),
...teamList
];
}, [showManage, t, teamList, router]);
}, [showManage, t, teamList]);
const handleChange = (value: string) => {
if (value === 'manage') {
router.push('/account/team');
} else {
onSwitchTeam(value);
}
};
return (
<Box w={'100%'}>
<MySelect {...props} value={userInfo?.team?.teamId} list={formatTeamList} />
<MySelect
{...props}
value={userInfo?.team?.teamId}
list={formatTeamList}
onChange={handleChange}
/>
</Box>
);
};

View File

@@ -111,7 +111,7 @@ const BillTable = () => {
list={billTypeList}
value={billType}
size={'sm'}
onchange={(e) => {
onChange={(e) => {
setBillType(e);
}}
w={'130px'}
@@ -220,13 +220,7 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
{bill.metadata.payWay === 'balance' ? (
t('user:bill.not_need_invoice')
) : (
<Box>
{
(bill.metadata.payWay = bill.hasInvoice
? t('account_bill:yes')
: t('account_bill:no'))
}
</Box>
<Box>{bill.hasInvoice ? t('account_bill:yes') : t('account_bill:no')}</Box>
)}
</Flex>
)}

View File

@@ -213,7 +213,7 @@ export const ModelEditModal = ({
<Td textAlign={'right'}>
<MySelect
value={provider}
onchange={(value) => setValue('provider', value)}
onChange={(value) => setValue('provider', value)}
list={providerList.current}
{...InputStyles}
/>

View File

@@ -79,7 +79,7 @@ const EditChannelModal = ({
order: provider.order,
defaultBaseUrl: value.defaultBaseUrl,
keyHelp: value.keyHelp,
icon: provider.avatar,
icon: mapData?.avatar ?? provider.avatar,
label: t(mapData.label as any),
value: Number(key)
};
@@ -90,6 +90,7 @@ const EditChannelModal = ({
manual: false
}
);
const selectedProvider = useMemo(() => {
const res = providerList.find((item) => item.value === providerType);
return res;
@@ -193,7 +194,7 @@ const EditChannelModal = ({
placeholder={t('account_model:select_provider_placeholder')}
value={providerType}
isSearch
onchange={(val) => {
onChange={(val) => {
setValue('type', val);
}}
/>
@@ -332,6 +333,8 @@ const MultipleSelect = ({ value = [], list = [], onSelect }: SelectProps) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { copyData } = useCopyData();
const [search, setSearch] = useState('');
const onclickItem = useCallback(
(val: string) => {
if (value.includes(val)) {
@@ -342,12 +345,11 @@ const MultipleSelect = ({ value = [], list = [], onSelect }: SelectProps) => {
top: BoxRef.current.scrollHeight
});
}
setSearch('');
},
[value, onSelect]
);
const [search, setSearch] = useState('');
const filterUnSelected = useMemo(() => {
return list
.filter((item) => !value.includes(item.value))

View File

@@ -25,6 +25,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { batchRun } from '@fastgpt/global/common/system/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
type ModelTestItem = {
label: React.ReactNode;
@@ -34,7 +35,15 @@ type ModelTestItem = {
duration?: number;
};
const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void }) => {
const ModelTest = ({
channelId,
models,
onClose
}: {
channelId: number;
models: string[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [testModelList, setTestModelList] = useState<ModelTestItem[]>([]);
@@ -57,6 +66,7 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
colorSchema: 'red'
}
});
const { loading: loadingModels } = useRequest2(getSystemModelList, {
manual: false,
refreshDeps: [models],
@@ -95,7 +105,7 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
);
const start = Date.now();
try {
await getTestModel(model);
await getTestModel({ model, channelId });
const duration = Date.now() - start;
setTestModelList((prev) =>
prev.map((item) =>
@@ -134,13 +144,47 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
refreshDeps: [testModelList]
}
);
const { runAsync: onTestOneModel, loading: testingOneModel } = useRequest2(
async (model: string) => {
const start = Date.now();
setTestModelList((prev) =>
prev.map((item) =>
item.model === model ? { ...item, status: 'running', message: '' } : item
)
);
try {
await getTestModel({ model, channelId });
const duration = Date.now() - start;
setTestModelList((prev) =>
prev.map((item) =>
item.model === model ? { ...item, status: 'success', duration: duration / 1000 } : item
)
);
} catch (error) {
setTestModelList((prev) =>
prev.map((item) =>
item.model === model ? { ...item, status: 'error', message: getErrText(error) } : item
)
);
}
},
{
manual: true
}
);
const isTestLoading = testingOneModel || isTesting;
return (
<MyModal
iconSrc={'core/chat/sendLight'}
isLoading={loadingModels}
title={t('account_model:model_test')}
w={'600px'}
w={'100%'}
maxW={['90vw', '1090px']}
isOpen
>
<ModalBody>
@@ -148,8 +192,10 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
<Table>
<Thead>
<Tr>
<Th>{t('account_model:model')}</Th>
<Th>{t('account_model:model_name')}</Th>
<Th>{t('account:model.model_id')}</Th>
<Th>{t('account_model:channel_status')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
@@ -158,6 +204,7 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
return (
<Tr key={item.model}>
<Td>{item.label}</Td>
<Td>{item.model}</Td>
<Td>
<Flex alignItems={'center'}>
<MyTag mr={1} type="borderSolid" colorSchema={data.colorSchema as any}>
@@ -173,6 +220,16 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
)}
</Flex>
</Td>
<Td>
<MyIconButton
isLoading={isTestLoading}
icon={'core/chat/sendLight'}
tip={t('account:model.test_model')}
onClick={() => {
onTestOneModel(item.model);
}}
/>
</Td>
</Tr>
);
})}
@@ -184,7 +241,7 @@ const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void
<Button mr={4} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={isTesting} variant={'primary'} onClick={onStartTest}>
<Button isLoading={isTestLoading} variant={'primary'} onClick={onStartTest}>
{t('account_model:start_test', { num: testModelList.length })}
</Button>
</ModalFooter>

View File

@@ -74,7 +74,7 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
}
});
const [testModels, setTestModels] = useState<string[]>();
const [modelTestData, setTestModelData] = useState<{ channelId: number; models: string[] }>();
const isLoading =
loadingChannelList ||
@@ -165,7 +165,11 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
{
icon: 'core/chat/sendLight',
label: t('account_model:model_test'),
onClick: () => setTestModels(item.models)
onClick: () =>
setTestModelData({
channelId: item.id,
models: item.models
})
},
...(item.status === ChannelStatusEnum.ChannelStatusEnabled
? [
@@ -222,7 +226,9 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
onSuccess={refreshChannelList}
/>
)}
{!!testModels && <ModelTest models={testModels} onClose={() => setTestModels(undefined)} />}
{!!modelTestData && (
<ModelTest {...modelTestData} onClose={() => setTestModelData(undefined)} />
)}
</>
);
};

View File

@@ -206,7 +206,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
list={channelList}
placeholder={t('account_model:select_channel')}
value={filterProps.channelId}
onchange={(val) => setFilterProps({ ...filterProps, channelId: val })}
onChange={(val) => setFilterProps({ ...filterProps, channelId: val })}
/>
</Box>
</HStack>
@@ -219,7 +219,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
list={modelList}
placeholder={t('account_model:select_model')}
value={filterProps.model}
onchange={(val) => setFilterProps({ ...filterProps, model: val })}
onChange={(val) => setFilterProps({ ...filterProps, model: val })}
/>
</Box>
</HStack>
@@ -234,7 +234,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
{ label: t('common:common.failed'), value: 'error' }
]}
value={filterProps.code_type}
onchange={(val) => setFilterProps({ ...filterProps, code_type: val })}
onChange={(val) => setFilterProps({ ...filterProps, code_type: val })}
/>
</Box>
</HStack>
@@ -298,11 +298,15 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
const { data: detailData } = useRequest2(
async () => {
if (data.code === 200) return data;
const res = await getLogDetail(data.id);
return {
...res,
...data
};
try {
const res = await getLogDetail(data.id);
return {
...res,
...data
};
} catch (error) {
return data;
}
},
{
manual: false

View File

@@ -280,6 +280,10 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
isCustom: true,
isActive: true,
isDefault: false,
isDefaultDatasetTextModel: false,
isDefaultDatasetImageModel: false,
// @ts-ignore
type
});
@@ -326,7 +330,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
w={'200px'}
bg={'myGray.50'}
value={provider}
onchange={setProvider}
onChange={setProvider}
list={filterProviderList}
/>
</HStack>
@@ -338,7 +342,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
w={'150px'}
bg={'myGray.50'}
value={modelType}
onchange={setModelType}
onChange={setModelType}
list={selectModelTypeList.current}
/>
</HStack>
@@ -436,7 +440,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
<MyIconButton
icon={'core/chat/sendLight'}
tip={t('account:model.test_model')}
onClick={() => onTestModel(item.model)}
onClick={() => onTestModel({ model: item.model })}
/>
<MyIconButton
icon={'common/settingLight'}
@@ -597,7 +601,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
llm: llmModelList.find((item) => item.model === e)
@@ -616,7 +620,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
embedding: embeddingModelList.find((item) => item.model === e)
@@ -635,7 +639,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
tts: ttsModelList.find((item) => item.model === e)
@@ -654,7 +658,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
stt: sttModelList.find((item) => item.model === e)
@@ -673,7 +677,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
rerank: reRankModelList.find((item) => item.model === e)
@@ -696,7 +700,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
datasetTextLLM: datasetModelList.find((item) => item.model === e)
@@ -718,7 +722,7 @@ const DefaultModelModal = ({
value: item.model,
label: item.name
}))}
onchange={(e) => {
onChange={(e) => {
setDefaultData((state) => ({
...state,
datasetImageLLM: vlmModelList.find((item) => item.model === e)

View File

@@ -142,7 +142,7 @@ function EditModal({
width={'fit-content'}
>
<Icon name="common/info" w="1rem" />
<Box width="fit-content">{t('account_info:please_bind_contact')}</Box>
<Box width="fit-content">{t('account_team:please_bind_contact')}</Box>
</HStack>
);
})()}

View File

@@ -172,7 +172,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
<AvatarGroup avatars={members.map((v) => v.avatar)} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group)}>
@@ -180,7 +180,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
@@ -189,7 +188,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>

View File

@@ -0,0 +1,105 @@
import { postCreateInvitationLink } from '@/web/support/user/team/api';
import {
Box,
Button,
Grid,
Radio,
RadioGroup,
Input,
ModalBody,
ModalCloseButton,
ModalFooter,
HStack
} from '@chakra-ui/react';
import {
InvitationLinkCreateType,
InvitationLinkExpiresType
} from '@fastgpt/service/support/user/team/invitationLink/type';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
function CreateInvitationModal({ onClose }: { onClose: () => void }) {
const { t } = useTranslation();
const expiresOptions: Array<{ label: string; value: InvitationLinkExpiresType }> = [
{ label: t('account_team:30mins'), value: '30m' }, // 30 mins
{ label: t('account_team:7days'), value: '7d' }, // 7 days
{ label: t('account_team:1year'), value: '1y' } // 1 year
];
const { register, handleSubmit, watch, setValue } = useForm<InvitationLinkCreateType>({
defaultValues: {
description: '',
expires: expiresOptions[1].value,
usedTimesLimit: 1
}
});
const expires = watch('expires');
const usedTimesLimit = watch('usedTimesLimit');
const { runAsync: createInvitationLink, loading } = useRequest2(postCreateInvitationLink, {
manual: true,
successToast: t('common:common.Create Success'),
errorToast: t('common:common.Create Failed'),
onFinally: () => onClose()
});
return (
<MyModal
isOpen
iconSrc="common/addLight"
iconColor="primary.500"
title={<Box>{t('account_team:create_invitation_link')}</Box>}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Grid gap={6} templateColumns="max-content 1fr" alignItems="center">
<>
<FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
<Input
placeholder={t('account_team:invitation_link_description')}
{...register('description', { required: true })}
/>
</>
<>
<FormLabel required={true}>{t('account_team:expires')}</FormLabel>
<MySelect
list={expiresOptions}
value={expires}
onChange={(val) => setValue('expires', val)}
minW="120px"
/>
</>
<>
<FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
<RadioGroup
onChange={(val: '1' | '-1') => setValue('usedTimesLimit', Number(val) as 1 | -1)}
value={String(usedTimesLimit)}
>
<HStack gap={6}>
<Radio value="1">{t('account_team:1person')}</Radio>
<Radio value="-1">{t('account_team:unlimited')}</Radio>
</HStack>
</RadioGroup>
</>
</Grid>
</ModalBody>
<ModalFooter>
<Button isLoading={loading} onClick={onClose} variant="outline">
{t('common:common.Cancel')}
</Button>
<Button isLoading={loading} onClick={handleSubmit(createInvitationLink)} ml="4">
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default CreateInvitationModal;

View File

@@ -0,0 +1,77 @@
import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
import { Box, Button, Flex, ModalBody, ModalCloseButton } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
function Invite({ invitelinkid }: { invitelinkid: string }) {
const router = useRouter();
const { t } = useTranslation();
const { onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
const onClose = () => {
router.push('/account/team');
};
const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
manual: false,
onError: onClose
});
const { runAsync: acceptInvitation, loading: accepting } = useRequest2(
() => postAcceptInvitationLink(invitelinkid),
{
manual: true,
successToast: t('common:common.Success'),
onSuccess: async () => {
onSwitchTeam(invitationInfo!.teamId);
onClose();
}
}
);
return invitationInfo ? (
<MyModal
isOpen={true}
iconSrc="support/user/usersLight"
title={t('account_team:handle_invitation')}
iconColor={'primary.600'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Flex
key={invitationInfo._id}
alignItems={'center'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'md'}
px={3}
py={2}
>
<Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
<Box mx={2}>{invitationInfo.teamName}</Box>
<Box flex={1} />
<Button
size="sm"
variant={'solid'}
colorScheme="green"
onClick={acceptInvitation}
isLoading={accepting}
>
{t('account_team:accept')}
</Button>
<Button size="sm" ml={2} variant="outline" onClick={onClose} isLoading={accepting}>
{t('account_team:ignore')}
</Button>
</Flex>
</ModalBody>
</MyModal>
) : null;
}
export default Invite;

View File

@@ -0,0 +1,278 @@
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getInvitationLinkList, putUpdateInvitationInfo } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Button,
Divider,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Icon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import Tag from '@fastgpt/web/components/common/Tag';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import format from 'date-fns/format';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useCallback } from 'react';
const CreateInvitationModal = dynamic(() => import('./CreateInvitationModal'));
const InviteModal = ({
teamId,
onClose,
onSuccess
}: {
teamId: string;
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const {
data: invitationLinkList,
loading: isLoadingLink,
runAsync: refetchInvitationLinkList
} = useRequest2(() => getInvitationLinkList(), {
manual: false
});
const { isOpen: isOpenCreate, onOpen: onOpenCreate, onClose: onCloseCreate } = useDisclosure();
const isLoading = isLoadingLink;
const { copyData } = useCopyData();
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const onCopy = useCallback(
(linkId: string) => {
const url = location.origin + `/account/team?invitelinkid=${linkId}`;
const teamName = userInfo?.team.teamName;
const systemName = feConfigs.systemTitle;
const userName = userInfo?.team.memberName;
copyData(
t('account_team:invitation_copy_link', {
teamName,
systemName,
userName,
url
})
);
},
[copyData]
);
const { runAsync: onForbid, loading: forbiding } = useRequest2(
(linkId: string) =>
putUpdateInvitationInfo({
linkId,
forbidden: true
}),
{
manual: true,
onSuccess: refetchInvitationLinkList,
successToast: t('account_team:forbid_success')
}
);
return (
<MyModal
isLoading={isLoading}
isOpen
iconSrc="common/inviteLight"
iconColor="primary.600"
title={t('account_team:invite_member')}
overflow={'unset'}
onClose={onClose}
w={'100%'}
maxW={['90vw', '820px']}
>
<ModalBody maxH="500px">
<Flex alignItems={'center'} justifyContent={'space-between'} mb={4}>
<HStack>
<Icon name="common/list" w="16px" />
<Box ml="6px" fontSize="md">
{t('account_team:invitation_link_list')}
</Box>
</HStack>
<Button onClick={onOpenCreate}>{t('account_team:create_invitation_link')}</Button>
</Flex>
<TableContainer overflowY={'auto'}>
<Table fontSize={'sm'} overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:invitation_link_description')}
</Th>
<Th bgColor="myGray.100">{t('account_team:expires')}</Th>
<Th bgColor="myGray.100">{t('account_team:used_times_limit')}</Th>
<Th bgColor="myGray.100">{t('account_team:invited')}</Th>
<Th bgColor="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
{!!invitationLinkList?.length && (
<Tbody overflow={'unset'}>
{invitationLinkList?.map((item) => {
const isForbidden = item.forbidden || new Date(item.expires) < new Date();
return (
<Tr key={item._id} overflow={'unset'}>
<Td maxW="200px" minW="100px">
{item.description}
</Td>
<Td>
{isForbidden ? (
<Tag colorSchema="gray">{t('account_team:has_forbidden')}</Tag>
) : (
format(new Date(item.expires), 'yyyy-MM-dd HH:mm')
)}
</Td>
<Td>
{item.usedTimesLimit === -1
? t('account_team:unlimited')
: item.usedTimesLimit}
</Td>
<Td>
{item.members.length > 0 && (
<MyPopover
w="fit-content"
Trigger={
<Box
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
p="1.5"
w="fit-content"
>
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
</Box>
}
trigger="click"
closeOnBlur={true}
>
{() => (
<Box py="4" maxH="200px" w="fit-content">
<Flex mx="4" justifyContent="center" alignItems={'center'}>
<Box>{t('account_team:has_invited')}</Box>
<Box
ml="auto"
bg="myGray.200"
px="2"
borderRadius="md"
fontSize="sm"
>
{item.members.length}
</Box>
</Flex>
<Divider my="2" mx="4" />
<Grid
w="fit-content"
mt="2"
gridRowGap="4"
gridTemplateColumns="1fr 1fr"
overflow="auto"
alignItems="center"
mx="4"
>
{item.members.map((member) => (
<Box key={member.tmbId} justifySelf="start">
<MemberTag name={member.name} avatar={member.avatar} />
</Box>
))}
</Grid>
</Box>
)}
</MyPopover>
)}
</Td>
<Td>
{!isForbidden && (
<>
<Button
size="sm"
variant="outline"
onClick={() => onCopy(item._id)}
color="myGray.900"
>
<Icon name="common/link" w="16px" mr="1" />
{t('account_team:copy_link')}
</Button>
<MyPopover
placement="bottom-end"
Trigger={
<Button variant="outline" ml="10px" size="sm" color="myGray.900">
<Icon name="common/lineStop" w="16px" mr="1" />
{t('account_team:forbidden')}
</Button>
}
closeOnBlur={true}
>
{({ onClose: onClosePopover }) => (
<Box p={4}>
<Box fontWeight={400} whiteSpace="pre-wrap">
{t('account_team:forbid_hint')}
</Box>
<Flex gap={2} mt={2} justifyContent={'flex-end'}>
<Button variant="outline" onClick={onClosePopover}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={forbiding}
variant="outline"
colorScheme="red"
onClick={() => {
onForbid(item._id);
onClosePopover();
}}
>
{t('account_team:confirm_forbidden')}
</Button>
</Flex>
</Box>
)}
</MyPopover>
</>
)}
</Td>
</Tr>
);
})}
</Tbody>
)}
</Table>
{!invitationLinkList?.length && <EmptyTip />}
</TableContainer>
</ModalBody>
<ModalFooter justifyContent={'flex-start'}>
<Tag colorSchema="blue" marginBlock="2">
<Box>{t('account_team:invitation_link_auto_clean_hint')}</Box>
</Tag>
</ModalFooter>
{isOpenCreate && (
<CreateInvitationModal
onClose={() => Promise.all([onCloseCreate(), refetchInvitationLinkList()])}
/>
)}
</MyModal>
);
};
export default InviteModal;

View File

@@ -1,90 +0,0 @@
import React, { useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
import TagTextarea from '@/components/common/Textarea/TagTextarea';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postInviteTeamMember } from '@/web/support/user/team/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
const InviteModal = ({
teamId,
onClose,
onSuccess
}: {
teamId: string;
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({
title: t('user:team.Invite Member Result Tip'),
showCancel: false
});
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
const { runAsync: onInvite, loading: isLoading } = useRequest2(
() =>
postInviteTeamMember({
teamId,
usernames: inviteUsernames
}),
{
onSuccess(res: InviteMemberResponse) {
onSuccess();
openConfirm(
() => onClose(),
undefined,
<Box whiteSpace={'pre-wrap'}>
{t('user:team.Invite Member Success Tip', {
success: res.invite.length,
inValid: res.inValid.map((item) => item.username).join(', '),
inTeam: res.inTeam.map((item) => item.username).join(', ')
})}
</Box>
)();
},
errorToast: t('user:team.Invite Member Failed Tip')
}
);
return (
<MyModal
isOpen
iconSrc="common/inviteLight"
iconColor="primary.600"
title={
<Box>
<Box>{t('common:user.team.Invite Member')}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{t('common:user.team.Invite Member Tips')}
</Box>
</Box>
}
maxW={['90vw', '400px']}
overflow={'unset'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Box mb={2}>{t('common:user.Account')}</Box>
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
</ModalBody>
<ModalFooter>
<Button
w={'100%'}
h={'34px'}
isDisabled={inviteUsernames.length === 0}
isLoading={isLoading}
onClick={onInvite}
>
{t('user:team.Confirm Invite')}
</Button>
</ModalFooter>
<ConfirmModal />
</MyModal>
);
};
export default InviteModal;

View File

@@ -17,7 +17,7 @@ import {
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { delRemoveMember, updateStatus } from '@/web/support/user/team/api';
import { delRemoveMember, postRestoreMember } from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
@@ -41,7 +41,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils';
const InviteModal = dynamic(() => import('./InviteModal'));
const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
@@ -118,7 +118,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
errorToast: t('account_team:sync_member_failed')
});
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(updateStatus, {
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(postRestoreMember, {
onSuccess() {
refetchMembers();
},
@@ -175,22 +175,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('common:user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
setNotSufficientModalType(TeamErrEnum.teamMemberOverSize);
} else {
onOpenInvite();
}
}}
onClick={onOpenInvite}
>
{t('account_team:user_team_invite_member')}
</Button>
@@ -236,7 +221,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:contact')}</Th>
<Th bgColor="myGray.100">{t('common:contact_way')}</Th>
<Th bgColor="myGray.100">{t('account_team:org')}</Th>
<Th bgColor="myGray.100">{t('account_team:join_update_time')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
@@ -253,12 +238,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{member.memberName}
{member.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
{member.status === 'leave' && (
{member.status !== 'active' && (
<Tag ml="2" colorSchema="gray">
{t('account_team:leave')}
</Tag>
@@ -295,7 +275,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
{userInfo?.team.permission.hasManagePer &&
member.role !== TeamMemberRoleEnum.owner &&
member.tmbId !== userInfo?.team.tmbId &&
(member.status !== TeamMemberStatusEnum.leave ? (
(member.status === TeamMemberStatusEnum.active ? (
<Icon
name={'common/trash'}
cursor={'pointer'}
@@ -320,30 +300,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}}
/>
) : (
<Icon
name={'common/confirm/restoreTip'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'primary.500',
bgColor: 'myGray.100'
}}
onClick={() => {
openRestoreMember(
() =>
onRestore({
tmbId: member.tmbId,
status: TeamMemberStatusEnum.active
}),
undefined,
t('account_team:restore_tip', {
username: member.memberName
})
)();
}}
/>
member.status === TeamMemberStatusEnum.forbidden && (
<Icon
name={'common/confirm/restoreTip'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'primary.500',
bgColor: 'myGray.100'
}}
onClick={() => {
openRestoreMember(
() => onRestore(member.tmbId),
undefined,
t('account_team:restore_tip', {
username: member.memberName
})
)();
}}
/>
)
))}
</Td>
</Tr>

View File

@@ -101,6 +101,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
refetchMembers();
return initUserInfo();
},
{