mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat: prompt editor variable display (#453)
* feat: prompt editor variable display * feat: prompt editor with variables docs
This commit is contained in:
parent
25e20d8c20
commit
9210f5041d
@ -185,7 +185,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
content: '',
|
content: '# User Input\nquery:{{loop_sGybT_locals.item.int}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
@ -262,7 +262,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
content: '',
|
content: '# User Input\nquery:{{loop_sGybT_locals.item.str}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
@ -358,7 +358,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
content: '',
|
content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
@ -435,7 +435,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
content: '',
|
content: '# LLM Input\nresult:{{llm_8--A3.result}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
|
|||||||
@ -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.
|
ConditionRow is used for configuring a **single row** of condition judgment.
|
||||||
</MaterialDisplay>
|
</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
|
## Currently Supported Effect Materials
|
||||||
|
|
||||||
### provideBatchInput
|
### 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 用于 **一行** 条件判断的配置
|
ConditionRow 用于 **一行** 条件判断的配置
|
||||||
</MaterialDisplay>
|
</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 物料
|
## 当前支持的 Effect 物料
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "prompt-editor",
|
"name": "prompt-editor",
|
||||||
"depMaterials": [],
|
"depMaterials": [
|
||||||
|
"variable-selector"
|
||||||
|
],
|
||||||
"depPackages": [
|
"depPackages": [
|
||||||
"@coze-editor/editor@0.1.0-alpha.8d7a30",
|
"@coze-editor/editor@0.1.0-alpha.8d7a30",
|
||||||
"@codemirror/view",
|
"@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';
|
import { useVariableTree } from '../../variable-selector';
|
||||||
|
|
||||||
function Variable() {
|
export function VariableTree() {
|
||||||
const [posKey, setPosKey] = useState('');
|
const [posKey, setPosKey] = useState('');
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [position, setPosition] = useState(-1);
|
const [position, setPosition] = useState(-1);
|
||||||
@ -53,7 +53,7 @@ function Variable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Mention triggerCharacters={['{', '{}']} onOpenChange={handleOpenChange} />
|
<Mention triggerCharacters={['{', '{}', '@']} onOpenChange={handleOpenChange} />
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -81,5 +81,3 @@ function Variable() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Variable;
|
|
||||||
@ -5,13 +5,15 @@
|
|||||||
|
|
||||||
import React from 'react';
|
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';
|
import { PromptEditor, PromptEditorPropsType } from '../prompt-editor';
|
||||||
|
|
||||||
export function PromptEditorWithVariables(props: PromptEditorPropsType) {
|
export function PromptEditorWithVariables(props: PromptEditorPropsType) {
|
||||||
return (
|
return (
|
||||||
<PromptEditor {...props}>
|
<PromptEditor {...props}>
|
||||||
<Variable />
|
<VariableTree />
|
||||||
|
<VariableTagInject />
|
||||||
</PromptEditor>
|
</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;
|
||||||
|
`;
|
||||||
@ -41,7 +41,7 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get keyPath(): string[] {
|
get keyPath(): string[] {
|
||||||
return this.parentFields.reverse().map((_field) => _field.key);
|
return [...this.parentFields.reverse().map((_field) => _field.key), this.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
get meta(): VariableMeta {
|
get meta(): VariableMeta {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
|
distinctUntilKeyChanged,
|
||||||
map,
|
map,
|
||||||
merge,
|
merge,
|
||||||
share,
|
share,
|
||||||
@ -179,8 +180,12 @@ export class ScopeAvailableData {
|
|||||||
merge(this.anyVariableChange$, this.variables$)
|
merge(this.anyVariableChange$, this.variables$)
|
||||||
.pipe(
|
.pipe(
|
||||||
triggerOnInit ? startWith() : tap(() => null),
|
triggerOnInit ? startWith() : tap(() => null),
|
||||||
map(() => this.getByKeyPath(keyPath)),
|
map(() => {
|
||||||
distinctUntilChanged((_prevNode, _node) => _prevNode?.hash !== _node?.hash)
|
const v = this.getByKeyPath(keyPath);
|
||||||
|
return { v, hash: v?.hash };
|
||||||
|
}),
|
||||||
|
distinctUntilKeyChanged('hash'),
|
||||||
|
map(({ v }) => v)
|
||||||
)
|
)
|
||||||
.subscribe(cb)
|
.subscribe(cb)
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user