Files
FastGPT/packages/web/components/common/Textarea/PromptEditor/utils.ts
T
Archer 76d6234de6 V4.14.7 features (#6406)
* 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

* rebase fdf933d
 and 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

* rebase fdf933d
 and 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>
2026-02-12 16:37:50 +08:00

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');
};