mirror of
https://github.com/labring/FastGPT.git
synced 2026-04-28 02:00:47 +08:00
76d6234de6
* Agent features (#6345) * Test agent (#6220) * squash: compress all commits into one * feat: plan response in ui * response ui * perf: agent config * merge * tool select ux * perf: chat ui * perf: agent editform * tmp code * feat: save chat * Complete agent parent (#6049) * add role and tools filling * add: file-upload --------- Co-authored-by: xxyyh <2289112474@qq> * perf: top agent code * top agent (#6062) Co-authored-by: xxyyh <2289112474@qq> * fix: ts * skill editor ui * ui * perf: rewrite type with zod * skill edit ui * skill agent (#6089) * cp skill chat * rebasefdf933dand add skill chat * 1. skill 的 CRUD 2. skill 的信息渲染到前端界面 * solve comment * remove chatid and chatItemId * skill match * perf: skill manage * fix: ts --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: ts * fix: loop import * skill tool config (#6114) Co-authored-by: xxyyh <2289112474@qq> * feat: load tool in agent * skill memory (#6126) Co-authored-by: xxyyh <2289112474@qq> * perf: agent skill editor * perf: helperbot ui * agent code * perf: context * fix: request context * agent usage * perf: agent context and pause * perf: plan response * Test agent sigle skill (#6184) * feat:top box fill * prompt fix --------- Co-authored-by: xxyyh <2289112474@qq> * perf: agent chat ui * Test agent new (#6219) * have-replan * agent --------- Co-authored-by: xxyyh <2289112474@qq> * fix: ts --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * feat: consolidate agent and MCP improvements This commit consolidates 17 commits including: - MCP tools enhancements and fixes - Agent system improvements and optimizations - Auth limit and prompt updates - Tool response compression and error tracking - Simple app adaptation - Code quality improvements (TypeScript, ESLint, Zod) - Version type migration to schema - Remove deprecated useRequest2 - Add LLM error tracking - Toolset ID validation fixes --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * fix: transform avatar copy;perf: filter invalid tool * update llm response storage time * fix: openapi schema * update skill desc * feat: cache hit data * i18n * lock * chat logs support error filter & user search (#6373) * chat log support searching by user name * support error filter * fix * fix overflow * optimize * fix init script * fix * perf: get log users * updat ecomment * fix: ts * fix: test --------- Co-authored-by: archer <545436317@qq.com> * Fix: agent (#6376) * Agent features (#6345) * Test agent (#6220) * squash: compress all commits into one * feat: plan response in ui * response ui * perf: agent config * merge * tool select ux * perf: chat ui * perf: agent editform * tmp code * feat: save chat * Complete agent parent (#6049) * add role and tools filling * add: file-upload --------- Co-authored-by: xxyyh <2289112474@qq> * perf: top agent code * top agent (#6062) Co-authored-by: xxyyh <2289112474@qq> * fix: ts * skill editor ui * ui * perf: rewrite type with zod * skill edit ui * skill agent (#6089) * cp skill chat * rebasefdf933dand add skill chat * 1. skill 的 CRUD 2. skill 的信息渲染到前端界面 * solve comment * remove chatid and chatItemId * skill match * perf: skill manage * fix: ts --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: ts * fix: loop import * skill tool config (#6114) Co-authored-by: xxyyh <2289112474@qq> * feat: load tool in agent * skill memory (#6126) Co-authored-by: xxyyh <2289112474@qq> * perf: agent skill editor * perf: helperbot ui * agent code * perf: context * fix: request context * agent usage * perf: agent context and pause * perf: plan response * Test agent sigle skill (#6184) * feat:top box fill * prompt fix --------- Co-authored-by: xxyyh <2289112474@qq> * perf: agent chat ui * Test agent new (#6219) * have-replan * agent --------- Co-authored-by: xxyyh <2289112474@qq> * fix: ts --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * feat: consolidate agent and MCP improvements This commit consolidates 17 commits including: - MCP tools enhancements and fixes - Agent system improvements and optimizations - Auth limit and prompt updates - Tool response compression and error tracking - Simple app adaptation - Code quality improvements (TypeScript, ESLint, Zod) - Version type migration to schema - Remove deprecated useRequest2 - Add LLM error tracking - Toolset ID validation fixes --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> * 1. 把辅助生成前端上的 system prompt 加入到上下文中 2. mcp工具的前端渲染(图标) 3. 文件读取工具和文件上传进行关联 4. 添加了辅助生成返回格式出错的重试方案 5. ask 不出现在 plan 步骤中 6. 添加了辅助生成的头像和交互 UI * fix:read_file * helperbot ui * ts error * helper ui * delete Unused import * perf: helper bot * lock --------- Co-authored-by: Archer <545436317@qq.com> Co-authored-by: xxyyh <2289112474@qq> * fix date variable required & model auth (#6386) * fix date variable required & model auth * doc * feat: add chat id to finish callback * fix: iphone safari shareId (#6387) * fix: iphone safari shareId * fix: mcp file list can't setting * fix: reason output field * fix: skip JSON validation for HTTP tool body with variable (#6392) * fix: skip JSON validation for HTTP tool body with variable * doc * workflow fitview * perf: selecting memory * perf: cp api * ui * perf: toolcall auto adapt * fix: catch workflow error * fix: ts * perf: pagination type * remove * ignore * update doc * fix: simple app tool select * add default avatar to logs user * perf: loading user * select dataset ui * rename version * feat: add global/common test * perf: packages/global/common test * feat: package/global/ai,app test * add global/chat test * global/core test * global/core test * feat: packages/global all test * perf: test * add server api test * perf: init shell * perf: init4150 shell * remove invalid code * update doc * remove log * fix: chat effect * fix: plan fake tool (#6398) * 1. 提示词防注入功能 2. 无工具不进入 plan,防止虚拟工具生成 * Agent-dataset * dataset * dataset presetInfo * prefix * perf: prompt --------- Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: archer <545436317@qq.com> * fix: review * adapt kimi2.5 think toolcall * feat: invoke fastgpt user info (#6403) feat: invoke fastgpt user info * fix: invoke fastgpt user info return orgs (#6404) * skill and version * retry helperbot (#6405) Co-authored-by: xxyyh <2289112474@qq> * update template * remove log * doc * update doc * doc * perf: internal ip check * adapt get paginationRecords * tool call adapt * fix: test * doc * fix: agent initial version * adapt completions v1 * feat: instrumentation check * rename skill * add workflow demo mode tracks (#6407) * chore: 统一 skills 目录命名为小写 将 .claude/Skills/ 重命名为 .claude/skills/ 以保持命名一致性。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add workflow demo mode tracks * code * optimize * fix: improve workflowDemoTrack based on PR review - Add comment to empty catch block for maintainability - Add @param docs to onDemoChange clarifying nodeCount usage - Replace silent .catch with console.debug for dev debugging - Handle appId changes by reporting old data before re-init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: archer <545436317@qq.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * remove repeat skill * fix(workflow): filter out orphan edges to prevent runtime errors (#6399) * fix(workflow): filter out orphan edges to prevent runtime errors Runtime edges that reference non-existent nodes (orphan edges) can cause unexpected behavior or crashes during workflow dispatch. This change adds a pre-check to filter out such edges before execution begins, ensuring system stability even with inconsistent graph data. * fix(workflow): enhance orphan edge filtering with logging and tests - Refactor: Extract logic to 'filterOrphanEdges' in utils.ts for better reusability - Feat: Add performance monitoring (warn if >100ms) and comprehensive logging - Feat: Support detailed edge inspection in debug mode - Docs: Add JSDoc explaining causes of orphan edges (migration, manual edits) - Test: Add unit tests covering edge cases and performance (1000 edges) Addresses PR review feedback regarding logging, variable naming, and testing." * move code * move code * add more unit test --------- Co-authored-by: archer <545436317@qq.com> * test * perf: test * add server/common/string test * fix: resolve $ref references in MCP tool input schemas (#6395) (#6409) * fix: resolve $ref references in MCP tool input schemas (#6395) * add test code --------- Co-authored-by: archer <545436317@qq.com> * chore(docs): add fastgpt, fastgpt-plugin version choice guide (#6411) * chore(doc): add fastgpt version description * doc * doc --------- Co-authored-by: archer <545436317@qq.com> * fix:dataset cite and description info (#6410) * 1. 添加知识库引用(plan 步骤和直接知识库调用) 2. 提示词框中的@知识库工具 3. plan 中 step 的 description dataset_search 改为中文 * fix: i18n * prompt * prompt --------- Co-authored-by: xxyyh <2289112474@qq> * fix: tool call * perf: workflow props * fix: merge ECharts toolbox options instead of overwriting (#6269) (#6412) * feat: integrate logtape and otel (#6400) * fix: deps * feat(logger): integrate logtape and otel * wip(log): add basic infras logs * wip(log): add request id and inject it into context * wip(log): add basic tx logs * wip(log): migrate * wip(log): category * wip(log): more sub category * fix: type * fix: sessionRun * fix: export getLogger from client.ts * chore: improve logs * docs: update signoz and changelog * change type * fix: ts * remove skill.md * fix: lockfile specifier * fix: test --------- Co-authored-by: archer <545436317@qq.com> * init log * doc * remove invalid log * fix: review * template * replace new log * fix: ts * remove log * chore: migrate all addLog to logtape * move skill * chore: migrate all addLog to logtape (#6417) * update skill * remove log * fix: tool check --------- Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com> Co-authored-by: xxyyh <2289112474@qq> Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: xuyafei1996 <54217479+xuyafei1996@users.noreply.github.com> Co-authored-by: ToukoYui <2331631097@qq.com> Co-authored-by: roy <whoeverimf5@gmail.com>
582 lines
16 KiB
TypeScript
582 lines
16 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
|
|
import type { EntityMatch } from '@lexical/text';
|
|
import { $createTextNode, $isTextNode, TextNode } from 'lexical';
|
|
import { useCallback } from 'react';
|
|
import type { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
|
|
import type { VariableNode } from './plugins/VariablePlugin/node';
|
|
import type { SkillNode } from './plugins/SkillLabelPlugin/node';
|
|
import type {
|
|
ListItemEditorNode,
|
|
ListEditorNode,
|
|
ParagraphEditorNode,
|
|
EditorState,
|
|
ListItemInfo,
|
|
ChildEditorNode
|
|
} from './type';
|
|
import { TabStr } from './constants';
|
|
|
|
export function registerLexicalTextEntity<
|
|
T extends TextNode | VariableLabelNode | VariableNode | SkillNode
|
|
>(
|
|
editor: LexicalEditor,
|
|
getMatch: (text: string) => null | EntityMatch,
|
|
targetNode: Klass<T>,
|
|
createNode: (textNode: TextNode) => T
|
|
): Array<() => void> {
|
|
const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
|
|
return node instanceof targetNode;
|
|
};
|
|
|
|
const replaceWithSimpleText = (
|
|
node: TextNode | VariableLabelNode | VariableNode | SkillNode
|
|
): void => {
|
|
const textNode = $createTextNode(node.getTextContent());
|
|
textNode.setFormat(node.getFormat());
|
|
node.replace(textNode);
|
|
};
|
|
|
|
const getMode = (node: TextNode): number => {
|
|
return node.getLatest().__mode;
|
|
};
|
|
|
|
const textNodeTransform = (node: TextNode) => {
|
|
if (!node.isSimpleText()) {
|
|
return;
|
|
}
|
|
|
|
const prevSibling = node.getPreviousSibling();
|
|
let text = node.getTextContent();
|
|
let currentNode = node;
|
|
let match;
|
|
|
|
if ($isTextNode(prevSibling)) {
|
|
const previousText = prevSibling.getTextContent();
|
|
const combinedText = previousText + text;
|
|
const prevMatch = getMatch(combinedText);
|
|
|
|
if (isTargetNode(prevSibling)) {
|
|
if (prevMatch === null || getMode(prevSibling) !== 0) {
|
|
replaceWithSimpleText(prevSibling);
|
|
|
|
return;
|
|
} else {
|
|
const diff = prevMatch.end - previousText.length;
|
|
|
|
if (diff > 0) {
|
|
const concatText = text.slice(0, diff);
|
|
const newTextContent = previousText + concatText;
|
|
prevSibling.select();
|
|
prevSibling.setTextContent(newTextContent);
|
|
|
|
if (diff === text.length) {
|
|
node.remove();
|
|
} else {
|
|
const remainingText = text.slice(diff);
|
|
node.setTextContent(remainingText);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
} else if (prevMatch === null || prevMatch.start < previousText.length) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
match = getMatch(text);
|
|
let nextText = match === null ? '' : text.slice(match.end);
|
|
text = nextText;
|
|
|
|
if (nextText === '') {
|
|
const nextSibling = currentNode.getNextSibling();
|
|
|
|
if ($isTextNode(nextSibling)) {
|
|
nextText = currentNode.getTextContent() + nextSibling.getTextContent();
|
|
const nextMatch = getMatch(nextText);
|
|
|
|
if (nextMatch === null) {
|
|
if (isTargetNode(nextSibling)) {
|
|
replaceWithSimpleText(nextSibling);
|
|
} else {
|
|
nextSibling.markDirty();
|
|
}
|
|
|
|
return;
|
|
} else if (nextMatch.start !== 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match === null) {
|
|
return;
|
|
}
|
|
|
|
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) {
|
|
continue;
|
|
}
|
|
|
|
let nodeToReplace;
|
|
|
|
if (match.start === 0) {
|
|
[nodeToReplace, currentNode] = currentNode.splitText(match.end);
|
|
} else {
|
|
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end);
|
|
}
|
|
|
|
const replacementNode = createNode(nodeToReplace);
|
|
replacementNode.setFormat(nodeToReplace.getFormat());
|
|
nodeToReplace.replace(replacementNode);
|
|
|
|
if (currentNode == null) {
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
const reverseNodeTransform = (node: T) => {
|
|
const text = node.getTextContent();
|
|
const match = getMatch(text);
|
|
|
|
if (match === null || match.start !== 0) {
|
|
replaceWithSimpleText(node);
|
|
|
|
return;
|
|
}
|
|
|
|
if (text.length > match.end && $isTextNode(node)) {
|
|
// This will split out the rest of the text as simple text
|
|
node.splitText(match.end);
|
|
|
|
return;
|
|
}
|
|
|
|
const prevSibling = node.getPreviousSibling();
|
|
|
|
if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
|
|
replaceWithSimpleText(prevSibling);
|
|
replaceWithSimpleText(node);
|
|
}
|
|
|
|
const nextSibling = node.getNextSibling();
|
|
|
|
if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
|
|
replaceWithSimpleText(nextSibling);
|
|
|
|
// This may have already been converted in the previous block
|
|
if (isTargetNode(node)) {
|
|
replaceWithSimpleText(node);
|
|
}
|
|
}
|
|
};
|
|
|
|
const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform);
|
|
const removeReverseNodeTransform = editor.registerNodeTransform<any>(
|
|
targetNode,
|
|
reverseNodeTransform
|
|
);
|
|
|
|
return [removePlainTextTransform, removeReverseNodeTransform];
|
|
}
|
|
|
|
// text to editor state
|
|
const parseTextLine = (line: string) => {
|
|
const trimmed = line.trimStart();
|
|
const leadingSpaces = line.length - trimmed.length;
|
|
const indentLevel = Math.floor(leadingSpaces / TabStr.length);
|
|
|
|
const bulletMatch = trimmed.match(/^- (.*)$/);
|
|
if (bulletMatch) {
|
|
return { type: 'bullet', text: bulletMatch[1], indent: indentLevel };
|
|
}
|
|
|
|
const numberMatch = trimmed.match(/^(\d+)\. (.*)$/);
|
|
if (numberMatch) {
|
|
return {
|
|
type: 'number',
|
|
text: numberMatch[2],
|
|
indent: indentLevel,
|
|
numberValue: parseInt(numberMatch[1])
|
|
};
|
|
}
|
|
|
|
// For paragraphs, preserve original leading spaces in text (don't use indent)
|
|
return { type: 'paragraph', text: line, indent: 0 };
|
|
};
|
|
|
|
const buildListStructure = (items: ListItemInfo[]) => {
|
|
const result: ListEditorNode[] = [];
|
|
|
|
let i = 0;
|
|
while (i < items.length) {
|
|
const currentListType = items[i].type;
|
|
const currentIndent = items[i].indent;
|
|
const currentListItems: ListItemEditorNode[] = [];
|
|
|
|
// Collect consecutive items of the same type
|
|
while (i < items.length && items[i].type === currentListType) {
|
|
const listItem: ListItemEditorNode = {
|
|
children: [
|
|
{
|
|
detail: 0,
|
|
format: 0,
|
|
mode: 'normal',
|
|
style: '',
|
|
text: items[i].text,
|
|
type: 'text' as const,
|
|
version: 1
|
|
}
|
|
],
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'listitem' as const,
|
|
version: 1,
|
|
value: items[i].numberValue || 1
|
|
};
|
|
|
|
// Collect nested items
|
|
const nestedItems: ListItemInfo[] = [];
|
|
let j = i + 1;
|
|
while (j < items.length && items[j].indent > currentIndent) {
|
|
nestedItems.push(items[j]);
|
|
j++;
|
|
}
|
|
|
|
// recursively build nested lists and add them to the current item's children
|
|
if (nestedItems.length > 0) {
|
|
const nestedLists = buildListStructure(nestedItems);
|
|
listItem.children.push(...nestedLists);
|
|
}
|
|
|
|
currentListItems.push(listItem);
|
|
i = j;
|
|
}
|
|
|
|
result.push({
|
|
children: currentListItems,
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'list' as const,
|
|
version: 1,
|
|
listType: currentListType,
|
|
start: 1,
|
|
tag: currentListType === 'bullet' ? 'ul' : ('ol' as const)
|
|
});
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
export const textToEditorState = (text = '', isRichText = false) => {
|
|
const lines = text.split('\n');
|
|
const children: Array<ParagraphEditorNode | ListEditorNode> = [];
|
|
|
|
if (!isRichText) {
|
|
return JSON.stringify({
|
|
root: {
|
|
children: lines.map((p) => {
|
|
return {
|
|
children: [
|
|
{
|
|
detail: 0,
|
|
format: 0,
|
|
mode: 'normal',
|
|
style: '',
|
|
text: p,
|
|
type: 'text',
|
|
version: 1
|
|
}
|
|
],
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'paragraph',
|
|
version: 1
|
|
};
|
|
}),
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'root',
|
|
version: 1
|
|
}
|
|
});
|
|
}
|
|
|
|
let i = 0;
|
|
while (i < lines.length) {
|
|
const parsed = parseTextLine(lines[i]);
|
|
|
|
if (parsed.type === 'paragraph') {
|
|
children.push({
|
|
children: [
|
|
{
|
|
detail: 0,
|
|
format: 0,
|
|
mode: 'normal',
|
|
style: '',
|
|
text: parsed.text,
|
|
type: 'text',
|
|
version: 1
|
|
}
|
|
],
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0, // Always use 0 for paragraphs, spaces are in text content
|
|
type: 'paragraph',
|
|
version: 1
|
|
});
|
|
i++;
|
|
} else {
|
|
const listItems: ListItemInfo[] = [];
|
|
|
|
while (i < lines.length) {
|
|
const currentParsed = parseTextLine(lines[i]);
|
|
if (currentParsed.type === 'paragraph') {
|
|
break;
|
|
}
|
|
listItems.push({
|
|
type: currentParsed.type as 'bullet' | 'number',
|
|
text: currentParsed.text,
|
|
indent: currentParsed.indent,
|
|
numberValue: currentParsed.numberValue
|
|
});
|
|
i++;
|
|
}
|
|
|
|
// build nested lists and add to children
|
|
const lists = buildListStructure(listItems) as ListEditorNode[];
|
|
children.push(...lists);
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({
|
|
root: {
|
|
children: children,
|
|
direction: 'ltr',
|
|
format: '',
|
|
indent: 0,
|
|
type: 'root',
|
|
version: 1
|
|
}
|
|
});
|
|
};
|
|
|
|
// menu text match
|
|
export type MenuTextMatch = {
|
|
leadOffset: number;
|
|
matchingString: string;
|
|
replaceableString: string;
|
|
};
|
|
export type TriggerFn = (text: string, editor: LexicalEditor) => MenuTextMatch | null;
|
|
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;';
|
|
export function useBasicTypeaheadTriggerMatch(
|
|
trigger: string,
|
|
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }
|
|
): TriggerFn {
|
|
return useCallback(
|
|
(text: string) => {
|
|
const validChars = `[^${trigger}${PUNCTUATION}\\s]`;
|
|
const TypeaheadTriggerRegex = new RegExp(
|
|
`([^${trigger}]|^)(` + `[${trigger}]` + `((?:${validChars}){0,${maxLength}})` + ')$'
|
|
);
|
|
const match = TypeaheadTriggerRegex.exec(text);
|
|
if (match !== null) {
|
|
const maybeLeadingWhitespace = match[1];
|
|
const matchingString = match[3];
|
|
if (matchingString.length >= minLength) {
|
|
return {
|
|
leadOffset: match.index + maybeLeadingWhitespace.length,
|
|
matchingString,
|
|
replaceableString: match[2]
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
[maxLength, minLength, trigger]
|
|
);
|
|
}
|
|
|
|
// editor state to text
|
|
const processListItem = ({
|
|
listItem,
|
|
listType,
|
|
index,
|
|
indentLevel
|
|
}: {
|
|
listItem: ListItemEditorNode;
|
|
listType: 'bullet' | 'number';
|
|
index: number;
|
|
indentLevel: number;
|
|
}) => {
|
|
const results = [];
|
|
|
|
const itemText: string[] = [];
|
|
const nestedLists: ListEditorNode[] = [];
|
|
|
|
// Separate text and nested lists
|
|
listItem.children.forEach((child) => {
|
|
if (child.type === 'linebreak') {
|
|
itemText.push('\n');
|
|
} else if (child.type === 'text') {
|
|
itemText.push(child.text);
|
|
} else if (child.type === 'tab') {
|
|
itemText.push(TabStr);
|
|
} else if (child.type === 'variableLabel' || child.type === 'Variable') {
|
|
itemText.push(child.variableKey);
|
|
} else if (child.type === 'skill') {
|
|
itemText.push(`{{@${child.id}@}}`);
|
|
} else if (child.type === 'list') {
|
|
nestedLists.push(child);
|
|
}
|
|
});
|
|
|
|
// Add prefix and indent (using TabStr for consistency)
|
|
const itemTextString = itemText.join('');
|
|
const indent = TabStr.repeat(indentLevel);
|
|
const prefix = listType === 'bullet' ? '- ' : `${index + 1}. `;
|
|
results.push(indent + prefix + itemTextString);
|
|
|
|
// Handle nested lists
|
|
nestedLists.forEach((nestedList) => {
|
|
const nestedResults = processList({
|
|
list: nestedList,
|
|
indentLevel: indentLevel + 1
|
|
});
|
|
results.push(...nestedResults);
|
|
});
|
|
|
|
return results;
|
|
};
|
|
const processList = ({ list, indentLevel = 0 }: { list: ListEditorNode; indentLevel?: number }) => {
|
|
const results: string[] = [];
|
|
|
|
list.children.forEach((listItem, index: number) => {
|
|
if (listItem.type === 'listitem') {
|
|
const itemResults = processListItem({
|
|
listItem,
|
|
listType: list.listType,
|
|
index,
|
|
indentLevel
|
|
});
|
|
results.push(...itemResults);
|
|
}
|
|
});
|
|
|
|
return results;
|
|
};
|
|
export const editorStateToText = (editor: LexicalEditor) => {
|
|
const editorStateTextString: string[] = [];
|
|
const editorState = editor.getEditorState().toJSON() as EditorState;
|
|
const paragraphs = editorState.root.children;
|
|
|
|
const extractText = (node: ChildEditorNode): string => {
|
|
if (!node) return '';
|
|
|
|
// Handle line break nodes
|
|
if (node.type === 'linebreak') {
|
|
return '\n';
|
|
}
|
|
|
|
// Handle tab nodes
|
|
if (node.type === 'tab') {
|
|
return TabStr;
|
|
}
|
|
|
|
// Handle text nodes
|
|
if (node.type === 'text') {
|
|
return node.text || '';
|
|
}
|
|
|
|
// Handle custom variable nodes
|
|
if (node.type === 'variableLabel' || node.type === 'Variable') {
|
|
return node.variableKey || '';
|
|
}
|
|
|
|
// Handle skill nodes
|
|
if (node.type === 'skill') {
|
|
return `{{@${node.id}@}}`;
|
|
}
|
|
|
|
// Handle paragraph nodes - recursively process children
|
|
if (node.type === 'paragraph') {
|
|
if (!node.children || node.children.length === 0) {
|
|
return '';
|
|
}
|
|
return node.children.map(extractText).join('');
|
|
}
|
|
|
|
// Handle list item nodes - recursively process children (excluding nested lists)
|
|
if (node.type === 'listitem') {
|
|
if (!node.children || node.children.length === 0) {
|
|
return '';
|
|
}
|
|
// Filter out nested list nodes as they are handled separately
|
|
return node.children
|
|
.filter((child) => child.type !== 'list')
|
|
.map(extractText)
|
|
.join('');
|
|
}
|
|
|
|
// Handle list nodes - recursively process children
|
|
if (node.type === 'list') {
|
|
if (!node.children || node.children.length === 0) {
|
|
return '';
|
|
}
|
|
return node.children.map(extractText).join('');
|
|
}
|
|
|
|
// Unknown node type - return the raw text content if available
|
|
console.warn('Unknown node type in extractText:', (node as any).type, node);
|
|
|
|
// Try to extract text content from unknown node types
|
|
if ('text' in node && typeof (node as any).text === 'string') {
|
|
return (node as any).text;
|
|
}
|
|
|
|
// Try to recursively extract from children if present
|
|
if ('children' in node && Array.isArray((node as any).children)) {
|
|
return (node as any).children.map(extractText).join('');
|
|
}
|
|
|
|
// Fallback to stringifying the node content
|
|
return JSON.stringify(node);
|
|
};
|
|
|
|
paragraphs.forEach((paragraph) => {
|
|
if (paragraph.type === 'list') {
|
|
const listResults = processList({ list: paragraph });
|
|
editorStateTextString.push(...listResults);
|
|
} else if (paragraph.type === 'paragraph') {
|
|
const children = paragraph.children;
|
|
const paragraphText: string[] = [];
|
|
|
|
// Don't add indent prefix for paragraphs, spaces are already in text content
|
|
children.forEach((child) => {
|
|
const val = extractText(child);
|
|
paragraphText.push(val);
|
|
});
|
|
|
|
const finalText = paragraphText.join('');
|
|
editorStateTextString.push(finalText);
|
|
} else {
|
|
const text = extractText(paragraph);
|
|
editorStateTextString.push(text);
|
|
}
|
|
});
|
|
return editorStateTextString.join('\n');
|
|
};
|