fix(web): worflow bugfix
This commit is contained in:
@@ -14,6 +14,8 @@ import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin
|
||||
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||
import CommandPlugin from './plugin/CommandPlugin';
|
||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||
import { VariableNode } from './nodes/VariableNode'
|
||||
|
||||
interface LexicalEditorProps {
|
||||
@@ -88,6 +90,35 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
.editor-paragraph:has-text('[') .editor-text {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
}
|
||||
.editor-with-line-numbers {
|
||||
display: flex;
|
||||
}
|
||||
.line-numbers {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
color: #656d76;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 4px 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.line-numbers > div {
|
||||
min-height: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.editor-content-with-numbers {
|
||||
flex: 1;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.editor-content-with-numbers p {
|
||||
margin: 0;
|
||||
min-height: 20px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
@@ -117,25 +148,49 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<div style={{ position: 'relative' }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||
enableJinja2 ? (
|
||||
<div className="editor-with-line-numbers" style={{
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
/>
|
||||
minHeight: `${height}px`,
|
||||
}}>
|
||||
<div className="line-numbers">
|
||||
<div>1</div>
|
||||
</div>
|
||||
<ContentEditable
|
||||
className="editor-content-with-numbers"
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: '4px 11px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: variant === 'borderless' ? '0' : '6px',
|
||||
left: variant === 'borderless' ? '0' : '11px',
|
||||
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
color: '#5B6167',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
@@ -149,6 +204,8 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
{enableJinja2 && <Jinja2HighlightPlugin />}
|
||||
{enableJinja2 && <LineNumberPlugin />}
|
||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||
|
||||
@@ -36,64 +36,68 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
|
||||
const parts = value.split(/(\{\{[^}]+\}\})/);
|
||||
|
||||
parts.forEach(part => {
|
||||
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
|
||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||
if (enableJinja2) {
|
||||
// Handle newlines properly in Jinja2 mode
|
||||
const lines = value.split('\n');
|
||||
lines.forEach((line) => {
|
||||
const paragraph = $createParagraphNode();
|
||||
paragraph.append($createTextNode(line));
|
||||
root.append(paragraph);
|
||||
});
|
||||
} else {
|
||||
const paragraph = $createParagraphNode();
|
||||
parts.forEach(part => {
|
||||
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
|
||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||
|
||||
if (enableJinja2) {
|
||||
paragraph.append($createTextNode(part));
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextMatch) {
|
||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||
if (contextSuggestion) {
|
||||
paragraph.append($createVariableNode(contextSuggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (conversationMatch) {
|
||||
const [_, variableName] = conversationMatch;
|
||||
const conversationSuggestion = options.find(s =>
|
||||
s.group === 'CONVERSATION' && s.label === variableName
|
||||
);
|
||||
if (conversationSuggestion) {
|
||||
paragraph.append($createVariableNode(conversationSuggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const [_, nodeId, label] = match;
|
||||
|
||||
const suggestion = options.find(s => {
|
||||
if (nodeId === 'sys') {
|
||||
return s.nodeData.type === 'start' && s.label === `sys.${label}`
|
||||
if (contextMatch) {
|
||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||
if (contextSuggestion) {
|
||||
paragraph.append($createVariableNode(contextSuggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
return s.nodeData.id === nodeId && s.label === label
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
if (conversationMatch) {
|
||||
const [_, variableName] = conversationMatch;
|
||||
const conversationSuggestion = options.find(s =>
|
||||
s.group === 'CONVERSATION' && s.label === variableName
|
||||
);
|
||||
if (conversationSuggestion) {
|
||||
paragraph.append($createVariableNode(conversationSuggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const [_, nodeId, label] = match;
|
||||
|
||||
if (suggestion) {
|
||||
paragraph.append($createVariableNode(suggestion));
|
||||
} else {
|
||||
const suggestion = options.find(s => {
|
||||
if (nodeId === 'sys') {
|
||||
return s.nodeData.type === 'start' && s.label === `sys.${label}`
|
||||
}
|
||||
return s.nodeData.id === nodeId && s.label === label
|
||||
});
|
||||
|
||||
if (suggestion) {
|
||||
paragraph.append($createVariableNode(suggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
} else if (part) {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
} else if (part) {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
});
|
||||
|
||||
root.append(paragraph);
|
||||
});
|
||||
root.append(paragraph);
|
||||
}
|
||||
}, { discrete: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { TextNode, $createTextNode } from 'lexical';
|
||||
|
||||
const Jinja2HighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (containsJinja2Patterns(text)) {
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const tokens = tokenizeJinja2(text);
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
|
||||
switch (token.type) {
|
||||
case 'number':
|
||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||
break;
|
||||
case 'header-0':
|
||||
case 'header-1':
|
||||
case 'header-2':
|
||||
case 'header-3':
|
||||
case 'header-4':
|
||||
case 'header-5':
|
||||
newNode.setStyle('color: #008000');
|
||||
break;
|
||||
case 'brace-0':
|
||||
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-1':
|
||||
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-2':
|
||||
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-3':
|
||||
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'expression-0':
|
||||
case 'expression-1':
|
||||
case 'expression-2':
|
||||
case 'expression-3':
|
||||
case 'statement-0':
|
||||
case 'statement-1':
|
||||
case 'statement-2':
|
||||
case 'statement-3':
|
||||
// Jinja2 delimiters use same color as braces
|
||||
break;
|
||||
case 'comment-0':
|
||||
case 'comment-1':
|
||||
case 'comment-2':
|
||||
case 'comment-3':
|
||||
newNode.setStyle('color: #721c24; font-family: monospace;');
|
||||
break;
|
||||
case 'variable':
|
||||
newNode.setStyle('color: #0969da; font-weight: 500;');
|
||||
break;
|
||||
case 'filter':
|
||||
newNode.setStyle('color: #8250df; font-weight: 500;');
|
||||
break;
|
||||
case 'keyword':
|
||||
newNode.setStyle('color: #cf222e; font-weight: 600;');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function containsJinja2Patterns(text: string): boolean {
|
||||
return /[{}#\d]/.test(text);
|
||||
}
|
||||
|
||||
function tokenizeJinja2(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
let i = 0;
|
||||
let braceLevel = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Check for markdown headers (at start or after whitespace)
|
||||
if (text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) {
|
||||
let headerLevel = 0;
|
||||
let start = i;
|
||||
while (i < text.length && text[i] === '#') {
|
||||
headerLevel++;
|
||||
i++;
|
||||
}
|
||||
// Skip space after #
|
||||
if (i < text.length && text[i] === ' ') {
|
||||
i++;
|
||||
}
|
||||
// Get the rest of the header text
|
||||
while (i < text.length && text[i] !== '\n' && !/[{}]/.test(text[i])) {
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: `header-${Math.min(headerLevel - 1, 5)}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for numbers
|
||||
if (/\d/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[\d.]/.test(text[i])) {
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text[i] === '{') {
|
||||
tokens.push({ text: '{', type: `brace-${braceLevel % 4}` });
|
||||
braceLevel++;
|
||||
i++;
|
||||
} else if (text[i] === '}') {
|
||||
braceLevel = Math.max(0, braceLevel - 1);
|
||||
tokens.push({ text: '}', type: `brace-${braceLevel % 4}` });
|
||||
i++;
|
||||
} else {
|
||||
let start = i;
|
||||
while (i < text.length && text[i] !== '{' && text[i] !== '}' &&
|
||||
!(text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) &&
|
||||
!/\d/.test(text[i])) {
|
||||
i++;
|
||||
}
|
||||
if (start < i) {
|
||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default Jinja2HighlightPlugin;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
|
||||
|
||||
const JsonHighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
// Check if text contains JSON-like patterns
|
||||
if (containsJsonPatterns(text)) {
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
// Split text into tokens and create new nodes with appropriate classes
|
||||
const tokens = tokenizeJson(text);
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
|
||||
// Set format based on token type
|
||||
switch (token.type) {
|
||||
case 'string':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #032f62');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #005cc5');
|
||||
break;
|
||||
case 'boolean':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #d73a49');
|
||||
break;
|
||||
case 'null':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #6f42c1');
|
||||
break;
|
||||
case 'key':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #22863a; font-weight: bold');
|
||||
break;
|
||||
case 'punctuation':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #24292e');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
// Replace the original text node with the new highlighted nodes
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function containsJsonPatterns(text: string): boolean {
|
||||
// Check for JSON-like patterns
|
||||
return /[{}\[\]:,]/.test(text) ||
|
||||
/"[^"]*"/.test(text) ||
|
||||
/\b\d+(\.\d+)?\b/.test(text) ||
|
||||
/\b(true|false|null)\b/.test(text);
|
||||
}
|
||||
|
||||
function tokenizeJson(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
|
||||
|
||||
if (string) {
|
||||
// Check if it's a key (followed by colon)
|
||||
const afterMatch = text.slice(match.index + fullMatch.length).trim();
|
||||
if (afterMatch.startsWith(':')) {
|
||||
tokens.push({ text: fullMatch, type: 'key' });
|
||||
} else {
|
||||
tokens.push({ text: fullMatch, type: 'string' });
|
||||
}
|
||||
} else if (punctuation) {
|
||||
tokens.push({ text: fullMatch, type: 'punctuation' });
|
||||
} else if (number) {
|
||||
tokens.push({ text: fullMatch, type: 'number' });
|
||||
} else if (boolean) {
|
||||
if (fullMatch === 'null') {
|
||||
tokens.push({ text: fullMatch, type: 'null' });
|
||||
} else {
|
||||
tokens.push({ text: fullMatch, type: 'boolean' });
|
||||
}
|
||||
} else if (whitespace || other) {
|
||||
tokens.push({ text: fullMatch, type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default JsonHighlightPlugin;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getRoot } from 'lexical';
|
||||
|
||||
const LineNumberPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [lineCount, setLineCount] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const paragraphCount = root.getChildren().length;
|
||||
const lines = Math.max(1, paragraphCount);
|
||||
setLineCount(lines);
|
||||
});
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateLineNumbers = () => {
|
||||
const lineNumbersElement = document.querySelector('.line-numbers');
|
||||
const editorElement = document.querySelector('.editor-content-with-numbers');
|
||||
|
||||
if (lineNumbersElement && editorElement) {
|
||||
const paragraphs = editorElement.querySelectorAll('p');
|
||||
|
||||
// Clear existing line numbers
|
||||
lineNumbersElement.innerHTML = '';
|
||||
|
||||
// Create line numbers positioned at each paragraph
|
||||
paragraphs.forEach((paragraph, index) => {
|
||||
const lineNumber = document.createElement('div');
|
||||
lineNumber.textContent = (index + 1).toString();
|
||||
lineNumber.style.position = 'absolute';
|
||||
lineNumber.style.top = paragraph.offsetTop + 'px';
|
||||
lineNumber.style.right = '8px';
|
||||
lineNumber.style.height = '20px';
|
||||
lineNumber.style.lineHeight = '20px';
|
||||
lineNumbersElement.appendChild(lineNumber);
|
||||
});
|
||||
|
||||
// Set line numbers container to relative positioning
|
||||
(lineNumbersElement as HTMLElement).style.position = 'relative';
|
||||
}
|
||||
};
|
||||
|
||||
// Update line numbers after content changes
|
||||
const timer = setTimeout(updateLineNumbers, 100);
|
||||
|
||||
// Also update on window resize
|
||||
window.addEventListener('resize', updateLineNumbers);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener('resize', updateLineNumbers);
|
||||
};
|
||||
}, [lineCount]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default LineNumberPlugin;
|
||||
Reference in New Issue
Block a user