fix(web): Editor

This commit is contained in:
zhaoying
2025-12-31 12:15:33 +08:00
parent d476d92b7d
commit 4c706048de
3 changed files with 94 additions and 86 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState, type FC } from 'react'; import { useEffect, useState, type FC } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $getSelection } from 'lexical'; import { $getSelection, $isRangeSelection } from 'lexical';
import { INSERT_VARIABLE_COMMAND } from '../commands'; import { INSERT_VARIABLE_COMMAND } from '../commands';
import type { NodeProperties } from '../../../types' import type { NodeProperties } from '../../../types'
@@ -23,43 +23,55 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
useEffect(() => { useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => { return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => { editorState.read(() => {
const root = $getRoot(); const selection = $getSelection();
const text = root.getTextContent();
const shouldShow = text.includes('/'); if (!selection || !$isRangeSelection(selection)) {
setShowSuggestions(false);
return;
}
const anchorNode = selection.anchor.getNode();
const anchorOffset = selection.anchor.offset;
// Get the text content of the current node
const nodeText = anchorNode.getTextContent();
// Check if we have a '/' at the current position or after line break
const textBeforeCursor = nodeText.substring(0, anchorOffset);
const shouldShow = textBeforeCursor.endsWith('/') ||
(textBeforeCursor === '/' && anchorOffset === 1);
setShowSuggestions(shouldShow); setShowSuggestions(shouldShow);
if (shouldShow) { if (shouldShow) {
const selection = $getSelection(); const domSelection = window.getSelection();
if (selection) { if (domSelection && domSelection.rangeCount > 0) {
const domSelection = window.getSelection(); const range = domSelection.getRangeAt(0);
if (domSelection && domSelection.rangeCount > 0) { const rect = range.getBoundingClientRect();
const range = domSelection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const popupWidth = 280; const popupWidth = 280;
const popupHeight = 200; const popupHeight = 200;
const viewportWidth = window.innerWidth; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
let left = rect.left; let left = rect.left;
let top = rect.top - 10; let top = rect.top - 10;
if (left + popupWidth > viewportWidth) { if (left + popupWidth > viewportWidth) {
left = viewportWidth - popupWidth - 10; left = viewportWidth - popupWidth - 10;
}
if (left < 10) {
left = 10;
}
if (top - popupHeight < 10) {
top = rect.bottom + 10;
if (top + popupHeight > viewportHeight) {
top = viewportHeight - popupHeight - 10;
}
}
setPopupPosition({ top, left });
} }
if (left < 10) {
left = 10;
}
if (top - popupHeight < 10) {
top = rect.bottom + 10;
if (top + popupHeight > viewportHeight) {
top = viewportHeight - popupHeight - 10;
}
}
setPopupPosition({ top, left });
} }
} }
}); });
@@ -112,7 +124,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}> <div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
{nodeName} {nodeName}
</div> </div>
{nodeOptions.map((option, index) => { {nodeOptions.map((option) => {
const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option); const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option);
return ( return (
<div <div

View File

@@ -4,10 +4,11 @@ import {
$createParagraphNode, $createParagraphNode,
$createTextNode, $createTextNode,
$getRoot, $getRoot,
$getSelection,
$setSelection, $setSelection,
$createRangeSelection, $createRangeSelection,
$isParagraphNode,
$isTextNode, $isTextNode,
$isRangeSelection,
} from 'lexical'; } from 'lexical';
import { $createVariableNode } from '../nodes/VariableNode'; import { $createVariableNode } from '../nodes/VariableNode';
@@ -26,64 +27,45 @@ const CommandPlugin = () => {
INSERT_VARIABLE_COMMAND, INSERT_VARIABLE_COMMAND,
(payload: InsertVariableCommandPayload) => { (payload: InsertVariableCommandPayload) => {
editor.update(() => { editor.update(() => {
const root = $getRoot(); const selection = $getSelection();
const text = root.getTextContent(); if (!selection || !$isRangeSelection(selection)) return;
const lastSlashIndex = text.lastIndexOf('/');
// Find the paragraph and the position to insert const anchorNode = selection.anchor.getNode();
const paragraph = root.getFirstChild(); const anchorOffset = selection.anchor.offset;
if (!paragraph || !$isParagraphNode(paragraph)) return;
const children = paragraph.getChildren(); if ($isTextNode(anchorNode)) {
let insertPosition = 0; const nodeText = anchorNode.getTextContent();
let currentTextLength = 0; const textBeforeCursor = nodeText.substring(0, anchorOffset);
const textAfterCursor = nodeText.substring(anchorOffset);
// Find where to insert the new tag
for (let i = 0; i < children.length; i++) {
const child = children[i];
const childText = child.getTextContent();
if (currentTextLength + childText.length > lastSlashIndex) { // Find the last '/' position
// Split this text node if needed const lastSlashIndex = textBeforeCursor.lastIndexOf('/');
if ($isTextNode(child)) {
const beforeSlash = childText.substring(0, lastSlashIndex - currentTextLength); if (lastSlashIndex !== -1) {
const afterSlash = childText.substring(lastSlashIndex - currentTextLength + 1); // Split the text: before slash, insert variable, after cursor
const beforeSlash = textBeforeCursor.substring(0, lastSlashIndex);
if (beforeSlash) {
child.setTextContent(beforeSlash); // Update the current text node with text before slash
insertPosition = i + 1; anchorNode.setTextContent(beforeSlash);
} else {
insertPosition = i; // Create and insert the variable node
child.remove(); const tagNode = $createVariableNode(payload.data);
} const spaceNode = $createTextNode(' ');
// Insert tag and space anchorNode.insertAfter(tagNode);
const tagNode = $createVariableNode(payload.data); tagNode.insertAfter(spaceNode);
const spaceNode = $createTextNode(' ');
// Add remaining text if any
if (insertPosition < paragraph.getChildrenSize()) { if (textAfterCursor) {
paragraph.getChildAtIndex(insertPosition)?.insertBefore(tagNode); spaceNode.insertAfter($createTextNode(textAfterCursor));
tagNode.insertAfter(spaceNode);
} else {
paragraph.append(tagNode);
paragraph.append(spaceNode);
}
if (afterSlash) {
spaceNode.insertAfter($createTextNode(afterSlash));
}
// Set cursor after space
const selection = $createRangeSelection();
selection.anchor.set(spaceNode.getKey(), 1, 'text');
selection.focus.set(spaceNode.getKey(), 1, 'text');
$setSelection(selection);
} }
break;
// Set cursor after space
const newSelection = $createRangeSelection();
newSelection.anchor.set(spaceNode.getKey(), 1, 'text');
newSelection.focus.set(spaceNode.getKey(), 1, 'text');
$setSelection(newSelection);
} }
currentTextLength += childText.length;
insertPosition = i + 1;
} }
}); });
return true; return true;

View File

@@ -237,6 +237,20 @@ const Properties: FC<PropertiesProps> = ({
}); });
} }
break break
case 'knowledge-retrieval':
const knowledgeKey = `${nodeId}_message`;
if (!addedKeys.has(knowledgeKey)) {
addedKeys.add(knowledgeKey);
variableList.push({
key: knowledgeKey,
label: 'message',
type: 'variable',
dataType: 'String',
value: `${nodeId}.message`,
nodeData: nodeData,
});
}
break
} }
}); });