Feat: Adjust the style of the canvas node #10703 (#10795)

### What problem does this PR solve?

Feat: Adjust the style of the canvas node #10703


### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-10-27 10:36:36 +08:00
committed by GitHub
parent 50e93d1528
commit 24ab857471
122 changed files with 290 additions and 7156 deletions

View File

@ -10,6 +10,7 @@ import useGraphStore from '../../store';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { NodeHandleId, Operator } from '../../constant';
@ -91,11 +92,14 @@ function InnerButtonEdge({
);
}, [data?.isHovered, isTargetPlaceholder, sourceHandleId, target]);
const activeMarkerEnd =
selected || !isEmpty(showHighlight) ? 'url(#selected-marker)' : markerEnd;
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
markerEnd={activeMarkerEnd}
style={{
...style,
...selectedStyle,

View File

@ -59,7 +59,6 @@ import { CategorizeNode } from './node/categorize-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
@ -84,7 +83,6 @@ export const nodeTypes: NodeTypes = {
relevantNode: RelevantNode,
noteNode: NoteNode,
switchNode: SwitchNode,
generateNode: GenerateNode,
retrievalNode: RetrievalNode,
messageNode: MessageNode,
rewriteNode: RewriteNode,
@ -249,6 +247,19 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
style={{ position: 'absolute', top: 10, left: 0 }}
>
<defs>
<marker
fill="rgb(var(--accent-primary))"
id="selected-marker"
viewBox="0 0 40 40"
refX="8"
refY="5"
markerUnits="strokeWidth"
markerWidth="20"
markerHeight="20"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<marker
fill="var(--text-disabled)"
id="logo"

View File

@ -1,4 +1,3 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { IAgentNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { Handle, NodeProps, Position } from '@xyflow/react';
@ -9,6 +8,7 @@ import { AgentExceptionMethod, NodeHandleId } from '../../constant';
import { AgentFormSchemaType } from '../../form/agent-form';
import useGraphStore from '../../store';
import { hasSubAgent, isBottomSubAgent } from '../../utils';
import { LLMLabelCard } from './card';
import { CommonHandle, LeftEndHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
@ -90,9 +90,7 @@ function InnerAgentNode({
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-bg-card rounded-sm p-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
{(isGotoMethod ||
exceptionMethod === AgentExceptionMethod.Comment) && (
<div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2">

View File

@ -12,6 +12,7 @@ import {
} from '../../constant';
import { BeginQuery } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { LabelCard } from './card';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
@ -43,15 +44,16 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
{Object.entries(inputs).map(([key, val], idx) => {
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
return (
<div
key={idx}
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
>
<Icon className="size-4" />
<label htmlFor="">{key}</label>
<span className={styles.parameterValue}>{val.name}</span>
<LabelCard key={idx} className={cn('flex gap-1.5 items-center')}>
<Icon className="size-3.5" />
<label htmlFor="" className="text-accent-primary text-sm italic">
{key}
</label>
<LabelCard className="py-0.5 truncate flex-1">
{val.name}
</LabelCard>
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
</div>
</LabelCard>
);
})}
</section>

View File

@ -1,3 +1,4 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { Button } from '@/components/ui/button';
import {
Card,
@ -16,7 +17,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { PropsWithChildren } from 'react';
@ -65,6 +65,21 @@ type LabelCardProps = {
export function LabelCard({ children, className }: LabelCardProps) {
return (
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
<div
className={cn(
'bg-bg-card rounded-sm p-1 text-text-secondary text-xs',
className,
)}
>
{children}
</div>
);
}
export function LLMLabelCard({ llmId }: { llmId?: string }) {
return (
<LabelCard>
<LLMLabel value={llmId}></LLMLabel>
</LabelCard>
);
}

View File

@ -1,8 +1,8 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { ICategorizeNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { CommonHandle, LeftEndHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
@ -24,9 +24,7 @@ export function InnerCategorizeNode({
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-bg-card rounded-sm px-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
{positions.map((position) => {
return (
<div key={position.uuid}>

View File

@ -96,7 +96,9 @@ export function InnerNextStepDropdown({
onClick={(e) => e.stopPropagation()}
className="w-[300px] font-semibold"
>
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
<DropdownMenuLabel className="text-xs text-text-primary">
{t('flow.nextStep')}
</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}>
{isPipeline ? (
<PipelineAccordionOperators></PipelineAccordionOperators>

View File

@ -1,8 +1,7 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { IRagNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { get } from 'lodash';
import { LabelCard } from './card';
import { LLMLabelCard } from './card';
import { RagNode } from './index';
export function ExtractorNode({ ...props }: NodeProps<IRagNode>) {
@ -10,9 +9,7 @@ export function ExtractorNode({ ...props }: NodeProps<IRagNode>) {
return (
<RagNode {...props}>
<LabelCard>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</LabelCard>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
</RagNode>
);
}

View File

@ -1,60 +0,0 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { IGenerateNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function InnerGenerateNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IGenerateNode>) {
const { theme } = useTheme();
return (
<section
className={classNames(
styles.logicNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
);
}
export const GenerateNode = memo(InnerGenerateNode);

View File

@ -1,6 +1,6 @@
import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { PropsWithChildren, memo } from 'react';
import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging, showCopyIcon } from '../../utils';
import { CommonHandle, LeftEndHandle } from './handle';
@ -9,12 +9,15 @@ import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
function InnerRagNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IRagNode>) {
children,
}: RagNodeProps) {
return (
<ToolBar
selected={selected}
@ -35,6 +38,7 @@ function InnerRagNode({
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
{children}
</NodeWrapper>
</ToolBar>
);

View File

@ -1,10 +1,10 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { IKeywordNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
@ -50,9 +50,7 @@ export function InnerKeywordNode({
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
</section>
);
}

View File

@ -7,7 +7,7 @@ export function NodeWrapper({ children, className, selected }: IProps) {
return (
<section
className={cn(
'bg-text-title-invert p-2.5 rounded-md w-[200px] text-xs group',
'bg-bg-component p-2.5 rounded-md w-[200px] border border-border-button text-xs group hover:shadow-md',
{ 'border border-accent-primary': selected },
className,
)}

View File

@ -57,7 +57,6 @@ function InnerRetrievalNode({
className="size-6 rounded-lg"
avatar={id}
name={item?.name || (label as string) || 'CN'}
isPerson={true}
/>
<div className={'truncate flex-1'}>{label || item?.name}</div>

View File

@ -1,10 +1,10 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { IRewriteNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
@ -50,9 +50,7 @@ function InnerRewriteNode({
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
</section>
);
}

View File

@ -43,7 +43,7 @@ const ConditionBlock = ({
}, []);
return (
<Card>
<Card className="bg-bg-card border-transparent rounded-md">
<CardContent className="p-0 divide-y divide-background-card">
{items.map((x, idx) => (
<div key={idx}>

View File

@ -3,6 +3,7 @@ import {
TooltipNode,
TooltipTrigger,
} from '@/components/xyflow/tooltip-node';
import { cn } from '@/lib/utils';
import { Position } from '@xyflow/react';
import { Copy, Play, Trash2 } from 'lucide-react';
import {
@ -15,9 +16,19 @@ import { Operator } from '../../constant';
import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
function IconWrapper({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
function IconWrapper({
children,
className,
...props
}: HTMLAttributes<HTMLDivElement>) {
return (
<div className="p-1.5 bg-text-title rounded-sm cursor-pointer" {...props}>
<div
className={cn(
'p-1.5 bg-bg-component border border-border-button rounded-sm cursor-pointer hover:text-text-primary',
className,
)}
{...props}
>
{children}
</div>
);
@ -71,7 +82,7 @@ export function ToolBar({
<TooltipTrigger className="h-full">{children}</TooltipTrigger>
<TooltipContent position={Position.Top}>
<section className="flex gap-2 items-center">
<section className="flex gap-2 items-center text-text-secondary">
{showRun && (
<IconWrapper>
<Play className="size-3.5" data-play />
@ -82,7 +93,10 @@ export function ToolBar({
<Copy className="size-3.5" />
</IconWrapper>
)}
<IconWrapper onClick={deleteNode}>
<IconWrapper
onClick={deleteNode}
className="hover:text-state-error hover:border-state-error"
>
<Trash2 className="size-3.5" />
</IconWrapper>
</section>

View File

@ -15,6 +15,7 @@ import {
useContext,
useMemo,
} from 'react';
import { LabelCard } from '../../canvas/node/card';
import { Operator } from '../../constant';
import { AgentInstanceContext } from '../../context';
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
@ -34,15 +35,9 @@ export function ToolCard({
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
const element = useMemo(() => {
return (
<li
{...props}
className={cn(
'flex bg-bg-card p-1 rounded-sm justify-between',
className,
)}
>
<LabelCard {...props} className={cn('flex justify-between', className)}>
{children}
</li>
</LabelCard>
);
}, [children, className, props]);

View File

@ -8,7 +8,6 @@ import {
} from '@/components/ui/dialog';
import { useSetAgentSetting } from '@/hooks/use-agent-request';
import { IModalProps } from '@/interfaces/common';
import { transformFile2Base64 } from '@/utils/file-util';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -23,11 +22,7 @@ export function SettingDialog({ hideModal }: IModalProps<any>) {
const submit = useCallback(
async (values: SettingFormSchemaType) => {
const avatar = values.avatar;
const code = await setAgentSetting({
...values,
avatar: avatar.length > 0 ? await transformFile2Base64(avatar[0]) : '',
});
const code = await setAgentSetting(values);
if (code === 0) {
hideModal?.();
}
@ -39,7 +34,7 @@ export function SettingDialog({ hideModal }: IModalProps<any>) {
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogTitle>{t('common.edit')}</DialogTitle>
</DialogHeader>
<SettingForm submit={submit}></SettingForm>
<DialogFooter>

View File

@ -1,32 +1,20 @@
import { z } from 'zod';
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadList,
FileUploadTrigger,
} from '@/components/file-upload';
import { AvatarUpload } from '@/components/avatar-upload';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormItem, FormLabel } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Textarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { transformBase64ToFile } from '@/utils/file-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { CloudUpload, X } from 'lucide-react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
const formSchema = z.object({
title: z.string().min(1, {}),
avatar: z.array(z.custom<File>()).optional().nullable(),
avatar: z.string().optional(),
description: z.string().optional().nullable(),
permission: z.string(),
});
@ -55,7 +43,7 @@ export function SettingForm({ submit }: SettingFormProps) {
form.reset({
title: data?.title,
description: data?.description,
avatar: data.avatar ? [transformBase64ToFile(data.avatar)] : [],
avatar: data.avatar,
permission: data?.permission,
});
}, [data, form]);
@ -71,45 +59,7 @@ export function SettingForm({ submit }: SettingFormProps) {
<Input />
</RAGFlowFormItem>
<RAGFlowFormItem name="avatar" label={t('photo')}>
{(field) => (
<FileUpload
value={field.value}
onValueChange={field.onChange}
accept="image/*"
maxFiles={1}
onFileReject={(_, message) => {
form.setError('avatar', {
message,
});
}}
multiple
>
<FileUploadDropzone className="flex-row flex-wrap border-dotted text-center">
<CloudUpload className="size-4" />
Drag and drop or
<FileUploadTrigger asChild>
<Button variant="link" size="sm" className="p-0">
choose files
</Button>
</FileUploadTrigger>
to upload
</FileUploadDropzone>
<FileUploadList>
{field.value?.map((file: File, index: number) => (
<FileUploadItem key={index} value={file}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
<Button variant="ghost" size="icon" className="size-7">
<X />
<span className="sr-only">Delete</span>
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
)}
<AvatarUpload></AvatarUpload>
</RAGFlowFormItem>
<RAGFlowFormItem name="description" label={t('description')}>
<Textarea rows={4} />

View File

@ -78,10 +78,7 @@ const buildComponentDownstreamOrUpstream = (
const removeUselessDataInTheOperator = curry(
(operatorName: string, params: Record<string, unknown>) => {
if (
operatorName === Operator.Generate ||
operatorName === Operator.Categorize
) {
if (operatorName === Operator.Categorize) {
return removeUselessFieldsFromValues(params, '');
}
return params;