From 26582bfb87f640f5c7f3269f425f00c13ca4fd65 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 25 Aug 2026 14:02:04 +0800 Subject: [PATCH] feat(frontend): add dynamic variable prompt editor --- .../dynamic-variable-template.ts | 102 +++ .../assistant-editor/dynamic-variables.tsx | 34 +- .../assistant-editor/prompt-editor.tsx | 18 +- .../prompt-template-editor.tsx | 602 ++++++++++++++++++ frontend/src/components/ui/popover.tsx | 8 +- 5 files changed, 732 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/assistant-editor/dynamic-variable-template.ts create mode 100644 frontend/src/components/assistant-editor/prompt-template-editor.tsx diff --git a/frontend/src/components/assistant-editor/dynamic-variable-template.ts b/frontend/src/components/assistant-editor/dynamic-variable-template.ts new file mode 100644 index 0000000..0a72d01 --- /dev/null +++ b/frontend/src/components/assistant-editor/dynamic-variable-template.ts @@ -0,0 +1,102 @@ +export const DYNAMIC_VARIABLE_PATTERN = + /{{\s*([A-Za-z][A-Za-z0-9_]{0,63})\s*}}/g; + +export const SYSTEM_DYNAMIC_VARIABLES = [ + ["system__conversation_id", "会话 ID"], + ["system__time", "当前时间"], + ["system__timezone", "会话时区"], + ["system__agent_turns", "助手轮次"], + ["system__conversation_history", "会话历史"], +] as const; + +const SYSTEM_DYNAMIC_VARIABLE_NAMES = new Set( + SYSTEM_DYNAMIC_VARIABLES.map(([name]) => name), +); + +const DYNAMIC_VARIABLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,63}$/; + +export type DynamicVariableToken = { + start: number; + end: number; + name: string; + source: "custom" | "system"; +}; + +export type DynamicVariableTrigger = { + start: number; + query: string; +}; + +export function isPublicDynamicVariableName(name: string): boolean { + return ( + DYNAMIC_VARIABLE_NAME_PATTERN.test(name) && + !name.startsWith("system__") && + !name.startsWith("secret__") + ); +} + +export function isKnownSystemDynamicVariableName(name: string): boolean { + return SYSTEM_DYNAMIC_VARIABLE_NAMES.has(name); +} + +export function extractDynamicVariableNames(...templates: string[]): string[] { + const names = new Set(); + for (const template of templates) { + for (const match of template.matchAll(DYNAMIC_VARIABLE_PATTERN)) { + if (isPublicDynamicVariableName(match[1])) names.add(match[1]); + } + } + return [...names]; +} + +export function findDynamicVariableTokens( + template: string, +): DynamicVariableToken[] { + const tokens: DynamicVariableToken[] = []; + + for (const match of template.matchAll(DYNAMIC_VARIABLE_PATTERN)) { + const name = match[1]; + const start = match.index; + const source = isPublicDynamicVariableName(name) + ? "custom" + : isKnownSystemDynamicVariableName(name) + ? "system" + : null; + + if (source && start !== undefined) { + tokens.push({ + start, + end: start + match[0].length, + name, + source, + }); + } + } + + return tokens; +} + +export function findDynamicVariableTrigger( + value: string, + selectionStart: number, + selectionEnd: number, +): DynamicVariableTrigger | null { + if (selectionStart !== selectionEnd) return null; + + const containingToken = findDynamicVariableTokens(value).some( + (token) => selectionStart > token.start && selectionStart < token.end, + ); + if (containingToken) return null; + + const beforeCaret = value.slice(0, selectionStart); + const start = beforeCaret.lastIndexOf("{{"); + if (start < 0 || beforeCaret[start - 1] === "{") return null; + + const query = beforeCaret.slice(start + 2); + if (query.includes("{") || query.includes("}") || /\s/.test(query)) { + return null; + } + if (query && !DYNAMIC_VARIABLE_NAME_PATTERN.test(query)) return null; + + return { start, query }; +} diff --git a/frontend/src/components/assistant-editor/dynamic-variables.tsx b/frontend/src/components/assistant-editor/dynamic-variables.tsx index ad274a9..447bfc3 100644 --- a/frontend/src/components/assistant-editor/dynamic-variables.tsx +++ b/frontend/src/components/assistant-editor/dynamic-variables.tsx @@ -20,33 +20,11 @@ import { } from "@/components/ui/tabs"; import type { WorkflowGraph } from "@/components/workflow/specs"; import type { DynamicVariableDefinition } from "@/lib/api"; - -const DYNAMIC_VARIABLE_PATTERN = /{{\s*([A-Za-z][A-Za-z0-9_]{0,63})\s*}}/g; -const SYSTEM_DYNAMIC_VARIABLES = [ - ["system__conversation_id", "会话 ID"], - ["system__time", "当前时间"], - ["system__timezone", "会话时区"], - ["system__agent_turns", "助手轮次"], - ["system__conversation_history", "会话历史"], -] as const; - -function isPublicDynamicVariableName(name: string): boolean { - return ( - /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) && - !name.startsWith("system__") && - !name.startsWith("secret__") - ); -} - -function extractDynamicVariableNames(...templates: string[]): string[] { - const names = new Set(); - for (const template of templates) { - for (const match of template.matchAll(DYNAMIC_VARIABLE_PATTERN)) { - if (isPublicDynamicVariableName(match[1])) names.add(match[1]); - } - } - return [...names]; -} +import { + extractDynamicVariableNames, + isPublicDynamicVariableName, + SYSTEM_DYNAMIC_VARIABLES, +} from "@/components/assistant-editor/dynamic-variable-template"; function collectStructuredVariableNames(value: unknown, names: Set): void { if (Array.isArray(value)) { @@ -136,7 +114,7 @@ export function DynamicVariableEditorHint({ {"{{"} {" "} - 添加变量,保存时自动识别 + 选择或创建变量,保存时自动识别