V4.9.1 feature (#4206)

* fix: remove DefaultTeam (#4037)

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

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* fix :Get application bound knowledge base information logical rewrite

* update package

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

* perf: pg config params

* perf: ai proxy avatar

* fix: import dataset step error

* feat: data input ux

* perf: app dataset rewite

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

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

* perf: extract array

* update doc

* perf: model test

* perf: model test

* perf: think tag parse (#4102)

* chat quote reader (#3912)

* init chat quote full text reader

* linked structure

* dataset data linked

* optimize code

* fix ts build

* test finish

* delete log

* fix

* fix ts

* fix ts

* remove nextId

* initial scroll

* fix

* fix

* perf: chunk read   (#4109)

* package

* perf: chunk read

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

* feat: api dataset support pdf parse

* fix: chunk reader auth

* feat: invitation link (#3979)

* feat: invitation link schema and apis

* feat: add invitation link

* feat: member status: active, leave, forbidden

* fix: expires show hours and minutes

* feat: invalid invitation link hint

* fix: typo

* chore: fix typo & i18n

* fix

* pref: fe

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

* perf: invite member code (#4118)

* perf: invite member code

* fix: ts

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

* fix: model test channel id

* fix: quote reader

* fix chat quote reader (#4125)

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

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

* perf: pg config params

* perf: ai proxy avatar

* fix: import dataset step error

* feat: data input ux

* perf: app dataset rewite

* perf: model test

* perf: sidebar trigger

* lock

* update nanoid version

* fix: select component ux

* fix: ts

* fix: vitest

* remove test

* fix: prompt toolcall ui (#4139)

* load log error adapt

* fix: prompt toolcall ui

* perf: commercial function tip

* update package

* pref: copy link (#4147)

* fix(i18n): namespace (#4143)

* hiden dataset source (#4152)

* hiden dataset source

* perf: reader

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

* fix modal close scroll (#4162)

* fix modal close scroll

* update refresh

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

* fix loadInitData refresh (#4169)

* fix

* fix

* form input number default & api dataset max token

* feat: mix search weight (#4170)

* feat: mix search weight

* feat: svg render

* fix: avatar error remove (#4173)

* fix: avatar error remove

* fix: index

* fix: guide

* fix: auth

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

* update package

* fix: ts

* update config

* update jieba package

* add type sign

* fix: input data ui

* fix: page title refresh (#4186)

* fix: ts

* update jieba package

* fix: page title refresh

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

* add env to check internal ip (#4187)

* fix: ts

* update jieba package

* add env to check internal ip

* package

* fix: jieba

* reset package

* update config

* fix: jieba package

* init shell

* init version

* change team reload

* update jieba package (#4200)

* update jieba package

* package

* update package

* remove invalid code

* action

* package (#4201)

* package

* update package

* remove invalid code

* package

* remove i18n tip (#4202)

* doc (#4205)

* fix: i18n (#4208)

* fix: next config (#4207)

* reset package

* i18n

* update config

* i18n

* remove log

---------

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import ChatRecordContextProvider, {
} from '@/web/core/chat/context/chatRecordContext';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
@@ -37,6 +38,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@@ -79,7 +82,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
right={0}
h={['100%', '96%']}
w={'100%'}
maxW={['100%', '600px']}
maxW={quoteData ? ['100%', '1080px'] : ['100%', '600px']}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
@@ -148,26 +151,48 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
)}
{/* Chat container */}
<Box pt={2} flex={'1 0 0'} h={0}>
{isPlugin ? (
<Box h={'100%'} overflow={'auto'}>
<Flex pt={2} flex={'1 0 0'} h={0}>
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
{isPlugin ? (
<Box px={5} py={2}>
<PluginRunBox appId={appId} chatId={chatId} />
</Box>
) : (
<ChatBox
isReady
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatType="log"
/>
)}
</Box>
{quoteData && (
<Box
flex={'1 0 0'}
w={0}
mr={4}
maxW={'460px'}
h={'98%'}
bg={'white'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</Box>
) : (
<ChatBox
isReady
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatType="log"
/>
)}
</Box>
</Flex>
</MyBox>
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
</>
);
@@ -189,6 +214,7 @@ const Render = (props: Props) => {
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
// isShowFullText={true}
showNodeStatus
>
<ChatRecordContextProvider params={params}>

View File

@@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import { useI18n } from '@/web/context/I18n';
const API = ({ appId }: { appId: string }) => {
const { publishT } = useI18n();
return <ApiKeyTable tips={publishT('app_key_tips')} appId={appId} />;
const { t } = useTranslation();
return <ApiKeyTable tips={t('publish:app_key_tips')} appId={appId} />;
};
export default API;

View File

@@ -42,7 +42,6 @@ import { getDocPath } from '@/web/common/system/doc';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
@@ -185,6 +184,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
name: item.name,
responseDetail: item.responseDetail ?? false,
showRawSource: item.showRawSource ?? false,
// showFullText: item.showFullText ?? false,
showNodeStatus: item.showNodeStatus ?? false,
limit: item.limit
})
@@ -270,7 +270,6 @@ function EditLinkModal({
}) {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { publishT } = useI18n();
const {
register,
setValue,
@@ -281,6 +280,7 @@ function EditLinkModal({
});
const responseDetail = watch('responseDetail');
// const showFullText = watch('showFullText');
const showRawSource = watch('showRawSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
@@ -306,7 +306,7 @@ function EditLinkModal({
<MyModal
isOpen={true}
iconSrc="/imgs/modal/shareFill.svg"
title={isEdit ? publishT('edit_link') : publishT('create_link')}
title={isEdit ? t('publish:edit_link') : t('publish:create_link')}
maxW={['90vw', '700px']}
w={'100%'}
h={['90vh', 'auto']}
@@ -325,10 +325,10 @@ function EditLinkModal({
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
placeholder={t('publish:link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
required: t('common:common.name_is_empty')
})}
/>
</Flex>
@@ -353,7 +353,7 @@ function EditLinkModal({
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips')}></QuestionTip>
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
</Flex>
<Input
max={1000}
@@ -361,7 +361,7 @@ function EditLinkModal({
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
required: t('publish:qpm_is_empty')
})}
/>
</Flex>
@@ -385,11 +385,11 @@ function EditLinkModal({
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
<FormLabel>{t('publish:token_auth')}</FormLabel>
<QuestionTip ml={1} label={t('publish:token_auth_tips')}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
placeholder={t('publish:token_auth_tips')}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
@@ -400,7 +400,7 @@ function EditLinkModal({
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
{t('publish:token_auth_use_cases')}
</Link>
</>
)}
@@ -421,8 +421,39 @@ function EditLinkModal({
label={t('common:support.outlink.share.Response Quote tips')}
></QuestionTip>
</Flex>
<Switch {...register('responseDetail')} isChecked={responseDetail} />
<Switch
{...register('responseDetail', {
onChange(e) {
if (!e.target.checked) {
// setValue('showFullText', false);
setValue('showRawSource', false);
}
}
})}
isChecked={responseDetail}
/>
</Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Chat_quote_reader')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.Full_text tips')}
></QuestionTip>
</Flex>
<Switch
{...register('showFullText', {
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
} else {
setValue('showRawSource', false);
}
}
})}
isChecked={showFullText}
/>
</Flex> */}
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
@@ -436,6 +467,7 @@ function EditLinkModal({
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
// setValue('showFullText', true);
}
}
})}

View File

@@ -99,7 +99,7 @@ const OutLink = () => {
if (!feConfigs.isPlus && config.isProFn) {
toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
title: t('common:commercial_function_tip')
});
} else {
setLinkType(e as PublishChannelEnum);

View File

@@ -7,24 +7,26 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSafeState } from 'ahooks';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useChatTest } from '../useChatTest';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
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';
type Props = { appForm: AppSimpleEditFormType };
const ChatTest = ({ appForm }: Props) => {
type Props = {
appForm: AppSimpleEditFormType;
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
};
const ChatTest = ({ appForm, setRenderEdit }: Props) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
// form2AppWorkflow dependent allDatasets
const { allDatasets } = useDatasetStore();
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
@@ -33,9 +35,12 @@ const ChatTest = ({ appForm }: Props) => {
useEffect(() => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
// console.log(form2AppWorkflow(appForm, t));
setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData, allDatasets, t]);
}, [appForm, setWorkflowData, t]);
useEffect(() => {
setRenderEdit(!quoteData);
}, [quoteData, setRenderEdit]);
const { ChatContainer, restartChat, loading } = useChatTest({
...workflowData,
@@ -44,41 +49,56 @@ const ChatTest = ({ appForm }: Props) => {
});
return (
<MyBox
isLoading={loading}
display={'flex'}
position={'relative'}
flexDirection={'column'}
h={'100%'}
py={4}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
{appT('chat_debug')}
<Flex h={'full'} gap={2}>
<MyBox
flex={'1 0 0'}
w={0}
isLoading={loading}
display={'flex'}
position={'relative'}
flexDirection={'column'}
h={'full'}
py={4}
{...cardStyles}
boxShadow={'3'}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
{t('app:chat_debug')}
</Box>
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
restartChat();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatContainer />
</Box>
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
restartChat();
}}
</MyBox>
{quoteData && (
<Box flex={'1 0 0'} w={0} maxW={'560px'} {...cardStyles} boxShadow={'3'}>
<ChatQuoteList
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatContainer />
</Box>
</MyBox>
</Box>
)}
</Flex>
);
};
const Render = ({ appForm }: Props) => {
const Render = ({ appForm, setRenderEdit }: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
@@ -95,10 +115,11 @@ const Render = ({ appForm }: Props) => {
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
// isShowFullText={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} />
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Box } from '@chakra-ui/react';
import ChatTest from './ChatTest';
@@ -21,6 +21,7 @@ const Edit = ({
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const { isPc } = useSystem();
const [renderEdit, setRenderEdit] = useState(true);
return (
<Box
@@ -32,24 +33,26 @@ const Edit = ({
borderRadius={'lg'}
overflowY={['auto', 'unset']}
>
<Box
className={styles.EditAppBox}
pr={[0, 1]}
overflowY={'auto'}
minW={['auto', '580px']}
flex={'1'}
>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard appForm={appForm} setPast={setPast} />
</Box>
{renderEdit && (
<Box
className={styles.EditAppBox}
pr={[0, 1]}
overflowY={'auto'}
minW={['auto', '580px']}
flex={'1'}
>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard appForm={appForm} setPast={setPast} />
</Box>
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
</Box>
</Box>
</Box>
)}
{isPc && (
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
<ChatTest appForm={appForm} />
<Box flex={'2 0 0'} w={0} mb={3}>
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
</Box>
)}
</Box>

View File

@@ -12,7 +12,6 @@ import {
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -68,18 +67,9 @@ const EditForm = ({
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]);
const [, startTst] = useTransition();
const selectDatasets = useMemo(
() =>
allDatasets.filter((item) =>
appForm.dataset?.datasets.find((dataset) => dataset.datasetId === item._id)
),
[allDatasets, appForm?.dataset?.datasets]
);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
@@ -115,6 +105,7 @@ const EditForm = ({
const tokenLimit = useMemo(() => {
return selectedModel?.quoteMaxToken || 3000;
}, [selectedModel?.quoteMaxToken]);
// Force close image select when model not support vision
useEffect(() => {
if (!selectedModel.vision) {
@@ -252,7 +243,7 @@ const EditForm = ({
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('common:core.dataset.Read Dataset')}>
<MyTooltip key={item.datasetId} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
@@ -266,7 +257,7 @@ const EditForm = ({
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
datasetId: item.datasetId
}
})
}
@@ -413,8 +404,10 @@ const EditForm = ({
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
datasetId: item.datasetId,
vectorModel: item.vectorModel,
name: item.name,
avatar: item.avatar
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
@@ -441,8 +434,6 @@ const EditForm = ({
...e
}
}));
console.dir(e);
}}
/>
)}

View File

@@ -17,7 +17,6 @@ import { publishStatusStyle } from '../constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import SaveButton from '../Workflow/components/SaveButton';
import { useBoolean, useDebounceEffect, useLockFn } from 'ahooks';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
@@ -61,7 +60,6 @@ const Header = ({
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
const { lastAppListRouteType } = useSystemStore();
const { allDatasets } = useDatasetStore();
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false,
@@ -159,7 +157,7 @@ const Header = ({
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
setIsSaved(val);
},
[past, allDatasets],
[past],
{ wait: 500 }
);

View File

@@ -10,7 +10,6 @@ import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SimpleAppSnapshotType, useSimpleAppSnapshots } from './useSnapshots';
import { useDebounceEffect, useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { getAppConfigByDiff } from '@/web/core/app/diff';
@@ -19,7 +18,6 @@ const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => {
const { t } = useTranslation();
const { loadAllDatasets } = useDatasetStore();
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots(
@@ -30,9 +28,6 @@ const SimpleEdit = () => {
// Init app form
useMount(() => {
// show selected dataset
loadAllDatasets();
if (appDetail.version !== 'v2') {
return setAppForm(
appWorkflow2Form({

View File

@@ -20,6 +20,7 @@ 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 ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
type Props = {
isOpen: boolean;
@@ -41,10 +42,13 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
});
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
return (
<>
<Flex h={'full'}>
<Box
zIndex={300}
display={isOpen ? 'block' : 'none'}
@@ -53,7 +57,10 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
left={0}
bottom={0}
right={0}
onClick={onClose}
onClick={() => {
setQuoteData(undefined);
onClose();
}}
/>
<MyBox
isLoading={loading}
@@ -64,7 +71,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
w={isOpen ? (quoteData ? ['100%', '960px'] : ['100%', '460px']) : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
@@ -137,11 +144,34 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
</Flex>
)}
<Box flex={'1 0 0'} overflow={'auto'}>
<ChatContainer />
</Box>
<Flex flex={'1 0 0'} alignItems={'end'}>
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ChatContainer />
</Box>
{quoteData && (
<Box
flex={'1 0 0'}
w={0}
mr={4}
maxW={'440px'}
h={'98%'}
bg={'white'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</Box>
)}
</Flex>
</MyBox>
</>
</Flex>
);
};
@@ -162,6 +192,7 @@ const Render = (Props: Props) => {
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
// isShowFullText={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>

View File

@@ -84,7 +84,7 @@ const ExtractFieldModal = ({
<MySelect<string>
list={toolValueTypeList}
value={valueType}
onchange={(e) => {
onChange={(e) => {
setValue('valueType', e as any);
}}
/>

View File

@@ -217,7 +217,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
value: 'PATCH'
}
]}
onchange={(e) => {
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',

View File

@@ -396,7 +396,7 @@ const ConditionSelect = ({
w={'100%'}
list={filterQuiredConditionList}
value={condition}
onchange={onSelect}
onChange={onSelect}
placeholder={t('common:chose_condition')}
/>
);
@@ -441,7 +441,7 @@ const ConditionValueInput = ({
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
onChange={onChange}
value={value}
placeholder={workflowT('ifelse.Select value')}
isDisabled={

View File

@@ -235,7 +235,7 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('common:core.module.laf.Select laf function')}
onchange={(e) => {
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',

View File

@@ -209,7 +209,7 @@ const InputTypeConfig = ({
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
onChange={(e) => {
setValue('valueType', e);
}}
/>
@@ -346,7 +346,7 @@ const InputTypeConfig = ({
? defaultValue
: ''
}
onchange={(e) => {
onChange={(e) => {
setValue('defaultValue', e);
}}
w={'200px'}

View File

@@ -144,7 +144,7 @@ const PluginOutputEditModal = ({
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
onChange={(e) => {
setValue('valueType', e);
}}
/>

View File

@@ -123,7 +123,7 @@ const ToolParamsEditModal = ({
<MySelect
list={toolValueTypeList}
value={valueType}
onchange={(e: any) => {
onChange={(e: any) => {
setValue('valueType', e);
}}
/>

View File

@@ -137,7 +137,7 @@ const FieldModal = ({
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
onChange={(e) => {
setValue('valueType', e);
}}
/>

View File

@@ -14,7 +14,7 @@ const SelectRender = ({ item, nodeId }: RenderInputProps) => {
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',

View File

@@ -1,13 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, Grid, Switch, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
@@ -32,26 +29,17 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
usingReRank: false
});
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenDatasetSelect,
onClose: onCloseDatasetSelect
} = useDisclosure();
const selectedDatasetsValue = useMemo(() => {
const selectedDatasets = useMemo(() => {
if (Array.isArray(item.value)) return item.value as SelectedDatasetType;
return [] as SelectedDatasetType;
}, [item.value]);
const selectedDatasets = useMemo(() => {
return allDatasets.filter((dataset) =>
selectedDatasetsValue?.find((item) => item.datasetId === dataset._id)
);
}, [allDatasets, selectedDatasetsValue]);
useQuery(['loadAllDatasets'], loadAllDatasets);
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
@@ -82,7 +70,7 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
</Button>
{selectedDatasets.map((item) => (
<Flex
key={item._id}
key={item.datasetId}
alignItems={'center'}
h={10}
boxShadow={'sm'}
@@ -108,7 +96,12 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectedDatasetsValue}
defaultSelectedDatasets={selectedDatasets.map((item) => ({
datasetId: item.datasetId,
vectorModel: item.vectorModel,
name: item.name,
avatar: item.avatar
}))}
onChange={(e) => {
onChangeNode({
nodeId,
@@ -133,7 +126,6 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
onCloseDatasetSelect,
onOpenDatasetSelect,
selectedDatasets,
selectedDatasetsValue,
t
]);

View File

@@ -6,13 +6,14 @@ import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DatasetParamsModal, { DatasetParamsProps } from '@/components/core/app/DatasetParamsModal';
import DatasetParamsModal from '@/components/core/app/DatasetParamsModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
import { getWebLLMModel } from '@/web/common/system/utils';
import { defaultDatasetMaxTokens } from '@fastgpt/global/core/app/constants';
import { AppDatasetSearchParamsType } from '@fastgpt/global/core/app/type';
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -21,11 +22,14 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const { defaultModels } = useSystemStore();
const [data, setData] = useState<DatasetParamsProps>({
const [data, setData] = useState<AppDatasetSearchParamsType>({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
embeddingWeight: 0.5,
limit: 3000,
similarity: 0.5,
usingReRank: false,
rerankModel: defaultModels.llm?.model,
rerankWeight: 0.6,
datasetSearchUsingExtensionQuery: true,
datasetSearchExtensionModel: defaultModels.llm?.model,
datasetSearchExtensionBg: ''

View File

@@ -57,7 +57,7 @@ const SelectAiModelRender = ({ item, nodeId }: RenderInputProps) => {
value: item.model,
label: item.name
}))}
onchange={onChangeModel}
onChange={onChangeModel}
/>
);
}, [item.value, modelList, onChangeModel]);

View File

@@ -201,7 +201,7 @@ const EditModal = ({ onClose, ...props }: RenderInputProps & { onClose: () => vo
description: t('workflow:dataset_quote_role_user_option_desc')
}
]}
onchange={(e) => {
onChange={(e) => {
setValue('quoteRole', e);
}}
/>

View File

@@ -131,7 +131,7 @@ const FieldModal = ({
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
onChange={(e) => {
setValue('valueType', e);
}}
/>

View File

@@ -97,7 +97,7 @@ const EditFieldModal = ({
<MySelect
list={toolValueTypeList}
value={valueType}
onchange={(e: any) => {
onChange={(e: any) => {
setValue('valueType', e);
}}
/>

View File

@@ -289,7 +289,7 @@ const TemplateMarketModal = ({
<MySelect<TemplateAppType>
h={'8'}
value={currentAppType}
onchange={(value) => {
onChange={(value) => {
setCurrentAppType(value);
}}
bg={'myGray.100'}

View File

@@ -3,31 +3,32 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useI18n } from '@/web/context/I18n';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
const { appT } = useI18n();
const { t } = useTranslation();
const map = useRef({
[AppTypeEnum.simple]: {
label: appT('type.Simple bot'),
label: t('app:type.Simple bot'),
icon: 'core/app/type/simple',
bg: '#DBF3FF',
color: '#0884DD'
},
[AppTypeEnum.workflow]: {
label: appT('type.Workflow bot'),
label: t('app:type.Workflow bot'),
icon: 'core/app/type/workflow',
bg: '#E4E1FC',
color: '#6F5DD7'
},
[AppTypeEnum.plugin]: {
label: appT('type.Plugin'),
label: t('app:type.Plugin'),
icon: 'core/app/type/plugin',
bg: '#D0F5EE',
color: '#007E7C'
},
[AppTypeEnum.httpPlugin]: {
label: appT('type.Http plugin'),
label: t('app:type.Http plugin'),
icon: 'core/app/type/httpPlugin',
bg: '#FFE4EE',
color: '#E82F72'

View File

@@ -46,6 +46,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const concatHistory = useMemo(() => {
const formatHistories: HistoryItemType[] = histories.map((item) => {
@@ -144,7 +145,10 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
borderRadius={'xl'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
overflow={'hidden'}
onClick={() => onChangeChatId()}
onClick={() => {
onChangeChatId();
setQuoteData(undefined);
}}
>
{t('common:core.chat.New Chat')}
</Button>
@@ -199,6 +203,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
: {
onClick: () => {
onChangeChatId(item.id);
setQuoteData(undefined);
}
})}
{...(i !== concatHistory.length - 1 && {
@@ -270,6 +275,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
onDelHistory(item.id);
if (item.id === activeChatId) {
onChangeChatId();
setQuoteData(undefined);
}
},
type: 'danger'

View File

@@ -0,0 +1,183 @@
import Markdown from '@/components/Markdown';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import InputDataModal from '@/pageComponents/dataset/detail/InputDataModal';
const CollectionQuoteItem = ({
quoteRefs,
quoteIndex,
setQuoteIndex,
refreshList,
canEdit,
updated,
isCurrentSelected,
q,
a,
dataId,
collectionId
}: {
quoteRefs: MutableRefObject<Map<string, HTMLDivElement | null>>;
quoteIndex: number;
setQuoteIndex: Dispatch<SetStateAction<number>>;
refreshList: () => void;
canEdit: boolean;
updated?: boolean;
isCurrentSelected: boolean;
q: string;
a?: string;
dataId: string;
collectionId: string;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const hasBeenSearched = quoteIndex !== undefined && quoteIndex > -1;
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
return (
<>
<Box
ref={(el: HTMLDivElement | null) => {
quoteRefs.current.set(dataId, el);
}}
p={2}
py={2}
cursor={hasBeenSearched ? 'pointer' : 'default'}
bg={isCurrentSelected ? '#FFF9E7' : hasBeenSearched ? '#FFFCF2' : ''}
position={'relative'}
overflow={'hidden'}
border={'1px solid '}
borderColor={isCurrentSelected ? 'yellow.200' : 'transparent'}
wordBreak={'break-all'}
fontSize={'sm'}
_hover={
hasBeenSearched
? {
'& .hover-data': { visibility: 'visible' }
}
: {
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
borderTopColor: 'myGray.50',
'& .hover-data': { visibility: 'visible' }
}
}
onClick={(e) => {
e.stopPropagation();
if (hasBeenSearched) {
setQuoteIndex(quoteIndex);
}
}}
>
{updated && (
<Flex mt={2}>
<Box
bg={'green.50'}
border={'1px solid'}
borderRadius={'xs'}
borderColor={'green.100'}
px={1}
color={'green.600'}
>
{t('common:core.dataset.data.Updated')}
</Box>
<Box flex={1} borderBottom={'1px dashed'} borderColor={'green.200'} />
</Flex>
)}
<Markdown source={q} />
{!!a && (
<Box>
<Markdown source={a} />
</Box>
)}
<Flex
className="hover-data"
position={'absolute'}
bottom={2}
right={5}
gap={1.5}
visibility={'hidden'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={2}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{q.length + (a?.length || 0)}
</Flex>
</MyTooltip>
{canEdit && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() =>
setEditInputData({
dataId,
collectionId
})
}
>
<MyIcon name="common/edit" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
)}
<MyTooltip label={t('common:common.Copy')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() => copyData(`${q}${a ? '\n' + a : ''}`)}
>
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
</Flex>
</Box>
{editInputData && (
<InputDataModal
onClose={() => setEditInputData(undefined)}
onSuccess={refreshList}
dataId={editInputData.dataId}
collectionId={editInputData.collectionId}
/>
)}
</>
);
};
export default CollectionQuoteItem;

View File

@@ -0,0 +1,304 @@
import { Box, Flex, HStack } from '@chakra-ui/react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import DownloadButton from './DownloadButton';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { downloadFetch } from '@/web/common/system/utils';
import { useMemo, useState } from 'react';
import { getDatasetDataPermission } from '@/web/core/dataset/api';
import ScoreTag from './ScoreTag';
import { formatScore } from '@/components/core/dataset/QuoteItem';
import NavButton from './NavButton';
import { useLinkedScroll } from '@fastgpt/web/hooks/useLinkedScroll';
import CollectionQuoteItem from './CollectionQuoteItem';
import { GetCollectionQuoteDataProps } from '@/web/core/chat/context/chatItemContext';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getCollectionQuote } from '@/web/core/chat/api';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { QuoteDataItemType } from '@/service/core/chat/constants';
const CollectionReader = ({
rawSearch,
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
metadata: GetCollectionQuoteDataProps;
onClose: () => void;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { userInfo } = useUserStore();
const { collectionId, datasetId, chatItemDataId, sourceId, sourceName } = metadata;
const [quoteIndex, setQuoteIndex] = useState(0);
// Get dataset permission
const { data: datasetData } = useRequest2(async () => await getDatasetDataPermission(datasetId), {
manual: !userInfo || !datasetId,
refreshDeps: [datasetId, userInfo]
});
const filterResults = useMemo(() => {
setQuoteIndex(0);
return rawSearch
.filter((item) => item.collectionId === collectionId)
.sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
}, [collectionId, rawSearch]);
const currentQuoteItem = useMemo(() => {
const item = filterResults[quoteIndex];
if (item) {
return {
id: item.id,
index: item.chunkIndex,
score: item.score
};
}
}, [filterResults, quoteIndex]);
// Get quote list
const params = useMemo(
() => ({
collectionId,
chatItemDataId,
chatId: metadata.chatId,
appId: metadata.appId,
...metadata.outLinkAuthData
}),
[chatItemDataId, collectionId, metadata.appId, metadata.chatId, metadata.outLinkAuthData]
);
const {
dataList: datasetDataList,
isLoading,
ScrollData,
itemRefs,
loadInitData
} = useLinkedScroll(getCollectionQuote, {
params,
currentData: currentQuoteItem
});
const isDeleted = useMemo(
() => !isLoading && !datasetDataList.find((item) => item._id === currentQuoteItem?.id),
[datasetDataList, currentQuoteItem?.id, isLoading]
);
const formatedDataList = useMemo(
() =>
datasetDataList.map((item: QuoteDataItemType) => {
const isCurrentSelected = currentQuoteItem?.id === item._id;
const quoteIndex = filterResults.findIndex((res) => res.id === item._id);
return {
...item,
isCurrentSelected,
quoteIndex
};
}),
[currentQuoteItem?.id, datasetDataList, filterResults]
);
const { runAsync: handleDownload } = useRequest2(async () => {
await downloadFetch({
url: '/api/core/dataset/collection/export',
filename: 'data.csv',
body: {
appId: metadata.appId,
chatId: metadata.chatId,
chatItemDataId,
collectionId,
...metadata.outLinkAuthData
}
});
});
const handleRead = getCollectionSourceAndOpen({
appId: metadata.appId,
chatId: metadata.chatId,
chatItemDataId,
collectionId,
...metadata.outLinkAuthData
});
return (
<MyBox display={'flex'} flexDirection={'column'} h={'full'}>
{/* title */}
<Box borderBottom={'1px solid'} borderBottomColor={'myGray.150'} px={3} py={2}>
{/* name */}
<HStack>
<Flex alignItems={'center'} flex={'1 0 0'} w={0}>
<MyIcon
name={getSourceNameIcon({ sourceId, sourceName }) as any}
w={['1rem', '1.25rem']}
color={'primary.600'}
/>
<Box
ml={1}
maxW={['200px', '220px']}
className={'textEllipsis'}
wordBreak={'break-all'}
fontSize={'sm'}
color={'myGray.900'}
fontWeight={'medium'}
{...(!!userInfo &&
datasetData?.permission?.hasReadPer && {
cursor: 'pointer',
_hover: { color: 'primary.600', textDecoration: 'underline' },
onClick: () => {
router.push(
`/dataset/detail?datasetId=${datasetId}&currentTab=dataCard&collectionId=${collectionId}`
);
}
})}
>
{sourceName || t('common:common.UnKnow Source')}
</Box>
<Box ml={3}>
<DownloadButton
canAccessRawData={true}
onDownload={handleDownload}
onRead={handleRead}
/>
</Box>
</Flex>
<MyIconButton
icon={'common/closeLight'}
size={'1.25rem'}
color={'myGray.900'}
onClick={onClose}
/>
</HStack>
{datasetData?.permission?.hasReadPer && (
<Box
fontSize={'mini'}
color={'myGray.500'}
{...(!!userInfo
? {
cursor: 'pointer',
_hover: { color: 'primary.600', textDecoration: 'underline' },
onClick: () => {
router.push(`/dataset/detail?datasetId=${datasetId}`);
}
}
: {})}
>
{t('chat:data_source', {
name: datasetData.datasetName
})}
</Box>
)}
</Box>
{/* header control */}
{datasetDataList.length > 0 && (
<Box>
<Flex
w={'full'}
px={4}
py={2}
alignItems={'center'}
borderBottom={'1px solid'}
borderColor={'myGray.150'}
>
{/* 引用序号 */}
<Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}>
<Box as={'span'} color={'myGray.900'}>
{t('common:core.chat.Quote')} {quoteIndex + 1}
</Box>
<Box as={'span'} color={'myGray.500'}>
/
</Box>
<Box as={'span'} color={'myGray.500'}>
{filterResults.length}
</Box>
</Flex>
{/* 检索分数 */}
{currentQuoteItem?.score ? (
<ScoreTag {...formatScore(currentQuoteItem?.score)} />
) : isDeleted ? (
<Flex
borderRadius={'sm'}
py={1}
px={2}
color={'red.600'}
bg={'red.50'}
alignItems={'center'}
fontSize={'11px'}
>
<MyIcon name="common/info" w={'14px'} mr={1} color={'red.600'} />
{t('chat:chat.quote.deleted')}
</Flex>
) : null}
<Box flex={1} />
{/* 检索按钮 */}
<Flex gap={1}>
<NavButton
direction="up"
isDisabled={quoteIndex === 0}
onClick={() => setQuoteIndex(quoteIndex - 1)}
/>
<NavButton
direction="down"
isDisabled={quoteIndex === filterResults.length - 1}
onClick={() => setQuoteIndex(quoteIndex + 1)}
/>
</Flex>
</Flex>
<Box fontSize={'mini'} color={'myGray.500'} bg={'myGray.25'} px={4} py={1}>
{t('common:core.chat.quote.Quote Tip')}
</Box>
</Box>
)}
{/* quote list */}
{isLoading || datasetDataList.length > 0 ? (
<ScrollData flex={'1 0 0'} mt={2} px={5} py={1}>
<Flex flexDir={'column'}>
{formatedDataList.map((item, index) => (
<CollectionQuoteItem
key={item._id}
quoteRefs={itemRefs as React.MutableRefObject<Map<string, HTMLDivElement | null>>}
quoteIndex={item.quoteIndex}
setQuoteIndex={setQuoteIndex}
refreshList={() => loadInitData({ scrollWhenFinish: false, refresh: true })}
updated={item.updated}
isCurrentSelected={item.isCurrentSelected}
q={item.q}
a={item.a}
dataId={item._id}
collectionId={collectionId}
canEdit={!!userInfo && !!datasetData?.permission?.hasWritePer}
/>
))}
</Flex>
</ScrollData>
) : (
<Flex
flex={'1 0 0'}
flexDirection={'column'}
gap={1}
justifyContent={'center'}
alignItems={'center'}
>
<Box border={'1px dashed'} borderColor={'myGray.400'} p={2} borderRadius={'full'}>
<MyIcon name="common/fileNotFound" />
</Box>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('chat:chat.quote.No Data')}
</Box>
</Flex>
)}
</MyBox>
);
};
export default CollectionReader;

View File

@@ -0,0 +1,54 @@
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'next-i18next';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
const DownloadButton = ({
canAccessRawData,
onDownload,
onRead
}: {
canAccessRawData: boolean;
onDownload: () => void;
onRead: () => void;
}) => {
const { t } = useTranslation();
if (canAccessRawData) {
return (
<MyMenu
size={'xs'}
Button={
<MyIconButton
icon="common/download"
size={'1rem'}
border={'1px solid'}
borderColor={'myGray.250'}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
/>
}
menuList={[
{
children: [
{
label: t('chat:download_chunks'),
type: 'grayBg',
onClick: onDownload
},
{
label: t('chat:read_raw_source'),
type: 'grayBg',
onClick: onRead
}
]
}
]}
/>
);
}
return <MyIconButton icon="common/download" size={'1rem'} onClick={onDownload} />;
};
export default DownloadButton;

View File

@@ -0,0 +1,47 @@
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const NavButton = ({
direction,
isDisabled,
onClick
}: {
direction: 'up' | 'down';
isDisabled: boolean;
onClick: () => void;
}) => {
const isUp = direction === 'up';
const baseStyles = {
color: 'myGray.500',
border: '1px solid',
borderColor: 'myGray.150',
borderRadius: 'sm',
w: 6,
h: 6,
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
};
const stateStyles = isDisabled
? {
cursor: 'not-allowed',
opacity: 0.5,
_hover: {}
}
: {
cursor: 'pointer',
opacity: 1,
_hover: { bg: 'myGray.100' },
onClick
};
return (
<Flex {...baseStyles} {...stateStyles}>
<MyIcon name={isUp ? `common/solidChevronUp` : `common/solidChevronDown`} w={'18px'} />
</Flex>
);
};
export default NavButton;

View File

@@ -0,0 +1,163 @@
import { ScoreItemType } from '@/components/core/dataset/QuoteItem';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ScoreTag from './ScoreTag';
import Markdown from '@/components/Markdown';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
const QuoteItem = ({
index,
icon,
sourceName,
score,
q,
a
}: {
index: number;
icon: string;
sourceName: string;
score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] };
q: string;
a?: string;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const isDeleted = !q;
return (
<Box
p={2}
position={'relative'}
overflow={'hidden'}
border={'1px solid transparent'}
borderBottomColor={'myGray.150'}
wordBreak={'break-all'}
fontSize={'sm'}
_hover={{
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
borderTopColor: 'myGray.50',
'& .hover-data': { visibility: 'visible' }
}}
>
<Flex gap={2} alignItems={'center'} mb={2}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
display={'inline-flex'}
height={6}
>
<Flex
color={'myGray.500'}
bg={'myGray.150'}
w={4}
justifyContent={'center'}
fontSize={'10px'}
h={'full'}
alignItems={'center'}
>
{index + 1}
</Flex>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className="textEllipsis3"
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceName}
</Box>
</Flex>
</Box>
{score && !isDeleted && (
<Box className="hover-data" visibility={'hidden'}>
<ScoreTag {...score} />
</Box>
)}
</Flex>
{!isDeleted ? (
<>
<Markdown source={q} />
{!!a && (
<Box>
<Markdown source={a} />
</Box>
)}
</>
) : (
<Flex
justifyContent={'center'}
alignItems={'center'}
h={'full'}
py={2}
bg={'#FAFAFA'}
color={'myGray.500'}
>
<MyIcon name="common/info" w={'14px'} mr={1} color={'myGray.500'} />
{t('chat:chat.quote.deleted')}
</Flex>
)}
<Flex
className="hover-data"
position={'absolute'}
bottom={2}
right={5}
gap={1.5}
visibility={'hidden'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={2}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{q.length + (a?.length || 0)}
</Flex>
</MyTooltip>
<MyTooltip label={t('common:common.Copy')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() => {
copyData(q + '\n' + a);
}}
>
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
</Flex>
</Box>
);
};
export default QuoteItem;

View File

@@ -0,0 +1,161 @@
import { Box, Flex } from '@chakra-ui/react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import QuoteItem from './QuoteItem';
import { useMemo } from 'react';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { formatScore } from '@/components/core/dataset/QuoteItem';
import { GetAllQuoteDataProps } from '@/web/core/chat/context/chatItemContext';
import { getQuoteDataList } from '@/web/core/chat/api';
const QuoteReader = ({
rawSearch,
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
metadata: GetAllQuoteDataProps;
onClose: () => void;
}) => {
const { t } = useTranslation();
const filterRawSearch = useMemo(() => {
return rawSearch.filter((item) => metadata.collectionIdList.includes(item.collectionId));
}, [rawSearch, metadata.collectionIdList]);
const { data: quoteList, loading } = useRequest2(
async () =>
await getQuoteDataList({
datasetDataIdList: filterRawSearch.map((item) => item.id),
collectionIdList: metadata.collectionIdList,
chatItemDataId: metadata.chatItemDataId,
appId: metadata.appId,
chatId: metadata.chatId,
...metadata.outLinkAuthData
}),
{
refreshDeps: [metadata, filterRawSearch],
manual: false
}
);
const formatedDataList = useMemo(() => {
return filterRawSearch
.map((searchItem) => {
const dataItem = quoteList?.find((item) => item._id === searchItem.id);
return {
id: searchItem.id,
q: dataItem?.q || 'Can not find Data',
a: dataItem?.a || '',
score: formatScore(searchItem.score),
sourceName: searchItem?.sourceName || '',
icon: getSourceNameIcon({
sourceId: searchItem.sourceId,
sourceName: searchItem.sourceName
})
};
})
.sort((a, b) => {
return (b.score.primaryScore?.value || 0) - (a.score.primaryScore?.value || 0);
});
}, [quoteList, filterRawSearch]);
return (
<Flex flexDirection={'column'} h={'full'}>
{/* title */}
<Flex
w={'full'}
alignItems={'center'}
px={5}
borderBottom={'1px solid'}
borderColor={'myGray.150'}
>
<Box flex={1} py={4}>
<Flex gap={2} mr={2} mb={1}>
{metadata.sourceId ? (
<>
<MyIcon
name={
getSourceNameIcon({
sourceId: metadata.sourceId,
sourceName: metadata.sourceName || ''
}) as any
}
w={['1rem', '1.25rem']}
color={'primary.600'}
/>
<Box
ml={1}
maxW={['200px', '220px']}
className={'textEllipsis'}
wordBreak={'break-all'}
fontSize={'sm'}
color={'myGray.900'}
fontWeight={'medium'}
>
{metadata.sourceName || t('common:common.UnKnow Source')}
</Box>
</>
) : (
<>
<MyIcon
name={'core/chat/quoteFill'}
w={['1rem', '1.25rem']}
color={'primary.600'}
/>
<Box
maxW={['200px', '300px']}
className={'textEllipsis'}
wordBreak={'break-all'}
color={'myGray.900'}
fontWeight={'medium'}
>
{t('common:core.chat.Quote Amount', { amount: filterRawSearch.length })}
</Box>
</>
)}
</Flex>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('common:core.chat.quote.Quote Tip')}
</Box>
</Box>
<Box
cursor={'pointer'}
borderRadius={'sm'}
p={1}
_hover={{
bg: 'myGray.100'
}}
onClick={onClose}
>
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
</Box>
</Flex>
{/* quote list */}
<MyBox flex={'1 0 0'} mt={2} px={5} py={1} overflow={'auto'} isLoading={loading}>
{!loading && (
<Flex flexDir={'column'} gap={3}>
{formatedDataList?.map((item, index) => (
<QuoteItem
key={item.id}
index={index}
icon={item.icon}
sourceName={item.sourceName}
score={item.score}
q={item.q}
a={item.a}
/>
))}
</Flex>
)}
</MyBox>
</Flex>
);
};
export default QuoteReader;

View File

@@ -0,0 +1,78 @@
import { ScoreItemType, scoreTheme } from '@/components/core/dataset/QuoteItem';
import { Box, Flex, Progress } from '@chakra-ui/react';
import { SearchScoreTypeMap } from '@fastgpt/global/core/dataset/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
const ScoreTag = (score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] }) => {
const { t } = useTranslation();
return (
<Flex alignItems={'center'} flexWrap={'wrap'} gap={3}>
{score?.primaryScore && (
<MyTooltip
label={
score.secondaryScore.length ? (
<Flex flexDir={'column'} gap={4}>
{score.secondaryScore.map((item, i) => (
<Box fontSize={'sm'} key={i}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
</Box>
))}
</Flex>
) : (
t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)
)
}
>
<Flex
borderRadius={'sm'}
py={1}
px={2}
color={'green.600'}
bg={'green.50'}
alignItems={'center'}
fontSize={'11px'}
>
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: `: ${score.primaryScore.index + 1}`}
</Box>
</Flex>
</MyTooltip>
)}
</Flex>
);
};
export default ScoreTag;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { GetQuoteProps } from '@/web/core/chat/context/chatItemContext';
import CollectionQuoteReader from './CollectionQuoteReader';
import QuoteReader from './QuoteReader';
const ChatQuoteList = ({
rawSearch = [],
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
metadata: GetQuoteProps;
onClose: () => void;
}) => {
return (
<>
{'collectionId' in metadata && (
<CollectionQuoteReader rawSearch={rawSearch} metadata={metadata} onClose={onClose} />
)}
{'collectionIdList' in metadata && (
<QuoteReader rawSearch={rawSearch} metadata={metadata} onClose={onClose} />
)}
</>
);
};
export default ChatQuoteList;

View File

@@ -60,7 +60,7 @@ const ApiDatasetForm = ({
<Input
bg={'myWhite.600'}
placeholder={t('dataset:request_headers')}
maxLength={200}
maxLength={2000}
{...register('apiServer.authorization')}
/>
</Flex>

View File

@@ -5,7 +5,6 @@ import {
delOneDatasetDataById,
getDatasetCollectionById
} from '@/web/core/dataset/api';
import { useQuery } from '@tanstack/react-query';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';

View File

@@ -113,7 +113,7 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
],
[ImportDataSourceEnum.fileLink]: [
{
title: t('dataset:import_select_file')
title: t('dataset:import_select_link')
},
{
title: t('dataset:import_param_setting')

View File

@@ -36,19 +36,19 @@ 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 { useToast } from '@fastgpt/web/hooks/useToast';
function DataProcess() {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const { goToNext, processParamsForm, chunkSizeField, minChunkSize, maxChunkSize } =
useContextSelector(DatasetImportContext, (v) => v);
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
const { getValues, setValue, register, watch } = processParamsForm;
const { setValue, register, watch } = processParamsForm;
const trainingType = watch('trainingType');
const chunkSettingMode = watch('chunkSettingMode');
const qaPrompt = watch('qaPrompt');
const {
isOpen: isOpenCustomPrompt,
@@ -65,7 +65,7 @@ function DataProcess() {
value: key as DatasetCollectionDataProcessModeEnum,
tooltip: t(value.tooltip as any)
}));
}, []);
}, [t]);
const Title = useCallback(({ title }: { title: string }) => {
return (
@@ -159,23 +159,36 @@ function DataProcess() {
gridTemplateColumns={'repeat(2, 1fr)'}
/>
</Box>
{trainingType === DatasetCollectionDataProcessModeEnum.chunk && feConfigs?.isPlus && (
{trainingType === DatasetCollectionDataProcessModeEnum.chunk && (
<Box mt={6}>
<Box fontSize={'sm'} mb={2} color={'myGray.600'}>
{t('dataset:enhanced_indexes')}
</Box>
<HStack gap={[3, 7]}>
<HStack flex={'1'} spacing={1}>
<Checkbox {...register('autoIndexes')}>
<FormLabel>{t('dataset:auto_indexes')}</FormLabel>
</Checkbox>
<MyTooltip
label={!feConfigs?.isPlus ? t('common:commercial_function_tip') : ''}
>
<Checkbox isDisabled={!feConfigs?.isPlus} {...register('autoIndexes')}>
<FormLabel>{t('dataset:auto_indexes')}</FormLabel>
</Checkbox>
</MyTooltip>
<QuestionTip label={t('dataset:auto_indexes_tips')} />
</HStack>
<HStack flex={'1'} spacing={1}>
<MyTooltip
label={!datasetDetail?.vlmModel ? t('common:error_vlm_not_config') : ''}
label={
!feConfigs?.isPlus
? t('common:commercial_function_tip')
: !datasetDetail?.vlmModel
? t('common:error_vlm_not_config')
: ''
}
>
<Checkbox isDisabled={!datasetDetail?.vlmModel} {...register('imageIndex')}>
<Checkbox
isDisabled={!feConfigs?.isPlus || !datasetDetail?.vlmModel}
{...register('imageIndex')}
>
<FormLabel>{t('dataset:image_auto_parse')}</FormLabel>
</Checkbox>
</MyTooltip>
@@ -271,7 +284,7 @@ function DataProcess() {
}
}}
>
{getValues('qaPrompt')}
{qaPrompt}
<Box
display={'none'}
@@ -320,44 +333,6 @@ function DataProcess() {
</AccordionPanel>
</AccordionItem>
{/* <AccordionItem mt={4} border={'none'}>
<Title title={t('dataset:import_model_config')} />
<AccordionPanel p={2} fontSize={'sm'}>
<Box>
<Box>{t('common:core.ai.model.Dataset Agent Model')}</Box>
<Box mt={1}>
<AIModelSelector
w={'100%'}
value={llmModel}
list={datasetModelList.map((item) => ({
label: item.name,
value: item.model
}))}
onchange={(e) => {
setValue('llmModel', e);
}}
/>
</Box>
</Box>
<Box pt={5}>
<Box>{t('dataset:vllm_model')}</Box>
<Box mt={1}>
<AIModelSelector
w={'100%'}
value={vlmModel}
list={vllmModelList.map((item) => ({
label: item.name,
value: item.model
}))}
onchange={(e) => {
setValue('vlmModel', e);
}}
/>
</Box>
</Box>
</AccordionPanel>
</AccordionItem> */}
<Flex mt={5} gap={3} justifyContent={'flex-end'}>
<Button
onClick={() => {
@@ -372,7 +347,7 @@ function DataProcess() {
{isOpenCustomPrompt && (
<PromptTextarea
defaultValue={getValues('qaPrompt')}
defaultValue={qaPrompt}
onChange={(e) => {
setValue('qaPrompt', e);
}}

View File

@@ -21,6 +21,7 @@ const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
loading: () => <Loading fixed={false} />
});
const Upload = dynamic(() => import('../commonProgress/Upload'));
const PreviewData = dynamic(() => import('../commonProgress/PreviewData'));
const APIDatasetCollection = () => {
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
@@ -29,7 +30,8 @@ const APIDatasetCollection = () => {
<>
{activeStep === 0 && <CustomAPIFileInput />}
{activeStep === 1 && <DataProcess />}
{activeStep === 2 && <Upload />}
{activeStep === 2 && <PreviewData />}
{activeStep === 3 && <Upload />}
</>
);
};

View File

@@ -27,6 +27,7 @@ const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
loading: () => <Loading fixed={false} />
});
const Upload = dynamic(() => import('../commonProgress/Upload'));
const PreviewData = dynamic(() => import('../commonProgress/PreviewData'));
const ExternalFileCollection = () => {
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
@@ -35,7 +36,8 @@ const ExternalFileCollection = () => {
<>
{activeStep === 0 && <CustomLinkInput />}
{activeStep === 1 && <DataProcess />}
{activeStep === 2 && <Upload />}
{activeStep === 2 && <PreviewData />}
{activeStep === 3 && <Upload />}
</>
);
};

View File

@@ -13,6 +13,7 @@ const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
loading: () => <Loading fixed={false} />
});
const Upload = dynamic(() => import('../commonProgress/Upload'));
const PreviewData = dynamic(() => import('../commonProgress/PreviewData'));
const CustomTet = () => {
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
@@ -20,7 +21,8 @@ const CustomTet = () => {
<>
{activeStep === 0 && <CustomTextInput />}
{activeStep === 1 && <DataProcess />}
{activeStep === 2 && <Upload />}
{activeStep === 2 && <PreviewData />}
{activeStep === 3 && <Upload />}
</>
);
};

View File

@@ -16,6 +16,7 @@ const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
loading: () => <Loading fixed={false} />
});
const Upload = dynamic(() => import('../commonProgress/Upload'));
const PreviewData = dynamic(() => import('../commonProgress/PreviewData'));
const LinkCollection = () => {
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
@@ -24,7 +25,8 @@ const LinkCollection = () => {
<>
{activeStep === 0 && <CustomLinkImport />}
{activeStep === 1 && <DataProcess />}
{activeStep === 2 && <Upload />}
{activeStep === 2 && <PreviewData />}
{activeStep === 3 && <Upload />}
</>
);
};

View File

@@ -57,7 +57,7 @@ const ReTraining = () => {
qaChunkSize: collection.chunkSize,
customSplitChar: collection.chunkSplitter,
qaPrompt: collection.qaPrompt,
webSelector: collection.metadata?.webSelector
webSelector: collection.metadata?.webPageSelector
});
}
});

View File

@@ -195,7 +195,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
label: item.name,
value: item.model
}))}
onchange={(e) => {
onChange={(e) => {
const vectorModel = embeddingModelList.find((item) => item.model === e);
if (!vectorModel) return;
return onOpenConfirmRebuild(async () => {
@@ -220,7 +220,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
value: item.model
}))}
fontSize={'mini'}
onchange={(e) => {
onChange={(e) => {
const agentModel = datasetModelList.find((item) => item.model === e);
if (!agentModel) return;
setValue('agentModel', agentModel);
@@ -230,30 +230,28 @@ const Info = ({ datasetId }: { datasetId: string }) => {
</Box>
</Box>
{feConfigs?.isPlus && (
<Box pt={5}>
<FormLabel fontSize={'mini'} fontWeight={'500'}>
{t('dataset:vllm_model')}
</FormLabel>
<Box pt={2}>
<AIModelSelector
w={'100%'}
value={vlmModel?.model}
list={vllmModelList.map((item) => ({
label: item.name,
value: item.model
}))}
fontSize={'mini'}
onchange={(e) => {
const vlmModel = vllmModelList.find((item) => item.model === e);
if (!vlmModel) return;
setValue('vlmModel', vlmModel);
return handleSubmit((data) => onSave({ ...data, vlmModel }))();
}}
/>
</Box>
<Box pt={5}>
<FormLabel fontSize={'mini'} fontWeight={'500'}>
{t('dataset:vllm_model')}
</FormLabel>
<Box pt={2}>
<AIModelSelector
w={'100%'}
value={vlmModel?.model}
list={vllmModelList.map((item) => ({
label: item.name,
value: item.model
}))}
fontSize={'mini'}
onChange={(e) => {
const vlmModel = vllmModelList.find((item) => item.model === e);
if (!vlmModel) return;
setValue('vlmModel', vlmModel);
return handleSubmit((data) => onSave({ ...data, vlmModel }))();
}}
/>
</Box>
)}
</Box>
{feConfigs?.isPlus && (
<Flex alignItems={'center'} pt={5}>

View File

@@ -1,12 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, Button, Textarea } from '@chakra-ui/react';
import {
FieldArrayWithId,
UseFieldArrayRemove,
UseFormRegister,
useFieldArray,
useForm
} from 'react-hook-form';
import { Box, Flex, Button, Textarea, ModalFooter, HStack, VStack } from '@chakra-ui/react';
import { UseFormRegister, useFieldArray, useForm } from 'react-hook-form';
import {
postInsertData2Dataset,
putDatasetDataById,
@@ -17,36 +11,36 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
import { defaultCollectionDetail } from '@/web/core/dataset/constants';
import { getDocPath } from '@/web/common/system/doc';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import styles from './styles.module.scss';
import {
DatasetDataIndexTypeEnum,
getDatasetIndexMapData
} from '@fastgpt/global/core/dataset/data/constants';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
export type InputDataType = {
q: string;
a: string;
indexes: (Omit<DatasetDataIndexItemType, 'dataId'> & {
dataId?: string; // pg data id
fold: boolean;
})[];
};
enum TabEnum {
content = 'content',
index = 'index'
chunk = 'chunk',
qa = 'qa'
}
const InputDataModal = ({
@@ -64,73 +58,47 @@ const InputDataModal = ({
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [currentTab, setCurrentTab] = useState(TabEnum.content);
const { embeddingModelList, defaultModels } = useSystemStore();
const [currentTab, setCurrentTab] = useState(TabEnum.chunk);
const { register, handleSubmit, reset, control } = useForm<InputDataType>();
const {
fields: indexes,
append: appendIndexes,
remove: removeIndexes
prepend: prependIndexes,
remove: removeIndexes,
update: updateIndexes
} = useFieldArray({
control,
name: 'indexes'
});
const tabList = [
{
label: (
<Flex align={'center'}>
<Box>{t('common:dataset.data.edit.divide_content')}</Box>
</Flex>
),
value: TabEnum.content
},
{
label: (
<Flex align={'center'}>
<Box>{t('common:dataset.data.edit.Index', { amount: indexes.length })}</Box>
<MyTooltip label={t('common:core.app.tool_label.view_doc')}>
<MyIcon
name={'book'}
w={'1rem'}
mr={'0.38rem'}
color={'myGray.500'}
ml={1}
onClick={() =>
window.open(getDocPath('/docs/guide/knowledge_base/dataset_engine/'), '_blank')
}
_hover={{
color: 'primary.600',
cursor: 'pointer'
}}
/>
</MyTooltip>
</Flex>
),
value: TabEnum.index
}
];
const { data: collection = defaultCollectionDetail } = useQuery(
['loadCollectionId', collectionId],
const { data: collection = defaultCollectionDetail } = useRequest2(
() => {
return getDatasetCollectionById(collectionId);
},
{
manual: false,
refreshDeps: [collectionId]
}
);
const { isFetching: isFetchingData } = useQuery(
['getDatasetDataItemById', dataId],
() => {
const { loading: isFetchingData } = useRequest2(
async () => {
if (dataId) return getDatasetDataItemById(dataId);
return null;
},
{
manual: false,
refreshDeps: [dataId],
onSuccess(res) {
if (res) {
reset({
q: res.q,
a: res.a,
indexes: res.indexes
indexes: res.indexes.map((item) => ({
...item,
fold: true
}))
});
} else if (defaultValue) {
reset({
@@ -138,6 +106,10 @@ const InputDataModal = ({
a: defaultValue.a
});
}
if (res?.a || defaultValue?.a) {
setCurrentTab(TabEnum.qa);
}
},
onError(err) {
toast({
@@ -161,7 +133,6 @@ const InputDataModal = ({
const { runAsync: sureImportData, loading: isImporting } = useRequest2(
async (e: InputDataType) => {
if (!e.q) {
setCurrentTab(TabEnum.content);
return Promise.reject(t('common:dataset.data.input is empty'));
}
@@ -175,9 +146,9 @@ const InputDataModal = ({
const dataId = await postInsertData2Dataset({
collectionId: collection._id,
q: e.q,
a: e.a,
a: currentTab === TabEnum.qa ? e.a : '',
// Contains no default index
indexes: e.indexes
indexes: e.indexes.filter((item) => !!item.text?.trim())
});
return {
@@ -186,6 +157,7 @@ const InputDataModal = ({
};
},
{
refreshDeps: [currentTab],
successToast: t('common:dataset.data.Input Success Tip'),
onSuccess(e) {
reset({
@@ -205,11 +177,11 @@ const InputDataModal = ({
async (e: InputDataType) => {
if (!dataId) return Promise.reject(t('common:common.error.unKnow'));
// not exactly same
await putDatasetDataById({
dataId,
...e,
indexes: e.indexes
q: e.q,
a: currentTab === TabEnum.qa ? e.a : '',
indexes: e.indexes.filter((item) => !!item.text?.trim())
});
return {
@@ -218,6 +190,7 @@ const InputDataModal = ({
};
},
{
refreshDeps: [currentTab],
successToast: t('common:dataset.data.Update Success Tip'),
onSuccess(data) {
onSuccess(data);
@@ -267,49 +240,174 @@ const InputDataModal = ({
isLoading={isLoading}
h={'100%'}
py={[6, '1.5rem']}
px={[5, '3.25rem']}
>
<Flex justify={'space-between'} gap={4} w={'100%'}>
<Flex justify={'space-between'} pb={4}>
<LightRowTabs<TabEnum>
list={tabList}
p={0}
value={currentTab}
onChange={(e: TabEnum) => setCurrentTab(e)}
/>
</Flex>
{currentTab === TabEnum.index && (
<Button
variant={'whiteBase'}
boxShadow={'1'}
p={0}
onClick={() =>
appendIndexes({
type: DatasetDataIndexTypeEnum.custom,
text: ''
})
}
>
<Flex px={'0.62rem'} py={2}>
<MyIcon name={'common/addLight'} w={'1rem'} mr={'0.38rem'} />
{t('common:add_new')}
</Flex>
</Button>
)}
</Flex>
<Box w={'100%'} flexGrow={1} overflow={'scroll'}>
{currentTab === TabEnum.content && <InputTab maxToken={maxToken} register={register} />}
{currentTab === TabEnum.index && (
<DataIndex
register={register}
maxToken={maxToken}
removeIndexes={removeIndexes}
indexes={indexes}
/>
)}
{/* Tab */}
<Box px={[5, '3.25rem']}>
<FillRowTabs
list={[
{ label: t('common:dataset_data_input_chunk'), value: TabEnum.chunk },
{ label: t('common:dataset_data_input_qa'), value: TabEnum.qa }
]}
py={1}
value={currentTab}
onChange={(e) => {
setCurrentTab(e);
}}
/>
</Box>
<Flex justifyContent={'flex-end'} pt={8} pb={[8, 0]} h={[24, 16]}>
<Flex flex={'1 0 0'} h={['auto', '0']} gap={6} flexDir={['column', 'row']} px={[5, '0']}>
{/* Data */}
<Flex
pt={4}
pl={[0, '3.25rem']}
flexDir={'column'}
h={'100%'}
gap={3}
flex={'1 0 0'}
w={['100%', 0]}
overflow={['unset', 'auto']}
>
<Flex flexDir={'column'} h={'100%'}>
<FormLabel required mb={1} h={'30px'}>
{currentTab === TabEnum.chunk
? t('common:dataset_data_input_chunk_content')
: t('common:dataset_data_input_q')}
</FormLabel>
<Textarea
resize={'none'}
placeholder={t('common:dataset_data_import_q_placeholder', { maxToken })}
className={styles.scrollbar}
maxLength={maxToken}
flex={'1 0 0'}
tabIndex={1}
_focus={{
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
bg={'myGray.25'}
borderRadius={'md'}
borderColor={'myGray.200'}
{...register(`q`, {
required: true
})}
/>
</Flex>
{currentTab === TabEnum.qa && (
<Flex flexDir={'column'} h={'100%'}>
<FormLabel required mb={1}>
{t('common:dataset_data_input_a')}
</FormLabel>
<Textarea
resize={'none'}
placeholder={t('common:dataset_data_import_q_placeholder', { maxToken })}
className={styles.scrollbar}
flex={'1 0 0'}
tabIndex={1}
bg={'myGray.25'}
maxLength={maxToken}
borderRadius={'md'}
border={'1.5px solid '}
borderColor={'myGray.200'}
{...register('a', { required: true })}
/>
</Flex>
)}
</Flex>
{/* Index */}
<Box
pt={4}
pr={[0, '3.25rem']}
flex={'1 0 0'}
w={['100%', 0]}
overflow={['unset', 'auto']}
>
<Flex alignItems={'flex-start'} justifyContent={'space-between'} h={'30px'}>
<FormLabel>
{t('common:dataset.data.edit.Index', {
amount: indexes.length
})}
</FormLabel>
<Button
variant={'whiteBase'}
size={'sm'}
p={0}
transform={'translateY(-6px)'}
onClick={() =>
prependIndexes({
type: DatasetDataIndexTypeEnum.custom,
text: '',
fold: false
})
}
>
<Flex px={'0.62rem'} py={2}>
<MyIcon name={'common/addLight'} w={'1rem'} mr={'0.38rem'} />
{t('common:add_new')}
</Flex>
</Button>
</Flex>
<VStack>
{indexes?.map((index, i) => {
const data = getDatasetIndexMapData(index.type);
return (
<Box
key={index.dataId || i}
p={4}
borderRadius={'md'}
border={'base'}
bg={'myGray.25'}
w={'100%'}
_hover={{
'& .delete': {
display: 'block'
}
}}
>
{/* Header */}
<Flex mb={2} alignItems={'center'}>
<FormLabel flex={'1 0 0'}>{t(data.label)}</FormLabel>
{/* Delete */}
{index.type !== 'default' && (
<HStack className={'delete'} borderRight={'base'} pr={3} mr={2}>
<DeleteIcon
onClick={() => {
removeIndexes(i);
}}
/>
</HStack>
)}
{indexes.length > 1 && (
<MyIconButton
icon={index.fold ? 'core/chat/chevronDown' : 'core/chat/chevronUp'}
onClick={() => {
updateIndexes(i, { ...index, fold: !index.fold });
}}
/>
)}
</Flex>
{/* Content */}
<DataIndexTextArea
disabled={index.type === 'default'}
index={i}
value={index.text}
isFolder={index.fold && indexes.length > 1}
maxToken={maxToken}
register={register}
onFocus={() => {
updateIndexes(i, { ...index, fold: false });
}}
/>
</Box>
);
})}
</VStack>
</Box>
</Flex>
<ModalFooter px={[5, '3.25rem']} py={0} pt={4}>
<MyTooltip
label={collection.permission.hasWritePer ? '' : t('common:dataset.data.Can not edit')}
>
@@ -322,7 +420,7 @@ const InputDataModal = ({
{dataId ? t('common:common.Confirm Update') : t('common:common.Confirm Import')}
</Button>
</MyTooltip>
</Flex>
</ModalFooter>
</MyBox>
</MyModal>
);
@@ -330,153 +428,23 @@ const InputDataModal = ({
export default React.memo(InputDataModal);
const InputTab = ({
maxToken,
register
}: {
maxToken: number;
register: UseFormRegister<InputDataType>;
}) => {
const { t } = useTranslation();
return (
<>
<Flex h={'100%'} gap={6} flexDir={['column', 'row']} w={'100%'}>
<Flex flexDir={'column'} flex={1}>
<Flex mb={2} fontWeight={'medium'} fontSize={'sm'} alignItems={'center'} h={8}>
<Box color={'red.600'}>*</Box>
<Box color={'myGray.900'}>{t('common:core.dataset.data.Main Content')}</Box>
<QuestionTip label={t('common:core.dataset.data.Data Content Tip')} ml={1} />
</Flex>
<Box
borderRadius={'md'}
border={'1.5px solid var(--Gray-Modern-200, #E8EBF0)'}
bg={'myGray.25'}
flex={1}
>
<Textarea
resize={'none'}
placeholder={t('core.dataset.data.Data Content Placeholder', { maxToken })}
className={styles.scrollbar}
maxLength={maxToken}
h={'100%'}
tabIndex={1}
_focus={{
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
borderColor={'transparent'}
bg={'myGray.25'}
{...register(`q`, {
required: true
})}
/>
</Box>
</Flex>
<Flex flex={1} flexDir={'column'}>
<Flex mb={2} fontWeight={'medium'} fontSize={'sm'} alignItems={'center'} h={8}>
<Box color={'myGray.900'}>{t('common:core.dataset.data.Auxiliary Data')}</Box>
<QuestionTip label={t('common:core.dataset.data.Auxiliary Data Tip')} ml={1} />
</Flex>
<Box
borderRadius={'md'}
border={'1.5px solid '}
borderColor={'myGray.200'}
bg={'myGray.25'}
flex={1}
>
<Textarea
resize={'none'}
placeholder={t('core.dataset.data.Auxiliary Data Placeholder', {
maxToken: maxToken * 1.5
})}
className={styles.scrollbar}
borderColor={'transparent'}
h={'100%'}
tabIndex={1}
bg={'myGray.25'}
maxLength={maxToken * 1.5}
{...register('a')}
/>
</Box>
</Flex>
</Flex>
</>
);
};
const DataIndex = ({
maxToken,
register,
indexes,
removeIndexes
}: {
maxToken: number;
register: UseFormRegister<InputDataType>;
indexes: FieldArrayWithId<InputDataType, 'indexes', 'id'>[];
removeIndexes: UseFieldArrayRemove;
}) => {
const { t } = useTranslation();
return (
<>
<Flex mt={3} gap={3} flexDir={'column'}>
{indexes?.map((index, i) => {
const data = getDatasetIndexMapData(index.type);
return (
<Box
key={index.dataId || i}
p={4}
borderRadius={'md'}
border={'1.5px solid var(--Gray-Modern-200, #E8EBF0)'}
bg={'myGray.25'}
_hover={{
'& .delete': {
display: 'block'
}
}}
>
<Flex mb={2}>
<Box flex={1} fontWeight={'medium'} fontSize={'sm'} color={'myGray.900'}>
{t(data.label)}
</Box>
{index.type !== 'default' && (
<DeleteIcon
onClick={() => {
removeIndexes(i);
}}
/>
)}
</Flex>
<DataIndexTextArea
disabled={index.type === 'default'}
index={i}
value={index.text}
maxToken={maxToken}
register={register}
/>
</Box>
);
})}
</Flex>
</>
);
};
const textareaMinH = '40px';
const DataIndexTextArea = ({
value,
index,
maxToken,
register,
disabled
disabled,
isFolder,
onFocus
}: {
value: string;
index: number;
maxToken: number;
register: UseFormRegister<InputDataType>;
disabled?: boolean;
isFolder: boolean;
onFocus: () => void;
}) => {
const { t } = useTranslation();
const TextareaDom = useRef<HTMLTextAreaElement | null>(null);
@@ -501,41 +469,76 @@ const DataIndexTextArea = ({
}
}, []);
return disabled ? (
<Box fontSize={'sm'} color={'myGray.500'} whiteSpace={'pre-wrap'}>
{value}
const onclickMark = () => {
TextareaDom?.current?.focus();
onFocus();
};
return (
<Box
pos={'relative'}
{...(isFolder
? {
maxH: '50px',
overflow: 'hidden'
}
: {
maxH: 'auto'
})}
>
{disabled ? (
<Box fontSize={'sm'} color={'myGray.500'} whiteSpace={'pre-wrap'}>
{value}
</Box>
) : (
<Textarea
maxLength={maxToken}
borderColor={'transparent'}
className={styles.scrollbar}
minH={textareaMinH}
px={0}
pt={0}
isRequired={required}
whiteSpace={'pre-wrap'}
resize={'none'}
_focus={{
px: 3,
py: 1,
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
placeholder={t('common:dataset.data.Index Placeholder')}
ref={(e) => {
if (e) TextareaDom.current = e;
TextareaRef(e);
}}
required
name={name}
onChange={(e) => {
autoHeight(e);
onTextChange(e);
}}
onFocus={autoHeight}
onBlur={onBlur}
/>
)}
{isFolder && (
<Box
pos={'absolute'}
bottom={0}
left={0}
right={0}
top={0}
bg={'linear-gradient(182deg, rgba(251, 251, 252, 0.00) 1.76%, #FBFBFC 84.07%)'}
{...(disabled
? {}
: {
cursor: 'pointer',
onClick: onclickMark
})}
/>
)}
</Box>
) : (
<Textarea
maxLength={maxToken}
borderColor={'transparent'}
className={styles.scrollbar}
minH={textareaMinH}
px={0}
pt={0}
isRequired={required}
whiteSpace={'pre-wrap'}
resize={'none'}
_focus={{
px: 3,
py: 1,
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
placeholder={t('common:dataset.data.Index Placeholder')}
ref={(e) => {
if (e) TextareaDom.current = e;
TextareaRef(e);
}}
required
name={name}
onChange={(e) => {
autoHeight(e);
onTextChange(e);
}}
onFocus={autoHeight}
onBlur={onBlur}
/>
);
};

View File

@@ -6,7 +6,6 @@ import { useRouter } from 'next/router';
import { useContextSelector } from 'use-context-selector';
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { useI18n } from '@/web/context/I18n';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import ParentPaths from '@/components/common/ParentPaths';
@@ -22,7 +21,6 @@ export enum TabEnum {
const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
const theme = useTheme();
const { t } = useTranslation();
const { datasetT } = useI18n();
const router = useRouter();
const query = router.query;
const { isPc } = useSystem();
@@ -168,7 +166,7 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
{rebuildingCount > 0 && (
<Box mb={3}>
<Box fontSize={'sm'}>
{datasetT('rebuilding_index_count', { count: rebuildingCount })}
{t('dataset:rebuilding_index_count', { count: rebuildingCount })}
</Box>
</Box>
)}

View File

@@ -36,9 +36,14 @@ type FormType = {
inputText: string;
searchParams: {
searchMode: `${DatasetSearchModeEnum}`;
embeddingWeight?: number;
usingReRank?: boolean;
rerankModel?: string;
rerankWeight?: number;
similarity?: number;
limit?: number;
usingReRank?: boolean;
datasetSearchUsingExtensionQuery?: boolean;
datasetSearchExtensionModel?: string;
datasetSearchExtensionBg?: string;
@@ -53,7 +58,6 @@ const Test = ({ datasetId }: { datasetId: string }) => {
const { pushDatasetTestItem } = useSearchTestStore();
const [inputType, setInputType] = useState<'text' | 'file'>('text');
const [datasetTestItem, setDatasetTestItem] = useState<SearchTestStoreItemType>();
const [refresh, setRefresh] = useState(false);
const [isFocus, setIsFocus] = useState(false);
const { File, onOpen } = useSelectFile({
fileType: '.csv',
@@ -66,7 +70,10 @@ const Test = ({ datasetId }: { datasetId: string }) => {
inputText: '',
searchParams: {
searchMode: DatasetSearchModeEnum.embedding,
embeddingWeight: 0.5,
usingReRank: false,
rerankModel: defaultModels?.rerank?.model,
rerankWeight: 0.5,
limit: 5000,
similarity: 0,
datasetSearchUsingExtensionQuery: false,
@@ -77,6 +84,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
});
const searchModeData = DatasetSearchModeMap[getValues(`searchParams.searchMode`)];
const searchParams = getValues('searchParams');
const {
isOpen: isOpenSelectMode,
@@ -186,7 +194,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
// }
]}
value={inputType}
onchange={(e) => setInputType(e)}
onChange={(e) => setInputType(e)}
/>
<Button
@@ -294,15 +302,14 @@ const Test = ({ datasetId }: { datasetId: string }) => {
{isOpenSelectMode && (
<DatasetParamsModal
{...getValues('searchParams')}
{...searchParams}
maxTokens={20000}
onClose={onCloseSelectMode}
onSuccess={(e) => {
setValue('searchParams', {
...getValues('searchParams'),
...searchParams,
...e
});
setRefresh((state) => !state);
}}
/>
)}

View File

@@ -205,7 +205,7 @@ const CreateModal = ({
label: item.name,
value: item.model
}))}
onchange={(e) => {
onChange={(e) => {
setValue('vectorModel' as const, e);
}}
/>
@@ -237,45 +237,43 @@ const CreateModal = ({
label: item.name,
value: item.model
}))}
onchange={(e) => {
onChange={(e) => {
setValue('agentModel', e);
}}
/>
</Box>
</Flex>
{feConfigs?.isPlus && (
<Flex
mt={6}
alignItems={['flex-start', 'center']}
justify={'space-between'}
flexDir={['column', 'row']}
<Flex
mt={6}
alignItems={['flex-start', 'center']}
justify={'space-between'}
flexDir={['column', 'row']}
>
<HStack
spacing={1}
flex={['', '0 0 110px']}
fontSize={'sm'}
color={'myGray.900'}
fontWeight={500}
pb={['12px', '0']}
>
<HStack
spacing={1}
flex={['', '0 0 110px']}
fontSize={'sm'}
color={'myGray.900'}
fontWeight={500}
pb={['12px', '0']}
>
<Box>{t('dataset:vllm_model')}</Box>
</HStack>
<Box w={['100%', '300px']}>
<AIModelSelector
w={['100%', '300px']}
value={vlmModel}
list={vllmModelList.map((item) => ({
label: item.name,
value: item.model
}))}
onchange={(e) => {
setValue('vlmModel', e);
}}
/>
</Box>
</Flex>
)}
<Box>{t('dataset:vllm_model')}</Box>
</HStack>
<Box w={['100%', '300px']}>
<AIModelSelector
w={['100%', '300px']}
value={vlmModel}
list={vllmModelList.map((item) => ({
label: item.name,
value: item.model
}))}
onChange={(e) => {
setValue('vlmModel', e);
}}
/>
</Box>
</Flex>
{/* @ts-ignore */}
<ApiDatasetForm type={type} form={form} />