mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 12:20:34 +00:00
feat: get node variables in prompt editor (#2087)
* feat: get node variables in prompt editor * fix * fix build * merge * fix build * delete default parent * fix * fix
This commit is contained in:
@@ -3,9 +3,10 @@ import {
|
||||
WorkflowIOValueTypeEnum,
|
||||
NodeInputKeyEnum,
|
||||
VariableInputEnum,
|
||||
variableMap
|
||||
variableMap,
|
||||
VARIABLE_NODE_ID
|
||||
} from './constants';
|
||||
import { FlowNodeInputItemType, FlowNodeOutputItemType } from './type/io.d';
|
||||
import { FlowNodeInputItemType, FlowNodeOutputItemType, ReferenceValueProps } from './type/io.d';
|
||||
import { StoreNodeItemType } from './type/node';
|
||||
import type {
|
||||
VariableItemType,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from '../app/constants';
|
||||
import { IfElseResultEnum } from './template/system/ifElse/constant';
|
||||
import { RuntimeNodeItemType } from './runtime/type';
|
||||
import { getReferenceVariableValue } from './runtime/utils';
|
||||
|
||||
export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => {
|
||||
return `${nodeId}-${type}-${key}`;
|
||||
@@ -226,3 +228,69 @@ export const updatePluginInputByVariables = (
|
||||
: node
|
||||
);
|
||||
};
|
||||
|
||||
export function replaceVariableLabel({
|
||||
text,
|
||||
nodes,
|
||||
variables,
|
||||
runningNode
|
||||
}: {
|
||||
text: any;
|
||||
nodes: RuntimeNodeItemType[];
|
||||
variables: Record<string, string | number>;
|
||||
runningNode: RuntimeNodeItemType;
|
||||
}) {
|
||||
if (!(typeof text === 'string')) return text;
|
||||
|
||||
const globalVariables = Object.keys(variables).map((key) => {
|
||||
return {
|
||||
nodeId: VARIABLE_NODE_ID,
|
||||
id: key,
|
||||
value: variables[key]
|
||||
};
|
||||
});
|
||||
|
||||
const nodeVariables = nodes
|
||||
.map((node) => {
|
||||
return node.outputs.map((output) => {
|
||||
return {
|
||||
nodeId: node.nodeId,
|
||||
id: output.id,
|
||||
value: output.value
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
|
||||
const customInputs = runningNode.inputs.flatMap((item) => {
|
||||
if (Array.isArray(item.value)) {
|
||||
return [
|
||||
{
|
||||
id: item.key,
|
||||
value: getReferenceVariableValue({
|
||||
value: item.value as ReferenceValueProps,
|
||||
nodes,
|
||||
variables
|
||||
}),
|
||||
nodeId: runningNode.nodeId
|
||||
}
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const allVariables = [...globalVariables, ...nodeVariables, ...customInputs];
|
||||
|
||||
for (const key in allVariables) {
|
||||
const val = allVariables[key];
|
||||
const regex = new RegExp(`\\{\\{\\$(${val.nodeId}\\.${val.id})\\$\\}\\}`, 'g');
|
||||
if (['string', 'number'].includes(typeof val.value)) {
|
||||
text = text.replace(regex, String(val.value));
|
||||
} else if (['object'].includes(typeof val.value)) {
|
||||
text = text.replace(regex, JSON.stringify(val.value));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return text || '';
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
import { replaceVariable } from '@fastgpt/global/common/string/tools';
|
||||
import { responseWriteNodeStatus } from '../../../common/response';
|
||||
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
|
||||
import { replaceVariableLabel } from '@fastgpt/global/core/workflow/utils';
|
||||
|
||||
import { dispatchWorkflowStart } from './init/workflowStart';
|
||||
import { dispatchChatCompletion } from './chat/oneapi';
|
||||
@@ -54,6 +55,7 @@ import { surrenderProcess } from '../../../common/system/tools';
|
||||
import { dispatchRunCode } from './code/run';
|
||||
import { dispatchTextEditor } from './tools/textEditor';
|
||||
import { dispatchCustomFeedback } from './tools/customFeedback';
|
||||
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
|
||||
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
|
||||
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
|
||||
@@ -291,6 +293,14 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
// replace {{}} variables
|
||||
let value = replaceVariable(input.value, variables);
|
||||
|
||||
// replace {{$$}} variables
|
||||
value = replaceVariableLabel({
|
||||
text: value,
|
||||
nodes: runtimeNodes,
|
||||
variables: variables,
|
||||
runningNode: node
|
||||
});
|
||||
|
||||
// replace reference variables
|
||||
value = getReferenceVariableValue({
|
||||
value,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import type { ImageProps } from '@chakra-ui/react';
|
||||
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
|
||||
import MyIcon from '../Icon';
|
||||
@@ -10,12 +10,13 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
const isIcon = !!iconPaths[src as any];
|
||||
|
||||
return isIcon ? (
|
||||
<MyIcon name={src as any} w={w} borderRadius={props.borderRadius} />
|
||||
<Box {...props}>
|
||||
<MyIcon name={src as any} w={w} borderRadius={props.borderRadius} />
|
||||
</Box>
|
||||
) : (
|
||||
<Image
|
||||
fallbackSrc={LOGO_ICON}
|
||||
fallbackStrategy={'onError'}
|
||||
// borderRadius={'md'}
|
||||
objectFit={'contain'}
|
||||
alt=""
|
||||
w={w}
|
||||
|
@@ -200,6 +200,7 @@ export const iconPaths = {
|
||||
'core/workflow/template/textConcat': () =>
|
||||
import('./icons/core/workflow/template/textConcat.svg'),
|
||||
'core/workflow/template/toolCall': () => import('./icons/core/workflow/template/toolCall.svg'),
|
||||
'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'),
|
||||
'core/workflow/template/variableUpdate': () =>
|
||||
import('./icons/core/workflow/template/variableUpdate.svg'),
|
||||
'core/workflow/template/workflowStart': () =>
|
||||
|
@@ -0,0 +1,12 @@
|
||||
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_7585_26727)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5796 26.9153C11.7938 26.9153 11.9675 26.7416 11.9675 26.5274V24.7153C11.9675 24.5011 11.7938 24.3274 11.5796 24.3274C11.3077 24.3274 11.1751 24.2403 11.0931 24.1123C10.9905 23.9444 10.9068 23.5904 10.9068 22.9711V20.9846C10.9068 20.17 10.8199 19.4794 10.6306 18.928C10.5033 18.557 10.3257 18.2383 10.0893 17.99C10.6741 17.3773 10.9068 16.338 10.9068 15.035V12.971C10.9068 12.4371 10.9931 12.1027 11.1124 11.913C11.2128 11.7534 11.349 11.6728 11.5796 11.6728C11.7938 11.6728 11.9675 11.4991 11.9675 11.2849V9.47286C11.9675 9.25863 11.7938 9.08496 11.5796 9.08496C10.5396 9.08496 9.7021 9.37682 9.16909 10.044C8.64841 10.6893 8.43401 11.7393 8.43401 13.0872V15.2481C8.43401 15.8426 8.35093 16.2274 8.22932 16.4508C8.17169 16.5566 8.11045 16.617 8.05257 16.6524C7.99561 16.6873 7.92076 16.711 7.81429 16.711C7.60006 16.711 7.42639 16.8847 7.42639 17.0989V18.8819C7.42639 19.0961 7.60006 19.2698 7.81429 19.2698C8.0075 19.2698 8.13178 19.3406 8.2323 19.5185C8.35119 19.7289 8.43401 20.0944 8.43401 20.6649V22.9905C8.43401 24.3313 8.65018 25.3744 9.18213 26.0028C9.72047 26.6387 10.5521 26.9153 11.5796 26.9153Z" fill="white"/>
|
||||
<path d="M24.0325 26.5274C24.0325 26.7416 24.2061 26.9153 24.4203 26.9153C25.4479 26.9153 26.2795 26.6387 26.8178 26.0028C27.3498 25.3744 27.5659 24.3313 27.5659 22.9905V20.6649C27.5659 20.0944 27.6488 19.7289 27.7676 19.5185C27.8682 19.3406 27.9925 19.2698 28.1857 19.2698C28.3999 19.2698 28.5736 19.0961 28.5736 18.8819V17.0989C28.5736 16.8847 28.3999 16.711 28.1857 16.711C28.0792 16.711 28.0043 16.6873 27.9474 16.6524C27.8895 16.617 27.8283 16.5566 27.7706 16.4508C27.649 16.2274 27.5659 15.8426 27.5659 15.2481V13.0872C27.5659 11.7392 27.3515 10.6891 26.8307 10.0438C26.2977 9.37677 25.4603 9.08496 24.4203 9.08496C24.2061 9.08496 24.0325 9.25863 24.0325 9.47286V11.2849C24.0325 11.4991 24.2061 11.6728 24.4203 11.6728C24.651 11.6728 24.7871 11.7534 24.8875 11.913C25.0069 12.1027 25.0931 12.4371 25.0931 12.971V15.035C25.0931 16.338 25.3259 17.3773 25.9107 17.99C25.6743 18.2383 25.4966 18.557 25.3693 18.928C25.1801 19.4794 25.0931 20.17 25.0931 20.9846V22.9711C25.0931 23.5904 25.0094 23.9445 24.9068 24.1123C24.8249 24.2404 24.6922 24.3274 24.4203 24.3274C24.2061 24.3274 24.0325 24.5011 24.0325 24.7153V26.5274Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8172 15.3258C13.4006 14.9093 13.4006 14.2339 13.8172 13.8173C14.2337 13.4007 14.9091 13.4007 15.3257 13.8173L18 16.4916L20.6742 13.8173C21.0908 13.4007 21.7662 13.4007 22.1828 13.8173C22.5994 14.2339 22.5994 14.9093 22.1828 15.3258L19.5085 18.0001L22.1828 20.6744C22.5994 21.091 22.5994 21.7664 22.1828 22.1829C21.7662 22.5995 21.0908 22.5995 20.6742 22.1829L18 19.5087L15.3257 22.1829C14.9091 22.5995 14.2337 22.5995 13.8172 22.1829C13.4006 21.7664 13.4006 21.091 13.8172 20.6744L16.4914 18.0001L13.8172 15.3258Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_7585_26727" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#69C1FF"/>
|
||||
<stop offset="1" stop-color="#53A3FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
@@ -5,7 +5,7 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
||||
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
|
||||
import VariableLabelPickerPlugin from './plugins/VariableLabelPickerPlugin';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import styles from './index.module.scss';
|
||||
import VariablePlugin from './plugins/VariablePlugin';
|
||||
@@ -18,6 +18,8 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import FocusPlugin from './plugins/FocusPlugin';
|
||||
import { textToEditorState } from './utils';
|
||||
import { MaxLengthPlugin } from './plugins/MaxLengthPlugin';
|
||||
import { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
|
||||
import VariableLabelPlugin from './plugins/VariableLabelPlugin';
|
||||
|
||||
export default function Editor({
|
||||
h = 200,
|
||||
@@ -51,7 +53,7 @@ export default function Editor({
|
||||
|
||||
const initialConfig = {
|
||||
namespace: 'promptEditor',
|
||||
nodes: [VariableNode],
|
||||
nodes: [VariableNode, VariableLabelNode],
|
||||
editorState: textToEditorState(value),
|
||||
onError: (error: Error) => {
|
||||
throw error;
|
||||
@@ -127,8 +129,9 @@ export default function Editor({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<VariablePickerPlugin variables={variables} />
|
||||
<VariableLabelPickerPlugin variables={variables} />
|
||||
<VariablePlugin variables={variables} />
|
||||
<VariableLabelPlugin variables={variables} />
|
||||
<OnBlurPlugin onBlur={onBlur} />
|
||||
</LexicalComposer>
|
||||
{showResize && (
|
||||
|
@@ -2,10 +2,11 @@
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid var(--chakra-colors-borderColor-base);
|
||||
border: 1px solid rgb(232, 235, 240);
|
||||
border-radius: var(--chakra-radii-md);
|
||||
padding: 8px 12px;
|
||||
background: var(--chakra-colors-gray-50);
|
||||
background: #fff;
|
||||
|
||||
font-size: var(--chakra-fontSizes-sm);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@@ -0,0 +1,228 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
||||
import { $createTextNode, $getSelection, $isRangeSelection, TextNode } from 'lexical';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../utils';
|
||||
import { EditorVariablePickerType } from '../../type';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Avatar from '../../../../Avatar';
|
||||
|
||||
type EditorVariablePickerType1 = {
|
||||
key: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
icon?: string;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
index: number;
|
||||
};
|
||||
interface TransformedParent {
|
||||
id: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
children: EditorVariablePickerType1[];
|
||||
}
|
||||
|
||||
export default function VariableLabelPickerPlugin({
|
||||
variables
|
||||
}: {
|
||||
variables: EditorVariablePickerType[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [queryString, setQueryString] = useState<string | null>(null);
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0
|
||||
});
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(selectedOption: any, nodeToRemove: TextNode | null, closeMenu: () => void) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection) || selectedOption == null) {
|
||||
return;
|
||||
}
|
||||
if (nodeToRemove) {
|
||||
nodeToRemove.remove();
|
||||
}
|
||||
selection.insertNodes([
|
||||
$createTextNode(`{{$${selectedOption.parent?.id}.${selectedOption.key}$}}`)
|
||||
]);
|
||||
closeMenu();
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
options={variables}
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
|
||||
) => {
|
||||
if (anchorElementRef.current == null) {
|
||||
return null;
|
||||
}
|
||||
return anchorElementRef.current && variables.length
|
||||
? ReactDOM.createPortal(
|
||||
<Box
|
||||
bg={'white'}
|
||||
boxShadow={'lg'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'borderColor.base'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
position={'absolute'}
|
||||
w={'auto'}
|
||||
maxH={'300px'}
|
||||
minW={'240px'}
|
||||
overflow={'auto'}
|
||||
zIndex={99999}
|
||||
>
|
||||
{variableFilter(variables, queryString || '').length === variables.length && (
|
||||
<Box fontSize={'xs'} ml={4}>
|
||||
{t('workflow:variable_picker_tips')}
|
||||
</Box>
|
||||
)}
|
||||
{variableFilter(variables, queryString || '').length > 0 ? (
|
||||
transformData(variableFilter(variables, queryString || '')).map((item) => {
|
||||
return (
|
||||
<Flex
|
||||
key={item.id}
|
||||
flexDirection={'column'}
|
||||
px={4}
|
||||
py={2}
|
||||
_notLast={{
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'myGray.200'
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} mb={1.5}>
|
||||
<Avatar
|
||||
src={item.avatar as any}
|
||||
w={'16px'}
|
||||
borderRadius={'2.8px'}
|
||||
display={'inline-flex'}
|
||||
verticalAlign={'middle'}
|
||||
/>
|
||||
<Box
|
||||
mx={2}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
color={'myGray.600'}
|
||||
fontWeight={'semibold'}
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
</Flex>
|
||||
{item.children?.map((child, index) => (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
as={'li'}
|
||||
key={child.key}
|
||||
px={4}
|
||||
py={1.5}
|
||||
rounded={'md'}
|
||||
cursor={'pointer'}
|
||||
overflow={'auto'}
|
||||
_notLast={{
|
||||
mb: 1
|
||||
}}
|
||||
{...(selectedIndex === child.index
|
||||
? {
|
||||
bg: '#1118240D',
|
||||
color: 'primary.700'
|
||||
}
|
||||
: {
|
||||
bg: 'white',
|
||||
color: 'myGray.600'
|
||||
})}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(child.index);
|
||||
selectOptionAndCleanUp({ ...child, parent: item });
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(child.index);
|
||||
}}
|
||||
>
|
||||
<Box ml={2} fontSize={'sm'} whiteSpace={'nowrap'}>
|
||||
{child.label}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Box p={2} color={'myGray.400'} fontSize={'sm'}>
|
||||
{t('common:unusable_variable')}
|
||||
</Box>
|
||||
)}
|
||||
</Box>,
|
||||
anchorElementRef.current
|
||||
)
|
||||
: null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function transformData(data: EditorVariablePickerType[]): TransformedParent[] {
|
||||
const transformedData: TransformedParent[] = [];
|
||||
const parentMap: { [key: string]: TransformedParent } = {};
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const parentId = item.parent!.id;
|
||||
const parentLabel = item.parent!.label;
|
||||
const parentAvatar = item.parent!.avatar;
|
||||
|
||||
if (!parentMap[parentId]) {
|
||||
parentMap[parentId] = {
|
||||
id: parentId,
|
||||
label: parentLabel,
|
||||
avatar: parentAvatar || '',
|
||||
children: []
|
||||
};
|
||||
}
|
||||
parentMap[parentId].children.push({
|
||||
label: item.label,
|
||||
key: item.key,
|
||||
icon: item.icon,
|
||||
index
|
||||
});
|
||||
});
|
||||
|
||||
const addedParents = new Set<string>();
|
||||
data.forEach((item) => {
|
||||
const parentId = item.parent!.id;
|
||||
if (!addedParents.has(parentId)) {
|
||||
transformedData.push(parentMap[parentId]);
|
||||
addedParents.add(parentId);
|
||||
}
|
||||
});
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
function variableFilter(
|
||||
data: EditorVariablePickerType[],
|
||||
queryString: string
|
||||
): EditorVariablePickerType[] {
|
||||
const lowerCaseQuery = queryString.toLowerCase();
|
||||
|
||||
return data.filter((item) => {
|
||||
const labelMatch = item.label.toLowerCase().includes(lowerCaseQuery);
|
||||
const keyMatch = item.key.toLowerCase().includes(lowerCaseQuery);
|
||||
const parentLabelMatch = item.parent!.label.toLowerCase().includes(lowerCaseQuery);
|
||||
|
||||
return labelMatch || keyMatch || parentLabelMatch;
|
||||
});
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Avatar from '../../../../../../../components/common/Avatar';
|
||||
|
||||
export default function VariableLabel({
|
||||
variableLabel,
|
||||
nodeAvatar
|
||||
}: {
|
||||
variableLabel: string;
|
||||
nodeAvatar: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [parentLabel, childLabel] = variableLabel.split('.');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
m={'2px'}
|
||||
rounded={'4px'}
|
||||
px={1.5}
|
||||
py={'1px'}
|
||||
bg={parentLabel !== 'undefined' ? 'primary.50' : 'red.50'}
|
||||
color={parentLabel !== 'undefined' ? 'myGray.900' : 'red.600'}
|
||||
>
|
||||
{parentLabel !== 'undefined' ? (
|
||||
<span>
|
||||
<Avatar
|
||||
src={nodeAvatar as any}
|
||||
w={'16px'}
|
||||
mr={1}
|
||||
borderRadius={'2.8px'}
|
||||
display={'inline-flex'}
|
||||
verticalAlign={'middle'}
|
||||
mb={'3px'}
|
||||
/>
|
||||
{parentLabel}
|
||||
<ChevronRightIcon />
|
||||
{childLabel}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Box>{t('common:invalid_variable')}</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { EditorVariablePickerType } from '../../type';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { $createVariableLabelNode, VariableLabelNode } from './node';
|
||||
import { TextNode } from 'lexical';
|
||||
import { getHashtagRegexString } from './utils';
|
||||
import { mergeRegister } from '@lexical/utils';
|
||||
import { registerLexicalTextEntity } from '../../utils';
|
||||
|
||||
const REGEX = new RegExp(getHashtagRegexString(), 'i');
|
||||
|
||||
export default function VariableLabelPlugin({
|
||||
variables
|
||||
}: {
|
||||
variables: EditorVariablePickerType[];
|
||||
}) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([VariableLabelNode]))
|
||||
throw new Error('VariableLabelPlugin: VariableLabelPlugin not registered on editor');
|
||||
}, [editor]);
|
||||
|
||||
const createVariableLabelPlugin = useCallback((textNode: TextNode): VariableLabelNode => {
|
||||
const [parentKey, childrenKey] = textNode.getTextContent().slice(3, -3).split('.');
|
||||
const currentVariable = variables.find(
|
||||
(item) => item.parent?.id === parentKey && item.key === childrenKey
|
||||
);
|
||||
const variableLabel = `${currentVariable && currentVariable.parent?.label}.${currentVariable?.label}`;
|
||||
const nodeAvatar = currentVariable?.parent?.avatar || '';
|
||||
return $createVariableLabelNode(textNode.getTextContent(), variableLabel, nodeAvatar);
|
||||
}, []);
|
||||
|
||||
const getVariableMatch = useCallback((text: string) => {
|
||||
const matches = REGEX.exec(text);
|
||||
if (!matches) return null;
|
||||
// if (variableKeys.indexOf(matches[4]) === -1) return null;
|
||||
const hashtagLength = matches[4].length + 6;
|
||||
const startOffset = matches.index;
|
||||
const endOffset = startOffset + hashtagLength;
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mergeRegister(
|
||||
...registerLexicalTextEntity(
|
||||
editor,
|
||||
getVariableMatch,
|
||||
VariableLabelNode,
|
||||
createVariableLabelPlugin
|
||||
)
|
||||
);
|
||||
}, [createVariableLabelPlugin, editor, getVariableMatch]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
DecoratorNode,
|
||||
DOMConversionMap,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
TextFormatType
|
||||
} from 'lexical';
|
||||
import VariableLabel from './components/VariableLabel';
|
||||
|
||||
export type SerializedVariableLabelNode = Spread<
|
||||
{
|
||||
variableKey: string;
|
||||
variableLabel: string;
|
||||
nodeAvatar: string;
|
||||
format: number | TextFormatType;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class VariableLabelNode extends DecoratorNode<JSX.Element> {
|
||||
__format: number | TextFormatType;
|
||||
__variableKey: string;
|
||||
__variableLabel: string;
|
||||
__nodeAvatar: string;
|
||||
static getType(): string {
|
||||
return 'variableLabel';
|
||||
}
|
||||
static clone(node: VariableLabelNode): VariableLabelNode {
|
||||
return new VariableLabelNode(
|
||||
node.__variableKey,
|
||||
node.__variableLabel,
|
||||
node.__nodeAvatar,
|
||||
node.__format,
|
||||
node.__key
|
||||
);
|
||||
}
|
||||
constructor(
|
||||
variableKey: string,
|
||||
variableLabel: string,
|
||||
nodeAvatar: string,
|
||||
format?: number | TextFormatType,
|
||||
key?: NodeKey
|
||||
) {
|
||||
super(key);
|
||||
this.__variableKey = variableKey;
|
||||
this.__format = format || 0;
|
||||
this.__variableLabel = variableLabel;
|
||||
this.__nodeAvatar = nodeAvatar;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedVariableLabelNode): VariableLabelNode {
|
||||
const node = $createVariableLabelNode(
|
||||
serializedNode.variableKey,
|
||||
serializedNode.variableLabel,
|
||||
serializedNode.nodeAvatar
|
||||
);
|
||||
node.setFormat(serializedNode.format);
|
||||
return node;
|
||||
}
|
||||
|
||||
setFormat(format: number | TextFormatType): void {
|
||||
const self = this.getWritable();
|
||||
self.__format = format;
|
||||
}
|
||||
getFormat(): number | TextFormatType {
|
||||
return this.__format;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedVariableLabelNode {
|
||||
return {
|
||||
format: this.__format || 0,
|
||||
type: 'variableLabel',
|
||||
version: 1,
|
||||
variableKey: this.getVariableKey(),
|
||||
variableLabel: this.__variableLabel,
|
||||
nodeAvatar: this.__nodeAvatar
|
||||
};
|
||||
}
|
||||
createDOM(): HTMLElement {
|
||||
const element = document.createElement('span');
|
||||
return element;
|
||||
}
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('span');
|
||||
return { element };
|
||||
}
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {};
|
||||
}
|
||||
updateDOM(): false {
|
||||
return false;
|
||||
}
|
||||
getVariableKey(): string {
|
||||
return this.__variableKey;
|
||||
}
|
||||
getTextContent(
|
||||
_includeInert?: boolean | undefined,
|
||||
_includeDirectionless?: false | undefined
|
||||
): string {
|
||||
return `${this.__variableKey}`;
|
||||
}
|
||||
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
return <VariableLabel variableLabel={this.__variableLabel} nodeAvatar={this.__nodeAvatar} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function $createVariableLabelNode(
|
||||
variableKey: string,
|
||||
variableLabel: string,
|
||||
nodeAvatar: string
|
||||
): VariableLabelNode {
|
||||
return new VariableLabelNode(variableKey, variableLabel, nodeAvatar);
|
||||
}
|
||||
|
||||
export function $isVariableLabelNode(
|
||||
node: VariableLabelNode | LexicalNode | null | undefined
|
||||
): node is VariableLabelNode {
|
||||
return node instanceof VariableLabelNode;
|
||||
}
|
@@ -0,0 +1,228 @@
|
||||
function getHashtagRegexVariableLabels(): Readonly<{
|
||||
alpha: string;
|
||||
alphanumeric: string;
|
||||
leftChars: string;
|
||||
rightChars: string;
|
||||
middleChars: string;
|
||||
}> {
|
||||
// Latin accented characters
|
||||
// Excludes 0xd7 from the range
|
||||
// (the multiplication sign, confusable with "x").
|
||||
// Also excludes 0xf7, the division sign
|
||||
const latinAccents =
|
||||
'\xC0-\xD6' +
|
||||
'\xD8-\xF6' +
|
||||
'\xF8-\xFF' +
|
||||
'\u0100-\u024F' +
|
||||
'\u0253-\u0254' +
|
||||
'\u0256-\u0257' +
|
||||
'\u0259' +
|
||||
'\u025B' +
|
||||
'\u0263' +
|
||||
'\u0268' +
|
||||
'\u026F' +
|
||||
'\u0272' +
|
||||
'\u0289' +
|
||||
'\u028B' +
|
||||
'\u02BB' +
|
||||
'\u0300-\u036F' +
|
||||
'\u1E00-\u1EFF';
|
||||
|
||||
// Cyrillic (Russian, Ukrainian, etc.)
|
||||
const nonLatinChars =
|
||||
'\u0400-\u04FF' + // Cyrillic
|
||||
'\u0500-\u0527' + // Cyrillic Supplement
|
||||
'\u2DE0-\u2DFF' + // Cyrillic Extended A
|
||||
'\uA640-\uA69F' + // Cyrillic Extended B
|
||||
'\u0591-\u05BF' + // Hebrew
|
||||
'\u05C1-\u05C2' +
|
||||
'\u05C4-\u05C5' +
|
||||
'\u05C7' +
|
||||
'\u05D0-\u05EA' +
|
||||
'\u05F0-\u05F4' +
|
||||
'\uFB12-\uFB28' + // Hebrew Presentation Forms
|
||||
'\uFB2A-\uFB36' +
|
||||
'\uFB38-\uFB3C' +
|
||||
'\uFB3E' +
|
||||
'\uFB40-\uFB41' +
|
||||
'\uFB43-\uFB44' +
|
||||
'\uFB46-\uFB4F' +
|
||||
'\u0610-\u061A' + // Arabic
|
||||
'\u0620-\u065F' +
|
||||
'\u066E-\u06D3' +
|
||||
'\u06D5-\u06DC' +
|
||||
'\u06DE-\u06E8' +
|
||||
'\u06EA-\u06EF' +
|
||||
'\u06FA-\u06FC' +
|
||||
'\u06FF' +
|
||||
'\u0750-\u077F' + // Arabic Supplement
|
||||
'\u08A0' + // Arabic Extended A
|
||||
'\u08A2-\u08AC' +
|
||||
'\u08E4-\u08FE' +
|
||||
'\uFB50-\uFBB1' + // Arabic Pres. Forms A
|
||||
'\uFBD3-\uFD3D' +
|
||||
'\uFD50-\uFD8F' +
|
||||
'\uFD92-\uFDC7' +
|
||||
'\uFDF0-\uFDFB' +
|
||||
'\uFE70-\uFE74' + // Arabic Pres. Forms B
|
||||
'\uFE76-\uFEFC' +
|
||||
'\u200C-\u200C' + // Zero-Width Non-Joiner
|
||||
'\u0E01-\u0E3A' + // Thai
|
||||
'\u0E40-\u0E4E' + // Hangul (Korean)
|
||||
'\u1100-\u11FF' + // Hangul Jamo
|
||||
'\u3130-\u3185' + // Hangul Compatibility Jamo
|
||||
'\uA960-\uA97F' + // Hangul Jamo Extended-A
|
||||
'\uAC00-\uD7AF' + // Hangul Syllables
|
||||
'\uD7B0-\uD7FF' + // Hangul Jamo Extended-B
|
||||
'\uFFA1-\uFFDC'; // Half-width Hangul
|
||||
|
||||
const charCode = String.fromCharCode;
|
||||
|
||||
const cjkChars =
|
||||
'\u30A1-\u30FA\u30FC-\u30FE' + // Katakana (full-width)
|
||||
'\uFF66-\uFF9F' + // Katakana (half-width)
|
||||
'\uFF10-\uFF19\uFF21-\uFF3A' +
|
||||
'\uFF41-\uFF5A' + // Latin (full-width)
|
||||
'\u3041-\u3096\u3099-\u309E' + // Hiragana
|
||||
'\u3400-\u4DBF' + // Kanji (CJK Extension A)
|
||||
`\u4E00-\u9FFF${
|
||||
// Kanji (Unified)
|
||||
// Disabled as it breaks the Regex.
|
||||
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
|
||||
charCode(0x2a700)
|
||||
}-${
|
||||
charCode(0x2b73f) // Kanji (CJK Extension C)
|
||||
}${charCode(0x2b740)}-${
|
||||
charCode(0x2b81f) // Kanji (CJK Extension D)
|
||||
}${charCode(0x2f800)}-${charCode(0x2fa1f)}\u3003\u3005\u303B`; // Kanji (CJK supplement)
|
||||
|
||||
const otherChars = latinAccents + nonLatinChars + cjkChars;
|
||||
// equivalent of \p{L}
|
||||
|
||||
const unicodeLetters =
|
||||
'\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6' +
|
||||
'\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386' +
|
||||
'\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481' +
|
||||
'\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587' +
|
||||
'\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F' +
|
||||
'\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710' +
|
||||
'\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950' +
|
||||
'\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0' +
|
||||
'\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1' +
|
||||
'\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33' +
|
||||
'\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D' +
|
||||
'\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD' +
|
||||
'\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30' +
|
||||
'\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83' +
|
||||
'\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F' +
|
||||
'\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10' +
|
||||
'\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C' +
|
||||
'\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE' +
|
||||
'\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39' +
|
||||
'\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6' +
|
||||
'\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88' +
|
||||
'\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7' +
|
||||
'\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6' +
|
||||
'\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021' +
|
||||
'\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC' +
|
||||
'\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D' +
|
||||
'\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0' +
|
||||
'\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310' +
|
||||
'\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C' +
|
||||
'\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711' +
|
||||
'\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7' +
|
||||
'\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974' +
|
||||
'\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B' +
|
||||
'\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D' +
|
||||
'\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC' +
|
||||
'\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC' +
|
||||
'\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107' +
|
||||
'\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D' +
|
||||
'\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E' +
|
||||
'\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96' +
|
||||
'\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6' +
|
||||
'\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035' +
|
||||
'\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF' +
|
||||
'\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5' +
|
||||
'\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A' +
|
||||
'\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9' +
|
||||
'\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C' +
|
||||
'\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F' +
|
||||
'\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A' +
|
||||
'\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7' +
|
||||
'\uFFDA-\uFFDC';
|
||||
|
||||
// equivalent of \p{Mn}\p{Mc}
|
||||
const unicodeAccents =
|
||||
'\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF' +
|
||||
'\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670' +
|
||||
'\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A' +
|
||||
'\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963' +
|
||||
'\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7' +
|
||||
'\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D' +
|
||||
'\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD' +
|
||||
'\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D' +
|
||||
'\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7' +
|
||||
'\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' +
|
||||
'\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6' +
|
||||
'\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83' +
|
||||
'\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A' +
|
||||
'\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19' +
|
||||
'\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97' +
|
||||
'\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F' +
|
||||
'\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD' +
|
||||
'\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9' +
|
||||
'\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F' +
|
||||
'\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F' +
|
||||
'\uFE20-\uFE23';
|
||||
|
||||
// equivalent of \p{Dn}
|
||||
const unicodeDigits =
|
||||
'\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF' +
|
||||
'\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F' +
|
||||
'\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29' +
|
||||
'\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9' +
|
||||
'\uFF10-\uFF19';
|
||||
|
||||
// An alpha char is a unicode chars including unicode marks or
|
||||
// letter or char in otherChars range
|
||||
const alpha = unicodeLetters;
|
||||
|
||||
// A numeric character is any with the number digit property, or
|
||||
// underscore. These characters can be included in hashtags, but a hashtag
|
||||
// cannot have only these characters.
|
||||
const numeric = `${unicodeDigits}_`;
|
||||
|
||||
// Alphanumeric char is any alpha char or a unicode char with decimal
|
||||
// number property \p{Nd}
|
||||
const alphanumeric = alpha + numeric;
|
||||
const leftChars = '{';
|
||||
const rightChars = '}';
|
||||
const middleChars = '$';
|
||||
|
||||
return {
|
||||
alpha,
|
||||
alphanumeric,
|
||||
leftChars,
|
||||
rightChars,
|
||||
middleChars
|
||||
};
|
||||
}
|
||||
|
||||
export function getHashtagRegexString(): string {
|
||||
const { leftChars, rightChars, middleChars } = getHashtagRegexVariableLabels();
|
||||
|
||||
const hashLeftCharList = `[${leftChars}]`;
|
||||
const hashRightCharList = `[${rightChars}]`;
|
||||
const hashMiddleCharList = `[${middleChars}]`;
|
||||
|
||||
// A hashtag contains characters, numbers and underscores,
|
||||
// but not all numbers.
|
||||
const hashtag =
|
||||
`(${hashLeftCharList})` +
|
||||
`(${hashLeftCharList})` +
|
||||
`(${hashMiddleCharList})([a-zA-Z0-9_\\.]{0,29})(${hashMiddleCharList})` +
|
||||
`(${hashRightCharList})(${hashRightCharList})`;
|
||||
|
||||
return hashtag;
|
||||
}
|
@@ -6,4 +6,9 @@ export type EditorVariablePickerType = {
|
||||
required?: boolean;
|
||||
icon?: string;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
parent?: {
|
||||
id: string;
|
||||
label: string;
|
||||
avatar?: string;
|
||||
};
|
||||
};
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
|
||||
import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical';
|
||||
import type { EntityMatch } from '@lexical/text';
|
||||
import { $createTextNode, $getRoot, $isTextNode, TextNode } from 'lexical';
|
||||
import { useCallback } from 'react';
|
||||
import { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
|
||||
|
||||
export function registerLexicalTextEntity<T extends TextNode>(
|
||||
export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode>(
|
||||
editor: LexicalEditor,
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
targetNode: Klass<T>,
|
||||
@@ -13,7 +14,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
|
||||
return node instanceof targetNode;
|
||||
};
|
||||
|
||||
const replaceWithSimpleText = (node: TextNode): void => {
|
||||
const replaceWithSimpleText = (node: TextNode | VariableLabelNode): void => {
|
||||
const textNode = $createTextNode(node.getTextContent());
|
||||
textNode.setFormat(node.getFormat());
|
||||
node.replace(textNode);
|
||||
@@ -136,7 +137,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > match.end) {
|
||||
if (text.length > match.end && $isTextNode(node)) {
|
||||
// This will split out the rest of the text as simple text
|
||||
node.splitText(match.end);
|
||||
|
||||
@@ -163,7 +164,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
|
||||
};
|
||||
|
||||
const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform);
|
||||
const removeReverseNodeTransform = editor.registerNodeTransform<T>(
|
||||
const removeReverseNodeTransform = editor.registerNodeTransform<any>(
|
||||
targetNode,
|
||||
reverseNodeTransform
|
||||
);
|
||||
@@ -217,6 +218,8 @@ export function editorStateToText(editor: LexicalEditor) {
|
||||
`);
|
||||
} else if (child.text) {
|
||||
paragraphText.push(child.text);
|
||||
} else if (child.type === 'variableLabel') {
|
||||
paragraphText.push(child.variableKey);
|
||||
}
|
||||
});
|
||||
editorStateTextString.push(paragraphText.join(''));
|
||||
|
@@ -901,6 +901,7 @@
|
||||
"overSize": "Team members exceed the limit"
|
||||
}
|
||||
},
|
||||
"invalid_variable": "Invalid variable",
|
||||
"navbar": {
|
||||
"Account": "Account",
|
||||
"Chat": "Chat",
|
||||
@@ -1146,6 +1147,7 @@
|
||||
"Quote Content Tip": "You can customize the structure of the quote content to better adapt to different scenarios. You can use some variables for template configuration:\n{{q}} - search content, {{a}} - expected content, {{source}} - source, {{sourceId}} - source file name, {{index}} - the nth quote, they are all optional, here are the default values:\n{{default}}",
|
||||
"Quote Prompt Tip": "You can use {{quote}} to insert the quote content template, and use {{question}} to insert the question. Here are the default values:\n{{default}}"
|
||||
},
|
||||
"unusable_variable": "no usable variable",
|
||||
"user": {
|
||||
"Account": "Account",
|
||||
"Amount of earnings": "Earnings (¥)",
|
||||
@@ -1232,4 +1234,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +1,32 @@
|
||||
{
|
||||
"add_new_input": "Add input",
|
||||
"Code": "Code",
|
||||
"confirm_delete_field_tip": "Confirm to delete the field?",
|
||||
"custom_input": "Custom input",
|
||||
"edit_input": "Edit input",
|
||||
"field_name_already_exists": "Field name already exists",
|
||||
"field_description": "Field description",
|
||||
"field_description_placeholder": "Describes the functionality of this input field, which affects the quality of model generation if the parameter is called for a tool",
|
||||
"field_required": "Required",
|
||||
"field_used_as_tool_input": "As tool input",
|
||||
"input_description": "Input descriotion",
|
||||
"only_the_reference_type_is_supported": "Only the Reference type is supported",
|
||||
"optional_value_type": "Optional value type",
|
||||
"optional_value_type_tip": "One or more data types can be specified, and users can only select the configured type when adding fields in winter",
|
||||
"tool_input": "Tool",
|
||||
"add_new_input": "Add input",
|
||||
"code": {
|
||||
"Reset template": "Reset template",
|
||||
"Reset template confirm": "Are you sure to restore the code template? All input and output to template values will be reset, please be careful to save the current code."
|
||||
},
|
||||
"confirm_delete_field_tip": "Confirm to delete the field?",
|
||||
"custom_input": "Custom input",
|
||||
"edit_input": "Edit input",
|
||||
"field_description": "Field description",
|
||||
"field_description_placeholder": "Describes the functionality of this input field, which affects the quality of model generation if the parameter is called for a tool",
|
||||
"field_name_already_exists": "Field name already exists",
|
||||
"field_required": "Required",
|
||||
"field_used_as_tool_input": "As tool input",
|
||||
"ifelse": {
|
||||
"Input value": "Input",
|
||||
"Select value": "Select"
|
||||
},
|
||||
"input_description": "Input descriotion",
|
||||
"only_the_reference_type_is_supported": "Only the Reference type is supported",
|
||||
"optional_value_type": "Optional value type",
|
||||
"optional_value_type_tip": "One or more data types can be specified, and users can only select the configured type when adding fields in winter",
|
||||
"response": {
|
||||
"Code log": "Log",
|
||||
"Custom inputs": "Custom inputs",
|
||||
"Custom outputs": "Custom outputs",
|
||||
"Error": "Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tool_input": "Tool",
|
||||
"variable_picker_tips": "tips: enter node name or variable name to search"
|
||||
}
|
||||
|
@@ -901,6 +901,7 @@
|
||||
"overSize": "团队成员超出上限"
|
||||
}
|
||||
},
|
||||
"invalid_variable": "无效变量",
|
||||
"navbar": {
|
||||
"Account": "账号",
|
||||
"Chat": "聊天",
|
||||
@@ -1146,6 +1147,7 @@
|
||||
"Quote Content Tip": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置:\n{{q}} - 检索内容,{{a}} - 预期内容,{{source}} - 来源,{{sourceId}} - 来源文件名,{{index}} - 第 n 个引用,他们都是可选的,下面是默认值:\n{{default}}",
|
||||
"Quote Prompt Tip": "可以用 {{quote}} 来插入引用内容模板,使用 {{question}} 来插入问题。下面是默认值:\n{{default}}"
|
||||
},
|
||||
"unusable_variable": "无可用变量",
|
||||
"user": {
|
||||
"Account": "账号",
|
||||
"Amount of earnings": "收益(¥)",
|
||||
@@ -1232,4 +1234,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +1,32 @@
|
||||
{
|
||||
"add_new_input": "新增输入",
|
||||
"Code": "代码",
|
||||
"confirm_delete_field_tip": "确认删除该字段?",
|
||||
"custom_input": "自定义输入",
|
||||
"edit_input": "编辑输入",
|
||||
"field_name_already_exists": "字段名已经存在",
|
||||
"field_description": "字段描述",
|
||||
"field_description_placeholder": "描述该输入字段的功能,如果为工具调用参数,则该描述会影响模型生成的质量",
|
||||
"field_required": "必填",
|
||||
"field_used_as_tool_input": "作为工具调用参数",
|
||||
"input_description": "字段描述",
|
||||
"only_the_reference_type_is_supported": "仅支持引用类型",
|
||||
"optional_value_type": "可选的数据类型",
|
||||
"optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在冬天添加字段时,仅可选择配置的类型",
|
||||
"tool_input": "工具参数",
|
||||
"add_new_input": "新增输入",
|
||||
"code": {
|
||||
"Reset template": "还原模板",
|
||||
"Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。"
|
||||
},
|
||||
"confirm_delete_field_tip": "确认删除该字段?",
|
||||
"custom_input": "自定义输入",
|
||||
"edit_input": "编辑输入",
|
||||
"field_description": "字段描述",
|
||||
"field_description_placeholder": "描述该输入字段的功能,如果为工具调用参数,则该描述会影响模型生成的质量",
|
||||
"field_name_already_exists": "字段名已经存在",
|
||||
"field_required": "必填",
|
||||
"field_used_as_tool_input": "作为工具调用参数",
|
||||
"ifelse": {
|
||||
"Input value": "输入值",
|
||||
"Select value": "选择值"
|
||||
},
|
||||
"input_description": "字段描述",
|
||||
"only_the_reference_type_is_supported": "仅支持引用类型",
|
||||
"optional_value_type": "可选的数据类型",
|
||||
"optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在冬天添加字段时,仅可选择配置的类型",
|
||||
"response": {
|
||||
"Code log": "Log 日志",
|
||||
"Custom inputs": "自定义输入",
|
||||
"Custom outputs": "自定义输出",
|
||||
"Error": "错误信息"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tool_input": "工具参数",
|
||||
"variable_picker_tips": "tips: 可输入节点名或变量名搜索"
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -13,7 +13,6 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
@@ -18,7 +18,6 @@ const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
</Flex>
|
||||
<MyTextarea
|
||||
mt={2}
|
||||
bg={'myWhite.400'}
|
||||
rows={6}
|
||||
fontSize={'sm'}
|
||||
placeholder={t('common:core.app.tip.welcomeTextTip')}
|
||||
|
@@ -107,7 +107,14 @@ const EditForm = ({
|
||||
formatEditorVariablePickerIcon([
|
||||
...getSystemVariables(t),
|
||||
...(appForm.chatConfig.variables || [])
|
||||
]),
|
||||
]).map((item) => ({
|
||||
...item,
|
||||
parent: {
|
||||
id: 'VARIABLE_NODE_ID',
|
||||
label: '全局变量',
|
||||
avatar: '/imgs/workflow/variable.png'
|
||||
}
|
||||
})),
|
||||
[appForm.chatConfig.variables, t]
|
||||
);
|
||||
|
||||
|
@@ -5,13 +5,15 @@ import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs);
|
||||
|
||||
@@ -19,20 +21,50 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
|
||||
// get variable
|
||||
const variables = useCreation(() => {
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
const currentNode = nodeList.find((node) => node.nodeId === nodeId);
|
||||
const nodeVariables = formatEditorVariablePickerIcon(
|
||||
getNodeDynamicInputs(nodeId).map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
parent: {
|
||||
id: currentNode?.nodeId,
|
||||
label: currentNode?.name,
|
||||
avatar: currentNode?.avatar
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
const sourceNodes = computedNodeInputReference({
|
||||
nodeId,
|
||||
nodes: nodeList,
|
||||
edges: edges,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
const nodeVariables = formatEditorVariablePickerIcon(
|
||||
getNodeDynamicInputs(nodeId).map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
);
|
||||
const sourceNodeVariables = !sourceNodes
|
||||
? []
|
||||
: sourceNodes
|
||||
.map((node) => {
|
||||
return node.outputs
|
||||
.filter((output) => !!output.label)
|
||||
.map((output) => {
|
||||
return {
|
||||
label: t((output.label as any) || ''),
|
||||
key: output.id,
|
||||
parent: {
|
||||
id: node.nodeId,
|
||||
label: node.name,
|
||||
avatar: node.avatar
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
|
||||
return [...globalVariables, ...nodeVariables];
|
||||
const formatSourceNodeVariables = formatEditorVariablePickerIcon(sourceNodeVariables);
|
||||
|
||||
return [...nodeVariables, ...formatSourceNodeVariables];
|
||||
}, [nodeList, inputs, t]);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
Reference in New Issue
Block a user