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:
Archer
2025-03-27 16:54:08 +08:00
committed by GitHub
209 changed files with 5156 additions and 2466 deletions

View File

@@ -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} />
</>
);

View File

@@ -71,7 +71,7 @@ const EditResourceModal = ({
{...register('name', { required: true })}
bg={'myGray.50'}
autoFocus
maxLength={20}
maxLength={100}
/>
</HStack>
</Box>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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}>

View File

@@ -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>
)}

View File

@@ -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}

View File

@@ -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

View File

@@ -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'
})}

View File

@@ -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,

View File

@@ -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}
/>
);
})}

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 />
</>
);
};

View File

@@ -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>

View File

@@ -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')

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>
)}
</>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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} />}
</>
);

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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}
/>
);
}

View File

@@ -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)}
/>
)}

View File

@@ -75,6 +75,7 @@ function PermissionManage({
const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), {
manual: false,
throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchKey]
});

View File

@@ -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 (

View File

@@ -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')
})}

View File

@@ -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')
})}

View File

@@ -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);

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 {

View File

@@ -96,7 +96,7 @@ const ExtractFieldModal = ({
<Input
bg={'myGray.50'}
placeholder="name/age/sql"
maxLength={20}
maxLength={100}
{...register('key', { required: true })}
/>
</Flex>

View File

@@ -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 && (

View File

@@ -418,7 +418,7 @@ const NodeCard = (props: Props) => {
{RenderToolHandle}
<ConfirmSyncModal />
<EditTitleModal maxLength={50} />
<EditTitleModal maxLength={100} />
</Flex>
);
};

View File

@@ -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,

View File

@@ -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]);
}}
/>
)}

View File

@@ -140,7 +140,7 @@ export const useChatTest = ({
appId={appId}
chatId={chatId}
showMarkIcon
chatType="chat"
chatType={'chat'}
onStartChat={startChat}
/>
)

View File

@@ -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'}

View File

@@ -319,7 +319,7 @@ const TemplateMarketModal = ({
onChange={(e) => setCurrentSearch(e.target.value)}
h={8}
bg={'myGray.50'}
maxLength={20}
maxLength={100}
borderRadius={'sm'}
/>
</Box>

View File

@@ -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>
);
};

View File

@@ -49,7 +49,7 @@ const EditFolderModal = ({
defaultValue={name}
placeholder={t('common:dataset.Folder Name') || ''}
autoFocus
maxLength={20}
maxLength={100}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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: (

View File

@@ -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,

View File

@@ -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}>

View File

@@ -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'}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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
});
}
});

View File

@@ -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

View File

@@ -239,8 +239,8 @@ function List() {
<Box
flex={1}
className={'textEllipsis3'}
whiteSpace={'pre-wrap'}
py={3}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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

View File

@@ -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
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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
});

View File

@@ -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);

View File

@@ -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
};
}

View File

@@ -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)
});

View File

@@ -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 />;
};

View File

@@ -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[] = [];

View File

@@ -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,

View File

@@ -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

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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]]
},
{

View File

@@ -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>;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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

View File

@@ -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]: '',

View File

@@ -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,

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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');

View File

@@ -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 = () =>

View File

@@ -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 });

View File

@@ -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 });

View 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;

View File

@@ -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',