Feat: Fixed the issue where the cursor would go to the end when changing its own data #9869 (#10316)

### What problem does this PR solve?

Feat: Fixed the issue where the cursor would go to the end when changing
its own data #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-26 19:55:42 +08:00
committed by GitHub
parent 4c0a89f262
commit 6b9b785b5c
12 changed files with 145 additions and 42 deletions

View File

@ -8,7 +8,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { PropsWithChildren } from 'react'; import { DialogProps } from '@radix-ui/react-dialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface IProps { interface IProps {
@ -24,7 +24,10 @@ export function ConfirmDeleteDialog({
onOk, onOk,
onCancel, onCancel,
hidden = false, hidden = false,
}: IProps & PropsWithChildren) { onOpenChange,
open,
defaultOpen,
}: IProps & DialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
if (hidden) { if (hidden) {
@ -32,7 +35,11 @@ export function ConfirmDeleteDialog({
} }
return ( return (
<AlertDialog> <AlertDialog
onOpenChange={onOpenChange}
open={open}
defaultOpen={defaultOpen}
>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger> <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent <AlertDialogContent
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}

View File

@ -1765,6 +1765,9 @@ Important structured information may include: names, dates, locations, events, k
metadata: `Content: [INSERT CONTENT HERE]`, metadata: `Content: [INSERT CONTENT HERE]`,
}, },
}, },
cancel: 'Cancel',
swicthPromptMessage:
'The prompt word will change. Please confirm whether to abandon the existing prompt word?',
}, },
}, },
}; };

View File

@ -1683,6 +1683,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
metadata: `内容:[在此处插入内容]`, metadata: `内容:[在此处插入内容]`,
}, },
}, },
cancel: '取消',
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
}, },
}, },
}; };

View File

@ -10,7 +10,6 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { import {
LexicalTypeaheadMenuPlugin, LexicalTypeaheadMenuPlugin,
MenuOption, MenuOption,
useBasicTypeaheadTriggerMatch,
} from '@lexical/react/LexicalTypeaheadMenuPlugin'; } from '@lexical/react/LexicalTypeaheadMenuPlugin';
import { import {
$createParagraphNode, $createParagraphNode,
@ -131,9 +130,23 @@ export default function VariablePickerMenuPlugin({
baseOptions, baseOptions,
}: VariablePickerMenuPluginProps): JSX.Element { }: VariablePickerMenuPluginProps): JSX.Element {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0, // const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
}); // minLength: 0,
// });
const testTriggerFn = React.useCallback((text: string) => {
const lastChar = text.slice(-1);
if (lastChar === '/') {
console.log('Found trigger character "/"');
return {
leadOffset: text.length - 1,
matchingString: '',
replaceableString: '/',
};
}
return null;
}, []);
const previousValue = useRef<string | undefined>(); const previousValue = useRef<string | undefined>();
@ -291,6 +304,21 @@ export default function VariablePickerMenuPlugin({
} }
}, [parseTextToVariableNodes, editor, value]); }, [parseTextToVariableNodes, editor, value]);
// Fixed the issue where the cursor would go to the end when changing its own data
useEffect(() => {
return editor.registerUpdateListener(({ editorState, tags }) => {
// If we trigger the programmatic update ourselves, we should not write back to avoid an infinite loop.
if (tags.has(ProgrammaticTag)) return;
editorState.read(() => {
const text = $getRoot().getTextContent();
if (text !== previousValue.current) {
previousValue.current = text;
}
});
});
}, [editor]);
return ( return (
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption> <LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
onQueryChange={setQueryString} onQueryChange={setQueryString}
@ -301,7 +329,7 @@ export default function VariablePickerMenuPlugin({
closeMenu, closeMenu,
) )
} }
triggerFn={checkForTriggerMatch} triggerFn={testTriggerFn}
options={buildNextOptions()} options={buildNextOptions()}
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => {
const nextOptions = buildNextOptions(); const nextOptions = buildNextOptions();

View File

@ -1,3 +1,4 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { LargeModelFormField } from '@/components/large-model-form-field'; import { LargeModelFormField } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { SelectWithSearch } from '@/components/originui/select-with-search'; import { SelectWithSearch } from '@/components/originui/select-with-search';
@ -6,7 +7,7 @@ import { Form } from '@/components/ui/form';
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor'; import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useCallback } from 'react'; import { memo } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
@ -19,6 +20,7 @@ import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper'; import { FormWrapper } from '../components/form-wrapper';
import { useSwitchPrompt } from './use-switch-prompt';
export const FormSchema = z.object({ export const FormSchema = z.object({
field_name: z.string(), field_name: z.string(),
@ -43,25 +45,13 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow'); const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow');
const setPromptValue = useCallback( const {
(field: keyof ExtractorFormSchemaType, key: string, value: string) => { handleFieldNameChange,
form.setValue(field, t(`dataflow.prompts.${key}.${value}`), { confirmSwitch,
shouldDirty: true, hideModal,
shouldValidate: true, visible,
}); cancelSwitch,
}, } = useSwitchPrompt(form);
[form, t],
);
const handleFieldNameChange = useCallback(
(value: string) => {
if (value) {
setPromptValue('sys_prompt', 'system', value);
setPromptValue('prompts', 'user', value);
}
},
[setPromptValue],
);
useWatchFormChange(node?.id, form); useWatchFormChange(node?.id, form);
@ -96,6 +86,15 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
></PromptEditor> ></PromptEditor>
</RAGFlowFormItem> </RAGFlowFormItem>
</FormWrapper> </FormWrapper>
{visible && (
<ConfirmDeleteDialog
title={t('dataflow.switchPromptMessage')}
open
onOpenChange={hideModal}
onOk={confirmSwitch}
onCancel={cancelSwitch}
></ConfirmDeleteDialog>
)}
</Form> </Form>
); );
}; };

View File

@ -0,0 +1,69 @@
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback, useRef } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
export const FormSchema = z.object({
field_name: z.string(),
sys_prompt: z.string(),
prompts: z.string().optional(),
...LlmSettingSchema,
});
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) {
const { visible, showModal, hideModal } = useSetModalState();
const { t } = useTranslation();
const previousFieldNames = useRef<string[]>([form.getValues('field_name')]);
const setPromptValue = useCallback(
(field: keyof ExtractorFormSchemaType, key: string, value: string) => {
form.setValue(field, t(`dataflow.prompts.${key}.${value}`), {
shouldDirty: true,
shouldValidate: true,
});
},
[form, t],
);
const handleFieldNameChange = useCallback(
(value: string) => {
if (value) {
const names = previousFieldNames.current;
if (names.length > 1) {
names.shift();
}
names.push(value);
showModal();
}
},
[showModal],
);
const confirmSwitch = useCallback(() => {
const value = form.getValues('field_name');
setPromptValue('sys_prompt', 'system', value);
setPromptValue('prompts', 'user', value);
}, [form, setPromptValue]);
const cancelSwitch = useCallback(() => {
const previousValue = previousFieldNames.current.at(-2);
if (previousValue) {
form.setValue('field_name', previousValue, {
shouldDirty: true,
shouldValidate: true,
});
}
}, [form]);
return {
handleFieldNameChange,
confirmSwitch,
hideModal,
visible,
cancelSwitch,
};
}

View File

@ -4,11 +4,9 @@ import { useCallback } from 'react';
export function useCancelCurrentDataflow({ export function useCancelCurrentDataflow({
messageId, messageId,
setMessageId, setMessageId,
hideLogSheet,
}: { }: {
messageId: string; messageId: string;
setMessageId: (messageId: string) => void; setMessageId: (messageId: string) => void;
hideLogSheet(): void;
}) { }) {
const { cancelDataflow } = useCancelDataflow(); const { cancelDataflow } = useCancelDataflow();
@ -16,9 +14,8 @@ export function useCancelCurrentDataflow({
const code = await cancelDataflow(messageId); const code = await cancelDataflow(messageId);
if (code === 0) { if (code === 0) {
setMessageId(''); setMessageId('');
hideLogSheet();
} }
}, [cancelDataflow, hideLogSheet, messageId, setMessageId]); }, [cancelDataflow, messageId, setMessageId]);
return { handleCancel }; return { handleCancel };
} }

View File

@ -4,10 +4,6 @@ import useGraphStore from '../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control }); let values = useWatch({ control: form?.control });
console.log(
'🚀 ~ useWatchFormChange ~ values:',
JSON.stringify(values, null, 2),
);
const updateNodeForm = useGraphStore((state) => state.updateNodeForm); const updateNodeForm = useGraphStore((state) => state.updateNodeForm);

View File

@ -103,7 +103,6 @@ export default function DataFlow() {
const { handleCancel } = useCancelCurrentDataflow({ const { handleCancel } = useCancelCurrentDataflow({
messageId, messageId,
setMessageId, setMessageId,
hideLogSheet,
}); });
const time = useWatchAgentChange(chatDrawerVisible); const time = useWatchAgentChange(chatDrawerVisible);

View File

@ -57,16 +57,16 @@ export function LogSheet({
</section> </section>
{isParsing ? ( {isParsing ? (
<Button <Button
className="w-full mt-8 bg-state-error/10 text-state-error" className="w-full mt-8 bg-state-error/10 text-state-error hover:bg-state-error hover:text-bg-base"
onClick={handleCancel} onClick={handleCancel}
> >
<CirclePause /> Cancel <CirclePause /> {t('dataflow.cancel')}
</Button> </Button>
) : ( ) : (
<Button <Button
onClick={handleDownloadJson} onClick={handleDownloadJson}
disabled={isEndOutputEmpty(logs)} disabled={isEndOutputEmpty(logs)}
className="w-full mt-8" className="w-full mt-8 bg-accent-primary-5 text-text-secondary hover:bg-accent-primary-5 hover:text-accent-primary hover:border-accent-primary hover:border"
> >
<SquareArrowOutUpRight /> <SquareArrowOutUpRight />
{t('dataflow.exportJson')} {t('dataflow.exportJson')}

View File

@ -76,7 +76,10 @@ module.exports = {
'border-default': 'var(--border-default)', 'border-default': 'var(--border-default)',
'border-accent': 'var(--border-accent)', 'border-accent': 'var(--border-accent)',
'border-button': 'var(--border-button)', 'border-button': 'var(--border-button)',
'accent-primary': 'var(--accent-primary)', 'accent-primary': {
DEFAULT: 'rgb(var(--accent-primary) / <alpha-value>)',
5: 'rgba(var(--accent-primary) / 0.05)', // 5%
},
'bg-accent': 'var(--bg-accent)', 'bg-accent': 'var(--bg-accent)',
'state-success': 'var(--state-success)', 'state-success': 'var(--state-success)',
'state-warning': 'var(--state-warning)', 'state-warning': 'var(--state-warning)',

View File

@ -112,7 +112,7 @@
--border-accent: #000000; --border-accent: #000000;
--border-button: rgba(0, 0, 0, 0.1); --border-button: rgba(0, 0, 0, 0.1);
/* Regulators, parsing, switches, variables */ /* Regulators, parsing, switches, variables */
--accent-primary: #00beb4; --accent-primary: 0 190 180;
/* Output Variables Box */ /* Output Variables Box */
--bg-accent: rgba(76, 164, 231, 0.05); --bg-accent: rgba(76, 164, 231, 0.05);