V4.9.2 feature (#4354)
* feat: custom dataset split sign (#4221) * feat: custom dataset split sign * feat: custom dataset split sign * add external variable debug (#4204) * add external variable debug * fix ui * plugin variables * perf: custom varialbe (#4225) * fix: invite link (#4229) * fix: invite link * feat: create invite link and copy it directly * feat: sync api collection will refresh title;perf: invite link ux (#4237) * update queue * feat: sync api collection will refresh title * sync collection * remove lock * perf: invite link ux * fix ts (#4239) * sync collection * remove lock * fix ts * fix: ts * Sso (#4235) * feat: redirect url can be inner url (#4138) * fix: update new user sync api (#4145) * feat: post all params to backend (#4151) * pref: sso getauthurl api (#4172) * pref: sso getauthurl api * pref: sso * solve the rootorglist (#4234) --------- Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com> * fix variable sync & popover button height (#4227) * fix variable sync & popover button height * required * feat: node prompt version (#4141) * feat: node prompt version * fix * delete unused code * fix * fix code * update prompt version (#4242) * sync collection * remove lock * update prompt version * perf: ai proxy (#4265) * sync collection * remove lock * perf: ai proxy * fix: member count (#4269) * feat: chunk index independent config (#4271) * sync collection * remove lock * feat: chunk index independent config * feat: add max chunksize to split chunk function * remove log * update doc * remove * remove log * fix input form label overflow (#4266) * add model test log (#4272) * sync collection * remove lock * add model test log * update ui * update log * fix: channel test * preview chunk ui * test model ux * test model log * perf: dataset selector * fix: system plugin auth * update nextjs * perf: ai proxy log remove retry log;perf: workflow type auto parse;add chunk spliter test (#4296) * sync collection * remove lock * perf: workflow type auto parse * add chunk spliter test * perf: ai proxy log remove retry log * udpate ai proxy field * pref: member/org/gourp list (#4295) * refactor: org api * refactor: org api * pref: member/org/group list * feat: change group owner api * fix: manage org member * pref: member search * tmp org api rewrite (#4304) * sync collection * remove lock * tmp org api rewrite * perf: text splitter (#4313) * sync collection * remove lock * perf: text splitter * update comment * update search filter code (#4317) * sync collection * remove lock * update search filter code * pref: member/group/org (#4316) * feat: change group owner api * pref: member/org/group * fix: member modal select clb * fix: search member when change owner * fix: member list, login button (#4322) * perf: member group (#4324) * sync collection * remove lock * perf: member group * fix: ts (#4325) * sync collection * remove lock * fix: ts * fix: group (#4330) * perf: intro wrap (#4346) * sync collection * remove lock * perf: intro wrap * pref: member list (#4344) * chore: search member new api * chore: permission * fix: ts error * fix: member modal * perf: long org name ui (#4347) * sync collection * remove lock * perf: long org name ui * perf: member tableui (#4353) * fix: ts (#4357) * docs: Add SSO Markdown Doc (#4334) * add sso doc * fix comment * update sso doc (#4358) * pref: useScrollPagination support debounce and throttle. (#4355) * pref: useScrollPagination support debounce and throttle. * fix: useScrollPagination loading * fix: isloading * fix: org search path hide * fix: simple app all_app button (#4365) * add qwen long (#4363) --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,9 @@ const NotSufficientModal = dynamic(() => import('@/components/support/wallet/Not
|
||||
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
|
||||
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
|
||||
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'));
|
||||
const ManualCopyModal = dynamic(() =>
|
||||
import('@fastgpt/web/hooks/useCopyData').then((mod) => mod.ManualCopyModal)
|
||||
);
|
||||
|
||||
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/': true,
|
||||
@@ -162,6 +165,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<ManualCopyModal />
|
||||
<Loading loading={loading} zIndex={999999} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ const EditResourceModal = ({
|
||||
{...register('name', { required: true })}
|
||||
bg={'myGray.50'}
|
||||
autoFocus
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
@@ -44,7 +44,9 @@ const FolderPath = (props: {
|
||||
py={0.5}
|
||||
px={1.5}
|
||||
borderRadius={'md'}
|
||||
{...(i === concatPaths.length - 1
|
||||
maxW={'45vw'}
|
||||
className={'textEllipsis'}
|
||||
{...(i === concatPaths.length - 1 && concatPaths.length > 1
|
||||
? {
|
||||
cursor: 'default',
|
||||
color: 'myGray.700',
|
||||
|
||||
@@ -69,31 +69,37 @@ export const DatasetSelectModal = ({
|
||||
{selectedDatasets.map((item) =>
|
||||
(() => {
|
||||
return (
|
||||
<Card
|
||||
key={item.datasetId}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
bg={'primary.200'}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['1.25rem', '1.75rem']}></Avatar>
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" mx={3}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
setSelectedDatasets((state) =>
|
||||
state.filter((dataset) => dataset.datasetId !== item.datasetId)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
<MyTooltip label={item.name}>
|
||||
<Card
|
||||
key={item.datasetId}
|
||||
p={3}
|
||||
border={'base'}
|
||||
boxShadow={'sm'}
|
||||
bg={'primary.200'}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar
|
||||
src={item.avatar}
|
||||
w={['1.25rem', '1.75rem']}
|
||||
borderRadius={'sm'}
|
||||
></Avatar>
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" mx={3} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
setSelectedDatasets((state) =>
|
||||
state.filter((dataset) => dataset.datasetId !== item.datasetId)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
</MyTooltip>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
@@ -117,7 +123,7 @@ export const DatasetSelectModal = ({
|
||||
label={
|
||||
item.type === DatasetTypeEnum.folder
|
||||
? t('common:dataset.Select Folder')
|
||||
: t('common:dataset.Select Dataset')
|
||||
: item.name
|
||||
}
|
||||
>
|
||||
<Card
|
||||
@@ -152,14 +158,18 @@ export const DatasetSelectModal = ({
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
|
||||
<Avatar
|
||||
src={item.avatar}
|
||||
w={['1.25rem', '1.75rem']}
|
||||
borderRadius={'sm'}
|
||||
></Avatar>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
className="textEllipsis"
|
||||
ml={3}
|
||||
fontSize={'md'}
|
||||
color={'myGray.900'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
|
||||
@@ -14,8 +14,8 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||
import CustomPromptEditor from '@fastgpt/web/components/common/Textarea/CustomPromptEditor';
|
||||
import {
|
||||
PROMPT_QUESTION_GUIDE,
|
||||
PROMPT_QUESTION_GUIDE_FOOTER
|
||||
QuestionGuideFooterPrompt,
|
||||
QuestionGuidePrompt
|
||||
} from '@fastgpt/global/core/ai/prompt/agent';
|
||||
|
||||
// question generator config
|
||||
@@ -38,7 +38,7 @@ const QGConfig = ({
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
|
||||
<FormLabel>{t('common:core.app.Question Guide')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{t('common:core.app.Question Guide')}</FormLabel>
|
||||
<ChatFunctionTip type={'nextQuestion'} />
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('app:config_question_guide')}>
|
||||
@@ -168,7 +168,7 @@ const QGConfigModal = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{customPrompt || PROMPT_QUESTION_GUIDE}
|
||||
{customPrompt || QuestionGuidePrompt}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
@@ -178,8 +178,8 @@ const QGConfigModal = ({
|
||||
{isOpenCustomPrompt && (
|
||||
<CustomPromptEditor
|
||||
defaultValue={customPrompt}
|
||||
defaultPrompt={PROMPT_QUESTION_GUIDE}
|
||||
footerPrompt={PROMPT_QUESTION_GUIDE_FOOTER}
|
||||
defaultPrompt={QuestionGuidePrompt}
|
||||
footerPrompt={QuestionGuideFooterPrompt}
|
||||
onChange={(e) => {
|
||||
onChange({
|
||||
...value,
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Controller, UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Card, Textarea } from '@chakra-ui/react';
|
||||
import { Box, Button, Card, Flex, Switch, Textarea } from '@chakra-ui/react';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
VariableInputEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ChatBoxInputFormType } from '../type.d';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
export const VariableInputItem = ({
|
||||
item,
|
||||
@@ -23,7 +29,11 @@ export const VariableInputItem = ({
|
||||
item: VariableItemType;
|
||||
variablesForm: UseFormReturn<any>;
|
||||
}) => {
|
||||
const { register, control, setValue } = variablesForm;
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = variablesForm;
|
||||
|
||||
return (
|
||||
<Box key={item.id} mb={4} pl={1}>
|
||||
@@ -43,37 +53,31 @@ export const VariableInputItem = ({
|
||||
)}
|
||||
{item.description && <QuestionTip ml={1} label={item.description} />}
|
||||
</Box>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<MyTextarea
|
||||
autoHeight
|
||||
minH={40}
|
||||
maxH={160}
|
||||
bg={'myGray.50'}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
bg={'myGray.50'}
|
||||
maxLength={item.maxLength || 4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<Controller
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={`variables.${item.key}`}
|
||||
rules={{ required: item.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
|
||||
<Controller
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={`variables.${item.key}`}
|
||||
rules={{
|
||||
required: item.required
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
if (item.type === VariableInputEnum.input) {
|
||||
return (
|
||||
<MyTextarea
|
||||
autoHeight
|
||||
minH={40}
|
||||
maxH={160}
|
||||
bg={'myGray.50'}
|
||||
value={value}
|
||||
isInvalid={errors?.variables && Object.keys(errors.variables).includes(item.key)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (item.type === VariableInputEnum.select) {
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={(item.enums || []).map((item: { value: any }) => ({
|
||||
label: item.value,
|
||||
@@ -83,86 +87,217 @@ export const VariableInputItem = ({
|
||||
onChange={(e) => setValue(`variables.${item.key}`, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.numberInput && (
|
||||
<Controller
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={`variables.${item.key}`}
|
||||
rules={{ required: item.required, min: item.min, max: item.max }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<MyNumberInput
|
||||
step={1}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
bg={'white'}
|
||||
}
|
||||
if (item.type === VariableInputEnum.numberInput) {
|
||||
return (
|
||||
<MyNumberInput
|
||||
step={1}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
bg={'white'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isInvalid={errors?.variables && Object.keys(errors.variables).includes(item.key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
rows={5}
|
||||
bg={'myGray.50'}
|
||||
maxLength={item.maxLength || 4000}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExternalVariableInputItem = ({
|
||||
item,
|
||||
variablesForm,
|
||||
showTag = false
|
||||
}: {
|
||||
item: VariableItemType;
|
||||
variablesForm: UseFormReturn<any>;
|
||||
showTag?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = variablesForm;
|
||||
|
||||
const Label = useMemo(() => {
|
||||
return (
|
||||
<Box display={'flex'} position={'relative'} mb={1} alignItems={'center'} w={'full'}>
|
||||
{item.label}
|
||||
{item.description && <QuestionTip ml={1} label={item.description} />}
|
||||
{showTag && (
|
||||
<Flex
|
||||
color={'primary.600'}
|
||||
bg={'primary.100'}
|
||||
px={2}
|
||||
py={1}
|
||||
gap={1}
|
||||
ml={2}
|
||||
fontSize={'mini'}
|
||||
rounded={'sm'}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
|
||||
{t('chat:variable_invisable_in_share')}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}, [item.description, item.label, showTag, t]);
|
||||
|
||||
return (
|
||||
<Box key={item.id} mb={4} pl={1}>
|
||||
{Label}
|
||||
<Controller
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={`variables.${item.key}`}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
if (item.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<MyTextarea
|
||||
autoHeight
|
||||
minH={40}
|
||||
maxH={160}
|
||||
bg={'myGray.50'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (item.valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return <MyNumberInput step={1} bg={'myGray.50'} value={value} onChange={onChange} />;
|
||||
}
|
||||
if (item.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return <Switch isChecked={value} onChange={onChange} />;
|
||||
}
|
||||
return <JsonEditor bg={'myGray.50'} resize value={value} onChange={onChange} />;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const VariableInput = ({
|
||||
chatForm,
|
||||
chatStarted
|
||||
chatStarted,
|
||||
showExternalVariables = false
|
||||
}: {
|
||||
chatStarted: boolean;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
chatStarted: boolean;
|
||||
showExternalVariables?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.avatar);
|
||||
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
|
||||
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
|
||||
const allVariableList = useContextSelector(ChatBoxContext, (v) => v.allVariableList);
|
||||
|
||||
const externalVariableList = useMemo(
|
||||
() =>
|
||||
allVariableList.filter((item) =>
|
||||
showExternalVariables ? item.type === VariableInputEnum.custom : false
|
||||
),
|
||||
[allVariableList, showExternalVariables]
|
||||
);
|
||||
|
||||
const { getValues, setValue, handleSubmit: handleSubmitChat } = variablesForm;
|
||||
|
||||
useEffect(() => {
|
||||
variableList.forEach((item) => {
|
||||
allVariableList.forEach((item) => {
|
||||
const val = getValues(`variables.${item.key}`);
|
||||
if (item.defaultValue !== undefined && (val === undefined || val === null || val === '')) {
|
||||
setValue(`variables.${item.key}`, item.defaultValue);
|
||||
}
|
||||
});
|
||||
}, [variableList]);
|
||||
}, [allVariableList, getValues, setValue, variableList]);
|
||||
|
||||
return (
|
||||
<Box py={3}>
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
w={'400px'}
|
||||
{...MessageCardStyle}
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
{variableList.map((item) => (
|
||||
<VariableInputItem key={item.id} item={item} variablesForm={variablesForm} />
|
||||
))}
|
||||
{!chatStarted && (
|
||||
<Box>
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat(() => {
|
||||
chatForm.setValue('chatStarted', true);
|
||||
})}
|
||||
>
|
||||
{t('common:core.chat.Start Chat')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
{externalVariableList.length > 0 && (
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
w={'400px'}
|
||||
{...MessageCardStyle}
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
<Flex
|
||||
color={'primary.600'}
|
||||
bg={'primary.100'}
|
||||
mb={3}
|
||||
px={3}
|
||||
py={1.5}
|
||||
gap={1}
|
||||
fontSize={'mini'}
|
||||
rounded={'sm'}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
|
||||
{t('chat:variable_invisable_in_share')}
|
||||
</Flex>
|
||||
{externalVariableList.map((item) => (
|
||||
<ExternalVariableInputItem key={item.id} item={item} variablesForm={variablesForm} />
|
||||
))}
|
||||
{variableList.length === 0 && !chatStarted && (
|
||||
<Box>
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat(() => {
|
||||
chatForm.setValue('chatStarted', true);
|
||||
})}
|
||||
>
|
||||
{t('common:core.chat.Start Chat')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{variableList.length > 0 && (
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
w={'400px'}
|
||||
{...MessageCardStyle}
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
{variableList.map((item) => (
|
||||
<VariableInputItem key={item.id} item={item} variablesForm={variablesForm} />
|
||||
))}
|
||||
{!chatStarted && (
|
||||
<Box>
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat(() => {
|
||||
console.log('start chat');
|
||||
chatForm.setValue('chatStarted', true);
|
||||
})}
|
||||
>
|
||||
{t('common:core.chat.Start Chat')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useEffect } from 'react';
|
||||
import { ExternalVariableInputItem, VariableInputItem } from './VariableInput';
|
||||
import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
||||
|
||||
const VariablePopover = ({
|
||||
showExternalVariables = false
|
||||
}: {
|
||||
showExternalVariables?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
|
||||
const variables = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.chatBoxData?.app?.chatConfig?.variables ?? []
|
||||
);
|
||||
const variableList = variables.filter((item) => item.type !== VariableInputEnum.custom);
|
||||
const externalVariableList = variables.filter((item) =>
|
||||
showExternalVariables ? item.type === VariableInputEnum.custom : false
|
||||
);
|
||||
|
||||
const hasExternalVariable = externalVariableList.length > 0;
|
||||
const hasVariable = variableList.length > 0;
|
||||
|
||||
const { getValues, setValue } = variablesForm;
|
||||
|
||||
useEffect(() => {
|
||||
variables.forEach((item) => {
|
||||
const val = getValues(`variables.${item.key}`);
|
||||
if (item.defaultValue !== undefined && (val === undefined || val === null || val === '')) {
|
||||
setValue(`variables.${item.key}`, item.defaultValue);
|
||||
}
|
||||
});
|
||||
}, [variables]);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement="bottom"
|
||||
trigger={'click'}
|
||||
closeOnBlur={true}
|
||||
Trigger={
|
||||
<Button variant={'whiteBase'} size={'sm'} leftIcon={<MyIcon name={'edit'} w={4} />}>
|
||||
{t('common:core.module.Variable')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={4}>
|
||||
{hasExternalVariable && (
|
||||
<Box textAlign={'left'}>
|
||||
<Flex
|
||||
color={'primary.600'}
|
||||
bg={'primary.100'}
|
||||
mb={3}
|
||||
px={3}
|
||||
py={1.5}
|
||||
gap={1}
|
||||
fontSize={'mini'}
|
||||
rounded={'sm'}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
|
||||
{t('chat:variable_invisable_in_share')}
|
||||
</Flex>
|
||||
{externalVariableList.map((item) => (
|
||||
<ExternalVariableInputItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
variablesForm={variablesForm}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{hasExternalVariable && hasVariable && <MyDivider h={'1px'} />}
|
||||
{hasVariable && (
|
||||
<Box textAlign={'left'}>
|
||||
{variableList.map((item) => (
|
||||
<VariableInputItem key={item.id} item={item} variablesForm={variablesForm} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Flex w={'full'} justifyContent={'flex-end'}>
|
||||
<Button size={'sm'} onClick={onClose}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablePopover;
|
||||
@@ -65,6 +65,7 @@ import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import TimeBox from './components/TimeBox';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
||||
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
|
||||
@@ -103,7 +104,8 @@ const ChatBox = ({
|
||||
showVoiceIcon = true,
|
||||
showEmptyIntro = false,
|
||||
active = true,
|
||||
onStartChat
|
||||
onStartChat,
|
||||
chatType
|
||||
}: Props) => {
|
||||
const ScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -129,6 +131,8 @@ const ChatBox = ({
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const ChatBoxRef = useContextSelector(ChatItemContext, (v) => v.ChatBoxRef);
|
||||
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
|
||||
const setIsVariableVisible = useContextSelector(ChatItemContext, (v) => v.setIsVariableVisible);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
|
||||
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
|
||||
@@ -150,6 +154,12 @@ const ChatBox = ({
|
||||
// Workflow running, there are user input or selection
|
||||
const isInteractive = useMemo(() => checkIsInteractiveByHistories(chatRecords), [chatRecords]);
|
||||
|
||||
const externalVariableList = useMemo(() => {
|
||||
if (chatType === 'chat') {
|
||||
return allVariableList.filter((item) => item.type === VariableInputEnum.custom);
|
||||
}
|
||||
return [];
|
||||
}, [allVariableList, chatType]);
|
||||
// compute variable input is finish.
|
||||
const chatForm = useForm<ChatBoxInputFormType>({
|
||||
defaultValues: {
|
||||
@@ -162,7 +172,9 @@ const ChatBox = ({
|
||||
const chatStartedWatch = watch('chatStarted');
|
||||
const chatStarted =
|
||||
chatBoxData?.appId === appId &&
|
||||
(chatStartedWatch || chatRecords.length > 0 || variableList.length === 0);
|
||||
(chatStartedWatch ||
|
||||
chatRecords.length > 0 ||
|
||||
[...variableList, ...externalVariableList].length === 0);
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useMemoizedFn((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
|
||||
@@ -891,6 +903,33 @@ const ChatBox = ({
|
||||
}
|
||||
}));
|
||||
|
||||
// Visibility check
|
||||
useEffect(() => {
|
||||
const checkVariableVisibility = () => {
|
||||
if (!ScrollContainerRef.current) return;
|
||||
const container = ScrollContainerRef.current;
|
||||
const variableInput = container.querySelector('#variable-input');
|
||||
if (!variableInput) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = variableInput.getBoundingClientRect();
|
||||
|
||||
setIsVariableVisible(
|
||||
elementRect.bottom > containerRect.top && elementRect.top < containerRect.bottom
|
||||
);
|
||||
};
|
||||
|
||||
const container = ScrollContainerRef.current;
|
||||
if (container) {
|
||||
checkVariableVisibility();
|
||||
container.addEventListener('scroll', checkVariableVisibility);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', checkVariableVisibility);
|
||||
};
|
||||
}
|
||||
}, [chatType, setIsVariableVisible]);
|
||||
|
||||
const RenderRecords = useMemo(() => {
|
||||
return (
|
||||
<ScrollData
|
||||
@@ -906,8 +945,14 @@ const ChatBox = ({
|
||||
{showEmpty && <Empty />}
|
||||
{!!welcomeText && <WelcomeBox welcomeText={welcomeText} />}
|
||||
{/* variable input */}
|
||||
{!!variableList?.length && (
|
||||
<VariableInput chatStarted={chatStarted} chatForm={chatForm} />
|
||||
{(!!variableList?.length || !!externalVariableList?.length) && (
|
||||
<Box id="variable-input">
|
||||
<VariableInput
|
||||
chatStarted={chatStarted}
|
||||
chatForm={chatForm}
|
||||
showExternalVariables={chatType === 'chat'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{/* chat history */}
|
||||
<Box id={'history'}>
|
||||
@@ -1006,7 +1051,9 @@ const ChatBox = ({
|
||||
chatForm,
|
||||
chatRecords,
|
||||
chatStarted,
|
||||
chatType,
|
||||
delOneMessage,
|
||||
externalVariableList?.length,
|
||||
isChatting,
|
||||
onAddUserDislike,
|
||||
onAddUserLike,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ChatBoxInputFormType } from '../../ChatBox/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
const RenderInput = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -213,36 +214,43 @@ const RenderInput = () => {
|
||||
</Box>
|
||||
)}
|
||||
{/* Filed */}
|
||||
{formatPluginInputs.map((input) => {
|
||||
return (
|
||||
<Controller
|
||||
key={`variables.${input.key}`}
|
||||
control={control}
|
||||
name={`variables.${input.key}`}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (!input.required) return true;
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return value !== undefined;
|
||||
{formatPluginInputs
|
||||
.filter((input) => {
|
||||
if (outLinkAuthData && Object.keys(outLinkAuthData).length > 0) {
|
||||
return input.renderTypeList[0] !== FlowNodeInputTypeEnum.customVariable;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((input) => {
|
||||
return (
|
||||
<Controller
|
||||
key={`variables.${input.key}`}
|
||||
control={control}
|
||||
name={`variables.${input.key}`}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (!input.required) return true;
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return value !== undefined;
|
||||
}
|
||||
return !!value;
|
||||
}
|
||||
return !!value;
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<RenderPluginInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isDisabled={isDisabledInput}
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
input={input}
|
||||
setUploading={setUploading}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<RenderPluginInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isDisabled={isDisabledInput}
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
input={input}
|
||||
setUploading={setUploading}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Run Button */}
|
||||
{onStartChat && onNewChat && (
|
||||
<Flex justifyContent={'end'} mt={8}>
|
||||
|
||||
@@ -157,9 +157,6 @@ const RenderPluginInput = ({
|
||||
const { llmModelList } = useSystemStore();
|
||||
|
||||
const render = (() => {
|
||||
if (inputType === FlowNodeInputTypeEnum.customVariable) {
|
||||
return null;
|
||||
}
|
||||
if (inputType === FlowNodeInputTypeEnum.select && input.list) {
|
||||
return (
|
||||
<MySelect list={input.list} value={value} onChange={onChange} isDisabled={isDisabled} />
|
||||
@@ -246,6 +243,21 @@ const RenderPluginInput = ({
|
||||
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
|
||||
{inputType === FlowNodeInputTypeEnum.customVariable && (
|
||||
<Flex
|
||||
color={'primary.600'}
|
||||
bg={'primary.100'}
|
||||
px={2}
|
||||
py={1}
|
||||
gap={1}
|
||||
ml={2}
|
||||
fontSize={'mini'}
|
||||
rounded={'sm'}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
|
||||
{t('chat:variable_invisable_in_share')}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
|
||||
@@ -268,10 +268,10 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
{interactive.params.description && <Markdown source={interactive.params.description} />}
|
||||
{interactive.params.inputForm?.map((input) => (
|
||||
<Box key={input.label}>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<FormLabel required={input.required}>{input.label}</FormLabel>
|
||||
<FormLabel mb={1} required={input.required} whiteSpace={'pre-wrap'}>
|
||||
{input.label}
|
||||
{input.description && <QuestionTip ml={1} label={input.description} />}
|
||||
</Flex>
|
||||
</FormLabel>
|
||||
{input.type === FlowNodeInputTypeEnum.input && (
|
||||
<MyTextarea
|
||||
isDisabled={interactive.params.submitted}
|
||||
|
||||
@@ -250,22 +250,24 @@ export const WholeResponseContent = ({
|
||||
value={activeModule?.similarity}
|
||||
/>
|
||||
<Row label={t('common:core.chat.response.module limit')} value={activeModule?.limit} />
|
||||
<Row
|
||||
label={t('common:core.chat.response.search using reRank')}
|
||||
rawDom={
|
||||
<Box border={'base'} borderRadius={'md'} p={2}>
|
||||
{activeModule?.searchUsingReRank ? (
|
||||
activeModule?.rerankModel ? (
|
||||
<Box>{`${activeModule.rerankModel}: ${activeModule.rerankWeight}`}</Box>
|
||||
{activeModule?.searchUsingReRank !== undefined && (
|
||||
<Row
|
||||
label={t('common:core.chat.response.search using reRank')}
|
||||
rawDom={
|
||||
<Box border={'base'} borderRadius={'md'} p={2}>
|
||||
{activeModule?.searchUsingReRank ? (
|
||||
activeModule?.rerankModel ? (
|
||||
<Box>{`${activeModule.rerankModel}: ${activeModule.rerankWeight}`}</Box>
|
||||
) : (
|
||||
'True'
|
||||
)
|
||||
) : (
|
||||
'True'
|
||||
)
|
||||
) : (
|
||||
`False`
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
`False`
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{activeModule.queryExtensionResult && (
|
||||
<>
|
||||
<Row
|
||||
|
||||
@@ -338,7 +338,7 @@ function EditKeyModal({
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={t('publish:key_alias') || 'key_alias'}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty') || 'name_is_empty'
|
||||
})}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import {
|
||||
Box,
|
||||
@@ -38,16 +39,29 @@ export function ChangeOwnerModal({
|
||||
pageSize: 15
|
||||
});
|
||||
|
||||
const memberList = teamMembers.filter((item) => {
|
||||
return item.memberName.includes(inputValue);
|
||||
});
|
||||
const { data: searchedData } = useRequest2(
|
||||
async () => {
|
||||
if (!inputValue) return;
|
||||
return GetSearchUserGroupOrg(inputValue);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [inputValue],
|
||||
throttleWait: 500,
|
||||
debounceWait: 200
|
||||
}
|
||||
);
|
||||
|
||||
const memberList = searchedData ? searchedData.members : teamMembers;
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
onClose: onCloseMemberListMenu,
|
||||
onOpen: onOpenMemberListMenu
|
||||
} = useDisclosure();
|
||||
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
|
||||
const [selectedMember, setSelectedMember] = useState<Omit<
|
||||
TeamMemberItemType,
|
||||
'permission' | 'teamId'
|
||||
> | null>(null);
|
||||
|
||||
const { runAsync, loading } = useRequest2(onChangeOwner, {
|
||||
onSuccess: onClose,
|
||||
|
||||
@@ -19,7 +19,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PermissionSelect from './PermissionSelect';
|
||||
import PermissionTags from './PermissionTags';
|
||||
import {
|
||||
@@ -28,17 +28,18 @@ import {
|
||||
DEFAULT_USER_AVATAR
|
||||
} from '@fastgpt/global/common/system/constants';
|
||||
import Path from '@/components/common/folder/Path';
|
||||
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollaboratorContext } from './context';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import { getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { getOrgList } from '@/web/support/user/team/org/api';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import MemberItemCard from './MemberItemCard';
|
||||
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
|
||||
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import _ from 'lodash';
|
||||
|
||||
const HoverBoxStyle = {
|
||||
bgColor: 'myGray.50',
|
||||
@@ -55,16 +56,46 @@ function MemberModal({
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
|
||||
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 15
|
||||
const {
|
||||
paths,
|
||||
onClickOrg,
|
||||
members: orgMembers,
|
||||
MemberScrollData: OrgMemberScrollData,
|
||||
onPathClick,
|
||||
orgs,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
} = useOrg({ withPermission: false });
|
||||
|
||||
const {
|
||||
data: members,
|
||||
ScrollData: TeamMemberScrollData,
|
||||
refreshList
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 15,
|
||||
params: {
|
||||
withPermission: true,
|
||||
withOrgs: true,
|
||||
status: 'active',
|
||||
searchKey
|
||||
},
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
const { data: [groups = [], orgs = []] = [], loading: loadingGroupsAndOrgs } = useRequest2(
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: loadingGroupsAndOrgs,
|
||||
runAsync: refreshGroups
|
||||
} = useRequest2(
|
||||
async () => {
|
||||
if (!userInfo?.team?.teamId) return [[], []];
|
||||
return Promise.all([getGroupList(), getOrgList()]);
|
||||
if (!userInfo?.team?.teamId) return [];
|
||||
return getGroupList<false>({
|
||||
withMembers: false,
|
||||
searchKey
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
@@ -72,84 +103,13 @@ function MemberModal({
|
||||
}
|
||||
);
|
||||
|
||||
const [parentPath, setParentPath] = useState('');
|
||||
const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
|
||||
|
||||
const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), {
|
||||
manual: false,
|
||||
throttleWait: 500,
|
||||
refreshDeps: [searchText]
|
||||
});
|
||||
|
||||
const paths = useMemo(() => {
|
||||
const splitPath = parentPath.split('/').filter(Boolean);
|
||||
return splitPath
|
||||
.map((id) => {
|
||||
const org = orgs.find((org) => org.pathId === id)!;
|
||||
|
||||
if (org.path === '') return;
|
||||
|
||||
return {
|
||||
parentId: getOrgChildrenPath(org),
|
||||
parentName: org.name
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ParentTreePathItemType[];
|
||||
}, [parentPath, orgs]);
|
||||
|
||||
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
|
||||
const currentOrg = useMemo(() => {
|
||||
const splitPath = parentPath.split('/');
|
||||
const currentOrgId = splitPath[splitPath.length - 1];
|
||||
if (!currentOrgId) return;
|
||||
|
||||
return orgs.find((org) => org.pathId === currentOrgId);
|
||||
}, [orgs, parentPath]);
|
||||
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
|
||||
if (searchText && searchedData) {
|
||||
const orgids = searchedData.orgs.map((item) => item._id);
|
||||
return orgs.filter((org) => orgids.includes(String(org._id)));
|
||||
}
|
||||
if (!searchText && filterClass !== 'org') return [];
|
||||
if (parentPath === '') {
|
||||
setParentPath(`/${orgs[0].pathId}`);
|
||||
return [];
|
||||
}
|
||||
return orgs
|
||||
.filter((org) => org.path === parentPath)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
count:
|
||||
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
|
||||
}));
|
||||
}, [searchText, filterClass, parentPath, orgs, searchedData]);
|
||||
|
||||
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
|
||||
const filterMembers = useMemo(() => {
|
||||
if (searchText) {
|
||||
return searchedData?.members || [];
|
||||
}
|
||||
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
|
||||
if (currentOrg && filterClass === 'org') {
|
||||
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
|
||||
}
|
||||
|
||||
return members;
|
||||
}, [members, searchedData, searchText, filterClass, currentOrg]);
|
||||
|
||||
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
|
||||
const filterGroups = useMemo(() => {
|
||||
if (searchText) {
|
||||
return searchedData?.groups.map((item) => ({
|
||||
groupName: item.name,
|
||||
_id: item.id,
|
||||
...item
|
||||
}));
|
||||
}
|
||||
if (!searchText && filterClass !== 'group') return [];
|
||||
|
||||
return groups;
|
||||
}, [searchText, filterClass, groups, searchedData]);
|
||||
const [selectedMemberList, setSelectedMemberList] = useState<
|
||||
Omit<TeamMemberItemType, 'permission' | 'teamId'>[]
|
||||
>([]);
|
||||
|
||||
const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]);
|
||||
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
|
||||
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
|
||||
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
|
||||
@@ -168,9 +128,9 @@ function MemberModal({
|
||||
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
|
||||
() =>
|
||||
onUpdateCollaborators({
|
||||
members: selectedMemberIdList,
|
||||
groups: selectedGroupIdList,
|
||||
orgs: selectedOrgIdList,
|
||||
members: selectedMemberList.map((item) => item.tmbId),
|
||||
groups: selectedGroupList.map((item) => item._id),
|
||||
orgs: selectedOrgList.map((item) => item._id),
|
||||
permission: selectedPermission!
|
||||
}),
|
||||
{
|
||||
@@ -188,39 +148,31 @@ function MemberModal({
|
||||
]);
|
||||
|
||||
const selectedList = useMemo(() => {
|
||||
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
|
||||
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
|
||||
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
|
||||
|
||||
return [
|
||||
...selectedOrgs.map((item) => ({
|
||||
...selectedOrgList.map((item) => ({
|
||||
id: `org-${item._id}`,
|
||||
avatar: item.avatar,
|
||||
name: item.name,
|
||||
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id))
|
||||
onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)),
|
||||
orgs: undefined
|
||||
})),
|
||||
...selectedGroups.map((item) => ({
|
||||
...selectedGroupList.map((item) => ({
|
||||
id: `group-${item._id}`,
|
||||
avatar: item.avatar,
|
||||
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
|
||||
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id))
|
||||
onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)),
|
||||
orgs: undefined
|
||||
})),
|
||||
...selectedMembers.map((item) => ({
|
||||
...selectedMemberList.map((item) => ({
|
||||
id: `member-${item.tmbId}`,
|
||||
avatar: item.avatar,
|
||||
name: item.memberName,
|
||||
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId))
|
||||
onDelete: () =>
|
||||
setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)),
|
||||
orgs: item.orgs
|
||||
}))
|
||||
];
|
||||
}, [
|
||||
orgs,
|
||||
groups,
|
||||
members,
|
||||
selectedOrgIdList,
|
||||
selectedGroupIdList,
|
||||
selectedMemberIdList,
|
||||
userInfo?.team.teamName
|
||||
]);
|
||||
}, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -253,12 +205,12 @@ function MemberModal({
|
||||
<SearchInput
|
||||
placeholder={t('user:search_group_org_user')}
|
||||
bgColor="myGray.50"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
|
||||
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{/* Entry */}
|
||||
{!searchText && !filterClass && (
|
||||
{!searchKey && !filterClass && (
|
||||
<>
|
||||
{entryList.current.map((item) => {
|
||||
return (
|
||||
@@ -285,7 +237,7 @@ function MemberModal({
|
||||
)}
|
||||
|
||||
{/* Path */}
|
||||
{!searchText && filterClass && (
|
||||
{!searchKey && filterClass && (
|
||||
<Box mb={1}>
|
||||
<Path
|
||||
paths={[
|
||||
@@ -303,37 +255,68 @@ function MemberModal({
|
||||
onClick={(parentId) => {
|
||||
if (parentId === '') {
|
||||
setFilterClass(undefined);
|
||||
setParentPath('');
|
||||
onPathClick('');
|
||||
} else if (
|
||||
parentId === 'member' ||
|
||||
parentId === 'org' ||
|
||||
parentId === 'group'
|
||||
) {
|
||||
setFilterClass(parentId);
|
||||
setParentPath('');
|
||||
onPathClick('');
|
||||
} else {
|
||||
setParentPath(parentId);
|
||||
onPathClick(parentId);
|
||||
}
|
||||
}}
|
||||
rootName={t('common:common.Team')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{(filterClass === 'org' || filterClass === 'member') && (
|
||||
<ScrollData
|
||||
flexDirection={'column'}
|
||||
gap={1}
|
||||
userSelect={'none'}
|
||||
height={'fit-content'}
|
||||
>
|
||||
{filterOrgs?.map((org) => {
|
||||
{(filterClass === 'member' || searchKey) &&
|
||||
(() => {
|
||||
const Members = members?.map((member) => {
|
||||
const onChange = () => {
|
||||
setSelectedMemberList((state) => {
|
||||
if (state.find((v) => v.tmbId === member.tmbId)) {
|
||||
return state.filter((v) => v.tmbId !== member.tmbId);
|
||||
}
|
||||
return [...state, member];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
name={member.memberName}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return searchKey ? (
|
||||
Members
|
||||
) : (
|
||||
<TeamMemberScrollData
|
||||
flexDirection={'column'}
|
||||
gap={1}
|
||||
userSelect={'none'}
|
||||
height={'fit-content'}
|
||||
>
|
||||
{Members}
|
||||
</TeamMemberScrollData>
|
||||
);
|
||||
})()}
|
||||
{(filterClass === 'org' || searchKey) &&
|
||||
(() => {
|
||||
const Orgs = orgs?.map((org) => {
|
||||
const onChange = () => {
|
||||
setSelectedOrgIdList((state) => {
|
||||
if (state.includes(org._id)) {
|
||||
return state.filter((v) => v !== org._id);
|
||||
if (state.find((v) => v._id === org._id)) {
|
||||
return state.filter((v) => v._id !== org._id);
|
||||
}
|
||||
return [...state, org._id];
|
||||
return [...state, org];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
|
||||
@@ -349,22 +332,22 @@ function MemberModal({
|
||||
onClick={onChange}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={selectedOrgIdList.includes(org._id)}
|
||||
isChecked={!!selectedOrgList.find((v) => v._id === org._id)}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<HStack ml="2" w="full" gap="5px">
|
||||
<HStack w="full">
|
||||
<Text>{org.name}</Text>
|
||||
{org.count && (
|
||||
{org.total && (
|
||||
<>
|
||||
<Tag size="sm" my="auto">
|
||||
{org.count}
|
||||
{org.total}
|
||||
</Tag>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
<PermissionTags permission={collaborator?.permission.value} />
|
||||
{org.count && (
|
||||
{org.total && (
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w="16px"
|
||||
@@ -374,77 +357,73 @@ function MemberModal({
|
||||
bgColor: 'myGray.200'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setParentPath(getOrgChildrenPath(org));
|
||||
onClickOrg(org);
|
||||
// setPath(getOrgChildrenPath(org));
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
{filterMembers?.map((member) => {
|
||||
const onChange = () => {
|
||||
setSelectedMembers((state) => {
|
||||
if (state.includes(member.tmbId)) {
|
||||
return state.filter((v) => v !== member.tmbId);
|
||||
}
|
||||
return [...state, member.tmbId];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
|
||||
const memberOrgs = orgs.filter((org) =>
|
||||
org.members.find((v) => String(v.tmbId) === String(member.tmbId))
|
||||
);
|
||||
const memberPathIds = memberOrgs.map((org) =>
|
||||
(org.path + '/' + org.pathId).split('/').slice(0)
|
||||
);
|
||||
const memberOrgNames = memberPathIds.map((pathIds) =>
|
||||
pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/')
|
||||
);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
name={member.memberName}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={selectedMemberIdList.includes(member.tmbId)}
|
||||
orgs={memberOrgNames}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollData>
|
||||
)}
|
||||
{filterGroups?.map((group) => {
|
||||
const onChange = () => {
|
||||
setSelectedGroupIdList((state) => {
|
||||
if (state.includes(group._id)) {
|
||||
return state.filter((v) => v !== group._id);
|
||||
}
|
||||
return [...state, group._id];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={group.avatar}
|
||||
key={group._id}
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={selectedGroupIdList.includes(group._id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return searchKey ? (
|
||||
Orgs
|
||||
) : (
|
||||
<OrgMemberScrollData>
|
||||
{Orgs}
|
||||
{orgMembers.map((member) => {
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
name={member.memberName}
|
||||
onChange={() => {
|
||||
setSelectedMemberList((state) => {
|
||||
if (state.find((v) => v.tmbId === member.tmbId)) {
|
||||
return state.filter((v) => v.tmbId !== member.tmbId);
|
||||
}
|
||||
return [...state, member];
|
||||
});
|
||||
}}
|
||||
isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OrgMemberScrollData>
|
||||
);
|
||||
})()}
|
||||
{(filterClass === 'group' || searchKey) &&
|
||||
groups?.map((group) => {
|
||||
const onChange = () => {
|
||||
setSelectedGroupList((state) => {
|
||||
if (state.find((v) => v._id === group._id)) {
|
||||
return state.filter((v) => v._id !== group._id);
|
||||
}
|
||||
return [...state, group];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={group.avatar}
|
||||
key={group._id}
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex h={'100%'} p="4" flexDirection="column">
|
||||
<Box>
|
||||
{`${t('user:has_chosen')}: `}
|
||||
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
|
||||
{selectedMemberList.length + selectedGroupList.length + selectedOrgList.length}
|
||||
</Box>
|
||||
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{selectedList.map((item) => {
|
||||
@@ -455,20 +434,7 @@ function MemberModal({
|
||||
name={item.name ?? ''}
|
||||
onChange={item.onDelete}
|
||||
onDelete={item.onDelete}
|
||||
orgs={(() => {
|
||||
if (!item.id.startsWith('member-')) return [];
|
||||
const id = item.id.replace('member-', '');
|
||||
const memberOrgs = orgs.filter((org) =>
|
||||
org.members.find((v) => v.tmbId === id)
|
||||
);
|
||||
const memberPathIds = memberOrgs.map((org) =>
|
||||
(org.path + '/' + org.pathId).split('/').slice(0)
|
||||
);
|
||||
const memberOrgNames = memberPathIds.map((pathIds) =>
|
||||
pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/')
|
||||
);
|
||||
return memberOrgNames;
|
||||
})()}
|
||||
orgs={item?.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,16 +3,16 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
avatar: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
function MemberTag({ name, avatar }: Props) {
|
||||
return (
|
||||
<HStack>
|
||||
<Avatar src={avatar} w={['18px', '22px']} rounded="50%" />
|
||||
<Box maxW={'150px'} className={'textEllipsis'}>
|
||||
{name}
|
||||
{avatar && <Avatar src={avatar} w={['18px', '22px']} rounded="50%" />}
|
||||
<Box maxW={'45vw'} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{name || '-'}
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import React from 'react';
|
||||
|
||||
function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' | 'tag' }) {
|
||||
return (
|
||||
function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' | 'tag' }) {
|
||||
return orgs?.length ? (
|
||||
<MyTooltip
|
||||
label={
|
||||
<VStack gap="1" alignItems={'start'}>
|
||||
@@ -39,6 +39,10 @@ function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' |
|
||||
</Flex>
|
||||
)}
|
||||
</MyTooltip>
|
||||
) : (
|
||||
<Box fontSize="xs" fontWeight={400} w="full" color="myGray.400" whiteSpace={'nowrap'}>
|
||||
-
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
11
projects/app/src/global/aiproxy/type.d.ts
vendored
11
projects/app/src/global/aiproxy/type.d.ts
vendored
@@ -30,6 +30,13 @@ export type CreateChannelProps = {
|
||||
};
|
||||
|
||||
// Log
|
||||
export type ChannelLogUsageType = {
|
||||
cache_creation_tokens?: number;
|
||||
cached_tokens?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
};
|
||||
export type ChannelLogListItemType = {
|
||||
token_name: string;
|
||||
model: string;
|
||||
@@ -40,8 +47,8 @@ export type ChannelLogListItemType = {
|
||||
created_at: number;
|
||||
request_at: number;
|
||||
code: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
usage?: ChannelLogUsageType;
|
||||
endpoint: string;
|
||||
content?: string;
|
||||
retry_times?: number;
|
||||
};
|
||||
|
||||
@@ -48,15 +48,6 @@ export type InsertOneDatasetDataProps = PushDatasetDataChunkProps & {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
export type GetTrainingQueueProps = {
|
||||
vectorModel: string;
|
||||
agentModel: string;
|
||||
};
|
||||
export type GetTrainingQueueResponse = {
|
||||
vectorTrainingCount: number;
|
||||
agentTrainingCount: number;
|
||||
};
|
||||
|
||||
/* -------------- search ---------------- */
|
||||
export type SearchTestProps = {
|
||||
datasetId: string;
|
||||
|
||||
@@ -36,9 +36,9 @@ import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { getCQPrompt, getExtractJsonPrompt } from '@fastgpt/global/core/ai/prompt/agent';
|
||||
|
||||
export const AddModelButton = ({
|
||||
onCreate,
|
||||
@@ -677,7 +677,9 @@ export const ModelEditModal = ({
|
||||
<HStack spacing={1}>
|
||||
<Box>{t('account:model.custom_cq_prompt')}</Box>
|
||||
<QuestionTip
|
||||
label={t('account:model.custom_cq_prompt_tip', { prompt: Prompt_CQJson })}
|
||||
label={t('account:model.custom_cq_prompt_tip', {
|
||||
prompt: getCQPrompt()
|
||||
})}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
@@ -691,7 +693,7 @@ export const ModelEditModal = ({
|
||||
<Box>{t('account:model.custom_extract_prompt')}</Box>
|
||||
<QuestionTip
|
||||
label={t('account:model.custom_extract_prompt_tip', {
|
||||
prompt: Prompt_ExtractJson
|
||||
prompt: getExtractJsonPrompt()
|
||||
})}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { deleteChannel, getChannelList, putChannel, putChannelStatus } from '@/web/core/ai/channel';
|
||||
import {
|
||||
deleteChannel,
|
||||
getChannelList,
|
||||
getChannelProviders,
|
||||
putChannel,
|
||||
putChannelStatus
|
||||
} from '@/web/core/ai/channel';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
@@ -32,6 +38,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
|
||||
const EditChannelModal = dynamic(() => import('./EditChannelModal'), { ssr: false });
|
||||
const ModelTest = dynamic(() => import('./ModelTest'), { ssr: false });
|
||||
@@ -50,6 +57,10 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const { data: channelProviders = {} } = useRequest2(getChannelProviders, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const [editChannel, setEditChannel] = useState<ChannelInfoType>();
|
||||
|
||||
const { runAsync: updateChannel, loading: loadingUpdateChannel } = useRequest2(putChannel, {
|
||||
@@ -67,6 +78,9 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
}
|
||||
);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
const { runAsync: onDeleteChannel, loading: loadingDeleteChannel } = useRequest2(deleteChannel, {
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
@@ -111,7 +125,10 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{channelList.map((item) => {
|
||||
const providerData = aiproxyIdMap[item.type];
|
||||
const providerData = aiproxyIdMap[item.type] || {
|
||||
label: channelProviders[item.type]?.name || 'Invalid provider',
|
||||
provider: 'Other'
|
||||
};
|
||||
const provider = getModelProvider(providerData?.provider);
|
||||
|
||||
return (
|
||||
@@ -119,14 +136,10 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
<Td>{item.id}</Td>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>
|
||||
{providerData ? (
|
||||
<HStack>
|
||||
<MyIcon name={provider?.avatar as any} w={'1rem'} />
|
||||
<Box>{t(providerData?.label as any)}</Box>
|
||||
</HStack>
|
||||
) : (
|
||||
'Invalid provider'
|
||||
)}
|
||||
<HStack>
|
||||
<MyIcon name={provider?.avatar as any} w={'1rem'} />
|
||||
<Box>{t(providerData?.label as any)}</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<MyTag
|
||||
@@ -203,7 +216,14 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
type: 'danger',
|
||||
icon: 'delete',
|
||||
label: t('common:common.Delete'),
|
||||
onClick: () => onDeleteChannel(item.id)
|
||||
onClick: () =>
|
||||
openConfirm(
|
||||
() => onDeleteChannel(item.id),
|
||||
undefined,
|
||||
t('account_model:confirm_delete_channel', {
|
||||
name: item.name
|
||||
})
|
||||
)()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -229,6 +249,7 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
{!!modelTestData && (
|
||||
<ModelTest {...modelTestData} onClose={() => setTestModelData(undefined)} />
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { ChannelLogUsageType } from '@/global/aiproxy/type';
|
||||
|
||||
type LogDetailType = {
|
||||
id: number;
|
||||
@@ -42,10 +43,10 @@ type LogDetailType = {
|
||||
duration: number;
|
||||
request_at: string;
|
||||
code: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
usage?: ChannelLogUsageType;
|
||||
endpoint: string;
|
||||
|
||||
retry_times?: number;
|
||||
content?: string;
|
||||
request_body?: string;
|
||||
response_body?: string;
|
||||
@@ -159,8 +160,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
duration: durationSecond,
|
||||
request_at: formatTime2YMDHMS(item.request_at),
|
||||
code: item.code,
|
||||
prompt_tokens: item.prompt_tokens,
|
||||
completion_tokens: item.completion_tokens,
|
||||
usage: item.usage,
|
||||
request_id: item.request_id,
|
||||
endpoint: item.endpoint,
|
||||
content: item.content
|
||||
@@ -197,7 +197,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack flex={'0 0 200px'}>
|
||||
<HStack>
|
||||
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect<string>
|
||||
@@ -210,7 +210,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack flex={'0 0 200px'}>
|
||||
<HStack>
|
||||
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect<string>
|
||||
@@ -260,7 +260,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
<Td>{item.channelName}</Td>
|
||||
<Td>{item.model}</Td>
|
||||
<Td>
|
||||
{item.prompt_tokens} / {item.completion_tokens}
|
||||
{item.usage?.input_tokens} / {item.usage?.output_tokens}
|
||||
</Td>
|
||||
<Td color={item.duration > 10 ? 'red.600' : ''}>{item.duration.toFixed(2)}s</Td>
|
||||
<Td color={item.code === 200 ? 'green.600' : 'red.600'}>
|
||||
@@ -297,6 +297,7 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
|
||||
const { t } = useTranslation();
|
||||
const { data: detailData } = useRequest2(
|
||||
async () => {
|
||||
console.log(data);
|
||||
if (data.code === 200) return data;
|
||||
try {
|
||||
const res = await getLogDetail(data.id);
|
||||
@@ -363,7 +364,7 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
|
||||
<Title>RequestID</Title>
|
||||
<Container>{detailData?.request_id}</Container>
|
||||
</GridItem>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||
<GridItem display={'flex'} borderBottomWidth="1px">
|
||||
<Title>{t('account_model:channel_status')}</Title>
|
||||
<Container color={detailData.code === 200 ? 'green.600' : 'red.600'}>
|
||||
{detailData?.code}
|
||||
@@ -373,7 +374,7 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
|
||||
<Title>Endpoint</Title>
|
||||
<Container>{detailData?.endpoint}</Container>
|
||||
</GridItem>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||
<GridItem display={'flex'} borderBottomWidth="1px">
|
||||
<Title>{t('account_model:channel_name')}</Title>
|
||||
<Container>{detailData?.channelName}</Container>
|
||||
</GridItem>
|
||||
@@ -381,7 +382,7 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
|
||||
<Title>{t('account_model:request_at')}</Title>
|
||||
<Container>{detailData?.request_at}</Container>
|
||||
</GridItem>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||
<GridItem display={'flex'} borderBottomWidth="1px">
|
||||
<Title>{t('account_model:duration')}</Title>
|
||||
<Container>{detailData?.duration.toFixed(2)}s</Container>
|
||||
</GridItem>
|
||||
@@ -389,20 +390,26 @@ const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void
|
||||
<Title>{t('account_model:model')}</Title>
|
||||
<Container>{detailData?.model}</Container>
|
||||
</GridItem>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||
<GridItem display={'flex'} borderBottomWidth="1px">
|
||||
<Title flex={'0 0 150px'}>{t('account_model:model_tokens')}</Title>
|
||||
<Container>
|
||||
{detailData?.prompt_tokens} / {detailData?.completion_tokens}
|
||||
{detailData?.usage?.input_tokens} / {detailData?.usage?.output_tokens}
|
||||
</Container>
|
||||
</GridItem>
|
||||
{detailData?.retry_times !== undefined && (
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" colSpan={2}>
|
||||
<Title>{t('account_model:retry_times')}</Title>
|
||||
<Container>{detailData?.retry_times}</Container>
|
||||
</GridItem>
|
||||
)}
|
||||
{detailData?.content && (
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" colSpan={2}>
|
||||
<Title>Content</Title>
|
||||
<Container>{detailData?.content}</Container>
|
||||
</GridItem>
|
||||
)}
|
||||
{detailData?.request_body && (
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
||||
<GridItem display={'flex'} borderBottomWidth="1px" colSpan={2}>
|
||||
<Title>Request Body</Title>
|
||||
<Container userSelect={'all'}>{detailData?.request_body}</Container>
|
||||
</GridItem>
|
||||
|
||||
@@ -117,7 +117,7 @@ function EditModal({
|
||||
ml={4}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
placeholder={t('user:team.Team Name')}
|
||||
{...register('name', {
|
||||
required: t('common:common.Please Input Name')
|
||||
|
||||
@@ -2,25 +2,31 @@ import { Input, HStack, ModalBody, Button, ModalFooter } from '@chakra-ui/react'
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
|
||||
export type GroupFormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
|
||||
function GroupInfoModal({
|
||||
onClose,
|
||||
editGroup,
|
||||
onSuccess
|
||||
}: {
|
||||
onClose: () => void;
|
||||
editGroup?: MemberGroupListItemType<true>;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
File: AvatarSelect,
|
||||
onOpen: onOpenSelectAvatar,
|
||||
@@ -30,14 +36,10 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
|
||||
defaultValues: {
|
||||
name: group?.name || '',
|
||||
avatar: group?.avatar || DEFAULT_TEAM_AVATAR
|
||||
name: editGroup?.name || '',
|
||||
avatar: editGroup?.avatar || DEFAULT_TEAM_AVATAR
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,21 +65,21 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
onSuccess: () => Promise.all([onClose(), onSuccess()])
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async (data: GroupFormType) => {
|
||||
if (!editGroupId) return;
|
||||
if (!editGroup) return;
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
groupId: editGroup._id,
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
onSuccess: () => Promise.all([onClose(), onSuccess()])
|
||||
}
|
||||
);
|
||||
|
||||
@@ -86,8 +88,8 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
return (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
title={editGroup ? t('user:team.group.edit') : t('user:team.group.create')}
|
||||
iconSrc={editGroup?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
>
|
||||
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
|
||||
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
|
||||
@@ -109,14 +111,14 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (editGroupId) {
|
||||
if (editGroup) {
|
||||
onUpdate(data);
|
||||
} else {
|
||||
onCreate(data);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{editGroupId ? t('common:common.Save') : t('common:new_create')}
|
||||
{editGroup ? t('common:common.Save') : t('common:new_create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<AvatarSelect onSelect={onSelectAvatar} />
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import {
|
||||
Box,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Checkbox,
|
||||
Grid,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, ModalBody, Flex, Button, ModalFooter, Grid, HStack } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import _ from 'lodash';
|
||||
import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
|
||||
|
||||
export type GroupFormType = {
|
||||
members: {
|
||||
@@ -35,63 +31,100 @@ export type GroupFormType = {
|
||||
// 1. Owner can not be deleted, toast
|
||||
// 2. Owner/Admin can manage members
|
||||
// 3. Owner can add/remove admins
|
||||
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
function GroupEditModal({
|
||||
onClose,
|
||||
group,
|
||||
onSuccess
|
||||
}: {
|
||||
onClose: () => void;
|
||||
group: MemberGroupListItemType<true>;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const groups = useContextSelector(TeamContext, (v) => v.groups);
|
||||
const refetchGroups = useContextSelector(TeamContext, (v) => v.refetchGroups);
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const allMembers = useContextSelector(TeamContext, (v) => v.members);
|
||||
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
|
||||
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
|
||||
|
||||
const selectedMembersRef = useRef<HTMLDivElement>(null);
|
||||
const [members, setMembers] = useState(group?.members || []);
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
const [selected, setSelected] = useState<
|
||||
{ name: string; tmbId: string; avatar: string; role: `${GroupMemberRole}` }[]
|
||||
>([]);
|
||||
|
||||
const {
|
||||
data: allMembers = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList
|
||||
} = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
status: 'active',
|
||||
withOrgs: true,
|
||||
searchKey
|
||||
},
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
const groupId = useMemo(() => String(group._id), [group._id]);
|
||||
|
||||
const { data: groupMembers = [], ScrollData: GroupScrollData } = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<
|
||||
TeamMemberItemType<{ withOrgs: true; withPermission: true; withGroupRole: true }>
|
||||
>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 100000,
|
||||
params: {
|
||||
groupId: groupId
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupId) return;
|
||||
setSelected(
|
||||
groupMembers.map((item) => ({
|
||||
name: item.memberName,
|
||||
tmbId: item.tmbId,
|
||||
avatar: item.avatar,
|
||||
role: (item.groupRole ?? 'member') as `${GroupMemberRole}`
|
||||
}))
|
||||
);
|
||||
}, [groupId, groupMembers]);
|
||||
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
|
||||
|
||||
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async () => {
|
||||
if (!editGroupId || !members.length) return;
|
||||
if (!group._id || !groupMembers.length) return;
|
||||
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
memberList: members
|
||||
groupId: group._id,
|
||||
memberList: selected
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
onSuccess: () => Promise.all([onClose(), onSuccess()])
|
||||
}
|
||||
);
|
||||
|
||||
const isSelected = (memberId: string) => {
|
||||
return members.find((item) => item.tmbId === memberId);
|
||||
return selected.find((item) => item.tmbId === memberId);
|
||||
};
|
||||
|
||||
const myRole = useMemo(() => {
|
||||
if (userInfo?.team.permission.hasManagePer) {
|
||||
return 'owner';
|
||||
}
|
||||
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member';
|
||||
}, [members, userInfo]);
|
||||
return groupMembers.find((item) => item.tmbId === userInfo?.team.tmbId)?.groupRole ?? 'member';
|
||||
}, [groupMembers, userInfo]);
|
||||
|
||||
const handleToggleSelect = (memberId: string) => {
|
||||
if (
|
||||
myRole === 'owner' &&
|
||||
memberId === group?.members.find((item) => item.role === 'owner')?.tmbId
|
||||
memberId === groupMembers.find((item) => item.role === 'owner')?.tmbId
|
||||
) {
|
||||
toast({
|
||||
title: t('user:team.group.toast.can_not_delete_owner'),
|
||||
@@ -102,28 +135,38 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
|
||||
if (
|
||||
myRole === 'admin' &&
|
||||
group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
selected.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected(memberId)) {
|
||||
setMembers(members.filter((item) => item.tmbId !== memberId));
|
||||
setSelected(selected.filter((item) => item.tmbId !== memberId));
|
||||
} else {
|
||||
setMembers([...members, { tmbId: memberId, role: 'member' }]);
|
||||
const member = allMembers.find((m) => m.tmbId === memberId);
|
||||
if (!member) return;
|
||||
setSelected([
|
||||
...selected,
|
||||
{
|
||||
name: member.memberName,
|
||||
avatar: member.avatar,
|
||||
tmbId: member.tmbId,
|
||||
role: 'member'
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAdmin = (memberId: string) => {
|
||||
if (myRole === 'owner' && isSelected(memberId)) {
|
||||
const oldRole = members.find((item) => item.tmbId === memberId)?.role;
|
||||
const oldRole = groupMembers.find((item) => item.tmbId === memberId)?.groupRole;
|
||||
if (oldRole === 'admin') {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
setSelected(
|
||||
selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
);
|
||||
} else {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
setSelected(
|
||||
selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -158,37 +201,24 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
}}
|
||||
/>
|
||||
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
|
||||
{filtered.map((member) => {
|
||||
{allMembers.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
name={member.memberName}
|
||||
onChange={() => handleToggleSelect(member.tmbId)}
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
|
||||
<MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}>
|
||||
{members.map((member) => {
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + selected.length}</Box>
|
||||
<GroupScrollData mt={3} flex={'1 0 0'} h={0}>
|
||||
{selected.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
|
||||
@@ -202,14 +232,8 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
|
||||
w="1.5rem"
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Box>
|
||||
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
|
||||
</Box>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
|
||||
<Box>{member.name}</Box>
|
||||
</HStack>
|
||||
<Box mr="auto">
|
||||
{(() => {
|
||||
@@ -264,7 +288,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</GroupScrollData>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { putGroupChangeOwner } from '@/web/support/user/team/group/api';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -15,34 +15,46 @@ 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 React, { useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TeamContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
export type ChangeOwnerModalProps = {
|
||||
groupId: string;
|
||||
};
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
|
||||
import { Omit } from '@fastgpt/web/components/common/DndDrag';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function ChangeOwnerModal({
|
||||
onClose,
|
||||
groupId
|
||||
}: ChangeOwnerModalProps & { onClose: () => void }) {
|
||||
group,
|
||||
onSuccess,
|
||||
onClose
|
||||
}: {
|
||||
group: MemberGroupListItemType<true>;
|
||||
onSuccess: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const { members: allMembers, groups, refetchGroups } = useContextSelector(TeamContext, (v) => v);
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === groupId);
|
||||
}, [groupId, groups]);
|
||||
|
||||
const memberList = allMembers.filter((item) => {
|
||||
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
|
||||
});
|
||||
const [searchKey, setSearchKey] = React.useState('');
|
||||
|
||||
const OldOwnerId = useMemo(() => {
|
||||
return group?.members.find((item) => item.role === 'owner')?.tmbId;
|
||||
}, [group]);
|
||||
|
||||
const [keepAdmin, setKeepAdmin] = useState(true);
|
||||
const {
|
||||
data: members = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList
|
||||
} = useScrollPagination<any, PaginationResponse<TeamMemberItemType<{ withGroupRole: true }>>>(
|
||||
getTeamMembers,
|
||||
{
|
||||
pageSize: 20,
|
||||
params: {
|
||||
searchKey
|
||||
},
|
||||
refreshDeps: [searchKey],
|
||||
debounceWait: 200,
|
||||
throttleWait: 500
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
@@ -50,44 +62,27 @@ export function ChangeOwnerModal({
|
||||
onOpen: onOpenMemberListMenu
|
||||
} = useDisclosure();
|
||||
|
||||
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
|
||||
const [selectedMember, setSelectedMember] = useState<Omit<
|
||||
TeamMemberItemType,
|
||||
'permission' | 'teamId'
|
||||
> | null>(null);
|
||||
|
||||
const onChangeOwner = async (tmbId: string) => {
|
||||
if (!group) {
|
||||
return;
|
||||
const [keepAdmin, setKeepAdmin] = useState(true);
|
||||
|
||||
const { runAsync: onTransfer, loading } = useRequest2(
|
||||
(tmbId: string) => putGroupChangeOwner(group._id, tmbId),
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), onSuccess()]),
|
||||
successToast: t('common:permission.change_owner_success'),
|
||||
errorToast: t('common:permission.change_owner_failed')
|
||||
}
|
||||
|
||||
const newMemberList = group.members
|
||||
.map((item) => {
|
||||
if (item.tmbId === OldOwnerId) {
|
||||
if (keepAdmin) {
|
||||
return { tmbId: OldOwnerId, role: 'admin' };
|
||||
}
|
||||
return { tmbId: OldOwnerId, role: 'member' };
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.tmbId !== tmbId) as any;
|
||||
|
||||
newMemberList.push({ tmbId, role: 'owner' });
|
||||
|
||||
return putUpdateGroup({
|
||||
groupId,
|
||||
memberList: newMemberList
|
||||
});
|
||||
};
|
||||
|
||||
const { runAsync, loading } = useRequest2(onChangeOwner, {
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups()]),
|
||||
successToast: t('common:permission.change_owner_success'),
|
||||
errorToast: t('common:permission.change_owner_failed')
|
||||
});
|
||||
);
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!selectedMember) {
|
||||
return;
|
||||
}
|
||||
await runAsync(selectedMember.tmbId);
|
||||
await onTransfer(selectedMember.tmbId);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -97,7 +92,6 @@ export function ChangeOwnerModal({
|
||||
iconColor="primary.600"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.change_owner')}
|
||||
isLoading={loading}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
@@ -120,9 +114,9 @@ export function ChangeOwnerModal({
|
||||
)}
|
||||
<Input
|
||||
placeholder={t('common:permission.change_owner_placeholder')}
|
||||
value={inputValue}
|
||||
value={searchKey}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSearchKey(e.target.value);
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
@@ -132,7 +126,7 @@ export function ChangeOwnerModal({
|
||||
{...(selectedMember && { pl: '10' })}
|
||||
/>
|
||||
</Flex>
|
||||
{isOpenMemberListMenu && memberList.length > 0 && (
|
||||
{isOpenMemberListMenu && members.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
@@ -146,26 +140,28 @@ export function ChangeOwnerModal({
|
||||
maxH={'300px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{memberList.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setInputValue(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<MemberScrollData>
|
||||
{members.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setSearchKey(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -186,7 +182,9 @@ export function ChangeOwnerModal({
|
||||
<Button onClick={onClose} variant={'whiteBase'}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
|
||||
<Button isLoading={loading} isDisabled={!selectedMember} onClick={onConfirm}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
@@ -16,20 +15,18 @@ import {
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { deleteGroup } from '@/web/support/user/team/group/api';
|
||||
import { deleteGroup, getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useState } from 'react';
|
||||
import IconButton from '../OrgManage/IconButton';
|
||||
import { MemberGroupType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
|
||||
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
|
||||
const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
|
||||
@@ -39,19 +36,23 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
|
||||
TeamContext,
|
||||
(v) => v
|
||||
);
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: isLoadingGroups,
|
||||
refresh: refetchGroups
|
||||
} = useRequest2(() => getGroupList<true>({ withMembers: true }), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const [editGroup, setEditGroup] = useState<MemberGroupListItemType<true>>();
|
||||
|
||||
const [editGroup, setEditGroup] = useState<MemberGroupType>();
|
||||
const {
|
||||
isOpen: isOpenGroupInfo,
|
||||
onOpen: onOpenGroupInfo,
|
||||
onClose: onCloseGroupInfo
|
||||
} = useDisclosure();
|
||||
|
||||
const onEditGroupInfo = (e: MemberGroupType) => {
|
||||
const onEditGroupInfo = (e: MemberGroupListItemType<true>) => {
|
||||
setEditGroup(e);
|
||||
onOpenGroupInfo();
|
||||
};
|
||||
@@ -60,11 +61,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
type: 'delete',
|
||||
content: t('account_team:confirm_delete_group')
|
||||
});
|
||||
|
||||
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
|
||||
onSuccess: () => {
|
||||
refetchGroups();
|
||||
refetchMembers();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,26 +72,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
onOpen: onOpenManageGroupMember,
|
||||
onClose: onCloseManageGroupMember
|
||||
} = useDisclosure();
|
||||
const onManageMember = (e: MemberGroupType) => {
|
||||
const onManageMember = (e: MemberGroupListItemType<true>) => {
|
||||
setEditGroup(e);
|
||||
onOpenManageGroupMember();
|
||||
};
|
||||
|
||||
const hasGroupManagePer = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
['admin', 'owner'].includes(
|
||||
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
|
||||
);
|
||||
const isGroupOwner = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
|
||||
|
||||
const {
|
||||
isOpen: isOpenChangeOwner,
|
||||
onOpen: onOpenChangeOwner,
|
||||
onClose: onCloseChangeOwner
|
||||
} = useDisclosure();
|
||||
const onChangeOwner = (e: MemberGroupType) => {
|
||||
const onChangeOwner = (e: MemberGroupListItemType<true>) => {
|
||||
setEditGroup(e);
|
||||
onOpenChangeOwner();
|
||||
};
|
||||
@@ -115,7 +105,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<MyBox flex={'1 0 0'} overflow={'auto'}>
|
||||
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingGroups}>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'}>
|
||||
<Table overflow={'unset'}>
|
||||
<Thead>
|
||||
@@ -133,66 +123,38 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<Tbody>
|
||||
{groups?.map((group) => (
|
||||
<Tr key={group._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName
|
||||
? userInfo?.team.teamName ?? ''
|
||||
: group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
<Box>
|
||||
({group.name === DefaultGroupName ? members.length : group.members.length})
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName
|
||||
? members.find((item) => item.role === 'owner')?.memberName ?? ''
|
||||
: members.find(
|
||||
(item) =>
|
||||
item.tmbId ===
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId
|
||||
)?.memberName ?? ''
|
||||
}
|
||||
avatar={
|
||||
group.name === DefaultGroupName
|
||||
? members.find((item) => item.role === 'owner')?.avatar ?? ''
|
||||
: members.find(
|
||||
(i) =>
|
||||
i.tmbId ===
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId
|
||||
)?.avatar ?? ''
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{group.name === DefaultGroupName ? (
|
||||
<AvatarGroup avatars={members.map((v) => v.avatar)} />
|
||||
) : hasGroupManagePer(group) ? (
|
||||
<MyTooltip label={t('account_team:manage_member')}>
|
||||
<Box cursor="pointer" onClick={() => onManageMember(group)}>
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
) : (
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<MemberTag name={group.owner?.name} avatar={group.owner?.avatar} />
|
||||
</Td>
|
||||
<Td>
|
||||
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
|
||||
<MyTooltip
|
||||
label={group.permission?.hasManagePer ? t('account_team:manage_member') : ''}
|
||||
>
|
||||
<Box
|
||||
{...(group.permission?.hasManagePer
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
onClick: () => onManageMember(group)
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<AvatarGroup
|
||||
avatars={group?.members.map((v) => v.avatar)}
|
||||
total={group.count}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
{group.permission?.hasManagePer && (
|
||||
<MyMenu
|
||||
Button={<IconButton name={'more'} />}
|
||||
menuList={[
|
||||
@@ -212,7 +174,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
onManageMember(group);
|
||||
}
|
||||
},
|
||||
...(isGroupOwner(group)
|
||||
...(group.permission?.isOwner
|
||||
? [
|
||||
{
|
||||
label: t('account_team:transfer_ownership'),
|
||||
@@ -246,25 +208,33 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</MyBox>
|
||||
|
||||
<ConfirmDeleteGroupModal />
|
||||
{isOpenChangeOwner && editGroup && (
|
||||
<ChangeOwnerModal groupId={editGroup._id} onClose={onCloseChangeOwner} />
|
||||
)}
|
||||
|
||||
{isOpenGroupInfo && (
|
||||
<GroupInfoModal
|
||||
editGroup={editGroup}
|
||||
onSuccess={refetchGroups}
|
||||
onClose={() => {
|
||||
onCloseGroupInfo();
|
||||
setEditGroup(undefined);
|
||||
}}
|
||||
editGroupId={editGroup?._id}
|
||||
/>
|
||||
)}
|
||||
{isOpenChangeOwner && editGroup && (
|
||||
<ChangeOwnerModal
|
||||
group={editGroup}
|
||||
onClose={onCloseChangeOwner}
|
||||
onSuccess={refetchGroups}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOpenManageGroupMember && editGroup && (
|
||||
<GroupManageMember
|
||||
group={editGroup}
|
||||
onClose={() => {
|
||||
onCloseManageGroupMember();
|
||||
setEditGroup(undefined);
|
||||
}}
|
||||
editGroupId={editGroup._id}
|
||||
onSuccess={refetchGroups}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,13 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
function CreateInvitationModal({ onClose }: { onClose: () => void }) {
|
||||
function CreateInvitationModal({
|
||||
onSuccess,
|
||||
onClose
|
||||
}: {
|
||||
onSuccess: (linkId: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const expiresOptions: Array<{ label: string; value: InvitationLinkExpiresType }> = [
|
||||
{ label: t('account_team:30mins'), value: '30m' }, // 30 mins
|
||||
@@ -43,9 +49,11 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
|
||||
|
||||
const { runAsync: createInvitationLink, loading } = useRequest2(postCreateInvitationLink, {
|
||||
manual: true,
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onFinally: () => onClose()
|
||||
onSuccess: (data) => {
|
||||
onSuccess(data);
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -55,7 +63,7 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
|
||||
iconColor="primary.500"
|
||||
title={<Box>{t('account_team:create_invitation_link')}</Box>}
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<ModalCloseButton onClick={() => onClose()} />
|
||||
<ModalBody>
|
||||
<Grid gap={6} templateColumns="max-content 1fr" alignItems="center">
|
||||
<>
|
||||
@@ -91,7 +99,7 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button isLoading={loading} onClick={onClose} variant="outline">
|
||||
<Button isLoading={loading} onClick={() => onClose()} variant="outline">
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button isLoading={loading} onClick={handleSubmit(createInvitationLink)} ml="4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { getInvitationLinkList, putForbidInvitationLink } from '@/web/support/user/team/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import {
|
||||
Box,
|
||||
@@ -35,15 +35,7 @@ import { useCallback } from 'react';
|
||||
|
||||
const CreateInvitationModal = dynamic(() => import('./CreateInvitationModal'));
|
||||
|
||||
const InviteModal = ({
|
||||
teamId,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
teamId: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const InviteModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
@@ -57,10 +49,10 @@ const InviteModal = ({
|
||||
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}`;
|
||||
@@ -76,21 +68,14 @@ const InviteModal = ({
|
||||
})
|
||||
);
|
||||
},
|
||||
[copyData]
|
||||
[copyData, feConfigs?.systemTitle, t, userInfo?.team.memberName, userInfo?.team.teamName]
|
||||
);
|
||||
|
||||
const { runAsync: onForbid, loading: forbiding } = useRequest2(
|
||||
(linkId: string) =>
|
||||
putUpdateInvitationInfo({
|
||||
linkId,
|
||||
forbidden: true
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: refetchInvitationLinkList,
|
||||
successToast: t('account_team:forbid_success')
|
||||
}
|
||||
);
|
||||
const { runAsync: onForbid, loading: forbiding } = useRequest2(putForbidInvitationLink, {
|
||||
manual: true,
|
||||
onSuccess: refetchInvitationLinkList,
|
||||
successToast: t('account_team:forbid_success')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -134,17 +119,11 @@ const InviteModal = ({
|
||||
{invitationLinkList?.map((item) => {
|
||||
const isForbidden = item.forbidden || new Date(item.expires) < new Date();
|
||||
return (
|
||||
<Tr key={item._id} overflow={'unset'}>
|
||||
<Tr key={item.linkId} 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>{format(new Date(item.expires), 'yyyy-MM-dd HH:mm')}</Td>
|
||||
<Td>
|
||||
{item.usedTimesLimit === -1
|
||||
? t('account_team:unlimited')
|
||||
@@ -160,7 +139,6 @@ const InviteModal = ({
|
||||
cursor="pointer"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
p="1.5"
|
||||
w="fit-content"
|
||||
>
|
||||
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
|
||||
</Box>
|
||||
@@ -169,7 +147,7 @@ const InviteModal = ({
|
||||
closeOnBlur={true}
|
||||
>
|
||||
{() => (
|
||||
<Box py="4" maxH="200px" w="fit-content">
|
||||
<Box py="4" maxH="200px">
|
||||
<Flex mx="4" justifyContent="center" alignItems={'center'}>
|
||||
<Box>{t('account_team:has_invited')}</Box>
|
||||
<Box
|
||||
@@ -182,15 +160,16 @@ const InviteModal = ({
|
||||
{item.members.length}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider my="2" mx="4" />
|
||||
<Divider my="2" />
|
||||
<Grid
|
||||
w="fit-content"
|
||||
mt="2"
|
||||
gridRowGap="4"
|
||||
mt="4"
|
||||
gap={4}
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
overflow="auto"
|
||||
alignItems="center"
|
||||
mx="4"
|
||||
maxH={'250px'}
|
||||
>
|
||||
{item.members.map((member) => (
|
||||
<Box key={member.tmbId} justifySelf="start">
|
||||
@@ -204,12 +183,14 @@ const InviteModal = ({
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{!isForbidden && (
|
||||
{isForbidden ? (
|
||||
<Tag colorSchema="red">{t('account_team:has_forbidden')}</Tag>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onCopy(item._id)}
|
||||
onClick={() => onCopy(item.linkId)}
|
||||
color="myGray.900"
|
||||
>
|
||||
<Icon name="common/link" w="16px" mr="1" />
|
||||
@@ -239,7 +220,7 @@ const InviteModal = ({
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
onClick={() => {
|
||||
onForbid(item._id);
|
||||
onForbid(item.linkId);
|
||||
onClosePopover();
|
||||
}}
|
||||
>
|
||||
@@ -268,7 +249,11 @@ const InviteModal = ({
|
||||
</ModalFooter>
|
||||
{isOpenCreate && (
|
||||
<CreateInvitationModal
|
||||
onClose={() => Promise.all([onCloseCreate(), refetchInvitationLinkList()])}
|
||||
onSuccess={(linkId) => {
|
||||
refetchInvitationLinkList();
|
||||
onCopy(linkId);
|
||||
}}
|
||||
onClose={onCloseCreate}
|
||||
/>
|
||||
)}
|
||||
</MyModal>
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
|
||||
import {
|
||||
delRemoveMember,
|
||||
postRestoreMember,
|
||||
putUpdateMemberNameByManager
|
||||
getTeamMembers,
|
||||
putUpdateMemberNameByManager,
|
||||
postRestoreMember
|
||||
} from '@/web/support/user/team/api';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
@@ -30,12 +30,9 @@ import { TeamContext } from './context';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { delLeaveTeam } from '@/web/support/user/team/api';
|
||||
import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api';
|
||||
import MyLoading from '@fastgpt/web/components/common/MyLoading';
|
||||
import {
|
||||
TeamMemberRoleEnum,
|
||||
TeamMemberStatusEnum
|
||||
@@ -43,9 +40,16 @@ import {
|
||||
import format from 'date-fns/format';
|
||||
import OrgTags from '@/components/support/user/team/OrgTags';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import _ from 'lodash';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
|
||||
const InviteModal = dynamic(() => import('./Invite/InviteModal'));
|
||||
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
|
||||
@@ -53,30 +57,86 @@ const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTa
|
||||
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { userInfo, teamPlanStatus } = useUserStore();
|
||||
const { feConfigs, setNotSufficientModalType } = useSystemStore();
|
||||
|
||||
const {
|
||||
refetchGroups,
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
members,
|
||||
refetchMembers,
|
||||
onSwitchTeam,
|
||||
MemberScrollData,
|
||||
orgs
|
||||
} = useContextSelector(TeamContext, (v) => v);
|
||||
const statusOptions = [
|
||||
{
|
||||
label: t('common:common.All'),
|
||||
value: undefined
|
||||
},
|
||||
{
|
||||
label: t('common:user.team.member.active'),
|
||||
value: 'active'
|
||||
},
|
||||
{
|
||||
label: t('account_team:leave'),
|
||||
value: 'inactive'
|
||||
}
|
||||
];
|
||||
const { userInfo } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const isSyncMember = feConfigs?.register_method?.includes('sync');
|
||||
|
||||
const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
|
||||
const [status, setStatus] = useState<string>();
|
||||
|
||||
const {
|
||||
isOpen: isOpenTeamTagsAsync,
|
||||
onOpen: onOpenTeamTagsAsync,
|
||||
onClose: onCloseTeamTagsAsync
|
||||
} = useDisclosure();
|
||||
|
||||
// member action
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
const {
|
||||
data: members = [],
|
||||
isLoading: loadingMembers,
|
||||
refreshList: refetchMemberList,
|
||||
ScrollData: MemberScrollData
|
||||
} = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
status,
|
||||
withPermission: true,
|
||||
withOrgs: true,
|
||||
searchKey
|
||||
},
|
||||
refreshDeps: [searchKey, status],
|
||||
throttleWait: 500,
|
||||
debounceWait: 200
|
||||
});
|
||||
|
||||
const onRefreshMembers = useCallback(() => {
|
||||
refetchMemberList();
|
||||
}, [refetchMemberList]);
|
||||
|
||||
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
|
||||
|
||||
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
|
||||
onSuccess: onRefreshMembers,
|
||||
successToast: t('account_team:sync_member_success'),
|
||||
errorToast: t('account_team:sync_member_failed')
|
||||
});
|
||||
|
||||
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
|
||||
content: t('account_team:confirm_leave_team')
|
||||
});
|
||||
const { runAsync: onLeaveTeam } = useRequest2(delLeaveTeam, {
|
||||
onSuccess() {
|
||||
const defaultTeam = myTeams[0];
|
||||
onSwitchTeam(defaultTeam.teamId);
|
||||
},
|
||||
errorToast: t('account_team:user_team_leave_team_failed')
|
||||
});
|
||||
|
||||
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
const { runAsync: onRemoveMember } = useRequest2(delRemoveMember, {
|
||||
onSuccess: onRefreshMembers
|
||||
});
|
||||
|
||||
const { ConfirmModal: ConfirmRestoreMemberModal, openConfirm: openRestoreMember } = useConfirm({
|
||||
type: 'common',
|
||||
@@ -84,69 +144,25 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
iconSrc: 'common/confirm/restoreTip',
|
||||
iconColor: 'primary.500'
|
||||
});
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const isSyncMember = feConfigs.register_method?.includes('sync');
|
||||
|
||||
const { data: searchMembersData } = useRequest2(
|
||||
() => GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false }),
|
||||
{
|
||||
manual: false,
|
||||
throttleWait: 500,
|
||||
refreshDeps: [searchText]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onLeaveTeam } = useRequest2(
|
||||
async () => {
|
||||
const defaultTeam = myTeams[0];
|
||||
// change to personal team
|
||||
onSwitchTeam(defaultTeam.teamId);
|
||||
return delLeaveTeam();
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
refetchTeams();
|
||||
refetchMembers();
|
||||
},
|
||||
errorToast: t('account_team:user_team_leave_team_failed')
|
||||
}
|
||||
);
|
||||
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
|
||||
content: t('account_team:confirm_leave_team')
|
||||
});
|
||||
|
||||
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
|
||||
onSuccess() {
|
||||
refetchMembers();
|
||||
},
|
||||
successToast: t('account_team:sync_member_success'),
|
||||
errorToast: t('account_team:sync_member_failed')
|
||||
});
|
||||
|
||||
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(postRestoreMember, {
|
||||
onSuccess() {
|
||||
refetchMembers();
|
||||
},
|
||||
successToast: t('common:user.team.invite.Accepted'),
|
||||
const { runAsync: onRestore } = useRequest2(postRestoreMember, {
|
||||
onSuccess: onRefreshMembers,
|
||||
successToast: t('common:common.Success'),
|
||||
errorToast: t('common:user.team.invite.Reject')
|
||||
});
|
||||
|
||||
const isLoading = isUpdateInvite || isSyncing;
|
||||
const isLoading = loadingMembers || isSyncing;
|
||||
|
||||
const { EditModal: EditMemberNameModal, onOpenModal: openEditMemberName } = useEditTextarea({
|
||||
const { EditModal: EditMemberNameModal, onOpenModal: openEditMemberName } = useEditTitle({
|
||||
title: t('account_team:edit_member'),
|
||||
tip: t('account_team:edit_member_tip'),
|
||||
canEmpty: false,
|
||||
rows: 1
|
||||
canEmpty: false
|
||||
});
|
||||
|
||||
const handleEditMemberName = (tmbId: string, memberName: string) => {
|
||||
openEditMemberName({
|
||||
defaultVal: memberName,
|
||||
onSuccess: (newName: string) => {
|
||||
return putUpdateMemberNameByManager(tmbId, newName).then(() => {
|
||||
Promise.all([refetchGroups(), refetchMembers()]);
|
||||
onRefreshMembers();
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -160,14 +176,16 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <MyLoading />}
|
||||
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
|
||||
{Tabs}
|
||||
<HStack alignItems={'center'}>
|
||||
<Box>
|
||||
<MySelect list={statusOptions} value={status} onChange={(v) => setStatus(v)} />
|
||||
</Box>
|
||||
<Box width={'200px'}>
|
||||
<SearchInput
|
||||
placeholder={t('account_team:search_member')}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
|
||||
@@ -242,7 +260,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
<Box flex={'1 0 0'} overflow={'auto'}>
|
||||
<MyBox isLoading={isLoading} flex={'1 0 0'} overflow={'auto'}>
|
||||
<MemberScrollData>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'}>
|
||||
<Table overflow={'unset'}>
|
||||
@@ -262,119 +280,104 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{(searchText && searchMembersData ? searchMembersData.members : members).map(
|
||||
(member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{member.memberName}
|
||||
{member.status !== 'active' && (
|
||||
<Tag ml="2" colorSchema="gray">
|
||||
{t('account_team:leave')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td maxW={'300px'}>{member.contact || '-'}</Td>
|
||||
<Td maxWidth="300px">
|
||||
{(() => {
|
||||
const memberOrgs = orgs.filter((org) =>
|
||||
org.members.find((v) => String(v.tmbId) === String(member.tmbId))
|
||||
);
|
||||
const memberPathIds = memberOrgs.map((org) =>
|
||||
(org.path + '/' + org.pathId).split('/').slice(0)
|
||||
);
|
||||
const memberOrgNames = memberPathIds.map((pathIds) =>
|
||||
pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/')
|
||||
);
|
||||
return <OrgTags orgs={memberOrgNames} type="tag" />;
|
||||
})()}
|
||||
</Td>
|
||||
<Td maxW={'300px'}>
|
||||
<VStack gap={0} alignItems="flex-start">
|
||||
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
|
||||
<Box>
|
||||
{member.updateTime
|
||||
? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss')
|
||||
: '-'}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
member.role !== TeamMemberRoleEnum.owner &&
|
||||
member.tmbId !== userInfo?.team.tmbId &&
|
||||
(member.status === TeamMemberStatusEnum.active ? (
|
||||
<>
|
||||
<Icon
|
||||
name={'edit'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'blue.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() =>
|
||||
handleEditMemberName(member.tmbId, member.memberName)
|
||||
}
|
||||
/>
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() =>
|
||||
delRemoveMember(member.tmbId).then(() =>
|
||||
Promise.all([refetchGroups(), refetchMembers()])
|
||||
),
|
||||
undefined,
|
||||
t('account_team:remove_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>
|
||||
)
|
||||
)}
|
||||
{members.map((member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{member.memberName}
|
||||
{member.status !== 'active' && (
|
||||
<Tag ml="2" colorSchema="gray" bg={'myGray.100'} color={'myGray.700'}>
|
||||
{t('account_team:leave')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td maxW={'300px'}>{member.contact || '-'}</Td>
|
||||
<Td maxWidth="300px">
|
||||
{(() => {
|
||||
return <OrgTags orgs={member.orgs || undefined} type="tag" />;
|
||||
})()}
|
||||
</Td>
|
||||
<Td maxW={'300px'}>
|
||||
<VStack gap={0}>
|
||||
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
|
||||
<Box>
|
||||
{member.updateTime
|
||||
? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss')
|
||||
: '-'}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
member.role !== TeamMemberRoleEnum.owner &&
|
||||
member.tmbId !== userInfo?.team.tmbId &&
|
||||
(member.status === TeamMemberStatusEnum.active ? (
|
||||
<>
|
||||
<Icon
|
||||
mr={2}
|
||||
name={'edit'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'blue.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => handleEditMemberName(member.tmbId, member.memberName)}
|
||||
/>
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() => onRemoveMember(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:remove_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>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<ConfirmRemoveMemberModal />
|
||||
@@ -382,16 +385,10 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<EditMemberNameModal />
|
||||
</TableContainer>
|
||||
</MemberScrollData>
|
||||
</Box>
|
||||
</MyBox>
|
||||
|
||||
<ConfirmLeaveTeamModal />
|
||||
{isOpenInvite && userInfo?.team?.teamId && (
|
||||
<InviteModal
|
||||
teamId={userInfo.team.teamId}
|
||||
onClose={onCloseInvite}
|
||||
onSuccess={refetchMembers}
|
||||
/>
|
||||
)}
|
||||
{isOpenInvite && userInfo?.team?.teamId && <InviteModal onClose={onCloseInvite} />}
|
||||
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export type OrgFormType = {
|
||||
@@ -15,8 +14,7 @@ export type OrgFormType = {
|
||||
avatar: string;
|
||||
description?: string;
|
||||
name: string;
|
||||
path: string;
|
||||
parentId?: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export const defaultOrgForm: OrgFormType = {
|
||||
@@ -30,11 +28,15 @@ export const defaultOrgForm: OrgFormType = {
|
||||
function OrgInfoModal({
|
||||
editOrg,
|
||||
onClose,
|
||||
onSuccess
|
||||
onSuccess,
|
||||
updateCurrentOrg,
|
||||
parentId
|
||||
}: {
|
||||
editOrg: OrgFormType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void;
|
||||
parentId?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -51,11 +53,11 @@ function OrgInfoModal({
|
||||
|
||||
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
|
||||
async (data: OrgFormType) => {
|
||||
if (!editOrg.parentId) return;
|
||||
if (parentId === undefined) return;
|
||||
return postCreateOrg({
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
parentId: editOrg.parentId,
|
||||
orgId: parentId,
|
||||
description: data.description
|
||||
});
|
||||
},
|
||||
@@ -68,7 +70,7 @@ function OrgInfoModal({
|
||||
}
|
||||
);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async (data: OrgFormType) => {
|
||||
if (!editOrg._id) return;
|
||||
return putUpdateOrg({
|
||||
@@ -145,7 +147,9 @@ function OrgInfoModal({
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (isEdit) {
|
||||
onUpdate(data);
|
||||
onUpdate(data).then(() => {
|
||||
updateCurrentOrg(data);
|
||||
});
|
||||
} else {
|
||||
onCreate(data);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
|
||||
|
||||
export type GroupFormType = {
|
||||
members: {
|
||||
@@ -30,45 +20,61 @@ export type GroupFormType = {
|
||||
}[];
|
||||
};
|
||||
|
||||
function CheckboxIcon({
|
||||
name
|
||||
}: {
|
||||
isChecked?: boolean;
|
||||
isIndeterminate?: boolean;
|
||||
name: IconNameType;
|
||||
}) {
|
||||
return <MyIcon name={name} w="12px" />;
|
||||
}
|
||||
|
||||
function OrgMemberManageModal({
|
||||
currentOrg,
|
||||
refetchOrgs,
|
||||
onClose
|
||||
}: {
|
||||
currentOrg: OrgType;
|
||||
currentOrg: OrgListItemType;
|
||||
refetchOrgs: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const [selectedMembers, setSelectedMembers] = useState<string[]>(
|
||||
currentOrg.members.map((item) => item.tmbId)
|
||||
const { data: allMembers, ScrollData: MemberScrollData } = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
withOrgs: true,
|
||||
withPermission: false,
|
||||
status: 'active',
|
||||
searchKey
|
||||
},
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
const { data: orgMembers, ScrollData: OrgMemberScrollData } = useScrollPagination(
|
||||
getTeamMembers,
|
||||
{
|
||||
pageSize: 100000,
|
||||
params: {
|
||||
orgId: currentOrg._id,
|
||||
withOrgs: false,
|
||||
withPermission: false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const filterMembers = useMemo(() => {
|
||||
if (!searchKey) return allMembers;
|
||||
const regx = new RegExp(searchKey, 'i');
|
||||
return allMembers.filter((member) => regx.test(member.memberName));
|
||||
}, [searchKey, allMembers]);
|
||||
const [selected, setSelected] = useState<{ name: string; tmbId: string; avatar: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(
|
||||
orgMembers.map((item) => ({
|
||||
name: item.memberName,
|
||||
tmbId: item.tmbId,
|
||||
avatar: item.avatar
|
||||
}))
|
||||
);
|
||||
}, [orgMembers]);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
() => {
|
||||
return putUpdateOrgMembers({
|
||||
orgId: currentOrg._id,
|
||||
members: selectedMembers.map((tmbId) => ({
|
||||
tmbId
|
||||
members: selected.map((member) => ({
|
||||
tmbId: member.tmbId
|
||||
}))
|
||||
});
|
||||
},
|
||||
@@ -81,15 +87,25 @@ function OrgMemberManageModal({
|
||||
}
|
||||
);
|
||||
|
||||
const isSelected = (memberId: string) => {
|
||||
return selectedMembers.find((tmbId) => tmbId === memberId);
|
||||
const isSelected = (tmbId: string) => {
|
||||
return selected.find((tmb) => tmb.tmbId === tmbId);
|
||||
};
|
||||
|
||||
const handleToggleSelect = (memberId: string) => {
|
||||
if (isSelected(memberId)) {
|
||||
setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId));
|
||||
const handleToggleSelect = (tmbId: string) => {
|
||||
if (isSelected(tmbId)) {
|
||||
setSelected((state) => state.filter((tmb) => tmb.tmbId !== tmbId));
|
||||
// setSelectedTmbIds((state) => state.filter((tmbId) => tmbId !== memberId));
|
||||
} else {
|
||||
setSelectedMembers((state) => [...state, memberId]);
|
||||
// setSelectedTmbIds((state) => [...state, memberId]);
|
||||
const member = allMembers.find((item) => item.tmbId === tmbId)!;
|
||||
setSelected((state) => [
|
||||
...state,
|
||||
{
|
||||
name: member.memberName,
|
||||
tmbId,
|
||||
avatar: member.avatar
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,7 +128,14 @@ function OrgMemberManageModal({
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
p="4"
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
borderRight={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
>
|
||||
<SearchInput
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
@@ -122,64 +145,49 @@ function OrgMemberManageModal({
|
||||
}}
|
||||
/>
|
||||
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
|
||||
{filterMembers.map((member) => {
|
||||
{allMembers.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
icon={<CheckboxIcon name={'common/check'} />}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
name={member.memberName}
|
||||
onChange={() => handleToggleSelect(member.tmbId)}
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box>
|
||||
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{selectedMembers.map((tmbId) => {
|
||||
const member = allMembers.find((item) => item.tmbId === tmbId)!;
|
||||
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
|
||||
<OrgMemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
|
||||
<Box mt={2}>{`${t('common:chosen')}:${selected.length}`}</Box>
|
||||
{selected.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={tmbId}
|
||||
key={member.tmbId}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} />
|
||||
<Box>{member?.memberName}</Box>
|
||||
<Box>{member?.name}</Box>
|
||||
</HStack>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(tmbId)}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</OrgMemberScrollData>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { putMoveOrg } from '@/web/support/user/team/org/api';
|
||||
import { getOrgList, putMoveOrg } from '@/web/support/user/team/org/api';
|
||||
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import OrgTree from './OrgTree';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
|
||||
|
||||
function OrgMoveModal({
|
||||
movingOrg,
|
||||
orgs,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
movingOrg: OrgType;
|
||||
orgs: OrgType[];
|
||||
movingOrg: OrgListItemType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedOrg, setSelectedOrg] = useState<OrgType>();
|
||||
const { userInfo } = useUserStore();
|
||||
const team = userInfo?.team!;
|
||||
const [selectedOrg, setSelectedOrg] = useState<OrgListItemType>();
|
||||
|
||||
const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, {
|
||||
onSuccess: () => {
|
||||
@@ -32,11 +28,6 @@ function OrgMoveModal({
|
||||
}
|
||||
});
|
||||
|
||||
const filterMovingOrgs = useMemo(
|
||||
() => orgs.filter((org) => org._id !== movingOrg._id),
|
||||
[movingOrg._id, orgs]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
@@ -46,11 +37,7 @@ function OrgMoveModal({
|
||||
iconColor="primary.600"
|
||||
>
|
||||
<ModalBody>
|
||||
<OrgTree
|
||||
orgs={filterMovingOrgs}
|
||||
selectedOrg={selectedOrg}
|
||||
setSelectedOrg={setSelectedOrg}
|
||||
/>
|
||||
<OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} movingOrg={movingOrg} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
import { Box, HStack, VStack } from '@chakra-ui/react';
|
||||
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useToggle } from 'ahooks';
|
||||
import { useMemo } from 'react';
|
||||
import { useState } from 'react';
|
||||
import IconButton from './IconButton';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getOrgList } from '@/web/support/user/team/org/api';
|
||||
import { getChildrenByOrg } from '@fastgpt/service/support/permission/org/controllers';
|
||||
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
|
||||
|
||||
function OrgTreeNode({
|
||||
org,
|
||||
list,
|
||||
selectedOrg,
|
||||
setSelectedOrg,
|
||||
index = 0
|
||||
index = 0,
|
||||
movingOrg
|
||||
}: {
|
||||
org: OrgType;
|
||||
list: OrgType[];
|
||||
selectedOrg?: OrgType;
|
||||
setSelectedOrg: (org?: OrgType) => void;
|
||||
org: OrgListItemType;
|
||||
selectedOrg?: OrgListItemType;
|
||||
setSelectedOrg: (org?: OrgListItemType) => void;
|
||||
index?: number;
|
||||
movingOrg: OrgListItemType;
|
||||
}) {
|
||||
const children = useMemo(
|
||||
() => list.filter((item) => item.path === getOrgChildrenPath(org)),
|
||||
[org, list]
|
||||
);
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
|
||||
const [canBeExpanded, setCanBeExpanded] = useState(true);
|
||||
const { data: orgs = [], runAsync: getOrgs } = useRequest2(() =>
|
||||
getOrgList({ orgId: org._id, withPermission: false })
|
||||
);
|
||||
const onClickExpand = async () => {
|
||||
const data = await getOrgs();
|
||||
if (data.length < 1) {
|
||||
setCanBeExpanded(false);
|
||||
}
|
||||
toggleIsExpanded.toggle();
|
||||
};
|
||||
|
||||
if (org._id === movingOrg._id) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box userSelect={'none'}>
|
||||
<HStack
|
||||
@@ -34,7 +48,7 @@ function OrgTreeNode({
|
||||
pr={2}
|
||||
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
|
||||
cursor={'pointer'}
|
||||
{...(selectedOrg === org
|
||||
{...(selectedOrg?._id === org._id
|
||||
? {
|
||||
bg: 'primary.50 !important',
|
||||
onClick: () => setSelectedOrg(undefined)
|
||||
@@ -43,19 +57,17 @@ function OrgTreeNode({
|
||||
onClick: () => setSelectedOrg(org)
|
||||
})}
|
||||
>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
|
||||
color={'myGray.500'}
|
||||
p={0}
|
||||
w={'1.25rem'}
|
||||
visibility={children.length > 0 ? 'visible' : 'hidden'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleIsExpanded.toggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
|
||||
color={'myGray.500'}
|
||||
p={0}
|
||||
w={'1.25rem'}
|
||||
visibility={canBeExpanded ? 'visible' : 'hidden'}
|
||||
onClick={(e) => {
|
||||
onClickExpand();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<HStack
|
||||
flex={'1 0 0'}
|
||||
onClick={() => setSelectedOrg(org)}
|
||||
@@ -67,13 +79,13 @@ function OrgTreeNode({
|
||||
</HStack>
|
||||
</HStack>
|
||||
{isExpanded &&
|
||||
children.length > 0 &&
|
||||
children.map((child) => (
|
||||
orgs.length > 0 &&
|
||||
orgs.map((child) => (
|
||||
<Box key={child._id} mt={0.5}>
|
||||
<OrgTreeNode
|
||||
movingOrg={movingOrg}
|
||||
org={child}
|
||||
index={index + 1}
|
||||
list={list}
|
||||
selectedOrg={selectedOrg}
|
||||
setSelectedOrg={setSelectedOrg}
|
||||
/>
|
||||
@@ -84,19 +96,32 @@ function OrgTreeNode({
|
||||
}
|
||||
|
||||
function OrgTree({
|
||||
orgs,
|
||||
selectedOrg,
|
||||
setSelectedOrg
|
||||
setSelectedOrg,
|
||||
movingOrg
|
||||
}: {
|
||||
orgs: OrgType[];
|
||||
selectedOrg?: OrgType;
|
||||
setSelectedOrg: (org?: OrgType) => void;
|
||||
selectedOrg?: OrgListItemType;
|
||||
setSelectedOrg: (org?: OrgListItemType) => void;
|
||||
movingOrg: OrgListItemType;
|
||||
}) {
|
||||
const root = orgs[0];
|
||||
if (!root) return;
|
||||
const { userInfo } = useUserStore();
|
||||
const root: OrgListItemType = {
|
||||
_id: '',
|
||||
path: '',
|
||||
pathId: '',
|
||||
name: userInfo?.team.teamName || '',
|
||||
avatar: userInfo?.team.avatar || ''
|
||||
} as any;
|
||||
|
||||
return (
|
||||
<OrgTreeNode org={root} list={orgs} setSelectedOrg={setSelectedOrg} selectedOrg={selectedOrg} />
|
||||
<OrgTreeNode
|
||||
movingOrg={movingOrg}
|
||||
key={'root'}
|
||||
org={root}
|
||||
selectedOrg={selectedOrg}
|
||||
setSelectedOrg={setSelectedOrg}
|
||||
index={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Tr,
|
||||
VStack
|
||||
} from '@chakra-ui/react';
|
||||
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
|
||||
@@ -23,10 +23,8 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import MemberTag from '@/components/support/user/team/Info/MemberTag';
|
||||
import { TeamContext } from '../context';
|
||||
import { deleteOrg, deleteOrgMember, getOrgList } from '@/web/support/user/team/org/api';
|
||||
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
|
||||
|
||||
import IconButton from './IconButton';
|
||||
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
|
||||
@@ -34,11 +32,10 @@ import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import Path from '@/components/common/folder/Path';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { delRemoveMember } from '@/web/support/user/team/api';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
|
||||
|
||||
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
|
||||
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
|
||||
@@ -76,69 +73,25 @@ function ActionButton({
|
||||
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, isTeamAdmin } = useUserStore();
|
||||
const [searchOrg, setSearchOrg] = useState('');
|
||||
|
||||
const { members, MemberScrollData, refetchMembers } = useContextSelector(TeamContext, (v) => v);
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const isSyncMember = feConfigs.register_method?.includes('sync');
|
||||
const [parentPath, setParentPath] = useState('');
|
||||
const {
|
||||
data: orgs = [],
|
||||
loading: isLoadingOrgs,
|
||||
refresh: refetchOrgs
|
||||
} = useRequest2(getOrgList, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const currentOrgs = useMemo(() => {
|
||||
if (orgs.length === 0) return [];
|
||||
// Auto select the first org(root org is team)
|
||||
if (parentPath === '') {
|
||||
setParentPath(getOrgChildrenPath(orgs[0]));
|
||||
return [];
|
||||
}
|
||||
|
||||
return orgs
|
||||
.filter((org) => org.path === parentPath)
|
||||
.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
// Member + org
|
||||
count:
|
||||
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
|
||||
};
|
||||
});
|
||||
}, [orgs, parentPath]);
|
||||
|
||||
const currentOrg = useMemo(() => {
|
||||
const splitPath = parentPath.split('/');
|
||||
const currentOrgId = splitPath[splitPath.length - 1];
|
||||
if (!currentOrgId) return;
|
||||
|
||||
return orgs.find((org) => org.pathId === currentOrgId);
|
||||
}, [orgs, parentPath]);
|
||||
|
||||
const paths = useMemo(() => {
|
||||
const splitPath = parentPath.split('/').filter(Boolean);
|
||||
return splitPath
|
||||
.map((id) => {
|
||||
const org = orgs.find((org) => org.pathId === id)!;
|
||||
|
||||
if (org?.path === '') return;
|
||||
|
||||
return {
|
||||
parentId: getOrgChildrenPath(org),
|
||||
parentName: org.name
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ParentTreePathItemType[];
|
||||
}, [parentPath, orgs]);
|
||||
|
||||
const [editOrg, setEditOrg] = useState<OrgFormType>();
|
||||
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
|
||||
const [movingOrg, setMovingOrg] = useState<OrgType>();
|
||||
const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>();
|
||||
const [movingOrg, setMovingOrg] = useState<OrgListItemType>();
|
||||
const {
|
||||
currentOrg,
|
||||
orgs,
|
||||
isLoading,
|
||||
paths,
|
||||
onClickOrg,
|
||||
members,
|
||||
MemberScrollData,
|
||||
onPathClick,
|
||||
refresh,
|
||||
updateCurrentOrg,
|
||||
setSearchKey,
|
||||
searchKey
|
||||
} = useOrg();
|
||||
|
||||
// Delete org
|
||||
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
|
||||
@@ -147,9 +100,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
});
|
||||
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
|
||||
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
|
||||
onSuccess: () => {
|
||||
refetchOrgs();
|
||||
}
|
||||
onSuccess: refresh
|
||||
});
|
||||
|
||||
// Delete member
|
||||
@@ -164,30 +115,13 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
});
|
||||
|
||||
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
|
||||
onSuccess: () => {
|
||||
refetchOrgs();
|
||||
}
|
||||
onSuccess: refresh
|
||||
});
|
||||
|
||||
const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, {
|
||||
onSuccess: () => {
|
||||
refetchOrgs();
|
||||
refetchMembers();
|
||||
}
|
||||
onSuccess: refresh
|
||||
});
|
||||
|
||||
const searchedOrgs = useMemo(() => {
|
||||
if (!searchOrg) return [];
|
||||
|
||||
return orgs
|
||||
.filter((org) => org.name.includes(searchOrg))
|
||||
.map((org) => ({
|
||||
...org,
|
||||
count:
|
||||
org.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(org)).length
|
||||
}));
|
||||
}, [orgs, searchOrg]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
|
||||
@@ -195,24 +129,19 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<Box w="200px">
|
||||
<SearchInput
|
||||
placeholder={t('account_team:search_org')}
|
||||
value={searchOrg}
|
||||
onChange={(e) => setSearchOrg(e.target.value)}
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<MyBox
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
isLoading={isLoadingOrgs}
|
||||
>
|
||||
<MyBox flex={'1 0 0'} h={0} display={'flex'} flexDirection={'column'}>
|
||||
<Box mb={3}>
|
||||
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
|
||||
{!searchKey && (
|
||||
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={onPathClick} />
|
||||
)}
|
||||
</Box>
|
||||
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
|
||||
<MemberScrollData h={'100%'} fontSize={'sm'} flexGrow={1}>
|
||||
{/* Table */}
|
||||
<MemberScrollData flex="1" isLoading={isLoading}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
@@ -226,45 +155,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{searchedOrgs.map((org) => (
|
||||
<Tr
|
||||
key={org._id}
|
||||
overflow={'unset'}
|
||||
onClick={() => setParentPath(getOrgChildrenPath(org))}
|
||||
>
|
||||
<Td>
|
||||
<HStack
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setParentPath(getOrgChildrenPath(org));
|
||||
setSearchOrg('');
|
||||
}}
|
||||
>
|
||||
<MemberTag name={org.name} avatar={org.avatar} />
|
||||
<Tag size="sm">{org.count}</Tag>
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w={'1rem'}
|
||||
h={'1rem'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{!searchOrg &&
|
||||
currentOrgs.map((org) => (
|
||||
{orgs
|
||||
.filter((org) => org.path !== '')
|
||||
.map((org) => (
|
||||
<Tr key={org._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setParentPath(getOrgChildrenPath(org));
|
||||
setSearchOrg('');
|
||||
}}
|
||||
>
|
||||
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
|
||||
<MemberTag name={org.name} avatar={org.avatar} />
|
||||
<Tag size="sm">{org.count}</Tag>
|
||||
<Tag size="sm">{org.total}</Tag>
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w={'1rem'}
|
||||
@@ -305,15 +203,12 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
{!searchOrg &&
|
||||
currentOrg?.members.map((member) => {
|
||||
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
|
||||
if (!memberInfo) return null;
|
||||
|
||||
{!searchKey &&
|
||||
members.map((member) => {
|
||||
return (
|
||||
<Tr key={member.tmbId}>
|
||||
<Td>
|
||||
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
|
||||
<MemberTag name={member.memberName} avatar={member.avatar} />
|
||||
</Td>
|
||||
<Td w={'6rem'}>
|
||||
{isTeamAdmin && (
|
||||
@@ -331,14 +226,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
}
|
||||
},
|
||||
label: t('account_team:delete_from_team', {
|
||||
username: memberInfo.memberName
|
||||
username: member.memberName
|
||||
}),
|
||||
onClick: () => {
|
||||
openDeleteMemberFromTeamModal(
|
||||
() => deleteMemberFromTeamReq(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:confirm_delete_from_team', {
|
||||
username: memberInfo.memberName
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}
|
||||
@@ -356,11 +251,17 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
label: t('account_team:delete_from_org'),
|
||||
onClick: () =>
|
||||
openDeleteMemberFromOrgModal(
|
||||
() =>
|
||||
deleteMemberReq(currentOrg._id, member.tmbId),
|
||||
() => {
|
||||
if (currentOrg) {
|
||||
return deleteMemberReq(
|
||||
currentOrg._id,
|
||||
member.tmbId
|
||||
);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
t('account_team:confirm_delete_from_org', {
|
||||
username: memberInfo.memberName
|
||||
username: member.memberName
|
||||
})
|
||||
)()
|
||||
}
|
||||
@@ -383,22 +284,29 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
{!isSyncMember && (
|
||||
<VStack w={'180px'} alignItems={'start'}>
|
||||
<HStack gap={'6px'}>
|
||||
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
|
||||
<Box fontWeight={500} color={'myGray.900'}>
|
||||
{currentOrg?.name}
|
||||
<Avatar src={currentOrg.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
|
||||
<Box
|
||||
title={currentOrg.name}
|
||||
fontWeight={500}
|
||||
color={'myGray.900'}
|
||||
className="textEllipsis3"
|
||||
>
|
||||
{currentOrg.name}
|
||||
</Box>
|
||||
{currentOrg?.path !== '' && (
|
||||
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
|
||||
)}
|
||||
</HStack>
|
||||
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
|
||||
{currentOrg?.path !== '' && (
|
||||
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
|
||||
)}
|
||||
|
||||
<Divider my={'20px'} />
|
||||
|
||||
<Box fontWeight={500} fontSize="sm" color="myGray.900">
|
||||
{t('common:common.Action')}
|
||||
</Box>
|
||||
{currentOrg && isTeamAdmin && (
|
||||
{isTeamAdmin && (
|
||||
<VStack gap="13px" w="100%">
|
||||
<ActionButton
|
||||
icon="common/add2"
|
||||
@@ -406,7 +314,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
onClick={() => {
|
||||
setEditOrg({
|
||||
...defaultOrgForm,
|
||||
parentId: currentOrg?._id
|
||||
path: currentOrg.path
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -440,21 +348,22 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<OrgInfoModal
|
||||
editOrg={editOrg}
|
||||
onClose={() => setEditOrg(undefined)}
|
||||
onSuccess={refetchOrgs}
|
||||
onSuccess={refresh}
|
||||
updateCurrentOrg={updateCurrentOrg}
|
||||
parentId={currentOrg._id}
|
||||
/>
|
||||
)}
|
||||
{!!movingOrg && (
|
||||
<OrgMoveModal
|
||||
orgs={orgs}
|
||||
movingOrg={movingOrg}
|
||||
onClose={() => setMovingOrg(undefined)}
|
||||
onSuccess={refetchOrgs}
|
||||
onSuccess={refresh}
|
||||
/>
|
||||
)}
|
||||
{!!manageMemberOrg && (
|
||||
<OrgMemberManageModal
|
||||
currentOrg={manageMemberOrg}
|
||||
refetchOrgs={refetchOrgs}
|
||||
refetchOrgs={refresh}
|
||||
onClose={() => setManageMemberOrg(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -75,6 +75,7 @@ function PermissionManage({
|
||||
const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), {
|
||||
manual: false,
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import type { EditTeamFormDataType } from './EditInfoModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { getTeamList, getTeamMembers, putSwitchTeam } from '@/web/support/user/team/api';
|
||||
import {
|
||||
getTeamList,
|
||||
getTeamMemberCount,
|
||||
getTeamMembers,
|
||||
putSwitchTeam
|
||||
} from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { getOrgList } from '@/web/support/user/team/org/api';
|
||||
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
|
||||
|
||||
type TeamModalContextType = {
|
||||
myTeams: TeamTmbItemType[];
|
||||
members: TeamMemberItemType[];
|
||||
groups: MemberGroupListType;
|
||||
orgs: OrgType[];
|
||||
isLoading: boolean;
|
||||
onSwitchTeam: (teamId: string) => void;
|
||||
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
|
||||
|
||||
refetchMembers: () => void;
|
||||
refetchTeamSize: () => void;
|
||||
refetchTeams: () => void;
|
||||
refetchGroups: () => void;
|
||||
refetchOrgs: () => void;
|
||||
teamSize: number;
|
||||
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||
};
|
||||
|
||||
export const TeamContext = createContext<TeamModalContextType>({
|
||||
myTeams: [],
|
||||
groups: [],
|
||||
members: [],
|
||||
orgs: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (_teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
@@ -48,21 +41,16 @@ export const TeamContext = createContext<TeamModalContextType>({
|
||||
refetchTeams: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchMembers: function (): void {
|
||||
refetchTeamSize: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchGroups: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchOrgs: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
teamSize: 0,
|
||||
MemberScrollData: () => <></>
|
||||
teamSize: 0
|
||||
});
|
||||
|
||||
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
|
||||
@@ -75,68 +63,36 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
||||
refreshDeps: [userInfo?._id]
|
||||
});
|
||||
|
||||
const {
|
||||
data: orgs = [],
|
||||
loading: isLoadingOrgs,
|
||||
refresh: refetchOrgs
|
||||
} = useRequest2(getOrgList, {
|
||||
const { data: teamMemberCountData, refresh: refetchTeamSize } = useRequest2(getTeamMemberCount, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
// member action
|
||||
const {
|
||||
data: members = [],
|
||||
isLoading: loadingMembers,
|
||||
refreshList: refetchMembers,
|
||||
total: memberTotal,
|
||||
ScrollData: MemberScrollData
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 1000,
|
||||
params: {
|
||||
withLeaved: true
|
||||
}
|
||||
});
|
||||
|
||||
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
await putSwitchTeam(teamId);
|
||||
refetchMembers();
|
||||
return initUserInfo();
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.reload();
|
||||
},
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: isLoadingGroups,
|
||||
refresh: refetchGroups
|
||||
} = useRequest2(getGroupList, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups || isLoadingOrgs;
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam;
|
||||
|
||||
const contextValue = {
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
isLoading,
|
||||
onSwitchTeam,
|
||||
orgs,
|
||||
refetchOrgs,
|
||||
|
||||
// create | update team
|
||||
setEditTeamData,
|
||||
members,
|
||||
refetchMembers,
|
||||
groups,
|
||||
refetchGroups,
|
||||
teamSize: memberTotal,
|
||||
MemberScrollData
|
||||
teamSize: teamMemberCountData?.count || 0,
|
||||
refetchTeamSize
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -326,7 +326,7 @@ function EditLinkModal({
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={t('publish:link_name')}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
|
||||
@@ -26,7 +26,7 @@ function BasicInfo({
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder={t('publish:publish_name')}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
|
||||
@@ -185,8 +185,7 @@ const TeamCloud = ({
|
||||
const {
|
||||
ScrollData,
|
||||
data: scrollDataList,
|
||||
setData,
|
||||
isLoading
|
||||
setData
|
||||
} = useScrollPagination(getWorkflowVersionList, {
|
||||
pageSize: 30,
|
||||
params: {
|
||||
@@ -230,7 +229,7 @@ const TeamCloud = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollData isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||
<ScrollData flex={'1 0 0'} px={5} isLoading={isLoadingVersion}>
|
||||
{scrollDataList.map((item, index) => {
|
||||
const firstPublishedIndex = scrollDataList.findIndex((data) => data.isPublish);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
@@ -10,12 +10,14 @@ import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
|
||||
|
||||
type Props = {
|
||||
appForm: AppSimpleEditFormType;
|
||||
@@ -27,6 +29,8 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
// form2AppWorkflow dependent allDatasets
|
||||
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
@@ -62,10 +66,12 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
|
||||
<Flex px={[2, 5]} pb={2}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} color={'myGray.900'} mr={3}>
|
||||
{t('app:chat_debug')}
|
||||
</Box>
|
||||
{!isVariableVisible && <VariablePopover showExternalVariables />}
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
|
||||
@@ -21,6 +21,7 @@ import ChatRecordContextProvider, {
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -45,6 +46,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
|
||||
return (
|
||||
@@ -115,10 +117,12 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
bg={'myGray.25'}
|
||||
borderBottom={'1px solid #F4F4F7'}
|
||||
>
|
||||
<Flex fontSize={'16px'} fontWeight={'bold'} flex={1} alignItems={'center'}>
|
||||
<Flex fontSize={'16px'} fontWeight={'bold'} alignItems={'center'} mr={3}>
|
||||
<MyIcon name={'common/paused'} w={'14px'} mr={2.5} />
|
||||
{t('common:core.chat.Run test')}
|
||||
</Flex>
|
||||
{!isVariableVisible && <VariablePopover showExternalVariables />}
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
|
||||
@@ -31,7 +31,10 @@ import { WorkflowContext } from '../../context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { AppContext } from '../../../context';
|
||||
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
||||
import {
|
||||
ExternalVariableInputItem,
|
||||
VariableInputItem
|
||||
} from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
|
||||
@@ -58,13 +61,17 @@ export const useDebug = () => {
|
||||
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
const filteredVar = useMemo(() => {
|
||||
const variables = appDetail.chatConfig?.variables;
|
||||
return variables?.filter((item) => item.type !== VariableInputEnum.custom) || [];
|
||||
const { filteredVar, customVar, variables } = useMemo(() => {
|
||||
const variables = appDetail.chatConfig?.variables || [];
|
||||
return {
|
||||
filteredVar: variables.filter((item) => item.type !== VariableInputEnum.custom) || [],
|
||||
customVar: variables.filter((item) => item.type === VariableInputEnum.custom) || [],
|
||||
variables
|
||||
};
|
||||
}, [appDetail.chatConfig?.variables]);
|
||||
|
||||
const [defaultGlobalVariables, setDefaultGlobalVariables] = useState<Record<string, any>>(
|
||||
filteredVar.reduce(
|
||||
variables.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.key] = item.defaultValue;
|
||||
return acc;
|
||||
@@ -241,7 +248,7 @@ export const useDebug = () => {
|
||||
px={0}
|
||||
>
|
||||
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
|
||||
{filteredVar.length > 0 && (
|
||||
{variables.length > 0 && (
|
||||
<LightRowTabs<TabEnum>
|
||||
gap={3}
|
||||
ml={-2}
|
||||
@@ -256,6 +263,14 @@ export const useDebug = () => {
|
||||
/>
|
||||
)}
|
||||
<Box display={currentTab === TabEnum.global ? 'block' : 'none'}>
|
||||
{customVar.map((item) => (
|
||||
<ExternalVariableInputItem
|
||||
key={item.id}
|
||||
item={{ ...item, key: item.key }}
|
||||
variablesForm={variablesForm}
|
||||
showTag={true}
|
||||
/>
|
||||
))}
|
||||
{filteredVar.map((item) => (
|
||||
<VariableInputItem
|
||||
key={item.id}
|
||||
@@ -354,13 +369,15 @@ export const useDebug = () => {
|
||||
</MyRightDrawer>
|
||||
);
|
||||
}, [
|
||||
defaultGlobalVariables,
|
||||
filteredVar,
|
||||
onStartNodeDebug,
|
||||
runtimeEdges,
|
||||
runtimeNodeId,
|
||||
runtimeNodes,
|
||||
t
|
||||
runtimeEdges,
|
||||
defaultGlobalVariables,
|
||||
t,
|
||||
variables.length,
|
||||
customVar,
|
||||
filteredVar,
|
||||
runtimeNodeId,
|
||||
onStartNodeDebug
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -96,7 +96,7 @@ const ExtractFieldModal = ({
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="name/age/sql"
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
{...register('key', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -297,8 +297,10 @@ const InputTypeConfig = ({
|
||||
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Default Value')}
|
||||
</FormLabel>
|
||||
<Flex alignItems={'start'} flex={1} h={10}>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<Flex alignItems={'center'} flex={1} h={10}>
|
||||
{(inputType === FlowNodeInputTypeEnum.numberInput ||
|
||||
(inputType === VariableInputEnum.custom &&
|
||||
valueType === WorkflowIOValueTypeEnum.number)) && (
|
||||
<MyNumberInput
|
||||
value={defaultValue}
|
||||
min={min}
|
||||
@@ -310,7 +312,8 @@ const InputTypeConfig = ({
|
||||
/>
|
||||
)}
|
||||
{(inputType === FlowNodeInputTypeEnum.input ||
|
||||
inputType === VariableInputEnum.custom) && (
|
||||
(inputType === VariableInputEnum.custom &&
|
||||
valueType === WorkflowIOValueTypeEnum.string)) && (
|
||||
<MyTextarea
|
||||
{...register('defaultValue')}
|
||||
bg={'myGray.50'}
|
||||
@@ -319,7 +322,13 @@ const InputTypeConfig = ({
|
||||
maxH={100}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
|
||||
{(inputType === FlowNodeInputTypeEnum.JSONEditor ||
|
||||
(inputType === VariableInputEnum.custom &&
|
||||
![
|
||||
WorkflowIOValueTypeEnum.number,
|
||||
WorkflowIOValueTypeEnum.string,
|
||||
WorkflowIOValueTypeEnum.boolean
|
||||
].includes(valueType))) && (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
resize
|
||||
@@ -330,7 +339,9 @@ const InputTypeConfig = ({
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.switch && (
|
||||
{(inputType === FlowNodeInputTypeEnum.switch ||
|
||||
(inputType === VariableInputEnum.custom &&
|
||||
valueType === WorkflowIOValueTypeEnum.boolean)) && (
|
||||
<Switch {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.select && (
|
||||
|
||||
@@ -418,7 +418,7 @@ const NodeCard = (props: Props) => {
|
||||
{RenderToolHandle}
|
||||
|
||||
<ConfirmSyncModal />
|
||||
<EditTitleModal maxLength={50} />
|
||||
<EditTitleModal maxLength={100} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -247,9 +247,9 @@ const MultipleReferenceSelector = ({
|
||||
|
||||
// Get valid item and remove invalid item
|
||||
const formatList = useMemo(() => {
|
||||
if (!value) return [];
|
||||
if (!value || !Array.isArray(value)) return [];
|
||||
|
||||
return value?.map((item) => {
|
||||
return value.map((item) => {
|
||||
const [nodeName, outputName] = getSelectValue(item);
|
||||
return {
|
||||
rawValue: item,
|
||||
|
||||
@@ -10,7 +10,9 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import {
|
||||
Prompt_userQuotePromptList,
|
||||
Prompt_QuoteTemplateList,
|
||||
Prompt_systemQuotePromptList
|
||||
Prompt_systemQuotePromptList,
|
||||
getQuoteTemplate,
|
||||
getQuotePrompt
|
||||
} from '@fastgpt/global/core/ai/prompt/AIChat';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import PromptTemplate from '@/components/PromptTemplate';
|
||||
@@ -48,6 +50,8 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const node = nodeList.find((item) => item.id === nodeId);
|
||||
const nodeVersion = node?.version;
|
||||
|
||||
const { watch, setValue, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
@@ -219,7 +223,7 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('workflow:quote_content_tip', {
|
||||
default: Prompt_QuoteTemplateList[0].value
|
||||
default: getQuoteTemplate(nodeVersion)
|
||||
})}
|
||||
></QuestionTip>
|
||||
<Box flex={1} />
|
||||
@@ -254,7 +258,7 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('workflow:quote_prompt_tip', {
|
||||
default: quotePromptTemplates[0].value
|
||||
default: getQuotePrompt(nodeVersion, aiChatQuoteRole)
|
||||
})}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
@@ -263,7 +267,7 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
|
||||
title={t('common:core.app.Quote prompt')}
|
||||
minH={300}
|
||||
placeholder={t('workflow:quote_prompt_tip', {
|
||||
default: quotePromptTemplates[0].value
|
||||
default: getQuotePrompt(nodeVersion, aiChatQuoteRole)
|
||||
})}
|
||||
value={aiChatQuotePrompt}
|
||||
onChange={(e) => {
|
||||
@@ -288,10 +292,10 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
|
||||
onSuccess={(e) => {
|
||||
const quoteVal = e.value;
|
||||
|
||||
const promptVal = quotePromptTemplates.find((item) => item.title === e.title)?.value;
|
||||
const promptVal = quotePromptTemplates.find((item) => item.title === e.title)?.value!;
|
||||
|
||||
setValue('quoteTemplate', quoteVal);
|
||||
setValue('quotePrompt', promptVal);
|
||||
setValue('quoteTemplate', Object.values(quoteVal)[0]);
|
||||
setValue('quotePrompt', Object.values(promptVal)[0]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -140,7 +140,7 @@ export const useChatTest = ({
|
||||
appId={appId}
|
||||
chatId={chatId}
|
||||
showMarkIcon
|
||||
chatType="chat"
|
||||
chatType={'chat'}
|
||||
onStartChat={startChat}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -212,7 +212,9 @@ const ListItem = () => {
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'}>{app.intro || t('common:common.no_intro')}</Box>
|
||||
<Box className={'textEllipsis2'} whiteSpace={'pre-wrap'}>
|
||||
{app.intro || t('common:common.no_intro')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex
|
||||
h={'24px'}
|
||||
|
||||
@@ -319,7 +319,7 @@ const TemplateMarketModal = ({
|
||||
onChange={(e) => setCurrentSearch(e.target.value)}
|
||||
h={8}
|
||||
bg={'myGray.50'}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import SelectOneResource from '@/components/common/folder/SelectOneResource';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
|
||||
|
||||
const ChatHeader = ({
|
||||
history,
|
||||
@@ -38,7 +39,10 @@ const ChatHeader = ({
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
|
||||
const isPlugin = chatData.app.type === AppTypeEnum.plugin;
|
||||
const router = useRouter();
|
||||
const isChat = router.pathname === '/chat';
|
||||
|
||||
return isPc && isPlugin ? null : (
|
||||
<Flex
|
||||
@@ -68,8 +72,12 @@ const ChatHeader = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* control */}
|
||||
{!isPlugin && <ToolMenu history={history} />}
|
||||
<Flex gap={2} alignItems={'center'}>
|
||||
{!isVariableVisible && <VariablePopover showExternalVariables={isChat} />}
|
||||
|
||||
{/* control */}
|
||||
{!isPlugin && <ToolMenu history={history} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ const EditFolderModal = ({
|
||||
defaultValue={name}
|
||||
placeholder={t('common:dataset.Folder Name') || ''}
|
||||
autoFocus
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
DatasetCollectionTypeEnum,
|
||||
DatasetStatusEnum,
|
||||
DatasetCollectionSyncResultMap,
|
||||
DatasetTypeEnum,
|
||||
DatasetCollectionDataProcessModeMap
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
@@ -45,7 +44,10 @@ import { CollectionPageContext } from './Context';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { checkCollectionIsFolder } from '@fastgpt/global/core/dataset/collection/utils';
|
||||
import {
|
||||
checkCollectionIsFolder,
|
||||
collectionCanSync
|
||||
} from '@fastgpt/global/core/dataset/collection/utils';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import TagsPopOver from './TagsPopOver';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
@@ -315,8 +317,7 @@ const CollectionCard = () => {
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
...(collection.type === DatasetCollectionTypeEnum.link ||
|
||||
datasetDetail.type === DatasetTypeEnum.apiDataset
|
||||
...(collectionCanSync(collection.type)
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
|
||||
@@ -10,11 +10,21 @@ import { useMyStep } from '@fastgpt/web/hooks/useStep';
|
||||
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { TabEnum } from '../NavBar';
|
||||
import { ChunkSettingModeEnum } from '@/web/core/dataset/constants';
|
||||
import { ChunkSettingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { UseFormReturn, useForm } from 'react-hook-form';
|
||||
import { ImportSourceItemType } from '@/web/core/dataset/type';
|
||||
import { Prompt_AgentQA } from '@fastgpt/global/core/ai/prompt/agent';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { DataChunkSplitModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import {
|
||||
getMaxChunkSize,
|
||||
getLLMDefaultChunkSize,
|
||||
getLLMMaxChunkSize,
|
||||
chunkAutoChunkSize,
|
||||
minChunkSize,
|
||||
getAutoIndexSize,
|
||||
getMaxIndexSize
|
||||
} from '@fastgpt/global/core/dataset/training/utils';
|
||||
|
||||
type TrainingFiledType = {
|
||||
chunkOverlapRatio: number;
|
||||
@@ -22,6 +32,9 @@ type TrainingFiledType = {
|
||||
minChunkSize: number;
|
||||
autoChunkSize: number;
|
||||
chunkSize: number;
|
||||
maxIndexSize?: number;
|
||||
indexSize?: number;
|
||||
autoIndexSize?: number;
|
||||
charsPointsPrice: number;
|
||||
priceTip: string;
|
||||
uploadRate: number;
|
||||
@@ -47,9 +60,13 @@ export type ImportFormType = {
|
||||
autoIndexes: boolean;
|
||||
|
||||
chunkSettingMode: ChunkSettingModeEnum;
|
||||
|
||||
chunkSplitMode: DataChunkSplitModeEnum;
|
||||
embeddingChunkSize: number;
|
||||
qaChunkSize: number;
|
||||
customSplitChar: string;
|
||||
chunkSplitter: string;
|
||||
indexSize: number;
|
||||
|
||||
qaPrompt: string;
|
||||
webSelector: string;
|
||||
};
|
||||
@@ -199,9 +216,12 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
trainingType: DatasetCollectionDataProcessModeEnum.chunk,
|
||||
|
||||
chunkSettingMode: ChunkSettingModeEnum.auto,
|
||||
embeddingChunkSize: vectorModel?.defaultToken || 512,
|
||||
qaChunkSize: Math.min(agentModel.maxResponse * 1, agentModel.maxContext * 0.7),
|
||||
customSplitChar: '',
|
||||
|
||||
chunkSplitMode: DataChunkSplitModeEnum.size,
|
||||
embeddingChunkSize: 2000,
|
||||
indexSize: vectorModel?.defaultToken || 512,
|
||||
qaChunkSize: getLLMDefaultChunkSize(agentModel),
|
||||
chunkSplitter: '',
|
||||
qaPrompt: Prompt_AgentQA.description,
|
||||
webSelector: '',
|
||||
customPdfParse: false
|
||||
@@ -215,17 +235,18 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
const chunkSettingMode = processParamsForm.watch('chunkSettingMode');
|
||||
const embeddingChunkSize = processParamsForm.watch('embeddingChunkSize');
|
||||
const qaChunkSize = processParamsForm.watch('qaChunkSize');
|
||||
const customSplitChar = processParamsForm.watch('customSplitChar');
|
||||
const chunkSplitter = processParamsForm.watch('chunkSplitter');
|
||||
const autoIndexes = processParamsForm.watch('autoIndexes');
|
||||
const indexSize = processParamsForm.watch('indexSize');
|
||||
|
||||
const TrainingModeMap = useMemo<TrainingFiledType>(() => {
|
||||
if (trainingType === DatasetCollectionDataProcessModeEnum.qa) {
|
||||
return {
|
||||
chunkSizeField: 'qaChunkSize',
|
||||
chunkOverlapRatio: 0,
|
||||
maxChunkSize: Math.min(agentModel.maxResponse * 4, agentModel.maxContext * 0.7),
|
||||
minChunkSize: 4000,
|
||||
autoChunkSize: Math.min(agentModel.maxResponse * 1, agentModel.maxContext * 0.7),
|
||||
maxChunkSize: getLLMMaxChunkSize(agentModel),
|
||||
minChunkSize: 1000,
|
||||
autoChunkSize: getLLMDefaultChunkSize(agentModel),
|
||||
chunkSize: qaChunkSize,
|
||||
charsPointsPrice: agentModel.charsPointsPrice || 0,
|
||||
priceTip: t('dataset:import.Auto mode Estimated Price Tips', {
|
||||
@@ -237,10 +258,13 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
return {
|
||||
chunkSizeField: 'embeddingChunkSize',
|
||||
chunkOverlapRatio: 0.2,
|
||||
maxChunkSize: 2048,
|
||||
minChunkSize: 100,
|
||||
autoChunkSize: vectorModel?.defaultToken ? vectorModel.defaultToken * 2 : 1024,
|
||||
maxChunkSize: getMaxChunkSize(agentModel),
|
||||
minChunkSize: minChunkSize,
|
||||
autoChunkSize: chunkAutoChunkSize,
|
||||
chunkSize: embeddingChunkSize,
|
||||
maxIndexSize: getMaxIndexSize(vectorModel),
|
||||
autoIndexSize: getAutoIndexSize(vectorModel),
|
||||
indexSize,
|
||||
charsPointsPrice: agentModel.charsPointsPrice || 0,
|
||||
priceTip: t('dataset:import.Auto mode Estimated Price Tips', {
|
||||
price: agentModel.charsPointsPrice
|
||||
@@ -251,10 +275,13 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
return {
|
||||
chunkSizeField: 'embeddingChunkSize',
|
||||
chunkOverlapRatio: 0.2,
|
||||
maxChunkSize: vectorModel?.maxToken || 512,
|
||||
minChunkSize: 100,
|
||||
autoChunkSize: vectorModel?.defaultToken || 512,
|
||||
maxChunkSize: getMaxChunkSize(agentModel),
|
||||
minChunkSize: minChunkSize,
|
||||
autoChunkSize: chunkAutoChunkSize,
|
||||
chunkSize: embeddingChunkSize,
|
||||
maxIndexSize: getMaxIndexSize(vectorModel),
|
||||
autoIndexSize: getAutoIndexSize(vectorModel),
|
||||
indexSize,
|
||||
charsPointsPrice: vectorModel.charsPointsPrice || 0,
|
||||
priceTip: t('dataset:import.Embedding Estimated Price Tips', {
|
||||
price: vectorModel.charsPointsPrice
|
||||
@@ -265,30 +292,36 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
}, [
|
||||
trainingType,
|
||||
autoIndexes,
|
||||
agentModel.maxResponse,
|
||||
agentModel.maxContext,
|
||||
agentModel.charsPointsPrice,
|
||||
agentModel,
|
||||
qaChunkSize,
|
||||
t,
|
||||
vectorModel.defaultToken,
|
||||
vectorModel?.maxToken,
|
||||
vectorModel.charsPointsPrice,
|
||||
embeddingChunkSize
|
||||
embeddingChunkSize,
|
||||
vectorModel,
|
||||
indexSize
|
||||
]);
|
||||
|
||||
const chunkSettingModeMap = useMemo(() => {
|
||||
if (chunkSettingMode === ChunkSettingModeEnum.auto) {
|
||||
return {
|
||||
chunkSize: TrainingModeMap.autoChunkSize,
|
||||
customSplitChar: ''
|
||||
indexSize: TrainingModeMap.autoIndexSize,
|
||||
chunkSplitter: ''
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
chunkSize: TrainingModeMap.chunkSize,
|
||||
customSplitChar
|
||||
indexSize: TrainingModeMap.indexSize,
|
||||
chunkSplitter
|
||||
};
|
||||
}
|
||||
}, [chunkSettingMode, TrainingModeMap.autoChunkSize, TrainingModeMap.chunkSize, customSplitChar]);
|
||||
}, [
|
||||
chunkSettingMode,
|
||||
TrainingModeMap.autoChunkSize,
|
||||
TrainingModeMap.autoIndexSize,
|
||||
TrainingModeMap.chunkSize,
|
||||
TrainingModeMap.indexSize,
|
||||
chunkSplitter
|
||||
]);
|
||||
|
||||
const contextValue = {
|
||||
...TrainingModeMap,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -20,10 +20,11 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
|
||||
import {
|
||||
DataChunkSplitModeEnum,
|
||||
DatasetCollectionDataProcessModeEnum,
|
||||
DatasetCollectionDataProcessModeMap
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { ChunkSettingModeEnum } from '@/web/core/dataset/constants';
|
||||
import { ChunkSettingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
@@ -36,26 +37,27 @@ import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { shadowLight } from '@fastgpt/web/styles/theme';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { getIndexSizeSelectList } from '@fastgpt/global/core/dataset/training/utils';
|
||||
import RadioGroup from '@fastgpt/web/components/common/Radio/RadioGroup';
|
||||
|
||||
function DataProcess() {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { goToNext, processParamsForm, chunkSizeField, minChunkSize, maxChunkSize } =
|
||||
useContextSelector(DatasetImportContext, (v) => v);
|
||||
const {
|
||||
goToNext,
|
||||
processParamsForm,
|
||||
chunkSizeField,
|
||||
minChunkSize,
|
||||
maxChunkSize,
|
||||
maxIndexSize,
|
||||
indexSize
|
||||
} = useContextSelector(DatasetImportContext, (v) => v);
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
const { setValue, register, watch } = processParamsForm;
|
||||
const { setValue, register, watch, getValues } = processParamsForm;
|
||||
|
||||
const trainingType = watch('trainingType');
|
||||
const chunkSettingMode = watch('chunkSettingMode');
|
||||
const qaPrompt = watch('qaPrompt');
|
||||
|
||||
const {
|
||||
isOpen: isOpenCustomPrompt,
|
||||
onOpen: onOpenCustomPrompt,
|
||||
onClose: onCloseCustomPrompt
|
||||
} = useDisclosure();
|
||||
|
||||
const trainingModeList = useMemo(() => {
|
||||
const list = Object.entries(DatasetCollectionDataProcessModeMap);
|
||||
return list
|
||||
@@ -67,6 +69,41 @@ function DataProcess() {
|
||||
}));
|
||||
}, [t]);
|
||||
|
||||
const chunkSettingMode = watch('chunkSettingMode');
|
||||
const chunkSplitMode = watch('chunkSplitMode');
|
||||
|
||||
const customSplitList = [
|
||||
{ label: t('dataset:split_sign_null'), value: '' },
|
||||
{ label: t('dataset:split_sign_break'), value: '\\n' },
|
||||
{ label: t('dataset:split_sign_break2'), value: '\\n\\n' },
|
||||
{ label: t('dataset:split_sign_period'), value: '.|。' },
|
||||
{ label: t('dataset:split_sign_exclamatiob'), value: '!|!' },
|
||||
{ label: t('dataset:split_sign_question'), value: '?|?' },
|
||||
{ label: t('dataset:split_sign_semicolon'), value: ';|;' },
|
||||
{ label: '=====', value: '=====' },
|
||||
{ label: t('dataset:split_sign_custom'), value: 'Other' }
|
||||
];
|
||||
|
||||
const [customListSelectValue, setCustomListSelectValue] = useState(getValues('chunkSplitter'));
|
||||
useEffect(() => {
|
||||
if (customListSelectValue === 'Other') {
|
||||
setValue('chunkSplitter', '');
|
||||
} else {
|
||||
setValue('chunkSplitter', customListSelectValue);
|
||||
}
|
||||
}, [customListSelectValue, setValue]);
|
||||
|
||||
// Index size
|
||||
const indexSizeSeletorList = useMemo(() => getIndexSizeSelectList(maxIndexSize), [maxIndexSize]);
|
||||
|
||||
// QA
|
||||
const qaPrompt = watch('qaPrompt');
|
||||
const {
|
||||
isOpen: isOpenCustomPrompt,
|
||||
onOpen: onOpenCustomPrompt,
|
||||
onClose: onCloseCustomPrompt
|
||||
} = useDisclosure();
|
||||
|
||||
const Title = useCallback(({ title }: { title: string }) => {
|
||||
return (
|
||||
<AccordionButton bg={'none !important'} p={2}>
|
||||
@@ -215,53 +252,97 @@ function DataProcess() {
|
||||
children: chunkSettingMode === ChunkSettingModeEnum.custom && (
|
||||
<Box mt={5}>
|
||||
<Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box>{t('dataset:ideal_chunk_length')}</Box>
|
||||
<QuestionTip label={t('dataset:ideal_chunk_length_tips')} />
|
||||
</Flex>
|
||||
<Box
|
||||
mt={1}
|
||||
css={{
|
||||
'& > span': {
|
||||
display: 'block'
|
||||
<RadioGroup<DataChunkSplitModeEnum>
|
||||
list={[
|
||||
{
|
||||
title: t('dataset:split_chunk_size'),
|
||||
value: DataChunkSplitModeEnum.size
|
||||
},
|
||||
{
|
||||
title: t('dataset:split_chunk_char'),
|
||||
value: DataChunkSplitModeEnum.char,
|
||||
tooltip: t('dataset:custom_split_sign_tip')
|
||||
}
|
||||
]}
|
||||
value={chunkSplitMode}
|
||||
onChange={(e) => {
|
||||
setValue('chunkSplitMode', e);
|
||||
}}
|
||||
>
|
||||
<MyTooltip
|
||||
label={t('common:core.dataset.import.Chunk Range', {
|
||||
min: minChunkSize,
|
||||
max: maxChunkSize
|
||||
})}
|
||||
/>
|
||||
|
||||
{chunkSplitMode === DataChunkSplitModeEnum.size && (
|
||||
<Box
|
||||
mt={1.5}
|
||||
css={{
|
||||
'& > span': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyNumberInput
|
||||
register={register}
|
||||
name={chunkSizeField}
|
||||
min={minChunkSize}
|
||||
max={maxChunkSize}
|
||||
size={'sm'}
|
||||
step={100}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<MyTooltip
|
||||
label={t('common:core.dataset.import.Chunk Range', {
|
||||
min: minChunkSize,
|
||||
max: maxChunkSize
|
||||
})}
|
||||
>
|
||||
<MyNumberInput
|
||||
register={register}
|
||||
name={chunkSizeField}
|
||||
min={minChunkSize}
|
||||
max={maxChunkSize}
|
||||
size={'sm'}
|
||||
step={100}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{chunkSplitMode === DataChunkSplitModeEnum.char && (
|
||||
<HStack mt={1.5}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect<string>
|
||||
list={customSplitList}
|
||||
size={'sm'}
|
||||
bg={'myGray.50'}
|
||||
value={customListSelectValue}
|
||||
h={'32px'}
|
||||
onChange={(val) => {
|
||||
setCustomListSelectValue(val);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{customListSelectValue === 'Other' && (
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
h={'32px'}
|
||||
size={'sm'}
|
||||
bg={'myGray.50'}
|
||||
placeholder="\n;======;==SPLIT=="
|
||||
{...register('chunkSplitter')}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box mt={3}>
|
||||
{trainingType === DatasetCollectionDataProcessModeEnum.chunk && (
|
||||
<Box>
|
||||
{t('common:core.dataset.import.Custom split char')}
|
||||
<QuestionTip
|
||||
label={t('common:core.dataset.import.Custom split char Tips')}
|
||||
/>
|
||||
<Flex alignItems={'center'} mt={3}>
|
||||
<Box>{t('dataset:index_size')}</Box>
|
||||
<QuestionTip label={t('dataset:index_size_tips')} />
|
||||
</Flex>
|
||||
<Box mt={1}>
|
||||
<MySelect<number>
|
||||
bg={'myGray.50'}
|
||||
list={indexSizeSeletorList}
|
||||
value={indexSize}
|
||||
onChange={(val) => {
|
||||
setValue('indexSize', val);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box mt={1}>
|
||||
<Input
|
||||
size={'sm'}
|
||||
bg={'myGray.50'}
|
||||
defaultValue={''}
|
||||
placeholder="\n;======;==SPLIT=="
|
||||
{...register('customSplitChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showQAPromptInput && (
|
||||
<Box mt={3}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContex
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getLLMMaxChunkSize } from '@fastgpt/global/core/dataset/training/utils';
|
||||
|
||||
const PreviewData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -23,6 +24,7 @@ const PreviewData = () => {
|
||||
const goToNext = useContextSelector(DatasetImportContext, (v) => v.goToNext);
|
||||
|
||||
const datasetId = useContextSelector(DatasetPageContext, (v) => v.datasetId);
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
|
||||
const sources = useContextSelector(DatasetImportContext, (v) => v.sources);
|
||||
const importSource = useContextSelector(DatasetImportContext, (v) => v.importSource);
|
||||
@@ -32,21 +34,25 @@ const PreviewData = () => {
|
||||
|
||||
const [previewFile, setPreviewFile] = useState<ImportSourceItemType>();
|
||||
|
||||
const { data = [], loading: isLoading } = useRequest2(
|
||||
const { data = { chunks: [], total: 0 }, loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
if (!previewFile) return;
|
||||
if (!previewFile) return { chunks: [], total: 0 };
|
||||
if (importSource === ImportDataSourceEnum.fileCustom) {
|
||||
const customSplitChar = processParamsForm.getValues('customSplitChar');
|
||||
const chunkSplitter = processParamsForm.getValues('chunkSplitter');
|
||||
const { chunks } = splitText2Chunks({
|
||||
text: previewFile.rawText || '',
|
||||
chunkLen: chunkSize,
|
||||
chunkSize,
|
||||
maxSize: getLLMMaxChunkSize(datasetDetail.agentModel),
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customReg: customSplitChar ? [customSplitChar] : []
|
||||
customReg: chunkSplitter ? [chunkSplitter] : []
|
||||
});
|
||||
return chunks.map((chunk) => ({
|
||||
q: chunk,
|
||||
a: ''
|
||||
}));
|
||||
return {
|
||||
chunks: chunks.map((chunk) => ({
|
||||
q: chunk,
|
||||
a: ''
|
||||
})),
|
||||
total: chunks.length
|
||||
};
|
||||
}
|
||||
|
||||
return getPreviewChunks({
|
||||
@@ -61,9 +67,12 @@ const PreviewData = () => {
|
||||
|
||||
customPdfParse: processParamsForm.getValues('customPdfParse'),
|
||||
|
||||
trainingType: processParamsForm.getValues('trainingType'),
|
||||
chunkSettingMode: processParamsForm.getValues('chunkSettingMode'),
|
||||
chunkSplitMode: processParamsForm.getValues('chunkSplitMode'),
|
||||
chunkSize,
|
||||
chunkSplitter: processParamsForm.getValues('chunkSplitter'),
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customSplitChar: processParamsForm.getValues('customSplitChar'),
|
||||
|
||||
selector: processParamsForm.getValues('webSelector'),
|
||||
isQAImport: importSource === ImportDataSourceEnum.csvTable,
|
||||
@@ -75,7 +84,7 @@ const PreviewData = () => {
|
||||
manual: false,
|
||||
onSuccess(result) {
|
||||
if (!previewFile) return;
|
||||
if (!result || result.length === 0) {
|
||||
if (!result || result.total === 0) {
|
||||
toast({
|
||||
title: t('dataset:preview_chunk_empty'),
|
||||
status: 'error'
|
||||
@@ -124,14 +133,14 @@ const PreviewData = () => {
|
||||
<Flex py={4} px={5} borderBottom={'base'} justifyContent={'space-between'}>
|
||||
<FormLabel fontSize={'md'}>{t('dataset:preview_chunk')}</FormLabel>
|
||||
<Box fontSize={'xs'} color={'myGray.500'}>
|
||||
{t('dataset:preview_chunk_intro')}
|
||||
{t('dataset:preview_chunk_intro', { total: data.total })}
|
||||
</Box>
|
||||
</Flex>
|
||||
<MyBox isLoading={isLoading} flex={'1 0 0'} h={0}>
|
||||
<Box h={'100%'} overflowY={'auto'} px={5} py={3}>
|
||||
{previewFile ? (
|
||||
<>
|
||||
{data.map((item, index) => (
|
||||
{data.chunks.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
fontSize={'sm'}
|
||||
|
||||
@@ -49,7 +49,7 @@ const Upload = () => {
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
const retrainNewCollectionId = useRef('');
|
||||
|
||||
const { importSource, parentId, sources, setSources, processParamsForm, chunkSize } =
|
||||
const { importSource, parentId, sources, setSources, processParamsForm, chunkSize, indexSize } =
|
||||
useContextSelector(DatasetImportContext, (v) => v);
|
||||
|
||||
const { handleSubmit } = processParamsForm;
|
||||
@@ -81,7 +81,7 @@ const Upload = () => {
|
||||
}, [waitingFilesCount, totalFilesCount, allFinished, t]);
|
||||
|
||||
const { runAsync: startUpload, loading: isLoading } = useRequest2(
|
||||
async ({ trainingType, customSplitChar, qaPrompt, webSelector }: ImportFormType) => {
|
||||
async ({ trainingType, chunkSplitter, qaPrompt, webSelector }: ImportFormType) => {
|
||||
if (sources.length === 0) return;
|
||||
const filterWaitingSources = sources.filter((item) => item.createStatus === 'waiting');
|
||||
|
||||
@@ -111,10 +111,16 @@ const Upload = () => {
|
||||
trainingType,
|
||||
imageIndex: processParamsForm.getValues('imageIndex'),
|
||||
autoIndexes: processParamsForm.getValues('autoIndexes'),
|
||||
|
||||
chunkSettingMode: processParamsForm.getValues('chunkSettingMode'),
|
||||
chunkSplitMode: processParamsForm.getValues('chunkSplitMode'),
|
||||
|
||||
chunkSize,
|
||||
chunkSplitter: customSplitChar,
|
||||
indexSize,
|
||||
chunkSplitter,
|
||||
qaPrompt: trainingType === DatasetCollectionDataProcessModeEnum.qa ? qaPrompt : undefined
|
||||
};
|
||||
|
||||
if (importSource === ImportDataSourceEnum.reTraining) {
|
||||
const res = await postReTrainingDatasetFileCollection({
|
||||
...commonParams,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { ImportSourceItemType } from '@/web/core/dataset/type';
|
||||
import MyRightDrawer from '@fastgpt/web/components/common/MyDrawer/MyRightDrawer';
|
||||
import { getPreviewChunks } from '@/web/core/dataset/api';
|
||||
import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { getPreviewSourceReadType } from '../utils';
|
||||
|
||||
const PreviewChunks = ({
|
||||
previewSource,
|
||||
onClose
|
||||
}: {
|
||||
previewSource: ImportSourceItemType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { importSource, chunkSize, chunkOverlapRatio, processParamsForm } = useContextSelector(
|
||||
DatasetImportContext,
|
||||
(v) => v
|
||||
);
|
||||
const datasetId = useContextSelector(DatasetPageContext, (v) => v.datasetId);
|
||||
|
||||
const { data = [], loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
if (importSource === ImportDataSourceEnum.fileCustom) {
|
||||
const customSplitChar = processParamsForm.getValues('customSplitChar');
|
||||
const { chunks } = splitText2Chunks({
|
||||
text: previewSource.rawText || '',
|
||||
chunkLen: chunkSize,
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customReg: customSplitChar ? [customSplitChar] : []
|
||||
});
|
||||
return chunks.map((chunk) => ({
|
||||
q: chunk,
|
||||
a: ''
|
||||
}));
|
||||
}
|
||||
|
||||
return getPreviewChunks({
|
||||
datasetId,
|
||||
type: getPreviewSourceReadType(previewSource),
|
||||
sourceId:
|
||||
previewSource.dbFileId ||
|
||||
previewSource.link ||
|
||||
previewSource.externalFileUrl ||
|
||||
previewSource.apiFileId ||
|
||||
'',
|
||||
|
||||
chunkSize,
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customSplitChar: processParamsForm.getValues('customSplitChar'),
|
||||
|
||||
selector: processParamsForm.getValues('webSelector'),
|
||||
isQAImport: importSource === ImportDataSourceEnum.csvTable,
|
||||
externalFileId: previewSource.externalFileId
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyRightDrawer
|
||||
onClose={onClose}
|
||||
iconSrc={previewSource.icon}
|
||||
title={previewSource.sourceName}
|
||||
isLoading={isLoading}
|
||||
maxW={['90vw', '40vw']}
|
||||
px={0}
|
||||
>
|
||||
<Box overflowY={'auto'} px={5} fontSize={'sm'}>
|
||||
{data.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
whiteSpace={'pre-wrap'}
|
||||
fontSize={'sm'}
|
||||
p={4}
|
||||
bg={index % 2 === 0 ? 'white' : 'myWhite.600'}
|
||||
mb={3}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'borderColor.low'}
|
||||
boxShadow={'2'}
|
||||
_notLast={{
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
<Box color={'myGray.900'}>{item.q}</Box>
|
||||
<Box color={'myGray.500'}>{item.a}</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</MyRightDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PreviewChunks);
|
||||
@@ -8,10 +8,11 @@ import { useRouter } from 'next/router';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getDatasetCollectionById } from '@/web/core/dataset/api';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { ChunkSettingModeEnum } from '@/web/core/dataset/constants';
|
||||
import { ChunkSettingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { DataChunkSplitModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { Prompt_AgentQA } from '@fastgpt/global/core/ai/prompt/agent';
|
||||
|
||||
const Upload = dynamic(() => import('../commonProgress/Upload'));
|
||||
const PreviewData = dynamic(() => import('../commonProgress/PreviewData'));
|
||||
@@ -23,7 +24,6 @@ const ReTraining = () => {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
|
||||
const setSources = useContextSelector(DatasetImportContext, (v) => v.setSources);
|
||||
const processParamsForm = useContextSelector(DatasetImportContext, (v) => v.processParamsForm);
|
||||
@@ -46,18 +46,21 @@ const ReTraining = () => {
|
||||
uploadedFileRate: 100
|
||||
}
|
||||
]);
|
||||
|
||||
processParamsForm.reset({
|
||||
customPdfParse: collection.customPdfParse,
|
||||
trainingType: collection.trainingType,
|
||||
imageIndex: collection.imageIndex,
|
||||
autoIndexes: collection.autoIndexes,
|
||||
|
||||
chunkSettingMode: ChunkSettingModeEnum.auto,
|
||||
chunkSettingMode: collection.chunkSettingMode || ChunkSettingModeEnum.auto,
|
||||
chunkSplitMode: collection.chunkSplitMode || DataChunkSplitModeEnum.size,
|
||||
embeddingChunkSize: collection.chunkSize,
|
||||
qaChunkSize: collection.chunkSize,
|
||||
customSplitChar: collection.chunkSplitter,
|
||||
qaPrompt: collection.qaPrompt,
|
||||
webSelector: collection.metadata?.webPageSelector
|
||||
indexSize: collection.indexSize || 512,
|
||||
chunkSplitter: collection.chunkSplitter,
|
||||
webSelector: collection.metadata?.webPageSelector,
|
||||
qaPrompt: collection.qaPrompt || Prompt_AgentQA.description
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex, IconButton, useTheme, Progress } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
@@ -9,6 +9,8 @@ import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import ParentPaths from '@/components/common/ParentPaths';
|
||||
import { getTrainingQueueLen } from '@/web/core/dataset/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
export enum TabEnum {
|
||||
dataCard = 'dataCard',
|
||||
@@ -24,8 +26,68 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
const router = useRouter();
|
||||
const query = router.query;
|
||||
const { isPc } = useSystem();
|
||||
const { datasetDetail, vectorTrainingMap, agentTrainingMap, rebuildingCount, paths } =
|
||||
useContextSelector(DatasetPageContext, (v) => v);
|
||||
const { datasetDetail, rebuildingCount, paths } = useContextSelector(
|
||||
DatasetPageContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
// global queue
|
||||
const {
|
||||
data: {
|
||||
vectorTrainingCount = 0,
|
||||
qaTrainingCount = 0,
|
||||
autoTrainingCount = 0,
|
||||
imageTrainingCount = 0
|
||||
} = {}
|
||||
} = useRequest2(getTrainingQueueLen, {
|
||||
manual: false,
|
||||
retryInterval: 10000
|
||||
});
|
||||
const { vectorTrainingMap, qaTrainingMap, autoTrainingMap, imageTrainingMap } = useMemo(() => {
|
||||
const vectorTrainingMap = (() => {
|
||||
if (vectorTrainingCount < 1000)
|
||||
return {
|
||||
colorSchema: 'green',
|
||||
tip: t('common:core.dataset.training.Leisure')
|
||||
};
|
||||
if (vectorTrainingCount < 20000)
|
||||
return {
|
||||
colorSchema: 'yellow',
|
||||
tip: t('common:core.dataset.training.Waiting')
|
||||
};
|
||||
return {
|
||||
colorSchema: 'red',
|
||||
tip: t('common:core.dataset.training.Full')
|
||||
};
|
||||
})();
|
||||
|
||||
const countLLMMap = (count: number) => {
|
||||
if (count < 100)
|
||||
return {
|
||||
colorSchema: 'green',
|
||||
tip: t('common:core.dataset.training.Leisure')
|
||||
};
|
||||
if (count < 1000)
|
||||
return {
|
||||
colorSchema: 'yellow',
|
||||
tip: t('common:core.dataset.training.Waiting')
|
||||
};
|
||||
return {
|
||||
colorSchema: 'red',
|
||||
tip: t('common:core.dataset.training.Full')
|
||||
};
|
||||
};
|
||||
const qaTrainingMap = countLLMMap(qaTrainingCount);
|
||||
const autoTrainingMap = countLLMMap(autoTrainingCount);
|
||||
const imageTrainingMap = countLLMMap(imageTrainingCount);
|
||||
|
||||
return {
|
||||
vectorTrainingMap,
|
||||
qaTrainingMap,
|
||||
autoTrainingMap,
|
||||
imageTrainingMap
|
||||
};
|
||||
}, [qaTrainingCount, autoTrainingCount, imageTrainingCount, vectorTrainingCount, t]);
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
@@ -172,12 +234,38 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
)}
|
||||
<Box mb={3}>
|
||||
<Box fontSize={'sm'} pb={1}>
|
||||
{t('common:core.dataset.training.Agent queue')}({agentTrainingMap.tip})
|
||||
{t('common:core.dataset.training.Agent queue')}({qaTrainingMap.tip})
|
||||
</Box>
|
||||
<Progress
|
||||
value={100}
|
||||
size={'xs'}
|
||||
colorScheme={agentTrainingMap.colorSchema}
|
||||
colorScheme={qaTrainingMap.colorSchema}
|
||||
borderRadius={'md'}
|
||||
isAnimated
|
||||
hasStripe
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Box fontSize={'sm'} pb={1}>
|
||||
{t('dataset:auto_training_queue')}({autoTrainingMap.tip})
|
||||
</Box>
|
||||
<Progress
|
||||
value={100}
|
||||
size={'xs'}
|
||||
colorScheme={autoTrainingMap.colorSchema}
|
||||
borderRadius={'md'}
|
||||
isAnimated
|
||||
hasStripe
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Box fontSize={'sm'} pb={1}>
|
||||
{t('dataset:image_training_queue')}({imageTrainingMap.tip})
|
||||
</Box>
|
||||
<Progress
|
||||
value={100}
|
||||
size={'xs'}
|
||||
colorScheme={imageTrainingMap.colorSchema}
|
||||
borderRadius={'md'}
|
||||
isAnimated
|
||||
hasStripe
|
||||
|
||||
@@ -239,8 +239,8 @@ function List() {
|
||||
<Box
|
||||
flex={1}
|
||||
className={'textEllipsis3'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
py={3}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { checkIsWecomTerminal } from '@fastgpt/global/support/user/login/constan
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { GET, POST } from '@/web/common/api/request';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
@@ -48,8 +49,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
{
|
||||
label: feConfigs.sso.title || 'Unknown',
|
||||
provider: OAuthEnum.sso,
|
||||
icon: feConfigs.sso.icon,
|
||||
redirectUrl: `${feConfigs.sso.url}/login/oauth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state.current}`
|
||||
icon: feConfigs.sso.icon
|
||||
}
|
||||
]
|
||||
: []),
|
||||
@@ -63,16 +63,6 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.oauth?.dingtalk
|
||||
? [
|
||||
{
|
||||
label: t('user:login.Dingtalk'),
|
||||
provider: OAuthEnum.dingtalk,
|
||||
icon: 'common/dingtalkFill',
|
||||
redirectUrl: `https://login.dingtalk.com/oauth2/auth?client_id=${feConfigs?.oauth?.dingtalk}&redirect_uri=${redirectUri}&state=${state.current}&response_type=code&scope=openid&prompt=consent`
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.oauth?.google
|
||||
? [
|
||||
{
|
||||
@@ -104,18 +94,6 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.oauth?.wecom
|
||||
? [
|
||||
{
|
||||
label: t('login:wecom'),
|
||||
provider: OAuthEnum.wecom,
|
||||
icon: 'common/wecom',
|
||||
redirectUrl: isWecomWorkTerminal
|
||||
? `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${feConfigs?.oauth?.wecom?.corpid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_privateinfo&agentid=${feConfigs?.oauth?.wecom?.agentid}&state=${state.current}#wechat_redirect`
|
||||
: `https://login.work.weixin.qq.com/wwlogin/sso/login?login_type=CorpApp&appid=${feConfigs?.oauth?.wecom?.corpid}&agentid=${feConfigs?.oauth?.wecom?.agentid}&redirect_uri=${redirectUri}&state=${state.current}`
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(pageType !== LoginPageTypeEnum.passwordLogin
|
||||
? [
|
||||
{
|
||||
@@ -135,6 +113,19 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
|
||||
const onClickOauth = useCallback(
|
||||
async (item: OAuthItem) => {
|
||||
if (item.provider === OAuthEnum.sso) {
|
||||
const redirectUrl = await POST<string>('/proApi/support/user/account/login/getAuthURL', {
|
||||
redirectUri,
|
||||
isWecomWorkTerminal
|
||||
});
|
||||
setLoginStore({
|
||||
provider: item.provider as OAuthEnum,
|
||||
lastRoute,
|
||||
state: state.current
|
||||
});
|
||||
router.replace(redirectUrl, '_self');
|
||||
return;
|
||||
}
|
||||
if (item.redirectUrl) {
|
||||
setLoginStore({
|
||||
provider: item.provider as OAuthEnum,
|
||||
@@ -152,14 +143,8 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
useEffect(() => {
|
||||
if (rootLogin) return;
|
||||
const sso = oAuthList.find((item) => item.provider === OAuthEnum.sso);
|
||||
const wecom = oAuthList.find((item) => item.provider === OAuthEnum.wecom);
|
||||
if (feConfigs?.sso?.autoLogin && sso) {
|
||||
// sso auto
|
||||
onClickOauth(sso);
|
||||
} else if (isWecomWorkTerminal && wecom) {
|
||||
// Auto wecom login
|
||||
onClickOauth(wecom);
|
||||
}
|
||||
// sso auto login
|
||||
if (sso && (feConfigs?.sso?.autoLogin || isWecomWorkTerminal)) onClickOauth(sso);
|
||||
}, [rootLogin, feConfigs?.sso?.autoLogin, isWecomWorkTerminal, onClickOauth]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -294,7 +294,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
title={t('account_info:click_modify_nickname')}
|
||||
borderColor={'transparent'}
|
||||
transform={'translateX(-11px)'}
|
||||
maxLength={20}
|
||||
maxLength={100}
|
||||
onBlur={async (e) => {
|
||||
const val = e.target.value;
|
||||
if (val === userInfo?.team?.memberName) return;
|
||||
|
||||
@@ -48,7 +48,7 @@ const Team = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v);
|
||||
const { setEditTeamData, teamSize } = useContextSelector(TeamContext, (v) => v);
|
||||
|
||||
const Tabs = useMemo(
|
||||
() => (
|
||||
@@ -75,7 +75,7 @@ const Team = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountContainer isLoading={isLoading}>
|
||||
<AccountContainer>
|
||||
<Flex h={'100%'} flexDirection={'column'}>
|
||||
{/* header */}
|
||||
<Flex
|
||||
|
||||
@@ -35,11 +35,17 @@ async function handler(
|
||||
|
||||
if (!modelData) return Promise.reject('Model not found');
|
||||
|
||||
if (channelId) {
|
||||
delete modelData.requestUrl;
|
||||
delete modelData.requestAuth;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = channelId
|
||||
? {
|
||||
'Aiproxy-Channel': String(channelId)
|
||||
}
|
||||
: {};
|
||||
addLog.debug(`Test model`, modelData);
|
||||
|
||||
if (modelData.type === 'llm') {
|
||||
return testLLMModel(modelData, headers);
|
||||
@@ -63,10 +69,6 @@ async function handler(
|
||||
export default NextAPI(handler);
|
||||
|
||||
const testLLMModel = async (model: LLMModelItemType, headers: Record<string, string>) => {
|
||||
const ai = getAIApi({
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const requestBody = llmCompletionsBodyFormat(
|
||||
{
|
||||
model: model.model,
|
||||
@@ -75,6 +77,7 @@ const testLLMModel = async (model: LLMModelItemType, headers: Record<string, str
|
||||
},
|
||||
model
|
||||
);
|
||||
|
||||
const { response, isStreamResponse } = await createChatCompletion({
|
||||
body: requestBody,
|
||||
options: {
|
||||
@@ -144,7 +147,7 @@ const testTTSModel = async (model: TTSModelType, headers: Record<string, string>
|
||||
const testSTTModel = async (model: STTModelType, headers: Record<string, string>) => {
|
||||
const path = isProduction ? '/app/data/test.mp3' : 'data/test.mp3';
|
||||
const { text } = await aiTranscriptions({
|
||||
model: model.model,
|
||||
model,
|
||||
fileStream: fs.createReadStream(path),
|
||||
headers
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
return Promise.reject(DatasetErrEnum.sameApiCollection);
|
||||
}
|
||||
|
||||
const content = await readApiServerFileContent({
|
||||
const { title, rawText } = await readApiServerFileContent({
|
||||
apiServer,
|
||||
feishuServer,
|
||||
yuqueServer,
|
||||
@@ -53,14 +53,14 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText: content,
|
||||
rawText,
|
||||
relatedId: apiFileId,
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: DatasetCollectionTypeEnum.apiFile,
|
||||
name,
|
||||
name: title || name,
|
||||
apiFileId,
|
||||
metadata: {
|
||||
relatedImgId: apiFileId
|
||||
|
||||
@@ -2,8 +2,7 @@ import { reTrainingDatasetFileCollectionParams } from '@fastgpt/global/core/data
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
DatasetSourceReadTypeEnum,
|
||||
TrainingModeEnum
|
||||
DatasetSourceReadTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { hashStr } from '@fastgpt/global/common/string/tools';
|
||||
@@ -77,7 +76,7 @@ async function handler(
|
||||
return Promise.reject(i18nT('dataset:collection_not_support_retraining'));
|
||||
})();
|
||||
|
||||
const rawText = await readDatasetSourceRawText({
|
||||
const { title, rawText } = await readDatasetSourceRawText({
|
||||
teamId,
|
||||
tmbId,
|
||||
customPdfParse,
|
||||
@@ -100,7 +99,7 @@ async function handler(
|
||||
teamId: collection.teamId,
|
||||
tmbId: collection.tmbId,
|
||||
datasetId: collection.dataset._id,
|
||||
name: collection.name,
|
||||
name: title || collection.name,
|
||||
type: collection.type,
|
||||
|
||||
customPdfParse,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { countPromptTokens } from '@fastgpt/service/common/string/tiktoken/index';
|
||||
import { getEmbeddingModel } from '@fastgpt/service/core/ai/model';
|
||||
import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model';
|
||||
import { hasSameValue } from '@/service/core/dataset/data/utils';
|
||||
import { insertData2Dataset } from '@/service/core/dataset/data/controller';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
@@ -16,6 +16,7 @@ import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { getLLMMaxChunkSize } from '@fastgpt/global/core/dataset/training/utils';
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { collectionId, q, a, indexes } = req.body as InsertOneDatasetDataProps;
|
||||
@@ -45,7 +46,7 @@ async function handler(req: NextApiRequest) {
|
||||
// auth collection and get dataset
|
||||
const [
|
||||
{
|
||||
dataset: { _id: datasetId, vectorModel }
|
||||
dataset: { _id: datasetId, vectorModel, agentModel }
|
||||
}
|
||||
] = await Promise.all([getCollectionWithDataset(collectionId)]);
|
||||
|
||||
@@ -60,9 +61,11 @@ async function handler(req: NextApiRequest) {
|
||||
// token check
|
||||
const token = await countPromptTokens(formatQ + formatA, '');
|
||||
const vectorModelData = getEmbeddingModel(vectorModel);
|
||||
const llmModelData = getLLMModel(agentModel);
|
||||
const maxChunkSize = getLLMMaxChunkSize(llmModelData);
|
||||
|
||||
if (token > vectorModelData.maxToken) {
|
||||
return Promise.reject('Q Over Tokens');
|
||||
if (token > maxChunkSize) {
|
||||
return Promise.reject(`Content over max chunk size: ${maxChunkSize}`);
|
||||
}
|
||||
|
||||
// Duplicate data check
|
||||
@@ -82,7 +85,7 @@ async function handler(req: NextApiRequest) {
|
||||
q: formatQ,
|
||||
a: formatA,
|
||||
chunkIndex: 0,
|
||||
model: vectorModelData.model,
|
||||
embeddingModel: vectorModelData.model,
|
||||
indexes: formatIndexes
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import {
|
||||
ChunkSettingModeEnum,
|
||||
DataChunkSplitModeEnum,
|
||||
DatasetCollectionDataProcessModeEnum,
|
||||
DatasetSourceReadTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { rawText2Chunks, readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
@@ -8,100 +13,130 @@ import {
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { authCollectionFile } from '@fastgpt/service/support/permission/auth/file';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import {
|
||||
computeChunkSize,
|
||||
computeChunkSplitter,
|
||||
getLLMMaxChunkSize
|
||||
} from '@fastgpt/global/core/dataset/training/utils';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { getLLMModel } from '@fastgpt/service/core/ai/model';
|
||||
|
||||
export type PostPreviewFilesChunksProps = {
|
||||
datasetId: string;
|
||||
type: DatasetSourceReadTypeEnum;
|
||||
sourceId: string;
|
||||
|
||||
chunkSize: number;
|
||||
overlapRatio: number;
|
||||
customSplitChar?: string;
|
||||
customPdfParse?: boolean;
|
||||
|
||||
trainingType: DatasetCollectionDataProcessModeEnum;
|
||||
|
||||
// Chunk settings
|
||||
chunkSettingMode: ChunkSettingModeEnum;
|
||||
chunkSplitMode: DataChunkSplitModeEnum;
|
||||
chunkSize: number;
|
||||
chunkSplitter?: string;
|
||||
overlapRatio: number;
|
||||
|
||||
// Read params
|
||||
selector?: string;
|
||||
isQAImport?: boolean;
|
||||
externalFileId?: string;
|
||||
};
|
||||
export type PreviewChunksResponse = {
|
||||
q: string;
|
||||
a: string;
|
||||
}[];
|
||||
chunks: {
|
||||
q: string;
|
||||
a: string;
|
||||
}[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<PostPreviewFilesChunksProps>
|
||||
): Promise<PreviewChunksResponse> {
|
||||
const {
|
||||
let {
|
||||
type,
|
||||
sourceId,
|
||||
customPdfParse = false,
|
||||
|
||||
trainingType,
|
||||
chunkSettingMode,
|
||||
chunkSplitMode,
|
||||
chunkSize,
|
||||
customSplitChar,
|
||||
chunkSplitter,
|
||||
|
||||
overlapRatio,
|
||||
selector,
|
||||
isQAImport,
|
||||
datasetId,
|
||||
externalFileId,
|
||||
customPdfParse = false
|
||||
externalFileId
|
||||
} = req.body;
|
||||
|
||||
if (!sourceId) {
|
||||
throw new Error('sourceId is empty');
|
||||
}
|
||||
if (chunkSize > 30000) {
|
||||
throw new Error('chunkSize is too large, should be less than 30000');
|
||||
|
||||
const fileAuthRes =
|
||||
type === DatasetSourceReadTypeEnum.fileLocal
|
||||
? await authCollectionFile({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
fileId: sourceId,
|
||||
per: OwnerPermissionVal
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const { dataset, teamId, tmbId } = await authDataset({
|
||||
req,
|
||||
authApiKey: true,
|
||||
authToken: true,
|
||||
datasetId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
if (fileAuthRes && String(fileAuthRes.tmbId) !== String(tmbId) && !fileAuthRes.isRoot) {
|
||||
return Promise.reject(CommonErrEnum.unAuthFile);
|
||||
}
|
||||
|
||||
const { teamId, tmbId, apiServer, feishuServer, yuqueServer } = await (async () => {
|
||||
if (type === DatasetSourceReadTypeEnum.fileLocal) {
|
||||
const res = await authCollectionFile({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
fileId: sourceId,
|
||||
per: OwnerPermissionVal
|
||||
});
|
||||
return {
|
||||
teamId: res.teamId,
|
||||
tmbId: res.tmbId
|
||||
};
|
||||
}
|
||||
const { dataset, teamId, tmbId } = await authDataset({
|
||||
req,
|
||||
authApiKey: true,
|
||||
authToken: true,
|
||||
datasetId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
return {
|
||||
teamId,
|
||||
tmbId,
|
||||
apiServer: dataset.apiServer,
|
||||
feishuServer: dataset.feishuServer,
|
||||
yuqueServer: dataset.yuqueServer
|
||||
};
|
||||
})();
|
||||
chunkSize = computeChunkSize({
|
||||
trainingType,
|
||||
chunkSettingMode,
|
||||
chunkSplitMode,
|
||||
chunkSize,
|
||||
llmModel: getLLMModel(dataset.agentModel)
|
||||
});
|
||||
|
||||
const rawText = await readDatasetSourceRawText({
|
||||
chunkSplitter = computeChunkSplitter({
|
||||
chunkSettingMode,
|
||||
chunkSplitMode,
|
||||
chunkSplitter
|
||||
});
|
||||
|
||||
const { rawText } = await readDatasetSourceRawText({
|
||||
teamId,
|
||||
tmbId,
|
||||
type,
|
||||
sourceId,
|
||||
selector,
|
||||
isQAImport,
|
||||
apiServer,
|
||||
feishuServer,
|
||||
yuqueServer,
|
||||
apiServer: dataset.apiServer,
|
||||
feishuServer: dataset.feishuServer,
|
||||
yuqueServer: dataset.yuqueServer,
|
||||
externalFileId,
|
||||
customPdfParse
|
||||
});
|
||||
|
||||
return rawText2Chunks({
|
||||
const chunks = rawText2Chunks({
|
||||
rawText,
|
||||
chunkLen: chunkSize,
|
||||
chunkSize,
|
||||
maxSize: getLLMMaxChunkSize(getLLMModel(dataset.agentModel)),
|
||||
overlapRatio,
|
||||
customReg: customSplitChar ? [customSplitChar] : [],
|
||||
customReg: chunkSplitter ? [chunkSplitter] : [],
|
||||
isQAImport: isQAImport
|
||||
}).slice(0, 10);
|
||||
});
|
||||
return {
|
||||
chunks: chunks.slice(0, 10),
|
||||
total: chunks.length
|
||||
};
|
||||
}
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { GetTrainingQueueProps } from '@/global/core/dataset/api';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
export type GetQueueLenResponse = {
|
||||
vectorTrainingCount: number;
|
||||
qaTrainingCount: number;
|
||||
autoTrainingCount: number;
|
||||
imageTrainingCount: number;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
await authCert({ req, authToken: true });
|
||||
const { vectorModel, agentModel } = req.query as GetTrainingQueueProps;
|
||||
|
||||
// get queue data
|
||||
// 分别统计 model = vectorModel和agentModel的数量
|
||||
const data = await MongoDatasetTraining.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
lockTime: { $lt: new Date('2040/1/1') },
|
||||
$or: [{ model: { $eq: vectorModel } }, { model: { $eq: agentModel } }]
|
||||
lockTime: { $lt: new Date('2040/1/1') }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$model',
|
||||
_id: '$mode',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
@@ -31,12 +35,16 @@ async function handler(req: NextApiRequest) {
|
||||
}
|
||||
);
|
||||
|
||||
const vectorTrainingCount = data.find((item) => item._id === vectorModel)?.count || 0;
|
||||
const agentTrainingCount = data.find((item) => item._id === agentModel)?.count || 0;
|
||||
const vectorTrainingCount = data.find((item) => item._id === TrainingModeEnum.chunk)?.count || 0;
|
||||
const qaTrainingCount = data.find((item) => item._id === TrainingModeEnum.qa)?.count || 0;
|
||||
const autoTrainingCount = data.find((item) => item._id === TrainingModeEnum.auto)?.count || 0;
|
||||
const imageTrainingCount = data.find((item) => item._id === TrainingModeEnum.image)?.count || 0;
|
||||
|
||||
return {
|
||||
vectorTrainingCount,
|
||||
agentTrainingCount
|
||||
qaTrainingCount,
|
||||
autoTrainingCount,
|
||||
imageTrainingCount
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
// }
|
||||
|
||||
const result = await aiTranscriptions({
|
||||
model: getDefaultSTTModel().model,
|
||||
model: getDefaultSTTModel(),
|
||||
fileStream: fs.createReadStream(file.path)
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const provider = () => {
|
||||
const { initd, loginStore, setLoginStore } = useSystemStore();
|
||||
const { setUserInfo } = useUserStore();
|
||||
const router = useRouter();
|
||||
const { code, state, error } = router.query as { code: string; state: string; error?: string };
|
||||
const { state, error, ...props } = router.query as Record<string, string>;
|
||||
const { toast } = useToast();
|
||||
|
||||
const loginSuccess = useCallback(
|
||||
@@ -31,12 +31,12 @@ const provider = () => {
|
||||
[setUserInfo, router, loginStore?.lastRoute]
|
||||
);
|
||||
|
||||
const authCode = useCallback(
|
||||
async (code: string) => {
|
||||
const authProps = useCallback(
|
||||
async (props: Record<string, string>) => {
|
||||
try {
|
||||
const res = await oauthLogin({
|
||||
type: loginStore?.provider || OAuthEnum.sso,
|
||||
code,
|
||||
props,
|
||||
callbackUrl: `${location.origin}/login/provider`,
|
||||
inviterId: localStorage.getItem('inviterId') || undefined,
|
||||
bd_vid: sessionStorage.getItem('bd_vid') || undefined,
|
||||
@@ -86,8 +86,8 @@ const provider = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('SSO', { initd, loginStore, code, state });
|
||||
if (!code || !initd) return;
|
||||
console.log('SSO', { initd, loginStore, props, state });
|
||||
if (!props || !initd) return;
|
||||
|
||||
if (isOauthLogging) return;
|
||||
|
||||
@@ -107,10 +107,10 @@ const provider = () => {
|
||||
}, 1000);
|
||||
return;
|
||||
} else {
|
||||
authCode(code);
|
||||
authProps(props);
|
||||
}
|
||||
})();
|
||||
}, [initd, authCode, code, error, loginStore, loginStore?.state, router, state, t, toast]);
|
||||
}, [initd, authProps, error, loginStore, loginStore?.state, router, state, t, toast, props]);
|
||||
|
||||
return <Loading />;
|
||||
};
|
||||
|
||||
@@ -5,25 +5,67 @@ import {
|
||||
UpdateDatasetDataProps
|
||||
} from '@fastgpt/global/core/dataset/controller';
|
||||
import { insertDatasetDataVector } from '@fastgpt/service/common/vectorStore/controller';
|
||||
import { getDefaultIndex } from '@fastgpt/global/core/dataset/utils';
|
||||
import { jiebaSplit } from '@fastgpt/service/common/string/jieba/index';
|
||||
import { deleteDatasetDataVector } from '@fastgpt/service/common/vectorStore/controller';
|
||||
import { DatasetDataIndexItemType, DatasetDataItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { getEmbeddingModel } from '@fastgpt/service/core/ai/model';
|
||||
import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
|
||||
import { DatasetDataIndexTypeEnum } from '@fastgpt/global/core/dataset/data/constants';
|
||||
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
|
||||
import { countPromptTokens } from '@fastgpt/service/common/string/tiktoken';
|
||||
|
||||
const formatIndexes = ({
|
||||
const formatIndexes = async ({
|
||||
indexes,
|
||||
q,
|
||||
a = ''
|
||||
a = '',
|
||||
indexSize,
|
||||
maxIndexSize
|
||||
}: {
|
||||
indexes?: (Omit<DatasetDataIndexItemType, 'dataId'> & { dataId?: string })[];
|
||||
q: string;
|
||||
a?: string;
|
||||
}) => {
|
||||
indexSize: number;
|
||||
maxIndexSize: number;
|
||||
}): Promise<
|
||||
{
|
||||
type: `${DatasetDataIndexTypeEnum}`;
|
||||
text: string;
|
||||
dataId?: string;
|
||||
}[]
|
||||
> => {
|
||||
/* get dataset data default index */
|
||||
const getDefaultIndex = ({
|
||||
q = '',
|
||||
a,
|
||||
indexSize
|
||||
}: {
|
||||
q?: string;
|
||||
a?: string;
|
||||
indexSize: number;
|
||||
}) => {
|
||||
const qChunks = splitText2Chunks({
|
||||
text: q,
|
||||
chunkSize: indexSize,
|
||||
maxSize: maxIndexSize
|
||||
}).chunks;
|
||||
const aChunks = a
|
||||
? splitText2Chunks({ text: a, chunkSize: indexSize, maxSize: maxIndexSize }).chunks
|
||||
: [];
|
||||
|
||||
return [
|
||||
...qChunks.map((text) => ({
|
||||
text,
|
||||
type: DatasetDataIndexTypeEnum.default
|
||||
})),
|
||||
...aChunks.map((text) => ({
|
||||
text,
|
||||
type: DatasetDataIndexTypeEnum.default
|
||||
}))
|
||||
];
|
||||
};
|
||||
|
||||
indexes = indexes || [];
|
||||
// If index not type, set it to custom
|
||||
indexes = indexes
|
||||
@@ -35,7 +77,7 @@ const formatIndexes = ({
|
||||
.filter((item) => !!item.text.trim());
|
||||
|
||||
// Recompute default indexes, Merge ids of the same index, reduce the number of rebuilds
|
||||
const defaultIndexes = getDefaultIndex({ q, a });
|
||||
const defaultIndexes = getDefaultIndex({ q, a, indexSize });
|
||||
const concatDefaultIndexes = defaultIndexes.map((item) => {
|
||||
const oldIndex = indexes!.find((index) => index.text === item.text);
|
||||
if (oldIndex) {
|
||||
@@ -56,11 +98,28 @@ const formatIndexes = ({
|
||||
(item, index, self) => index === self.findIndex((t) => t.text === item.text)
|
||||
);
|
||||
|
||||
return indexes.map((index) => ({
|
||||
type: index.type,
|
||||
text: index.text,
|
||||
dataId: index.dataId
|
||||
}));
|
||||
const chekcIndexes = (
|
||||
await Promise.all(
|
||||
indexes.map(async (item) => {
|
||||
// If oversize tokens, split it
|
||||
const tokens = await countPromptTokens(item.text);
|
||||
if (tokens > indexSize) {
|
||||
const splitText = splitText2Chunks({
|
||||
text: item.text,
|
||||
chunkSize: 512,
|
||||
maxSize: maxIndexSize
|
||||
}).chunks;
|
||||
return splitText.map((text) => ({
|
||||
text,
|
||||
type: item.type
|
||||
}));
|
||||
}
|
||||
return item;
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
|
||||
return chekcIndexes;
|
||||
};
|
||||
/* insert data.
|
||||
* 1. create data id
|
||||
@@ -75,30 +134,41 @@ export async function insertData2Dataset({
|
||||
q,
|
||||
a = '',
|
||||
chunkIndex = 0,
|
||||
indexSize = 512,
|
||||
indexes,
|
||||
model,
|
||||
embeddingModel,
|
||||
session
|
||||
}: CreateDatasetDataProps & {
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
indexSize?: number;
|
||||
session?: ClientSession;
|
||||
}) {
|
||||
if (!q || !datasetId || !collectionId || !model) {
|
||||
return Promise.reject('q, datasetId, collectionId, model is required');
|
||||
if (!q || !datasetId || !collectionId || !embeddingModel) {
|
||||
return Promise.reject('q, datasetId, collectionId, embeddingModel is required');
|
||||
}
|
||||
if (String(teamId) === String(tmbId)) {
|
||||
return Promise.reject("teamId and tmbId can't be the same");
|
||||
}
|
||||
|
||||
const embModel = getEmbeddingModel(embeddingModel);
|
||||
indexSize = Math.min(embModel.maxToken, indexSize);
|
||||
|
||||
// 1. Get vector indexes and insert
|
||||
// Empty indexes check, if empty, create default index
|
||||
const newIndexes = formatIndexes({ indexes, q, a });
|
||||
const newIndexes = await formatIndexes({
|
||||
indexes,
|
||||
q,
|
||||
a,
|
||||
indexSize,
|
||||
maxIndexSize: embModel.maxToken
|
||||
});
|
||||
|
||||
// insert to vector store
|
||||
const result = await Promise.all(
|
||||
newIndexes.map(async (item) => {
|
||||
const result = await insertDatasetDataVector({
|
||||
query: item.text,
|
||||
model: getEmbeddingModel(model),
|
||||
model: embModel,
|
||||
teamId,
|
||||
datasetId,
|
||||
collectionId
|
||||
@@ -163,8 +233,9 @@ export async function updateData2Dataset({
|
||||
q = '',
|
||||
a,
|
||||
indexes,
|
||||
model
|
||||
}: UpdateDatasetDataProps & { model: string }) {
|
||||
model,
|
||||
indexSize = 512
|
||||
}: UpdateDatasetDataProps & { model: string; indexSize?: number }) {
|
||||
if (!Array.isArray(indexes)) {
|
||||
return Promise.reject('indexes is required');
|
||||
}
|
||||
@@ -174,7 +245,13 @@ export async function updateData2Dataset({
|
||||
if (!mongoData) return Promise.reject('core.dataset.error.Data not found');
|
||||
|
||||
// 2. Compute indexes
|
||||
const formatIndexesResult = formatIndexes({ indexes, q, a });
|
||||
const formatIndexesResult = await formatIndexes({
|
||||
indexes,
|
||||
q,
|
||||
a,
|
||||
indexSize,
|
||||
maxIndexSize: getEmbeddingModel(model).maxToken
|
||||
});
|
||||
|
||||
// 3. Patch indexes, create, update, delete
|
||||
const patchResult: PatchIndexesProps[] = [];
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
llmCompletionsBodyFormat,
|
||||
llmStreamResponseToAnswerText
|
||||
} from '@fastgpt/service/core/ai/utils';
|
||||
import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
|
||||
import {
|
||||
chunkAutoChunkSize,
|
||||
getLLMMaxChunkSize
|
||||
} from '@fastgpt/global/core/dataset/training/utils';
|
||||
|
||||
const reduceQueue = () => {
|
||||
global.qaQueueLen = global.qaQueueLen > 0 ? global.qaQueueLen - 1 : 0;
|
||||
@@ -129,7 +134,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
|
||||
});
|
||||
const answer = await llmStreamResponseToAnswerText(chatResponse);
|
||||
|
||||
const qaArr = formatSplitText(answer, text); // 格式化后的QA对
|
||||
const qaArr = formatSplitText({ answer, rawText: text, llmModel: modelData }); // 格式化后的QA对
|
||||
|
||||
addLog.info(`[QA Queue] Finish`, {
|
||||
time: Date.now() - startTime,
|
||||
@@ -180,10 +185,18 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
|
||||
}
|
||||
|
||||
// Format qa answer
|
||||
function formatSplitText(text: string, rawText: string) {
|
||||
text = text.replace(/\\n/g, '\n'); // 将换行符替换为空格
|
||||
function formatSplitText({
|
||||
answer,
|
||||
rawText,
|
||||
llmModel
|
||||
}: {
|
||||
answer: string;
|
||||
rawText: string;
|
||||
llmModel: LLMModelItemType;
|
||||
}) {
|
||||
answer = answer.replace(/\\n/g, '\n'); // 将换行符替换为空格
|
||||
const regex = /Q\d+:(\s*)(.*)(\s*)A\d+:(\s*)([\s\S]*?)(?=Q\d|$)/g; // 匹配Q和A的正则表达式
|
||||
const matches = text.matchAll(regex); // 获取所有匹配到的结果
|
||||
const matches = answer.matchAll(regex); // 获取所有匹配到的结果
|
||||
|
||||
const result: PushDatasetDataChunkProps[] = []; // 存储最终的结果
|
||||
for (const match of matches) {
|
||||
@@ -199,7 +212,11 @@ function formatSplitText(text: string, rawText: string) {
|
||||
|
||||
// empty result. direct split chunk
|
||||
if (result.length === 0) {
|
||||
const { chunks } = splitText2Chunks({ text: rawText, chunkLen: 512 });
|
||||
const { chunks } = splitText2Chunks({
|
||||
text: rawText,
|
||||
chunkSize: chunkAutoChunkSize,
|
||||
maxSize: getLLMMaxChunkSize(llmModel)
|
||||
});
|
||||
chunks.forEach((chunk) => {
|
||||
result.push({
|
||||
q: chunk,
|
||||
|
||||
@@ -245,7 +245,7 @@ const insertData = async ({
|
||||
a: trainingData.a,
|
||||
chunkIndex: trainingData.chunkIndex,
|
||||
indexes: trainingData.indexes,
|
||||
model: trainingData.model,
|
||||
embeddingModel: trainingData.model,
|
||||
session
|
||||
});
|
||||
// delete data from training
|
||||
|
||||
@@ -4,6 +4,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
export const useEditTitle = ({
|
||||
title,
|
||||
@@ -89,11 +90,7 @@ export const useEditTitle = ({
|
||||
return (
|
||||
<MyModal isOpen={isOpen} onClose={onClose} iconSrc={iconSrc} title={title} maxW={'500px'}>
|
||||
<ModalBody>
|
||||
{!!tip && (
|
||||
<Box mb={2} color={'myGray.500'} fontSize={'sm'}>
|
||||
{tip}
|
||||
</Box>
|
||||
)}
|
||||
{!!tip && <FormLabel mb={2}>{tip}</FormLabel>}
|
||||
|
||||
<Input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
import axios from 'axios';
|
||||
import { OAuthEnum } from '@fastgpt/global/support/user/constant';
|
||||
import type {
|
||||
|
||||
@@ -166,6 +166,7 @@ export const getChannelLog = (params: {
|
||||
logs: ChannelLogListItemType[];
|
||||
total: number;
|
||||
}>(`/logs/search`, {
|
||||
result_only: true,
|
||||
request_id: params.request_id,
|
||||
channel: params.channel,
|
||||
model_name: params.model_name,
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { DatasetSearchModule } from '@fastgpt/global/core/workflow/template/system/datasetSearch';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import {
|
||||
Input_Template_File_Link_Prompt,
|
||||
Input_Template_File_Link,
|
||||
Input_Template_UserChatInput
|
||||
} from '@fastgpt/global/core/workflow/template/input';
|
||||
import { workflowStartNodeId } from './constants';
|
||||
@@ -175,7 +175,7 @@ export function form2AppWorkflow(
|
||||
value: selectedDatasets?.length > 0 ? [datasetNodeId, 'quoteQA'] : undefined
|
||||
},
|
||||
{
|
||||
...Input_Template_File_Link_Prompt,
|
||||
...Input_Template_File_Link,
|
||||
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
|
||||
},
|
||||
{
|
||||
@@ -502,7 +502,7 @@ export function form2AppWorkflow(
|
||||
value: formData.aiSettings.maxHistories
|
||||
},
|
||||
{
|
||||
...Input_Template_File_Link_Prompt,
|
||||
...Input_Template_File_Link,
|
||||
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -74,6 +74,8 @@ type ChatItemContextType = {
|
||||
|
||||
quoteData?: QuoteDataType;
|
||||
setQuoteData: React.Dispatch<React.SetStateAction<QuoteDataType | undefined>>;
|
||||
isVariableVisible: boolean;
|
||||
setIsVariableVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
} & ContextProps;
|
||||
|
||||
export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
@@ -97,6 +99,10 @@ export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
quoteData: undefined,
|
||||
setQuoteData: function (value: React.SetStateAction<QuoteDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isVariableVisible: true,
|
||||
setIsVariableVisible: function (value: React.SetStateAction<boolean>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,6 +122,8 @@ const ChatItemContextProvider = ({
|
||||
const ChatBoxRef = useRef<ChatComponentRef>(null);
|
||||
const variablesForm = useForm<ChatBoxInputFormType>();
|
||||
const [quoteData, setQuoteData] = useState<QuoteDataType>();
|
||||
const [isVariableVisible, setIsVariableVisible] = useState(true);
|
||||
|
||||
const [chatBoxData, setChatBoxData] = useState<ChatBoxDataType>({
|
||||
...defaultChatData
|
||||
});
|
||||
@@ -172,7 +180,9 @@ const ChatItemContextProvider = ({
|
||||
showNodeStatus,
|
||||
|
||||
quoteData,
|
||||
setQuoteData
|
||||
setQuoteData,
|
||||
isVariableVisible,
|
||||
setIsVariableVisible
|
||||
};
|
||||
}, [
|
||||
chatBoxData,
|
||||
@@ -187,7 +197,9 @@ const ChatItemContextProvider = ({
|
||||
// isShowFullText,
|
||||
showNodeStatus,
|
||||
quoteData,
|
||||
setQuoteData
|
||||
setQuoteData,
|
||||
isVariableVisible,
|
||||
setIsVariableVisible
|
||||
]);
|
||||
|
||||
return <ChatItemContext.Provider value={contextValue}>{children}</ChatItemContext.Provider>;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, createJSONStorage, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
|
||||
type State = {
|
||||
localUId?: string;
|
||||
|
||||
@@ -27,12 +27,7 @@ import type {
|
||||
TextCreateDatasetCollectionParams,
|
||||
UpdateDatasetCollectionTagParams
|
||||
} from '@fastgpt/global/core/dataset/api.d';
|
||||
import type {
|
||||
GetTrainingQueueProps,
|
||||
GetTrainingQueueResponse,
|
||||
SearchTestProps,
|
||||
SearchTestResponse
|
||||
} from '@/global/core/dataset/api.d';
|
||||
import type { SearchTestProps, SearchTestResponse } from '@/global/core/dataset/api.d';
|
||||
import type { CreateDatasetParams, InsertOneDatasetDataProps } from '@/global/core/dataset/api.d';
|
||||
import type { DatasetCollectionItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { DatasetCollectionSyncResultEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
@@ -67,6 +62,7 @@ import type {
|
||||
} from '@/pages/api/core/dataset/apiDataset/listExistId';
|
||||
import type { GetQuoteDataResponse } from '@/pages/api/core/dataset/data/getQuoteData';
|
||||
import type { GetQuotePermissionResponse } from '@/pages/api/core/dataset/data/getPermission';
|
||||
import type { GetQueueLenResponse } from '@/pages/api/core/dataset/training/getQueueLen';
|
||||
|
||||
/* ======================== dataset ======================= */
|
||||
export const getDatasets = (data: GetDatasetListBody) =>
|
||||
@@ -215,8 +211,8 @@ export const postRebuildEmbedding = (data: rebuildEmbeddingBody) =>
|
||||
POST(`/core/dataset/training/rebuildEmbedding`, data);
|
||||
|
||||
/* get length of system training queue */
|
||||
export const getTrainingQueueLen = (data: GetTrainingQueueProps) =>
|
||||
GET<GetTrainingQueueResponse>(`/core/dataset/training/getQueueLen`, data);
|
||||
export const getTrainingQueueLen = () =>
|
||||
GET<GetQueueLenResponse>(`/core/dataset/training/getQueueLen`);
|
||||
export const getDatasetTrainingQueue = (datasetId: string) =>
|
||||
GET<getDatasetTrainingQueueResponse>(`/core/dataset/training/getDatasetTrainingQueue`, {
|
||||
datasetId
|
||||
|
||||
@@ -60,15 +60,11 @@ export const defaultCollectionDetail: DatasetCollectionItemType = {
|
||||
createTime: new Date(),
|
||||
trainingType: DatasetCollectionDataProcessModeEnum.chunk,
|
||||
chunkSize: 0,
|
||||
indexSize: 512,
|
||||
permission: new DatasetPermission(),
|
||||
indexAmount: 0
|
||||
};
|
||||
|
||||
export enum ChunkSettingModeEnum {
|
||||
auto = 'auto',
|
||||
custom = 'custom'
|
||||
}
|
||||
|
||||
export const datasetTypeCourseMap: Record<`${DatasetTypeEnum}`, string> = {
|
||||
[DatasetTypeEnum.folder]: '',
|
||||
[DatasetTypeEnum.dataset]: '',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react';
|
||||
import { Dispatch, ReactNode, SetStateAction, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
getDatasetCollectionTags,
|
||||
getDatasetPaths,
|
||||
getDatasetTrainingQueue,
|
||||
getTrainingQueueLen,
|
||||
postCreateDatasetCollectionTag,
|
||||
putDatasetById
|
||||
} from '../api';
|
||||
@@ -37,28 +36,13 @@ type DatasetPageContextType = {
|
||||
setSearchTagKey: Dispatch<SetStateAction<string>>;
|
||||
paths: ParentTreePathItemType[];
|
||||
refetchPaths: () => void;
|
||||
vectorTrainingMap: {
|
||||
colorSchema: string;
|
||||
tip: string;
|
||||
};
|
||||
agentTrainingMap: {
|
||||
colorSchema: string;
|
||||
tip: string;
|
||||
};
|
||||
|
||||
rebuildingCount: number;
|
||||
trainingCount: number;
|
||||
refetchDatasetTraining: () => void;
|
||||
};
|
||||
|
||||
export const DatasetPageContext = createContext<DatasetPageContextType>({
|
||||
vectorTrainingMap: {
|
||||
colorSchema: '',
|
||||
tip: ''
|
||||
},
|
||||
agentTrainingMap: {
|
||||
colorSchema: '',
|
||||
tip: ''
|
||||
},
|
||||
rebuildingCount: 0,
|
||||
trainingCount: 0,
|
||||
refetchDatasetTraining: function (): void {
|
||||
@@ -191,57 +175,6 @@ export const DatasetPageContextProvider = ({
|
||||
}
|
||||
);
|
||||
|
||||
// global queue
|
||||
const { data: { vectorTrainingCount = 0, agentTrainingCount = 0 } = {} } = useQuery(
|
||||
['getTrainingQueueLen'],
|
||||
() =>
|
||||
getTrainingQueueLen({
|
||||
vectorModel: datasetDetail.vectorModel.model,
|
||||
agentModel: datasetDetail.agentModel.model
|
||||
}),
|
||||
{
|
||||
refetchInterval: 10000
|
||||
}
|
||||
);
|
||||
const { vectorTrainingMap, agentTrainingMap } = useMemo(() => {
|
||||
const vectorTrainingMap = (() => {
|
||||
if (vectorTrainingCount < 1000)
|
||||
return {
|
||||
colorSchema: 'green',
|
||||
tip: t('common:core.dataset.training.Leisure')
|
||||
};
|
||||
if (vectorTrainingCount < 10000)
|
||||
return {
|
||||
colorSchema: 'yellow',
|
||||
tip: t('common:core.dataset.training.Waiting')
|
||||
};
|
||||
return {
|
||||
colorSchema: 'red',
|
||||
tip: t('common:core.dataset.training.Full')
|
||||
};
|
||||
})();
|
||||
const agentTrainingMap = (() => {
|
||||
if (agentTrainingCount < 100)
|
||||
return {
|
||||
colorSchema: 'green',
|
||||
tip: t('common:core.dataset.training.Leisure')
|
||||
};
|
||||
if (agentTrainingCount < 1000)
|
||||
return {
|
||||
colorSchema: 'yellow',
|
||||
tip: t('common:core.dataset.training.Waiting')
|
||||
};
|
||||
return {
|
||||
colorSchema: 'red',
|
||||
tip: t('common:core.dataset.training.Full')
|
||||
};
|
||||
})();
|
||||
return {
|
||||
vectorTrainingMap,
|
||||
agentTrainingMap
|
||||
};
|
||||
}, [agentTrainingCount, t, vectorTrainingCount]);
|
||||
|
||||
// training and rebuild queue
|
||||
const { data: { rebuildingCount = 0, trainingCount = 0 } = {}, refetch: refetchDatasetTraining } =
|
||||
useQuery(['getDatasetTrainingQueue'], () => getDatasetTrainingQueue(datasetId), {
|
||||
@@ -273,8 +206,7 @@ export const DatasetPageContextProvider = ({
|
||||
updateDataset,
|
||||
paths,
|
||||
refetchPaths,
|
||||
vectorTrainingMap,
|
||||
agentTrainingMap,
|
||||
|
||||
rebuildingCount,
|
||||
trainingCount,
|
||||
refetchDatasetTraining,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
|
||||
import { getDatasets } from '@/web/core/dataset/api';
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, devtools, immer } from '@fastgpt/web/common/zustand';
|
||||
|
||||
export type MarkDataStore = {
|
||||
dataId: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { create, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
|
||||
4
projects/app/src/web/core/dataset/type.d.ts
vendored
4
projects/app/src/web/core/dataset/type.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import type { PushDatasetDataChunkProps } from '@fastgpt/global/core/dataset/api';
|
||||
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { ChunkSettingModeEnum } from './constants';
|
||||
import { ChunkSettingModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
|
||||
|
||||
@@ -41,7 +41,7 @@ export type ImportSourceParamsType = UseFormReturn<
|
||||
{
|
||||
chunkSize: number;
|
||||
chunkOverlapRatio: number;
|
||||
customSplitChar: string;
|
||||
chunkSplitter: string;
|
||||
prompt: string;
|
||||
mode: TrainingModeEnum;
|
||||
way: ChunkSettingModeEnum;
|
||||
|
||||
@@ -96,7 +96,7 @@ export const getCaptchaPic = (username: string) =>
|
||||
captchaImage: string;
|
||||
}>('/proApi/support/user/account/captcha/getImgCaptcha', { username });
|
||||
|
||||
export const postSyncMembers = () => POST('/proApi/support/user/team/org/sync');
|
||||
export const postSyncMembers = () => POST('/proApi/support/user/sync');
|
||||
|
||||
export const GetSearchUserGroupOrg = (
|
||||
searchKey: string,
|
||||
@@ -105,6 +105,7 @@ export const GetSearchUserGroupOrg = (
|
||||
orgs?: boolean;
|
||||
groups?: boolean;
|
||||
}
|
||||
) => GET<SearchResult>('/proApi/support/user/search', { searchKey, ...options });
|
||||
) =>
|
||||
GET<SearchResult>('/proApi/support/user/search', { searchKey, ...options }, { maxQuantity: 1 });
|
||||
|
||||
export const ExportMembers = () => GET<{ csv: string }>('/proApi/support/user/team/member/export');
|
||||
|
||||
@@ -21,7 +21,6 @@ import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fe
|
||||
import type {
|
||||
InvitationInfoType,
|
||||
InvitationLinkCreateType,
|
||||
InvitationLinkUpdateType,
|
||||
InvitationType
|
||||
} from '@fastgpt/service/support/user/team/invitationLink/type';
|
||||
|
||||
@@ -35,8 +34,18 @@ export const putSwitchTeam = (teamId: string) =>
|
||||
PUT<string>(`/proApi/support/user/team/switch`, { teamId });
|
||||
|
||||
/* --------------- team member ---------------- */
|
||||
export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) =>
|
||||
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
|
||||
export const getTeamMembers = (
|
||||
props: PaginationProps<{
|
||||
status?: 'active' | 'inactive';
|
||||
withOrgs?: boolean;
|
||||
withPermission?: boolean;
|
||||
searchKey?: string;
|
||||
orgId?: string;
|
||||
groupId?: string;
|
||||
}>
|
||||
) => POST<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
|
||||
export const getTeamMemberCount = () =>
|
||||
GET<{ count: number }>(`/proApi/support/user/team/member/count`);
|
||||
|
||||
// export const postInviteTeamMember = (data: InviteMemberProps) =>
|
||||
// POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
|
||||
@@ -66,9 +75,8 @@ export const postAcceptInvitationLink = (linkId: string) =>
|
||||
|
||||
export const getInvitationInfo = (linkId: string) =>
|
||||
GET<InvitationInfoType>(`/proApi/support/user/team/invitationLink/info`, { linkId });
|
||||
|
||||
export const putUpdateInvitationInfo = (data: InvitationLinkUpdateType) =>
|
||||
PUT('/proApi/support/user/team/invitationLink/update', data);
|
||||
export const putForbidInvitationLink = (linkId: string) =>
|
||||
PUT<string>(`/proApi/support/user/team/invitationLink/forbid`, { linkId });
|
||||
|
||||
/* -------------- team collaborator -------------------- */
|
||||
export const getTeamClbs = () =>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
|
||||
import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { GetGroupListBody } from '@fastgpt/global/support/permission/memberGroup/api';
|
||||
import type {
|
||||
GroupMemberItemType,
|
||||
MemberGroupListItemType
|
||||
} from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import type {
|
||||
postCreateGroupData,
|
||||
putUpdateGroupData
|
||||
} from '@fastgpt/global/support/user/team/group/api';
|
||||
|
||||
export const getGroupList = () => GET<MemberGroupListType>('/proApi/support/user/team/group/list');
|
||||
export const getGroupList = <T extends boolean>(data: GetGroupListBody) =>
|
||||
POST<MemberGroupListItemType<T>[]>('/proApi/support/user/team/group/list', data).then((res) => {
|
||||
console.log(res);
|
||||
return res;
|
||||
});
|
||||
|
||||
export const postCreateGroup = (data: postCreateGroupData) =>
|
||||
POST('/proApi/support/user/team/group/create', data);
|
||||
@@ -15,3 +23,9 @@ export const deleteGroup = (groupId: string) =>
|
||||
|
||||
export const putUpdateGroup = (data: putUpdateGroupData) =>
|
||||
PUT('/proApi/support/user/team/group/update', data);
|
||||
|
||||
export const getGroupMembers = (groupId: string) =>
|
||||
GET<GroupMemberItemType[]>(`/proApi/support/user/team/group/members`, { groupId });
|
||||
|
||||
export const putGroupChangeOwner = (groupId: string, tmbId: string) =>
|
||||
PUT(`/proApi/support/user/team/group/changeOwner`, { groupId, tmbId });
|
||||
|
||||
@@ -4,10 +4,17 @@ import type {
|
||||
putUpdateOrgData,
|
||||
putUpdateOrgMembersData
|
||||
} from '@fastgpt/global/support/user/team/org/api';
|
||||
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { putMoveOrgType } from '@fastgpt/global/support/user/team/org/api';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
|
||||
export const getOrgList = () => GET<OrgType[]>('/proApi/support/user/team/org/list');
|
||||
export const getOrgList = (params: {
|
||||
orgId: string;
|
||||
withPermission?: boolean;
|
||||
searchKey?: string;
|
||||
}) => POST<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
|
||||
|
||||
export const postCreateOrg = (data: postCreateOrgData) =>
|
||||
POST('/proApi/support/user/team/org/create', data);
|
||||
@@ -15,16 +22,17 @@ export const postCreateOrg = (data: postCreateOrgData) =>
|
||||
export const deleteOrg = (orgId: string) =>
|
||||
DELETE('/proApi/support/user/team/org/delete', { orgId });
|
||||
|
||||
export const deleteOrgMember = (orgId: string, tmbId: string) =>
|
||||
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
|
||||
|
||||
export const putMoveOrg = (data: putMoveOrgType) => PUT('/proApi/support/user/team/org/move', data);
|
||||
|
||||
export const putUpdateOrg = (data: putUpdateOrgData) =>
|
||||
PUT('/proApi/support/user/team/org/update', data);
|
||||
|
||||
// org members
|
||||
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
|
||||
PUT('/proApi/support/user/team/org/updateMembers', data);
|
||||
|
||||
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
|
||||
// PUT('/proApi/support/user/team/org/changeOwner', data);
|
||||
export const getOrgMembers = (data: PaginationProps<{ orgPath?: string }>) =>
|
||||
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/org/members`, data);
|
||||
|
||||
export const deleteOrgMember = (orgId: string, tmbId: string) =>
|
||||
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
|
||||
|
||||
130
projects/app/src/web/support/user/team/org/hooks/useOrg.tsx
Normal file
130
projects/app/src/web/support/user/team/org/hooks/useOrg.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
|
||||
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useUserStore } from '../../../useUserStore';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getOrgList, getOrgMembers } from '../api';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { getTeamMembers } from '../../api';
|
||||
import _ from 'lodash';
|
||||
|
||||
function useOrg({ withPermission = true }: { withPermission?: boolean } = {}) {
|
||||
const [orgStack, setOrgStack] = useState<OrgListItemType[]>([]);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const path = useMemo(
|
||||
() => (orgStack.length ? getOrgChildrenPath(orgStack[orgStack.length - 1]) : ''),
|
||||
[orgStack]
|
||||
);
|
||||
|
||||
const currentOrg = useMemo(() => {
|
||||
return (
|
||||
orgStack.at(-1) ??
|
||||
({
|
||||
_id: '',
|
||||
path: '',
|
||||
pathId: '',
|
||||
avatar: userInfo?.team.avatar,
|
||||
name: userInfo?.team.teamName
|
||||
} as OrgListItemType) // root org
|
||||
);
|
||||
}, [orgStack, userInfo?.team.avatar, userInfo?.team.teamName]);
|
||||
|
||||
const {
|
||||
data: orgs = [],
|
||||
loading: isLoadingOrgs,
|
||||
refresh: refetchOrgs
|
||||
} = useRequest2(
|
||||
() => getOrgList({ orgId: currentOrg._id, withPermission: withPermission, searchKey }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId, path, currentOrg._id, searchKey],
|
||||
debounceWait: 200,
|
||||
throttleWait: 500
|
||||
}
|
||||
);
|
||||
|
||||
const paths = useMemo(() => {
|
||||
if (!currentOrg) return [];
|
||||
return orgStack
|
||||
.map((org) => {
|
||||
return {
|
||||
parentId: getOrgChildrenPath(org),
|
||||
parentName: org.name
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ParentTreePathItemType[];
|
||||
}, [currentOrg, orgStack]);
|
||||
|
||||
const onClickOrg = (org: OrgListItemType) => {
|
||||
if (searchKey) {
|
||||
setOrgStack([org]);
|
||||
setSearchKey('');
|
||||
} else {
|
||||
setOrgStack([...orgStack, org]);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: members = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList: refetchMembers,
|
||||
isLoading: isLoadingMembers
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
orgId: currentOrg._id,
|
||||
withOrgs: false,
|
||||
withPermission: true,
|
||||
status: 'active'
|
||||
},
|
||||
refreshDeps: [path]
|
||||
});
|
||||
|
||||
const onPathClick = (path: string) => {
|
||||
const pathIds = path.split('/');
|
||||
setOrgStack(orgStack.filter((org) => pathIds.includes(org.pathId)));
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refetchOrgs();
|
||||
refetchMembers();
|
||||
};
|
||||
|
||||
const updateCurrentOrg = (data: { name?: string; description?: string; avatar?: string }) => {
|
||||
if (currentOrg.path === '') return;
|
||||
setOrgStack([
|
||||
...orgStack.slice(0, -1),
|
||||
{
|
||||
...currentOrg,
|
||||
name: data.name || currentOrg.name,
|
||||
description: data.description || currentOrg.description,
|
||||
avatar: data.avatar || currentOrg.avatar
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingOrgs || isLoadingMembers;
|
||||
|
||||
return {
|
||||
orgStack,
|
||||
currentOrg,
|
||||
orgs,
|
||||
isLoading,
|
||||
paths,
|
||||
onClickOrg,
|
||||
members,
|
||||
MemberScrollData,
|
||||
onPathClick,
|
||||
refresh,
|
||||
updateCurrentOrg,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
};
|
||||
}
|
||||
|
||||
export default useOrg;
|
||||
@@ -1,16 +1,13 @@
|
||||
import { create, devtools, persist, immer } from '@fastgpt/web/common/zustand';
|
||||
|
||||
import type { UserUpdateParams } from '@/types/user';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
|
||||
import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import type { UserType } from '@fastgpt/global/support/user/type.d';
|
||||
import type { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { getTeamPlanStatus } from './team/api';
|
||||
import { getGroupList } from './team/group/api';
|
||||
import { getOrgList } from './team/org/api';
|
||||
|
||||
type State = {
|
||||
systemMsgReadId: string;
|
||||
@@ -28,13 +25,7 @@ type State = {
|
||||
teamPlanStatus: FeTeamPlanStatusType | null;
|
||||
initTeamPlanStatus: () => Promise<any>;
|
||||
|
||||
teamMemberGroups: MemberGroupListType;
|
||||
myGroups: MemberGroupListType;
|
||||
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
|
||||
|
||||
teamOrgs: OrgType[];
|
||||
myOrgs: OrgType[];
|
||||
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
|
||||
};
|
||||
|
||||
export const useUserStore = create<State>()(
|
||||
@@ -106,42 +97,7 @@ export const useUserStore = create<State>()(
|
||||
});
|
||||
},
|
||||
teamMemberGroups: [],
|
||||
teamOrgs: [],
|
||||
myGroups: [],
|
||||
loadAndGetGroups: async (init = false) => {
|
||||
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
|
||||
|
||||
const randomRefresh = Math.random() > 0.7;
|
||||
if (!randomRefresh && !init && get().teamMemberGroups.length)
|
||||
return Promise.resolve(get().teamMemberGroups);
|
||||
|
||||
const res = await getGroupList();
|
||||
set((state) => {
|
||||
state.teamMemberGroups = res;
|
||||
state.myGroups = res.filter((item) =>
|
||||
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
|
||||
);
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
myOrgs: [],
|
||||
loadAndGetOrgs: async (init = false) => {
|
||||
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
|
||||
|
||||
const randomRefresh = Math.random() > 0.7;
|
||||
if (!randomRefresh && !init && get().myOrgs.length) return Promise.resolve(get().myOrgs);
|
||||
|
||||
const res = await getOrgList();
|
||||
set((state) => {
|
||||
state.teamOrgs = res;
|
||||
state.myOrgs = res.filter((item) =>
|
||||
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
|
||||
);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
teamOrgs: []
|
||||
})),
|
||||
{
|
||||
name: 'userStore',
|
||||
|
||||
Reference in New Issue
Block a user