mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
Compare commits
4 Commits
25e20d8c20
...
4a857ba9a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a857ba9a3 | ||
|
|
e5d73966b4 | ||
|
|
0903f0fe07 | ||
|
|
9210f5041d |
@ -8,6 +8,7 @@ import React, { useState, useContext } from 'react';
|
||||
import { WorkflowPortRender } from '@flowgram.ai/free-layout-editor';
|
||||
import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeMeta } from '../../typings';
|
||||
import { useNodeRenderContext, usePortClick } from '../../hooks';
|
||||
import { SidebarContext } from '../../context';
|
||||
import { scrollToView } from './utils';
|
||||
@ -25,12 +26,13 @@ export interface NodeWrapperProps {
|
||||
export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
||||
const { children, isScrollToView = false } = props;
|
||||
const nodeRender = useNodeRenderContext();
|
||||
const { selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;
|
||||
const { node, selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const sidebar = useContext(SidebarContext);
|
||||
const form = nodeRender.form;
|
||||
const ctx = useClientContext();
|
||||
const onPortClick = usePortClick();
|
||||
const meta = node.getNodeMeta<FlowNodeMeta>();
|
||||
|
||||
const portsRender = ports.map((p) => (
|
||||
<WorkflowPortRender key={p.id} entity={p} onClick={onPortClick} />
|
||||
@ -66,6 +68,7 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
||||
onBlur={onBlur}
|
||||
data-node-selected={String(selected)}
|
||||
style={{
|
||||
...meta.wrapperStyle,
|
||||
outline: form?.state.invalid ? '1px solid red' : 'none',
|
||||
}}
|
||||
>
|
||||
|
||||
@ -10,7 +10,7 @@ import { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';
|
||||
import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { visibleNodeRegistries } from '../../nodes';
|
||||
import { nodeRegistries } from '../../nodes';
|
||||
|
||||
const NodeWrap = styled.div`
|
||||
width: 100%;
|
||||
@ -77,7 +77,9 @@ export const NodeList: FC<NodeListProps> = (props) => {
|
||||
};
|
||||
return (
|
||||
<NodesWrap style={{ width: 80 * 2 + 20 }}>
|
||||
{visibleNodeRegistries.map((registry) => (
|
||||
{nodeRegistries
|
||||
.filter((register) => register.meta.nodePanelVisible !== false)
|
||||
.map((registry) => (
|
||||
<Node
|
||||
key={registry.type}
|
||||
disabled={!(registry.canAdd?.(context) ?? true)}
|
||||
|
||||
@ -68,8 +68,8 @@ export const SidebarRenderer = () => {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const { sidebarDisable = false } = node.getNodeMeta<FlowNodeMeta>();
|
||||
return !sidebarDisable;
|
||||
const { sidebarDisabled = false } = node.getNodeMeta<FlowNodeMeta>();
|
||||
return !sidebarDisabled;
|
||||
}, [node]);
|
||||
|
||||
if (playground.config.readonly) {
|
||||
|
||||
@ -124,14 +124,18 @@ export function useEditorProps(
|
||||
return true;
|
||||
},
|
||||
canDropToNode: (ctx, params) => {
|
||||
const { dragNodeType, dropNodeType } = params;
|
||||
const { dragNodeType } = params;
|
||||
/**
|
||||
* 开始/结束节点无法拖入 loop or group
|
||||
* The start and end nodes cannot be dragged into loop or group
|
||||
* 开始/结束节点无法更改容器
|
||||
* The start and end nodes cannot change container
|
||||
*/
|
||||
if (
|
||||
(dragNodeType === 'start' || dragNodeType === 'end') &&
|
||||
(dropNodeType === 'loop' || dropNodeType === 'group')
|
||||
[
|
||||
WorkflowNodeType.Start,
|
||||
WorkflowNodeType.End,
|
||||
WorkflowNodeType.BlockStart,
|
||||
WorkflowNodeType.BlockEnd,
|
||||
].includes(dragNodeType as WorkflowNodeType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
type: 'start',
|
||||
meta: {
|
||||
position: {
|
||||
x: 180,
|
||||
x: 186.39660158249967,
|
||||
y: 381.75,
|
||||
},
|
||||
},
|
||||
@ -91,7 +91,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
type: 'end',
|
||||
meta: {
|
||||
position: {
|
||||
x: 2202.9953917050693,
|
||||
x: 2489.2950705293442,
|
||||
y: 381.75,
|
||||
},
|
||||
},
|
||||
@ -125,12 +125,194 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'loop_sGybT',
|
||||
id: 'group_5ci0o',
|
||||
type: 'group',
|
||||
meta: {
|
||||
position: {
|
||||
x: 163.32056949283722,
|
||||
y: -76.50012170998413,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
blocks: [
|
||||
{
|
||||
id: 'llm_8--A3',
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1177.8341013824886,
|
||||
y: 9.249999999999977,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
title: 'LLM_1',
|
||||
inputsValues: {
|
||||
modelName: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'constant',
|
||||
content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'constant',
|
||||
content: 'https://mock-ai-url/api/v3',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelName: {
|
||||
type: 'string',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_vTyMa',
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1621.3675909579388,
|
||||
y: 19.24999999999997,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
title: 'LLM_2',
|
||||
inputsValues: {
|
||||
modelName: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'constant',
|
||||
content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'constant',
|
||||
content: 'https://mock-ai-url/api/v3',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
content: '# LLM Input\nresult:{{llm_8--A3.result}}',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelName: {
|
||||
type: 'string',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'condition_0',
|
||||
targetNodeID: 'llm_8--A3',
|
||||
sourcePortID: 'if_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_8--A3',
|
||||
targetNodeID: 'llm_vTyMa',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_vTyMa',
|
||||
targetNodeID: 'end_0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'loop_ANNyh',
|
||||
type: 'loop',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1373.5714285714287,
|
||||
y: 394.9758064516129,
|
||||
x: 1451.8161064396056,
|
||||
y: 384.9037102954011,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
@ -139,16 +321,6 @@ export const initialData: FlowDocumentJSON = {
|
||||
type: 'ref',
|
||||
content: ['start_0', 'array_obj'],
|
||||
},
|
||||
batchOutputs: {
|
||||
results: {
|
||||
type: 'ref',
|
||||
content: ['llm_6aSyo', 'result'],
|
||||
},
|
||||
indexList: {
|
||||
type: 'ref',
|
||||
content: ['loop_sGybT_locals', 'index'],
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
@ -156,8 +328,8 @@ export const initialData: FlowDocumentJSON = {
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: -196.8663594470046,
|
||||
y: 142.0046082949309,
|
||||
x: -110.10677817900246,
|
||||
y: 182.98973079191808,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
@ -233,8 +405,8 @@ export const initialData: FlowDocumentJSON = {
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: 253.1797235023041,
|
||||
y: 142.00460829493088,
|
||||
x: 332.31739662589257,
|
||||
y: 182.98973079191802,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
@ -305,193 +477,41 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block_start_loop_ANNyh',
|
||||
type: 'block-start',
|
||||
meta: {
|
||||
position: {
|
||||
x: -404.5309529838977,
|
||||
y: 346.08973079191816,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: 'block_end_loop_ANNyh',
|
||||
type: 'block-end',
|
||||
meta: {
|
||||
position: {
|
||||
x: 626.7415714307878,
|
||||
y: 346.08973079191793,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'block_start_loop_ANNyh',
|
||||
targetNodeID: 'llm_6aSyo',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_6aSyo',
|
||||
targetNodeID: 'llm_ZqKlP',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'group_5ci0o',
|
||||
type: 'group',
|
||||
meta: {
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
blocks: [
|
||||
{
|
||||
id: 'llm_8--A3',
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1177.8341013824886,
|
||||
y: 19.25,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
title: 'LLM_1',
|
||||
inputsValues: {
|
||||
modelName: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'constant',
|
||||
content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'constant',
|
||||
content: 'https://mock-ai-url/api/v3',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelName: {
|
||||
type: 'string',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_vTyMa',
|
||||
type: 'llm',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1625.6221198156682,
|
||||
y: 19.25,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
title: 'LLM_2',
|
||||
inputsValues: {
|
||||
modelName: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'constant',
|
||||
content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'constant',
|
||||
content: 'https://mock-ai-url/api/v3',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelName: {
|
||||
type: 'string',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'condition_0',
|
||||
targetNodeID: 'llm_8--A3',
|
||||
sourcePortID: 'if_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_8--A3',
|
||||
targetNodeID: 'llm_vTyMa',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_vTyMa',
|
||||
targetNodeID: 'end_0',
|
||||
sourceNodeID: 'llm_ZqKlP',
|
||||
targetNodeID: 'block_end_loop_ANNyh',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -508,7 +528,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'condition_0',
|
||||
targetNodeID: 'loop_sGybT',
|
||||
targetNodeID: 'loop_ANNyh',
|
||||
sourcePortID: 'if_f0rOAt',
|
||||
},
|
||||
{
|
||||
@ -516,7 +536,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
targetNodeID: 'end_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'loop_sGybT',
|
||||
sourceNodeID: 'loop_ANNyh',
|
||||
targetNodeID: 'end_0',
|
||||
},
|
||||
],
|
||||
|
||||
49
apps/demo-free-layout/src/nodes/block-end/form-meta.tsx
Normal file
49
apps/demo-free-layout/src/nodes/block-end/form-meta.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import iconEnd from '../../assets/icon-end.jpg';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
cursor: 'move',
|
||||
}}
|
||||
alt="Icon"
|
||||
src={iconEnd}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
},
|
||||
};
|
||||
45
apps/demo-free-layout/src/nodes/block-end/index.ts
Normal file
45
apps/demo-free-layout/src/nodes/block-end/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const BlockEndNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.BlockEnd,
|
||||
meta: {
|
||||
isNodeEnd: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
size: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
cursor: 'move',
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: 'The final node of the block.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* Start Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
49
apps/demo-free-layout/src/nodes/block-start/form-meta.tsx
Normal file
49
apps/demo-free-layout/src/nodes/block-start/form-meta.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
cursor: 'move',
|
||||
}}
|
||||
alt="Icon"
|
||||
src={iconStart}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
},
|
||||
};
|
||||
45
apps/demo-free-layout/src/nodes/block-start/index.ts
Normal file
45
apps/demo-free-layout/src/nodes/block-start/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const BlockStartNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.BlockStart,
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
cursor: 'move',
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: 'The starting node of the block.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* Start Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@ -9,7 +9,8 @@ import { FlowNodeRegistry } from '../../typings';
|
||||
export const CommentNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Comment,
|
||||
meta: {
|
||||
sidebarDisable: true,
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [],
|
||||
renderKey: WorkflowNodeType.Comment,
|
||||
size: {
|
||||
|
||||
@ -9,5 +9,7 @@ export enum WorkflowNodeType {
|
||||
LLM = 'llm',
|
||||
Condition = 'condition',
|
||||
Loop = 'loop',
|
||||
BlockStart = 'block-start',
|
||||
BlockEnd = 'block-end',
|
||||
Comment = 'comment',
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export const EndNodeRegistry: FlowNodeRegistry = {
|
||||
meta: {
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
size: {
|
||||
width: 360,
|
||||
|
||||
@ -8,9 +8,10 @@ import { StartNodeRegistry } from './start';
|
||||
import { LoopNodeRegistry } from './loop';
|
||||
import { LLMNodeRegistry } from './llm';
|
||||
import { EndNodeRegistry } from './end';
|
||||
import { WorkflowNodeType } from './constants';
|
||||
import { ConditionNodeRegistry } from './condition';
|
||||
import { CommentNodeRegistry } from './comment';
|
||||
import { BlockStartNodeRegistry } from './block-start';
|
||||
import { BlockEndNodeRegistry } from './block-end';
|
||||
export { WorkflowNodeType } from './constants';
|
||||
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
@ -20,8 +21,6 @@ export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
LLMNodeRegistry,
|
||||
LoopNodeRegistry,
|
||||
CommentNodeRegistry,
|
||||
BlockStartNodeRegistry,
|
||||
BlockEndNodeRegistry,
|
||||
];
|
||||
|
||||
export const visibleNodeRegistries = nodeRegistries.filter(
|
||||
(r) => r.type !== WorkflowNodeType.Comment
|
||||
);
|
||||
|
||||
@ -20,7 +20,7 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
meta: {
|
||||
size: {
|
||||
width: 360,
|
||||
height: 300,
|
||||
height: 390,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { delay, WorkflowDocument, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const createBuiltInNodes = async (node: WorkflowNodeEntity) => {
|
||||
// wait for node render - 等待节点渲染
|
||||
await delay(16);
|
||||
if (node.blocks.length) {
|
||||
return;
|
||||
}
|
||||
const document = node.document as WorkflowDocument;
|
||||
document.createWorkflowNode(
|
||||
{
|
||||
id: `block_start_${node.id}`,
|
||||
type: WorkflowNodeType.BlockStart,
|
||||
meta: {
|
||||
position: {
|
||||
x: -80,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
false,
|
||||
node.id
|
||||
);
|
||||
document.createWorkflowNode(
|
||||
{
|
||||
id: `block_end_${node.id}`,
|
||||
type: WorkflowNodeType.BlockEnd,
|
||||
meta: {
|
||||
position: {
|
||||
x: 80,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
false,
|
||||
node.id
|
||||
);
|
||||
};
|
||||
@ -16,6 +16,7 @@ import { FlowNodeRegistry } from '../../typings';
|
||||
import iconLoop from '../../assets/icon-loop.jpg';
|
||||
import { LoopFormRender } from './loop-form-render';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { createBuiltInNodes } from './create-built-in-nodes';
|
||||
|
||||
let index = 0;
|
||||
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
@ -62,16 +63,22 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||
},
|
||||
expandable: false, // disable expanded
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `loop_${nanoid(5)}`,
|
||||
type: 'loop',
|
||||
type: WorkflowNodeType.Loop,
|
||||
data: {
|
||||
title: `Loop_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
onCreate(node, json) {
|
||||
createBuiltInNodes(node);
|
||||
},
|
||||
formMeta: {
|
||||
...defaultFormMeta,
|
||||
render: LoopFormRender,
|
||||
|
||||
@ -14,6 +14,7 @@ export const StartNodeRegistry: FlowNodeRegistry = {
|
||||
isStart: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: {
|
||||
width: 360,
|
||||
|
||||
@ -49,7 +49,9 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
||||
* 你可以自定义节点的meta
|
||||
*/
|
||||
export interface FlowNodeMeta extends WorkflowNodeMeta {
|
||||
sidebarDisable?: boolean;
|
||||
sidebarDisabled?: boolean;
|
||||
nodePanelHidden?: boolean;
|
||||
wrapperStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -12,7 +12,7 @@ export function MaterialDisplay(props: any) {
|
||||
<br />
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
'By Import': `import { ${props.exportName} } from '@flowgram.ai/materials'`,
|
||||
'By Import': `import { ${props.exportName} } from '@flowgram.ai/form-materials'`,
|
||||
// components/type-selector/index.tsx -> components/type-selector
|
||||
'By CLI': `npx @flowgram.ai/form-materials@latest ${props.filePath
|
||||
.split('/')
|
||||
|
||||
@ -106,6 +106,24 @@ After the CLI runs successfully, the relevant materials will be automatically ad
|
||||
ConditionRow is used for configuring a **single row** of condition judgment.
|
||||
</MaterialDisplay>
|
||||
|
||||
### PromptEditorWithVariables
|
||||
|
||||
<MaterialDisplay
|
||||
imgs={[{ src: '/materials/prompt-editor-with-variables.png', caption: 'LLM_3 and LLM_4 use variables from batch item of Loop' }]}
|
||||
filePath="components/prompt-editor-with-variables/index.tsx"
|
||||
exportName="PromptEditorWithVariables"
|
||||
>
|
||||
PromptEditorWithVariables is a Prompt editor that supports variable configuration.
|
||||
|
||||
Below is a configuration example for the Prompt editor, where the `query` variable is of string type and the `enable` variable is of boolean type:
|
||||
```typescript
|
||||
{
|
||||
type: "template",
|
||||
content: "#User Input:\nquery:{{start_0.query}}\nenable:{{start_0.enable}}"
|
||||
}
|
||||
```
|
||||
</MaterialDisplay>
|
||||
|
||||
## Currently Supported Effect Materials
|
||||
|
||||
### provideBatchInput
|
||||
|
||||
BIN
apps/docs/src/public/materials/prompt-editor-with-variables.png
Normal file
BIN
apps/docs/src/public/materials/prompt-editor-with-variables.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 411 KiB |
@ -108,7 +108,23 @@ CLI 运行成功后,相关物料会自动添加到当前项目下的 `src/form
|
||||
ConditionRow 用于 **一行** 条件判断的配置
|
||||
</MaterialDisplay>
|
||||
|
||||
### PromptEditorWithVariables
|
||||
|
||||
<MaterialDisplay
|
||||
imgs={[{ src: '/materials/prompt-editor-with-variables.png', caption: 'LLM_3 和 LLM_4 的提示词中引用了循环的批处理变量' }]}
|
||||
filePath="components/prompt-editor-with-variables/index.tsx"
|
||||
exportName="PromptEditorWithVariables"
|
||||
>
|
||||
PromptEditorWithVariables 用于支持变量配置的 Prompt 编辑器。
|
||||
|
||||
下面是一个 Prompt 编辑器的配置示例,其中 `query` 变量为字符串类型,`enable` 变量为布尔类型:
|
||||
```typescript
|
||||
{
|
||||
type: "template",
|
||||
content: "#User Input:\nquery:{{start_0.query}}\nenable:{{start_0.enable}}"
|
||||
}
|
||||
```
|
||||
</MaterialDisplay>
|
||||
|
||||
## 当前支持的 Effect 物料
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ const ignoreFile = fs.readFileSync(path.join(__dirname, "../../../.gitignore"),
|
||||
encoding: "utf-8",
|
||||
});
|
||||
ig.add(ignoreFile);
|
||||
// ignore cli install demos
|
||||
ig.add(['.next', 'doc_build']);
|
||||
|
||||
const src = path.resolve(__dirname, '../../../');
|
||||
|
||||
|
||||
@ -11,5 +11,5 @@
|
||||
if [ "$PRE_LINT" != "1" ]; then
|
||||
node common/scripts/install-run-rush.js -q lint-staged || exit $?
|
||||
node common/scripts/install-run-rush.js check
|
||||
node common/autoinstallers/license-header/index.js
|
||||
node common/scripts/install-run-rush.js license-header
|
||||
fi
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
"Sandpack",
|
||||
"testrun",
|
||||
"zoomin",
|
||||
"zoomout"
|
||||
"zoomout",
|
||||
"Bytedance"
|
||||
],
|
||||
"ignoreWords": [],
|
||||
"import": []
|
||||
|
||||
@ -67,7 +67,12 @@ export function ConditionRow({ style, value, onChange, readonly }: PropTypes) {
|
||||
onChange={(v) => onChange({ ...value, right: v })}
|
||||
/>
|
||||
) : (
|
||||
<UIInput size="small" disabled value={opConfig?.rightDisplay || 'Empty'} />
|
||||
<UIInput
|
||||
size="small"
|
||||
disabled
|
||||
style={{ pointerEvents: 'none' }}
|
||||
value={opConfig?.rightDisplay || 'Empty'}
|
||||
/>
|
||||
)}
|
||||
</UIRight>
|
||||
</UIValues>
|
||||
|
||||
@ -65,7 +65,12 @@ export function ConditionRow({ style, value, onChange, readonly }: PropTypes) {
|
||||
onChange={(v) => onChange({ ...value, right: v })}
|
||||
/>
|
||||
) : (
|
||||
<Input size="small" disabled value={opConfig?.rightDisplay || 'Empty'} />
|
||||
<Input
|
||||
size="small"
|
||||
disabled
|
||||
style={{ pointerEvents: 'none' }}
|
||||
value={opConfig?.rightDisplay || 'Empty'}
|
||||
/>
|
||||
)}
|
||||
</UIRight>
|
||||
</UIValues>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "prompt-editor",
|
||||
"depMaterials": [],
|
||||
"depMaterials": [
|
||||
"variable-selector"
|
||||
],
|
||||
"depPackages": [
|
||||
"@coze-editor/editor@0.1.0-alpha.8d7a30",
|
||||
"@codemirror/view",
|
||||
|
||||
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { isEqual, last } from 'lodash';
|
||||
import {
|
||||
BaseVariableField,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
Scope,
|
||||
useCurrentScope,
|
||||
} from '@flowgram.ai/editor';
|
||||
import { Popover } from '@douyinfe/semi-ui';
|
||||
import { IconIssueStroked } from '@douyinfe/semi-icons';
|
||||
import { useInjector } from '@coze-editor/editor/react';
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
MatchDecorator,
|
||||
ViewPlugin,
|
||||
WidgetType,
|
||||
} from '@codemirror/view';
|
||||
|
||||
import { UIPopoverContent, UIRootTitle, UITag, UIVarName } from '../styles';
|
||||
|
||||
class VariableTagWidget extends WidgetType {
|
||||
keyPath?: string[];
|
||||
|
||||
toDispose = new DisposableCollection();
|
||||
|
||||
scope: Scope;
|
||||
|
||||
root: Root;
|
||||
|
||||
constructor({ keyPath, scope }: { keyPath?: string[]; scope: Scope }) {
|
||||
super();
|
||||
|
||||
this.keyPath = keyPath;
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
renderIcon = (icon: string | JSX.Element) => {
|
||||
if (typeof icon === 'string') {
|
||||
return <img style={{ marginRight: 8 }} width={12} height={12} src={icon} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
renderVariable(v?: BaseVariableField) {
|
||||
if (!v) {
|
||||
this.root.render(
|
||||
<UITag prefixIcon={<IconIssueStroked />} color="amber">
|
||||
Unknown
|
||||
</UITag>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootField = last(v.parentFields);
|
||||
|
||||
const rootTitle = (
|
||||
<UIRootTitle>{rootField?.meta.title ? `${rootField.meta.title} -` : ''}</UIRootTitle>
|
||||
);
|
||||
const rootIcon = this.renderIcon(rootField?.meta.icon);
|
||||
|
||||
this.root.render(
|
||||
<Popover
|
||||
content={
|
||||
<UIPopoverContent>
|
||||
{rootIcon}
|
||||
{rootTitle}
|
||||
<UIVarName>{v?.keyPath.slice(1).join('.')}</UIVarName>
|
||||
</UIPopoverContent>
|
||||
}
|
||||
>
|
||||
<UITag prefixIcon={rootIcon}>
|
||||
{rootTitle}
|
||||
<UIVarName>{v?.key}</UIVarName>
|
||||
</UITag>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
const dom = document.createElement('span');
|
||||
|
||||
this.root = createRoot(dom);
|
||||
|
||||
this.toDispose.push(
|
||||
Disposable.create(() => {
|
||||
this.root.unmount();
|
||||
})
|
||||
);
|
||||
|
||||
this.toDispose.push(
|
||||
this.scope.available.trackByKeyPath(
|
||||
this.keyPath,
|
||||
(v) => {
|
||||
this.renderVariable(v);
|
||||
},
|
||||
{ triggerOnInit: false }
|
||||
)
|
||||
);
|
||||
|
||||
this.renderVariable(this.scope.available.getByKeyPath(this.keyPath));
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
eq(other: VariableTagWidget) {
|
||||
return isEqual(this.keyPath, other.keyPath);
|
||||
}
|
||||
|
||||
ignoreEvent(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function VariableTagInject() {
|
||||
const injector = useInjector();
|
||||
|
||||
const scope = useCurrentScope();
|
||||
|
||||
// 基于 {{var}} 的正则进行匹配,匹配后进行自定义渲染
|
||||
useLayoutEffect(() => {
|
||||
const atMatcher = new MatchDecorator({
|
||||
regexp: /\{\{([^\}]+)\}\}/g,
|
||||
decoration: (match) =>
|
||||
Decoration.replace({
|
||||
widget: new VariableTagWidget({
|
||||
keyPath: match[1]?.split('.') ?? [],
|
||||
scope,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
return injector.inject([
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.decorations = atMatcher.createDeco(view);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.decorations = atMatcher.createDeco(this.view);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (p) => p.decorations,
|
||||
provide(p) {
|
||||
return EditorView.atomicRanges.of(
|
||||
(view) => view.plugin(p)?.decorations ?? Decoration.none
|
||||
);
|
||||
},
|
||||
}
|
||||
),
|
||||
]);
|
||||
}, [injector]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -17,7 +17,7 @@ import { EditorAPI } from '@coze-editor/editor/preset-prompt';
|
||||
|
||||
import { useVariableTree } from '../../variable-selector';
|
||||
|
||||
function Variable() {
|
||||
export function VariableTree() {
|
||||
const [posKey, setPosKey] = useState('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState(-1);
|
||||
@ -53,7 +53,7 @@ function Variable() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Mention triggerCharacters={['{', '{}']} onOpenChange={handleOpenChange} />
|
||||
<Mention triggerCharacters={['{', '{}', '@']} onOpenChange={handleOpenChange} />
|
||||
|
||||
<Popover
|
||||
visible={visible}
|
||||
@ -81,5 +81,3 @@ function Variable() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Variable;
|
||||
@ -5,13 +5,15 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Variable from './extensions/variable';
|
||||
import { VariableTree } from './extensions/variable-tree';
|
||||
import { VariableTagInject } from './extensions/variable-tag';
|
||||
import { PromptEditor, PromptEditorPropsType } from '../prompt-editor';
|
||||
|
||||
export function PromptEditorWithVariables(props: PromptEditorPropsType) {
|
||||
return (
|
||||
<PromptEditor {...props}>
|
||||
<Variable />
|
||||
<VariableTree />
|
||||
<VariableTagInject />
|
||||
</PromptEditor>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { Tag } from '@douyinfe/semi-ui';
|
||||
|
||||
export const UIRootTitle = styled.div`
|
||||
margin-right: 4px;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--semi-color-text-2);
|
||||
`;
|
||||
|
||||
export const UIVarName = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const UITag = styled(Tag)`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
max-width: 300px;
|
||||
|
||||
& .semi-tag-content-center {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.semi-tag {
|
||||
margin: 0 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const UIPopoverContent = styled.div`
|
||||
padding: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
@ -88,17 +88,16 @@ export class NodeIntoContainerService {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const parentTransform = parentNode.getData<TransformData>(TransformData);
|
||||
this.operationService.moveNode(node, {
|
||||
parent: containerNode,
|
||||
});
|
||||
const parentTransform = parentNode.getData<TransformData>(TransformData);
|
||||
await this.nextFrame();
|
||||
parentTransform.fireChange();
|
||||
this.operationService.updateNodePosition(node, {
|
||||
x: parentTransform.position.x + nodeJSON.meta!.position!.x,
|
||||
y: parentTransform.position.y + nodeJSON.meta!.position!.y,
|
||||
});
|
||||
parentTransform.fireChange();
|
||||
await this.nextFrame();
|
||||
parentTransform.fireChange();
|
||||
this.emitter.fire({
|
||||
type: NodeIntoContainerType.Out,
|
||||
node,
|
||||
|
||||
@ -41,7 +41,7 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
||||
}
|
||||
|
||||
get keyPath(): string[] {
|
||||
return this.parentFields.reverse().map((_field) => _field.key);
|
||||
return [...this.parentFields.reverse().map((_field) => _field.key), this.key];
|
||||
}
|
||||
|
||||
get meta(): VariableMeta {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
Observable,
|
||||
Subject,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
map,
|
||||
merge,
|
||||
share,
|
||||
@ -179,8 +180,12 @@ export class ScopeAvailableData {
|
||||
merge(this.anyVariableChange$, this.variables$)
|
||||
.pipe(
|
||||
triggerOnInit ? startWith() : tap(() => null),
|
||||
map(() => this.getByKeyPath(keyPath)),
|
||||
distinctUntilChanged((_prevNode, _node) => _prevNode?.hash !== _node?.hash)
|
||||
map(() => {
|
||||
const v = this.getByKeyPath(keyPath);
|
||||
return { v, hash: v?.hash };
|
||||
}),
|
||||
distinctUntilKeyChanged('hash'),
|
||||
map(({ v }) => v)
|
||||
)
|
||||
.subscribe(cb)
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user