perf:textarea auto height (#2967)

* perf:textarea auto height

* optimize editor height & fix variable label split
This commit is contained in:
heheer
2024-10-22 18:33:02 +08:00
committed by shilin66
parent 90dcb35b40
commit f8a45a63bb
11 changed files with 100 additions and 46 deletions

View File

@@ -61,6 +61,7 @@ export default function Editor({
const [key, setKey] = useState(getNanoid(6)); const [key, setKey] = useState(getNanoid(6));
const [_, startSts] = useTransition(); const [_, startSts] = useTransition();
const [focus, setFocus] = useState(false); const [focus, setFocus] = useState(false);
const [scrollHeight, setScrollHeight] = useState(0);
const initialConfig = { const initialConfig = {
namespace: 'promptEditor', namespace: 'promptEditor',
@@ -128,6 +129,8 @@ export default function Editor({
<FocusPlugin focus={focus} setFocus={setFocus} /> <FocusPlugin focus={focus} setFocus={setFocus} />
<OnChangePlugin <OnChangePlugin
onChange={(editorState, editor) => { onChange={(editorState, editor) => {
const rootElement = editor.getRootElement();
setScrollHeight(rootElement?.scrollHeight || 0);
startSts(() => { startSts(() => {
onChange?.(editorState, editor); onChange?.(editorState, editor);
}); });
@@ -139,7 +142,7 @@ export default function Editor({
<VariablePickerPlugin variables={variableLabels.length > 0 ? [] : variables} /> <VariablePickerPlugin variables={variableLabels.length > 0 ? [] : variables} />
<OnBlurPlugin onBlur={onBlur} /> <OnBlurPlugin onBlur={onBlur} />
</LexicalComposer> </LexicalComposer>
{showOpenModal && ( {showOpenModal && scrollHeight > maxH && (
<Box <Box
zIndex={10} zIndex={10}
position={'absolute'} position={'absolute'}

View File

@@ -11,7 +11,9 @@ export default function VariableLabel({
nodeAvatar: string; nodeAvatar: string;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [parentLabel, childLabel] = variableLabel.split('.'); // avoid including '.' in the variable name.
const [parentLabel, ...childLabels] = variableLabel.split('.');
const childLabel = childLabels.join('.');
return ( return (
<> <>

View File

@@ -57,6 +57,7 @@
"react-i18next": "14.1.2", "react-i18next": "14.1.2",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.4",
"reactflow": "^11.7.4", "reactflow": "^11.7.4",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.0",

View File

@@ -1,4 +1,4 @@
import React, { useRef } from 'react'; import React, { useRef, useState } from 'react';
import { import {
Box, Box,
@@ -13,11 +13,12 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import ResizeTextarea from 'react-textarea-autosize';
type Props = TextareaProps & { type Props = TextareaProps & {
title?: string; title?: string;
iconSrc?: string; iconSrc?: string;
// variables: string[]; autoHeight?: boolean;
}; };
const MyTextarea = React.forwardRef<HTMLTextAreaElement, Props>(function MyTextarea(props, ref) { const MyTextarea = React.forwardRef<HTMLTextAreaElement, Props>(function MyTextarea(props, ref) {
@@ -28,6 +29,10 @@ const MyTextarea = React.forwardRef<HTMLTextAreaElement, Props>(function MyTexta
const { const {
title = t('common:core.app.edit.Prompt Editor'), title = t('common:core.app.edit.Prompt Editor'),
iconSrc = 'modal/edit', iconSrc = 'modal/edit',
autoHeight = false,
onChange,
maxH,
minH,
...childProps ...childProps
} = props; } = props;
@@ -35,16 +40,27 @@ const MyTextarea = React.forwardRef<HTMLTextAreaElement, Props>(function MyTexta
return ( return (
<> <>
<Editor textareaRef={TextareaRef} {...childProps} onOpenModal={onOpen} /> <Editor
textareaRef={TextareaRef}
autoHeight={autoHeight}
onChange={onChange}
maxH={maxH}
minH={minH}
showResize={!autoHeight}
{...childProps}
onOpenModal={onOpen}
/>
{isOpen && ( {isOpen && (
<MyModal iconSrc={iconSrc} title={title} isOpen onClose={onClose}> <MyModal iconSrc={iconSrc} title={title} isOpen onClose={onClose}>
<ModalBody> <ModalBody>
<Editor <Editor
textareaRef={ModalTextareaRef} textareaRef={ModalTextareaRef}
onChange={onChange}
{...childProps} {...childProps}
minH={'300px'} maxH={500}
maxH={'auto'} minH={500}
minW={['100%', '512px']} minW={['100%', '512px']}
showResize={false}
/> />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@@ -71,17 +87,44 @@ export default React.memo(MyTextarea);
const Editor = React.memo(function Editor({ const Editor = React.memo(function Editor({
onOpenModal, onOpenModal,
textareaRef, textareaRef,
autoHeight = false,
onChange,
maxH,
minH,
showResize,
...props ...props
}: Props & { }: Props & {
textareaRef: React.RefObject<HTMLTextAreaElement>; textareaRef: React.RefObject<HTMLTextAreaElement>;
onOpenModal?: () => void; onOpenModal?: () => void;
showResize?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [scrollHeight, setScrollHeight] = useState(0);
return ( return (
<Box h={'100%'} w={'100%'} position={'relative'}> <Box h={'100%'} w={'100%'} position={'relative'}>
<Textarea ref={textareaRef} maxW={'100%'} {...props} /> <Textarea
{onOpenModal && ( ref={textareaRef}
maxW={'100%'}
as={autoHeight ? ResizeTextarea : undefined}
sx={
!showResize
? {
'::-webkit-resizer': {
display: 'none'
}
}
: {}
}
{...props}
maxH={`${maxH}px`}
minH={`${minH}px`}
onChange={(e) => {
setScrollHeight(e.target.scrollHeight);
onChange?.(e);
}}
/>
{onOpenModal && maxH && scrollHeight > Number(maxH) && (
<Box <Box
zIndex={1} zIndex={1}
position={'absolute'} position={'absolute'}

View File

@@ -23,12 +23,12 @@ import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import MyRadio from '@/components/common/MyRadio'; import MyRadio from '@/components/common/MyRadio';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import SelectAiModel from '@/components/Select/AIModelSelector'; import SelectAiModel from '@/components/Select/AIModelSelector';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
export type DatasetParamsProps = { export type DatasetParamsProps = {
searchMode: `${DatasetSearchModeEnum}`; searchMode: `${DatasetSearchModeEnum}`;
@@ -317,14 +317,14 @@ const DatasetParamsModal = ({
></QuestionTip> ></QuestionTip>
</Flex> </Flex>
<Box mt={1}> <Box mt={1}>
<PromptEditor <MyTextarea
autoHeight
minH={150} minH={150}
maxH={300} maxH={300}
showOpenModal={false}
placeholder={t('common:core.module.QueryExtension.placeholder')} placeholder={t('common:core.module.QueryExtension.placeholder')}
value={cfbBgDesc} value={cfbBgDesc}
onChange={(e) => { onChange={(e) => {
setValue('datasetSearchExtensionBg', e); setValue('datasetSearchExtensionBg', e.target.value);
}} }}
/> />
</Box> </Box>

View File

@@ -24,6 +24,9 @@ const WelcomeTextConfig = (props: TextareaProps) => {
fontSize={'sm'} fontSize={'sm'}
bg={'myGray.50'} bg={'myGray.50'}
placeholder={t('common:core.app.tip.welcomeTextTip')} placeholder={t('common:core.app.tip.welcomeTextTip')}
autoHeight
minH={100}
maxH={200}
{...props} {...props}
/> />
</> </>

View File

@@ -1,11 +1,10 @@
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useMemo } from 'react';
import { Controller, UseFormReturn } from 'react-hook-form'; import { Controller, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { import {
Box, Box,
Button, Button,
Card, Card,
Input,
NumberDecrementStepper, NumberDecrementStepper,
NumberIncrementStepper, NumberIncrementStepper,
NumberInput, NumberInput,
@@ -24,7 +23,7 @@ import { ChatBoxContext } from '../Provider';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useDeepCompareEffect } from 'ahooks'; import { useDeepCompareEffect } from 'ahooks';
import { VariableItemType } from '@fastgpt/global/core/app/type'; import { VariableItemType } from '@fastgpt/global/core/app/type';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
export const VariableInputItem = ({ export const VariableInputItem = ({
item, item,
@@ -60,13 +59,13 @@ export const VariableInputItem = ({
{item.description && <QuestionTip ml={1} label={item.description} />} {item.description && <QuestionTip ml={1} label={item.description} />}
</Box> </Box>
{item.type === VariableInputEnum.input && ( {item.type === VariableInputEnum.input && (
<PromptEditor <MyTextarea
value={item.defaultValue} autoHeight
onChange={(e) => setValue(item.key, e)}
bg={'myGray.50'}
minH={40} minH={40}
maxH={150} maxH={160}
showOpenModal={false} bg={'myGray.50'}
value={item.defaultValue}
onChange={(e) => setValue(item.key, e.target.value)}
/> />
)} )}
{item.type === VariableInputEnum.textarea && ( {item.type === VariableInputEnum.textarea && (

View File

@@ -38,7 +38,7 @@ import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/consta
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
type props = { type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType; value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -221,12 +221,15 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
{input.description && <QuestionTip ml={1} label={input.description} />} {input.description && <QuestionTip ml={1} label={input.description} />}
</Flex> </Flex>
{input.type === FlowNodeInputTypeEnum.input && ( {input.type === FlowNodeInputTypeEnum.input && (
<PromptEditor <MyTextarea
value={input.value} isDisabled={interactive.params.submitted}
onChange={(e) => setValue(input.label, e)} {...register(input.label, {
required: input.required
})}
bg={'white'}
autoHeight
minH={40} minH={40}
maxH={100} maxH={100}
showOpenModal={false}
/> />
)} )}
{input.type === FlowNodeInputTypeEnum.textarea && ( {input.type === FlowNodeInputTypeEnum.textarea && (

View File

@@ -33,7 +33,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context'; import { AppContext } from '../../../context';
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput'; import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
const MyRightDrawer = dynamic( const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer') () => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
@@ -270,16 +270,16 @@ export const useDebug = () => {
const RenderInput = (() => { const RenderInput = (() => {
if (input.valueType === WorkflowIOValueTypeEnum.string) { if (input.valueType === WorkflowIOValueTypeEnum.string) {
return ( return (
<PromptEditor <MyTextarea
autoHeight
minH={40}
maxH={160}
bg={'myGray.50'}
placeholder={t(input.placeholder || ('' as any))}
value={getValues(`nodeVariables.${input.key}`)} value={getValues(`nodeVariables.${input.key}`)}
onChange={(e) => { onChange={(e) => {
setValue(`nodeVariables.${input.key}`, e); setValue(`nodeVariables.${input.key}`, e.target.value);
}} }}
minH={50}
maxH={150}
showOpenModal={false}
placeholder={t(input.placeholder || ('' as any))}
bg={'myGray.50'}
/> />
); );
} }

View File

@@ -34,7 +34,7 @@ import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
type ListValueType = { id: string; value: string; label: string }[]; type ListValueType = { id: string; value: string; label: string }[];
@@ -316,15 +316,12 @@ const InputTypeConfig = ({
</NumberInput> </NumberInput>
)} )}
{inputType === FlowNodeInputTypeEnum.input && ( {inputType === FlowNodeInputTypeEnum.input && (
<PromptEditor <MyTextarea
value={defaultValue} {...register('defaultValue')}
onChange={(e) => {
setValue('defaultValue', e);
}}
minH={40}
maxH={200}
showOpenModal={false}
bg={'myGray.50'} bg={'myGray.50'}
autoHeight
minH={40}
maxH={100}
/> />
)} )}
{inputType === FlowNodeInputTypeEnum.JSONEditor && ( {inputType === FlowNodeInputTypeEnum.JSONEditor && (

View File

@@ -1,4 +1,4 @@
import React, { Dispatch, useMemo, useState } from 'react'; import React, { Dispatch, useMemo } from 'react';
import { NodeProps } from 'reactflow'; import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard'; import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -10,7 +10,7 @@ import MyTextarea from '@/components/common/Textarea/MyTextarea';
import { AppContext } from '../../../../context'; import { AppContext } from '../../../../context';
import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type'; import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type';
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useCreation, useMount } from 'ahooks'; import { useMount } from 'ahooks';
import ChatFunctionTip from '@/components/core/app/Tip'; import ChatFunctionTip from '@/components/core/app/Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { WorkflowContext } from '../../../context'; import { WorkflowContext } from '../../../context';
@@ -96,6 +96,9 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
resize={'both'} resize={'both'}
placeholder={t('workflow:plugin.Instruction_Tip')} placeholder={t('workflow:plugin.Instruction_Tip')}
value={instruction} value={instruction}
autoHeight
minH={100}
maxH={240}
onChange={(e) => { onChange={(e) => {
setAppDetail((state) => ({ setAppDetail((state) => ({
...state, ...state,