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: {
|
||||
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: {
|
||||
|
||||
@ -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 物料
|
||||
|
||||
|
||||
@ -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;
|
||||
`;
|
||||
@ -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