From 0083ad0debc12e0b621dd4fd0a8d98e9cb8bc9b9 Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 9 Jul 2026 17:49:16 +0800 Subject: [PATCH] Feat: Add a data compilation layer. (#16777) ### Summary Feat: Add a data compilation layer. --- .gitignore | 1 + web/CLAUDE.md | 10 +- web/package-lock.json | 1167 ++++++++++++++--- web/package.json | 20 +- .../components/artifact-force-graph/index.tsx | 93 ++ .../components/artifact-force-graph/types.ts | 12 + .../use-container-dimensions.ts | 25 + .../components/artifact-force-graph/utils.ts | 26 + .../components/chunk-method-dialog/index.tsx | 6 + .../use-default-parser-values.ts | 1 + .../compilation-template-form-field.tsx | 117 ++ .../document-preview/document-header.tsx | 35 +- .../components/expandable-search-input.tsx | 87 ++ web/src/components/json-edit/index.tsx | 21 +- web/src/components/json-edit/interface.ts | 3 + web/src/components/llm-select/llm-label.tsx | 2 +- web/src/components/markdown-editor.tsx | 97 ++ web/src/components/model-tree-select.tsx | 8 +- .../originui/select-with-search.tsx | 4 +- .../graph-rag-form-fields.tsx | 8 +- .../raptor-form-fields.tsx | 4 +- web/src/components/ui/tree-view.tsx | 2 + web/src/constants/agent.tsx | 11 +- web/src/constants/compilation.ts | 14 + web/src/hooks/logic-hooks.ts | 8 +- web/src/hooks/logic-hooks/navigate-hooks.ts | 25 + web/src/hooks/use-chunk-request.ts | 10 +- .../use-compilation-template-group-request.ts | 255 ++++ .../hooks/use-compilation-template-request.ts | 304 +++++ web/src/hooks/use-dataset-skill-request.ts | 53 + web/src/hooks/use-document-request.ts | 105 ++ web/src/hooks/use-knowledge-request.ts | 383 +++++- .../database/compilation-template.ts | 74 ++ web/src/interfaces/database/dataset-skill.ts | 29 + web/src/interfaces/database/dataset.ts | 66 + .../interfaces/database/document-structure.ts | 32 + web/src/interfaces/database/document.ts | 1 + .../request/compilation-template.ts | 58 + web/src/interfaces/request/document.ts | 1 + web/src/interfaces/request/knowledge.ts | 31 + web/src/lib/editor/content-editable.tsx | 30 + web/src/lib/editor/editor-theme.css | 668 ++++++++++ web/src/lib/editor/editor-theme.ts | 93 ++ web/src/lib/editor/index.ts | 23 + web/src/lib/editor/lexical-editor.tsx | 198 +++ .../markdown/enhanced-markdown-export.ts | 481 +++++++ .../markdown/enhanced-markdown-import.ts | 166 +++ .../editor/markdown/horizontal-rule-node.ts | 85 ++ .../markdown/horizontal-rule-transformer.ts | 31 + web/src/lib/editor/markdown/index.ts | 75 ++ .../lib/editor/markdown/list-transformers.ts | 175 +++ .../editor/markdown/markdown-transformers.ts | 308 +++++ web/src/lib/editor/mermaid-node.tsx | 428 ++++++ web/src/lib/editor/mermaid-transformer.ts | 38 + web/src/lib/editor/nodes.ts | 33 + .../plugins/floating-selection-toolbar.tsx | 442 +++++++ .../editor/plugins/table-actions-plugin.tsx | 315 +++++ .../editor/plugins/table-row-size-guard.ts | 46 + .../lib/editor/plugins/table-transformer.ts | 228 ++++ web/src/lib/editor/plugins/toolbar-plugin.tsx | 183 +++ web/src/lib/editor/raw-markdown-editor.tsx | 148 +++ web/src/locales/en.ts | 148 +++ web/src/locales/zh.ts | 144 ++ web/src/pages/agent/canvas/index.tsx | 2 + .../agent/canvas/node/compilation-node.tsx | 15 + .../node/dropdown/accordion-operators.tsx | 2 + web/src/pages/agent/constant/index.tsx | 4 + web/src/pages/agent/constant/pipeline.tsx | 15 +- .../agent/form-sheet/form-config-map.tsx | 4 + .../agent/form/compilation-form/index.tsx | 47 + .../form/components/prompt-editor/index.tsx | 26 +- .../prompt-editor/variable-picker-plugin.tsx | 13 +- web/src/pages/agent/hooks/use-add-node.ts | 5 + web/src/pages/agent/operator-icon.tsx | 2 + web/src/pages/agent/utils/dsl-bridge.ts | 7 +- .../components/document-view-switch/index.tsx | 110 ++ .../components/knowledge-chunk/index.tsx | 39 +- .../components/representation-renderer.tsx | 167 +++ .../components/representation-select.tsx | 55 + .../timeline-x6-graph/hooks/use-x6-graph.ts | 88 ++ .../components/timeline-x6-graph/index.tsx | 24 + .../components/timeline-x6-graph/types.ts | 19 + .../hooks/use-selected-template.ts | 31 + web/src/pages/chunk/representation/index.tsx | 105 ++ .../chunk/representation/utils/adapters.ts | 251 ++++ .../pages/dataset/compilation/constants.ts | 10 + .../create-directory-form.tsx | 25 + .../create-directory-dialog/index.tsx | 51 + .../pages/dataset/compilation/empty-state.tsx | 132 ++ .../compilation/hooks/use-commit-artifact.ts | 80 ++ .../hooks/use-compilation-artifact.ts | 32 + .../hooks/use-compilation-skill.ts | 20 + .../compilation/hooks/use-compilation-view.ts | 67 + .../compilation/hooks/use-create-directory.ts | 59 + .../compilation/hooks/use-wiki-editor.ts | 41 + .../compilation/hooks/use-wiki-version.ts | 30 + web/src/pages/dataset/compilation/index.tsx | 194 +++ .../pages/dataset/compilation/interface.ts | 20 + .../compilation/knowledge-force-graph.tsx | 347 +++++ .../dataset/compilation/skills-left-panel.tsx | 127 ++ .../compilation/utils/parse-wiki-diff.ts | 53 + .../compilation/version-history-item.tsx | 51 + .../compilation/version-history-sheet.tsx | 86 ++ .../dataset/compilation/wiki-commit-modal.tsx | 56 + .../compilation/wiki-detail-content.tsx | 238 ++++ .../wiki-left-panel/hooks/use-wiki-clear.ts | 21 + .../hooks/use-wiki-navigation.ts | 118 ++ .../compilation/wiki-left-panel/index.tsx | 92 ++ .../wiki-left-panel/wiki-artifact-list.tsx | 48 + .../wiki-left-panel/wiki-list-item.tsx | 64 + .../wiki-left-panel/wiki-nav-bar.tsx | 171 +++ .../wiki-left-panel/wiki-topic-list.tsx | 45 + .../compilation/wiki-version-diff-panel.tsx | 58 + .../dataset-overview/dataset-common.ts | 4 + .../dataset-setting/configuration/naive.tsx | 2 + .../dataset/dataset-setting/form-schema.ts | 1 + .../pages/dataset/dataset-setting/index.tsx | 52 +- .../dataset/generate-button/constants.ts | 36 + .../generate-button/generate-log-button.tsx | 116 ++ .../dataset/generate-button/generate.tsx | 355 ----- .../dataset/dataset/generate-button/hook.ts | 148 ++- .../generate-button/use-generate-status.ts | 33 + web/src/pages/dataset/dataset/index.tsx | 39 - web/src/pages/dataset/sidebar/index.tsx | 21 +- .../components/add-field-modal.tsx | 108 ++ .../components/basic-info-step.tsx | 34 + .../components/blueprints-step.tsx | 160 +++ .../create-next/components/field-card.tsx | 54 + .../components/json-preview-sheet.tsx | 62 + .../components/section-field-grid.tsx | 104 ++ .../components/template-configuration.tsx | 255 ++++ .../components/template-preview-header.tsx | 43 + .../components/template-sidebar.tsx | 182 +++ .../components/workflow-preview-sheet.tsx | 56 + .../hooks/use-active-section-tab.ts | 13 + .../create-next/hooks/use-add-field-form.ts | 115 ++ .../hooks/use-available-kind-options.ts | 53 + .../create-next/hooks/use-builtin-template.ts | 35 + ...-create-next-compilation-template-group.ts | 66 + .../hooks/use-field-array-handlers.ts | 39 + .../create-next/hooks/use-field-modal.ts | 35 + .../hooks/use-template-add-button.ts | 29 + .../hooks/use-template-preview-sheets.ts | 28 + .../hooks/use-template-section-data.ts | 40 + .../create-next/index.tsx | 170 +++ .../components/fields-section.tsx | 174 +++ .../components/template-card.tsx | 213 +++ .../components/tree-template-fields.tsx | 52 + .../use-compilation-template-group-data.ts | 49 + .../use-compilation-template-group-form.ts | 55 + .../use-compilation-template-group-submit.ts | 48 + .../use-edit-compilation-template-group.ts | 51 + .../hooks/use-field-type-change.ts | 66 + .../hooks/use-template-kind-change.ts | 42 + .../edit-template/index.tsx | 183 +++ .../edit-template/schema.ts | 41 + .../edit-template/utils.ts | 259 ++++ .../compilation-templates/index.tsx | 110 ++ .../compilation-templates/template-card.tsx | 75 ++ .../template-dropdown.tsx | 58 + web/src/pages/user-setting/index.tsx | 4 +- web/src/pages/user-setting/sidebar/index.tsx | 6 + web/src/routes.tsx | 31 + .../compilation-template-group-service.ts | 35 + .../services/compilation-template-service.ts | 39 + web/src/services/dataset-skill-service.ts | 11 + .../services/document-structure-service.ts | 23 + web/src/services/knowledge-service.ts | 52 + web/src/utils/api.ts | 32 + web/src/utils/file-util.ts | 5 + web/vite.config.ts | 4 +- 171 files changed, 14477 insertions(+), 791 deletions(-) create mode 100644 web/src/components/artifact-force-graph/index.tsx create mode 100644 web/src/components/artifact-force-graph/types.ts create mode 100644 web/src/components/artifact-force-graph/use-container-dimensions.ts create mode 100644 web/src/components/artifact-force-graph/utils.ts create mode 100644 web/src/components/compilation-template-form-field.tsx create mode 100644 web/src/components/expandable-search-input.tsx create mode 100644 web/src/components/markdown-editor.tsx create mode 100644 web/src/constants/compilation.ts create mode 100644 web/src/hooks/use-compilation-template-group-request.ts create mode 100644 web/src/hooks/use-compilation-template-request.ts create mode 100644 web/src/hooks/use-dataset-skill-request.ts create mode 100644 web/src/interfaces/database/compilation-template.ts create mode 100644 web/src/interfaces/database/dataset-skill.ts create mode 100644 web/src/interfaces/database/document-structure.ts create mode 100644 web/src/interfaces/request/compilation-template.ts create mode 100644 web/src/lib/editor/content-editable.tsx create mode 100644 web/src/lib/editor/editor-theme.css create mode 100644 web/src/lib/editor/editor-theme.ts create mode 100644 web/src/lib/editor/index.ts create mode 100644 web/src/lib/editor/lexical-editor.tsx create mode 100644 web/src/lib/editor/markdown/enhanced-markdown-export.ts create mode 100644 web/src/lib/editor/markdown/enhanced-markdown-import.ts create mode 100644 web/src/lib/editor/markdown/horizontal-rule-node.ts create mode 100644 web/src/lib/editor/markdown/horizontal-rule-transformer.ts create mode 100644 web/src/lib/editor/markdown/index.ts create mode 100644 web/src/lib/editor/markdown/list-transformers.ts create mode 100644 web/src/lib/editor/markdown/markdown-transformers.ts create mode 100644 web/src/lib/editor/mermaid-node.tsx create mode 100644 web/src/lib/editor/mermaid-transformer.ts create mode 100644 web/src/lib/editor/nodes.ts create mode 100644 web/src/lib/editor/plugins/floating-selection-toolbar.tsx create mode 100644 web/src/lib/editor/plugins/table-actions-plugin.tsx create mode 100644 web/src/lib/editor/plugins/table-row-size-guard.ts create mode 100644 web/src/lib/editor/plugins/table-transformer.ts create mode 100644 web/src/lib/editor/plugins/toolbar-plugin.tsx create mode 100644 web/src/lib/editor/raw-markdown-editor.tsx create mode 100644 web/src/pages/agent/canvas/node/compilation-node.tsx create mode 100644 web/src/pages/agent/form/compilation-form/index.tsx create mode 100644 web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-view-switch/index.tsx create mode 100644 web/src/pages/chunk/representation/components/representation-renderer.tsx create mode 100644 web/src/pages/chunk/representation/components/representation-select.tsx create mode 100644 web/src/pages/chunk/representation/components/timeline-x6-graph/hooks/use-x6-graph.ts create mode 100644 web/src/pages/chunk/representation/components/timeline-x6-graph/index.tsx create mode 100644 web/src/pages/chunk/representation/components/timeline-x6-graph/types.ts create mode 100644 web/src/pages/chunk/representation/hooks/use-selected-template.ts create mode 100644 web/src/pages/chunk/representation/index.tsx create mode 100644 web/src/pages/chunk/representation/utils/adapters.ts create mode 100644 web/src/pages/dataset/compilation/constants.ts create mode 100644 web/src/pages/dataset/compilation/create-directory-dialog/create-directory-form.tsx create mode 100644 web/src/pages/dataset/compilation/create-directory-dialog/index.tsx create mode 100644 web/src/pages/dataset/compilation/empty-state.tsx create mode 100644 web/src/pages/dataset/compilation/hooks/use-commit-artifact.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-compilation-artifact.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-compilation-skill.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-compilation-view.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-create-directory.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-wiki-editor.ts create mode 100644 web/src/pages/dataset/compilation/hooks/use-wiki-version.ts create mode 100644 web/src/pages/dataset/compilation/index.tsx create mode 100644 web/src/pages/dataset/compilation/interface.ts create mode 100644 web/src/pages/dataset/compilation/knowledge-force-graph.tsx create mode 100644 web/src/pages/dataset/compilation/skills-left-panel.tsx create mode 100644 web/src/pages/dataset/compilation/utils/parse-wiki-diff.ts create mode 100644 web/src/pages/dataset/compilation/version-history-item.tsx create mode 100644 web/src/pages/dataset/compilation/version-history-sheet.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-commit-modal.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-detail-content.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-clear.ts create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-navigation.ts create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/index.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/wiki-artifact-list.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/wiki-list-item.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-left-panel/wiki-topic-list.tsx create mode 100644 web/src/pages/dataset/compilation/wiki-version-diff-panel.tsx create mode 100644 web/src/pages/dataset/dataset/generate-button/constants.ts create mode 100644 web/src/pages/dataset/dataset/generate-button/generate-log-button.tsx delete mode 100644 web/src/pages/dataset/dataset/generate-button/generate.tsx create mode 100644 web/src/pages/dataset/dataset/generate-button/use-generate-status.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/add-field-modal.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/basic-info-step.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/blueprints-step.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/field-card.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/json-preview-sheet.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/section-field-grid.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/template-preview-header.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/template-sidebar.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/components/workflow-preview-sheet.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-active-section-tab.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-add-field-form.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-available-kind-options.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-builtin-template.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-create-next-compilation-template-group.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-field-array-handlers.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-field-modal.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-template-add-button.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-template-preview-sheets.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/hooks/use-template-section-data.ts create mode 100644 web/src/pages/user-setting/compilation-templates/create-next/index.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/components/fields-section.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/components/template-card.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/components/tree-template-fields.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-compilation-template-group-data.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-compilation-template-group-form.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-compilation-template-group-submit.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-edit-compilation-template-group.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-field-type-change.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/hooks/use-template-kind-change.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/index.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/schema.ts create mode 100644 web/src/pages/user-setting/compilation-templates/edit-template/utils.ts create mode 100644 web/src/pages/user-setting/compilation-templates/index.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/template-card.tsx create mode 100644 web/src/pages/user-setting/compilation-templates/template-dropdown.tsx create mode 100644 web/src/services/compilation-template-group-service.ts create mode 100644 web/src/services/compilation-template-service.ts create mode 100644 web/src/services/dataset-skill-service.ts create mode 100644 web/src/services/document-structure-service.ts diff --git a/.gitignore b/.gitignore index 295e08f857..943af38a73 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,4 @@ internal/deepdoc/parser/docx/tool/ # test data compare tool internal/ingestion/task/tool/generate_dataflow_golden.py internal/ingestion/task/tool/README.md +internal/cpp/cmake-build-release \ No newline at end of file diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 3f8b326055..4a755ac752 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -30,6 +30,10 @@ When fixing CSS/layout issues (especially flex truncation, ellipsis, or element - Before editing, explain: (1) the full flex/container hierarchy from the target element up to the nearest non-flex ancestor, (2) what constraint is actually causing the bug, and (3) how the proposed fix addresses that root cause. +### Color Tokens + +When writing or modifying styles, **use the project-defined color tokens from `src/tailwind.css`** (e.g., `bg-bg-base`, `text-text-primary`, `text-text-secondary`, `text-text-disabled`, `border-border-button`, `bg-bg-card`). Do not use arbitrary hex/RGB values or Tailwind's default palette colors (e.g., `emerald-500`, `blue-400`) directly in component class names. These tokens are defined for both light and dark modes and keep the UI consistent with the design system. + ### Scope and Boundaries Respect explicit boundaries from the user. If the user says **"only fix the selected line"** or **"do not touch shared types/files"**, follow that instruction exactly. Do not investigate unrelated errors, modify shared schemas (e.g., `LlmSettingFieldSchema`), or refactor other files without confirmation. If a change outside the described scope seems necessary, ask for permission first. @@ -103,10 +107,14 @@ The folder `src/components/ui/` is the project's **shared UI library** — it co ### React Patterns and Conventions +- **Avoid inline event handlers.** Do not define arrow functions directly on JSX event props like `onClick={() => ...}` or `onChange={(e) => ...}`. Instead, extract the handler and reference it by name (e.g., `onClick={handleClick}`). Inline handlers are recreated on every render, which can break `React.memo` optimizations and make stack traces harder to read. - **Prefer `requestAnimationFrame` or `useLayoutEffect`** over `setTimeout(..., 0)` for focus or DOM measurement operations. - **Prefer `useTranslation` from `react-i18next`** over project-wrapped utilities like `useTranslate`. - Extract complex logic into hooks or utils; keep components lean. -- Use `PascalCase` for constants and component names. +- Use **PascalCase** for constants and component names. + - Components: `EditableTextarea`, `RAGFlowFormItem` + - Constants: `InitialMockData`, `DefaultPlaceholder` +- Avoid camelCase or SCREAMING_SNAKE_CASE for components and top-level constants. - Avoid duplicating component structures in JSX; favor render props or reusable components. ### Utility Libraries and Reuse diff --git a/web/package-lock.json b/web/package-lock.json index f69342535a..d993e77f10 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,11 +10,23 @@ "@ant-design/icons": "^5.2.6", "@antv/g2": "^5.2.10", "@antv/g6": "^5.1.0", + "@antv/layout": "^2.0.0", + "@antv/x6": "^3.1.7", "@extend-ai/react-docx": "^0.6.7", "@floating-ui/react": "^0.27.19", "@hookform/resolvers": "^3.9.1", "@js-preview/excel": "^1.7.14", - "@lexical/react": "^0.23.1", + "@lexical/code": "^0.45.0", + "@lexical/hashtag": "^0.45.0", + "@lexical/history": "^0.45.0", + "@lexical/link": "^0.45.0", + "@lexical/list": "^0.45.0", + "@lexical/markdown": "^0.45.0", + "@lexical/react": "^0.45.0", + "@lexical/rich-text": "^0.45.0", + "@lexical/selection": "^0.45.0", + "@lexical/table": "^0.45.0", + "@lexical/utils": "^0.45.0", "@mdx-js/rollup": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.3", @@ -71,9 +83,10 @@ "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", "jsoneditor": "^10.4.2", - "lexical": "^0.23.1", + "lexical": "^0.45.0", "lodash": "^4.18.1", "lucide-react": "^1.7.0", + "mermaid": "^11.15.0", "next-themes": "^0.4.6", "openai-speech-stream-player": "^1.0.8", "papaparse": "^5.5.3", @@ -86,6 +99,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.3.5", "react-error-boundary": "^4.0.13", + "react-force-graph-2d": "^1.29.1", "react-hook-form": "^7.56.4", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", @@ -244,6 +258,19 @@ "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", "license": "MIT" }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@antv/algorithm": { "version": "0.1.26", "resolved": "https://registry.npmmirror.com/@antv/algorithm/-/algorithm-0.1.26.tgz", @@ -553,6 +580,21 @@ "d3-timer": "^3.0.1" } }, + "node_modules/@antv/x6": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@antv/x6/-/x6-3.1.7.tgz", + "integrity": "sha512-NLKXtbCK51oLbazfFD0XsD93rMmih08UBW4gAuEyLBpwAqHmHe+vP8VhOZDkl5O9jV1LSv85IJghr9CT5tZjWw==", + "license": "MIT", + "dependencies": { + "dom-align": "^1.12.4", + "lodash-es": "^4.17.15", + "mousetrap": "^1.6.5", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1278,12 +1320,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@chenglou/pretext": { "version": "0.0.5", "resolved": "https://registry.npmmirror.com/@chenglou/pretext/-/pretext-0.0.5.tgz", "integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==", "license": "MIT" }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -2202,6 +2256,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2727,41 +2798,69 @@ "license": "MIT" }, "node_modules/@lexical/clipboard": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/clipboard/-/clipboard-0.23.1.tgz", - "integrity": "sha512-MT8IXl1rhTe8VcwnkhgFtWra6sRYNsl/I7nE9aw6QxwvPReKmRDmyBmEIeXwnKSGHRe19OJhu4/A9ciKPyVdMA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/clipboard/-/clipboard-0.45.0.tgz", + "integrity": "sha512-9oDu2SNj/EZGjpXJTruz74Ls3VzF2Q41v1QB7JnTq5lh24byvIOdgEpOFHtr/7WVHssfXCbMtgFb5tW8rMaZ2g==", "license": "MIT", "dependencies": { - "@lexical/html": "0.23.1", - "@lexical/list": "0.23.1", - "@lexical/selection": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/list": "0.45.0", + "@lexical/selection": "0.45.0", + "@lexical/utils": "0.45.0", + "@types/trusted-types": "^2.0.7", + "lexical": "0.45.0" } }, "node_modules/@lexical/code": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/code/-/code-0.23.1.tgz", - "integrity": "sha512-TOxaFAwoewrX3rHp4Po+u1LJT8oteP/6Kn2z6j9DaynBW62gIqTuSAFcMPysVx/Puq5hhJHPRD/be9RWDteDZw==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/code/-/code-0.45.0.tgz", + "integrity": "sha512-vtD5IKbJvgIxhKIl96pWKqjlqialzg/WbWNUNbeoyuf9ht0/MQ6WFdEdCNnnXqx4rj7CTzafB3c0jup7h96b6A==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1", - "prismjs": "^1.27.0" + "@lexical/code-core": "0.45.0", + "@lexical/code-prism": "0.45.0", + "@lexical/extension": "0.45.0", + "lexical": "0.45.0" + } + }, + "node_modules/@lexical/code-core": { + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/code-core/-/code-core-0.45.0.tgz", + "integrity": "sha512-zU3noYmfluSK38r0eBz3gHKzsgOksf9eHqoPIwXH6HovKCDo848HoW0i/hCRzRo9oPqydKikx0LIKUvWSaSNIw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/internal": "0.45.0", + "lexical": "0.45.0" + } + }, + "node_modules/@lexical/code-prism": { + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/code-prism/-/code-prism-0.45.0.tgz", + "integrity": "sha512-eevIcapWzWk4Kb2WE6mmMjTXjf+Mfn4pLXAJ92BkRO6gc5RPkYgpn7zsYTzSBWEM/GptKf7ta+XouLcnLXNOSQ==", + "license": "MIT", + "dependencies": { + "@lexical/code-core": "0.45.0", + "@lexical/extension": "0.45.0", + "lexical": "0.45.0", + "prismjs": "^1.30.0" } }, "node_modules/@lexical/devtools-core": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/devtools-core/-/devtools-core-0.23.1.tgz", - "integrity": "sha512-QsgcrECy11ZHhWAfyNW/ougXFF1o0EuQnhFybgTdqQmw0rJ2ZgPLpPjD5lws3CE8mP8g5knBV4/cyxvv42fzzg==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/devtools-core/-/devtools-core-0.45.0.tgz", + "integrity": "sha512-XiVedKif5nWbNj2CeNy30kokfk/iYb7Db+B1Z18bnr/uzp6IzkxpX6I4GeZu4HVbQJhGMCAdq19tFpYwXOx4sw==", "license": "MIT", "dependencies": { - "@lexical/html": "0.23.1", - "@lexical/link": "0.23.1", - "@lexical/mark": "0.23.1", - "@lexical/table": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/html": "0.45.0", + "@lexical/link": "0.45.0", + "@lexical/mark": "0.45.0", + "@lexical/table": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" }, "peerDependencies": { "react": ">=17.x", @@ -2769,230 +2868,260 @@ } }, "node_modules/@lexical/dragon": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/dragon/-/dragon-0.23.1.tgz", - "integrity": "sha512-ZoY9VJDrTpO69sinRhIs3RlPAWviy4mwnC7lqtM77/pVK0Kaknv7z2iDqv+414PKQCgUhyoXp7PfYXu/3yb6LQ==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/dragon/-/dragon-0.45.0.tgz", + "integrity": "sha512-34k2/QuM8A9WVqVCtNGx1ySBWXxMVKejKH3VBWnXEb1NdrDa/N8jLpu0gOCLcrrDjMdUzs9uVgW0D4FRf1Lixg==", "license": "MIT", "dependencies": { - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "lexical": "0.45.0" + } + }, + "node_modules/@lexical/extension": { + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/extension/-/extension-0.45.0.tgz", + "integrity": "sha512-oJs1Zvrsqh32zZmeCkTQi7R2WoVIvaQE265JFZ/VczWlsWRdkkWQbGZ6iOlBZ3AmtiNgHoBeB0gZSoRQtWEQyA==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.45.0", + "@lexical/utils": "0.45.0", + "@preact/signals-core": "^1.14.1", + "lexical": "0.45.0" } }, "node_modules/@lexical/hashtag": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/hashtag/-/hashtag-0.23.1.tgz", - "integrity": "sha512-EkRCHV/IQwKlggy3VQDF9b4Krc9DKNZEjXe84CkEVrRpQSOwXi0qORzuaAipARyN632WKLSXOZJmNzkUNocJ6A==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/hashtag/-/hashtag-0.45.0.tgz", + "integrity": "sha512-gFX4n+9RjOFVkNUkEcv3JeQT0LvZ8fiHWHrIkg1m3JbyloG6mKDlsSlO1qnIPUaAB4V62Tbnmfv3I0qp1UvNuw==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/text": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/history": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/history/-/history-0.23.1.tgz", - "integrity": "sha512-5Vro4bIePw37MwffpvPm56WlwPdlY/u+fVkvXsxdhK9bqiFesmLZhBirokDPvJEMP35V59kzmN5mmWXSYfuRpg==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/history/-/history-0.45.0.tgz", + "integrity": "sha512-KihtI8485Wx7kyFCtKNKk/nO2jSibRa8iL0OiKPZsYWVAQF5vFG3J00epX8EkEpeCx8Zjmj+3eytY05HPNd2nA==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/html": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/html/-/html-0.23.1.tgz", - "integrity": "sha512-kNkDUaDe/Awypaw8JZn65BzT1gwNj2bNkaGFcmIkXUrTtiqlvgYvKvJeOKLkoAb/i2xq990ZAbHOsJrJm1jMbw==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/html/-/html-0.45.0.tgz", + "integrity": "sha512-eK215f25cnAcIzNB1iRltg8hbvkK5wyyXvKQUqhfG/9HaSSVBCPuCyXKZicjZllQNvbFINGHFhTIqSP56drFSw==", "license": "MIT", "dependencies": { - "@lexical/selection": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/selection": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, + "node_modules/@lexical/internal": { + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/internal/-/internal-0.45.0.tgz", + "integrity": "sha512-IhyzCdb1/xdpTtvK0xnqUXpaVRsD2KTZ6EMZpavm6dhKaglf1zorpdbH7r6Hjps6SRb4DCn/t7lac+GMdDFbXg==", + "license": "MIT" + }, "node_modules/@lexical/link": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/link/-/link-0.23.1.tgz", - "integrity": "sha512-HRaOp7prtcbHjbgq8AjJ4O02jYb8pTeS8RrGcgIRhCOq3/EcsSb1dXMwuraqmh9oxbuFyEu/JE31EFksiOW6qA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/link/-/link-0.45.0.tgz", + "integrity": "sha512-9xpsRQ06IA4eVnx4OtFV0+Tmp2Etvh0dh9FTFd/LJYaqo2A0ZIuNUggueDZ0i2dhi8wWW8J08O7U0HxLcDWHAg==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/list": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/list/-/list-0.23.1.tgz", - "integrity": "sha512-TI3WyWk3avv9uaJwaq8V+m9zxLRgnzXDYNS0rREafnW09rDpaFkpVmDuX+PZVR3NqPlwVt+slWVSBuyfguAFbA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/list/-/list-0.45.0.tgz", + "integrity": "sha512-Eyff9KVLu5mV4QSid2aw9uYmKDKgDYToAaagI9Sp3VgqTMmvRUQJ0w7sr4zq8/FRKSzrsf4V2FjvLAuQE7eBUQ==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/mark": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/mark/-/mark-0.23.1.tgz", - "integrity": "sha512-E7cMOBVMrNGMw0LsyWKNFQZ5Io3bUIHCC3aCUdH24z1XWnuTmDFKMqNrphywPniO7pzSgVyGpkQBZIAIN76+YA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/mark/-/mark-0.45.0.tgz", + "integrity": "sha512-PMyGmnF6koE5TKCDlGe1k1t1ZOefReyYaKtx6OY3Tf1AyyKII6zRGR+dANrgWzYliYO7j4mpKBK00ykOnuF3zA==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/markdown": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/markdown/-/markdown-0.23.1.tgz", - "integrity": "sha512-TQx8oXenaiVYffBPxD85m4CydbDAuYOonATiABAFG6CHkA6vi898M1TCTgVDS6/iISjtjQpqHo0SW7YjLt14jw==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/markdown/-/markdown-0.45.0.tgz", + "integrity": "sha512-fl/UUwudBXGnz44kOS07ZRN0DMfOWNnVl7GSSNyz6zDo1cMD60URt7LjF51bAG2oL8i1eRDX4/GV+7vBcuoGzw==", "license": "MIT", "dependencies": { - "@lexical/code": "0.23.1", - "@lexical/link": "0.23.1", - "@lexical/list": "0.23.1", - "@lexical/rich-text": "0.23.1", - "@lexical/text": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" - } - }, - "node_modules/@lexical/offset": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/offset/-/offset-0.23.1.tgz", - "integrity": "sha512-ylw5egME/lldacVXDoRsdGDXPuk9lGmYgcqx/aITGrSymav+RDjQoAapHbz1HQqGmm/m18+VLaWTdjtkbrIN6g==", - "license": "MIT", - "dependencies": { - "lexical": "0.23.1" + "@lexical/code-core": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/link": "0.45.0", + "@lexical/list": "0.45.0", + "@lexical/rich-text": "0.45.0", + "@lexical/selection": "0.45.0", + "@lexical/text": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/overflow": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/overflow/-/overflow-0.23.1.tgz", - "integrity": "sha512-WubTqozpxOeyTm/tKIHXinsjuRcgPESacOvu93dS+sC7q3n+xeBIu5FL7lM6bbsk3zNtNJQ9sG0svZngmWRjCw==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/overflow/-/overflow-0.45.0.tgz", + "integrity": "sha512-iw6Gc85phIGEjAFRDLiGOrnxK/EjyRlEfQHI7BeU0m+qHDCc94cThbeuaPTC7nnxHytFbRdtJIhoQvvcUR+PKA==", "license": "MIT", "dependencies": { - "lexical": "0.23.1" + "lexical": "0.45.0" } }, "node_modules/@lexical/plain-text": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/plain-text/-/plain-text-0.23.1.tgz", - "integrity": "sha512-tM4DJw+HyT9XV4BKGVECDnejcC//jsFggjFmJgwIMTCxJPiGXEEZLZTXmGqf8QdFZ6cH1I5bhreZPQUWu6dRvg==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/plain-text/-/plain-text-0.45.0.tgz", + "integrity": "sha512-453bsKVk3oY5/ktp3A2zO2nFdXniU/zfD87eIcN50aXrIwvNm082GkfmBaDeVujk7fLgC2hG980xdcEH4xcG3w==", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.23.1", - "@lexical/selection": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/clipboard": "0.45.0", + "@lexical/dragon": "0.45.0", + "@lexical/extension": "0.45.0", + "@lexical/selection": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/react": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/react/-/react-0.23.1.tgz", - "integrity": "sha512-g5CQMOiK+Djqp75UaSFUceHZEUQVIXBzWBuVR69pCiptCgNqN3CNAoIxy0hTTaVrLq6S0SCjUOduBDtioN0bLA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/react/-/react-0.45.0.tgz", + "integrity": "sha512-LAaceAtcQpVMDvldhkrQC1MSawpj7aAm1CfGg6RbyQgTDDp2fKnP1gDhjQIef3lH8W2fYeXRe66EdzaFkoGHKA==", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.23.1", - "@lexical/code": "0.23.1", - "@lexical/devtools-core": "0.23.1", - "@lexical/dragon": "0.23.1", - "@lexical/hashtag": "0.23.1", - "@lexical/history": "0.23.1", - "@lexical/link": "0.23.1", - "@lexical/list": "0.23.1", - "@lexical/mark": "0.23.1", - "@lexical/markdown": "0.23.1", - "@lexical/overflow": "0.23.1", - "@lexical/plain-text": "0.23.1", - "@lexical/rich-text": "0.23.1", - "@lexical/selection": "0.23.1", - "@lexical/table": "0.23.1", - "@lexical/text": "0.23.1", - "@lexical/utils": "0.23.1", - "@lexical/yjs": "0.23.1", - "lexical": "0.23.1", - "react-error-boundary": "^3.1.4" + "@floating-ui/react": "^0.27.19", + "@lexical/devtools-core": "0.45.0", + "@lexical/dragon": "0.45.0", + "@lexical/extension": "0.45.0", + "@lexical/hashtag": "0.45.0", + "@lexical/history": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/link": "0.45.0", + "@lexical/list": "0.45.0", + "@lexical/mark": "0.45.0", + "@lexical/markdown": "0.45.0", + "@lexical/overflow": "0.45.0", + "@lexical/plain-text": "0.45.0", + "@lexical/rich-text": "0.45.0", + "@lexical/table": "0.45.0", + "@lexical/text": "0.45.0", + "@lexical/utils": "0.45.0", + "@lexical/yjs": "0.45.0", + "lexical": "0.45.0", + "react-error-boundary": "^6.1.1" }, "peerDependencies": { "react": ">=17.x", - "react-dom": ">=17.x" + "react-dom": ">=17.x", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "yjs": { + "optional": true + } } }, "node_modules/@lexical/react/node_modules/react-error-boundary": { - "version": "3.1.4", - "resolved": "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-6.1.2.tgz", + "integrity": "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, "peerDependencies": { - "react": ">=16.13.1" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/@lexical/rich-text": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/rich-text/-/rich-text-0.23.1.tgz", - "integrity": "sha512-Y77HGxdF5aemjw/H44BXETD5KNeaNdwMRu9P7IrlK7cC1dvvimzL2D6ezbub5i7F1Ef5T0quOXjwK056vrqaKQ==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/rich-text/-/rich-text-0.45.0.tgz", + "integrity": "sha512-jd+jIsDirqILuUQiNUJAzZWynVZHuN1ekqYcydgDFaUXMFBQgVr3mBUieQzMw2IDAEk98txntu/mZQav4BryQQ==", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.23.1", - "@lexical/selection": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/clipboard": "0.45.0", + "@lexical/dragon": "0.45.0", + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/selection": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/selection": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/selection/-/selection-0.23.1.tgz", - "integrity": "sha512-xoehAURMZJZYf046GHUXiv8FSv5zTobhwDD2dML4fmNHPp9NxugkWHlNUinTK/b+jGgjSYVsqpEKPBmue4ZHdQ==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/selection/-/selection-0.45.0.tgz", + "integrity": "sha512-V2DYMhl84F1aZ24YpWMMqy9ikO7cVfD3G+GjMwrZnMt0JVnIghjPiYIEBb/OtqqA+mCbBwNhfpdK1RtF9HTbJA==", "license": "MIT", "dependencies": { - "lexical": "0.23.1" + "@lexical/internal": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/table": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/table/-/table-0.23.1.tgz", - "integrity": "sha512-Qs+iuwSVkV4OGTt+JdL9hvyl/QO3X9waH70L5Fxu9JmQk/jLl02tIGXbE38ocJkByfpyk4PrphoXt6l7CugJZA==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/table/-/table-0.45.0.tgz", + "integrity": "sha512-OXbwEg0DqkTdvqnu7z+/u8bgnDz2sjHHXCG3zGRhEzDgHcWoVQ18IciwMdse7dTTNfBIJMuGJcRoWPrHf9UlhQ==", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.23.1", - "@lexical/utils": "0.23.1", - "lexical": "0.23.1" + "@lexical/clipboard": "0.45.0", + "@lexical/extension": "0.45.0", + "@lexical/html": "0.45.0", + "@lexical/internal": "0.45.0", + "@lexical/utils": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/text": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/text/-/text-0.23.1.tgz", - "integrity": "sha512-aOuuAhmc+l2iSK99uP0x/Zg9LSQswQdNG3IxzGa0rTx844mWUHuEbAUaOqqlgDA1/zZ0WjObyhPfZJL775y63g==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/text/-/text-0.45.0.tgz", + "integrity": "sha512-ozI8fDsLTYAzEWXaBrGYZ0GnRbbIHi4IL99w5q06UfTQzBMYmF7WaccDP1krOOwwioPYQbiqZM7dh/41jsFELQ==", "license": "MIT", "dependencies": { - "lexical": "0.23.1" + "@lexical/internal": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/utils": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/utils/-/utils-0.23.1.tgz", - "integrity": "sha512-yXEkF6fj32+mJblCoP0ZT/vA0S05FA0nRUkVrvGX6sbZ9y+cIzuIbBoHi4z1ytutcWHQrwCK4TsN9hPYBIlb2w==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/utils/-/utils-0.45.0.tgz", + "integrity": "sha512-Fo5MMErqoPlUyTw1qVMZ9kE33ZjJF957KU683tsCrrlaVisVxpboL7BbAnxWTwCcIpAsfc6AUndJJ70M9NDMGg==", "license": "MIT", "dependencies": { - "@lexical/list": "0.23.1", - "@lexical/selection": "0.23.1", - "@lexical/table": "0.23.1", - "lexical": "0.23.1" + "@lexical/internal": "0.45.0", + "@lexical/selection": "0.45.0", + "lexical": "0.45.0" } }, "node_modules/@lexical/yjs": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/@lexical/yjs/-/yjs-0.23.1.tgz", - "integrity": "sha512-ygodSxmC65srNicMIhqBRIXI2LHhmnHcR1EO9fLO7flZWGCR1HIoeGmwhHo9FLgJoc5LHanV+dE0z1onFo1qqQ==", + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/@lexical/yjs/-/yjs-0.45.0.tgz", + "integrity": "sha512-HYbOovvOavtF1S7C13l9g21s4MFLLd/o49qdn+/uGj23j4yjG556XBSrF0c3BREXDXSUr+RTbI6uiQ+L5Ofcmw==", "license": "MIT", "dependencies": { - "@lexical/offset": "0.23.1", - "@lexical/selection": "0.23.1", - "lexical": "0.23.1" + "@lexical/internal": "0.45.0", + "@lexical/selection": "0.45.0", + "lexical": "0.45.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -3090,6 +3219,15 @@ "node": ">= 12" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@mjackson/node-fetch-server": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", @@ -3168,6 +3306,16 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@preact/signals-core": { + "version": "1.14.3", + "resolved": "https://registry.npmmirror.com/@preact/signals-core/-/signals-core-1.14.3.tgz", + "integrity": "sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", @@ -7938,6 +8086,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -8001,18 +8155,96 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", @@ -8091,6 +8323,12 @@ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", @@ -8139,6 +8377,12 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", @@ -8528,7 +8772,6 @@ "version": "2.0.7", "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -8906,6 +9149,16 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -9397,6 +9650,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmmirror.com/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ace-builds": { "version": "1.43.5", "resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.43.5.tgz", @@ -10290,6 +10552,16 @@ "node": ">=12.0.0" } }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -10528,6 +10800,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/capture-exit/-/capture-exit-2.0.0.tgz", @@ -11245,6 +11529,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -11540,6 +11833,95 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", @@ -11552,12 +11934,49 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-binarytree": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/d3-binarytree/-/d3-binarytree-1.0.2.tgz", "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", "license": "MIT" }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", @@ -11567,6 +11986,30 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -11773,6 +12216,46 @@ "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", "license": "BSD-3-Clause" }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", @@ -11891,6 +12374,15 @@ "node": ">=12" } }, + "node_modules/d3/node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/dagre": { "version": "0.8.5", "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", @@ -11901,6 +12393,16 @@ "lodash": "^4.17.15" } }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz", @@ -11987,9 +12489,9 @@ "license": "MIT" }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debug": { @@ -12147,6 +12649,15 @@ "node": ">=0.10.0" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -12320,6 +12831,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-align": { + "version": "1.12.4", + "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz", + "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", + "license": "MIT" + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/dom-converter/-/dom-converter-0.2.0.tgz", @@ -12862,6 +13379,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -14266,6 +14793,20 @@ "dev": true, "license": "ISC" }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/flru": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/flru/-/flru-1.0.2.tgz", @@ -14347,6 +14888,32 @@ "node": ">=0.10.0" } }, + "node_modules/force-graph": { + "version": "1.51.4", + "resolved": "https://registry.npmmirror.com/force-graph/-/force-graph-1.51.4.tgz", + "integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -14953,6 +15520,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmmirror.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -16011,6 +16584,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -16031,6 +16614,15 @@ "node": ">=8" } }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", @@ -17010,6 +17602,15 @@ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", "license": "MIT" }, + "node_modules/jerrypick": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/jerrypick/-/jerrypick-1.1.2.tgz", + "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmmirror.com/jest/-/jest-29.7.0.tgz", @@ -18215,10 +18816,22 @@ "setimmediate": "^1.0.5" } }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/katex": { - "version": "0.16.27", - "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.27.tgz", - "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "version": "0.16.47", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -18250,6 +18863,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", @@ -18270,6 +18888,12 @@ "node": ">=6" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lefthook": { "version": "1.13.6", "resolved": "https://registry.npmmirror.com/lefthook/-/lefthook-1.13.6.tgz", @@ -18534,10 +19158,13 @@ } }, "node_modules/lexical": { - "version": "0.23.1", - "resolved": "https://registry.npmmirror.com/lexical/-/lexical-0.23.1.tgz", - "integrity": "sha512-iuS72HcAYUemsCRQCm4XZzkGhZb8a9KagW+ee2TFfkkf9f3ZpUYSrobMpjYVZRkgMOx7Zk5VCPMxm1nouJTfnQ==", - "license": "MIT" + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/lexical/-/lexical-0.45.0.tgz", + "integrity": "sha512-z2M9C2ILPW7SopQE1aKbOFdZJbe3HvsrgWnJKveMDrSEvVjU9Hce4UpKVsAdkwY7TgY9RDuCV3+sfrGu0WkL+w==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.45.0" + } }, "node_modules/lib0": { "version": "0.2.117", @@ -18644,6 +19271,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -19227,6 +19860,69 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", @@ -20130,6 +20826,12 @@ "marked": "14.0.0" } }, + "node_modules/mousetrap": { + "version": "1.6.5", + "resolved": "https://registry.npmmirror.com/mousetrap/-/mousetrap-1.6.5.tgz", + "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==", + "license": "Apache-2.0 WITH LLVM-exception" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/mri/-/mri-1.2.0.tgz", @@ -20764,6 +21466,12 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", @@ -20905,6 +21613,12 @@ "node": ">=0.10.0" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-5.0.0.tgz", @@ -21217,6 +21931,22 @@ "node": ">=4" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmmirror.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -21513,6 +22243,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22384,6 +23124,23 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "node_modules/react-force-graph-2d": { + "version": "1.29.1", + "resolved": "https://registry.npmmirror.com/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz", + "integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==", + "license": "MIT", + "dependencies": { + "force-graph": "^1.51", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-hook-form": { "version": "7.69.0", "resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.69.0.tgz", @@ -22449,6 +23206,21 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-kapsule": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/react-kapsule/-/react-kapsule-2.6.0.tgz", + "integrity": "sha512-HzLJoYb1n1kfwjXbqFFcRR0EA6oPsJ64tNdDmCSaL/bz2o9wUZRSb0cMe//grLFeF9EVoL4CD/e6ozLyzEv+PQ==", + "license": "MIT", + "dependencies": { + "jerrypick": "^1.1.2" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-markdown": { "version": "9.1.0", "resolved": "https://registry.npmmirror.com/react-markdown/-/react-markdown-9.1.0.tgz", @@ -24646,6 +25418,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.54.0.tgz", @@ -24687,6 +25465,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmmirror.com/rsvp/-/rsvp-4.8.5.tgz", @@ -26209,6 +26999,12 @@ "integrity": "sha512-RmGftIhY4tqtD1ERwKsVEDlt/M6UyxN/rcr95UmlooWmhtL0RwVUYJkpo1kSx3ppd9/JZzbknhy742zbMAawjQ==", "license": "MIT" }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", @@ -26833,6 +27629,21 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -27049,7 +27860,6 @@ "version": "2.2.0", "resolved": "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -27750,6 +28560,15 @@ "dev": true, "license": "MIT" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", @@ -29377,9 +30196,9 @@ } }, "node_modules/yjs": { - "version": "13.6.29", - "resolved": "https://registry.npmmirror.com/yjs/-/yjs-13.6.29.tgz", - "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", + "version": "13.6.31", + "resolved": "https://registry.npmmirror.com/yjs/-/yjs-13.6.31.tgz", + "integrity": "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw==", "license": "MIT", "peer": true, "dependencies": { diff --git a/web/package.json b/web/package.json index 435a65546d..6597b0e400 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,8 @@ "build": "vite build --mode production", "build-storybook": "storybook build", "dev": "vite --host", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives", "prepare": "cd .. && git rev-parse --git-dir >/dev/null 2>&1 && lefthook install || true", "preview": "vite preview", @@ -25,11 +27,23 @@ "@ant-design/icons": "^5.2.6", "@antv/g2": "^5.2.10", "@antv/g6": "^5.1.0", + "@antv/layout": "^2.0.0", + "@antv/x6": "^3.1.7", "@extend-ai/react-docx": "^0.6.7", "@floating-ui/react": "^0.27.19", "@hookform/resolvers": "^3.9.1", "@js-preview/excel": "^1.7.14", - "@lexical/react": "^0.23.1", + "@lexical/code": "^0.45.0", + "@lexical/hashtag": "^0.45.0", + "@lexical/history": "^0.45.0", + "@lexical/link": "^0.45.0", + "@lexical/list": "^0.45.0", + "@lexical/markdown": "^0.45.0", + "@lexical/react": "^0.45.0", + "@lexical/rich-text": "^0.45.0", + "@lexical/selection": "^0.45.0", + "@lexical/table": "^0.45.0", + "@lexical/utils": "^0.45.0", "@mdx-js/rollup": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.3", @@ -86,9 +100,10 @@ "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", "jsoneditor": "^10.4.2", - "lexical": "^0.23.1", + "lexical": "^0.45.0", "lodash": "^4.18.1", "lucide-react": "^1.7.0", + "mermaid": "^11.15.0", "next-themes": "^0.4.6", "openai-speech-stream-player": "^1.0.8", "papaparse": "^5.5.3", @@ -101,6 +116,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.3.5", "react-error-boundary": "^4.0.13", + "react-force-graph-2d": "^1.29.1", "react-hook-form": "^7.56.4", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", diff --git a/web/src/components/artifact-force-graph/index.tsx b/web/src/components/artifact-force-graph/index.tsx new file mode 100644 index 0000000000..3c0b9d4275 --- /dev/null +++ b/web/src/components/artifact-force-graph/index.tsx @@ -0,0 +1,93 @@ +import { type IArtifactGraphEntity } from '@/interfaces/database/dataset'; +import { cn } from '@/lib/utils'; +import isEmpty from 'lodash/isEmpty'; +import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; +import ForceGraph2D, { type ForceGraphMethods } from 'react-force-graph-2d'; +import { type ArtifactForceGraphProps } from './types'; +import { useContainerDimensions } from './use-container-dimensions'; +import { defaultMapNodeToValue, renderNodeLabel } from './utils'; + +function ArtifactForceGraph({ + data, + show = true, + onNodeClick, + mapNodeToValue = defaultMapNodeToValue as ( + node: IArtifactGraphEntity, + ) => TNodeValue, + getNodeId = (node) => node.slug, +}: ArtifactForceGraphProps) { + const containerRef = useRef(null); + const fgRef = useRef | undefined>( + undefined, + ); + const hasFittedRef = useRef(false); + const dimensions = useContainerDimensions(containerRef, show); + + const graphData = useMemo(() => { + if (isEmpty(data) || !data) { + return { nodes: [], links: [] }; + } + + const nodes = (data.entities || []).map((entity) => ({ + ...entity, + id: getNodeId(entity), + })); + + const links = (data.relations || []).map((relation) => ({ + source: relation.from, + target: relation.to, + })); + + return { nodes, links }; + }, [data, getNodeId]); + + useEffect(() => { + hasFittedRef.current = false; + }, [graphData]); + + const handleEngineStop = useCallback(() => { + if (!hasFittedRef.current && fgRef.current) { + fgRef.current.zoomToFit(400); + hasFittedRef.current = true; + } + }, []); + + const handleNodeClick = useCallback( + (node: IArtifactGraphEntity) => { + onNodeClick?.(mapNodeToValue(node)); + }, + [onNodeClick, mapNodeToValue], + ); + + return ( +
+ {dimensions.width > 0 && dimensions.height > 0 && ( + 'after'} + /> + )} +
+ ); +} + +const MemoizedArtifactForceGraph = memo(ArtifactForceGraph) as < + TNodeValue = IArtifactGraphEntity, +>( + props: ArtifactForceGraphProps, +) => React.ReactElement; + +export { MemoizedArtifactForceGraph as ArtifactForceGraph }; +export default MemoizedArtifactForceGraph; diff --git a/web/src/components/artifact-force-graph/types.ts b/web/src/components/artifact-force-graph/types.ts new file mode 100644 index 0000000000..783a645894 --- /dev/null +++ b/web/src/components/artifact-force-graph/types.ts @@ -0,0 +1,12 @@ +import { + type IArtifactGraph, + type IArtifactGraphEntity, +} from '@/interfaces/database/dataset'; + +export interface ArtifactForceGraphProps { + data?: IArtifactGraph; + show?: boolean; + onNodeClick?: (node: TNodeValue) => void; + mapNodeToValue?: (node: IArtifactGraphEntity) => TNodeValue; + getNodeId?: (node: IArtifactGraphEntity) => string; +} diff --git a/web/src/components/artifact-force-graph/use-container-dimensions.ts b/web/src/components/artifact-force-graph/use-container-dimensions.ts new file mode 100644 index 0000000000..b263dc1b4a --- /dev/null +++ b/web/src/components/artifact-force-graph/use-container-dimensions.ts @@ -0,0 +1,25 @@ +import { useEffect, useState, type RefObject } from 'react'; + +export function useContainerDimensions( + ref: RefObject, + enabled: boolean = true, +) { + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + + useEffect(() => { + if (!ref.current || !enabled) return; + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + const { width, height } = entry.contentRect; + setDimensions({ width, height }); + } + }); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, enabled]); + + return dimensions; +} diff --git a/web/src/components/artifact-force-graph/utils.ts b/web/src/components/artifact-force-graph/utils.ts new file mode 100644 index 0000000000..e77f4861f8 --- /dev/null +++ b/web/src/components/artifact-force-graph/utils.ts @@ -0,0 +1,26 @@ +import { type IArtifactGraphEntity } from '@/interfaces/database/dataset'; +import { type ComponentProps } from 'react'; +import ForceGraph2D from 'react-force-graph-2d'; + +export const renderNodeLabel: NonNullable< + ComponentProps['nodeCanvasObject'] +> = (node, ctx) => { + const label = node.name; + const fontSize = 2; + ctx.font = `${fontSize}px Sans-Serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + + const textSecondary = getComputedStyle(document.documentElement) + .getPropertyValue('--text-secondary') + .trim(); + ctx.fillStyle = `rgb(${textSecondary})`; + + if (node.x && node.y) { + ctx.fillText(label, node.x, node.y + 5); + } +}; + +export const defaultMapNodeToValue = ( + node: TNode, +): TNode => node; diff --git a/web/src/components/chunk-method-dialog/index.tsx b/web/src/components/chunk-method-dialog/index.tsx index 63aaec7503..f9b117e731 100644 --- a/web/src/components/chunk-method-dialog/index.tsx +++ b/web/src/components/chunk-method-dialog/index.tsx @@ -37,6 +37,7 @@ import { AutoQuestionsFormField, } from '../auto-keywords-form-field'; import { ChildrenDelimiterForm } from '../children-delimiter-form'; +import { CompilationTemplateFormField } from '../compilation-template-form-field'; import { DataFlowSelect } from '../data-pipeline-select'; import { DelimiterFormField } from '../delimiter-form-field'; import { EntityTypesFormField } from '../entity-types-form-field'; @@ -152,6 +153,7 @@ export function ChunkMethodDialog({ ) .optional(), enable_metadata: z.boolean().optional(), + compilation_template_group_id: z.array(z.string()).optional(), }), }) .superRefine((data, ctx) => { @@ -300,6 +302,10 @@ export function ChunkMethodDialog({ {parseType === ParseType.BuiltIn && } + {parseType === ParseType.BuiltIn && ( + + )} + {showPages && parseType === ParseType.BuiltIn && ( )} diff --git a/web/src/components/chunk-method-dialog/use-default-parser-values.ts b/web/src/components/chunk-method-dialog/use-default-parser-values.ts index b80d80b902..5e0a1d189e 100644 --- a/web/src/components/chunk-method-dialog/use-default-parser-values.ts +++ b/web/src/components/chunk-method-dialog/use-default-parser-values.ts @@ -42,6 +42,7 @@ export function useDefaultParserValues() { metadata: [], built_in_metadata: [], enable_metadata: false, + compilation_template_group_id: [], }; return defaultParserValues as IParserConfig; diff --git a/web/src/components/compilation-template-form-field.tsx b/web/src/components/compilation-template-form-field.tsx new file mode 100644 index 0000000000..7407bffcfe --- /dev/null +++ b/web/src/components/compilation-template-form-field.tsx @@ -0,0 +1,117 @@ +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { + MultiSelect, + MultiSelectOptionType, +} from '@/components/ui/multi-select'; +import { useFetchAllCompilationTemplateGroups } from '@/hooks/use-compilation-template-group-request'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type CompilationTemplateFormFieldProps = { + horizontal?: boolean; + name?: string; +}; + +const ScopeTranslationKeyMap: Record = { + file: 'scopeFile', + dataset: 'scopeDataset', +}; + +type CompilationTemplateMultiSelectProps = { + value?: string[]; + onChange(value: string[]): void; +}; + +/** + * Renders a multi-select for compilation template groups. + * + * Selection rule: + * - Each group has a scope: "file" or "dataset". + * - At most one group per scope can be selected. + * - The two scopes can be combined (one file group + one dataset group). + * - Once a scope is selected, remaining options of the same scope are disabled. + * - The scope is displayed as a suffix after each option label. + */ +function CompilationTemplateMultiSelect({ + value: rawValue = [], + onChange, +}: CompilationTemplateMultiSelectProps) { + const { t } = useTranslation(); + const { groups } = useFetchAllCompilationTemplateGroups(); + + const value = useMemo(() => { + // Normalize legacy single-string values into an array during the migration. + if (Array.isArray(rawValue)) return rawValue; + if (typeof rawValue === 'string' && rawValue) return [rawValue]; + return []; + }, [rawValue]); + + // Collect scopes that are already selected by the current value. + const selectedScopes = useMemo(() => { + const scopes = new Set(); + value.forEach((id) => { + const group = groups?.find((g) => g.id === id); + if (group?.scope) { + scopes.add(group.scope); + } + }); + return scopes; + }, [value, groups]); + + const options = useMemo(() => { + return (groups ?? []).map((group) => { + const scopeTranslationKey = group.scope + ? ScopeTranslationKeyMap[group.scope] + : undefined; + // Disable other options that share an already-selected scope. + const isSameScopeSelected = + !!group.scope && + selectedScopes.has(group.scope) && + !value.includes(group.id); + + return { + label: group.name, + value: group.id, + disabled: isSameScopeSelected, + suffix: scopeTranslationKey ? ( + + ({t(`knowledgeConfiguration.${scopeTranslationKey}`)}) + + ) : undefined, + }; + }); + }, [groups, selectedScopes, value, t]); + + return ( + + ); +} + +export function CompilationTemplateFormField({ + horizontal, + name = 'parser_config.compilation_template_group_id', +}: CompilationTemplateFormFieldProps) { + const { t } = useTranslation(); + + return ( + + {(field) => ( + + )} + + ); +} diff --git a/web/src/components/document-preview/document-header.tsx b/web/src/components/document-preview/document-header.tsx index aadac8c7e2..22f71ebfb9 100644 --- a/web/src/components/document-preview/document-header.tsx +++ b/web/src/components/document-preview/document-header.tsx @@ -1,5 +1,6 @@ import { formatDate } from '@/utils/date'; import { formatBytes } from '@/utils/file-util'; +import { PropsWithChildren } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { @@ -7,6 +8,7 @@ type Props = { name: string; create_date: string; className?: string; + wrapperClassName?: string; }; export default function DocumentHeader({ @@ -14,27 +16,32 @@ export default function DocumentHeader({ name, create_date, className, -}: Props) { + children, + wrapperClassName, +}: PropsWithChildren) { const sizeName = formatBytes(size); const dateStr = formatDate(create_date); const { t } = useTranslation(); return ( -
-

{name}

-
-
{t('chunk.size')}
-
{sizeName}
+
+
+

{name}

+
+
{t('chunk.size')}
+
{sizeName}
-
{t('chunk.uploadedTime')}
-
{dateStr}
-
+
{t('chunk.uploadedTime')}
+
{dateStr}
+
+ + {children}
); } diff --git a/web/src/components/expandable-search-input.tsx b/web/src/components/expandable-search-input.tsx new file mode 100644 index 0000000000..3b2d3990eb --- /dev/null +++ b/web/src/components/expandable-search-input.tsx @@ -0,0 +1,87 @@ +import { Button } from '@/components/ui/button'; +import { SearchInput } from '@/components/ui/input'; +import { cn } from '@/lib/utils'; +import { Search, X } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ExpandableSearchInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; + inputClassName?: string; + width?: number | string; +} + +export function ExpandableSearchInput({ + value, + onChange, + placeholder, + className, + inputClassName, + width = 192, +}: ExpandableSearchInputProps) { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + const handleToggle = useCallback(() => { + setIsOpen((prev) => { + const next = !prev; + if (!next) { + onChange(''); + } + return next; + }); + }, [onChange]); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + onChange(e.target.value); + }, + [onChange], + ); + + return ( +
+
+ + + + ) : null + } + /> +
+ {!isOpen && ( + + )} +
+ ); +} diff --git a/web/src/components/json-edit/index.tsx b/web/src/components/json-edit/index.tsx index 99a4a3bf80..f1185872e6 100644 --- a/web/src/components/json-edit/index.tsx +++ b/web/src/components/json-edit/index.tsx @@ -22,6 +22,7 @@ const JsonEditor: React.FC = ({ height = '400px', className = '', options = {}, + defaultExpanded = false, }) => { const containerRef = useRef(null); const editorRef = useRef(null); @@ -66,6 +67,10 @@ const JsonEditor: React.FC = ({ if (value) { editorRef.current.set(value); + + if (defaultExpanded) { + editorRef.current.expandAll?.(); + } } setIsLoading(false); @@ -138,6 +143,10 @@ const JsonEditor: React.FC = ({ editorRef.current = new JSONEditor(containerRef.current, newOptions); editorRef.current.set(currentData); + + if (defaultExpanded) { + editorRef.current.expandAll?.(); + } } catch (error) { console.error( 'Failed to reload jsoneditor with new language:', @@ -148,7 +157,7 @@ const JsonEditor: React.FC = ({ initEditorWithNewLanguage(); } - }, [i18n.language, value, onChange, options]); + }, [i18n.language, value, onChange, options, defaultExpanded]); useEffect(() => { if (editorRef.current && value !== undefined) { @@ -157,14 +166,22 @@ const JsonEditor: React.FC = ({ const currentJson = editorRef.current.get(); if (JSON.stringify(currentJson) !== JSON.stringify(value)) { editorRef.current.set(value); + + if (defaultExpanded) { + editorRef.current.expandAll?.(); + } } } catch (err) { console.error(err); // Skip update if there is a syntax error in the current editor editorRef.current.set(value); + + if (defaultExpanded) { + editorRef.current.expandAll?.(); + } } } - }, [value]); + }, [value, defaultExpanded]); return (
{ /> {modelName} {instanceName && ( - + {instanceName} )} diff --git a/web/src/components/markdown-editor.tsx b/web/src/components/markdown-editor.tsx new file mode 100644 index 0000000000..643e49fcbe --- /dev/null +++ b/web/src/components/markdown-editor.tsx @@ -0,0 +1,97 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import LexicalEditor from '@/lib/editor/lexical-editor'; +import RawMarkdownEditor from '@/lib/editor/raw-markdown-editor'; + +interface MarkdownEditorProps { + content: string; + onChange?: (content: string) => void; + readOnly?: boolean; +} + +export default function MarkdownEditor({ + content, + onChange, + readOnly = false, +}: MarkdownEditorProps) { + const [showSource, setShowSource] = useState(false); + const [rawContent, setRawContent] = useState(content); + const contentRef = useRef(content); + + useEffect(() => { + contentRef.current = content; + if (showSource) setRawContent(content); + }, [showSource, content]); + + const handleWysiwygChange = useCallback( + (md: string) => { + if (readOnly) return; + contentRef.current = md; + onChange?.(md); + }, + [onChange, readOnly], + ); + + const handleRawChange = useCallback( + (value: string) => { + if (readOnly) return; + setRawContent(value); + contentRef.current = value; + onChange?.(value); + }, + [onChange, readOnly], + ); + + const toggleSource = useCallback(() => { + if (!showSource) setRawContent(contentRef.current); + setShowSource((prev) => !prev); + }, [showSource]); + + return ( +
+
+
+ +
+
+ {readOnly ? ( +
+
+ +
+
+                {rawContent}
+              
+
+ ) : ( + + )} +
+
+
+ ); +} diff --git a/web/src/components/model-tree-select.tsx b/web/src/components/model-tree-select.tsx index b8f6c1c5aa..b7f4aa5b1f 100644 --- a/web/src/components/model-tree-select.tsx +++ b/web/src/components/model-tree-select.tsx @@ -197,12 +197,14 @@ export interface ModelTreeSelectFormFieldProps extends ModelTreeSelectProps { name?: string; label?: string; tooltip?: string; + required?: boolean; } export function ModelTreeSelectFormField({ name = 'llm_id', label, tooltip, + required, ...rest }: ModelTreeSelectFormFieldProps) { const form = useFormContext(); @@ -214,7 +216,11 @@ export function ModelTreeSelectFormField({ name={name} render={({ field }) => ( - {label && {label}} + {label && ( + + {label} + + )} { +}: GraphRagItemsProps) { const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); diff --git a/web/src/components/parse-configuration/raptor-form-fields.tsx b/web/src/components/parse-configuration/raptor-form-fields.tsx index e66ef54534..c0eb41d225 100644 --- a/web/src/components/parse-configuration/raptor-form-fields.tsx +++ b/web/src/components/parse-configuration/raptor-form-fields.tsx @@ -1,11 +1,11 @@ import { FormLayout } from '@/constants/form'; import { DocumentParserType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; +import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants'; import { GenerateLogButton, - GenerateType, IGenerateLogButtonProps, -} from '@/pages/dataset/dataset/generate-button/generate'; +} from '@/pages/dataset/dataset/generate-button/generate-log-button'; import random from 'lodash/random'; import { Shuffle } from 'lucide-react'; import { useCallback, useEffect, useMemo } from 'react'; diff --git a/web/src/components/ui/tree-view.tsx b/web/src/components/ui/tree-view.tsx index 6327370ef6..04568725b7 100644 --- a/web/src/components/ui/tree-view.tsx +++ b/web/src/components/ui/tree-view.tsx @@ -1,3 +1,5 @@ +// https://github.com/MrLightful/shadcn-tree-view + 'use client'; import { cn } from '@/lib/utils'; diff --git a/web/src/constants/agent.tsx b/web/src/constants/agent.tsx index baf98484d1..e5e5d16e5a 100644 --- a/web/src/constants/agent.tsx +++ b/web/src/constants/agent.tsx @@ -66,16 +66,6 @@ export enum AgentQuery { Category = 'category', } -export enum DataflowOperator { - Begin = 'File', - Note = 'Note', - Parser = 'Parser', - Tokenizer = 'Tokenizer', - TokenChunker = 'TokenChunker', - TitleChunker = 'TitleChunker', - Extractor = 'Extractor', -} - export enum Operator { Begin = 'Begin', Retrieval = 'Retrieval', @@ -124,6 +114,7 @@ export enum Operator { TokenChunker = 'TokenChunker', TitleChunker = 'TitleChunker', Extractor = 'Extractor', + Compilation = 'Compiler', Loop = 'Loop', LoopStart = 'LoopItem', ExitLoop = 'ExitLoop', diff --git a/web/src/constants/compilation.ts b/web/src/constants/compilation.ts new file mode 100644 index 0000000000..362092c1ce --- /dev/null +++ b/web/src/constants/compilation.ts @@ -0,0 +1,14 @@ +export const enum CompilationTemplateKind { + Artifacts = 'artifacts', + KnowledgeGraph = 'knowledge_graph', + Timeline = 'timeline', + PageIndex = 'page_index', + Tree = 'tree', + Empty = 'empty', + MindMap = 'mind_map', +} + +export const enum CompilationTemplateScope { + File = 'file', + Dataset = 'dataset', +} diff --git a/web/src/hooks/logic-hooks.ts b/web/src/hooks/logic-hooks.ts index 1174cbe97d..a8b926e771 100644 --- a/web/src/hooks/logic-hooks.ts +++ b/web/src/hooks/logic-hooks.ts @@ -126,8 +126,11 @@ export const useHandleSearchChange = () => { return { handleInputChange, searchString, pagination, setPagination }; }; -export const useGetPagination = () => { - const [pagination, setPagination] = useState({ page: 1, pageSize: 10 }); +export const useGetPagination = (options?: { pageSize?: number }) => { + const [pagination, setPagination] = useState({ + page: 1, + pageSize: options?.pageSize ?? 10, + }); const { t } = useTranslate('common'); const onPageChange: Pagination['onChange'] = useCallback( @@ -152,6 +155,7 @@ export const useGetPagination = () => { return { pagination: currentPagination, + setPagination, }; }; diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index b8bca526f9..e9f0a79c70 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -57,6 +57,13 @@ export const useNavigatePage = () => { [navigate], ); + const navigateToCompilation = useCallback( + (id: string) => () => { + navigate(`${Routes.DatasetBase}${Routes.Compilation}/${id}`); + }, + [navigate], + ); + const navigateToHome = useCallback(() => { navigate(Routes.Root); }, [navigate]); @@ -201,6 +208,21 @@ export const useNavigatePage = () => { navigate(`${Routes.UserSetting}${Routes.Model}`); }, [navigate]); + const navigateToCompilationTemplates = useCallback(() => { + navigate(`${Routes.UserSetting}${Routes.CompilationTemplates}`); + }, [navigate]); + + const navigateToCompilationTemplate = useCallback( + (id?: string) => () => { + if (id && id !== 'create') { + navigate(`${Routes.CompilationTemplatesCreateNext}/${id}`); + } else { + navigate(Routes.CompilationTemplatesCreateNext); + } + }, + [navigate], + ); + return { navigateToDatasetList, navigateToDataset, @@ -224,9 +246,12 @@ export const useNavigatePage = () => { navigateToOldProfile, navigateToDataflowResult, navigateToDataFile, + navigateToCompilation, navigateToDataSourceDetail, navigateToMemory, navigateToMemoryList, navigateToModelSetting, + navigateToCompilationTemplates, + navigateToCompilationTemplate, }; }; diff --git a/web/src/hooks/use-chunk-request.ts b/web/src/hooks/use-chunk-request.ts index ac1b11c630..b8698dbd8d 100644 --- a/web/src/hooks/use-chunk-request.ts +++ b/web/src/hooks/use-chunk-request.ts @@ -122,12 +122,14 @@ export const useFetchChunk = ( export const useFetchNextChunkList = ( enabled = true, + options?: { chunkIds?: string[] }, ): ResponseGetType<{ data: IChunk[]; total: number; documentInfo: IKnowledgeFile; }> & IChunkListResult => { + const chunkIds = options?.chunkIds; const { pagination, setPagination } = useGetPaginationWithRouter(); const { documentId, knowledgeId } = useGetKnowledgeSearchParams(); const { searchString, handleInputChange } = useHandleSearchChange(); @@ -147,6 +149,7 @@ export const useFetchNextChunkList = ( pagination.pageSize, debouncedSearchString, available, + chunkIds, ], placeholderData: (previousData: any) => previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183 @@ -156,10 +159,13 @@ export const useFetchNextChunkList = ( const { data } = await kbService.chunkList({ kb_id: knowledgeId, doc_id: documentId, - page: pagination.current, - size: pagination.pageSize, + page: chunkIds?.length ? 1 : pagination.current, + size: chunkIds?.length + ? Math.max(chunkIds.length, 100) + : pagination.pageSize, available_int: available, keywords: searchString, + chunk_ids: chunkIds, }); if (data.code === 0) { const res = data.data; diff --git a/web/src/hooks/use-compilation-template-group-request.ts b/web/src/hooks/use-compilation-template-group-request.ts new file mode 100644 index 0000000000..b07068e4b9 --- /dev/null +++ b/web/src/hooks/use-compilation-template-group-request.ts @@ -0,0 +1,255 @@ +import message from '@/components/ui/message'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; +import { + ICreateCompilationTemplateGroupRequestBody, + IUpdateCompilationTemplateGroupRequestBody, +} from '@/interfaces/request/compilation-template'; +import i18n from '@/locales/config'; +import { + compilationTemplateGroupService, + createCompilationTemplateGroup, + deleteCompilationTemplateGroup, + getCompilationTemplateGroup, + updateCompilationTemplateGroup, +} from '@/services/compilation-template-group-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDebounce } from 'ahooks'; +import { useCallback, useMemo } from 'react'; + +import { + useGetPaginationWithRouter, + useHandleSearchChange, +} from './logic-hooks'; + +export const enum CompilationTemplateGroupApiAction { + FetchCompilationTemplateGroups = 'fetchCompilationTemplateGroups', + FetchCompilationTemplateGroup = 'fetchCompilationTemplateGroup', + CreateCompilationTemplateGroup = 'createCompilationTemplateGroup', + UpdateCompilationTemplateGroup = 'updateCompilationTemplateGroup', + DeleteCompilationTemplateGroup = 'deleteCompilationTemplateGroup', +} + +export const CompilationTemplateGroupKeys = { + list: (keywords?: string, page?: number, pageSize?: number) => + [ + CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups, + { keywords, page, pageSize }, + ] as const, + detail: (id?: string) => + [ + CompilationTemplateGroupApiAction.FetchCompilationTemplateGroup, + id, + ] as const, + all: () => + [CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups] as const, +}; + +export const useFetchCompilationTemplateGroupsByPage = () => { + const { searchString, handleInputChange } = useHandleSearchChange(); + const { pagination, setPagination } = useGetPaginationWithRouter(); + const debouncedSearchString = useDebounce(searchString, { wait: 500 }); + + const { data, isFetching: loading } = useQuery<{ + groups: ICompilationTemplateGroup[]; + total: number; + }>({ + queryKey: CompilationTemplateGroupKeys.list( + debouncedSearchString, + pagination.current, + pagination.pageSize, + ), + initialData: { + groups: [], + total: 0, + }, + gcTime: 0, + queryFn: async () => { + const { data } = await compilationTemplateGroupService.listGroups( + { + params: { + keywords: debouncedSearchString, + page: pagination.current, + page_size: pagination.pageSize, + }, + }, + true, + ); + + return { + groups: (data?.data?.groups ?? []) as ICompilationTemplateGroup[], + total: data?.data?.total ?? 0, + }; + }, + }); + + const currentPagination = useMemo( + () => ({ ...pagination, total: data?.total ?? 0 }), + [pagination, data?.total], + ); + + return { + groups: data?.groups ?? [], + total: data?.total ?? 0, + searchString, + handleInputChange, + pagination: currentPagination, + setPagination, + loading, + }; +}; + +export const useFetchCompilationTemplateGroup = (id?: string) => { + const { data, isFetching: loading } = useQuery< + ICompilationTemplateGroup | undefined + >({ + queryKey: CompilationTemplateGroupKeys.detail(id), + enabled: !!id && id !== 'create', + gcTime: 0, + queryFn: async () => { + if (!id || id === 'create') return undefined; + const { data } = await getCompilationTemplateGroup(id); + return data?.data as ICompilationTemplateGroup | undefined; + }, + }); + + return { data, loading }; +}; + +export const useCreateCompilationTemplateGroup = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [ + CompilationTemplateGroupApiAction.CreateCompilationTemplateGroup, + ], + mutationFn: async (params: ICreateCompilationTemplateGroupRequestBody) => { + const { data } = await createCompilationTemplateGroup(params); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ + CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups, + ], + }); + }, + }); + + const createGroup = useCallback( + async (params: ICreateCompilationTemplateGroupRequestBody) => { + const result = await mutateAsync(params); + if (result.code === 0) { + message.success(i18n.t('message.created')); + } + return result; + }, + [mutateAsync], + ); + + return { data, loading, createGroup }; +}; + +export const useUpdateCompilationTemplateGroup = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [ + CompilationTemplateGroupApiAction.UpdateCompilationTemplateGroup, + ], + mutationFn: async ({ + id, + params, + }: { + id: string; + params: IUpdateCompilationTemplateGroupRequestBody; + }) => { + const { data } = await updateCompilationTemplateGroup(id, params); + return data; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups, + ], + }); + queryClient.invalidateQueries({ + queryKey: CompilationTemplateGroupKeys.detail(variables.id), + }); + }, + }); + + const updateGroup = useCallback( + async (id: string, params: IUpdateCompilationTemplateGroupRequestBody) => { + const result = await mutateAsync({ id, params }); + if (result.code === 0) { + message.success(i18n.t('message.modified')); + } + return result; + }, + [mutateAsync], + ); + + return { data, loading, updateGroup }; +}; + +export const useDeleteCompilationTemplateGroup = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [ + CompilationTemplateGroupApiAction.DeleteCompilationTemplateGroup, + ], + mutationFn: async (id: string) => { + const { data } = await deleteCompilationTemplateGroup(id); + if (data.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: [ + CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups, + ], + }); + } + return data?.data ?? true; + }, + }); + + const deleteGroup = useCallback( + async (id: string) => { + await mutateAsync(id); + }, + [mutateAsync], + ); + + return { data, loading, deleteGroup }; +}; + +export const useFetchAllCompilationTemplateGroups = () => { + const { data, isFetching: loading } = useQuery({ + queryKey: CompilationTemplateGroupKeys.all(), + initialData: [], + gcTime: 0, + queryFn: async () => { + const { data } = await compilationTemplateGroupService.listGroups( + { + params: { keywords: '', page: 1, page_size: 100 }, + }, + true, + ); + return (data?.data?.groups ?? []) as ICompilationTemplateGroup[]; + }, + }); + + return { groups: data ?? [], loading }; +}; diff --git a/web/src/hooks/use-compilation-template-request.ts b/web/src/hooks/use-compilation-template-request.ts new file mode 100644 index 0000000000..438e5c5bc9 --- /dev/null +++ b/web/src/hooks/use-compilation-template-request.ts @@ -0,0 +1,304 @@ +import message from '@/components/ui/message'; +import { + ICompilationTemplate, + ICompilationTemplateBuiltin, + ICompilationTemplateListResult, + ICompilationTemplateSection, + IWikiPreset, +} from '@/interfaces/database/compilation-template'; +import { + ICreateCompilationTemplateRequestBody, + IUpdateCompilationTemplateRequestBody, +} from '@/interfaces/request/compilation-template'; +import i18n from '@/locales/config'; +import compilationTemplateService, { + createCompilationTemplate, + deleteCompilationTemplate, + getCompilationTemplate, + listBuiltinCompilationTemplates, + listWikiPresets, + updateCompilationTemplate, +} from '@/services/compilation-template-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDebounce } from 'ahooks'; +import { useCallback, useMemo } from 'react'; + +import { + useGetPaginationWithRouter, + useHandleSearchChange, +} from './logic-hooks'; + +export const enum CompilationTemplateApiAction { + FetchCompilationTemplates = 'fetchCompilationTemplates', + FetchCompilationTemplate = 'fetchCompilationTemplate', + FetchBuiltinCompilationTemplates = 'fetchBuiltinCompilationTemplates', + CreateCompilationTemplate = 'createCompilationTemplate', + UpdateCompilationTemplate = 'updateCompilationTemplate', + DeleteCompilationTemplate = 'deleteCompilationTemplate', + FetchWikiPresets = 'fetchWikiPresets', +} + +export const CompilationTemplateKeys = { + list: (keywords?: string, page?: number, pageSize?: number) => + [ + CompilationTemplateApiAction.FetchCompilationTemplates, + { keywords, page, pageSize }, + ] as const, + detail: (id?: string) => + [CompilationTemplateApiAction.FetchCompilationTemplate, id] as const, + builtins: () => + [CompilationTemplateApiAction.FetchBuiltinCompilationTemplates] as const, + all: () => [CompilationTemplateApiAction.FetchCompilationTemplates] as const, + wikiPresets: () => [CompilationTemplateApiAction.FetchWikiPresets] as const, +}; + +export const useFetchCompilationTemplatesByPage = () => { + const { searchString, handleInputChange } = useHandleSearchChange(); + const { pagination, setPagination } = useGetPaginationWithRouter(); + const debouncedSearchString = useDebounce(searchString, { wait: 500 }); + + const { data, isFetching: loading } = + useQuery({ + queryKey: CompilationTemplateKeys.list( + debouncedSearchString, + pagination.current, + pagination.pageSize, + ), + initialData: { + templates: [], + total: 0, + }, + gcTime: 0, + queryFn: async () => { + const { data } = await compilationTemplateService.listTemplates( + { + params: { + keywords: debouncedSearchString, + page: pagination.current, + page_size: pagination.pageSize, + }, + }, + true, + ); + + return { + templates: (data?.data?.templates ?? []) as ICompilationTemplate[], + total: data?.data?.total ?? 0, + }; + }, + }); + + const currentPagination = useMemo( + () => ({ ...pagination, total: data?.total ?? 0 }), + [pagination, data?.total], + ); + + return { + templates: data?.templates ?? [], + total: data?.total ?? 0, + searchString, + handleInputChange, + pagination: currentPagination, + setPagination, + loading, + }; +}; + +export const useFetchCompilationTemplate = (id?: string) => { + const { data, isFetching: loading } = useQuery< + ICompilationTemplate | undefined + >({ + queryKey: CompilationTemplateKeys.detail(id), + enabled: !!id && id !== 'create', + gcTime: 0, + queryFn: async () => { + if (!id || id === 'create') return undefined; + const { data } = await getCompilationTemplate(id); + return data?.data as ICompilationTemplate | undefined; + }, + }); + + return { data, loading }; +}; + +export const useFetchBuiltinCompilationTemplates = () => { + const { data, isFetching: loading } = useQuery( + { + queryKey: CompilationTemplateKeys.builtins(), + initialData: [], + gcTime: 0, + queryFn: async () => { + const { data } = await listBuiltinCompilationTemplates(); + return (data?.data ?? []) as ICompilationTemplateBuiltin[]; + }, + }, + ); + + const kindOptions = useMemo(() => { + const kindSet = new Set(); + (data ?? []).forEach((template) => { + if (template?.kind) kindSet.add(template.kind); + }); + return Array.from(kindSet) + .sort() + .map((value) => ({ label: value, value })); + }, [data]); + + const typeOptions = useMemo(() => { + const typeSet = new Set(); + (data ?? []).forEach((template) => { + Object.entries(template?.config ?? {}).forEach(([key, section]) => { + if (['kind', 'llm_id', 'global_rules'].includes(key)) return; + (section as ICompilationTemplateSection)?.fields?.forEach((field) => { + if (field?.type) typeSet.add(field.type); + }); + }); + }); + return Array.from(typeSet) + .sort() + .map((value) => ({ label: value, value })); + }, [data]); + + return { data, typeOptions, kindOptions, loading }; +}; + +export const useCreateCompilationTemplate = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [CompilationTemplateApiAction.CreateCompilationTemplate], + mutationFn: async (params: ICreateCompilationTemplateRequestBody) => { + const { data } = await createCompilationTemplate(params); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CompilationTemplateApiAction.FetchCompilationTemplates], + }); + }, + }); + + const createTemplate = useCallback( + async (params: ICreateCompilationTemplateRequestBody) => { + const result = await mutateAsync(params); + if (result.code === 0) { + message.success(i18n.t('message.created')); + } + return result; + }, + [mutateAsync], + ); + + return { data, loading, createTemplate }; +}; + +export const useUpdateCompilationTemplate = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [CompilationTemplateApiAction.UpdateCompilationTemplate], + mutationFn: async ({ + id, + params, + }: { + id: string; + params: IUpdateCompilationTemplateRequestBody; + }) => { + const { data } = await updateCompilationTemplate(id, params); + return data; + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: [CompilationTemplateApiAction.FetchCompilationTemplates], + }); + queryClient.invalidateQueries({ + queryKey: CompilationTemplateKeys.detail(variables.id), + }); + }, + }); + + const updateTemplate = useCallback( + async (id: string, params: IUpdateCompilationTemplateRequestBody) => { + const result = await mutateAsync({ id, params }); + if (result.code === 0) { + message.success(i18n.t('message.modified')); + } + return result; + }, + [mutateAsync], + ); + + return { data, loading, updateTemplate }; +}; + +export const useDeleteCompilationTemplate = () => { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [CompilationTemplateApiAction.DeleteCompilationTemplate], + mutationFn: async (id: string) => { + const { data } = await deleteCompilationTemplate(id); + if (data.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: [CompilationTemplateApiAction.FetchCompilationTemplates], + }); + } + return data?.data ?? true; + }, + }); + + const deleteTemplate = useCallback( + async (id: string) => { + await mutateAsync(id); + }, + [mutateAsync], + ); + + return { data, loading, deleteTemplate }; +}; + +export const useFetchAllCompilationTemplates = () => { + const { data, isFetching: loading } = useQuery({ + queryKey: CompilationTemplateKeys.all(), + initialData: [], + gcTime: 0, + queryFn: async () => { + const { data } = await compilationTemplateService.listTemplates( + { + params: { keywords: '', page: 1, page_size: 100 }, + }, + true, + ); + return (data?.data?.templates ?? []) as ICompilationTemplate[]; + }, + }); + + return { templates: data ?? [], loading }; +}; + +export const useFetchWikiPresets = () => { + const { data, isFetching: loading } = useQuery({ + queryKey: CompilationTemplateKeys.wikiPresets(), + initialData: [], + gcTime: 0, + queryFn: async () => { + const { data } = await listWikiPresets(); + return (data?.data ?? []) as IWikiPreset[]; + }, + }); + + return { data: data ?? [], loading }; +}; diff --git a/web/src/hooks/use-dataset-skill-request.ts b/web/src/hooks/use-dataset-skill-request.ts new file mode 100644 index 0000000000..b6ac07cb7c --- /dev/null +++ b/web/src/hooks/use-dataset-skill-request.ts @@ -0,0 +1,53 @@ +import { + DatasetSkillPage, + DatasetSkillTree, +} from '@/interfaces/database/dataset-skill'; +import datasetSkillService from '@/services/dataset-skill-service'; +import { useQuery } from '@tanstack/react-query'; + +import { useKnowledgeBaseId } from './use-knowledge-request'; + +export const DatasetSkillKeys = { + all: (kbId: string) => ['dataset_skill', kbId] as const, + tree: (kbId: string) => ['dataset_skill', kbId, 'tree'] as const, + page: (kbId: string, skillKwd: string) => + ['dataset_skill', kbId, 'page', skillKwd] as const, +}; + +export function useFetchDatasetSkillTree() { + const kbId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery({ + queryKey: DatasetSkillKeys.tree(kbId), + initialData: null, + enabled: !!kbId, + gcTime: 0, + queryFn: async () => { + const { data } = await datasetSkillService.getTree({ datasetId: kbId }); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} + +export function useFetchDatasetSkillPage(skillKwd: string | null | undefined) { + const kbId = useKnowledgeBaseId(); + const enabled = !!kbId && !!skillKwd; + + const { data, isFetching: loading } = useQuery({ + queryKey: DatasetSkillKeys.page(kbId, skillKwd ?? ''), + initialData: null, + enabled, + gcTime: 0, + queryFn: async () => { + const { data } = await datasetSkillService.getPage({ + datasetId: kbId, + skillKwd: skillKwd!, + }); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} diff --git a/web/src/hooks/use-document-request.ts b/web/src/hooks/use-document-request.ts index 81daad4aa3..70ed0943b4 100644 --- a/web/src/hooks/use-document-request.ts +++ b/web/src/hooks/use-document-request.ts @@ -9,12 +9,14 @@ import { IDocumentInfo, IDocumentInfoFilter, } from '@/interfaces/database/document'; +import { IStructureGraphResponse } from '@/interfaces/database/document-structure'; import { IChangeParserConfigRequestBody, IDocumentMetaRequestBody, } from '@/interfaces/request/document'; import i18n from '@/locales/config'; import { EMPTY_METADATA_FIELD } from '@/pages/dataset/dataset/use-select-filters'; +import documentStructureService from '@/services/document-structure-service'; import kbService, { changeDocumentParser, changeDocumentsStatus, @@ -59,6 +61,25 @@ export const enum DocumentApiAction { ParseDocument = 'parseDocument', } +export const enum DocumentStructureApiAction { + FetchDocumentStructureGraph = 'fetchDocumentStructureGraph', + DeleteDocumentStructureGraph = 'deleteDocumentStructureGraph', +} + +const DocumentKeys = { + byIds: (ids: string[]) => + [DocumentApiAction.FetchDocumentList, 'byIds', ids] as const, +}; + +export const DocumentStructureKeys = { + graph: (datasetId: string, documentId: string) => + [ + DocumentStructureApiAction.FetchDocumentStructureGraph, + datasetId, + documentId, + ] as const, +}; + export const useUploadDocument = () => { const queryClient = useQueryClient(); const { id } = useParams(); @@ -214,6 +235,37 @@ export const useFetchDocumentList = (loop = true) => { }; }; +export const useFetchDocumentsByIds = (ids: string[]) => { + const { id: datasetId } = useParams(); + + const { data, isFetching: loading } = useQuery<{ + docs: IDocumentInfo[]; + total: number; + }>({ + queryKey: DocumentKeys.byIds(ids), + enabled: ids.length > 0 && !!datasetId, + initialData: { docs: [], total: 0 }, + queryFn: async () => { + const ret = await listDocument( + { + id: datasetId, + page: 1, + page_size: ids.length, + }, + { + ids, + }, + ); + if (ret.data.code === 0) { + return ret.data.data; + } + return { docs: [], total: 0 }; + }, + }); + + return { documents: data.docs, loading }; +}; + // get document filter export const useGetDocumentFilter = (): { filter: IDocumentInfoFilter; @@ -566,3 +618,56 @@ export const useFetchDocumentThumbnailsByIds = () => { return { data, setDocumentIds }; }; + +export function useFetchDocumentStructureGraph() { + const { knowledgeId: datasetId, documentId } = useGetKnowledgeSearchParams(); + const enabled = !!datasetId && !!documentId; + + const { data, isFetching: loading } = + useQuery({ + queryKey: DocumentStructureKeys.graph(datasetId, documentId), + enabled, + initialData: null, + gcTime: 0, + queryFn: async () => { + const { data } = + await documentStructureService.getDocumentStructureGraph( + datasetId, + documentId, + ); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} + +export function useDeleteDocumentStructureGraph() { + const { knowledgeId: datasetId, documentId } = useGetKnowledgeSearchParams(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [DocumentStructureApiAction.DeleteDocumentStructureGraph], + mutationFn: async (templateId: string) => { + const { data } = + await documentStructureService.deleteDocumentStructureGraph( + datasetId, + documentId, + templateId, + ); + if (data.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: DocumentStructureKeys.graph(datasetId, documentId), + }); + } + return data; + }, + }); + + return { deleteDocumentStructureGraph: mutateAsync, loading, data }; +} diff --git a/web/src/hooks/use-knowledge-request.ts b/web/src/hooks/use-knowledge-request.ts index c218d391d0..09e03bef91 100644 --- a/web/src/hooks/use-knowledge-request.ts +++ b/web/src/hooks/use-knowledge-request.ts @@ -1,28 +1,48 @@ import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; import message from '@/components/ui/message'; import { ParseType } from '@/constants/knowledge'; -import { ResponsePostType } from '@/interfaces/database/base'; +import { ResponsePostType, ResponseType } from '@/interfaces/database/base'; import { + IArtifact, + IArtifactGraph, + IArtifactPage, + IArtifactTopic, IDataset, IDatasetListResult, IKnowledgeGraph, INextTestingResult, IRenameTag, ITestingResult, + IWikiCommit, + IWikiCommitDetail, + IWikiCommitListResponse, } from '@/interfaces/database/dataset'; -import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge'; +import { + IFetchArtifactGraphRequestParams, + ITestRetrievalRequestBody, + IUpdateArtifactPageRequestParams, +} from '@/interfaces/request/knowledge'; import i18n from '@/locales/config'; import kbService, { + clearWiki, deleteKnowledgeGraph, + getArtifactGraph, + getArtifactPage, getKbDetail, getKnowledgeGraph, + getWikiCommit, + listArtifactTopics, + listArtifacts, listDataset, listTag, + listWikiCommits, removeTag, renameTag, + updateArtifactPage, updateKb, } from '@/services/knowledge-service'; import { + useInfiniteQuery, useIsMutating, useMutation, useMutationState, @@ -47,10 +67,18 @@ export const enum KnowledgeApiAction { SaveKnowledge = 'saveKnowledge', FetchKnowledgeDetail = 'fetchKnowledgeDetail', FetchKnowledgeGraph = 'fetchKnowledgeGraph', + FetchArtifactList = 'fetchArtifactList', + FetchArtifactTopicList = 'fetchArtifactTopicList', + FetchArtifactPage = 'fetchArtifactPage', + FetchArtifactGraph = 'fetchArtifactGraph', + UpdateArtifactPage = 'updateArtifactPage', + FetchWikiCommits = 'fetchWikiCommits', + FetchWikiCommit = 'fetchWikiCommit', FetchMetadata = 'fetchMetadata', FetchMetadataKeys = 'fetchMetadataKeys', FetchKnowledgeList = 'fetchKnowledgeList', RemoveKnowledgeGraph = 'removeKnowledgeGraph', + ClearWiki = 'clearWiki', } export const useKnowledgeBaseId = (): string => { @@ -324,6 +352,301 @@ export const useFetchKnowledgeBaseConfiguration = (props?: { return { data, loading }; }; +export const ArtifactKeys = { + list: ( + datasetId: string, + keywords: string, + topic?: string, + pageType?: string, + ) => + [ + KnowledgeApiAction.FetchArtifactList, + datasetId, + keywords, + topic, + pageType, + ] as const, + listByDataset: (datasetId: string) => + [KnowledgeApiAction.FetchArtifactList, datasetId] as const, + detail: (datasetId: string, pageType: string, slug: string) => + [KnowledgeApiAction.FetchArtifactPage, datasetId, pageType, slug] as const, +}; + +export const ArtifactTopicKeys = { + list: (datasetId: string, keywords: string) => + [KnowledgeApiAction.FetchArtifactTopicList, datasetId, keywords] as const, + listByDataset: (datasetId: string) => + [KnowledgeApiAction.FetchArtifactTopicList, datasetId] as const, +}; + +const wikiCommitKeys = { + list: (datasetId: string, pageType: string, slug: string) => + [KnowledgeApiAction.FetchWikiCommits, datasetId, pageType, slug] as const, + detail: (datasetId: string, commitId: string) => + [KnowledgeApiAction.FetchWikiCommit, datasetId, commitId] as const, +}; + +export const useFetchWikiCommits = ( + artifact: IArtifact | null, + enabled = true, +) => { + const knowledgeBaseId = useKnowledgeBaseId(); + const pageType = artifact?.page_type ?? ''; + const slug = artifact?.slug ?? ''; + + const { data, isFetching: loading } = + useQuery({ + queryKey: wikiCommitKeys.list(knowledgeBaseId, pageType, slug), + enabled: + !!knowledgeBaseId && !!artifact && !!pageType && !!slug && enabled, + gcTime: 0, + queryFn: async () => { + const { data } = await listWikiCommits(knowledgeBaseId, pageType, slug); + // The merged file-commit endpoint returns {total, page, page_size, commits}, + // while the existing components expect {total, items}. Normalize here. + const raw = (data?.data ?? {}) as { + total?: number; + items?: IWikiCommit[]; + commits?: IWikiCommit[]; + }; + return { + total: raw.total ?? 0, + items: raw.items ?? raw.commits ?? [], + }; + }, + }); + + return { + commits: data?.items ?? [], + total: data?.total ?? 0, + loading, + }; +}; + +export function useFetchWikiCommit(commitId: string | null) { + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery({ + queryKey: wikiCommitKeys.detail(knowledgeBaseId, commitId ?? ''), + enabled: !!knowledgeBaseId && !!commitId, + gcTime: 0, + queryFn: async () => { + const { data } = await getWikiCommit(knowledgeBaseId, commitId!); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} + +type UseFetchArtifactListOptions = { + keywords?: string; + topic?: string; + pageType?: string; + enabled?: boolean; +}; + +export const useFetchArtifactList = ( + options: UseFetchArtifactListOptions = {}, +) => { + const { keywords = '', topic, pageType, enabled = true } = options; + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + useInfiniteQuery<{ + artifacts: IArtifact[]; + total: number; + }>({ + queryKey: ArtifactKeys.list(knowledgeBaseId, keywords, topic, pageType), + enabled: !!knowledgeBaseId && enabled && !!topic, + gcTime: 0, + initialPageParam: 1, + queryFn: async ({ pageParam }) => { + const page = pageParam as number; + const { data } = await listArtifacts(knowledgeBaseId, { + page, + page_size: 30, + keywords, + topic, + page_type: pageType, + }); + + const responseData = data?.data; + + return { + artifacts: responseData?.items ?? [], + total: responseData?.total ?? 0, + }; + }, + getNextPageParam: (lastPage, allPages) => { + const loadedCount = allPages.reduce( + (sum, page) => sum + page.artifacts.length, + 0, + ); + return loadedCount < lastPage.total ? allPages.length + 1 : undefined; + }, + }); + + const artifacts = useMemo( + () => data?.pages.flatMap((page) => page.artifacts) ?? [], + [data], + ); + + const loading = isFetching || isFetchingNextPage; + + const handleScroll = useCallback( + (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const threshold = 50; + if ( + scrollHeight - scrollTop - clientHeight <= threshold && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); + } + }, + [fetchNextPage, hasNextPage, isFetchingNextPage], + ); + + return { + artifacts, + loading, + handleScroll, + hasMore: !!hasNextPage, + }; +}; + +type UseFetchArtifactTopicListOptions = { + keywords?: string; + enabled?: boolean; +}; + +export const useFetchArtifactTopicList = ( + options: UseFetchArtifactTopicListOptions = {}, +) => { + const { keywords = '', enabled = true } = options; + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + useInfiniteQuery<{ + topics: IArtifactTopic[]; + total: number; + }>({ + queryKey: ArtifactTopicKeys.list(knowledgeBaseId, keywords), + enabled: !!knowledgeBaseId && enabled, + gcTime: 0, + initialPageParam: 1, + queryFn: async ({ pageParam }) => { + const page = pageParam as number; + const { data } = await listArtifactTopics(knowledgeBaseId, { + page, + page_size: 30, + keywords, + }); + + const responseData = data?.data; + + return { + topics: responseData?.items ?? [], + total: responseData?.total ?? 0, + }; + }, + getNextPageParam: (lastPage, allPages) => { + const loadedCount = allPages.reduce( + (sum, page) => sum + page.topics.length, + 0, + ); + return loadedCount < lastPage.total ? allPages.length + 1 : undefined; + }, + }); + + const topics = useMemo( + () => data?.pages.flatMap((page) => page.topics) ?? [], + [data], + ); + + const loading = isFetching || isFetchingNextPage; + + const handleScroll = useCallback( + (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const threshold = 50; + if ( + scrollHeight - scrollTop - clientHeight <= threshold && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); + } + }, + [fetchNextPage, hasNextPage, isFetchingNextPage], + ); + + return { + topics, + loading, + handleScroll, + hasMore: !!hasNextPage, + }; +}; + +export function useFetchArtifactPage(artifact: IArtifact | null) { + const knowledgeBaseId = useKnowledgeBaseId(); + const pageType = artifact?.page_type ?? ''; + const slug = artifact?.slug ?? ''; + + const { data, isFetching: loading } = useQuery({ + queryKey: ArtifactKeys.detail(knowledgeBaseId, pageType, slug), + enabled: !!knowledgeBaseId && !!artifact && !!pageType && !!slug, + gcTime: 0, + queryFn: async () => { + const { data } = await getArtifactPage(knowledgeBaseId, pageType, slug); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} + +export const useUpdateArtifactPage = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation< + ResponseType, + Error, + IUpdateArtifactPageRequestParams + >({ + mutationKey: [KnowledgeApiAction.UpdateArtifactPage], + mutationFn: async (params) => { + const { data = {} } = await updateArtifactPage( + knowledgeBaseId, + params.pageType, + params.slug, + params.body, + ); + if (data.code === 0) { + message.success(i18n.t(`message.updated`)); + queryClient.invalidateQueries({ + queryKey: ArtifactKeys.detail( + knowledgeBaseId, + params.pageType, + params.slug, + ), + }); + } + return data; + }, + }); + + return { data, loading, updateArtifactPage: mutateAsync }; +}; + export function useFetchKnowledgeGraph() { const knowledgeBaseId = useKnowledgeBaseId(); @@ -341,6 +664,31 @@ export function useFetchKnowledgeGraph() { return { data, loading }; } +export const artifactGraphKeys = { + graph: (datasetId: string, params?: IFetchArtifactGraphRequestParams) => + [KnowledgeApiAction.FetchArtifactGraph, datasetId, params?.node] as const, +}; + +export function useFetchArtifactGraph( + params?: IFetchArtifactGraphRequestParams, + options?: { enabled?: boolean }, +) { + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery({ + queryKey: artifactGraphKeys.graph(knowledgeBaseId, params), + initialData: { entities: [], relations: [] } as IArtifactGraph, + enabled: !!knowledgeBaseId && (options?.enabled ?? true), + gcTime: 0, + queryFn: async () => { + const { data } = await getArtifactGraph(knowledgeBaseId, params); + return data?.data ?? { entities: [], relations: [] }; + }, + }); + + return { data, loading }; +} + export function useFetchKnowledgeMetadata(kbIds: string[] = []) { const { data, isFetching: loading } = useQuery< Record> @@ -403,6 +751,37 @@ export const useRemoveKnowledgeGraph = () => { return { data, loading, removeKnowledgeGraph: mutateAsync }; }; +export const useClearWiki = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [KnowledgeApiAction.ClearWiki], + mutationFn: async () => { + const { data } = await clearWiki(knowledgeBaseId); + if (data?.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: ArtifactKeys.listByDataset(knowledgeBaseId), + }); + queryClient.invalidateQueries({ + queryKey: ArtifactTopicKeys.listByDataset(knowledgeBaseId), + }); + queryClient.invalidateQueries({ + queryKey: artifactGraphKeys.graph(knowledgeBaseId), + }); + } + return data; + }, + }); + + return { data, loading, clearWiki: mutateAsync }; +}; + export const useFetchKnowledgeList = ( shouldFilterListWithoutDocument: boolean = false, keywords = '', diff --git a/web/src/interfaces/database/compilation-template.ts b/web/src/interfaces/database/compilation-template.ts new file mode 100644 index 0000000000..d68461a1f2 --- /dev/null +++ b/web/src/interfaces/database/compilation-template.ts @@ -0,0 +1,74 @@ +export interface ICompilationTemplateField { + type?: string; + description?: string; + rule?: string; + [key: string]: string | undefined; +} + +export interface ICompilationTemplateSection { + description?: string; + fields: ICompilationTemplateField[]; +} + +export interface ICompilationTemplateRaptorConfig { + prompt?: string; + max_token?: number; + threshold?: number; + rechunk?: boolean; +} + +export interface ICompilationTemplateConfig { + kind?: string; + llm_id?: string; + entity?: ICompilationTemplateSection; + relation?: ICompilationTemplateSection; + raptor?: ICompilationTemplateRaptorConfig; + global_rules?: string; + [section: string]: + | ICompilationTemplateSection + | ICompilationTemplateRaptorConfig + | string + | boolean + | undefined; +} + +export interface ICompilationTemplate { + id: string; + name: string; + description: string; + kind: string; + config: ICompilationTemplateConfig; + create_time?: number; + update_time?: number; +} + +export interface ICompilationTemplateListResult { + templates: ICompilationTemplate[]; + total: number; +} + +export interface ICompilationTemplateBuiltin { + id: string; + kind: string; + display_name: string; + description: string; + config: ICompilationTemplateConfig; +} + +export interface ICompilationTemplateGroup { + id: string; + name: string; + description?: string; + scope?: string; + avatar?: string; + create_time?: number; + update_time?: number; + templates: ICompilationTemplate[]; +} + +export interface IWikiPreset { + id: string; + topic: string; + instruction: string; + page_example: string; +} diff --git a/web/src/interfaces/database/dataset-skill.ts b/web/src/interfaces/database/dataset-skill.ts new file mode 100644 index 0000000000..f1574a5769 --- /dev/null +++ b/web/src/interfaces/database/dataset-skill.ts @@ -0,0 +1,29 @@ +export interface DatasetSkillTreeNode { + skill_kwd: string; + md_with_weight?: string; + children_kwd?: DatasetSkillTreeNode[]; +} + +export interface DatasetSkillTree { + id: string; + kb_id: string; + doc_id: string; + compile_kwd: 'skill_all'; + skill_with_weight: DatasetSkillTreeNode[]; +} + +export interface DatasetSkillPage { + id?: string; + kb_id?: string; + doc_id?: string; + compile_kwd?: 'skill'; + skill_kwd: string; + depth_int?: number; + children_kwd?: string[]; + source_doc_ids?: string[]; + md_with_weight: string; +} + +export interface HasAnySkillResponse { + has: boolean; +} diff --git a/web/src/interfaces/database/dataset.ts b/web/src/interfaces/database/dataset.ts index b199bcefa6..4eae9de8b5 100644 --- a/web/src/interfaces/database/dataset.ts +++ b/web/src/interfaces/database/dataset.ts @@ -222,3 +222,69 @@ export interface IKnowledgeGraph { graph: Record; mind_map: import('@antv/g6/lib/types').TreeData; } + +export interface IArtifact { + slug: string; + title: string; + page_type?: string; + summary?: string; +} + +export interface IArtifactTopic { + topic: string; + title: string; + slug: string; +} + +export interface IArtifactPage { + slug: string; + title: string; + page_type: string; + content_md_rendered: string; + summary: string; + entity_names: string[]; + outlinks: string[]; + related_kb_pages: string[]; + source_chunk_ids: string[]; + source_doc_ids: string[]; +} + +export interface IWikiCommit { + id: string; + title: string; + comments: string; + user_id: string; + create_time: number; + create_date: string; + user_nickname: string; +} + +export interface IWikiCommitDetail extends IWikiCommit { + diff: string; + content_after: string; +} + +export interface IWikiCommitListResponse { + total: number; + items: IWikiCommit[]; +} + +export interface IArtifactGraphEntity { + slug: string; + name: string; + aliases: string[]; + description: string; + type: string; + weight: number; + source_chunk_ids?: string[]; +} + +export interface IArtifactGraphRelation { + from: string; + to: string; +} + +export interface IArtifactGraph { + entities: IArtifactGraphEntity[]; + relations: IArtifactGraphRelation[]; +} diff --git a/web/src/interfaces/database/document-structure.ts b/web/src/interfaces/database/document-structure.ts new file mode 100644 index 0000000000..6ba6e70363 --- /dev/null +++ b/web/src/interfaces/database/document-structure.ts @@ -0,0 +1,32 @@ +import { CompilationTemplateKind } from '@/constants/compilation'; + +export type StructureTemplateKind = CompilationTemplateKind | 'raptor'; + +export interface IStructureGraphEntity { + id?: string; + name?: string; + aliases?: string[]; + description?: string; + discription?: string; + mention_count?: number; + source_chunk_ids?: string[]; + type?: string; +} + +export interface IStructureGraphRelation { + from: string; + to: string; + type?: string; +} + +export interface IStructureGraphTemplate { + kind: StructureTemplateKind; + template_id: string; + template_name: string; + entities: IStructureGraphEntity[]; + relations: IStructureGraphRelation[]; +} + +export interface IStructureGraphResponse { + templates: IStructureGraphTemplate[]; +} diff --git a/web/src/interfaces/database/document.ts b/web/src/interfaces/database/document.ts index 3f676970dc..9e985fc94d 100644 --- a/web/src/interfaces/database/document.ts +++ b/web/src/interfaces/database/document.ts @@ -60,6 +60,7 @@ export interface IParserConfig { enum?: string[]; }>; enable_metadata?: boolean; + compilation_template_group_id?: string[]; } interface Raptor { diff --git a/web/src/interfaces/request/compilation-template.ts b/web/src/interfaces/request/compilation-template.ts new file mode 100644 index 0000000000..fde2677aa9 --- /dev/null +++ b/web/src/interfaces/request/compilation-template.ts @@ -0,0 +1,58 @@ +export interface IFetchCompilationTemplatesRequestParams { + keywords?: string; + page?: number; + page_size?: number; + kind?: string; +} + +export interface ICompilationTemplateSectionRequest { + description?: string; + fields: Array>; +} + +export interface ICompilationTemplateRaptorConfigRequest { + prompt?: string; + max_token?: number; + threshold?: number; + rechunk?: boolean; +} + +export interface ICompilationTemplateConfigRequest { + kind?: string; + llm_id?: string; + entity?: ICompilationTemplateSectionRequest; + relation?: ICompilationTemplateSectionRequest; + raptor?: ICompilationTemplateRaptorConfigRequest; + global_rules?: string; + [section: string]: + | ICompilationTemplateSectionRequest + | ICompilationTemplateRaptorConfigRequest + | string + | boolean + | undefined; +} + +export interface ICreateCompilationTemplateRequestBody { + name: string; + description?: string; + kind: string; + config: ICompilationTemplateConfigRequest; +} + +export type IUpdateCompilationTemplateRequestBody = + Partial; + +export interface ICreateCompilationTemplateGroupRequestBody { + name: string; + description?: string; + avatar?: string; + templates: Array<{ + name?: string; + description?: string; + kind: string; + config: ICompilationTemplateConfigRequest; + }>; +} + +export type IUpdateCompilationTemplateGroupRequestBody = + Partial; diff --git a/web/src/interfaces/request/document.ts b/web/src/interfaces/request/document.ts index 05693ca356..a6f97be152 100644 --- a/web/src/interfaces/request/document.ts +++ b/web/src/interfaces/request/document.ts @@ -22,6 +22,7 @@ export interface IChangeParserConfigRequestBody { clustering_method?: 'gmm' | 'ahc'; tree_builder?: 'raptor' | 'psi'; }; + compilation_template_group_id?: string[]; // Metadata fields metadata?: Array<{ key?: string; diff --git a/web/src/interfaces/request/knowledge.ts b/web/src/interfaces/request/knowledge.ts index f93c5dd932..92edc54c07 100644 --- a/web/src/interfaces/request/knowledge.ts +++ b/web/src/interfaces/request/knowledge.ts @@ -39,4 +39,35 @@ export interface IFetchDocumentListRequestBody { run_status?: string[]; return_empty_metadata?: boolean; metadata?: Record; + ids?: string[]; +} + +export interface IFetchArtifactListRequestParams { + page?: number; + page_size?: number; + keywords?: string; + page_type?: string; + topic?: string; +} + +export interface IFetchArtifactTopicListRequestParams { + page?: number; + page_size?: number; + keywords?: string; +} + +export interface IFetchArtifactGraphRequestParams { + node?: string; +} + +export interface IUpdateArtifactPageRequestBody { + content_md: string; + comments: string; + title?: string; +} + +export interface IUpdateArtifactPageRequestParams { + pageType: string; + slug: string; + body: IUpdateArtifactPageRequestBody; } diff --git a/web/src/lib/editor/content-editable.tsx b/web/src/lib/editor/content-editable.tsx new file mode 100644 index 0000000000..a60339ff5a --- /dev/null +++ b/web/src/lib/editor/content-editable.tsx @@ -0,0 +1,30 @@ +/** + * ContentEditable wrapper — thin wrapper around Lexical's contenteditable. + */ + +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import type { JSX } from 'react'; + +interface Props { + className?: string; + placeholderClassName?: string; + placeholder: string; +} + +export default function LexicalContentEditable({ + className, + placeholder, + placeholderClassName, +}: Props): JSX.Element { + return ( + + {placeholder} +
+ } + /> + ); +} diff --git a/web/src/lib/editor/editor-theme.css b/web/src/lib/editor/editor-theme.css new file mode 100644 index 0000000000..e41f446d8b --- /dev/null +++ b/web/src/lib/editor/editor-theme.css @@ -0,0 +1,668 @@ +/** + * Nimbalyst Editor Theme — direct port of NimbalystEditorTheme.css + * + * CSS variables defined on BOTH :root (global fallback) and + * .nimbalyst-editor (scoped override) for maximum reliability. + */ + +/* ===== THEME-AWARE VARIABLES — mapped to project design tokens ===== */ +:root { + --nim-bg: var(--bg-base); + --nim-bg-secondary: var(--bg-card); + --nim-bg-tertiary: var(--bg-component); + --nim-bg-hover: rgba(var(--text-primary), 0.05); + --nim-bg-selected: rgba(var(--accent-primary), 0.15); + --nim-bg-active: var(--bg-component); + --nim-text: rgb(var(--text-primary)); + --nim-text-muted: rgb(var(--text-secondary)); + --nim-text-faint: var(--text-disabled); + --nim-text-disabled: var(--text-disabled); + --nim-border: var(--border-default); + --nim-border-focus: rgb(var(--accent-primary)); + --nim-primary: rgb(var(--accent-primary)); + --nim-primary-hover: rgb(var(--accent-primary)); + --nim-link: rgb(var(--accent-primary)); + --nim-link-hover: rgba(var(--accent-primary), 0.8); + --nim-accent-subtle: rgba(var(--accent-primary), 0.1); + --nim-success: rgb(var(--state-success)); + --nim-warning: rgb(var(--state-warning)); + --nim-error: rgb(var(--state-error)); + --nim-info: rgb(var(--accent-primary)); + --nim-purple: var(--colors-outline-sentiment-primary); + --nim-code-bg: var(--bg-component); + --nim-code-text: rgb(var(--text-primary)); + --nim-code-border: var(--border-default); + --nim-code-gutter: var(--bg-card); + --nim-table-border: var(--border-default); + --nim-table-header: var(--bg-card); + --nim-table-cell: var(--bg-base); + --nim-table-stripe: var(--bg-card); + --nim-toolbar-bg: var(--bg-card); + --nim-toolbar-border: var(--border-default); + --nim-toolbar-hover: var(--bg-component); + --nim-toolbar-active: rgba(var(--accent-primary), 0.2); + --nim-highlight-bg: rgba(var(--state-warning), 0.2); + --nim-highlight-border: rgba(var(--state-warning), 0.4); + --nim-quote-text: rgb(var(--text-secondary)); + --nim-quote-border: var(--border-default); + --nim-scrollbar-thumb: var(--border-default); + --nim-scrollbar-thumb-hover: var(--border-button); + --nim-scrollbar-track: transparent; + --nim-diff-add-bg: rgba(var(--state-success), 0.15); + --nim-diff-add-border: rgba(var(--state-success), 0.4); + --nim-diff-remove-bg: rgba(var(--state-error), 0.15); + --nim-diff-remove-border: rgba(var(--state-error), 0.4); + --nim-code-comment: #6a9955; + --nim-code-punctuation: #cccccc; + --nim-code-property: #9cdcfe; + --nim-code-selector: #d7ba7d; + --nim-code-operator: #d4d4d4; + --nim-code-attr: #92c5f8; + --nim-code-variable: #4fc1ff; + --nim-code-function: #dcdcaa; + --max-text-width: 800px; +} + +/* ===== Editor container base styles (inherits --nim-* from :root/html inline) ===== */ +.nimbalyst-editor { + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + '.SFNSText-Regular', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: var(--nim-bg); + color: var(--nim-text); + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +/* ===== BASE — with #root prefix for max specificity ===== */ +#root .nim-ltr { + text-align: left; +} +#root .nim-rtl { + text-align: right; +} +#root .nim-paragraph { + margin: 0; + position: relative; +} + +#root .nim-content-editable { + outline: none; + min-height: 200px; + padding: 16px 24px; + font-size: 15px; + line-height: 1.8; + max-width: var(--max-text-width); + flex: 1; +} + +#root .nim-placeholder { + color: var(--nim-text-faint); + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 16px; + left: 24px; + font-size: 15px; + user-select: none; + display: inline-block; + pointer-events: none; +} + +/* ===== HEADINGS ===== */ +#root .nim-h1 { + font-size: 28px; + color: var(--nim-text); + font-weight: 500; + margin: 0; +} +#root .nim-h2 { + font-size: 20px; + color: var(--nim-text-muted); + font-weight: 600; + margin: 0; +} +#root .nim-h3 { + font-size: 16px; + color: var(--nim-text-muted); + margin: 0; +} +#root .nim-h4 { + font-size: 14px; + color: var(--nim-text-muted); + margin: 0; +} +#root .nim-h5 { + font-size: 12px; + color: var(--nim-text-muted); + margin: 0; +} +#root .nim-h6 { + font-size: 10px; + color: var(--nim-text-muted); + margin: 0; +} + +/* ===== TEXT FORMATTING ===== */ +#root .nim-text-bold { + font-weight: bold; +} +#root .nim-text-italic { + font-style: italic; +} +#root .nim-text-underline { + text-decoration: underline; +} +#root .nim-text-strikethrough { + text-decoration: line-through; +} +#root .nim-text-underline-strikethrough { + text-decoration: underline line-through; +} +#root .nim-text-subscript { + font-size: 0.8em; + vertical-align: sub !important; +} +#root .nim-text-superscript { + font-size: 0.8em; + vertical-align: super; +} +#root .nim-text-code { + background-color: var(--nim-code-bg); + padding: 1px 0.25rem; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 94%; +} +#root .nim-text-highlight { + background: var(--nim-highlight-bg); + border-bottom: 2px solid var(--nim-highlight-border); +} + +/* ===== QUOTE ===== */ +#root .nim-quote { + margin: 0; + margin-left: 20px; + margin-bottom: 10px; + font-size: 15px; + color: var(--nim-quote-text); + border-left-color: var(--nim-quote-border); + border-left-width: 4px; + border-left-style: solid; + padding-left: 16px; +} + +/* ===== LINK ===== */ +#root .nim-link { + color: var(--nim-link); + text-decoration: none; +} +#root .nim-link:hover { + text-decoration: underline; + cursor: pointer; +} + +/* ===== CODE BLOCKS ===== */ +#root .nim-code { + background-color: var(--nim-code-bg); + font-family: Menlo, Consolas, Monaco, monospace; + display: block; + padding: 8px 8px 8px 52px; + line-height: 1.53; + font-size: 13px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; + overflow-x: auto; + position: relative; + tab-size: 2; + border: 1px solid var(--nim-code-border); + border-radius: 8px; +} +#root .nim-code:before { + content: attr(data-gutter); + position: absolute; + background-color: var(--nim-code-gutter); + left: 0; + top: 0; + border-right: 1px solid var(--nim-code-border); + padding: 8px; + color: var(--nim-text-faint); + white-space: pre-wrap; + text-align: right; + min-width: 25px; +} + +/* ===== SYNTAX HIGHLIGHTING ===== */ +#root .nim-token-comment { + color: var(--nim-code-comment); + font-style: italic; +} +#root .nim-token-punctuation { + color: var(--nim-code-punctuation); +} +#root .nim-token-property { + color: var(--nim-code-property); +} +#root .nim-token-selector { + color: var(--nim-code-selector); +} +#root .nim-token-operator { + color: var(--nim-code-operator); +} +#root .nim-token-attr { + color: var(--nim-code-attr); +} +#root .nim-token-variable { + color: var(--nim-code-variable); +} +#root .nim-token-function { + color: var(--nim-code-function); +} +#root .nim-token-inserted { + color: #9ccc65; +} +#root .nim-token-deleted { + color: #ef5350; +} + +/* ===== LISTS ===== */ +#root .nim-ol1 { + padding: 0; + margin: 0; + list-style: decimal inside; +} +#root .nim-ol2 { + padding: 0; + margin: 0; + list-style: upper-alpha inside; +} +#root .nim-ol3 { + padding: 0; + margin: 0; + list-style: lower-alpha inside; +} +#root .nim-ol4 { + padding: 0; + margin: 0; + list-style: upper-roman inside; +} +#root .nim-ol5 { + padding: 0; + margin: 0; + list-style: lower-roman inside; +} +#root .nim-ul { + padding: 0; + margin: 0; + list-style: disc inside; + max-width: var(--max-text-width); +} +#root .nim-list-item { + margin: 0 24px; +} +#root .nim-list-item::marker { + color: var(--nim-text); +} + +#root .nim-list-item-checked, +#root .nim-list-item-unchecked { + position: relative; + margin-left: 8px; + margin-right: 8px; + padding-left: 24px; + padding-right: 24px; + list-style-type: none; + outline: none; +} +#root .nim-list-item-checked { + text-decoration: line-through; +} +#root .nim-list-item-unchecked:before, +#root .nim-list-item-checked:before { + content: ''; + width: 16px; + height: 16px; + top: 2px; + left: 0; + cursor: pointer; + display: block; + background-size: cover; + position: absolute; +} +#root .nim-list-item-unchecked:before { + border: 1px solid var(--nim-text-muted); + border-radius: 2px; +} +#root .nim-list-item-checked:before { + border: 1px solid var(--nim-primary); + border-radius: 2px; + background-color: var(--nim-primary); +} +#root .nim-list-item-checked:after { + content: ''; + cursor: pointer; + border-color: #fff; + border-style: solid; + position: absolute; + top: 6px; + width: 3px; + left: 7px; + right: 7px; + height: 6px; + transform: rotate(45deg); + border-width: 0 2px 2px 0; +} +#root .nim-nested-list-item { + list-style-type: none; +} +#root .nim-nested-list-item:before, +#root .nim-nested-list-item:after { + display: none; +} + +/* ===== HORIZONTAL RULE ===== */ +#root .nim-hr { + padding: 2px 2px; + border: none; + margin: 1em 0; + cursor: pointer; +} +#root .nim-hr:after { + content: ''; + display: block; + height: 2px; + background-color: var(--nim-border); + line-height: 2px; +} +#root .nim-hr.nim-hr-selected { + outline: 2px solid var(--nim-primary); + user-select: none; +} + +/* ===== HASHTAG ===== */ +#root .nim-hashtag { + background-color: color-mix(in srgb, var(--nim-primary) 15%, transparent); + border-radius: 4px; + padding-left: 3px; + padding-right: 3px; + display: inline-block; +} + +/* ===== MERMAID ===== */ +.mermaid-container { + margin: 8px 0; +} +.mermaid-block { + margin: 12px 0; + text-align: center; +} +.mermaid-render-container { + overflow-x: auto; +} +.mermaid-render-container svg { + max-width: 100%; + height: auto; +} + +/* Mermaid dark theme */ +.mermaid-render-container svg .node rect, +.mermaid-render-container svg .node circle, +.mermaid-render-container svg .node ellipse, +.mermaid-render-container svg .node polygon, +.mermaid-render-container svg .node path { + fill: var(--nim-bg-tertiary) !important; + stroke: var(--nim-border) !important; +} +.mermaid-render-container svg text, +.mermaid-render-container svg .nodeLabel, +.mermaid-render-container svg .label, +.mermaid-render-container svg .edgeLabel { + fill: var(--nim-text) !important; + color: var(--nim-text) !important; +} +.mermaid-render-container svg .edge path, +.mermaid-render-container svg .flowchart-link { + stroke: var(--nim-text-muted) !important; +} +.mermaid-render-container svg marker path { + fill: var(--nim-text-muted) !important; + stroke: var(--nim-text-muted) !important; +} +.mermaid-render-container svg .edgeLabel rect, +.mermaid-render-container svg .edgeLabel span { + background-color: var(--nim-bg) !important; + fill: var(--nim-bg) !important; + color: var(--nim-text) !important; +} +.mermaid-render-container svg .cluster rect { + fill: var(--nim-bg-secondary) !important; + stroke: var(--nim-border) !important; +} + +/* ===== EDITOR CONTAINER ===== */ +#root .nim-editor-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +#root .nim-editor-scroller { + flex: 1; + overflow-y: auto; + position: relative; + display: flex; + flex-direction: column; +} +#root .nim-editor { + flex: 1; + position: relative; +} + +/* ===== MODE SWITCH ===== */ +#root .nim-mode-bar { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-bottom: 1px solid var(--nim-border); + background: var(--nim-bg-secondary); + flex-shrink: 0; +} +#root .nim-mode-btn { + padding: 4px 10px; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + background: transparent; + color: var(--nim-text-muted); + font-family: inherit; + transition: 0.1s; +} +#root .nim-mode-btn:hover { + background: var(--nim-bg-hover); + color: var(--nim-text); +} +#root .nim-mode-btn.active { + background: var(--nim-bg-selected); + border-color: var(--nim-primary); + color: var(--nim-primary); +} + +/* ===== Lexical plugin containers ===== */ +#root .nimbalyst-editor .editor-shell { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} +#root .nimbalyst-editor .editor-container { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + min-height: 0; +} + +/* ===== TABLES ===== */ +#root .nim-table-scrollable-wrapper { + overflow-x: auto; + margin: 0px 25px 30px 0px; +} + +#root .nim-table-scrollable-wrapper > .nim-table { + margin-top: 0; + margin-bottom: 0; +} + +#root .nim-table-alignment-center { + margin-left: auto; + margin-right: auto; +} + +#root .nim-table-alignment-right { + margin-left: auto; +} + +#root .nim-table { + border-collapse: collapse; + border-spacing: 0; + overflow-y: scroll; + overflow-x: scroll; + table-layout: fixed; + margin-top: 25px; + margin-bottom: 30px; +} + +#root .nim-table-scrollable-wrapper.nim-table-frozen-row { + overflow-x: clip; +} + +#root .nim-table-frozen-row tr:nth-of-type(1) > td { + overflow: clip; + background-color: var(--nim-table-cell); + position: sticky; + z-index: 2; + top: 44px; +} + +#root .nim-table-frozen-row tr:nth-of-type(1) > th { + overflow: clip; + background-color: var(--nim-table-header); + position: sticky; + z-index: 2; + top: 44px; +} + +#root .nim-table-frozen-row tr:nth-of-type(1) > th:after, +#root .nim-table-frozen-row tr:nth-of-type(1) > td:after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + border-bottom: 1px solid var(--nim-table-border); +} + +#root .nim-table-frozen-column tr > td:first-child { + background-color: var(--nim-table-cell); + position: sticky; + z-index: 2; + left: 0; +} + +#root .nim-table-frozen-column tr > th:first-child { + background-color: var(--nim-table-header); + position: sticky; + z-index: 2; + left: 0; +} + +#root .nim-table-frozen-column tr > :first-child::after { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + height: 100%; + border-right: 1px solid var(--nim-table-border); +} + +#root .nim-table-row-striping tr:nth-child(even), +#root + .nim-table-frozen-column + .nim-table.nim-table-row-striping + tr:nth-child(even) + > td:first-child { + background-color: var(--nim-table-stripe); +} + +#root .nim-table-selection *::selection { + background-color: transparent; +} + +#root .nim-table-selected { + outline: 2px solid var(--nim-primary); +} + +#root .nim-table-cell { + border: 1px solid var(--nim-table-border); + color: var(--nim-text); + vertical-align: top; + text-align: start; + padding: 6px 8px; + position: relative; + outline: none; + overflow: auto; + min-width: 50px; +} + +#root .nim-table-cell > * { + overflow: inherit; +} + +#root .nim-table-cell-resizer { + position: absolute; + right: -4px; + height: 100%; + width: 8px; + cursor: ew-resize; + z-index: 10; + top: 0; +} + +#root .nim-table-cell-header { + background-color: var(--nim-table-header); + color: var(--nim-text); + text-align: start; +} + +#root .nim-table-cell-selected { + caret-color: transparent; +} + +#root .nim-table-cell-selected::after { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + background-color: highlight; + mix-blend-mode: multiply; + content: ''; + pointer-events: none; +} + +/* Table add buttons (simplified) */ +#root .nim-table-add-columns, +#root .nim-table-add-rows { + display: none; /* Hide by default - shown only during table editing */ +} diff --git a/web/src/lib/editor/editor-theme.ts b/web/src/lib/editor/editor-theme.ts new file mode 100644 index 0000000000..160e32d3f8 --- /dev/null +++ b/web/src/lib/editor/editor-theme.ts @@ -0,0 +1,93 @@ +/** + * Lexical editor theme — maps node types to `nim-*` CSS class names. + * Direct port from Nimbalyst's NimbalystEditorTheme.ts. + */ + +import type { EditorThemeClasses } from 'lexical'; + +const theme: EditorThemeClasses = { + autocomplete: 'nim-autocomplete', + blockCursor: 'nim-block-cursor', + characterLimit: 'nim-character-limit', + code: 'nim-code', + codeHighlight: { + atrule: 'nim-token-attr', + attr: 'nim-token-attr', + boolean: 'nim-token-property', + builtin: 'nim-token-selector', + cdata: 'nim-token-comment', + char: 'nim-token-selector', + class: 'nim-token-function', + 'class-name': 'nim-token-function', + comment: 'nim-token-comment', + constant: 'nim-token-property', + deleted: 'nim-token-deleted', + doctype: 'nim-token-comment', + entity: 'nim-token-operator', + function: 'nim-token-function', + important: 'nim-token-variable', + inserted: 'nim-token-inserted', + keyword: 'nim-token-attr', + namespace: 'nim-token-variable', + number: 'nim-token-property', + operator: 'nim-token-operator', + prolog: 'nim-token-comment', + property: 'nim-token-property', + punctuation: 'nim-token-punctuation', + regex: 'nim-token-variable', + selector: 'nim-token-selector', + string: 'nim-token-selector', + symbol: 'nim-token-property', + tag: 'nim-token-property', + unchanged: 'nim-token-unchanged', + url: 'nim-token-operator', + variable: 'nim-token-variable', + }, + embedBlock: { base: 'nim-embed-block', focus: 'nim-embed-block-focus' }, + hashtag: 'nim-hashtag', + heading: { + h1: 'nim-h1', + h2: 'nim-h2', + h3: 'nim-h3', + h4: 'nim-h4', + h5: 'nim-h5', + h6: 'nim-h6', + }, + hr: 'nim-hr', + hrSelected: 'nim-hr-selected', + image: 'editor-image', + indent: 'nim-indent', + link: 'nim-link', + list: { + checklist: 'nim-checklist', + listitem: 'nim-list-item', + listitemChecked: 'nim-list-item-checked', + listitemUnchecked: 'nim-list-item-unchecked', + nested: { listitem: 'nim-nested-list-item' }, + olDepth: ['nim-ol1', 'nim-ol2', 'nim-ol3', 'nim-ol4', 'nim-ol5'], + ul: 'nim-ul', + }, + ltr: 'nim-ltr', + mark: 'nim-mark', + markOverlap: 'nim-mark-overlap', + paragraph: 'nim-paragraph', + quote: 'nim-quote', + rtl: 'nim-rtl', + tab: 'nim-tab-node', + table: 'nim-table', + tableCell: 'nim-table-cell', + tableCellHeader: 'nim-table-cell-header', + text: { + bold: 'nim-text-bold', + code: 'nim-text-code', + highlight: 'nim-text-highlight', + italic: 'nim-text-italic', + strikethrough: 'nim-text-strikethrough', + subscript: 'nim-text-subscript', + superscript: 'nim-text-superscript', + underline: 'nim-text-underline', + underlineStrikethrough: 'nim-text-underline-strikethrough', + }, +}; + +export default theme; diff --git a/web/src/lib/editor/index.ts b/web/src/lib/editor/index.ts new file mode 100644 index 0000000000..12b57609b9 --- /dev/null +++ b/web/src/lib/editor/index.ts @@ -0,0 +1,23 @@ +/** + * Editor module public API. + */ + +export { default as theme } from './editor-theme'; +export { default as LexicalEditor } from './lexical-editor'; +export { default as nodes } from './nodes'; +export { default as RawMarkdownEditor } from './raw-markdown-editor'; + +export { + $convertFromEnhancedMarkdownString, + $convertToEnhancedMarkdownString, + CORE_TRANSFORMERS, +} from './markdown'; + +export { + $createMermaidNode, + $isMermaidNode, + MermaidNode, +} from './mermaid-node'; +export type { MermaidPayload } from './mermaid-node'; + +export { MERMAID_TRANSFORMER } from './mermaid-transformer'; diff --git a/web/src/lib/editor/lexical-editor.tsx b/web/src/lib/editor/lexical-editor.tsx new file mode 100644 index 0000000000..f314827916 --- /dev/null +++ b/web/src/lib/editor/lexical-editor.tsx @@ -0,0 +1,198 @@ +/** + * LexicalEditor — WYSIWYG markdown editor using Meta's Lexical framework. + * + * Ported from Nimbalyst's NimbalystEditor + Editor.tsx. + * Markdown import on init, markdown export on change. + * Wraps content in nimbalyst-editor container for theming. + */ + +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin'; +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; +import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'; +import { ListPlugin } from '@lexical/react/LexicalListPlugin'; +import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { TablePlugin } from '@lexical/react/LexicalTablePlugin'; +import { $createParagraphNode, $getRoot } from 'lexical'; +import type { JSX } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; + +import ContentEditable from './content-editable'; +import theme from './editor-theme'; +import { + $convertFromEnhancedMarkdownString, + $convertToEnhancedMarkdownString, + CORE_TRANSFORMERS, + type Transformer, +} from './markdown'; +import nodes from './nodes'; + +// Direct CSS import (more reliable than @import in App.css) +import './editor-theme.css'; + +interface Props { + content: string; + onChange?: (markdown: string) => void; + readOnly?: boolean; + placeholder?: string; + onToggleSource?: () => void; + showSource?: boolean; +} + +function InitialContentPlugin({ + content, + transformers, +}: { + content: string; + transformers: Transformer[]; +}) { + const [editor] = useLexicalComposerContext(); + const seeded = useRef(false); + + useEffect(() => { + // Seed ONCE on mount. Subsequent external content changes are handled by ContentSyncPlugin. + if (seeded.current) return; + seeded.current = true; + + editor.update(() => { + const root = $getRoot(); + root.clear(); + if (content && content.trim()) { + $convertFromEnhancedMarkdownString(content, transformers); + } else { + root.append($createParagraphNode()); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editor]); + + return null; +} + +function ContentSyncPlugin({ + content, + transformers, +}: { + content: string; + transformers: Transformer[]; +}) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + editor.update(() => { + const currentMarkdown = $convertToEnhancedMarkdownString(transformers); + if (currentMarkdown === content) return; + + const root = $getRoot(); + root.clear(); + if (content && content.trim()) { + $convertFromEnhancedMarkdownString(content, transformers); + } else { + root.append($createParagraphNode()); + } + }); + }, [content, editor, transformers]); + + return null; +} + +function ChangeListener({ + onChange, + transformers, +}: { + onChange?: (md: string) => void; + transformers: Transformer[]; +}) { + const [editor] = useLexicalComposerContext(); + const initRef = useRef(false); + + useEffect(() => { + if (!onChange) return; + const removeListener = editor.registerUpdateListener( + ({ dirtyElements, dirtyLeaves }) => { + if (!initRef.current) { + initRef.current = true; + return; + } + if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return; + const markdown = editor.read(() => + $convertToEnhancedMarkdownString(transformers), + ); + onChange(markdown); + }, + ); + return removeListener; + }, [editor, onChange, transformers]); + + return null; +} + +import FloatingSelectionToolbar from './plugins/floating-selection-toolbar'; +import TableActionsPlugin from './plugins/table-actions-plugin'; +import ToolbarPlugin from './plugins/toolbar-plugin'; + +export default function LexicalEditor({ + content, + onChange, + readOnly = false, + placeholder = 'Start writing...', + onToggleSource, + showSource = false, +}: Props): JSX.Element { + const transformers = useMemo(() => CORE_TRANSFORMERS, []); + + const initialConfig = useMemo( + () => ({ + namespace: 'LlmWikiEditor', + nodes: [...nodes], + theme, + editable: !readOnly, + onError: (error: Error) => { + console.error('[LexicalEditor] Error:', error); + }, + }), + [readOnly], + ); + + return ( +
+
+ +
+ {})} + showSource={showSource} + /> + + + + + +
+ +
+
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + +
+ +
+ + ); +} diff --git a/web/src/lib/editor/markdown/enhanced-markdown-export.ts b/web/src/lib/editor/markdown/enhanced-markdown-export.ts new file mode 100644 index 0000000000..6fe923b5fb --- /dev/null +++ b/web/src/lib/editor/markdown/enhanced-markdown-export.ts @@ -0,0 +1,481 @@ +/** + * Enhanced markdown export supporting individual node export and + * proper handling of all node types. + * Ported from Nimbalyst — simplified: no frontmatter, no diff/reject mode. + */ + +import type { + ElementTransformer, + MultilineElementTransformer, + TextFormatTransformer, + TextMatchTransformer, +} from '@lexical/markdown'; +import { + $getRoot, + $isDecoratorNode, + $isElementNode, + $isLineBreakNode, + $isRootOrShadowRoot, + $isTextNode, + type ElementNode, + type LexicalNode, + type TextFormatType, + type TextNode, +} from 'lexical'; +export type LexicalTransformer = + | ElementTransformer + | MultilineElementTransformer + | TextFormatTransformer + | TextMatchTransformer; + +type UnclosedFormatTag = { format: TextFormatType; tag: string }; + +export interface EnhancedExportOptions { + shouldPreserveNewLines?: boolean; +} + +export function $convertToEnhancedMarkdownString( + transformers: LexicalTransformer[], + options: EnhancedExportOptions = {}, +): string { + const { shouldPreserveNewLines = true } = options; + + return $exportNodeToMarkdown( + transformers, + $getRoot(), + shouldPreserveNewLines, + ); +} + +export function $convertNodeToEnhancedMarkdownString( + transformers: LexicalTransformer[], + node?: ElementNode | null, + shouldPreserveNewLines: boolean = true, +): string { + return $exportNodeToMarkdown(transformers, node, shouldPreserveNewLines); +} + +function $exportNodeToMarkdown( + transformers: LexicalTransformer[], + node?: ElementNode | null, + shouldPreserveNewLines: boolean = true, +): string { + const byType = transformersByType(transformers); + const isNewlineDelimited = !byType.multilineElement.length; + + const textFormatTransformers = byType.textFormat + .filter((t) => t.format.length === 1) + .sort( + (a, b) => + Number(a.format.includes('code')) - Number(b.format.includes('code')), + ); + + const textMatchTransformers = byType.textMatch; + const elementTransformers = [...byType.element, ...byType.multilineElement]; + + const output: string[] = []; + const target = node || $getRoot(); + + if ( + target && + (!$isRootOrShadowRoot(target) || target.getType() === 'table') + ) { + const result = exportTopLevelElements( + target, + elementTransformers, + textFormatTransformers, + textMatchTransformers, + shouldPreserveNewLines, + ); + if (result !== null) output.push(result); + } else { + const children = target.getChildren(); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const result = exportTopLevelElements( + child, + elementTransformers, + textFormatTransformers, + textMatchTransformers, + shouldPreserveNewLines, + ); + if (result !== null) { + output.push( + isNewlineDelimited && + i > 0 && + !isEmptyParagraph(child) && + !isEmptyParagraph(children[i - 1]) + ? '\n' + result + : result, + ); + } + } + } + + return output.join(shouldPreserveNewLines ? '\n' : '\n\n'); +} + +function exportTopLevelElements( + node: LexicalNode, + elementTransformers: Array, + textFormatTransformers: Array, + textMatchTransformers: Array, + shouldPreserveNewLines: boolean = false, +): string | null { + for (const transformer of elementTransformers) { + if (!transformer.export) continue; + const result = transformer.export(node, (_node) => + exportChildren( + _node, + textFormatTransformers, + textMatchTransformers, + undefined, + undefined, + shouldPreserveNewLines, + elementTransformers, + ), + ); + if (result != null) return result; + } + + if ($isElementNode(node)) { + return exportChildren( + node, + textFormatTransformers, + textMatchTransformers, + undefined, + undefined, + shouldPreserveNewLines, + elementTransformers, + ); + } else if ($isDecoratorNode(node)) { + return node.getTextContent(); + } + return null; +} + +function exportChildren( + node: ElementNode, + textFormatTransformers: Array, + textMatchTransformers: Array, + textContent?: string, + textTransformer?: TextFormatTransformer | null, + shouldPreserveNewLines: boolean = false, + elementTransformers?: Array, + unclosedTags?: Array, + unclosableTags?: Array, +): string { + const output: string[] = []; + const children = node.getChildren(); + const activeUnclosedTags = unclosedTags ?? []; + const activeUnclosableTags = unclosableTags ?? []; + + for (const child of children) { + if ($isLineBreakNode(child)) { + if (shouldPreserveNewLines) output.push('\n'); + } else if ($isTextNode(child)) { + const textContentForTransform = textContent || child.getTextContent(); + + if (textTransformer) { + output.push(textContentForTransform); + } else { + const hasFormatting = child.getFormat() !== 0; + let handled = false; + + if (hasFormatting) { + output.push( + exportTextFormat( + child, + textContentForTransform, + textFormatTransformers, + activeUnclosedTags, + activeUnclosableTags, + shouldPreserveNewLines, + ), + ); + handled = true; + } else { + for (const transformer of textMatchTransformers) { + if (!transformer.export) continue; + const result = transformer.export( + child, + (_node: ElementNode, textContent?: string) => + exportChildren( + _node, + textFormatTransformers, + textMatchTransformers, + textContent, + textTransformer, + shouldPreserveNewLines, + elementTransformers, + activeUnclosedTags, + [...activeUnclosableTags, ...activeUnclosedTags], + ), + (node: TextNode, textContent: string) => + exportTextFormat( + node, + textContent, + textFormatTransformers, + activeUnclosedTags, + activeUnclosableTags, + shouldPreserveNewLines, + ), + ); + if (result != null) { + output.push(result); + handled = true; + break; + } + } + } + + if (!handled) output.push(textContentForTransform); + } + } else if ($isElementNode(child)) { + let handled = false; + for (const transformer of textMatchTransformers) { + if (!transformer.export) continue; + const result = transformer.export( + child, + (_node: ElementNode) => + exportChildren( + _node, + textFormatTransformers, + textMatchTransformers, + undefined, + undefined, + shouldPreserveNewLines, + elementTransformers, + activeUnclosedTags, + [...activeUnclosableTags, ...activeUnclosedTags], + ), + (node: TextNode, textContent: string) => + exportTextFormat( + node, + textContent, + textFormatTransformers, + activeUnclosedTags, + activeUnclosableTags, + shouldPreserveNewLines, + ), + ); + if (result != null) { + output.push(result); + handled = true; + break; + } + } + if (!handled) { + const result = exportTopLevelElements( + child, + elementTransformers || [], + textFormatTransformers, + textMatchTransformers, + shouldPreserveNewLines, + ); + if (result != null) output.push(result); + } + } else if ($isDecoratorNode(child)) { + let handled = false; + for (const transformer of textMatchTransformers) { + if (!transformer.export) continue; + const result = transformer.export( + child, + (_node: ElementNode) => + exportChildren( + _node, + textFormatTransformers, + textMatchTransformers, + undefined, + undefined, + shouldPreserveNewLines, + elementTransformers, + activeUnclosedTags, + [...activeUnclosableTags, ...activeUnclosedTags], + ), + (node: TextNode, textContent: string) => + exportTextFormat( + node, + textContent, + textFormatTransformers, + activeUnclosedTags, + activeUnclosableTags, + shouldPreserveNewLines, + ), + ); + if (result != null) { + output.push(result); + handled = true; + break; + } + } + if (!handled && elementTransformers) { + for (const transformer of elementTransformers) { + const result = transformer.export?.(child, () => ''); + if (result != null) { + output.push(result); + handled = true; + break; + } + } + } + if (!handled) output.push(child.getTextContent()); + } + } + return output.join(''); +} + +function exportTextFormat( + node: TextNode, + textContent: string, + textTransformers: Array, + unclosedTags: Array, + unclosableTags: Array, + shouldPreserveNewLines: boolean = false, +): string { + let output = textContent; + + if (!node.hasFormat('code')) { + if (shouldPreserveNewLines) { + // Use HTML NCR for * and _ instead of backslash escapes (round-trip safe) + output = output + .replace(/\*/g, '*') + .replace(/_/g, '_') + .replace(/([`~])/g, '\\$1'); + } else { + output = output.replace(/([*_`~\\])/g, '\\$1'); + } + } + + const match = output.match(/^(\s*)(.*?)(\s*)$/s) || ['', '', output, '']; + const leadingSpace = match[1]; + const trimmedOutput = match[2]; + const trailingSpace = match[3]; + const isWhitespaceOnly = trimmedOutput === ''; + + let openingTags = ''; + let closingTagsBefore = ''; + let closingTagsAfter = ''; + const previousTextNode = getTextSibling(node, true); + const nextTextNode = getTextSibling(node, false); + const appliedFormats = new Set(); + + for (const transformer of textTransformers) { + const format = transformer.format[0]; + const tag = transformer.tag; + if (hasTextFormat(node, format) && !appliedFormats.has(format)) { + appliedFormats.add(format); + if ( + !hasTextFormat(previousTextNode, format) || + !unclosedTags.find((entry) => entry.tag === tag) + ) { + unclosedTags.push({ format, tag }); + openingTags += tag; + } + } + } + + for (let i = 0; i < unclosedTags.length; i++) { + const currentTag = unclosedTags[i]; + const nodeHasFormat = hasTextFormat(node, currentTag.format); + const nextNodeHasFormat = hasTextFormat(nextTextNode, currentTag.format); + + if (nodeHasFormat && nextNodeHasFormat) continue; + + const remainingTags = [...unclosedTags]; + while (remainingTags.length > i) { + const tagToClose = remainingTags.pop(); + if ( + tagToClose && + unclosableTags.find((entry) => entry.tag === tagToClose.tag) + ) + continue; + if (tagToClose) { + if (!nodeHasFormat) closingTagsBefore += tagToClose.tag; + else if (!nextNodeHasFormat) closingTagsAfter += tagToClose.tag; + } + unclosedTags.pop(); + } + break; + } + + if (isWhitespaceOnly && !node.hasFormat('code')) { + return closingTagsBefore + output; + } + + return ( + closingTagsBefore + + leadingSpace + + openingTags + + trimmedOutput + + closingTagsAfter + + trailingSpace + ); +} + +function isEmptyParagraph(node: LexicalNode): boolean { + if (!$isElementNode(node)) return false; + const children = node.getChildren(); + if (children.length === 0) return true; + if (children.length === 1) { + const child = children[0]; + if ($isTextNode(child) && child.getTextContent().trim() === '') return true; + } + return false; +} + +function transformersByType(transformers: LexicalTransformer[]) { + const byType: { + element: Array; + multilineElement: Array; + textFormat: Array; + textMatch: Array; + } = { element: [], multilineElement: [], textFormat: [], textMatch: [] }; + + for (const transformer of transformers) { + const type = transformer.type; + if (type === 'element') + byType.element.push(transformer as ElementTransformer); + else if (type === 'multiline-element') + byType.multilineElement.push(transformer as MultilineElementTransformer); + else if (type === 'text-format') + byType.textFormat.push(transformer as TextFormatTransformer); + else if (type === 'text-match') + byType.textMatch.push(transformer as TextMatchTransformer); + } + return byType; +} + +function getTextSibling(node: TextNode, backward: boolean): TextNode | null { + let sibling = backward ? node.getPreviousSibling() : node.getNextSibling(); + if (!sibling) { + const parent = node.getParent(); + if (parent?.isInline()) + sibling = backward + ? parent.getPreviousSibling() + : parent.getNextSibling(); + } + while (sibling) { + if ($isElementNode(sibling)) { + if (!sibling.isInline()) break; + const descendant = backward + ? sibling.getLastDescendant() + : sibling.getFirstDescendant(); + if ($isTextNode(descendant)) return descendant; + sibling = backward + ? sibling.getPreviousSibling() + : sibling.getNextSibling(); + continue; + } + if ($isTextNode(sibling)) return sibling; + break; + } + return null; +} + +function hasTextFormat( + node: LexicalNode | null | undefined, + format: TextFormatType, +): boolean { + return $isTextNode(node) && node.hasFormat(format); +} diff --git a/web/src/lib/editor/markdown/enhanced-markdown-import.ts b/web/src/lib/editor/markdown/enhanced-markdown-import.ts new file mode 100644 index 0000000000..5e20247915 --- /dev/null +++ b/web/src/lib/editor/markdown/enhanced-markdown-import.ts @@ -0,0 +1,166 @@ +/** + * Enhanced markdown import with CRLF normalization and tab collapse. + * Ported from Nimbalyst. + * + * Uses placeholder-based approach for mermaid blocks: + * 1. Replace ```mermaid blocks with unique placeholders in the raw text + * 2. Standard Lexical import (placeholders become TextNodes) + * 3. Walk tree, replace placeholder TextNodes with MermaidNode + * + * This preserves the original position of mermaid diagrams in the document. + */ + +import { $isCodeNode } from '@lexical/code'; +import type { + ElementTransformer, + MultilineElementTransformer, + TextFormatTransformer, + TextMatchTransformer, +} from '@lexical/markdown'; +import { $convertFromMarkdownString } from '@lexical/markdown'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $isElementNode, + $isTabNode, + $isTextNode, + type ElementNode, + type LexicalNode, +} from 'lexical'; +import { $createMermaidNode } from '../mermaid-node'; +type LexicalTransformer = + | ElementTransformer + | MultilineElementTransformer + | TextFormatTransformer + | TextMatchTransformer; + +export interface EnhancedImportOptions { + preserveNewLines?: boolean; +} + +// Unique placeholder prefix — must not appear in normal markdown text +const PREFIX = '%%MERMAID_PLACEHOLDER_'; +const SUFFIX = '%%'; + +export function $convertFromEnhancedMarkdownString( + markdown: string, + transformers?: LexicalTransformer[], + node?: ElementNode, + preserveNewLines: boolean = true, +): void { + if (!markdown) { + const root = node ?? $getRoot(); + root.append($createParagraphNode()); + return; + } + + const root = node ?? $getRoot(); + + // Step 1: Normalize line endings + const normalizedMarkdown = markdown + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + + // Step 2: Replace ```mermaid ... ``` blocks with unique placeholders + // Matches both ```mermaid and ``` mermaid (with optional space after backticks) + const MERMAID_REGEX = /```\s*mermaid\s*\n([\s\S]*?)```/g; + const mermaidContent: string[] = []; + let placeholderIdx = 0; + const markdownWithPlaceholders = normalizedMarkdown.replace( + MERMAID_REGEX, + (_match, content) => { + mermaidContent.push(content.trim()); + const ph = `${PREFIX}${placeholderIdx}${SUFFIX}`; + placeholderIdx++; + return ph; + }, + ); + + // Step 3: Standard Lexical import (placeholders are just text) + $convertFromMarkdownString( + markdownWithPlaceholders, + transformers || [], + root, + preserveNewLines, + ); + + // Step 4: Walk tree and replace placeholder TextNodes with MermaidNode + $replacePlaceholders(root, mermaidContent); + + // Step 5: Fallback — also handle CodeNode(language='mermaid') if any + $replaceMermaidCodeNodes(root); + + // Step 6: Collapse TabNodes + $collapseTabNodes(root); +} + +/** + * Walks the subtree and replaces TextNodes containing %%MERMAID_PLACEHOLDER_N%% + * with MermaidNode containing the corresponding content. + */ +function $replacePlaceholders( + root: ElementNode, + mermaidContent: string[], +): void { + const visit = (node: LexicalNode): void => { + if ($isTextNode(node)) { + const text = node.getTextContent(); + if (text.startsWith(PREFIX)) { + const idxStr = text.slice(PREFIX.length, -SUFFIX.length); + const idx = parseInt(idxStr, 10); + if (!isNaN(idx) && idx >= 0 && idx < mermaidContent.length) { + const content = mermaidContent[idx]; + if (content) { + const mermaidNode = $createMermaidNode({ content }); + node.replace(mermaidNode); + return; + } + } + } + } + if ($isElementNode(node)) { + // Walk children in reverse to maintain indices during mutation + const children = node.getChildren(); + for (let i = children.length - 1; i >= 0; i--) { + visit(children[i]); + } + } + }; + visit(root); +} + +/** + * Fallback: replaces CodeNode(language='mermaid') with MermaidNode. + */ +function $replaceMermaidCodeNodes(root: ElementNode): void { + const visit = (node: LexicalNode): void => { + if ($isCodeNode(node) && node.getLanguage() === 'mermaid') { + const text = node.getTextContent(); + const mermaidNode = $createMermaidNode({ content: text }); + node.replace(mermaidNode); + return; + } + if ($isElementNode(node)) { + for (const child of node.getChildren()) { + visit(child); + } + } + }; + visit(root); +} + +function $collapseTabNodes(root: ElementNode): void { + const visit = (node: LexicalNode): void => { + if ($isTabNode(node)) { + node.replace($createTextNode('\t')); + return; + } + if ($isElementNode(node)) { + for (const child of node.getChildren()) { + visit(child); + } + } + }; + visit(root); +} diff --git a/web/src/lib/editor/markdown/horizontal-rule-node.ts b/web/src/lib/editor/markdown/horizontal-rule-node.ts new file mode 100644 index 0000000000..1975ccd74e --- /dev/null +++ b/web/src/lib/editor/markdown/horizontal-rule-node.ts @@ -0,0 +1,85 @@ +/** + * Custom HorizontalRuleNode for Lexical. + * Uses ElementNode (no decorative rendering needed). + */ + +import { + $applyNodeReplacement, + ElementNode, + type EditorConfig, + type ElementFormatType, + type LexicalNode, + type SerializedLexicalNode, + type Spread, +} from 'lexical'; + +export type SerializedHorizontalRuleNode = Spread< + { + children: []; + direction: 'ltr' | 'rtl' | null; + format: ElementFormatType; + indent: number; + }, + SerializedLexicalNode +>; + +export class HorizontalRuleNode extends ElementNode { + static getType(): string { + return 'horizontalrule'; + } + + static clone(node: HorizontalRuleNode): HorizontalRuleNode { + return new HorizontalRuleNode(node.__key); + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + static importJSON( + _serializedNode: SerializedHorizontalRuleNode, + ): HorizontalRuleNode { + return $createHorizontalRuleNode(); + } + /* eslint-enable @typescript-eslint/no-unused-vars */ + + exportJSON(): SerializedHorizontalRuleNode { + return { + children: [], + direction: 'ltr', + format: '', + indent: 0, + type: 'horizontalrule', + version: 1, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + createDOM(_config: EditorConfig, _editor: any): HTMLElement { + const element = document.createElement('hr'); + return element; + } + + updateDOM(): boolean { + return false; + } + + getTextContent(): string { + return '\n'; + } + + isInline(): boolean { + return false; + } + + canBeEmpty(): boolean { + return true; + } +} + +export function $createHorizontalRuleNode(): HorizontalRuleNode { + return $applyNodeReplacement(new HorizontalRuleNode()); +} + +export function $isHorizontalRuleNode( + node: LexicalNode | null | undefined, +): node is HorizontalRuleNode { + return node instanceof HorizontalRuleNode; +} diff --git a/web/src/lib/editor/markdown/horizontal-rule-transformer.ts b/web/src/lib/editor/markdown/horizontal-rule-transformer.ts new file mode 100644 index 0000000000..9971371d41 --- /dev/null +++ b/web/src/lib/editor/markdown/horizontal-rule-transformer.ts @@ -0,0 +1,31 @@ +/** + * Horizontal Rule transformer for markdown import/export + * Uses a custom inline node type to represent horizontal rules in Lexical. + */ + +import { type ElementTransformer } from '@lexical/markdown'; +import { type LexicalNode } from 'lexical'; + +import { + $createHorizontalRuleNode, + $isHorizontalRuleNode, + HorizontalRuleNode, +} from './horizontal-rule-node'; + +export const HR_TRANSFORMER: ElementTransformer = { + dependencies: [HorizontalRuleNode], + export: (node: LexicalNode) => { + return $isHorizontalRuleNode(node) ? '---' : null; + }, + regExp: /^(---|\*\*\*|___)\s?$/, + replace: (parentNode, _1, _2, isImport) => { + const line = $createHorizontalRuleNode(); + if (isImport || parentNode.getNextSibling() != null) { + parentNode.replace(line); + } else { + parentNode.insertBefore(line); + } + line.selectNext(); + }, + type: 'element', +}; diff --git a/web/src/lib/editor/markdown/index.ts b/web/src/lib/editor/markdown/index.ts new file mode 100644 index 0000000000..ec8a73acdd --- /dev/null +++ b/web/src/lib/editor/markdown/index.ts @@ -0,0 +1,75 @@ +/** + * Public API for the markdown pipeline. + */ + +import { CORE_TRANSFORMERS } from './markdown-transformers'; + +import type { LexicalTransformer } from './enhanced-markdown-export'; + +export { $convertFromEnhancedMarkdownString } from './enhanced-markdown-import'; +export type { EnhancedImportOptions } from './enhanced-markdown-import'; + +export { + $convertNodeToEnhancedMarkdownString, + $convertToEnhancedMarkdownString, +} from './enhanced-markdown-export'; +export type { + EnhancedExportOptions, + LexicalTransformer, +} from './enhanced-markdown-export'; + +export { + BOLD_ITALIC_STAR, + BOLD_ITALIC_UNDERSCORE, + BOLD_STAR, + BOLD_UNDERSCORE, + CHECK_LIST, + CODE, + CORE_TRANSFORMERS, + ELEMENT_TRANSFORMERS, + getListConfig, + getMarkdownConfig, + HEADING, + HIGHLIGHT, + INLINE_CODE, + ITALIC_STAR, + ITALIC_UNDERSCORE, + LINK, + MERMAID_TRANSFORMER, + MULTILINE_ELEMENT_TRANSFORMERS, + ORDERED_LIST, + QUOTE, + setListConfig, + setMarkdownConfig, + STRIKETHROUGH, + TABLE_TRANSFORMER, + TEXT_FORMAT_TRANSFORMERS, + TEXT_MATCH_TRANSFORMERS, + UNORDERED_LIST, +} from './markdown-transformers'; +export type { ListConfig, MarkdownConfig } from './markdown-transformers'; + +// Re-export types from @lexical/markdown +export type { + ElementTransformer, + MultilineElementTransformer, + TextFormatTransformer, + TextMatchTransformer, + Transformer, +} from '@lexical/markdown'; + +export { + $createHorizontalRuleNode, + $isHorizontalRuleNode, + HorizontalRuleNode, +} from './horizontal-rule-node'; +export type { SerializedHorizontalRuleNode } from './horizontal-rule-node'; + +/** + * Returns the core set of transformers used for markdown import/export. + * This function is used by plugins (e.g., TableTransformer) that need to + * perform markdown conversion inside their own logic. + */ +export function getEditorTransformers(): LexicalTransformer[] { + return CORE_TRANSFORMERS as LexicalTransformer[]; +} diff --git a/web/src/lib/editor/markdown/list-transformers.ts b/web/src/lib/editor/markdown/list-transformers.ts new file mode 100644 index 0000000000..139607719e --- /dev/null +++ b/web/src/lib/editor/markdown/list-transformers.ts @@ -0,0 +1,175 @@ +/** + * List transformers for markdown import/export. + * Ported from Nimbalyst with minimal dependencies. + */ + +import type { ListType } from '@lexical/list'; +import { + $createListItemNode, + $createListNode, + $isListItemNode, + $isListNode, + ListItemNode, + ListNode, +} from '@lexical/list'; +import type { ElementTransformer } from '@lexical/markdown'; +import type { ElementNode, LexicalNode } from 'lexical'; + +export const ORDERED_LIST_REGEX = /^(\s*)(\d{1,})\.\s/; +export const UNORDERED_LIST_REGEX = /^(\s*)[-*+]\s/; +export const CHECK_LIST_REGEX = /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i; + +export interface ListConfig { + exportIndentSize?: number; + importMinIndentSize?: number; + importMaxIndentSize?: number; + autoDetectIndent?: boolean; +} + +const DEFAULT_LIST_CONFIG: ListConfig = { + exportIndentSize: 2, + importMinIndentSize: 2, + importMaxIndentSize: 4, + autoDetectIndent: true, +}; + +let globalListConfig: ListConfig = { ...DEFAULT_LIST_CONFIG }; + +export function setListConfig(config: Partial): void { + globalListConfig = { ...DEFAULT_LIST_CONFIG, ...config }; +} + +export function getListConfig(): ListConfig { + return { ...globalListConfig }; +} + +export function getIndentLevel( + whitespaces: string, + isImport: boolean = true, +): number { + const tabs = whitespaces.match(/\t/g); + const spaces = whitespaces.match(/ /g); + let indent = 0; + if (tabs) indent += tabs.length; + if (spaces) { + const spaceCount = spaces.length; + const config = getListConfig(); + if (isImport) { + const importSize = config.importMinIndentSize ?? 2; + indent += Math.floor(spaceCount / importSize); + } else { + const exportSize = config.exportIndentSize ?? 2; + indent += Math.floor(spaceCount / exportSize); + } + } + return indent; +} + +export function createListReplace( + listType: ListType, +): ElementTransformer['replace'] { + return (parentNode, children, match, isImport) => { + const previousNode = parentNode.getPreviousSibling(); + const nextNode = parentNode.getNextSibling(); + const listItem = $createListItemNode( + listType === 'check' ? match[3] === 'x' : undefined, + ); + const indent = getIndentLevel(match[1], true); + + if ($isListNode(nextNode) && nextNode.getListType() === listType) { + const firstChild = nextNode.getFirstChild(); + if (firstChild !== null) firstChild.insertBefore(listItem); + else nextNode.append(listItem); + parentNode.remove(); + } else if ( + $isListNode(previousNode) && + previousNode.getListType() === listType + ) { + previousNode.append(listItem); + parentNode.remove(); + } else { + const list = $createListNode( + listType, + listType === 'number' ? Number(match[2]) : undefined, + ); + list.append(listItem); + parentNode.replace(list); + } + + listItem.append(...children); + if (!isImport) listItem.select(0, 0); + if (indent > 0) listItem.setIndent(indent); + }; +} + +export function listExport( + listNode: ListNode, + exportChildren: (node: ElementNode) => string, + depth: number = 0, + config?: ListConfig, +): string { + const mergedConfig = { ...globalListConfig, ...config }; + const indentSize = mergedConfig.exportIndentSize ?? 2; + const output: string[] = []; + const children = listNode.getChildren(); + let index = 0; + + for (const listItemNode of children) { + if (!$isListItemNode(listItemNode)) continue; + + if (listItemNode.getChildrenSize() === 1) { + const firstChild = listItemNode.getFirstChild(); + if ($isListNode(firstChild)) { + output.push(listExport(firstChild, exportChildren, depth + 1, config)); + continue; + } + } + + const itemIndent = listItemNode.getIndent(); + const actualDepth = itemIndent !== undefined ? itemIndent : depth; + const indent = ' '.repeat(actualDepth * indentSize); + const listType = listNode.getListType(); + const prefix = + listType === 'number' + ? `${listNode.getStart() + index}. ` + : listType === 'check' + ? `- [${listItemNode.getChecked() ? 'x' : ' '}] ` + : '- '; + + output.push(indent + prefix + exportChildren(listItemNode)); + index++; + } + return output.join('\n'); +} + +export function createListTransformer( + listType: ListType, + regex: RegExp, +): ElementTransformer { + return { + dependencies: [ListNode, ListItemNode], + export: ( + node: LexicalNode, + exportChildren: (node: ElementNode) => string, + ) => { + if (!$isListNode(node)) return null; + return listExport(node, exportChildren, 0, getListConfig()); + }, + regExp: regex, + replace: createListReplace(listType), + type: 'element', + }; +} + +export const UNORDERED_LIST: ElementTransformer = createListTransformer( + 'bullet', + UNORDERED_LIST_REGEX, +); +export const ORDERED_LIST: ElementTransformer = createListTransformer( + 'number', + ORDERED_LIST_REGEX, +); +export const CHECK_LIST: ElementTransformer = createListTransformer( + 'check', + CHECK_LIST_REGEX, +); diff --git a/web/src/lib/editor/markdown/markdown-transformers.ts b/web/src/lib/editor/markdown/markdown-transformers.ts new file mode 100644 index 0000000000..81d0d05ff0 --- /dev/null +++ b/web/src/lib/editor/markdown/markdown-transformers.ts @@ -0,0 +1,308 @@ +/** + * Markdown transformers for Lexical import/export. + * Ported from Nimbalyst — handles: + * - HEADING, QUOTE, CODE, MERMAID + * - LISTS (via ListTransformers) + * - Text format (bold, italic, strikethrough, code, highlight) + * - LINK, HR + */ + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type, no-useless-escape */ + +import { $createCodeNode, $isCodeNode, CodeNode } from '@lexical/code'; +import { + $createLinkNode, + $isAutoLinkNode, + $isLinkNode, + LinkNode, +} from '@lexical/link'; +import { + BOLD_ITALIC_STAR as UPSTREAM_BOLD_ITALIC_STAR, + BOLD_ITALIC_UNDERSCORE as UPSTREAM_BOLD_ITALIC_UNDERSCORE, + BOLD_STAR as UPSTREAM_BOLD_STAR, + BOLD_UNDERSCORE as UPSTREAM_BOLD_UNDERSCORE, + HIGHLIGHT as UPSTREAM_HIGHLIGHT, + INLINE_CODE as UPSTREAM_INLINE_CODE, + ITALIC_STAR as UPSTREAM_ITALIC_STAR, + ITALIC_UNDERSCORE as UPSTREAM_ITALIC_UNDERSCORE, + STRIKETHROUGH as UPSTREAM_STRIKETHROUGH, +} from '@lexical/markdown'; +import type { HeadingTagType } from '@lexical/rich-text'; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + $isQuoteNode, + HeadingNode, + QuoteNode, +} from '@lexical/rich-text'; +import { + $createLineBreakNode, + $createTextNode, + type ElementNode, + type LexicalNode, +} from 'lexical'; + +import type { + ElementTransformer, + MultilineElementTransformer, + TextFormatTransformer, + TextMatchTransformer, + Transformer, +} from '@lexical/markdown'; + +import { + CHECK_LIST, + getListConfig, + ORDERED_LIST, + setListConfig, + UNORDERED_LIST, + type ListConfig, +} from './list-transformers'; + +export { + CHECK_LIST, + getListConfig, + ORDERED_LIST, + setListConfig, + UNORDERED_LIST, + type ListConfig, +}; + +import { MERMAID_TRANSFORMER } from '../mermaid-transformer'; +import { TABLE_TRANSFORMER } from '../plugins/table-transformer'; +import { HR_TRANSFORMER } from './horizontal-rule-transformer'; + +const HEADING_REGEX = /^(#{1,6})\s/; +const QUOTE_REGEX = /^>\s/; +const CODE_START_REGEX = /^[ \t]*```([\w-]+)?/; +const CODE_END_REGEX = /[ \t]*```$/; + +const createBlockNode = ( + createNode: (match: Array) => ElementNode, +): ElementTransformer['replace'] => { + return (parentNode, children, match, isImport) => { + const node = createNode(match); + node.append(...children); + parentNode.replace(node); + if (!isImport) node.select(0, 0); + }; +}; + +export interface MarkdownConfig {} + +export function setMarkdownConfig( + config: Partial, +): void { + if ( + 'exportIndentSize' in config || + 'importMinIndentSize' in config || + 'importMaxIndentSize' in config || + 'autoDetectIndent' in config + ) { + setListConfig(config); + } +} + +export function getMarkdownConfig(): MarkdownConfig & ListConfig { + return { ...getListConfig() }; +} + +export const HEADING: ElementTransformer = { + dependencies: [HeadingNode], + export: (node, exportChildren) => { + if (!$isHeadingNode(node)) return null; + const level = Number(node.getTag().slice(1)); + return '#'.repeat(level) + ' ' + exportChildren(node); + }, + regExp: HEADING_REGEX, + replace: createBlockNode((match) => { + const tag = ('h' + match[1].length) as HeadingTagType; + return $createHeadingNode(tag); + }), + type: 'element', +}; + +export const QUOTE: ElementTransformer = { + dependencies: [QuoteNode], + export: (node, exportChildren) => { + if (!$isQuoteNode(node)) return null; + const lines = exportChildren(node).split('\n'); + return lines.map((line) => '> ' + line).join('\n'); + }, + regExp: QUOTE_REGEX, + replace: (parentNode, children, _match, isImport) => { + if (isImport) { + const previousNode = parentNode.getPreviousSibling(); + if ($isQuoteNode(previousNode)) { + previousNode.splice(previousNode.getChildrenSize(), 0, [ + $createLineBreakNode(), + ...children, + ]); + parentNode.remove(); + return; + } + } + const node = $createQuoteNode(); + node.append(...children); + parentNode.replace(node); + if (!isImport) node.select(0, 0); + }, + type: 'element', +}; + +const NO_LANGUAGE_MARKER = 'plain'; + +export const CODE: MultilineElementTransformer = { + dependencies: [CodeNode], + export: (node: LexicalNode) => { + if (!$isCodeNode(node)) return null; + const textContent = node.getTextContent(); + const language = node.getLanguage(); + const langOutput = language === NO_LANGUAGE_MARKER ? '' : language || ''; + return ( + '```' + + langOutput + + (textContent ? '\n' + textContent : '') + + '\n' + + '```' + ); + }, + regExpEnd: { optional: true, regExp: CODE_END_REGEX }, + regExpStart: CODE_START_REGEX, + replace: ( + rootNode, + children, + startMatch, + endMatch, + linesInBetween, + isImport, + ) => { + let codeBlockNode: CodeNode; + let code: string; + + if (!children && linesInBetween) { + if (linesInBetween.length === 1) { + if (endMatch) { + codeBlockNode = $createCodeNode(NO_LANGUAGE_MARKER); + code = startMatch[1] + linesInBetween[0]; + } else { + codeBlockNode = $createCodeNode(startMatch[1] || NO_LANGUAGE_MARKER); + code = linesInBetween[0].startsWith(' ') + ? linesInBetween[0].slice(1) + : linesInBetween[0]; + } + } else { + codeBlockNode = $createCodeNode(startMatch[1] || NO_LANGUAGE_MARKER); + if (linesInBetween[0].trim().length === 0) { + while (linesInBetween.length > 0 && !linesInBetween[0].length) + linesInBetween.shift(); + } else { + linesInBetween[0] = linesInBetween[0].startsWith(' ') + ? linesInBetween[0].slice(1) + : linesInBetween[0]; + } + while ( + linesInBetween.length > 0 && + !linesInBetween[linesInBetween.length - 1].length + ) + linesInBetween.pop(); + code = linesInBetween.join('\n'); + } + const textNode = $createTextNode(code); + codeBlockNode.append(textNode); + rootNode.append(codeBlockNode); + } else if (children) { + createBlockNode((match) => { + return $createCodeNode( + match && match[1] ? match[1] : NO_LANGUAGE_MARKER, + ); + })(rootNode, children, startMatch, isImport); + } + }, + type: 'multiline-element', +}; + +// Re-export upstream text-format transformers +export const INLINE_CODE: TextFormatTransformer = UPSTREAM_INLINE_CODE; +export const HIGHLIGHT: TextFormatTransformer = UPSTREAM_HIGHLIGHT; +export const BOLD_ITALIC_STAR: TextFormatTransformer = + UPSTREAM_BOLD_ITALIC_STAR; +export const BOLD_ITALIC_UNDERSCORE: TextFormatTransformer = + UPSTREAM_BOLD_ITALIC_UNDERSCORE; +export const BOLD_STAR: TextFormatTransformer = UPSTREAM_BOLD_STAR; +export const BOLD_UNDERSCORE: TextFormatTransformer = UPSTREAM_BOLD_UNDERSCORE; +export const STRIKETHROUGH: TextFormatTransformer = UPSTREAM_STRIKETHROUGH; +export const ITALIC_STAR: TextFormatTransformer = UPSTREAM_ITALIC_STAR; +export const ITALIC_UNDERSCORE: TextFormatTransformer = + UPSTREAM_ITALIC_UNDERSCORE; + +export const LINK: TextMatchTransformer = { + dependencies: [LinkNode], + export: (node, exportChildren, _exportFormat) => { + if (!$isLinkNode(node) || $isAutoLinkNode(node)) return null; + const title = node.getTitle(); + const textContent = exportChildren(node); + return title + ? `[${textContent}](${node.getURL()} "${title}")` + : `[${textContent}](${node.getURL()})`; + }, + importRegExp: + /(? { + const [, linkText, rawUrl, linkTitle] = match; + const linkUrl = + rawUrl && rawUrl.startsWith('<') && rawUrl.endsWith('>') + ? rawUrl.slice(1, -1) + : rawUrl; + const linkNode = $createLinkNode(linkUrl, { title: linkTitle }); + const linkTextNode = $createTextNode(linkText); + linkTextNode.setFormat(textNode.getFormat()); + linkNode.append(linkTextNode); + textNode.replace(linkNode); + return linkTextNode; + }, + trigger: ')', + type: 'text-match', +}; + +// Aggregated core transformer arrays +export const ELEMENT_TRANSFORMERS: Transformer[] = [ + HEADING, + QUOTE, + UNORDERED_LIST, + ORDERED_LIST, + TABLE_TRANSFORMER, +]; + +// MERMAID must come BEFORE CODE to intercept ```mermaid blocks first +export const MULTILINE_ELEMENT_TRANSFORMERS: Transformer[] = [ + MERMAID_TRANSFORMER, + CODE, +]; + +export const TEXT_FORMAT_TRANSFORMERS: Transformer[] = [ + INLINE_CODE, + BOLD_ITALIC_STAR, + BOLD_ITALIC_UNDERSCORE, + BOLD_STAR, + BOLD_UNDERSCORE, + HIGHLIGHT, + ITALIC_STAR, + ITALIC_UNDERSCORE, + STRIKETHROUGH, +]; +export const TEXT_MATCH_TRANSFORMERS: Transformer[] = [LINK]; + +export const CORE_TRANSFORMERS: Transformer[] = [ + HR_TRANSFORMER, + CHECK_LIST, + ...ELEMENT_TRANSFORMERS, + ...MULTILINE_ELEMENT_TRANSFORMERS, + ...TEXT_FORMAT_TRANSFORMERS, + ...TEXT_MATCH_TRANSFORMERS, +]; + +export { MERMAID_TRANSFORMER } from '../mermaid-transformer'; +export { TABLE_TRANSFORMER } from '../plugins/table-transformer'; diff --git a/web/src/lib/editor/mermaid-node.tsx b/web/src/lib/editor/mermaid-node.tsx new file mode 100644 index 0000000000..d554723fbc --- /dev/null +++ b/web/src/lib/editor/mermaid-node.tsx @@ -0,0 +1,428 @@ +/** + * MermaidNode - A Lexical DecoratorNode for rendering Mermaid diagrams. + * Ported from Nimbalyst — supports click-to-edit source code. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { addClassNamesToElement } from '@lexical/utils'; +import type { + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + LexicalEditor, + LexicalNode, + NodeKey, + SerializedLexicalNode, + Spread, +} from 'lexical'; +import { $applyNodeReplacement, DecoratorNode } from 'lexical'; +import type { JSX } from 'react'; + +export interface MermaidPayload { + content: string; + key?: NodeKey; +} + +export type SerializedMermaidNode = Spread< + { content: string }, + SerializedLexicalNode +>; + +export class MermaidNode extends DecoratorNode { + __content: string; + + constructor(content: string, key?: NodeKey) { + super(key); + this.__content = content; + } + + static getType(): string { + return 'mermaid'; + } + + static clone(node: MermaidNode): MermaidNode { + return new MermaidNode(node.__content, node.__key); + } + + static importJSON(serializedNode: SerializedMermaidNode): MermaidNode { + return $createMermaidNode({ content: serializedNode.content }); + } + + exportJSON(): SerializedMermaidNode { + return { + content: this.__content, + type: 'mermaid', + version: 1, + }; + } + + createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement { + const div = document.createElement('div'); + addClassNamesToElement(div, 'mermaid-container'); + return div; + } + + updateDOM(_prevNode: MermaidNode, _dom: HTMLElement): boolean { + return _prevNode.__content !== this.__content; + } + + exportDOM(_editor: LexicalEditor): DOMExportOutput { + const element = document.createElement('div'); + element.classList.add('mermaid-container'); + const pre = document.createElement('pre'); + const code = document.createElement('code'); + code.classList.add('language-mermaid'); + code.textContent = this.__content; + pre.appendChild(code); + element.appendChild(pre); + return { element }; + } + + static importDOM(): DOMConversionMap | null { + return { + div: (domNode: HTMLElement) => { + if (!domNode.classList.contains('mermaid-container')) return null; + return { conversion: convertMermaidElement, priority: 1 }; + }, + }; + } + + getContent(): string { + return this.__content; + } + + getTextContent(): string { + return this.__content; + } + + setContent(content: string): void { + const writable = this.getWritable(); + writable.__content = content; + } + + decorate(_editor: LexicalEditor, _config: EditorConfig): JSX.Element { + return ( + + ); + } +} + +function convertMermaidElement( + domNode: HTMLElement, +): DOMConversionOutput | null { + const codeElement = domNode.querySelector('code.language-mermaid'); + if (codeElement) { + const node = $createMermaidNode({ content: codeElement.textContent || '' }); + return { node }; + } + return null; +} + +export function $createMermaidNode(payload?: MermaidPayload): MermaidNode { + const content = payload?.content || 'graph TD\n A[Start] --> B[End]'; + return $applyNodeReplacement(new MermaidNode(content, payload?.key)); +} + +export function $isMermaidNode( + node: LexicalNode | null | undefined, +): node is MermaidNode { + return node instanceof MermaidNode; +} + +// ─── React rendering component ───────────────────────────── + +import { useTheme } from '@/components/theme-provider'; +import { $getNodeByKey } from 'lexical'; +import mermaid from 'mermaid'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +let mermaidInitialized = false; +function ensureMermaidInit(themeName: string) { + const mermaidTheme = themeName === 'dark' ? 'dark' : 'default'; + if (!mermaidInitialized) { + mermaid.initialize({ + startOnLoad: false, + theme: mermaidTheme, + securityLevel: 'antiscript', + fontFamily: 'monospace', + }); + mermaidInitialized = true; + } else { + mermaidInitialized = false; + mermaid.initialize({ + startOnLoad: false, + theme: mermaidTheme, + securityLevel: 'antiscript', + fontFamily: 'monospace', + }); + mermaidInitialized = true; + } +} + +// Sequential render queue — avoids freezing with many diagrams +let renderQueue = Promise.resolve(); +function queueRender(fn: () => Promise): void { + renderQueue = renderQueue.then( + () => + new Promise((resolve) => { + setTimeout(async () => { + await fn(); + resolve(); + }, 30); + }), + ); +} + +interface MermaidComponentProps { + content: string; + nodeKey: NodeKey; + editor: LexicalEditor; +} + +function MermaidComponent({ content, nodeKey, editor }: MermaidComponentProps) { + const containerRef = useRef(null); + const textareaRef = useRef(null); + const [state, setState] = useState<'loading' | 'error' | 'done'>('loading'); + const [error, setError] = useState(null); + const [editing, setEditing] = useState(false); + const [editContent, setEditContent] = useState(content); + const { theme } = useTheme(); + + // ── Render/re-render diagram ── + const doRender = useCallback( + async (source: string) => { + if (!containerRef.current) return; + try { + const id = `mermaid_${nodeKey}_${Date.now()}`; + const { svg } = await mermaid.render(id, source); + if (containerRef.current) { + containerRef.current.innerHTML = svg; + setState('done'); + setError(null); + } + } catch (err: any) { + setState('error'); + setError(err?.message || String(err)); + } + }, + [nodeKey], + ); + + useEffect(() => { + let mounted = true; + ensureMermaidInit(theme); + + queueRender(async () => { + if (!mounted) return; + if (!containerRef.current) return; + await doRender(content); + }); + + return () => { + mounted = false; + }; + }, [content, nodeKey, theme, doRender, editing]); + + // ── Click to edit ── + const handleDiagramClick = () => { + setEditContent(content); + setEditing(true); + }; + + // ── Commit changes back to Lexical node ── + const commitChanges = useCallback(() => { + const newContent = editContent.trim(); + if (newContent && newContent !== content) { + editor.update(() => { + const node = $getNodeByKey(nodeKey); + if ($isMermaidNode(node)) { + node.setContent(newContent); + } + }); + } + setEditing(false); + }, [editContent, content, editor, nodeKey]); + + // ── Live preview while editing (debounced) ── + const debounceRef = useRef>(); + const handleEditChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setEditContent(val); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(async () => { + if (val.trim()) { + ensureMermaidInit(theme); + await doRender(val); + } + }, 350); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + commitChanges(); + } + if (e.key === 'Escape') { + setEditing(false); + setEditContent(content); + } + }; + + // Error state + if (state === 'error' && !editing) { + return ( +
+
+          {content}
+        
+
+ {error} — click to edit source +
+
+ ); + } + + // Edit mode + if (editing) { + return ( +
+
+