mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
Compare commits
6 Commits
867ceb8bc2
...
25e20d8c20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e20d8c20 | ||
|
|
de7f2d3c07 | ||
|
|
800a820e10 | ||
|
|
0a9c3a0167 | ||
|
|
522bc0770d | ||
|
|
d8e2b4a838 |
@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||
import { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
|
||||
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FormItem } from '../form-item';
|
||||
@ -13,6 +13,7 @@ import { useNodeRenderContext } from '../../hooks';
|
||||
|
||||
export function FormInputs() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
return (
|
||||
<Field<JsonSchema> name="inputs">
|
||||
{({ field: inputsField }) => {
|
||||
@ -23,21 +24,39 @@ export function FormInputs() {
|
||||
}
|
||||
const content = Object.keys(properties).map((key) => {
|
||||
const property = properties[key];
|
||||
|
||||
const formComponent = property.extra?.formComponent;
|
||||
|
||||
const vertical = ['prompt-editor'].includes(formComponent || '');
|
||||
|
||||
return (
|
||||
<Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem
|
||||
name={key}
|
||||
vertical={vertical}
|
||||
type={property.type as string}
|
||||
required={required.includes(key)}
|
||||
>
|
||||
<DynamicValueInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
schema={property}
|
||||
/>
|
||||
{formComponent === 'prompt-editor' && (
|
||||
<PromptEditorWithVariables
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
)}
|
||||
{!formComponent && (
|
||||
<DynamicValueInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
constantProps={{
|
||||
schema: property,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -19,6 +19,7 @@ interface FormItemProps {
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
labelWidth?: number;
|
||||
vertical?: boolean;
|
||||
}
|
||||
export function FormItem({
|
||||
children,
|
||||
@ -27,6 +28,7 @@ export function FormItem({
|
||||
description,
|
||||
type,
|
||||
labelWidth,
|
||||
vertical,
|
||||
}: FormItemProps): JSX.Element {
|
||||
const renderTitle = useCallback(
|
||||
(showTooltip?: boolean) => (
|
||||
@ -47,9 +49,13 @@ export function FormItem({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
...(vertical
|
||||
? { flexDirection: 'column' }
|
||||
: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -59,7 +59,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -78,9 +78,11 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -35,7 +35,7 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -54,9 +54,11 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||
import { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FormItem } from '../form-item';
|
||||
import { Feedback } from '../feedback';
|
||||
@ -13,6 +13,7 @@ import { useNodeRenderContext } from '../../hooks';
|
||||
|
||||
export function FormInputs() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
return (
|
||||
<Field<JsonSchema> name="inputs">
|
||||
{({ field: inputsField }) => {
|
||||
@ -23,23 +24,39 @@ export function FormInputs() {
|
||||
}
|
||||
const content = Object.keys(properties).map((key) => {
|
||||
const property = properties[key];
|
||||
|
||||
const formComponent = property.extra?.formComponent;
|
||||
|
||||
const vertical = ['prompt-editor'].includes(formComponent || '');
|
||||
|
||||
return (
|
||||
<Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem
|
||||
name={key}
|
||||
vertical={vertical}
|
||||
type={property.type as string}
|
||||
required={required.includes(key)}
|
||||
>
|
||||
<DynamicValueInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
constantProps={{
|
||||
schema: property,
|
||||
}}
|
||||
/>
|
||||
{formComponent === 'prompt-editor' && (
|
||||
<PromptEditorWithVariables
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
)}
|
||||
{!formComponent && (
|
||||
<DynamicValueInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
constantProps={{
|
||||
schema: property,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -181,7 +181,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -206,9 +206,15 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -252,7 +258,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -277,9 +283,15 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -342,7 +354,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -367,9 +379,15 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -413,7 +431,7 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -438,9 +456,15 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -48,7 +48,7 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'constant',
|
||||
content: 'You are an AI assistant.',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'constant',
|
||||
@ -73,9 +73,15 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -18,7 +18,7 @@ import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
import { onDragLineEnd } from '@editor/utils';
|
||||
import { FlowNodeRegistry } from '@editor/typings';
|
||||
import { createContextMenuPlugin, createSyncVariablePlugin } from '@editor/plugins';
|
||||
import { createContextMenuPlugin } from '@editor/plugins';
|
||||
import { defaultFormMeta } from '@editor/nodes/default-form-meta';
|
||||
import { WorkflowNodeType } from '@editor/nodes';
|
||||
import { BaseNode } from '@editor/components/base-node';
|
||||
@ -156,11 +156,6 @@ export const useEditorProps = (initialData: WorkflowJSON, nodeRegistries: FlowNo
|
||||
},
|
||||
inactiveDebounceTime: 1,
|
||||
}),
|
||||
/**
|
||||
* Variable plugin
|
||||
* 变量插件
|
||||
*/
|
||||
createSyncVariablePlugin({}),
|
||||
/**
|
||||
* Snap plugin
|
||||
* 自动对齐及辅助线插件
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { FormMeta, FormRenderProps, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import { autoRenameRefEffect } from '@flowgram.ai/form-antd-materials';
|
||||
import { autoRenameRefEffect, syncVariableTitle, provideJsonSchemaOutputs } from '@flowgram.ai/form-antd-materials';
|
||||
|
||||
import { FormContent, FormHeader, FormInputs, FormOutputs } from '@editor/form-components';
|
||||
import { FlowNodeJSON } from '../typings';
|
||||
@ -36,7 +36,9 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
effect: {
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
inputsValues: autoRenameRefEffect,
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
FormRenderProps,
|
||||
ValidateTrigger,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { JsonSchemaEditor } from '@flowgram.ai/form-antd-materials';
|
||||
import { JsonSchemaEditor, syncVariableTitle, provideJsonSchemaOutputs } from '@flowgram.ai/form-antd-materials';
|
||||
|
||||
import { FlowNodeJSON, JsonSchema } from '@editor/typings';
|
||||
import { useIsSidebar } from '@editor/hooks';
|
||||
@ -57,4 +57,8 @@ export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,5 +3,4 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { createSyncVariablePlugin } from './sync-variable-plugin/sync-variable-plugin';
|
||||
export { createContextMenuPlugin } from './context-menu-plugin';
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ASTFactory,
|
||||
FlowNodeVariableData,
|
||||
FreeLayoutPluginContext,
|
||||
PluginCreator,
|
||||
definePluginCreator,
|
||||
getNodeForm,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { JsonSchemaUtils } from '@flowgram.ai/form-antd-materials';
|
||||
|
||||
export interface SyncVariablePluginOptions {}
|
||||
|
||||
/**
|
||||
* Creates a plugin to synchronize output data to the variable engine when nodes are created or updated.
|
||||
* @param ctx - The plugin context, containing the document and other relevant information.
|
||||
* @param options - Plugin options, currently an empty object.
|
||||
*/
|
||||
export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
|
||||
definePluginCreator<SyncVariablePluginOptions, FreeLayoutPluginContext>({
|
||||
onInit(ctx, options) {
|
||||
const flowDocument = ctx.document;
|
||||
|
||||
// Listen for node creation events
|
||||
flowDocument.onNodeCreate(({ node }) => {
|
||||
const form = getNodeForm(node);
|
||||
const variableData = node.getData(FlowNodeVariableData);
|
||||
|
||||
/**
|
||||
* Synchronizes output data to the variable engine.
|
||||
* @param value - The output data to synchronize.
|
||||
*/
|
||||
const syncOutputs = (value: any) => {
|
||||
if (!value) {
|
||||
// If the output data is empty, clear the variable
|
||||
variableData?.clearVar();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an Type AST from the output data's JSON schema
|
||||
// NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
|
||||
const typeAST = JsonSchemaUtils.schemaToAST(value);
|
||||
|
||||
if (typeAST) {
|
||||
// Use the node's title or its ID as the title for the variable
|
||||
const title = form?.getValueIn('title') || node.id;
|
||||
|
||||
// Set the variable in the variable engine
|
||||
variableData?.setVar(
|
||||
ASTFactory.createVariableDeclaration({
|
||||
meta: {
|
||||
title: `${title}`,
|
||||
icon: node.getNodeRegistry()?.info?.icon,
|
||||
// NOTICE: You can add more metadata here as needed
|
||||
},
|
||||
key: `${node.id}`,
|
||||
type: typeAST,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// If the AST cannot be created, clear the variable
|
||||
variableData?.clearVar();
|
||||
}
|
||||
};
|
||||
|
||||
if (form) {
|
||||
// Initially synchronize the output data
|
||||
syncOutputs(form.getValueIn('outputs'));
|
||||
|
||||
// Listen for changes in the form values and re-synchronize when outputs change
|
||||
form.onFormValuesChange((props) => {
|
||||
if (props.name.match(/^outputs/) || props.name.match(/^title/)) {
|
||||
syncOutputs(form.getValueIn('outputs'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
1085
common/config/rush/pnpm-lock.yaml
generated
1085
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -458,8 +458,14 @@ export class TransformData extends EntityData<TransformSchema> implements Transf
|
||||
|
||||
private _parentChangedDispose?: DisposableCollection;
|
||||
|
||||
private entityDispose?: Disposable;
|
||||
|
||||
setParent(parent: TransformData | undefined, listenParentData = true): void {
|
||||
if (this._parent !== parent) {
|
||||
if (this.entityDispose) {
|
||||
this.entityDispose.dispose();
|
||||
this.entityDispose = undefined;
|
||||
}
|
||||
if (this._parentChangedDispose) {
|
||||
this._parentChangedDispose.dispose();
|
||||
this._parentChangedDispose = undefined;
|
||||
@ -470,6 +476,9 @@ export class TransformData extends EntityData<TransformSchema> implements Transf
|
||||
parent._children.push(this);
|
||||
this._parentChangedDispose = new DisposableCollection();
|
||||
this.toDispose.push(this._parentChangedDispose);
|
||||
this.entityDispose = this.entity.onDispose(() => {
|
||||
parent.fireChange();
|
||||
});
|
||||
this._parentChangedDispose.pushAll([
|
||||
parent.onDispose(() => {
|
||||
this.setParent(undefined);
|
||||
@ -478,7 +487,6 @@ export class TransformData extends EntityData<TransformSchema> implements Transf
|
||||
const index = parent._children!.indexOf(this);
|
||||
if (index !== -1) {
|
||||
parent._children!.splice(index, 1);
|
||||
parent.fireChange();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -66,6 +66,8 @@ export class WorkflowLineEntity extends Entity<WorkflowLineEntityOpts> {
|
||||
|
||||
private _hasError = false;
|
||||
|
||||
public stackIndex = 0;
|
||||
|
||||
/**
|
||||
* 线条数据
|
||||
*/
|
||||
|
||||
@ -331,8 +331,7 @@ export class WorkflowDragService {
|
||||
public adjustSubNodePosition(
|
||||
subNodeType?: string,
|
||||
containerNode?: WorkflowNodeEntity,
|
||||
mousePos?: IPoint,
|
||||
resetEmptyPos: boolean = true
|
||||
mousePos?: IPoint
|
||||
): IPoint {
|
||||
if (!mousePos) {
|
||||
return { x: 0, y: 0 };
|
||||
@ -342,8 +341,8 @@ export class WorkflowDragService {
|
||||
}
|
||||
const isParentEmpty = !containerNode.children || containerNode.children.length === 0;
|
||||
const parentPadding = this.document.layout.getPadding(containerNode);
|
||||
const parentTransform = containerNode.getData<TransformData>(TransformData);
|
||||
if (isParentEmpty && resetEmptyPos) {
|
||||
const containerWorldTransform = containerNode.transform.transform.worldTransform;
|
||||
if (isParentEmpty) {
|
||||
// 确保空容器节点不偏移
|
||||
return {
|
||||
x: 0,
|
||||
@ -351,8 +350,8 @@ export class WorkflowDragService {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
x: mousePos.x - parentTransform.position.x,
|
||||
y: mousePos.y - parentTransform.position.y,
|
||||
x: mousePos.x - containerWorldTransform.tx,
|
||||
y: mousePos.y - containerWorldTransform.ty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { last } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { DisposableCollection, Emitter, type IPoint } from '@flowgram.ai/utils';
|
||||
import { FlowNodeRenderData, FlowNodeTransformData } from '@flowgram.ai/document';
|
||||
import { EntityManager, PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
||||
import { EntityManager, PlaygroundConfigEntity } from '@flowgram.ai/core';
|
||||
|
||||
import { WorkflowDocumentOptions } from './workflow-document-option';
|
||||
import { type WorkflowDocument } from './workflow-document';
|
||||
@ -416,13 +416,8 @@ export class WorkflowLinesManager {
|
||||
.filter((port) => port.node.flowNodeType !== 'root');
|
||||
const targetPort = allPorts.find((port) => port.isHovered(pos.x, pos.y));
|
||||
if (targetPort) {
|
||||
// 后创建的要先校验
|
||||
const targetNode = this.document
|
||||
.getAllNodes()
|
||||
.slice()
|
||||
.reverse()
|
||||
.filter((node) => targetPort.node?.parent?.id !== node.id)
|
||||
.find((node) => node.getData(TransformData)!.contains(pos.x, pos.y));
|
||||
const containNodes = this.getContainNodesFromMousePos(pos);
|
||||
const targetNode = last(containNodes);
|
||||
// 点位可能会被节点覆盖
|
||||
if (targetNode && targetNode !== targetPort.node) {
|
||||
return;
|
||||
@ -436,27 +431,9 @@ export class WorkflowLinesManager {
|
||||
* @param pos - 鼠标位置
|
||||
*/
|
||||
getNodeFromMousePos(pos: IPoint): WorkflowNodeEntity | undefined {
|
||||
const allNodes = this.document
|
||||
.getAllNodes()
|
||||
.sort((a, b) => this.getNodeIndex(a) - this.getNodeIndex(b));
|
||||
// 先挑选出 bounds 区域符合的 node
|
||||
const containNodes: WorkflowNodeEntity[] = [];
|
||||
const { selection } = this.selectService;
|
||||
const zoom =
|
||||
this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)?.config?.zoom ||
|
||||
1;
|
||||
allNodes.forEach((node) => {
|
||||
const { bounds } = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
// 交互要求,节点边缘 4px 的时候就生效连线逻辑
|
||||
if (
|
||||
bounds
|
||||
.clone()
|
||||
.pad(4 / zoom)
|
||||
.contains(pos.x, pos.y)
|
||||
) {
|
||||
containNodes.push(node);
|
||||
}
|
||||
});
|
||||
const containNodes = this.getContainNodesFromMousePos(pos);
|
||||
// 当有元素被选中的时候选中元素在顶层
|
||||
if (selection?.length) {
|
||||
const filteredNodes = containNodes.filter((node) =>
|
||||
@ -479,6 +456,31 @@ export class WorkflowLinesManager {
|
||||
line.addData(WorkflowLineRenderData);
|
||||
}
|
||||
|
||||
/** 获取鼠标坐标位置的所有节点(stackIndex 从小到大排序) */
|
||||
private getContainNodesFromMousePos(pos: IPoint): WorkflowNodeEntity[] {
|
||||
const allNodes = this.document
|
||||
.getAllNodes()
|
||||
.sort((a, b) => this.getNodeIndex(a) - this.getNodeIndex(b));
|
||||
const zoom =
|
||||
this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)?.config?.zoom ||
|
||||
1;
|
||||
const containNodes = allNodes
|
||||
.map((node) => {
|
||||
const { bounds } = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
// 交互要求,节点边缘 4px 的时候就认为选中节点
|
||||
if (
|
||||
bounds
|
||||
.clone()
|
||||
.pad(4 / zoom)
|
||||
.contains(pos.x, pos.y)
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as WorkflowNodeEntity[];
|
||||
return containNodes;
|
||||
}
|
||||
|
||||
private getNodeIndex(node: WorkflowNodeEntity): number {
|
||||
const nodeRenderData = node.getData(FlowNodeRenderData);
|
||||
return nodeRenderData.stackIndex;
|
||||
|
||||
@ -6,3 +6,5 @@
|
||||
export * from './provide-batch-input';
|
||||
export * from './provide-batch-outputs';
|
||||
export * from './auto-rename-ref';
|
||||
export * from './provide-json-schema-outputs';
|
||||
export * from './sync-variable-title';
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
import { IFlowRefValue } from '../../typings';
|
||||
|
||||
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||
private: true,
|
||||
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: `${ctx.node.id}`,
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "provide-json-schema-outputs",
|
||||
"depMaterials": [
|
||||
"typings/json-schema",
|
||||
"utils/json-schema"
|
||||
],
|
||||
"depPackages": []
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
ASTFactory,
|
||||
EffectOptions,
|
||||
FlowNodeRegistry,
|
||||
createEffectFromVariableProvider,
|
||||
getNodeForm,
|
||||
} from '@flowgram.ai/editor';
|
||||
|
||||
import { JsonSchemaUtils } from '../../utils';
|
||||
import { IJsonSchema } from '../../typings';
|
||||
|
||||
export const provideJsonSchemaOutputs: EffectOptions[] = createEffectFromVariableProvider({
|
||||
parse: (value: IJsonSchema, ctx) => [
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: `${ctx.node.id}`,
|
||||
meta: {
|
||||
title: getNodeForm(ctx.node)?.getValueIn('title') || ctx.node.id,
|
||||
icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
|
||||
},
|
||||
type: JsonSchemaUtils.schemaToAST(value),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sync-variable-title",
|
||||
"depMaterials": [],
|
||||
"depPackages": []
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
DataEvent,
|
||||
Effect,
|
||||
EffectOptions,
|
||||
FlowNodeRegistry,
|
||||
FlowNodeVariableData,
|
||||
} from '@flowgram.ai/editor';
|
||||
|
||||
export const syncVariableTitle: EffectOptions[] = [
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: (({ value, context }) => {
|
||||
context.node.getData(FlowNodeVariableData).allScopes.forEach((_scope) => {
|
||||
_scope.output.variables.forEach((_var) => {
|
||||
_var.updateMeta({
|
||||
title: value || context.node.id,
|
||||
icon: context.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
|
||||
});
|
||||
});
|
||||
});
|
||||
}) as Effect,
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "batch-outputs-plugin",
|
||||
"depMaterials": [
|
||||
"flow-value"
|
||||
],
|
||||
"depPackages": []
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
ASTFactory,
|
||||
createEffectFromVariableProvider,
|
||||
defineFormPluginCreator,
|
||||
FlowNodeRegistry,
|
||||
getNodeForm,
|
||||
getNodePrivateScope,
|
||||
getNodeScope,
|
||||
ScopeChainTransformService,
|
||||
type EffectOptions,
|
||||
type FormPluginCreator,
|
||||
FlowNodeScopeType,
|
||||
} from '@flowgram.ai/editor';
|
||||
|
||||
import { IFlowRefValue } from '../../typings';
|
||||
|
||||
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: `${ctx.node.id}`,
|
||||
meta: {
|
||||
title: getNodeForm(ctx.node)?.getValueIn('title'),
|
||||
icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
|
||||
},
|
||||
type: ASTFactory.createObject({
|
||||
properties: Object.entries(value).map(([_key, value]) =>
|
||||
ASTFactory.createProperty({
|
||||
key: _key,
|
||||
initializer: ASTFactory.createWrapArrayExpression({
|
||||
wrapFor: ASTFactory.createKeyPathExpression({
|
||||
keyPath: value?.content || [],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Free Layout only right now
|
||||
*/
|
||||
export const createBatchOutputsFormPlugin: FormPluginCreator<{ outputKey: string }> =
|
||||
defineFormPluginCreator({
|
||||
name: 'batch-outputs-plugin',
|
||||
onSetupFormMeta({ mergeEffect }, { outputKey }) {
|
||||
mergeEffect({
|
||||
[outputKey]: provideBatchOutputsEffect,
|
||||
});
|
||||
},
|
||||
onInit(ctx, { outputKey }) {
|
||||
const chainTransformService = ctx.node.getService(ScopeChainTransformService);
|
||||
|
||||
const batchNodeType = ctx.node.flowNodeType;
|
||||
|
||||
const transformerId = `${batchNodeType}-outputs`;
|
||||
|
||||
if (chainTransformService.hasTransformer(transformerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chainTransformService.registerTransformer(transformerId, {
|
||||
transformCovers: (covers, ctx) => {
|
||||
const node = ctx.scope.meta?.node;
|
||||
|
||||
// Child Node's variable can cover parent
|
||||
if (node?.parent?.flowNodeType === batchNodeType) {
|
||||
return [...covers, getNodeScope(node.parent)];
|
||||
}
|
||||
|
||||
return covers;
|
||||
},
|
||||
transformDeps(scopes, ctx) {
|
||||
const scopeMeta = ctx.scope.meta;
|
||||
|
||||
if (scopeMeta?.type === FlowNodeScopeType.private) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
const node = scopeMeta?.node;
|
||||
|
||||
// Public of Loop Node depends on child Node
|
||||
if (node?.flowNodeType === batchNodeType) {
|
||||
// Get all child blocks
|
||||
const childBlocks = node.blocks;
|
||||
|
||||
// public scope of all child blocks
|
||||
return [
|
||||
getNodePrivateScope(node),
|
||||
...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),
|
||||
];
|
||||
}
|
||||
|
||||
return scopes;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './sync-variable-plugin';
|
||||
export { createBatchOutputsFormPlugin } from './batch-outputs-plugin';
|
||||
@ -7,3 +7,4 @@ export * from './components';
|
||||
export * from './effects';
|
||||
export * from './utils';
|
||||
export * from './typings';
|
||||
export * from './form-plugins';
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
*/
|
||||
|
||||
export * from './format-legacy-refs';
|
||||
export * from './json-schema';
|
||||
export * from './svg-icon';
|
||||
export * from './json-schema';
|
||||
|
||||
@ -42,7 +42,10 @@ export namespace JsonSchemaUtils {
|
||||
.map(([key, _property]) => ({
|
||||
key,
|
||||
type: schemaToAST(_property),
|
||||
meta: { description: _property.description },
|
||||
meta: {
|
||||
title: _property.title,
|
||||
description: _property.description,
|
||||
},
|
||||
})),
|
||||
});
|
||||
case 'array':
|
||||
@ -114,7 +117,18 @@ export namespace JsonSchemaUtils {
|
||||
type: 'object',
|
||||
properties: drilldown
|
||||
? Object.fromEntries(
|
||||
Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
|
||||
typeAST.properties.map((property) => {
|
||||
const schema = astToSchema(property.type);
|
||||
|
||||
if (property.meta?.title && schema) {
|
||||
schema.title = property.meta.title;
|
||||
}
|
||||
if (property.meta?.description && schema) {
|
||||
schema.description = property.meta.description;
|
||||
}
|
||||
|
||||
return [property.key, schema!];
|
||||
})
|
||||
)
|
||||
: {},
|
||||
};
|
||||
|
||||
@ -41,7 +41,9 @@
|
||||
"commander": "^11.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"inquirer": "^9.2.7",
|
||||
"immer": "~10.1.1"
|
||||
"immer": "~10.1.1",
|
||||
"@coze-editor/editor": "0.1.0-alpha.8d7a30",
|
||||
"@codemirror/view": "~6.38.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flowgram.ai/eslint-config": "workspace:*",
|
||||
|
||||
@ -11,3 +11,5 @@ export * from './constant-input';
|
||||
export * from './dynamic-value-input';
|
||||
export * from './condition-row';
|
||||
export * from './batch-outputs';
|
||||
export * from './prompt-editor';
|
||||
export * from './prompt-editor-with-variables';
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "prompt-editor",
|
||||
"depMaterials": [],
|
||||
"depPackages": [
|
||||
"@coze-editor/editor@0.1.0-alpha.8d7a30",
|
||||
"@codemirror/view",
|
||||
"styled-components",
|
||||
"@douyinfe/semi-ui"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Popover, Tree } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Mention,
|
||||
MentionOpenChangeEvent,
|
||||
getCurrentMentionReplaceRange,
|
||||
useEditor,
|
||||
PositionMirror,
|
||||
} from '@coze-editor/editor/react';
|
||||
import { EditorAPI } from '@coze-editor/editor/preset-prompt';
|
||||
|
||||
import { useVariableTree } from '../../variable-selector';
|
||||
|
||||
function Variable() {
|
||||
const [posKey, setPosKey] = useState('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState(-1);
|
||||
const editor = useEditor<EditorAPI>();
|
||||
|
||||
function insert(variablePath: string) {
|
||||
const range = getCurrentMentionReplaceRange(editor.$view.state);
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.replaceText({
|
||||
...range,
|
||||
text: '{{' + variablePath + '}}',
|
||||
});
|
||||
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
function handleOpenChange(e: MentionOpenChangeEvent) {
|
||||
setPosition(e.state.selection.main.head);
|
||||
setVisible(e.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
}, [editor, visible]);
|
||||
|
||||
const treeData = useVariableTree({});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Mention triggerCharacters={['{', '{}']} onOpenChange={handleOpenChange} />
|
||||
|
||||
<Popover
|
||||
visible={visible}
|
||||
trigger="custom"
|
||||
position="topLeft"
|
||||
rePosKey={posKey}
|
||||
content={
|
||||
<div style={{ width: 300 }}>
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
onSelect={(v) => {
|
||||
insert(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* PositionMirror allows the Popover to appear at the specified cursor position */}
|
||||
<PositionMirror
|
||||
position={position}
|
||||
// When Doc scroll, update position
|
||||
onChange={() => setPosKey(String(Math.random()))}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Variable;
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Variable from './extensions/variable';
|
||||
import { PromptEditor, PromptEditorPropsType } from '../prompt-editor';
|
||||
|
||||
export function PromptEditorWithVariables(props: PromptEditorPropsType) {
|
||||
return (
|
||||
<PromptEditor {...props}>
|
||||
<Variable />
|
||||
</PromptEditor>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "prompt-editor",
|
||||
"depMaterials": [],
|
||||
"depPackages": [
|
||||
"@coze-editor/editor@0.1.0-alpha.8d7a30",
|
||||
"@codemirror/view",
|
||||
"styled-components"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { useInjector } from '@coze-editor/editor/react';
|
||||
import { astDecorator } from '@coze-editor/editor';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
function JinjaHighlight() {
|
||||
const injector = useInjector();
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
injector.inject([
|
||||
astDecorator.whole.of((cursor) => {
|
||||
if (cursor.name === 'JinjaStatementStart' || cursor.name === 'JinjaStatementEnd') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'jinja-statement-bracket',
|
||||
};
|
||||
}
|
||||
|
||||
if (cursor.name === 'JinjaComment') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'jinja-comment',
|
||||
};
|
||||
}
|
||||
|
||||
if (cursor.name === 'JinjaExpression') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'jinja-expression',
|
||||
};
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'.jinja-statement-bracket': {
|
||||
color: '#D1009D',
|
||||
},
|
||||
'.jinja-comment': {
|
||||
color: '#0607094D',
|
||||
},
|
||||
'.jinja-expression': {
|
||||
color: '#4E40E5',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
[injector]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default JinjaHighlight;
|
||||
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { useInjector } from '@coze-editor/editor/react';
|
||||
import { languageSupport } from '@coze-editor/editor/preset-prompt';
|
||||
|
||||
function LanguageSupport() {
|
||||
const injector = useInjector();
|
||||
|
||||
useLayoutEffect(() => injector.inject([languageSupport]), [injector]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default LanguageSupport;
|
||||
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { useInjector } from '@coze-editor/editor/react';
|
||||
import { astDecorator } from '@coze-editor/editor';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
function MarkdownHighlight() {
|
||||
const injector = useInjector();
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
injector.inject([
|
||||
astDecorator.whole.of((cursor) => {
|
||||
// # heading
|
||||
if (cursor.name.startsWith('ATXHeading')) {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'heading',
|
||||
};
|
||||
}
|
||||
|
||||
// *italic*
|
||||
if (cursor.name === 'Emphasis') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'emphasis',
|
||||
};
|
||||
}
|
||||
|
||||
// **bold**
|
||||
if (cursor.name === 'StrongEmphasis') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'strong-emphasis',
|
||||
};
|
||||
}
|
||||
|
||||
// -
|
||||
// 1.
|
||||
// >
|
||||
if (cursor.name === 'ListMark' || cursor.name === 'QuoteMark') {
|
||||
return {
|
||||
type: 'className',
|
||||
className: 'mark',
|
||||
};
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'.heading': {
|
||||
color: '#00818C',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
'.emphasis': {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
'.strong-emphasis': {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
'.mark': {
|
||||
color: '#4E40E5',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
[injector]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default MarkdownHighlight;
|
||||
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Renderer, EditorProvider } from '@coze-editor/editor/react';
|
||||
import preset from '@coze-editor/editor/preset-prompt';
|
||||
|
||||
import { PropsType } from './types';
|
||||
import { UIContainer } from './styles';
|
||||
import MarkdownHighlight from './extensions/markdown';
|
||||
import LanguageSupport from './extensions/language-support';
|
||||
import JinjaHighlight from './extensions/jinja';
|
||||
|
||||
export type PromptEditorPropsType = PropsType;
|
||||
|
||||
export function PromptEditor(props: PropsType) {
|
||||
const { value, onChange, readonly, style, hasError, children } = props || {};
|
||||
|
||||
return (
|
||||
<UIContainer $hasError={hasError} style={style}>
|
||||
<EditorProvider>
|
||||
<Renderer
|
||||
plugins={preset}
|
||||
defaultValue={String(value?.content)}
|
||||
options={{
|
||||
readOnly: readonly,
|
||||
editable: !readonly,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
onChange({ type: 'template', content: e.value });
|
||||
}}
|
||||
/>
|
||||
<MarkdownHighlight />
|
||||
<LanguageSupport />
|
||||
<JinjaHighlight />
|
||||
{children}
|
||||
</EditorProvider>
|
||||
</UIContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const UIContainer = styled.div<{ $hasError?: boolean }>`
|
||||
background-color: var(--semi-color-fill-0);
|
||||
padding-left: 10px;
|
||||
padding-right: 6px;
|
||||
|
||||
${({ $hasError }) =>
|
||||
$hasError &&
|
||||
css`
|
||||
border: 1px solid var(--semi-color-danger-6);
|
||||
`}
|
||||
`;
|
||||
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IFlowTemplateValue } from '../../typings';
|
||||
|
||||
export type PropsType = React.PropsWithChildren<{
|
||||
value?: IFlowTemplateValue;
|
||||
onChange: (value?: IFlowTemplateValue) => void;
|
||||
readonly?: boolean;
|
||||
hasError?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}>;
|
||||
@ -1,8 +1,14 @@
|
||||
{
|
||||
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["./src", "./bin/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"./src",
|
||||
"./bin/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { LayoutConfig } from './type';
|
||||
import { LayoutConfig, LayoutOptions } from './type';
|
||||
|
||||
export const DefaultLayoutConfig: LayoutConfig = {
|
||||
rankdir: 'LR',
|
||||
@ -16,3 +16,8 @@ export const DefaultLayoutConfig: LayoutConfig = {
|
||||
acyclicer: undefined,
|
||||
ranker: 'network-simplex',
|
||||
};
|
||||
|
||||
export const DefaultLayoutOptions: LayoutOptions = {
|
||||
getFollowNode: undefined,
|
||||
enableAnimation: false,
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { GetFollowNode, LayoutConfig, LayoutOptions, LayoutParams } from './type';
|
||||
import { LayoutConfig, LayoutOptions, LayoutParams } from './type';
|
||||
import { LayoutStore } from './store';
|
||||
import { LayoutPosition } from './position';
|
||||
import { DagreLayout } from './dagre';
|
||||
@ -21,9 +21,8 @@ export class Layout {
|
||||
this._position = new LayoutPosition(this._store);
|
||||
}
|
||||
|
||||
public init(params: LayoutParams, options: LayoutOptions = {}): void {
|
||||
this._store.create(params);
|
||||
this.setFollowNode(options.getFollowNode);
|
||||
public init(params: LayoutParams, options: LayoutOptions): void {
|
||||
this._store.create(params, options);
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
@ -39,20 +38,4 @@ export class Layout {
|
||||
}
|
||||
return await this._position.position();
|
||||
}
|
||||
|
||||
public setFollowNode(getFollowNode?: GetFollowNode): void {
|
||||
if (!getFollowNode) return;
|
||||
const context = { store: this._store };
|
||||
this._store.nodes.forEach((node) => {
|
||||
const followTo = getFollowNode(node, context)?.followTo;
|
||||
if (!followTo) return;
|
||||
const followToNode = this._store.getNode(followTo);
|
||||
if (!followToNode) return;
|
||||
if (!followToNode.followedBy) {
|
||||
followToNode.followedBy = [];
|
||||
}
|
||||
followToNode.followedBy.push(node.id);
|
||||
node.followTo = followTo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,19 @@ export class LayoutPosition {
|
||||
constructor(private readonly store: LayoutStore) {}
|
||||
|
||||
public async position(): Promise<void> {
|
||||
if (this.store.options.enableAnimation) {
|
||||
return this.positionWithAnimation();
|
||||
}
|
||||
return this.positionDirectly();
|
||||
}
|
||||
|
||||
private positionDirectly(): void {
|
||||
this.store.nodes.forEach((layoutNode) => {
|
||||
this.updateNodePosition({ layoutNode, step: 100 });
|
||||
});
|
||||
}
|
||||
|
||||
private async positionWithAnimation(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
startTween({
|
||||
from: { d: 0 },
|
||||
|
||||
@ -10,7 +10,14 @@ import {
|
||||
} from '@flowgram.ai/free-layout-core';
|
||||
import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
|
||||
|
||||
import type { LayoutConfig, LayoutEdge, LayoutNode, LayoutParams } from './type';
|
||||
import type {
|
||||
GetFollowNode,
|
||||
LayoutConfig,
|
||||
LayoutEdge,
|
||||
LayoutNode,
|
||||
LayoutOptions,
|
||||
LayoutParams,
|
||||
} from './type';
|
||||
|
||||
interface LayoutStoreData {
|
||||
nodes: Map<string, LayoutNode>;
|
||||
@ -26,6 +33,8 @@ export class LayoutStore {
|
||||
|
||||
private container: WorkflowNodeEntity;
|
||||
|
||||
public options: LayoutOptions;
|
||||
|
||||
constructor(public readonly config: LayoutConfig) {}
|
||||
|
||||
public get initialized(): boolean {
|
||||
@ -56,9 +65,10 @@ export class LayoutStore {
|
||||
return Array.from(this.store.edges.values());
|
||||
}
|
||||
|
||||
public create(params: LayoutParams): void {
|
||||
public create(params: LayoutParams, options: LayoutOptions): void {
|
||||
this.store = this.createStore(params);
|
||||
this.indexMap = this.createIndexMap();
|
||||
this.setOptions(options);
|
||||
this.init = true;
|
||||
}
|
||||
|
||||
@ -279,4 +289,27 @@ export class LayoutStore {
|
||||
|
||||
return uniqueNodeIds;
|
||||
}
|
||||
|
||||
/** 记录运行选项 */
|
||||
private setOptions(options: LayoutOptions): void {
|
||||
this.options = options;
|
||||
this.setFollowNode(options.getFollowNode);
|
||||
}
|
||||
|
||||
/** 设置跟随节点配置 */
|
||||
private setFollowNode(getFollowNode?: GetFollowNode): void {
|
||||
if (!getFollowNode) return;
|
||||
const context = { store: this };
|
||||
this.nodes.forEach((node) => {
|
||||
const followTo = getFollowNode(node, context)?.followTo;
|
||||
if (!followTo) return;
|
||||
const followToNode = this.getNode(followTo);
|
||||
if (!followToNode) return;
|
||||
if (!followToNode.followedBy) {
|
||||
followToNode.followedBy = [];
|
||||
}
|
||||
followToNode.followedBy.push(node.id);
|
||||
node.followTo = followTo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,7 @@ export interface LayoutParams {
|
||||
|
||||
export interface LayoutOptions {
|
||||
getFollowNode?: GetFollowNode;
|
||||
enableAnimation: boolean;
|
||||
}
|
||||
|
||||
export interface LayoutConfig {
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { AutoLayoutOptions } from './type';
|
||||
import { LayoutConfig } from './layout/type';
|
||||
import { DefaultLayoutOptions } from './layout/constant';
|
||||
import { DefaultLayoutConfig, Layout, type LayoutOptions } from './layout';
|
||||
|
||||
@injectable()
|
||||
@ -28,8 +29,11 @@ export class AutoLayoutService {
|
||||
};
|
||||
}
|
||||
|
||||
public async layout(options: LayoutOptions = {}): Promise<void> {
|
||||
await this.layoutNode(this.document.root, options);
|
||||
public async layout(options: Partial<LayoutOptions> = {}): Promise<void> {
|
||||
await this.layoutNode(this.document.root, {
|
||||
...DefaultLayoutOptions,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
|
||||
|
||||
@ -236,38 +236,32 @@ export class HoverLayer extends Layer<HoverLayerOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
const nodeInContainer = !!(nodeHovered?.parent && nodeHovered.parent.flowNodeType !== 'root');
|
||||
|
||||
// 获取最接近的线条
|
||||
// 线条会相交需要获取最接近点位的线条,不能删除的线条不能被选中
|
||||
const lineHovered = checkTargetFromLine
|
||||
? this.linesManager.getCloseInLineFromMousePos(mousePos)
|
||||
: undefined;
|
||||
const lineInContainer = !!lineHovered?.inContainer;
|
||||
|
||||
// 判断容器内节点是否 hover
|
||||
if (nodeHovered && nodeInContainer) {
|
||||
this.updateHoveredKey(nodeHovered.id);
|
||||
return;
|
||||
}
|
||||
// 判断容器内线条是否 hover
|
||||
if (lineHovered && lineInContainer) {
|
||||
this.updateHoveredKey(lineHovered.id);
|
||||
return;
|
||||
if (nodeHovered && lineHovered) {
|
||||
const nodeStackIndex = nodeHovered.renderData.stackIndex;
|
||||
const lineStackIndex = lineHovered.stackIndex;
|
||||
if (nodeStackIndex > lineStackIndex) {
|
||||
return this.updateHoveredKey(nodeHovered.id);
|
||||
} else {
|
||||
return this.updateHoveredKey(lineHovered.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断节点是否 hover
|
||||
if (nodeHovered) {
|
||||
this.updateHoveredKey(nodeHovered.id);
|
||||
return;
|
||||
return this.updateHoveredKey(nodeHovered.id);
|
||||
}
|
||||
// 判断线条是否 hover
|
||||
if (lineHovered) {
|
||||
this.hoverService.updateHoveredKey(lineHovered.id);
|
||||
return;
|
||||
return this.updateHoveredKey(lineHovered.id);
|
||||
}
|
||||
|
||||
// 上述逻辑都未命中 则清空 hoverd
|
||||
// 上述逻辑都未命中 则清空 hovered
|
||||
hoverService.clearHovered();
|
||||
|
||||
const currentState = this.editorStateConfig.getCurrentState();
|
||||
|
||||
@ -80,23 +80,26 @@ export class StackingContextManager {
|
||||
const element = nodeRenderData.node;
|
||||
element.style.position = 'absolute';
|
||||
if (level === undefined) {
|
||||
element.style.zIndex = 'auto';
|
||||
nodeRenderData.stackIndex = 0;
|
||||
element.style.zIndex = 'auto';
|
||||
return;
|
||||
}
|
||||
const stackIndex = StackingConfig.startIndex + level;
|
||||
element.style.zIndex = String(stackIndex);
|
||||
nodeRenderData.stackIndex = stackIndex;
|
||||
nodeRenderData.stackIndex = level;
|
||||
const zIndex = StackingConfig.startIndex + level;
|
||||
element.style.zIndex = String(zIndex);
|
||||
});
|
||||
this.lines.forEach((line) => {
|
||||
const level = lineLevel.get(line.id);
|
||||
const element = line.node;
|
||||
element.style.position = 'absolute';
|
||||
if (level === undefined) {
|
||||
line.stackIndex = 0;
|
||||
element.style.zIndex = 'auto';
|
||||
return;
|
||||
}
|
||||
element.style.zIndex = String(StackingConfig.startIndex + level);
|
||||
line.stackIndex = level;
|
||||
const zIndex = StackingConfig.startIndex + level;
|
||||
element.style.zIndex = String(zIndex);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user