feat: question guide (#1508)

* feat: question guide

* fix

* fix

* fix

* change interface

* fix
This commit is contained in:
heheer
2024-05-19 17:34:16 +08:00
committed by GitHub
parent fd31a0b763
commit e35ce2caa0
40 changed files with 1071 additions and 34 deletions

View File

@@ -16,6 +16,11 @@ import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '.
import { textareaMinH } from './constants';
import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { useChatProviderStore } from './Provider';
import QuestionGuide from './components/QustionGuide';
import { useQuery } from '@tanstack/react-query';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const MessageInput = ({
@@ -53,6 +58,7 @@ const MessageInput = ({
const { isPc, whisperModel } = useSystemStore();
const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation();
const { appDetail } = useAppStore();
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
@@ -205,6 +211,23 @@ const MessageInput = ({
startSpeak(finishWhisperTranscription);
}, [finishWhisperTranscription, isSpeaking, startSpeak, stopSpeak]);
const { data } = useQuery(
[appId, inputValue],
async () => {
if (!appId) return { list: [], total: 0 };
return getMyQuestionGuides({
appId,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 5,
current: 1,
searchKey: inputValue
});
},
{
enabled: !!appId
}
);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
@@ -214,7 +237,7 @@ const MessageInput = ({
boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`}
borderRadius={['none', 'md']}
bg={'white'}
overflow={'hidden'}
overflow={'display'}
{...(isPc
? {
border: '1px solid',
@@ -243,6 +266,21 @@ const MessageInput = ({
{t('core.chat.Converting to text')}
</Flex>
{/* popup */}
{havInput && (
<QuestionGuide
guides={data?.list || []}
setDropdownValue={(value) => setValue('input', value)}
bottom={'100%'}
top={'auto'}
left={0}
right={0}
mb={2}
overflowY={'auto'}
boxShadow={'sm'}
/>
)}
{/* file preview */}
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
{fileList.map((item, index) => (
@@ -377,7 +415,12 @@ const MessageInput = ({
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
if (
(isPc || window !== parent) &&
e.keyCode === 13 &&
!e.shiftKey &&
!(havInput && data?.list.length && data?.list.length > 0)
) {
handleSend();
e.preventDefault();
}

View File

@@ -0,0 +1,98 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import React, { useCallback, useEffect } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
export default function QuestionGuide({
guides,
setDropdownValue,
...props
}: {
guides: string[];
setDropdownValue?: (value: string) => void;
} & BoxProps) {
const [highlightedIndex, setHighlightedIndex] = React.useState(0);
const { appT } = useI18n();
const handleKeyDown = useCallback(
(event: any) => {
if (event.keyCode === 38) {
setHighlightedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
} else if (event.keyCode === 40) {
setHighlightedIndex((prevIndex) => Math.min(prevIndex + 1, guides.length - 1));
} else if (event.keyCode === 13 && guides[highlightedIndex]) {
setDropdownValue?.(guides[highlightedIndex]);
event.preventDefault();
}
},
[highlightedIndex, setDropdownValue, guides]
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
return guides.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={'100%'}
w={'auto'}
zIndex={99999}
maxH={'300px'}
overflow={'auto'}
className="nowheel"
{...props}
>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'} gap={2} mb={2} px={2}>
<MyIcon name={'union'} />
<Box>{appT('modules.Input Guide')}</Box>
</Flex>
{guides.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item}
px={4}
py={3}
borderRadius={'sm'}
cursor={'pointer'}
maxH={'300px'}
overflow={'auto'}
_notLast={{
mb: 1
}}
{...(highlightedIndex === index
? {
bg: 'primary.50',
color: 'primary.600'
}
: {
bg: 'myGray.50',
color: 'myGray.600'
})}
onMouseDown={(e) => {
e.preventDefault();
setDropdownValue?.(item);
}}
onMouseEnter={() => {
setHighlightedIndex(index);
}}
>
<Box fontSize={'sm'}>{item}</Box>
</Flex>
))}
</Box>
) : null;
}

View File

@@ -58,7 +58,7 @@ import ChatProvider, { useChatProviderStore } from './Provider';
import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic';
import { useCreation, useUpdateEffect } from 'ahooks';
import { useCreation } from 'ahooks';
const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal'));

View File

@@ -0,0 +1,479 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import {
Box,
Button,
Flex,
ModalBody,
useDisclosure,
Switch,
Input,
Textarea,
InputGroup,
InputRightElement,
Checkbox,
useCheckboxGroup,
ModalFooter,
BoxProps
} from '@chakra-ui/react';
import React, { ChangeEvent, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import type { AppQuestionGuideTextConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useI18n } from '@/web/context/I18n';
import { fileDownload } from '@/web/common/file/utils';
import { getDocPath } from '@/web/common/system/doc';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useQuery } from '@tanstack/react-query';
const csvTemplate = `"第一列内容"
"必填列"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段"
`;
const QGuidesConfig = ({
value,
onChange
}: {
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
}) => {
const { t } = useTranslation();
const { appT, commonT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isOpenTexts, onOpen: onOpenTexts, onClose: onCloseTexts } = useDisclosure();
const isOpenQuestionGuide = value.open;
const { appDetail } = useAppStore();
const [searchKey, setSearchKey] = React.useState<string>('');
const { data } = useQuery(
[appDetail._id, searchKey],
async () => {
return getMyQuestionGuides({
appId: appDetail._id,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 30,
current: 1,
searchKey
});
},
{
enabled: !!appDetail._id
}
);
useEffect(() => {
onChange({
...value,
textList: data?.list || []
});
}, [data]);
const formLabel = useMemo(() => {
if (!isOpenQuestionGuide) {
return t('core.app.whisper.Close');
}
return t('core.app.whisper.Open');
}, [t, isOpenQuestionGuide]);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Box fontWeight={'medium'}>{appT('modules.Question Guide')}</Box>
<Box flex={1} />
<MyTooltip label={appT('modules.Config question guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={appT('modules.Question Guide')}
iconSrc="core/app/inputGuides"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody px={[5, 16]} pt={[4, 8]} w={'500px'}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{appT('modules.Question Guide Switch')}
<Switch
isChecked={isOpenQuestionGuide}
size={'lg'}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenQuestionGuide && (
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Question Guide Texts')}
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{value.textList.length || 0}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={() => {
onOpenTexts();
onClose();
}}
>
{appT('modules.Config Texts')}
</Button>
</Flex>
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Custom question guide URL')}
<Flex
onClick={() => window.open(getDocPath('/docs/course/custom_link'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} ml={4} mr={1} />
{commonT('common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<Textarea
mt={2}
bg={'myGray.50'}
defaultValue={value.customURL}
onBlur={(e) =>
onChange({
...value,
customURL: e.target.value
})
}
/>
</>
</>
)}
</ModalBody>
<ModalFooter px={[5, 16]} pb={[4, 8]}>
<Button onClick={() => onClose()}>{commonT('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
{isOpenTexts && (
<TextConfigModal
onCloseTexts={onCloseTexts}
onOpen={onOpen}
value={value}
onChange={onChange}
setSearchKey={setSearchKey}
/>
)}
</Flex>
);
};
export default React.memo(QGuidesConfig);
const TextConfigModal = ({
onCloseTexts,
onOpen,
value,
onChange,
setSearchKey
}: {
onCloseTexts: () => void;
onOpen: () => void;
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
setSearchKey: (key: string) => void;
}) => {
const { appT, commonT } = useI18n();
const fileInputRef = useRef<HTMLInputElement>(null);
const [checkboxValue, setCheckboxValue] = React.useState<string[]>([]);
const [isEditIndex, setIsEditIndex] = React.useState(-1);
const [isAdding, setIsAdding] = React.useState(false);
const [showIcons, setShowIcons] = React.useState<number | null>(null);
const { getCheckboxProps } = useCheckboxGroup();
const handleFileSelected = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
const content = e.target?.result as string;
const rows = content.split('\n');
const texts = rows.map((row) => row.split(',')[0]);
const newText = texts.filter((row) => value.textList.indexOf(row) === -1 && !!row);
onChange({
...value,
textList: [...newText, ...value.textList]
});
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
reader.readAsText(file);
}
};
const allSelected = useMemo(() => {
return value.textList.length === checkboxValue.length && value.textList.length !== 0;
}, [value.textList, checkboxValue]);
return (
<MyModal
title={appT('modules.Config Texts')}
iconSrc="core/app/inputGuides"
isOpen={true}
onClose={() => {
setCheckboxValue([]);
onCloseTexts();
onOpen();
}}
>
<ModalBody w={'500px'} px={0}>
<Flex gap={4} px={8} alignItems={'center'} borderBottom={'1px solid #E8EBF0'} pb={4}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={commonT('common.Search')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Input
type="file"
accept=".csv"
style={{ display: 'none' }}
ref={fileInputRef}
onChange={handleFileSelected}
/>
<Button
onClick={() => {
fileInputRef.current?.click();
}}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{commonT('common.Import')}
</Button>
<Box
cursor={'pointer'}
onClick={() => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
}}
>
<QuestionTip ml={-2} label={appT('modules.Only support CSV')} />
</Box>
</Flex>
<Box mt={4}>
<Flex justifyContent={'space-between'} px={8}>
<Flex alignItems={'center'}>
<Checkbox
sx={{
'.chakra-checkbox__control': {
bg: allSelected ? 'primary.50' : 'none',
boxShadow: allSelected && '0 0 0 2px #F0F4FF',
_hover: {
bg: 'primary.50'
},
border: allSelected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
value={'all'}
size={'lg'}
mr={2}
isChecked={allSelected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue(value.textList);
} else {
setCheckboxValue([]);
}
}}
/>
<Box fontSize={'sm'} color={'myGray.600'} fontWeight={'medium'}>
{commonT('common.Select all')}
</Box>
</Flex>
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={checkboxValue.length === 0 ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={() => {
setCheckboxValue([]);
onChange({
...value,
textList: value.textList.filter((_) => !checkboxValue.includes(_))
});
}}
>
{commonT('common.Delete')}
</Button>
<Button
display={checkboxValue.length !== 0 ? 'none' : 'flex'}
onClick={() => {
onChange({
...value,
textList: ['', ...value.textList]
});
setIsEditIndex(0);
setIsAdding(true);
}}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{commonT('common.Add')}
</Button>
</Flex>
</Flex>
<Box h={'400px'} pb={4} overflow={'auto'} px={8}>
{value.textList.map((text, index) => {
const selected = checkboxValue.includes(text);
return (
<Flex
key={index}
alignItems={'center'}
h={10}
mt={2}
onMouseEnter={() => setShowIcons(index)}
onMouseLeave={() => setShowIcons(null)}
>
<Checkbox
{...getCheckboxProps({ value: text })}
sx={{
'.chakra-checkbox__control': {
bg: selected ? 'primary.50' : 'none',
boxShadow: selected ? '0 0 0 2px #F0F4FF' : 'none',
_hover: {
bg: 'primary.50'
},
border: selected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
size={'lg'}
mr={2}
isChecked={selected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue([...checkboxValue, text]);
} else {
setCheckboxValue(checkboxValue.filter((_) => _ !== text));
}
}}
/>
{index === isEditIndex ? (
<InputGroup alignItems={'center'} h={'full'}>
<Input
autoFocus
h={'full'}
defaultValue={text}
onBlur={(e) => {
setIsEditIndex(-1);
if (
!e.target.value ||
(value.textList.indexOf(e.target.value) !== -1 &&
value.textList.indexOf(e.target.value) !== index)
) {
isAdding &&
onChange({
...value,
textList: value.textList.filter((_, i) => i !== index)
});
} else {
onChange({
...value,
textList: value.textList?.map((v, i) =>
i !== index ? v : e.target.value
)
});
}
setIsAdding(false);
}}
/>
<InputRightElement alignItems={'center'} pr={4} display={'flex'}>
<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />
</InputRightElement>
</InputGroup>
) : (
<Flex
h={10}
w={'full'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'1px solid #F0F1F6'}
_hover={{ border: '1px solid #94B5FF' }}
>
{text}
<Box flex={1} />
{checkboxValue.length === 0 && (
<Box display={showIcons === index ? 'flex' : 'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => setIsEditIndex(index)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => {
const temp = value.textList?.filter((_, i) => i !== index);
onChange({
...value,
textList: temp
});
}}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
})}
</Box>
</Box>
</ModalBody>
</MyModal>
);
};

View File

@@ -16,8 +16,10 @@ import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import {
checkChatSupportSelectFileByModules,
getAppQuestionGuidesByModules
} from '@/web/core/chat/utils';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
@@ -26,6 +28,7 @@ import {
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;

View File

@@ -9,6 +9,7 @@ import { welcomeTextTip } from '@fastgpt/global/core/workflow/template/tip';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import QGuidesConfig from '@/components/core/app/QGuidesConfig';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants';
@@ -21,11 +22,6 @@ import { WorkflowContext } from '../../context';
import { VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit';
import {
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme();
@@ -60,6 +56,9 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionInputGuide data={data} />
</Box>
</Box>
</NodeCard>
</>
@@ -240,3 +239,26 @@ function ScheduledTrigger({ data }: { data: FlowNodeItemType }) {
/>
);
}
function QuestionInputGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { questionGuideText } = splitGuideModule({ inputs } as StoreNodeItemType);
return (
<QGuidesConfig
value={questionGuideText}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.questionGuideText,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuideText),
value: e
}
});
}}
/>
);
}

View File

@@ -6,6 +6,7 @@ import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema';
import { NextAPI } from '@/service/middleware/entry';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -46,6 +47,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
},
{ session }
);
await MongoAppQGuide.deleteMany(
{
appId
},
{ session }
);
// delete app
await MongoApp.deleteOne(
{

View File

@@ -0,0 +1,41 @@
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { NextApiRequest, NextApiResponse } from 'next';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema';
import axios from 'axios';
import { NextAPI } from '@/service/middleware/entry';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { textList = [], appId, customURL } = req.body;
if (!customURL) {
const { teamId } = await authUserNotVisitor({ req, authToken: true });
const currentQGuide = await MongoAppQGuide.find({ appId, teamId });
const currentTexts = currentQGuide.map((item) => item.text);
const textsToDelete = currentTexts.filter((text) => !textList.includes(text));
await MongoAppQGuide.deleteMany({ text: { $in: textsToDelete }, appId, teamId });
const newTexts = textList.filter((text: string) => !currentTexts.includes(text));
const newDocuments = newTexts.map((text: string) => ({
text: text,
appId: appId,
teamId: teamId
}));
await MongoAppQGuide.insertMany(newDocuments);
} else {
try {
const response = await axios.post(customURL, {
textList,
appId
});
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({ error });
}
}
}
export default NextAPI(handler);

View File

@@ -0,0 +1,48 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema';
import axios from 'axios';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
import { NextAPI } from '@/service/middleware/entry';
type Props = PaginationProps<{
appId: string;
customURL: string;
searchKey: string;
}>;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId, customURL, current, pageSize, searchKey } = req.query as unknown as Props;
if (!customURL) {
const [result, total] = await Promise.all([
MongoAppQGuide.find({
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
})
.sort({
time: -1
})
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoAppQGuide.countDocuments({ appId })
]);
return {
list: result.map((item) => item.text) || [],
total
};
} else {
try {
const response = await axios.get(customURL as string, {
params: {
appid: appId
}
});
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({ error });
}
}
}
export default NextAPI(handler);

View File

@@ -27,6 +27,9 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval, useUpdateEffect } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { importQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL, getNodesWithNoQGuide } from '@/web/core/app/utils';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic(
@@ -139,8 +142,18 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const data = await flowData2StoreDataAndCheck();
if (data) {
try {
const { questionGuideText } = splitGuideModule(getGuideModule(data.nodes));
await importQuestionGuides({
appId: app._id,
textList: questionGuideText.textList,
customURL: getAppQGuideCustomURL(app)
});
const newNodes = getNodesWithNoQGuide(data.nodes, questionGuideText);
await publishApp(app._id, {
...data,
nodes: newNodes,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'

View File

@@ -28,7 +28,7 @@ const Render = ({ app, onClose }: Props) => {
useEffect(() => {
if (!isV2Workflow) return;
initData(JSON.parse(workflowStringData));
}, [isV2Workflow, initData, app._id]);
}, [isV2Workflow, initData, app._id, workflowStringData]);
useEffect(() => {
if (!isV2Workflow) {

View File

@@ -104,7 +104,7 @@ const ChatTest = ({
return () => {
wat.unsubscribe();
};
}, []);
}, [setWorkflowData, watch]);
return (
<Flex

View File

@@ -12,7 +12,11 @@ import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { form2AppWorkflow } from '@/web/core/app/utils';
import {
form2AppWorkflow,
getAppQGuideCustomURL,
getNodesWithNoQGuide
} from '@/web/core/app/utils';
import dynamic from 'next/dynamic';
import MyTooltip from '@/components/MyTooltip';
@@ -30,6 +34,7 @@ import { TTSTypeEnum } from '@/web/core/app/constants';
import { getSystemVariables } from '@/web/core/app/utils';
import { useUpdate } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { importQuestionGuides } from '@/web/core/app/api';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@@ -37,6 +42,7 @@ const ToolSelectModal = dynamic(() => import('./ToolSelectModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const QGuidesConfigModal = dynamic(() => import('@/components/core/app/QGuidesConfig'));
const BoxStyles: BoxProps = {
px: 5,
@@ -64,7 +70,7 @@ const EditForm = ({
const { t } = useTranslation();
const { appT } = useI18n();
const { publishApp, appDetail } = useAppStore();
const { appDetail, publishApp } = useAppStore();
const { allDatasets } = useDatasetStore();
const { llmModelList } = useSystemStore();
@@ -103,7 +109,7 @@ const EditForm = ({
const datasetSearchSetting = watch('dataset');
const variables = watch('userGuide.variables');
const formatVariables = useMemo(
const formatVariables: any = useMemo(
() => formatEditorVariablePickerIcon([...getSystemVariables(t), ...variables]),
[t, variables]
);
@@ -112,6 +118,7 @@ const EditForm = ({
const whisperConfig = getValues('userGuide.whisper');
const postQuestionGuide = getValues('userGuide.questionGuide');
const selectedTools = watch('selectedTools');
const QGuidesConfig = watch('userGuide.questionGuideText');
const selectDatasets = useMemo(
() => allDatasets.filter((item) => datasets.find((dataset) => dataset.datasetId === item._id)),
@@ -125,10 +132,19 @@ const EditForm = ({
/* on save app */
const { mutate: onSubmitPublish, isLoading: isSaving } = useRequest({
mutationFn: async (data: AppSimpleEditFormType) => {
const questionGuideText = data.userGuide.questionGuideText;
await importQuestionGuides({
appId: appDetail._id,
textList: questionGuideText.textList,
customURL: getAppQGuideCustomURL(appDetail)
});
const { nodes, edges } = form2AppWorkflow(data);
const newNodes = getNodesWithNoQGuide(nodes, questionGuideText);
await publishApp(appDetail._id, {
nodes,
nodes: newNodes,
edges,
type: AppTypeEnum.simple
});
@@ -435,7 +451,7 @@ const EditForm = ({
</Box>
{/* question guide */}
<Box {...BoxStyles} borderBottom={'none'}>
<Box {...BoxStyles}>
<QGSwitch
isChecked={postQuestionGuide}
size={'lg'}
@@ -444,6 +460,16 @@ const EditForm = ({
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles} borderBottom={'none'}>
<QGuidesConfigModal
value={QGuidesConfig}
onChange={(e) => {
setValue('userGuide.questionGuideText', e);
}}
/>
</Box>
</Box>
</Box>

View File

@@ -110,7 +110,6 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
maxW={['90vw', '700px']}
w={'700px'}
h={['90vh', '80vh']}
overflow={'none'}
>
{/* Header: row and search */}
<Box px={[3, 6]} pt={4} display={'flex'} justifyContent={'space-between'} w={'full'}>

View File

@@ -18,6 +18,7 @@ import { useAppStore } from '@/web/core/app/store/useAppStore';
import Head from 'next/head';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
const FlowEdit = dynamic(() => import('./components/FlowEdit'), {
loading: () => <Loading />

View File

@@ -33,10 +33,15 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import {
checkChatSupportSelectFileByChatModels,
getAppQuestionGuidesByUserGuideModule
} from '@/web/core/chat/utils';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
const router = useRouter();

View File

@@ -20,7 +20,10 @@ import PageContainer from '@/components/PageContainer';
import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import {
checkChatSupportSelectFileByChatModels,
getAppQuestionGuidesByUserGuideModule
} from '@/web/core/chat/utils';
import { useTranslation } from 'next-i18next';
import { getInitOutLinkChatInfo } from '@/web/core/chat/api';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
@@ -31,6 +34,9 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { OutLinkWithAppType } from '@fastgpt/global/support/outLink/type';
import { addLog } from '@fastgpt/service/common/system/log';
import { connectToDatabase } from '@/service/mongo';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
const OutLink = ({
appName,
@@ -378,6 +384,7 @@ const OutLink = ({
history={chatData.history}
showHistory={showHistory === '1'}
onOpenSlider={onOpenSlider}
appId={chatData.appId}
/>
{/* chat box */}
<Box flex={1}>

View File

@@ -21,7 +21,10 @@ import ChatHistorySlider from './components/ChatHistorySlider';
import ChatHeader from './components/ChatHeader';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import {
checkChatSupportSelectFileByChatModels,
getAppQuestionGuidesByUserGuideModule
} from '@/web/core/chat/utils';
import { useChatStore } from '@/web/core/chat/storeChat';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
@@ -35,6 +38,9 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import MyBox from '@fastgpt/web/components/common/MyBox';
import SliderApps from './components/SliderApps';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
const OutLink = () => {
const { t } = useTranslation();

View File

@@ -1,7 +1,12 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import type {
AppDetailType,
AppListItemType,
AppQuestionGuideTextConfigType
} from '@fastgpt/global/core/app/type.d';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { AppUpdateParams, CreateAppParams } from '@/global/core/app/api';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
/**
* 获取模型列表
@@ -32,3 +37,19 @@ export const putAppById = (id: string, data: AppUpdateParams) =>
// =================== chat logs
export const getAppChatLogs = (data: GetAppChatLogsParams) => POST(`/core/app/getChatLogs`, data);
/**
* 导入提示词库
*/
export const importQuestionGuides = (data: {
appId: string;
textList: string[];
customURL: string;
}) => POST(`/core/app/questionGuides/import`, data);
/**
* 获取提示词库
*/
export const getMyQuestionGuides = (
data: PaginationProps<{ appId: string; customURL: string; searchKey: string }>
) => GET<PaginationResponse<string>>(`/core/app/questionGuides/list`, data);

View File

@@ -1,12 +1,12 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { getMyApps, getModelById, putAppById } from '@/web/core/app/api';
import { defaultApp } from '../constants';
import { getMyApps, getModelById, putAppById, getMyQuestionGuides } from '@/web/core/app/api';
import type { AppUpdateParams } from '@/global/core/app/api.d';
import { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import { PostPublishAppProps } from '@/global/core/app/api';
import { postPublishApp } from '../versionApi';
import { defaultApp } from '../constants';
type State = {
myApps: AppListItemType[];

View File

@@ -1,4 +1,9 @@
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import {
AppDetailType,
AppQuestionGuideTextConfigType,
AppSchema,
AppSimpleEditFormType
} from '@fastgpt/global/core/app/type';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import {
FlowNodeInputTypeEnum,
@@ -64,6 +69,12 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType {
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
value: formData.userGuide.scheduleTrigger
},
{
key: NodeInputKeyEnum.questionGuideText,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
value: formData.userGuide.questionGuideText
}
],
outputs: []
@@ -757,3 +768,34 @@ export const getSystemVariables = (t: TFunction): EditorVariablePickerType[] =>
}
];
};
export const getAppQGuideCustomURL = (appDetail: AppDetailType | AppSchema): string => {
return (
appDetail?.modules
.find((m) => m.flowNodeType === FlowNodeTypeEnum.systemConfig)
?.inputs.find((i) => i.key === NodeInputKeyEnum.questionGuideText)?.value.customURL || ''
);
};
export const getNodesWithNoQGuide = (
nodes: StoreNodeItemType[],
questionGuideText: AppQuestionGuideTextConfigType
): StoreNodeItemType[] => {
return nodes.map((node) => {
if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) {
return {
...node,
inputs: node.inputs.map((input) => {
if (input.key === NodeInputKeyEnum.questionGuideText) {
return {
...input,
value: { ...questionGuideText, textList: [] }
};
}
return input;
})
};
}
return node;
});
};

View File

@@ -1,6 +1,7 @@
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
export function checkChatSupportSelectFileByChatModels(models: string[] = []) {
const llmModelList = useSystemStore.getState().llmModelList;
@@ -25,3 +26,23 @@ export function checkChatSupportSelectFileByModules(modules: StoreNodeItemType[]
);
return checkChatSupportSelectFileByChatModels(models);
}
export function getAppQuestionGuidesByModules(modules: StoreNodeItemType[] = []) {
const systemModule = modules.find((item) => item.flowNodeType === FlowNodeTypeEnum.systemConfig);
const questionGuideText = systemModule?.inputs.find(
(item) => item.key === NodeInputKeyEnum.questionGuideText
)?.value;
return questionGuideText?.open ? questionGuideText?.textList : [];
}
export function getAppQuestionGuidesByUserGuideModule(
module: StoreNodeItemType,
qGuideText: string[] = []
) {
const questionGuideText = module?.inputs.find(
(item) => item.key === NodeInputKeyEnum.questionGuideText
)?.value;
return questionGuideText?.open ? qGuideText : [];
}