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>
<CircleAlert className="size-4 inline-block" />
</HoverCardTrigger>
<HoverCardContent>
<HoverCardContent className="max-w-3xl">
{renderPopoverContent(chunkIndex)}
</HoverCardContent>
</HoverCard>

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import i18n from '@/locales/config';
import { BeginId } from '@/pages/flow/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react';
@ -7,19 +6,36 @@ const prefix = BeginId + '@';
export class VariableNode extends DecoratorNode<ReactNode> {
__value: string;
__label: string;
key?: NodeKey;
__parentLabel?: string | ReactNode;
__icon?: ReactNode;
static getType(): string {
return 'variable';
}
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);
this.__value = value;
this.__label = label;
this.__parentLabel = parent;
this.__icon = icon;
}
createDOM(): HTMLElement {
@ -35,17 +51,20 @@ export class VariableNode extends DecoratorNode<ReactNode> {
decorate(): 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 = (
<div>
<span>{i18n.t(`flow.begin`)}</span> / {content}
<div className="flex items-center gap-1 text-text-primary ">
<div>{this.__icon}</div>
<div>{this.__parentLabel}</div>
<div className="text-text-disabled mr-1">/</div>
{content}
</div>
);
}
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}
</div>
);
@ -59,8 +78,10 @@ export class VariableNode extends DecoratorNode<ReactNode> {
export function $createVariableNode(
value: string,
label: string,
parentLabel: string | ReactNode,
icon?: ReactNode,
): VariableNode {
return new VariableNode(value, label);
return new VariableNode(value, label, undefined, parentLabel, icon);
}
export function $isVariableNode(

View File

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

View File

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

View File

@ -64,7 +64,12 @@ const OperatorIcon = ({ name, className }: IProps) => {
if (name === Operator.Begin) {
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" />
</div>
);

View File

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

View File

@ -2,10 +2,10 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { IFlowTemplate } from '@/interfaces/database/flow';
import i18n from '@/locales/config';
import { Plus } from 'lucide-react';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface IProps {
data: IFlowTemplate;
isCreate?: boolean;
@ -18,6 +18,11 @@ export function TemplateCard({ data, showModal, isCreate = false }: IProps) {
const handleClick = useCallback(() => {
showModal(data);
}, [data, showModal]);
const language = useMemo(() => {
return i18n.language || 'en';
}, []) as 'en' | 'zh';
return (
<Card className="border-colors-outline-neutral-standard group relative min-h-40">
<CardContent className="p-4 ">
@ -38,11 +43,13 @@ export function TemplateCard({ data, showModal, isCreate = false }: IProps) {
avatar={
data.avatar ? data.avatar : 'https://github.com/shadcn.png'
}
name={data?.title || 'CN'}
name={data?.title[language] || 'CN'}
></RAGFlowAvatar>
<div className="text-[18px] font-bold ">{data.title}</div>
<div className="text-[18px] font-bold ">
{data?.title[language]}
</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">
<Button
variant="default"