fix(web): workflow bugfix

This commit is contained in:
zhaoying
2026-01-07 17:35:23 +08:00
parent 72c27273e4
commit 030a141c64
14 changed files with 833 additions and 186 deletions

View File

@@ -1,4 +1,4 @@
import { type FC, useState } from 'react';
import { type FC, useState, useEffect } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -23,6 +23,7 @@ interface LexicalEditorProps {
options: Suggestion[];
variant?: 'outlined' | 'borderless';
height?: number;
enableJinja2?: boolean;
}
const theme = {
@@ -33,6 +34,15 @@ const theme = {
},
};
const jinja2Theme = {
...theme,
code: 'jinja2-expression',
text: {
...theme.text,
code: 'jinja2-inline',
},
};
const Editor: FC<LexicalEditorProps> =({
placeholder = "请输入内容...",
value = "",
@@ -40,19 +50,62 @@ const Editor: FC<LexicalEditorProps> =({
options,
variant = 'borderless',
height = 60,
enableJinja2 = false,
}) => {
const [_count, setCount] = useState(0);
useEffect(() => {
if (enableJinja2) {
const styleId = 'jinja2-styles';
let existingStyle = document.getElementById(styleId);
if (!existingStyle) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.jinja2-expression {
background-color: #f6f8fa !important;
border: 1px solid #d1d9e0 !important;
border-radius: 3px !important;
padding: 2px 4px !important;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
color: #0969da !important;
}
.jinja2-inline {
background-color: #f6f8fa !important;
padding: 1px 3px !important;
border-radius: 2px !important;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
color: #0969da !important;
}
.editor-paragraph {
margin: 0;
}
.editor-paragraph:has-text('{') .editor-text,
.editor-paragraph:has-text('[') .editor-text {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
}
`;
document.head.appendChild(style);
}
}
}, [enableJinja2]);
const initialConfig = {
namespace: 'AutocompleteEditor',
theme,
nodes: [
theme: enableJinja2 ? jinja2Theme : theme,
nodes: enableJinja2 ? [
// 当启用jinja2时不使用VariableNode使用普通文本
] : [
// HeadingNode,
// QuoteNode,
// ListItemNode,
// ListNode,
// LinkNode,
// CodeNode,
VariableNode
VariableNode,
],
onError: (error: Error) => {
console.error(error);
@@ -96,9 +149,9 @@ const Editor: FC<LexicalEditorProps> =({
/>
<HistoryPlugin />
<CommandPlugin />
<AutocompletePlugin options={options} />
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
<InitialValuePlugin value={value} options={options} />
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
</div>
</LexicalComposer>
);

View File

@@ -17,7 +17,7 @@ export interface Suggestion {
disabled?: boolean; // 标记是否禁用
}
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
const [editor] = useLexicalComposerContext();
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
}, [editor]);
const insertMention = (suggestion: Suggestion) => {
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
if (enableJinja2) {
// 在jinja2模式下插入{{variable}}格式的文本
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const anchorOffset = selection.anchor.offset;
const nodeText = anchorNode.getTextContent();
// 移除触发字符'/'
const textBefore = nodeText.substring(0, anchorOffset - 1);
const textAfter = nodeText.substring(anchorOffset);
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
anchorNode.setTextContent(newText);
// 设置光标位置到插入文本之后
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
selection.anchor.offset = newOffset;
selection.focus.offset = newOffset;
}
});
} else {
// 普通模式下使用VariableNode
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
}
setShowSuggestions(false);
};

View File

@@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
interface InitialValuePluginProps {
value: string;
options?: Suggestion[];
enableJinja2?: boolean;
}
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
const [editor] = useLexicalComposerContext();
const initializedRef = useRef(false);
const prevValueRef = useRef<string>('');
const isUserInputRef = useRef(false);
useEffect(() => {
if (!initializedRef.current && value) {
// 监听编辑器变化,标记是否为用户输入
const removeListener = editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const root = $getRoot();
const textContent = root.getTextContent();
if (textContent !== prevValueRef.current) {
isUserInputRef.current = true;
}
});
});
return removeListener;
}, [editor]);
useEffect(() => {
if (value !== prevValueRef.current && !isUserInputRef.current) {
editor.update(() => {
const root = $getRoot();
root.clear();
@@ -28,7 +45,11 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
const contextMatch = part.match(/^\{\{context\}\}$/);
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
// 匹配{{context}}格式
if (enableJinja2) {
paragraph.append($createTextNode(part));
return;
}
if (contextMatch) {
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
if (contextSuggestion) {
@@ -39,7 +60,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
return
}
// 匹配{{conv.xx}}格式
if (conversationMatch) {
const [_, variableName] = conversationMatch;
const conversationSuggestion = options.find(s =>
@@ -53,7 +73,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
return
}
// 匹配普通变量{{nodeId.label}}格式
if (match) {
const [_, nodeId, label] = match;
@@ -75,11 +94,12 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
});
root.append(paragraph);
});
initializedRef.current = true;
}, { discrete: true });
}
}, [options]);
prevValueRef.current = value;
isUserInputRef.current = false;
}, [value, options, editor, enableJinja2]);
return null;
};

View File

@@ -0,0 +1,109 @@
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;