diff --git a/apps/demo-free-layout/src/initial-data.ts b/apps/demo-free-layout/src/initial-data.ts index cd67fa94..546bc631 100644 --- a/apps/demo-free-layout/src/initial-data.ts +++ b/apps/demo-free-layout/src/initial-data.ts @@ -185,7 +185,7 @@ export const initialData: FlowDocumentJSON = { }, prompt: { type: 'constant', - content: '', + content: '# User Input\nquery:{{loop_sGybT_locals.item.int}}', }, }, inputs: { @@ -262,7 +262,7 @@ export const initialData: FlowDocumentJSON = { }, prompt: { type: 'constant', - content: '', + content: '# User Input\nquery:{{loop_sGybT_locals.item.str}}', }, }, inputs: { @@ -358,7 +358,7 @@ export const initialData: FlowDocumentJSON = { }, prompt: { type: 'constant', - content: '', + content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}', }, }, inputs: { @@ -435,7 +435,7 @@ export const initialData: FlowDocumentJSON = { }, prompt: { type: 'constant', - content: '', + content: '# LLM Input\nresult:{{llm_8--A3.result}}', }, }, inputs: { diff --git a/apps/docs/src/en/guide/advanced/form-materials.mdx b/apps/docs/src/en/guide/advanced/form-materials.mdx index 8f704816..c438f243 100644 --- a/apps/docs/src/en/guide/advanced/form-materials.mdx +++ b/apps/docs/src/en/guide/advanced/form-materials.mdx @@ -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. +### 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}}" + } + ``` + + ## Currently Supported Effect Materials ### provideBatchInput diff --git a/apps/docs/src/public/materials/prompt-editor-with-variables.png b/apps/docs/src/public/materials/prompt-editor-with-variables.png new file mode 100644 index 00000000..b236980f Binary files /dev/null and b/apps/docs/src/public/materials/prompt-editor-with-variables.png differ diff --git a/apps/docs/src/zh/guide/advanced/form-materials.mdx b/apps/docs/src/zh/guide/advanced/form-materials.mdx index f7481278..b7931723 100644 --- a/apps/docs/src/zh/guide/advanced/form-materials.mdx +++ b/apps/docs/src/zh/guide/advanced/form-materials.mdx @@ -108,7 +108,23 @@ CLI 运行成功后,相关物料会自动添加到当前项目下的 `src/form ConditionRow 用于 **一行** 条件判断的配置 +### PromptEditorWithVariables + + PromptEditorWithVariables 用于支持变量配置的 Prompt 编辑器。 + + 下面是一个 Prompt 编辑器的配置示例,其中 `query` 变量为字符串类型,`enable` 变量为布尔类型: + ```typescript + { + type: "template", + content: "#User Input:\nquery:{{start_0.query}}\nenable:{{start_0.enable}}" + } + ``` + ## 当前支持的 Effect 物料 diff --git a/packages/materials/form-materials/src/components/prompt-editor-with-variables/config.json b/packages/materials/form-materials/src/components/prompt-editor-with-variables/config.json index 893a6d94..ecce6619 100644 --- a/packages/materials/form-materials/src/components/prompt-editor-with-variables/config.json +++ b/packages/materials/form-materials/src/components/prompt-editor-with-variables/config.json @@ -1,6 +1,8 @@ { "name": "prompt-editor", - "depMaterials": [], + "depMaterials": [ + "variable-selector" + ], "depPackages": [ "@coze-editor/editor@0.1.0-alpha.8d7a30", "@codemirror/view", diff --git a/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tag.tsx b/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tag.tsx new file mode 100644 index 00000000..64f58f9c --- /dev/null +++ b/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tag.tsx @@ -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 ; + } + + return icon; + }; + + renderVariable(v?: BaseVariableField) { + if (!v) { + this.root.render( + } color="amber"> + Unknown + + ); + return; + } + + const rootField = last(v.parentFields); + + const rootTitle = ( + {rootField?.meta.title ? `${rootField.meta.title} -` : ''} + ); + const rootIcon = this.renderIcon(rootField?.meta.icon); + + this.root.render( + + {rootIcon} + {rootTitle} + {v?.keyPath.slice(1).join('.')} + + } + > + + {rootTitle} + {v?.key} + + + ); + } + + 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; +} diff --git a/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable.tsx b/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tree.tsx similarity index 93% rename from packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable.tsx rename to packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tree.tsx index ae5888ad..c2f0429b 100644 --- a/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable.tsx +++ b/packages/materials/form-materials/src/components/prompt-editor-with-variables/extensions/variable-tree.tsx @@ -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 ( <> - + ); } - -export default Variable; diff --git a/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx b/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx index 02d7f8ee..afc55dc7 100644 --- a/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx +++ b/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx @@ -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 ( - + + ); } diff --git a/packages/materials/form-materials/src/components/prompt-editor-with-variables/styles.tsx b/packages/materials/form-materials/src/components/prompt-editor-with-variables/styles.tsx new file mode 100644 index 00000000..e4f32375 --- /dev/null +++ b/packages/materials/form-materials/src/components/prompt-editor-with-variables/styles.tsx @@ -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; +`; diff --git a/packages/variable-engine/variable-core/src/ast/declaration/base-variable-field.ts b/packages/variable-engine/variable-core/src/ast/declaration/base-variable-field.ts index 33ca45eb..0cf0cbec 100644 --- a/packages/variable-engine/variable-core/src/ast/declaration/base-variable-field.ts +++ b/packages/variable-engine/variable-core/src/ast/declaration/base-variable-field.ts @@ -41,7 +41,7 @@ export abstract class BaseVariableField 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 { diff --git a/packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts b/packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts index 10d42583..efcc92ac 100644 --- a/packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts +++ b/packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts @@ -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) );