Fix: Optimized variable node display and Agent template multi-language support #3221 (#9787)

### What problem does this PR solve?

Fix: Optimized variable node display and Agent template multi-language
support #3221

- Modified the VariableNode component to add parent label and icon
properties
- Updated the VariablePickerMenuPlugin to support displaying parent
labels and icons
- Adjusted useBuildNodeOutputOptions and useBuildBeginVariableOptions to
pass new properties
- Optimized the Agent TemplateCard component to switch the title and
description based on the language

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-08-28 15:43:25 +08:00
committed by GitHub
parent 5fe8cf6018
commit f89e55ec42
9 changed files with 134 additions and 50 deletions

View File

@ -235,7 +235,7 @@ function MarkdownContent({
<HoverCardTrigger> <HoverCardTrigger>
<CircleAlert className="size-4 inline-block" /> <CircleAlert className="size-4 inline-block" />
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent> <HoverCardContent className="max-w-3xl">
{renderPopoverContent(chunkIndex)} {renderPopoverContent(chunkIndex)}
</HoverCardContent> </HoverCardContent>
</HoverCard> </HoverCard>

View File

@ -48,10 +48,16 @@ export interface IFlowTemplate {
canvas_type: string; canvas_type: string;
create_date: string; create_date: string;
create_time: number; create_time: number;
description: string; description: {
en: string;
zh: string;
};
dsl: DSL; dsl: DSL;
id: string; id: string;
title: string; title: {
en: string;
zh: string;
};
update_date: string; update_date: string;
update_time: number; update_time: number;
} }

View File

@ -10,7 +10,6 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { import {
$getRoot, $getRoot,
$getSelection, $getSelection,
$nodesOfType,
EditorState, EditorState,
Klass, Klass,
LexicalNode, LexicalNode,
@ -135,9 +134,8 @@ export function PromptEditor({
const onValueChange = useCallback( const onValueChange = useCallback(
(editorState: EditorState) => { (editorState: EditorState) => {
editorState?.read(() => { editorState?.read(() => {
const listNodes = $nodesOfType(VariableNode); // to be removed // const listNodes = $nodesOfType(VariableNode); // to be removed
// const allNodes = $dfs(); // const allNodes = $dfs();
console.log('🚀 ~ onChange ~ allNodes:', listNodes);
const text = $getRoot().getTextContent(); const text = $getRoot().getTextContent();

View File

@ -1,4 +1,3 @@
import i18n from '@/locales/config';
import { BeginId } from '@/pages/flow/constant'; import { BeginId } from '@/pages/flow/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical'; import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
@ -7,19 +6,36 @@ const prefix = BeginId + '@';
export class VariableNode extends DecoratorNode<ReactNode> { export class VariableNode extends DecoratorNode<ReactNode> {
__value: string; __value: string;
__label: string; __label: string;
key?: NodeKey;
__parentLabel?: string | ReactNode;
__icon?: ReactNode;
static getType(): string { static getType(): string {
return 'variable'; return 'variable';
} }
static clone(node: VariableNode): VariableNode { static clone(node: VariableNode): VariableNode {
return new VariableNode(node.__value, node.__label, node.__key); return new VariableNode(
node.__value,
node.__label,
node.__key,
node.__parentLabel,
node.__icon,
);
} }
constructor(value: string, label: string, key?: NodeKey) { constructor(
value: string,
label: string,
key?: NodeKey,
parent?: string | ReactNode,
icon?: ReactNode,
) {
super(key); super(key);
this.__value = value; this.__value = value;
this.__label = label; this.__label = label;
this.__parentLabel = parent;
this.__icon = icon;
} }
createDOM(): HTMLElement { createDOM(): HTMLElement {
@ -35,17 +51,20 @@ export class VariableNode extends DecoratorNode<ReactNode> {
decorate(): ReactNode { decorate(): ReactNode {
let content: ReactNode = ( let content: ReactNode = (
<span className="text-blue-600">{this.__label}</span> <div className="text-blue-600">{this.__label}</div>
); );
if (this.__value?.startsWith(prefix)) { if (this.__parentLabel) {
content = ( content = (
<div> <div className="flex items-center gap-1 text-text-primary ">
<span>{i18n.t(`flow.begin`)}</span> / {content} <div>{this.__icon}</div>
<div>{this.__parentLabel}</div>
<div className="text-text-disabled mr-1">/</div>
{content}
</div> </div>
); );
} }
return ( return (
<div className="bg-gray-200 dark:bg-gray-400 text-primary inline-flex items-center rounded-md px-2 py-0"> <div className="bg-gray-200 dark:bg-gray-400 text-sm inline-flex items-center rounded-md px-2 py-1">
{content} {content}
</div> </div>
); );
@ -59,8 +78,10 @@ export class VariableNode extends DecoratorNode<ReactNode> {
export function $createVariableNode( export function $createVariableNode(
value: string, value: string,
label: string, label: string,
parentLabel: string | ReactNode,
icon?: ReactNode,
): VariableNode { ): VariableNode {
return new VariableNode(value, label); return new VariableNode(value, label, undefined, parentLabel, icon);
} }
export function $isVariableNode( export function $isVariableNode(

View File

@ -20,7 +20,13 @@ import {
$isRangeSelection, $isRangeSelection,
TextNode, TextNode,
} from 'lexical'; } from 'lexical';
import React, { ReactElement, useCallback, useEffect, useRef } from 'react'; import React, {
ReactElement,
ReactNode,
useCallback,
useEffect,
useRef,
} from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { $createVariableNode } from './variable-node'; import { $createVariableNode } from './variable-node';
@ -31,11 +37,20 @@ import './index.css';
class VariableInnerOption extends MenuOption { class VariableInnerOption extends MenuOption {
label: string; label: string;
value: string; value: string;
parentLabel: string | JSX.Element;
icon?: ReactNode;
constructor(label: string, value: string) { constructor(
label: string,
value: string,
parentLabel: string | JSX.Element,
icon?: ReactNode,
) {
super(value); super(value);
this.label = label; this.label = label;
this.value = value; this.value = value;
this.parentLabel = parentLabel;
this.icon = icon;
} }
} }
@ -111,7 +126,6 @@ export default function VariablePickerMenuPlugin({
const buildNextOptions = useCallback(() => { const buildNextOptions = useCallback(() => {
let filteredOptions = options; let filteredOptions = options;
if (queryString) { if (queryString) {
const lowerQuery = queryString.toLowerCase(); const lowerQuery = queryString.toLowerCase();
filteredOptions = options filteredOptions = options
@ -131,23 +145,28 @@ export default function VariablePickerMenuPlugin({
new VariableOption( new VariableOption(
x.label, x.label,
x.title, x.title,
x.options.map((y) => new VariableInnerOption(y.label, y.value)), x.options.map((y) => {
return new VariableInnerOption(y.label, y.value, x.label, y.icon);
}),
), ),
); );
return nextOptions; return nextOptions;
}, [options, queryString]); }, [options, queryString]);
const findLabelByValue = useCallback( const findItemByValue = useCallback(
(value: string) => { (value: string) => {
const children = options.reduce<Array<{ label: string; value: string }>>( const children = options.reduce<
(pre, cur) => { Array<{
return pre.concat(cur.options); label: string;
}, value: string;
[], parentLabel?: string | ReactNode;
); icon?: ReactNode;
}>
>((pre, cur) => {
return pre.concat(cur.options);
}, []);
return children.find((x) => x.value === value)?.label; return children.find((x) => x.value === value);
}, },
[options], [options],
); );
@ -168,13 +187,13 @@ export default function VariablePickerMenuPlugin({
if (nodeToRemove) { if (nodeToRemove) {
nodeToRemove.remove(); nodeToRemove.remove();
} }
const variableNode = $createVariableNode(
selection.insertNodes([ (selectedOption as VariableInnerOption).value,
$createVariableNode( selectedOption.label as string,
(selectedOption as VariableInnerOption).value, selectedOption.parentLabel as string | ReactNode,
selectedOption.label as string, selectedOption.icon as ReactNode,
), );
]); selection.insertNodes([variableNode]);
closeMenu(); closeMenu();
}); });
@ -190,7 +209,6 @@ export default function VariablePickerMenuPlugin({
const regex = /{([^}]*)}/g; const regex = /{([^}]*)}/g;
let match; let match;
let lastIndex = 0; let lastIndex = 0;
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
const { 1: content, index, 0: template } = match; const { 1: content, index, 0: template } = match;
@ -202,9 +220,17 @@ export default function VariablePickerMenuPlugin({
} }
// Add variable node or text node // Add variable node or text node
const label = findLabelByValue(content); const nodeItem = findItemByValue(content);
if (label) {
paragraph.append($createVariableNode(content, label)); if (nodeItem) {
paragraph.append(
$createVariableNode(
content,
nodeItem.label,
nodeItem.parentLabel,
nodeItem.icon,
),
);
} else { } else {
paragraph.append($createTextNode(template)); paragraph.append($createTextNode(template));
} }
@ -225,7 +251,7 @@ export default function VariablePickerMenuPlugin({
$getRoot().selectEnd(); $getRoot().selectEnd();
} }
}, },
[findLabelByValue], [findItemByValue],
); );
useEffect(() => { useEffect(() => {

View File

@ -6,7 +6,14 @@ import { DefaultOptionType } from 'antd/es/select';
import { t } from 'i18next'; import { t } from 'i18next';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import get from 'lodash/get'; import get from 'lodash/get';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import {
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { import {
AgentDialogueMode, AgentDialogueMode,
BeginId, BeginId,
@ -17,6 +24,7 @@ import {
import { AgentFormContext } from '../context'; import { AgentFormContext } from '../context';
import { buildBeginInputListFromObject } from '../form/begin-form/utils'; import { buildBeginInputListFromObject } from '../form/begin-form/utils';
import { BeginQuery } from '../interface'; import { BeginQuery } from '../interface';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store'; import useGraphStore from '../store';
export function useSelectBeginNodeDataInputs() { export function useSelectBeginNodeDataInputs() {
@ -98,10 +106,14 @@ function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
export function buildOutputOptions( export function buildOutputOptions(
outputs: Record<string, any> = {}, outputs: Record<string, any> = {},
nodeId?: string, nodeId?: string,
parentLabel?: string | ReactNode,
icon?: ReactNode,
) { ) {
return Object.keys(outputs).map((x) => ({ return Object.keys(outputs).map((x) => ({
label: x, label: x,
value: `${nodeId}@${x}`, value: `${nodeId}@${x}`,
parentLabel,
icon,
type: outputs[x]?.type, type: outputs[x]?.type,
})); }));
} }
@ -127,7 +139,12 @@ export function useBuildNodeOutputOptions(nodeId?: string) {
label: x.data.name, label: x.data.name,
value: x.id, value: x.id,
title: x.data.name, title: x.data.name,
options: buildOutputOptions(x.data.form.outputs, x.id), options: buildOutputOptions(
x.data.form.outputs,
x.id,
x.data.name,
<OperatorIcon name={x.data.label as Operator} />,
),
})); }));
}, [edges, nodeId, nodes]); }, [edges, nodeId, nodes]);
@ -162,9 +179,11 @@ export function useBuildBeginVariableOptions() {
return [ return [
{ {
label: <span>{t('flow.beginInput')}</span>, label: <span>{t('flow.beginInput')}</span>,
title: 'Begin Input', title: t('flow.beginInput'),
options: inputs.map((x) => ({ options: inputs.map((x) => ({
label: x.name, label: x.name,
parentLabel: <span>{t('flow.beginInput')}</span>,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
value: `begin@${x.key}`, value: `begin@${x.key}`,
type: transferToVariableType(x.type), type: transferToVariableType(x.type),
})), })),
@ -191,12 +210,13 @@ export function useBuildQueryVariableOptions(n?: RAGFlowNodeType) {
const { data } = useFetchAgent(); const { data } = useFetchAgent();
const node = useContext(AgentFormContext) || n; const node = useContext(AgentFormContext) || n;
const options = useBuildVariableOptions(node?.id, node?.parentId); const options = useBuildVariableOptions(node?.id, node?.parentId);
const nextOptions = useMemo(() => { const nextOptions = useMemo(() => {
const globals = data?.dsl?.globals ?? {}; const globals = data?.dsl?.globals ?? {};
const globalOptions = Object.entries(globals).map(([key, value]) => ({ const globalOptions = Object.entries(globals).map(([key, value]) => ({
label: key, label: key,
value: key, value: key,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
parentLabel: <span>{t('flow.beginInput')}</span>,
type: Array.isArray(value) type: Array.isArray(value)
? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}` ? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}`
: typeof value, : typeof value,

View File

@ -64,7 +64,12 @@ const OperatorIcon = ({ name, className }: IProps) => {
if (name === Operator.Begin) { if (name === Operator.Begin) {
return ( return (
<div className="inline-block p-1 bg-accent-primary rounded-sm"> <div
className={cn(
'inline-block p-1 bg-accent-primary rounded-sm',
className,
)}
>
<HousePlus className="rounded size-3" /> <HousePlus className="rounded size-3" />
</div> </div>
); );

View File

@ -204,6 +204,7 @@ const useGraphStore = create<RFState>()(
set({ nodes: nextNodes }); set({ nodes: nextNodes });
}, },
getNode: (id?: string | null) => { getNode: (id?: string | null) => {
// console.log('getNode', id, get().nodes);
return get().nodes.find((x) => x.id === id); return get().nodes.find((x) => x.id === id);
}, },
getOperatorTypeFromId: (id?: string | null) => { getOperatorTypeFromId: (id?: string | null) => {

View File

@ -2,10 +2,10 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { IFlowTemplate } from '@/interfaces/database/flow'; import { IFlowTemplate } from '@/interfaces/database/flow';
import i18n from '@/locales/config';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface IProps { interface IProps {
data: IFlowTemplate; data: IFlowTemplate;
isCreate?: boolean; isCreate?: boolean;
@ -18,6 +18,11 @@ export function TemplateCard({ data, showModal, isCreate = false }: IProps) {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
showModal(data); showModal(data);
}, [data, showModal]); }, [data, showModal]);
const language = useMemo(() => {
return i18n.language || 'en';
}, []) as 'en' | 'zh';
return ( return (
<Card className="border-colors-outline-neutral-standard group relative min-h-40"> <Card className="border-colors-outline-neutral-standard group relative min-h-40">
<CardContent className="p-4 "> <CardContent className="p-4 ">
@ -38,11 +43,13 @@ export function TemplateCard({ data, showModal, isCreate = false }: IProps) {
avatar={ avatar={
data.avatar ? data.avatar : 'https://github.com/shadcn.png' data.avatar ? data.avatar : 'https://github.com/shadcn.png'
} }
name={data?.title || 'CN'} name={data?.title[language] || 'CN'}
></RAGFlowAvatar> ></RAGFlowAvatar>
<div className="text-[18px] font-bold ">{data.title}</div> <div className="text-[18px] font-bold ">
{data?.title[language]}
</div>
</div> </div>
<p className="break-words">{data.description}</p> <p className="break-words">{data?.description[language]}</p>
<div className="group-hover:bg-gradient-to-t from-black/70 from-10% via-black/0 via-50% to-black/0 w-full h-full group-hover:block absolute top-0 left-0 hidden rounded-xl"> <div className="group-hover:bg-gradient-to-t from-black/70 from-10% via-black/0 via-50% to-black/0 w-full h-full group-hover:block absolute top-0 left-0 hidden rounded-xl">
<Button <Button
variant="default" variant="default"