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)
);