fix: preserve XML/custom tags in AgentFlow prompt editor#6123
fix: preserve XML/custom tags in AgentFlow prompt editor#6123octo-patch wants to merge 2 commits intoFlowiseAI:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the RichTextEditor and VariableInput components, leveraging TipTap for rich text editing and syntax highlighting. The review identifies several issues: missing typography properties in the theme tokens which will lead to runtime errors, a regex anchoring bug in HTML detection, and a potential infinite loop in the value synchronization logic. Suggestions were also provided to fix prop forwarding in styled components, correct a syntax error in the mention chip rendering, and extract shared utility functions to reduce code duplication.
| height: rows ? `${rows * tokens.typography.rowHeightRem}rem` : `${tokens.typography.singleLineHeightRem}rem`, | ||
| overflowY: rows ? 'auto' : 'hidden', | ||
| overflowX: rows ? 'auto' : 'hidden', | ||
| lineHeight: rows ? `${tokens.typography.rowHeightRem}em` : `${tokens.typography.singleLineLineHeightEm}em`, |
There was a problem hiding this comment.
The tokens object imported from @/core/theme/tokens is missing the typography property, which will cause a runtime error when accessing tokens.typography.rowHeightRem, tokens.typography.singleLineHeightRem, or tokens.typography.singleLineLineHeightEm. Please ensure the design tokens are updated to include these values.
| */ | ||
| const isTiptapHtml = (content: string): boolean => { | ||
| if (!content) return false | ||
| return /<(?:p|h[1-6]|ul|ol|pre|blockquote|hr)\b/i.test(content.trim()) |
There was a problem hiding this comment.
The regex in isTiptapHtml should be anchored to the start of the string. Currently, it returns true if a known block tag appears anywhere in the content (e.g., in a plain-text sentence like "The
tag is used for paragraphs"), which would incorrectly prevent escaping of literal angle brackets. Additionally, consider extracting this and the other helper functions (plainTextToTiptapHtml, toTiptapContent) to a shared utility file to avoid duplication with VariableInput.tsx.
| return /<(?:p|h[1-6]|ul|ol|pre|blockquote|hr)\b/i.test(content.trim()) | |
| return /^<(?:p|h[1-6]|ul|ol|pre|blockquote|hr)\b/i.test(content.trim()) |
|
|
||
| /** Normalises a stored value for safe consumption by TipTap's `setContent`. */ | ||
| const toTiptapContent = (value: string): string => { | ||
| if (!value) return '' |
There was a problem hiding this comment.
Returning an empty string for nullish or empty values causes an infinite loop in the useEffect sync logic. TipTap's getHTML() returns
for an empty document, so toTiptapContent("") !== editor.getHTML() will always be true, triggering setContent on every render. Removing this early return allows plainTextToTiptapHtml to correctly return for empty strings, matching TipTap's internal state.| if (!value) return '' | |
| if (value == null) return '<p></p>' |
References
- Avoid calling setState in a useEffect cleanup function for components that do not unmount, as it can cause infinite re-render loops when props change. Reset state in the main useEffect body instead.
| /* ── Styled wrapper ── */ | ||
|
|
||
| const StyledEditorContent = styled(EditorContent, { | ||
| shouldForwardProp: (prop) => prop !== 'rows' |
There was a problem hiding this comment.
The disabled prop should be added to shouldForwardProp to prevent it from being passed down to the underlying EditorContent component, which may not expect it as a standard HTML attribute. This ensures consistency with the implementation in VariableInput.tsx.
| shouldForwardProp: (prop) => prop !== 'rows' | |
| shouldForwardProp: (prop) => !['rows', 'disabled'].includes(prop as string) |
| return [ | ||
| 'span', | ||
| mergeAttributes(this.HTMLAttributes ?? {}, options.HTMLAttributes), | ||
| `${options.suggestion?.char ?? '{{'}${node.attrs.label ?? node.attrs.id}}}` |
There was a problem hiding this comment.
There is an extra closing brace in the mention chip rendering. If the trigger character is {{, this template literal results in {{variableName}}} (three closing braces). It should likely be }} to match the standard double-brace syntax.
| `${options.suggestion?.char ?? '{{'}${node.attrs.label ?? node.attrs.id}}}` | |
| (options.suggestion?.char ?? '{{') + (node.attrs.label ?? node.attrs.id) |
Fixes #6047
Problem
When a prompt containing XML-like custom tags (e.g.
<question>,<context>) is loaded into the AgentFlow V2 prompt editor, TipTap/ProseMirror strips those tags because they are not part of its HTML schema. The text content is preserved but the enclosing tags are lost. This breaks structured prompting techniques recommended by Claude, OpenAI, and other major LLM providers.Root cause:
setContent(value)in bothVariableInputandRichTextEditorpasses the stored string directly to ProseMirror's HTML parser. Unknown HTML elements are silently dropped.Solution
Add two small helpers —
isTiptapHtmlandtoTiptapContent— to both editor components:isTiptapHtml(content)— detects whether the stored value is already TipTap-generated HTML (starts with a known block element like<p>,<h1>,<ul>, etc.).toTiptapContent(value)— when the value is not TipTap HTML, escapes&,<,>as HTML entities and wraps each line in<p>so ProseMirror stores angle brackets as literal text.This is transparent to the LLM:
resolveVariablesinbuildAgentflow.tsalready runs Turndown (withescapedisabled) on HTML content before variable substitution, which decodes<question>back to<question>.Changes
packages/agentflow/src/atoms/VariableInput.tsxpackages/agentflow/src/atoms/RichTextEditor.tsxTesting
<question>\n{{question}}\n</question>.<question>…</question>intact.