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 (
+
+
+
+
+ Ctrl+Enter to apply
+ Esc to cancel
+
+
+ );
+ }
+
+ // Normal display mode with border + edit button above
+ return (
+
+
+
+ mermaid
+
+
+
+
+
+ );
+}
diff --git a/web/src/lib/editor/mermaid-transformer.ts b/web/src/lib/editor/mermaid-transformer.ts
new file mode 100644
index 0000000000..4f97dae5c4
--- /dev/null
+++ b/web/src/lib/editor/mermaid-transformer.ts
@@ -0,0 +1,38 @@
+/**
+ * MermaidTransformer - handles ```mermaid code blocks.
+ * Ported from Nimbalyst. Creates MermaidNode instead of CodeNode.
+ */
+
+/* eslint-disable no-console */
+
+import type { MultilineElementTransformer } from '@lexical/markdown';
+import {
+ $createMermaidNode,
+ $isMermaidNode,
+ MermaidNode,
+} from './mermaid-node';
+
+const MERMAID_START_REGEX = /^[ \t]*```mermaid/;
+const MERMAID_END_REGEX = /[ \t]*```$/;
+
+export const MERMAID_TRANSFORMER: MultilineElementTransformer = {
+ dependencies: [MermaidNode],
+ export: (node) => {
+ if (!$isMermaidNode(node)) return null;
+ return '```mermaid\n' + node.getContent() + '\n```';
+ },
+ regExpStart: MERMAID_START_REGEX,
+ regExpEnd: { optional: true, regExp: MERMAID_END_REGEX },
+ replace: (rootNode, _children, _startMatch, _endMatch, linesInBetween) => {
+ const content = linesInBetween ? linesInBetween.join('\n').trim() : '';
+ console.log(
+ '[MermaidTransformer] Creating MermaidNode, content length:',
+ content.length,
+ 'lines:',
+ linesInBetween?.length,
+ );
+ const mermaidNode = $createMermaidNode({ content });
+ rootNode.append(mermaidNode);
+ },
+ type: 'multiline-element',
+};
diff --git a/web/src/lib/editor/nodes.ts b/web/src/lib/editor/nodes.ts
new file mode 100644
index 0000000000..8e08e65e8d
--- /dev/null
+++ b/web/src/lib/editor/nodes.ts
@@ -0,0 +1,33 @@
+/**
+ * Lexical nodes for the editor.
+ * Each node type must be registered in the LexicalComposer config.
+ */
+
+import { CodeHighlightNode, CodeNode } from '@lexical/code';
+import { HashtagNode } from '@lexical/hashtag';
+import { AutoLinkNode, LinkNode } from '@lexical/link';
+import { ListItemNode, ListNode } from '@lexical/list';
+import { HeadingNode, QuoteNode } from '@lexical/rich-text';
+import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
+import type { Klass, LexicalNode } from 'lexical';
+import { HorizontalRuleNode } from './markdown/horizontal-rule-node';
+import { MermaidNode } from './mermaid-node';
+
+const EditorNodes: Array> = [
+ HeadingNode,
+ QuoteNode,
+ CodeNode,
+ CodeHighlightNode,
+ ListItemNode,
+ ListNode,
+ LinkNode,
+ AutoLinkNode,
+ HashtagNode,
+ HorizontalRuleNode,
+ MermaidNode,
+ TableNode,
+ TableRowNode,
+ TableCellNode,
+];
+
+export default EditorNodes;
diff --git a/web/src/lib/editor/plugins/floating-selection-toolbar.tsx b/web/src/lib/editor/plugins/floating-selection-toolbar.tsx
new file mode 100644
index 0000000000..2c25388cd8
--- /dev/null
+++ b/web/src/lib/editor/plugins/floating-selection-toolbar.tsx
@@ -0,0 +1,442 @@
+/**
+ * FloatingSelectionToolbar — appears when text is selected,
+ * provides formatting: heading, bold, italic, code, strikethrough, link, quote.
+ */
+import { $createCodeNode, $isCodeNode } from '@lexical/code';
+import { TOGGLE_LINK_COMMAND } from '@lexical/link';
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import type { HeadingTagType } from '@lexical/rich-text';
+import {
+ $createHeadingNode,
+ $createQuoteNode,
+ $isHeadingNode,
+ $isQuoteNode,
+} from '@lexical/rich-text';
+import { $setBlocksType } from '@lexical/selection';
+import {
+ $createParagraphNode,
+ $getRoot,
+ $getSelection,
+ $isElementNode,
+ $isRangeSelection,
+ COMMAND_PRIORITY_LOW,
+ FORMAT_TEXT_COMMAND,
+ SELECTION_CHANGE_COMMAND,
+} from 'lexical';
+import { useEffect, useRef, useState } from 'react';
+
+export default function FloatingSelectionToolbar() {
+ const [editor] = useLexicalComposerContext();
+ const [show, setShow] = useState(false);
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+ const barRef = useRef(null);
+ const [currentBlock, setCurrentBlock] = useState('paragraph');
+
+ useEffect(() => {
+ const removeUpdateListener = editor.registerUpdateListener(() => {
+ editor.getEditorState().read(() => {
+ const selection = $getSelection();
+ if (!$isRangeSelection(selection) || selection.isCollapsed()) {
+ setShow(false);
+ return;
+ }
+ const nativeSel = window.getSelection();
+ if (!nativeSel || nativeSel.isCollapsed || !nativeSel.rangeCount) {
+ setShow(false);
+ return;
+ }
+
+ // Detect current block type
+ const anchor = selection.anchor.getNode();
+ let block = 'paragraph';
+ let parent = $isElementNode(anchor) ? anchor : anchor.getParent();
+ while (parent && parent !== $getRoot()) {
+ if ($isHeadingNode(parent)) {
+ block = parent.getTag();
+ break;
+ }
+ if ($isQuoteNode(parent)) {
+ block = 'quote';
+ break;
+ }
+ if ($isCodeNode(parent)) {
+ block = 'code';
+ break;
+ }
+ parent = parent.getParent();
+ }
+ setCurrentBlock(block);
+
+ const range = nativeSel.getRangeAt(0);
+ const rect = range.getBoundingClientRect();
+ setPos({
+ x: rect.left + rect.width / 2,
+ y: rect.top - 10,
+ });
+ setShow(true);
+ });
+ });
+
+ const removeSelectionListener = editor.registerCommand(
+ SELECTION_CHANGE_COMMAND,
+ () => false,
+ COMMAND_PRIORITY_LOW,
+ );
+
+ return () => {
+ removeUpdateListener();
+ removeSelectionListener();
+ };
+ }, [editor]);
+
+ const format = (type: 'bold' | 'italic' | 'strikethrough' | 'code') => {
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, type);
+ setShow(false);
+ };
+
+ const toggleLink = () => {
+ const url = window.prompt('Enter URL:');
+ if (url) {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
+ }
+ setShow(false);
+ };
+
+ const setHeading = (tag: HeadingTagType) => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createHeadingNode(tag));
+ }
+ });
+ setShow(false);
+ };
+
+ const setQuote = () => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createQuoteNode());
+ }
+ });
+ setShow(false);
+ };
+
+ const setParagraph = () => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ }
+ });
+ setShow(false);
+ };
+
+ const [showCodeLang, setShowCodeLang] = useState(false);
+
+ const setCodeBlock = (lang?: string) => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ const node = $createCodeNode(lang);
+ $setBlocksType(selection, () => node);
+ }
+ });
+ setShow(false);
+ };
+
+ const codeLangs = [
+ '',
+ 'javascript',
+ 'typescript',
+ 'python',
+ 'html',
+ 'css',
+ 'scss',
+ 'less',
+ 'json',
+ 'yaml',
+ 'xml',
+ 'bash',
+ 'sh',
+ 'powershell',
+ 'sql',
+ 'graphql',
+ 'go',
+ 'rust',
+ 'cpp',
+ 'c',
+ 'csharp',
+ 'java',
+ 'kotlin',
+ 'swift',
+ 'php',
+ 'ruby',
+ 'perl',
+ 'lua',
+ 'r',
+ 'matlab',
+ 'dart',
+ 'zig',
+ 'makefile',
+ 'dockerfile',
+ 'ini',
+ 'toml',
+ 'diff',
+ 'markdown',
+ 'mermaid',
+ 'plaintext',
+ ];
+
+ const [showHeadingMenu, setShowHeadingMenu] = useState(false);
+
+ if (!show) return null;
+
+ const btnStyle: React.CSSProperties = {
+ padding: '4px 7px',
+ border: 'none',
+ borderRadius: 4,
+ cursor: 'pointer',
+ fontSize: 12,
+ fontWeight: 500,
+ background: 'transparent',
+ color: 'var(--nim-text)',
+ fontFamily: 'inherit',
+ lineHeight: 1,
+ whiteSpace: 'nowrap',
+ };
+ const activeStyle: React.CSSProperties = {
+ ...btnStyle,
+ background: 'var(--nim-bg-selected)',
+ color: 'var(--nim-primary)',
+ };
+
+ const headingLabels: Record = {
+ h1: 'H1',
+ h2: 'H2',
+ h3: 'H3',
+ h4: 'H4',
+ h5: 'H5',
+ h6: 'H6',
+ };
+
+ return (
+
+ {/* Heading dropdown (H1-H6 only) */}
+
+
+ {showHeadingMenu && (
+
+ {(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const).map((tag) => (
+
{
+ setShowHeadingMenu(false);
+ setHeading(tag);
+ }}
+ style={{
+ padding: '5px 12px',
+ cursor: 'pointer',
+ fontSize: 12,
+ fontWeight: tag === 'h1' ? 700 : tag === 'h2' ? 600 : 500,
+ color:
+ currentBlock === tag
+ ? 'var(--nim-primary)'
+ : 'var(--nim-text)',
+ background:
+ currentBlock === tag
+ ? 'var(--nim-bg-selected)'
+ : 'transparent',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.background = 'var(--nim-bg-hover)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background =
+ currentBlock === tag
+ ? 'var(--nim-bg-selected)'
+ : 'transparent';
+ }}
+ >
+ {headingLabels[tag]}
+
+ ))}
+
+ )}
+
+
+ {/* Block type buttons: Paragraph, Quote */}
+
+
+
+ {/* Code Block with language picker */}
+
+
+ {showCodeLang && (
+
+ {codeLangs.map((lang) => (
+
{
+ setShowCodeLang(false);
+ setCodeBlock(lang || undefined);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setShowCodeLang(false);
+ setCodeBlock(lang || undefined);
+ }
+ }}
+ style={{
+ padding: '4px 12px',
+ cursor: 'pointer',
+ fontSize: 11,
+ color: 'var(--nim-text)',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.background = 'var(--nim-bg-hover)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background = 'transparent';
+ }}
+ >
+ {lang || '(no language)'}
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Inline format controls */}
+
+
+
+
+
+
+ {/* Link */}
+
+
+ );
+}
diff --git a/web/src/lib/editor/plugins/table-actions-plugin.tsx b/web/src/lib/editor/plugins/table-actions-plugin.tsx
new file mode 100644
index 0000000000..94f6ef9ac6
--- /dev/null
+++ b/web/src/lib/editor/plugins/table-actions-plugin.tsx
@@ -0,0 +1,315 @@
+/**
+ * TableActionsPlugin — floating toolbar for table editing.
+ * Appears above the table when cursor is inside it.
+ * Provides: row/column count adjustment, text alignment.
+ */
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import {
+ $createTableCellNode,
+ $createTableRowNode,
+ $isTableCellNode,
+ $isTableNode,
+ $isTableRowNode,
+ $removeTableRowAtIndex,
+ TableCellHeaderStates,
+ type TableNode,
+} from '@lexical/table';
+import {
+ $findMatchingParent,
+ $getNodeByKey,
+ $getSelection,
+ $isElementNode,
+ $isParagraphNode,
+ $isRangeSelection,
+ COMMAND_PRIORITY_LOW,
+ SELECTION_CHANGE_COMMAND,
+} from 'lexical';
+import { useCallback, useEffect, useState } from 'react';
+
+interface TableInfo {
+ key: string;
+ rows: number;
+ cols: number;
+}
+
+/** Read-only: check if selection is inside a table and return its info. */
+function $readTableInfo(): TableInfo | null {
+ const selection = $getSelection();
+ if (!$isRangeSelection(selection)) return null;
+ const cellNode = $findMatchingParent(
+ selection.anchor.getNode(),
+ $isTableCellNode,
+ );
+ if (!cellNode) return null;
+ const table = $findMatchingParent(cellNode, $isTableNode) as TableNode | null;
+ if (!table) return null;
+ const rows = table.getChildren().length;
+ const firstRow = table.getFirstChild();
+ const cols = $isTableRowNode(firstRow) ? firstRow.getChildren().length : 0;
+ return { key: table.getKey(), rows, cols };
+}
+
+export default function TableActionsPlugin() {
+ const [editor] = useLexicalComposerContext();
+ const [show, setShow] = useState(false);
+ const [rows, setRows] = useState(0);
+ const [cols, setCols] = useState(0);
+ const [tableKey, setTableKey] = useState(null);
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+
+ const checkTable = useCallback(() => {
+ const info: TableInfo | null = editor.getEditorState().read(() => {
+ return $readTableInfo();
+ });
+
+ if (!info) {
+ setShow(false);
+ setTableKey(null);
+ return;
+ }
+
+ const { key, rows: r, cols: c } = info;
+ setTableKey(key);
+ setRows(r);
+ setCols(c);
+
+ // Try to position toolbar above the table's DOM element
+ try {
+ editor.read(() => {
+ const domEl = editor.getElementByKey(key);
+ if (domEl) {
+ const tableRect = domEl.getBoundingClientRect();
+ setPos({ x: tableRect.left, y: tableRect.top - 36 });
+ setShow(true);
+ }
+ });
+ } catch {
+ // DOM not ready yet
+ }
+ }, [editor]);
+
+ useEffect(() => {
+ const removeUpdateListener = editor.registerUpdateListener(() => {
+ checkTable();
+ });
+
+ const removeSelectionListener = editor.registerCommand(
+ SELECTION_CHANGE_COMMAND,
+ () => {
+ checkTable();
+ return false;
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+
+ return () => {
+ removeUpdateListener();
+ removeSelectionListener();
+ };
+ }, [editor, checkTable]);
+
+ const handleAddRow = useCallback(() => {
+ if (!tableKey) return;
+ editor.update(() => {
+ const node = $getNodeByKey(tableKey);
+ if (!$isTableNode(node)) return;
+ const firstRow = node.getFirstChild();
+ if (!$isTableRowNode(firstRow)) return;
+ const colCount = firstRow.getChildren().length;
+ const newRow = $createTableRowNode();
+ for (let i = 0; i < colCount; i++) {
+ newRow.append($createTableCellNode(TableCellHeaderStates.NO_STATUS));
+ }
+ node.append(newRow);
+ });
+ }, [editor, tableKey]);
+
+ const handleRemoveRow = useCallback(() => {
+ if (!tableKey || rows <= 1) return;
+ editor.update(() => {
+ const node = $getNodeByKey(tableKey);
+ if (!$isTableNode(node)) return;
+ $removeTableRowAtIndex(node, rows - 1);
+ });
+ }, [editor, tableKey, rows]);
+
+ const handleAddCol = useCallback(() => {
+ if (!tableKey) return;
+ editor.update(() => {
+ const node = $getNodeByKey(tableKey);
+ if (!$isTableNode(node)) return;
+ for (const row of node.getChildren()) {
+ if ($isTableRowNode(row)) {
+ row.append($createTableCellNode(TableCellHeaderStates.NO_STATUS));
+ }
+ }
+ });
+ }, [editor, tableKey]);
+
+ const handleRemoveCol = useCallback(() => {
+ if (!tableKey || cols <= 1) return;
+ editor.update(() => {
+ const node = $getNodeByKey(tableKey);
+ if (!$isTableNode(node)) return;
+ for (const row of node.getChildren()) {
+ if ($isTableRowNode(row)) {
+ const lastCell = row.getLastChild();
+ lastCell?.remove();
+ }
+ }
+ });
+ }, [editor, tableKey, cols]);
+
+ const setAlignment = useCallback(
+ (align: 'left' | 'center' | 'right') => {
+ if (!tableKey) return;
+ editor.update(() => {
+ const node = $getNodeByKey(tableKey);
+ if (!$isTableNode(node)) return;
+ for (const row of node.getChildren()) {
+ if (!$isTableRowNode(row)) continue;
+ for (const cell of row.getChildren()) {
+ if (!$isTableCellNode(cell)) continue;
+ for (const child of cell.getChildren()) {
+ if ($isParagraphNode(child) || $isElementNode(child)) {
+ child.setFormat(align);
+ }
+ }
+ }
+ }
+ });
+ },
+ [editor, tableKey],
+ );
+
+ if (!show) return null;
+
+ const btnStyle: React.CSSProperties = {
+ padding: '4px 7px',
+ border: 'none',
+ borderRadius: 4,
+ cursor: 'pointer',
+ fontSize: 11,
+ fontWeight: 500,
+ background: 'transparent',
+ color: 'var(--nim-text)',
+ fontFamily: 'inherit',
+ lineHeight: 1,
+ };
+
+ const countStyle: React.CSSProperties = {
+ padding: '2px 6px',
+ fontSize: 12,
+ fontWeight: 600,
+ color: 'var(--nim-text)',
+ minWidth: 22,
+ textAlign: 'center',
+ };
+
+ return (
+
+
+
+ {/* Row controls */}
+
+
{rows}
+
+
+
+
+ {/* Column controls */}
+
+
{cols}
+
+
+
+
+ {/* Alignment */}
+
+
+
+
+ );
+}
diff --git a/web/src/lib/editor/plugins/table-row-size-guard.ts b/web/src/lib/editor/plugins/table-row-size-guard.ts
new file mode 100644
index 0000000000..ac9f2b0349
--- /dev/null
+++ b/web/src/lib/editor/plugins/table-row-size-guard.ts
@@ -0,0 +1,46 @@
+/**
+ * Pure guard for table-row size. Kept in its own file so unit tests can
+ * import the threshold and the check without pulling in TableTransformer's
+ * markdown-import dependency chain (which transitively imports
+ * `@lexical/extension` and other modules that fail to resolve under the
+ * current `@lexical/*` version pinning - a pre-existing condition flagged
+ * by `npx tsc --noEmit` and unrelated to this fix).
+ *
+ * Background (issue #321): @aawbeck reported a crash-on-load loop. The
+ * TableTransformer's `$createTableCell` path recursively calls
+ * `$convertFromEnhancedMarkdownString` for each cell on the main thread,
+ * inside a single Lexical `editor.update()` transaction. With many large
+ * cells the cumulative synchronous Lexical node allocation exhausts the
+ * V8 heap and trips a TurboFan integrity-level assertion (SIGTRAP).
+ * The next launch re-opens the same file (the `restoreTabs: true`
+ * preference) and the loop repeats until the user manually deletes the
+ * file.
+ *
+ * Concrete repro shape: 83 rows of 5,451 chars each (5 cells of ~1,090
+ * chars each). The guard below sits above legitimate wide-table sizes
+ * (a 10-column table with 400-char cells is ~4,400 chars including
+ * pipes and spaces) and below the repro, so it fires on pathological
+ * input without affecting normal use. Oversized rows fall through to
+ * plain markdown text rendering rather than table parsing - no crash,
+ * file still opens, content visible as piped text.
+ *
+ * Limitation: the guard is per-row only. A table built from many
+ * narrow-but-numerous rows can still exhaust heap in aggregate. The
+ * residual class is not addressed here; the right path for that is
+ * either raising the per-row threshold (no help against narrow-row
+ * pile-ups) or moving the recursive cell parse off the main thread.
+ * Both are out of scope for this fix; this PR closes the reported
+ * crash and unblocks the file-open path.
+ */
+
+export const MAX_TABLE_ROW_BYTES = 5_000;
+
+/**
+ * Returns true when the given table-row text content exceeds the
+ * per-row size threshold and should be skipped by the table
+ * transformer. The caller falls back to rendering the row as plain
+ * markdown text.
+ */
+export function isTableRowOversized(textContent: string): boolean {
+ return textContent.length > MAX_TABLE_ROW_BYTES;
+}
diff --git a/web/src/lib/editor/plugins/table-transformer.ts b/web/src/lib/editor/plugins/table-transformer.ts
new file mode 100644
index 0000000000..44bed60dce
--- /dev/null
+++ b/web/src/lib/editor/plugins/table-transformer.ts
@@ -0,0 +1,228 @@
+/**
+ * Table transformer for markdown import/export
+ */
+
+/* eslint-disable @typescript-eslint/no-unused-vars, no-console, @typescript-eslint/no-use-before-define */
+
+import { ElementTransformer } from '@lexical/markdown';
+import {
+ $createTableCellNode,
+ $createTableNode,
+ $createTableRowNode,
+ $isTableCellNode,
+ $isTableNode,
+ $isTableRowNode,
+ TableCellHeaderStates,
+ TableCellNode,
+ TableNode,
+ TableRowNode,
+} from '@lexical/table';
+import { $isParagraphNode, $isTextNode, LexicalNode } from 'lexical';
+import {
+ $convertFromEnhancedMarkdownString,
+ $convertNodeToEnhancedMarkdownString,
+} from '../markdown';
+import { isTableRowOversized } from './table-row-size-guard';
+
+// Import transformers from editor
+import { getEditorTransformers } from '../markdown';
+
+// Get the complete set of transformers including core and plugin transformers
+const getTransformers = () => {
+ return getEditorTransformers();
+};
+
+// Very primitive table setup
+const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
+const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;
+
+export const TABLE_TRANSFORMER: ElementTransformer = {
+ dependencies: [TableNode, TableRowNode, TableCellNode],
+ export: (node: LexicalNode, _exportChildren) => {
+ if (!$isTableNode(node)) {
+ return null;
+ }
+
+ const output: string[] = [];
+ const rows = node.getChildren();
+
+ for (const row of rows) {
+ const rowOutput = [];
+ if (!$isTableRowNode(row)) {
+ continue;
+ }
+
+ let isHeaderRow = false;
+ for (const cell of row.getChildren()) {
+ // It's TableCellNode so it's just to make flow happy
+ if ($isTableCellNode(cell)) {
+ // Use $convertNodeToEnhancedMarkdownString for single nodes
+ rowOutput.push(
+ $convertNodeToEnhancedMarkdownString(getTransformers(), cell)
+ .replace(/\n/g, '\\n')
+ .trim(),
+ );
+ if (cell.__headerState === TableCellHeaderStates.ROW) {
+ isHeaderRow = true;
+ }
+ }
+ }
+
+ output.push(`| ${rowOutput.join(' | ')} |`);
+ if (isHeaderRow) {
+ output.push(`| ${rowOutput.map((_) => '---').join(' | ')} |`);
+ }
+ }
+
+ return output.join('\n');
+ },
+ regExp: TABLE_ROW_REG_EXP,
+ replace: (parentNode, _1, match) => {
+ console.log('[TableTransformer] replace called, match:', match[0]);
+ // Header row
+ if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
+ const table = parentNode.getPreviousSibling();
+ if (!table || !$isTableNode(table)) {
+ return;
+ }
+
+ const rows = table.getChildren();
+ const lastRow = rows[rows.length - 1];
+ if (!lastRow || !$isTableRowNode(lastRow)) {
+ return;
+ }
+
+ // Add header state to row cells
+ lastRow.getChildren().forEach((cell) => {
+ if (!$isTableCellNode(cell)) {
+ return;
+ }
+ cell.setHeaderStyles(
+ TableCellHeaderStates.ROW,
+ TableCellHeaderStates.ROW,
+ );
+ });
+
+ // Remove line
+ parentNode.remove();
+ return;
+ }
+
+ const matchCells = mapToTableCells(match[0]);
+
+ if (matchCells == null) {
+ return;
+ }
+
+ const rows = [matchCells];
+ let sibling = parentNode.getPreviousSibling();
+ let maxCells = matchCells.length;
+
+ while (sibling) {
+ if (!$isParagraphNode(sibling)) {
+ break;
+ }
+
+ if (sibling.getChildrenSize() !== 1) {
+ break;
+ }
+
+ const firstChild = sibling.getFirstChild();
+
+ if (!$isTextNode(firstChild)) {
+ break;
+ }
+
+ const cells = mapToTableCells(firstChild.getTextContent());
+
+ if (cells == null) {
+ break;
+ }
+
+ maxCells = Math.max(maxCells, cells.length);
+ rows.unshift(cells);
+ const previousSibling = sibling.getPreviousSibling();
+ sibling.remove();
+ sibling = previousSibling;
+ }
+
+ const table = $createTableNode();
+
+ for (const cells of rows) {
+ const tableRow = $createTableRowNode();
+ table.append(tableRow);
+
+ for (let i = 0; i < maxCells; i++) {
+ tableRow.append(i < cells.length ? cells[i] : $createTableCell(''));
+ }
+ }
+
+ const previousSibling = parentNode.getPreviousSibling();
+ if (
+ $isTableNode(previousSibling) &&
+ getTableColumnsSize(previousSibling) === maxCells
+ ) {
+ previousSibling.append(...table.getChildren());
+ parentNode.remove();
+ } else {
+ try {
+ parentNode.replace(table);
+ } catch (e) {
+ console.error('Error replacing node with table:', match, e);
+ throw e;
+ }
+ }
+ },
+ type: 'element',
+};
+
+function getTableColumnsSize(table: TableNode) {
+ const row = table.getFirstChild();
+ return $isTableRowNode(row) ? row.getChildrenSize() : 0;
+}
+
+const $createTableCell = (textContent: string): TableCellNode => {
+ console.log(
+ '[TableTransformer] $createTableCell with:',
+ JSON.stringify(textContent),
+ );
+ textContent = textContent.replace(/\\n/g, '\n');
+ const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
+ $convertFromEnhancedMarkdownString(
+ textContent,
+ getTransformers(),
+ cell,
+ true,
+ );
+ console.log(
+ '[TableTransformer] cell created, children:',
+ cell.getChildrenSize(),
+ );
+ return cell;
+};
+
+const mapToTableCells = (textContent: string): Array | null => {
+ if (isTableRowOversized(textContent)) {
+ console.log('[TableTransformer] row oversized:', textContent.length);
+ return null;
+ }
+ const match = textContent.match(TABLE_ROW_REG_EXP);
+ if (!match || !match[1]) {
+ console.log('[TableTransformer] no regex match for:', textContent);
+ return null;
+ }
+ console.log('[TableTransformer] matched row:', match[1]);
+ const cells = match[1].split('|').map((text) => {
+ // Remove exactly one space from start and end if present
+ const trimmed = text.replace(/^ | $/g, '');
+ console.log(
+ '[TableTransformer] cell text:',
+ JSON.stringify(text),
+ '->',
+ JSON.stringify(trimmed),
+ );
+ return $createTableCell(trimmed);
+ });
+ console.log('[TableTransformer] created', cells.length, 'cells');
+ return cells;
+};
diff --git a/web/src/lib/editor/plugins/toolbar-plugin.tsx b/web/src/lib/editor/plugins/toolbar-plugin.tsx
new file mode 100644
index 0000000000..e96ac43699
--- /dev/null
+++ b/web/src/lib/editor/plugins/toolbar-plugin.tsx
@@ -0,0 +1,183 @@
+/**
+ * ToolbarPlugin — editor toolbar with Toggle Source and Table of Contents.
+ */
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { $isHeadingNode, type HeadingTagType } from '@lexical/rich-text';
+import {
+ $getNodeByKey,
+ $getRoot,
+ $isElementNode,
+ type LexicalNode,
+} from 'lexical';
+import { Code } from 'lucide-react';
+import { useCallback, useEffect, useState } from 'react';
+
+interface TOCItem {
+ text: string;
+ tag: HeadingTagType;
+ key: string;
+}
+
+interface Props {
+ onToggleSource: () => void;
+ showSource: boolean;
+}
+
+export default function ToolbarPlugin({ onToggleSource, showSource }: Props) {
+ const [editor] = useLexicalComposerContext();
+ const [showToc, setShowToc] = useState(false);
+ const [tocItems, setTocItems] = useState([]);
+
+ // Scan headings for TOC
+ const scanHeadings = useCallback(() => {
+ editor.read(() => {
+ const items: TOCItem[] = [];
+ const visit = (node: LexicalNode) => {
+ if ($isHeadingNode(node)) {
+ items.push({
+ text: node.getTextContent(),
+ tag: node.getTag(),
+ key: node.__key,
+ });
+ }
+ if ($isElementNode(node)) {
+ for (const child of node.getChildren()) {
+ visit(child);
+ }
+ }
+ };
+ visit($getRoot());
+ setTocItems(items);
+ });
+ }, [editor]);
+
+ useEffect(() => {
+ if (showToc) scanHeadings();
+ }, [showToc, scanHeadings]);
+
+ const navigateToHeading = (key: string) => {
+ editor.update(() => {
+ const node = $getNodeByKey(key);
+ if (node && 'selectStart' in node) {
+ (node as any).selectStart();
+ (node as any).scrollIntoView?.();
+ }
+ });
+ setShowToc(false);
+ };
+
+ const tagToPadding: Record = {
+ h1: '0',
+ h2: '12px',
+ h3: '24px',
+ h4: '36px',
+ h5: '48px',
+ h6: '60px',
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {/* TOC Dropdown */}
+ {showToc && tocItems.length > 0 && (
+
+
+ Table of Contents
+
+ {tocItems.map((item, i) => (
+
navigateToHeading(item.key)}
+ style={{
+ padding: `6px 12px 6px calc(12px + ${tagToPadding[item.tag] || '0px'})`,
+ fontSize: 13,
+ cursor: 'pointer',
+ color: 'var(--nim-text)',
+ borderLeft: `3px solid ${item.tag === 'h1' ? 'var(--nim-primary)' : 'transparent'}`,
+ }}
+ onMouseEnter={(e) =>
+ (e.currentTarget.style.background = 'var(--nim-bg-hover)')
+ }
+ onMouseLeave={(e) =>
+ (e.currentTarget.style.background = 'transparent')
+ }
+ >
+ {item.text || `(heading ${i + 1})`}
+
+ ))}
+
+ )}
+ >
+ );
+}
diff --git a/web/src/lib/editor/raw-markdown-editor.tsx b/web/src/lib/editor/raw-markdown-editor.tsx
new file mode 100644
index 0000000000..e6f7474bf2
--- /dev/null
+++ b/web/src/lib/editor/raw-markdown-editor.tsx
@@ -0,0 +1,148 @@
+/**
+ * RawMarkdownEditor — raw markdown editing using Monaco Editor.
+ * Theme follows the app's Nimbalyst theme system.
+ */
+
+import type { OnChange, OnMount } from '@monaco-editor/react';
+import Editor from '@monaco-editor/react';
+import { Eye } from 'lucide-react';
+import type { editor as monacoEditor } from 'monaco-editor';
+import type { JSX } from 'react';
+import { useEffect, useRef } from 'react';
+
+import { useTheme } from '@/components/theme-provider';
+
+interface Props {
+ content: string;
+ onChange?: (value: string) => void;
+ readOnly?: boolean;
+ onToggleSource?: () => void;
+ language?: string;
+}
+
+function defineTheme(monaco: typeof import('monaco-editor'), isDark: boolean) {
+ const name = isDark ? 'llmwiki-dark' : 'llmwiki-light';
+ monaco.editor.defineTheme(name, {
+ base: isDark ? 'vs-dark' : 'vs',
+ inherit: true,
+ rules: [],
+ colors: isDark
+ ? {
+ 'editor.background': '#2d2d2d',
+ 'editor.foreground': '#ffffff',
+ 'editor.lineHighlightBackground': '#3a3a3a',
+ 'editor.selectionBackground': 'rgba(96,165,250,0.2)',
+ 'editorCursor.foreground': '#60a5fa',
+ 'editorLineNumber.foreground': '#808080',
+ 'editorLineNumber.activeForeground': '#b3b3b3',
+ 'editor.selectionHighlightBackground': 'rgba(96,165,250,0.1)',
+ }
+ : {
+ 'editor.background': '#ffffff',
+ 'editor.foreground': '#111827',
+ 'editor.lineHighlightBackground': '#f3f4f6',
+ 'editor.selectionBackground': 'rgba(59,130,246,0.2)',
+ 'editorCursor.foreground': '#3b82f6',
+ 'editorLineNumber.foreground': '#9ca3af',
+ 'editorLineNumber.activeForeground': '#6b7280',
+ 'editor.selectionHighlightBackground': 'rgba(59,130,246,0.1)',
+ },
+ });
+ return name;
+}
+
+export default function RawMarkdownEditor({
+ content,
+ onChange,
+ readOnly = false,
+ onToggleSource,
+ language = 'markdown',
+}: Props & { showSource?: boolean }): JSX.Element {
+ const { theme } = useTheme();
+ const editorRef = useRef(null);
+ const monacoRef = useRef(null);
+ const themeApplied = useRef('');
+
+ const handleMount: OnMount = (editor, monaco) => {
+ editorRef.current = editor;
+ monacoRef.current = monaco;
+ const isDark = theme === 'dark';
+ const name = defineTheme(monaco, isDark);
+ monaco.editor.setTheme(name);
+ themeApplied.current = name;
+ };
+
+ // Switch Monaco theme when app theme changes
+ useEffect(() => {
+ if (!monacoRef.current) return;
+ const isDark = theme === 'dark';
+ const expected = isDark ? 'llmwiki-dark' : 'llmwiki-light';
+ if (themeApplied.current === expected) return;
+ const name = defineTheme(monacoRef.current, isDark);
+ monacoRef.current.editor.setTheme(name);
+ themeApplied.current = name;
+ }, [theme]);
+
+ const handleChange: OnChange = (value) => {
+ if (onChange && value !== undefined) {
+ onChange(value);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index a61265b63b..5089549781 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -453,8 +453,19 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.',
generateRaptor:
'Performs recursive clustering and summarization of document chunks to build a hierarchical tree structure, enabling more context-aware retrieval across lengthy documents.',
+ generateArtifact:
+ 'Compiles artifact pages (entity / concept / topic wiki) from every document configured with a knowledge compilation template. Only newly added chunks are processed on each run.',
+ generateToSkills:
+ 'Builds a hierarchical skill tree from this dataset and stores the generated skill pages for search and reuse.',
+ noWikiPages: 'No wiki pages yet',
+ clearWikiTitle: 'Clear wiki',
+ clearWikiDescription:
+ 'Are you sure you want to clear all wiki pages in this dataset? This action cannot be undone.',
+ noSkills: 'No skills yet',
generate: 'Generate',
raptor: 'RAPTOR',
+ artifact: 'Artifact',
+ toSkills: 'To skills',
processingType: 'Processing type',
dataPipeline: 'Switch or configure ingestion pipeline.',
dataPipelineTitle: 'Ingestion pipeline',
@@ -489,6 +500,36 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
files: 'files',
configuration: 'Configuration',
knowledgeGraph: 'Knowledge graph',
+ compilation: 'Compilation',
+ export: 'Export',
+ version: 'Version',
+ versionHistory: 'Version history',
+ currentVersion: 'Current version',
+ versionDiff: "What's changed in this version",
+ noDiffAvailable: 'No changes to display',
+ viewingVersion: 'Viewing version',
+ commit: 'Commit',
+ confirmCommit: 'Confirm commit',
+ versionContent: 'Version content',
+ versionContentPlaceholder: 'Enter version content',
+ versionContentRequired: 'Please input version content',
+ graph: 'Graph',
+ graphPlaceholder: 'Graph view placeholder',
+ llmWiki: 'LLM Wiki',
+ skills: 'Skills',
+ contents: 'Contents',
+ topics: 'Topics',
+ concept: 'Concept',
+ entity: 'Entity',
+ createDirectoryFolder: 'Create directory',
+ directoryName: 'Name',
+ directoryNamePlaceholder: 'Instance name',
+ directoryRule: 'Rule',
+ directoryRulePlaceholder: 'Input',
+ entity: 'Entity',
+ selectArtifact: 'Select an item from the contents to view details',
+ sourceDocuments: 'Source documents',
+ compilationTitleSuffix: "' dataset",
name: 'Name',
namePlaceholder: 'Please input name!',
doc: 'Docs',
@@ -880,6 +921,8 @@ The above is the content you need to summarize.`,
randomSeed: 'Random seed',
randomSeedMessage: 'Random seed is required',
entityTypes: 'Entity types',
+ compilationTemplate: 'Compilation template',
+ scopeFile: 'File',
vietnamese: 'Vietnamese',
pageRank: 'Page rank',
pageRankTip: `You can assign a higher PageRank score to specific datasets during retrieval. The corresponding score is added to the hybrid similarity scores of retrieved chunks from these datasets, increasing their ranking. See https://ragflow.io/docs/dev/set_page_rank for details.`,
@@ -958,6 +1001,18 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
questionTip: `If there are given questions, the embedding of the chunk will be based on them.`,
chunkResult: 'Chunk result',
chunkResultTip: `View the chunked segments used for embedding and retrieval.`,
+ representation: 'Representation',
+ representationKinds: {
+ knowledge_graph: 'Knowledge graph',
+ mind_map: 'Mind map',
+ page_index: 'Page index',
+ timeline: 'Timeline',
+ tree: 'Tree',
+ raptor: 'RAPTOR',
+ },
+ representationUnsupported:
+ 'This representation type is not supported yet.',
+ representationEmpty: 'No representation templates available.',
enable: 'Enable',
disable: 'Disable',
delete: 'Delete',
@@ -1525,6 +1580,77 @@ Example: Virtual Hosted Style`,
datasourceDescription: 'Manage your data source and connections',
chatChannels: 'Chat channels',
chatChannelsDescription: 'Manage your chat channel bots and credentials',
+ compilationTemplates: 'Compilation templates',
+ compilationTemplatesDescription: 'Manage your compilation templates',
+ addTemplateGroup: 'Add template group',
+ editTemplateGroup: 'Edit template group',
+ groupName: 'Group name',
+ groupNameRequired: 'Please input group name',
+ groupDescription: 'Group description',
+ templateCount: '{{count}} template(s)',
+ atLeastOneTemplateRequired: 'At least one template is required',
+ template: 'Template',
+ deleteTemplateGroupModalTitle: 'Delete template group',
+ deleteTemplateGroupModalContent:
+ 'Are you sure you want to delete this template group? This action cannot be undone.',
+ addTemplate: 'Add template',
+ noTemplates: 'No compilation templates yet.',
+ deleteTemplateModalTitle: 'Delete template',
+ deleteTemplateModalContent:
+ 'Are you sure you want to delete this template? This action cannot be undone.',
+ editTemplate: 'Edit template',
+ templateName: 'Name',
+ templateNameRequired: 'Please input template name',
+ templateDescription: 'Description',
+ llmForExtraction: 'LLM for extraction',
+ llmForExtractionRequired: 'Please select an LLM model',
+ templateKind: 'Kind',
+ templateKindRequired: 'Please select a kind',
+ entitySpecification: 'Entity specification',
+ relationSpecification: 'Relation specification',
+ conceptSpecification: 'Concept specification',
+ claimSpecification: 'Claim specification',
+ field: 'Field',
+ fieldType: 'Type',
+ fieldTypeRequired: 'Please select a type',
+ fieldDescription: 'Description',
+ fieldDescriptionRequired: 'Please input description',
+ fieldRule: 'Rule',
+ rulePlaceholder: 'Input extraction rules',
+ description: 'Description',
+ descriptionPlaceholder: 'Enter description',
+ addField: 'Add field',
+ example: 'Page-structure example',
+ examplePlaceholder: 'Input example',
+ instruction: 'Instruction',
+ globalRules: 'Global rules',
+ globalRulesPlaceholder: 'Input global compilation rules',
+ raptorTreeSettings: 'RAPTOR tree settings',
+ summarizationPrompt: 'Summarization prompt',
+ maxToken: 'Max token',
+ maxTokenRequired: 'Please input max token',
+ threshold: 'Threshold',
+ rechunkByTreeLeaves: 'Re-chunk by tree leaves',
+ rechunkByTreeLeavesTip:
+ "Merge each leaf cluster's source chunks into a single replacement chunk. Originals are kept but marked unavailable for retrieval. Only one tree template per group may enable this.",
+ jsonPreview: 'JSON preview',
+ processFlow: 'Process flow',
+ processFlowComingSoon: 'Process flow preview coming soon',
+ basicInfo: 'Basic info',
+ basicInfoDescription: 'Template group basic information',
+ templateWizardConfiguration: 'Configuration',
+ templateWizardConfigurationDescription: 'Configure templates',
+ blueprints: 'Blueprints',
+ blueprintsDescription: 'Select required blueprints',
+ templates: 'Templates',
+ addFieldModalTitle: 'Add field',
+ editFieldModalTitle: 'Edit field',
+ selectFieldType: 'Select field type',
+ blueprintsPlaceholder: 'Blueprints placeholder',
+ blueprintsPlaceholderDescription:
+ "Select or customize a specific structure from the Blueprint library on the left to define your Wiki's content framework and visual presentation.",
+ blueprintsPlaceholderSkip:
+ 'If you have no special requirements, feel free to skip this step, and the system will automatically apply the default structure.',
channelEmptyTip:
'No chat channels added yet. Select one below to connect.',
availableChannels: 'Available channels',
@@ -1928,6 +2054,25 @@ Example: Virtual Hosted Style`,
modelMaxTokensMessage: 'Max tokens must be a number',
modelMaxTokensMinMessage: 'Max tokens must be at least 0',
},
+ knowledgeCompilation: {
+ builtinTemplates: 'Built-in template',
+ kind: {
+ knowledge_graph: 'Knowledge graph',
+ artifacts: 'Artifacts',
+ timeline: 'Timeline',
+ page_index: 'Page index',
+ tree: 'Tree',
+ mind_map: 'Mind map',
+ empty: 'Empty',
+ },
+ },
+ datasetSkill: {
+ folders: 'Skills',
+ empty: 'No skills available',
+ selectFolder: 'Select a skill to view details',
+ currentFolder: 'Skill',
+ noContent: 'No content',
+ },
message: {
registered: 'Registered!',
logout: 'logout',
@@ -2801,6 +2946,9 @@ This process aggregates variables from multiple branches into a single variable
extractor: 'Transformer',
extractorDescription:
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
+ compiler: 'Compilation',
+ compilerDescription:
+ 'Compiles document chunks using knowledge compilation templates into structured artifacts.',
outputFormat: 'Output format',
fileFormats: 'File type',
fileFormatOptions: {
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index a7c3c7443f..6a552ff050 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -402,6 +402,17 @@ export default {
subbarFiles: '文件列表',
generate: '生成',
raptor: 'RAPTOR',
+ artifact: 'Artifact',
+ toSkills: '生成技能',
+ generateArtifact:
+ '从配置了知识编译模板的每个文档中编译 Artifact 页面(实体 / 概念 / 主题 wiki)。每次运行仅处理新添加的 chunk。',
+ generateToSkills:
+ '从该数据集构建分层技能树,并存储生成的技能页面以供搜索和复用。',
+ noWikiPages: '暂无 Wiki 页面',
+ clearWikiTitle: '清空 Wiki',
+ clearWikiDescription:
+ '确定要清空该数据集下的所有 Wiki 页面吗?此操作无法撤销。',
+ noSkills: '暂无技能',
processingType: '处理类型',
dataPipeline: '切换或配置 ingestion pipeline。',
dataPipelineTitle: '数据管道',
@@ -435,6 +446,35 @@ export default {
testing: '检索测试',
configuration: '配置',
knowledgeGraph: '知识图谱',
+ compilation: '汇编',
+ export: '导出',
+ version: '版本',
+ versionHistory: '版本历史',
+ currentVersion: '当前版本',
+ versionDiff: '本版本改了什么',
+ noDiffAvailable: '暂无变更内容',
+ viewingVersion: '正在查看版本',
+ commit: '提交',
+ confirmCommit: '确认提交',
+ versionContent: '版本内容',
+ versionContentPlaceholder: '请输入版本内容',
+ versionContentRequired: '请输入版本内容',
+ graph: '图谱',
+ graphPlaceholder: '图谱视图占位',
+ llmWiki: 'LLM Wiki',
+ skills: '技能',
+ contents: '目录',
+ topics: '主题',
+ concept: '概念',
+ createDirectoryFolder: '创建目录',
+ directoryName: '名称',
+ directoryNamePlaceholder: '实例名称',
+ directoryRule: '规则',
+ directoryRulePlaceholder: '输入',
+ entity: '实体',
+ selectArtifact: '从目录中选择一个条目以查看详情',
+ sourceDocuments: '来源文档',
+ compilationTitleSuffix: '的数据集',
files: '个文件',
name: '名称',
namePlaceholder: '请输入名称',
@@ -795,6 +835,8 @@ export default {
'在 RAPTOR 中,数据块会根据它们的语义相似性进行聚类。阈值设定了数据块被分到同一组所需的最小相似度。阈值越高,每个聚类中的数据块越少;阈值越低,则每个聚类中的数据块越多。',
maxClusterTip: '最多可创建的聚类数。',
entityTypes: '实体类型',
+ compilationTemplate: '编译模板',
+ scopeFile: '文件',
pageRank: '页面排名',
pageRankTip: `知识库检索时,你可以为特定知识库设置较高的 PageRank 分数,该知识库中匹配文本块的混合相似度得分会自动叠加 PageRank 分数,从而提升排序权重。详见 https://ragflow.io/docs/dev/set_page_rank。`,
tagName: '标签',
@@ -859,6 +901,17 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
questionTip: `如果有给定的问题,则块的嵌入将基于它们。`,
chunkResult: '切片结果',
chunkResultTip: `查看用于嵌入和召回的切片段落。`,
+ representation: '表征',
+ representationKinds: {
+ knowledge_graph: '知识图谱',
+ mind_map: '思维导图',
+ page_index: '页面索引',
+ timeline: '时间线',
+ tree: '树',
+ raptor: 'RAPTOR',
+ },
+ representationUnsupported: '暂不支持该表征类型。',
+ representationEmpty: '暂无表征模板。',
enable: '启用',
disable: '禁用',
delete: '删除',
@@ -1227,6 +1280,76 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
datasourceDescription: '管理您的数据源和连接',
chatChannels: '聊天渠道',
chatChannelsDescription: '管理您的聊天渠道机器人及凭证',
+ compilationTemplates: 'Compilation templates',
+ compilationTemplatesDescription: '管理您的 Compilation templates',
+ addTemplateGroup: '添加模板分组',
+ editTemplateGroup: '编辑模板分组',
+ groupName: '分组名称',
+ groupNameRequired: '请输入分组名称',
+ groupDescription: '分组描述',
+ templateCount: '{{count}} 个模板',
+ atLeastOneTemplateRequired: '至少需要一个模板',
+ template: '模板',
+ deleteTemplateGroupModalTitle: '删除模板分组',
+ deleteTemplateGroupModalContent:
+ '确定要删除此模板分组吗?此操作无法撤销。',
+ addTemplate: '添加模板',
+ noTemplates: '暂无 Compilation templates。',
+ deleteTemplateModalTitle: '删除模板',
+ deleteTemplateModalContent: '确定要删除此模板吗?此操作无法撤销。',
+ editTemplate: '编辑模板',
+ templateName: '名称',
+ templateNameRequired: '请输入模板名称',
+ templateDescription: '描述',
+ llmForExtraction: '用于提取的 LLM',
+ llmForExtractionRequired: '请选择 LLM 模型',
+ templateKind: '类型',
+ templateKindRequired: '请选择类型',
+ entitySpecification: 'Entity specification',
+ relationSpecification: 'Relation specification',
+ conceptSpecification: 'Concept specification',
+ claimSpecification: 'Claim specification',
+ field: '字段',
+ fieldType: 'Type',
+ fieldTypeRequired: '请选择 type',
+ fieldDescription: '描述',
+ fieldDescriptionRequired: '请输入描述',
+ fieldRule: '规则',
+ rulePlaceholder: '请输入提取规则',
+ description: '描述',
+ descriptionPlaceholder: '请输入描述',
+ addField: '添加字段',
+ example: '页面结构示例',
+ examplePlaceholder: '请输入示例',
+ instruction: 'Instruction',
+ globalRules: '全局规则',
+ globalRulesPlaceholder: '请输入全局编译规则',
+ raptorTreeSettings: 'RAPTOR 树设置',
+ summarizationPrompt: '摘要提示词',
+ maxToken: '最大 token 数',
+ maxTokenRequired: '请输入最大 token 数',
+ threshold: '阈值',
+ rechunkByTreeLeaves: '按树叶重新分块',
+ rechunkByTreeLeavesTip:
+ '将每个叶簇的源数据块合并为单个替换数据块。原始数据块保留但标记为不可检索。每个分组最多只能有一个树模板启用此功能。',
+ jsonPreview: 'JSON 预览',
+ processFlow: '流程视图',
+ processFlowComingSoon: '流程视图预览即将到来',
+ basicInfo: '基本信息',
+ basicInfoDescription: '模板分组基础信息',
+ templateWizardConfiguration: '配置',
+ templateWizardConfigurationDescription: '管理模板的配置',
+ blueprints: '蓝图',
+ blueprintsDescription: '选择所需要的 blueprints',
+ templates: '模板',
+ addFieldModalTitle: '添加字段',
+ editFieldModalTitle: '编辑字段',
+ selectFieldType: '选择字段类型',
+ blueprintsPlaceholder: '蓝图占位',
+ blueprintsPlaceholderDescription:
+ '从左侧的 Blueprint 库中选择或自定义特定结构,以定义您的 Wiki 内容框架和视觉呈现。',
+ blueprintsPlaceholderSkip:
+ '如果您没有特殊要求,可以跳过此步骤,系统将自动应用默认结构。',
channelEmptyTip: '暂未添加任何聊天渠道,请从下方选择一个进行连接。',
availableChannels: '可用渠道',
availableChannelsDescription: '选择要添加的聊天渠道',
@@ -1589,6 +1712,25 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
modelMaxTokensMessage: '最大 Token 数必须为数字',
modelMaxTokensMinMessage: '最大 Token 数不能小于 0',
},
+ knowledgeCompilation: {
+ builtinTemplates: '内置模板',
+ kind: {
+ knowledge_graph: '知识图谱',
+ artifacts: 'Artifacts',
+ timeline: 'Timeline',
+ page_index: 'Page index',
+ tree: '树',
+ mind_map: '思维导图',
+ empty: '空',
+ },
+ },
+ datasetSkill: {
+ folders: '技能',
+ empty: '暂无技能',
+ selectFolder: '选择一个技能以查看详情',
+ currentFolder: '技能',
+ noContent: '无内容',
+ },
message: {
registered: '注册成功',
logout: '登出成功',
@@ -2405,6 +2547,8 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
extractor: '提取器',
extractorDescription:
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
+ compiler: '汇编',
+ compilerDescription: '使用知识汇编模板将文档块编译为结构化产物。',
outputFormat: '输出格式',
fileFormats: '文件类型',
fileFormatOptions: {
diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx
index ffd8cd076f..951d2bf1d7 100644
--- a/web/src/pages/agent/canvas/index.tsx
+++ b/web/src/pages/agent/canvas/index.tsx
@@ -55,6 +55,7 @@ import { AgentNode } from './node/agent-node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { ChunkerNode } from './node/chunker-node';
+import { CompilationNode } from './node/compilation-node';
import { DataOperationsNode } from './node/data-operations-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExitLoopNode } from './node/exit-loop-node';
@@ -96,6 +97,7 @@ export const nodeTypes: NodeTypes = {
parserNode: ParserNode,
tokenizerNode: TokenizerNode,
chunkerNode: ChunkerNode,
+ compilationNode: CompilationNode,
contextNode: ExtractorNode,
dataOperationsNode: DataOperationsNode,
listOperationsNode: ListOperationsNode,
diff --git a/web/src/pages/agent/canvas/node/compilation-node.tsx b/web/src/pages/agent/canvas/node/compilation-node.tsx
new file mode 100644
index 0000000000..6180ecbccd
--- /dev/null
+++ b/web/src/pages/agent/canvas/node/compilation-node.tsx
@@ -0,0 +1,15 @@
+import { IRagNode } from '@/interfaces/database/agent';
+import { NodeProps } from '@xyflow/react';
+import { get } from 'lodash';
+import { LLMLabelCard } from './card';
+import { RagNode } from './index';
+
+export function CompilationNode({ ...props }: NodeProps) {
+ const { data } = props;
+
+ return (
+
+
+
+ );
+}
diff --git a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
index 29e3e29af5..475240b0d5 100644
--- a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
+++ b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
@@ -173,6 +173,7 @@ export function PipelineAccordionOperators({
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
];
list.push(Operator.Extractor);
+ list.push(Operator.Compilation);
return list;
}, [restrictSingleOperatorOnCanvas]);
@@ -188,6 +189,7 @@ export function PipelineAccordionOperators({
const showChunker = useMemo(() => {
return (
getOperatorTypeFromId(nodeId) !== Operator.Extractor &&
+ getOperatorTypeFromId(nodeId) !== Operator.Compilation &&
chunkerOperators.length > 0
);
}, [chunkerOperators.length, getOperatorTypeFromId, nodeId]);
diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx
index 741ed6a563..5f1e4050bc 100644
--- a/web/src/pages/agent/constant/index.tsx
+++ b/web/src/pages/agent/constant/index.tsx
@@ -736,6 +736,7 @@ export const RestrictedUpstreamMap = {
[Operator.TitleChunker]: [Operator.Begin],
[Operator.Tokenizer]: [Operator.Begin],
[Operator.Extractor]: [Operator.Begin],
+ [Operator.Compilation]: [Operator.Begin],
[Operator.File]: [Operator.Begin],
[Operator.Loop]: [Operator.Begin],
[Operator.LoopStart]: [Operator.Begin],
@@ -786,6 +787,7 @@ export const NodeMap = {
[Operator.TokenChunker]: 'chunkerNode',
[Operator.TitleChunker]: 'chunkerNode',
[Operator.Extractor]: 'contextNode',
+ [Operator.Compilation]: 'compilationNode',
[Operator.DataOperations]: 'dataOperationsNode',
[Operator.ListOperations]: 'listOperationsNode',
[Operator.VariableAssigner]: 'variableAssignerNode',
@@ -832,6 +834,7 @@ export const NoDebugOperatorsList = [
Operator.TokenChunker,
Operator.TitleChunker,
Operator.Extractor,
+ Operator.Compilation,
Operator.Tool,
Operator.Loop,
];
@@ -843,6 +846,7 @@ export const NoCopyOperatorsList = [
Operator.TokenChunker,
Operator.TitleChunker,
Operator.Extractor,
+ Operator.Compilation,
];
export enum NodeHandleId {
diff --git a/web/src/pages/agent/constant/pipeline.tsx b/web/src/pages/agent/constant/pipeline.tsx
index 48c3302646..f13baf214c 100644
--- a/web/src/pages/agent/constant/pipeline.tsx
+++ b/web/src/pages/agent/constant/pipeline.tsx
@@ -1,8 +1,5 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
-import {
- initialLlmBaseValues,
- DataflowOperator as Operator,
-} from '@/constants/agent';
+import { initialLlmBaseValues, Operator } from '@/constants/agent';
import { cloneDeep } from 'lodash';
export enum FileType {
@@ -359,7 +356,15 @@ export const initialExtractorValues = {
},
};
-export const NoDebugOperatorsList = [Operator.Begin];
+export const initialCompilationValues = {
+ ...initialLlmBaseValues,
+ compilation_template_group_ids: [],
+ outputs: {
+ chunks: { type: 'Array
`,
+ );
+ }
+
+ if (node.weight) {
+ parts.push(
+ `Weight: ${node.weight}`,
+ );
+ }
+
+ if (node.description) {
+ parts.push(
+ `${node.description}
`,
+ );
+ }
+
+ return `${parts.join('')}
`;
+ }, []);
+
+ const getLinkLabel = useCallback((link: GLink) => {
+ const sourceId =
+ typeof link.source === 'object' ? link.source.id : link.source;
+ const targetId =
+ typeof link.target === 'object' ? link.target.id : link.target;
+ return `${sourceId} → ${targetId}`;
+ }, []);
+
+ const nodeCanvasObject = useCallback(
+ (node: GNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
+ const size = getNodeSize(node);
+ const label = (node.id as string) || '';
+ const fontSize = Math.max(12 / globalScale, Math.min(size / 2, 40));
+
+ // Dim non-connected nodes when hovering
+ const isDimmed = hoverNode && !connectedNodeIds.has(node.id as string);
+ ctx.globalAlpha = isDimmed ? 0.1 : 1;
+
+ // Node circle (solid fill by entity_type color)
+ const nodeColor = (node as any).color || NodeColorPalette[0];
+ ctx.beginPath();
+ ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI);
+ ctx.fillStyle = nodeColor;
+ ctx.fill();
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)';
+ ctx.lineWidth = Math.max(2 / globalScale, 3);
+ ctx.stroke();
+
+ // Only show label on hover
+ if (hoverNode?.id === node.id) {
+ ctx.font = `${fontSize}px Sans-Serif`;
+ const textWidth = ctx.measureText(label).width;
+ const padding = fontSize * 0.4;
+ const bckgDimensions: [number, number] = [
+ textWidth + padding * 2,
+ fontSize + padding * 2,
+ ];
+
+ // Label background
+ ctx.fillStyle = isDark
+ ? 'rgba(0, 0, 0, 0.1)'
+ : 'rgba(255, 255, 255, 0.1)';
+ ctx.fillRect(
+ node.x! - bckgDimensions[0] / 2,
+ node.y! + size + fontSize / 2,
+ bckgDimensions[0],
+ bckgDimensions[1],
+ );
+
+ // Label text
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = isDark ? '#fff' : '#000';
+ ctx.fillText(label, node.x!, node.y! + size + fontSize);
+
+ (node as any).__bckgDimensions = bckgDimensions;
+ }
+
+ ctx.globalAlpha = 1;
+ },
+ [isDark, hoverNode, connectedNodeIds],
+ );
+
+ const nodePointerAreaPaint = useCallback(
+ (node: GNode, color: string, ctx: CanvasRenderingContext2D) => {
+ const size = getNodeSize(node);
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI);
+ ctx.fill();
+ },
+ [],
+ );
+
+ const linkCanvasObject = useCallback(
+ (link: GLink, ctx: CanvasRenderingContext2D) => {
+ const source =
+ typeof link.source === 'object'
+ ? (link.source as GNode)
+ : (graphData.nodes.find((n: GNode) => n.id === link.source) as GNode);
+ const target =
+ typeof link.target === 'object'
+ ? (link.target as GNode)
+ : (graphData.nodes.find((n: GNode) => n.id === link.target) as GNode);
+
+ if (!source || !target) return;
+
+ const isDimmed =
+ hoverNode &&
+ !(
+ connectedNodeIds.has(source.id as string) &&
+ connectedNodeIds.has(target.id as string)
+ );
+ ctx.globalAlpha = isDimmed ? 0.1 : 1;
+
+ const lineWidth = getLinkWidth(link);
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)';
+
+ ctx.beginPath();
+ ctx.moveTo(source.x!, source.y!);
+ ctx.lineTo(target.x!, target.y!);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ },
+ [graphData.nodes, hoverNode, connectedNodeIds, isDark],
+ );
+
+ const handleNodeHover = useCallback((node: GNode | null) => {
+ setHoverNode(node);
+ }, []);
+
+ const backgroundColor = isDark ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)';
+
+ return (
+
+ {dimensions.width > 0 && dimensions.height > 0 && (
+
+ isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
+ }
+ onNodeHover={handleNodeHover}
+ enableNodeDrag
+ enableZoomInteraction
+ enablePanInteraction
+ warmupTicks={50}
+ cooldownTicks={100}
+ />
+ )}
+
+ );
+}
+
+export default KnowledgeForceGraph;
diff --git a/web/src/pages/dataset/compilation/skills-left-panel.tsx b/web/src/pages/dataset/compilation/skills-left-panel.tsx
new file mode 100644
index 0000000000..3e1ce489f1
--- /dev/null
+++ b/web/src/pages/dataset/compilation/skills-left-panel.tsx
@@ -0,0 +1,127 @@
+import { SearchInput } from '@/components/ui/input';
+import { Spin } from '@/components/ui/spin';
+import { useFetchDatasetSkillTree } from '@/hooks/use-dataset-skill-request';
+import { DatasetSkillTreeNode } from '@/interfaces/database/dataset-skill';
+import { cn } from '@/lib/utils';
+import { useDebounce } from 'ahooks';
+import { useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type FlatSkillNode = {
+ skill_kwd: string;
+ depth: number;
+};
+
+type SkillsLeftPanelProps = {
+ selectedSkill: string | null;
+ onSelectSkill: (skillKwd: string) => void;
+};
+
+function flattenSkillTree(nodes: DatasetSkillTreeNode[] = []): FlatSkillNode[] {
+ const result: FlatSkillNode[] = [];
+
+ function walk(items: DatasetSkillTreeNode[], depth: number) {
+ for (const item of items) {
+ result.push({ skill_kwd: item.skill_kwd, depth });
+ if (item.children_kwd?.length) {
+ walk(item.children_kwd, depth + 1);
+ }
+ }
+ }
+
+ walk(nodes, 0);
+ return result;
+}
+
+type SkillListItemProps = {
+ item: FlatSkillNode;
+ isSelected: boolean;
+ onSelect: (skillKwd: string) => void;
+};
+
+function SkillListItem({ item, isSelected, onSelect }: SkillListItemProps) {
+ const handleClick = useCallback(() => {
+ onSelect(item.skill_kwd);
+ }, [item.skill_kwd, onSelect]);
+
+ return (
+
+ {item.skill_kwd}
+
+ );
+}
+
+export function SkillsLeftPanel({
+ selectedSkill,
+ onSelectSkill,
+}: SkillsLeftPanelProps) {
+ const { t } = useTranslation();
+ const { data: tree, loading } = useFetchDatasetSkillTree();
+ const [searchString, setSearchString] = useState('');
+ const debouncedSearchString = useDebounce(searchString, { wait: 500 });
+
+ const allNodes = useMemo(
+ () => flattenSkillTree(tree?.skill_with_weight),
+ [tree?.skill_with_weight],
+ );
+
+ const filteredNodes = useMemo(() => {
+ const keyword = debouncedSearchString.trim().toLowerCase();
+ if (!keyword) return allNodes;
+ return allNodes.filter((node) =>
+ node.skill_kwd.toLowerCase().includes(keyword),
+ );
+ }, [allNodes, debouncedSearchString]);
+
+ const handleSearchChange = useCallback(
+ (e: React.ChangeEvent) => {
+ setSearchString(e.target.value);
+ },
+ [],
+ );
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/utils/parse-wiki-diff.ts b/web/src/pages/dataset/compilation/utils/parse-wiki-diff.ts
new file mode 100644
index 0000000000..42646c498b
--- /dev/null
+++ b/web/src/pages/dataset/compilation/utils/parse-wiki-diff.ts
@@ -0,0 +1,53 @@
+import type {
+ WikiDiffHunk,
+ WikiDiffLine,
+ WikiDiffLineType,
+} from '../interface';
+
+export function parseWikiDiff(diff: string): WikiDiffHunk[] {
+ const rawLines = (diff || '').split('\n');
+ const hunks: WikiDiffHunk[] = [];
+ let currentHunk: WikiDiffHunk | null = null;
+
+ for (const rawLine of rawLines) {
+ if (rawLine.startsWith('@@')) {
+ if (currentHunk) {
+ hunks.push(currentHunk);
+ }
+ currentHunk = { header: rawLine, lines: [] };
+ continue;
+ }
+
+ if (rawLine.startsWith('---') || rawLine.startsWith('+++')) {
+ // File header — not rendered per-line.
+ continue;
+ }
+
+ let type: WikiDiffLineType = 'context';
+ let content = rawLine;
+
+ if (rawLine.startsWith('+')) {
+ type = 'added';
+ content = rawLine.slice(1);
+ } else if (rawLine.startsWith('-')) {
+ type = 'removed';
+ content = rawLine.slice(1);
+ }
+
+ if (currentHunk) {
+ currentHunk.lines.push({ type, content });
+ }
+ }
+
+ if (currentHunk) {
+ hunks.push(currentHunk);
+ }
+
+ return hunks;
+}
+
+export function getWikiDiffChanges(hunks: WikiDiffHunk[]): WikiDiffLine[] {
+ return hunks
+ .flatMap((hunk) => hunk.lines)
+ .filter((line) => line.type === 'added' || line.type === 'removed');
+}
diff --git a/web/src/pages/dataset/compilation/version-history-item.tsx b/web/src/pages/dataset/compilation/version-history-item.tsx
new file mode 100644
index 0000000000..a20ffe3378
--- /dev/null
+++ b/web/src/pages/dataset/compilation/version-history-item.tsx
@@ -0,0 +1,51 @@
+import { IWikiCommit } from '@/interfaces/database/dataset';
+import { cn } from '@/lib/utils';
+import { formatDate } from '@/utils/date';
+
+type VersionHistoryItemProps = {
+ version: IWikiCommit;
+ isSelected: boolean;
+ onSelect: (version: IWikiCommit) => void;
+};
+
+export function VersionHistoryItem({
+ version,
+ isSelected,
+ onSelect,
+}: VersionHistoryItemProps) {
+ const handleClick = () => {
+ onSelect(version);
+ };
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/version-history-sheet.tsx b/web/src/pages/dataset/compilation/version-history-sheet.tsx
new file mode 100644
index 0000000000..1e8e15b32c
--- /dev/null
+++ b/web/src/pages/dataset/compilation/version-history-sheet.tsx
@@ -0,0 +1,86 @@
+import { Button } from '@/components/ui/button';
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@/components/ui/sheet';
+import { Spin } from '@/components/ui/spin';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { useFetchWikiCommits } from '@/hooks/use-knowledge-request';
+import { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+import { ClipboardClock } from 'lucide-react';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { VersionHistoryItem } from './version-history-item';
+
+type VersionHistorySheetProps = {
+ selectedArtifact: IArtifact | null;
+ selectedVersion: IWikiCommit | null;
+ onSelectVersion: (version: IWikiCommit | null) => void;
+};
+
+export function VersionHistorySheet({
+ selectedArtifact,
+ selectedVersion,
+ onSelectVersion,
+}: VersionHistorySheetProps) {
+ const { t } = useTranslation();
+ const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false);
+ const { commits, loading } = useFetchWikiCommits(
+ selectedArtifact,
+ isVersionHistoryOpen,
+ );
+
+ const handleSelect = (version: IWikiCommit) => {
+ onSelectVersion(version);
+ setIsVersionHistoryOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+ {t('knowledgeDetails.version')}
+
+
+
+ {t('knowledgeDetails.versionHistory')}
+
+
+ {loading && commits.length === 0 && (
+
+
+
+ )}
+ {commits.map((version) => {
+ const isSelected = selectedVersion?.id === version.id;
+ return (
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-commit-modal.tsx b/web/src/pages/dataset/compilation/wiki-commit-modal.tsx
new file mode 100644
index 0000000000..c09e35cf17
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-commit-modal.tsx
@@ -0,0 +1,56 @@
+import { RAGFlowFormItem } from '@/components/ragflow-form';
+import { Form } from '@/components/ui/form';
+import { Modal } from '@/components/ui/modal/modal';
+import { Textarea } from '@/components/ui/textarea';
+import { UseFormReturn } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { CommitFormValues } from './interface';
+
+type WikiCommitModalProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ form: UseFormReturn;
+ onConfirm: (values: CommitFormValues) => void;
+ loading?: boolean;
+};
+
+export function WikiCommitModal({
+ open,
+ onOpenChange,
+ form,
+ onConfirm,
+ loading,
+}: WikiCommitModalProps) {
+ const { t } = useTranslation();
+
+ return (
+ form.handleSubmit(onConfirm)()}
+ confirmLoading={loading}
+ okText={t('common.confirm')}
+ cancelText={t('common.cancel')}
+ >
+
+
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-detail-content.tsx b/web/src/pages/dataset/compilation/wiki-detail-content.tsx
new file mode 100644
index 0000000000..0165528d84
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-detail-content.tsx
@@ -0,0 +1,238 @@
+import Empty from '@/components/empty/empty';
+import MarkdownEditor from '@/components/markdown-editor';
+import { ReferenceDocumentList } from '@/components/next-message-item/reference-document-list';
+import { Button } from '@/components/ui/button';
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from '@/components/ui/resizable';
+import { Spin } from '@/components/ui/spin';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { useFetchDocumentsByIds } from '@/hooks/use-document-request';
+import {
+ useFetchArtifactPage,
+ useFetchWikiCommit,
+} from '@/hooks/use-knowledge-request';
+import { Docagg } from '@/interfaces/database/chat';
+import { IArtifact, IWikiCommit } from '@/interfaces/database/dataset';
+import { downloadMarkdownFile } from '@/utils/file-util';
+import { Download } from 'lucide-react';
+import { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useCommitArtifact } from './hooks/use-commit-artifact';
+import { useWikiEditor } from './hooks/use-wiki-editor';
+import { VersionHistorySheet } from './version-history-sheet';
+import { WikiCommitModal } from './wiki-commit-modal';
+import { WikiVersionDiffPanel } from './wiki-version-diff-panel';
+
+type WikiDetailContentProps = {
+ selectedArtifact: IArtifact | null;
+ selectedVersion: IWikiCommit | null;
+ onSelectVersion: (version: IWikiCommit | null) => void;
+};
+
+export function WikiDetailContent({
+ selectedArtifact,
+ selectedVersion,
+ onSelectVersion,
+}: WikiDetailContentProps) {
+ const { t } = useTranslation();
+ const isVersionView = !!selectedVersion;
+
+ const { data: pageData, loading: pageLoading } = useFetchArtifactPage(
+ isVersionView ? null : selectedArtifact,
+ );
+ const { data: commitDetail, loading: commitLoading } = useFetchWikiCommit(
+ selectedVersion?.id ?? null,
+ );
+
+ const title = pageData?.title ?? selectedArtifact?.title;
+
+ const pageType = isVersionView
+ ? selectedArtifact?.page_type
+ : pageData?.page_type;
+ const slug = isVersionView ? selectedArtifact?.slug : pageData?.slug;
+ const content = isVersionView
+ ? (commitDetail?.content_after ?? '')
+ : (pageData?.content_md_rendered ?? '');
+ const sourceDocIds = isVersionView ? [] : (pageData?.source_doc_ids ?? []);
+
+ const editorKey = isVersionView
+ ? `${selectedArtifact?.slug}@${selectedVersion?.id}`
+ : selectedArtifact?.slug;
+
+ const {
+ editedContent,
+ isDirty,
+ handleContentChange,
+ handleCancelEdit,
+ handleMarkAsSaved,
+ } = useWikiEditor({
+ content,
+ artifactSlug: editorKey,
+ });
+
+ const handleCommitSuccess = useCallback(() => {
+ handleMarkAsSaved();
+ if (isVersionView) {
+ onSelectVersion(null);
+ }
+ }, [handleMarkAsSaved, isVersionView, onSelectVersion]);
+
+ const { isOpen, open, close, form, handleConfirm, isUpdating } =
+ useCommitArtifact({
+ editedContent,
+ pageType: pageType ?? '',
+ slug: slug ?? '',
+ onSuccess: handleCommitSuccess,
+ });
+
+ const { documents } = useFetchDocumentsByIds(sourceDocIds);
+
+ const referenceDocuments = useMemo(() => {
+ return documents.map(
+ (doc): Docagg => ({
+ doc_id: doc.id,
+ doc_name: doc.name,
+ count: 0,
+ }),
+ );
+ }, [documents]);
+
+ const handleExport = useCallback(() => {
+ const filename = `${title ?? 'document'}.md`;
+ downloadMarkdownFile(editedContent, filename);
+ }, [title, editedContent]);
+
+ const renderToolbarButtons = () => {
+ if (isDirty) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {t('knowledgeDetails.export')}
+
+
+
+ );
+ };
+
+ const loading = isVersionView ? commitLoading : pageLoading;
+
+ return (
+
+ {selectedArtifact ? (
+ <>
+
+
+
+
+
+
+ {loading && !content ? (
+
+
+
+ ) : (
+
+ )}
+
+ {referenceDocuments.length > 0 && (
+
+
+ {t('knowledgeDetails.sourceDocuments')}
+
+
+
+ )}
+
+
+
+ {isVersionView && commitDetail && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+ >
+ ) : (
+
+
+
+ )}
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-clear.ts b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-clear.ts
new file mode 100644
index 0000000000..171ca27832
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-clear.ts
@@ -0,0 +1,21 @@
+import { useClearWiki } from '@/hooks/use-knowledge-request';
+import { useCallback, useState } from 'react';
+
+type UseWikiClearOptions = {
+ onClearWiki?: () => void;
+};
+
+export function useWikiClear({ onClearWiki }: UseWikiClearOptions = {}) {
+ const { clearWiki, loading } = useClearWiki();
+ const [open, setOpen] = useState(false);
+
+ const handleConfirm = useCallback(async () => {
+ setOpen(false);
+ const data = await clearWiki();
+ if (data?.code === 0) {
+ onClearWiki?.();
+ }
+ }, [clearWiki, onClearWiki]);
+
+ return { open, setOpen, handleConfirm, loading };
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-navigation.ts b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-navigation.ts
new file mode 100644
index 0000000000..c1696ad136
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-navigation.ts
@@ -0,0 +1,118 @@
+import {
+ useFetchArtifactList,
+ useFetchArtifactTopicList,
+} from '@/hooks/use-knowledge-request';
+import { IArtifactTopic } from '@/interfaces/database/dataset';
+import { useDebounce } from 'ahooks';
+import { useCallback, useMemo, useRef, useState } from 'react';
+
+export type WikiPageType = 'concept' | 'entity';
+
+export function useWikiNavigation() {
+ const scrollRef = useRef(null);
+ const [searchString, setSearchString] = useState('');
+ const debouncedSearchString = useDebounce(searchString, { wait: 500 });
+ const [selectedTopic, setSelectedTopic] = useState(
+ null,
+ );
+ const [pageType, setPageType] = useState('entity');
+
+ const {
+ topics,
+ loading: topicLoading,
+ handleScroll: handleTopicScroll,
+ hasMore: topicHasMore,
+ } = useFetchArtifactTopicList({
+ keywords: debouncedSearchString,
+ });
+
+ const {
+ artifacts,
+ loading: artifactLoading,
+ handleScroll: handleArtifactScroll,
+ hasMore: artifactHasMore,
+ } = useFetchArtifactList({
+ keywords: debouncedSearchString,
+ topic: selectedTopic?.topic,
+ pageType,
+ enabled: !!selectedTopic,
+ });
+
+ const handleSearchChange = useCallback(
+ (e: React.ChangeEvent) => {
+ setSearchString(e.target.value);
+ },
+ [],
+ );
+
+ const resetScroll = useCallback(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = 0;
+ }
+ }, []);
+
+ const handleSelectTopic = useCallback(
+ (topic: IArtifactTopic) => {
+ setSelectedTopic(topic);
+ resetScroll();
+ },
+ [resetScroll],
+ );
+
+ const handleBackToTopics = useCallback(() => {
+ setSelectedTopic(null);
+ resetScroll();
+ }, [resetScroll]);
+
+ const handlePageTypeChange = useCallback((value: WikiPageType) => {
+ setPageType(value);
+ }, []);
+
+ const handleScroll = useCallback(
+ (e: React.UIEvent) => {
+ if (selectedTopic) {
+ handleArtifactScroll(e);
+ } else {
+ handleTopicScroll(e);
+ }
+ },
+ [selectedTopic, handleArtifactScroll, handleTopicScroll],
+ );
+
+ const loading = selectedTopic ? artifactLoading : topicLoading;
+ const hasMore = selectedTopic ? artifactHasMore : topicHasMore;
+
+ return useMemo(
+ () => ({
+ scrollRef,
+ searchString,
+ debouncedSearchString,
+ selectedTopic,
+ pageType,
+ topics,
+ artifacts,
+ loading,
+ hasMore,
+ handleSearchChange,
+ handleSelectTopic,
+ handleBackToTopics,
+ handlePageTypeChange,
+ handleScroll,
+ }),
+ [
+ searchString,
+ debouncedSearchString,
+ selectedTopic,
+ pageType,
+ topics,
+ artifacts,
+ loading,
+ hasMore,
+ handleSearchChange,
+ handleSelectTopic,
+ handleBackToTopics,
+ handlePageTypeChange,
+ handleScroll,
+ ],
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx
new file mode 100644
index 0000000000..e9cf875520
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx
@@ -0,0 +1,92 @@
+import ArtifactForceGraph from '@/components/artifact-force-graph';
+import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
+import { Button } from '@/components/ui/button';
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useFetchArtifactGraph } from '@/hooks/use-knowledge-request';
+import { IArtifact, IArtifactGraphEntity } from '@/interfaces/database/dataset';
+import { Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import { LeftPanelTab } from '../constants';
+import { useWikiClear } from './hooks/use-wiki-clear';
+import { WikiNavBar } from './wiki-nav-bar';
+
+const mapNodeToValue = (node: IArtifactGraphEntity) => ({
+ slug: node.slug,
+ title: node.name,
+ page_type: node.type,
+});
+
+type WikiLeftPanelProps = {
+ tab: LeftPanelTab;
+ onTabChange: (value: string) => void;
+ selectedArtifact: IArtifact | null;
+ onSelectArtifact: (artifact: IArtifact) => void;
+ onClearWiki?: () => void;
+};
+
+export function WikiLeftPanel({
+ tab,
+ onTabChange,
+ selectedArtifact,
+ onSelectArtifact,
+ onClearWiki,
+}: WikiLeftPanelProps) {
+ const { t } = useTranslation();
+ const { data } = useFetchArtifactGraph(undefined, {
+ enabled: tab === LeftPanelTab.Graph,
+ });
+ const { open, setOpen, handleConfirm, loading } = useWikiClear({
+ onClearWiki,
+ });
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-artifact-list.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-artifact-list.tsx
new file mode 100644
index 0000000000..d5604d39c9
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-artifact-list.tsx
@@ -0,0 +1,48 @@
+import { Spin } from '@/components/ui/spin';
+import { IArtifact } from '@/interfaces/database/dataset';
+import { useTranslation } from 'react-i18next';
+
+import { WikiArtifactListItem } from './wiki-list-item';
+
+type WikiArtifactListProps = {
+ artifacts: IArtifact[];
+ selectedArtifact: IArtifact | null;
+ loading: boolean;
+ hasMore: boolean;
+ onSelectArtifact: (artifact: IArtifact) => void;
+};
+
+export function WikiArtifactList({
+ artifacts,
+ selectedArtifact,
+ loading,
+ hasMore,
+ onSelectArtifact,
+}: WikiArtifactListProps) {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {artifacts.map((item) => (
+
+ ))}
+
+ {loading && (
+
+
+
+ )}
+ {!loading && !hasMore && artifacts.length > 0 && (
+
+ {t('knowledgeList.noMoreData')}
+
+ )}
+ >
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-list-item.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-list-item.tsx
new file mode 100644
index 0000000000..832d93f993
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-list-item.tsx
@@ -0,0 +1,64 @@
+import { IArtifact, IArtifactTopic } from '@/interfaces/database/dataset';
+import { cn } from '@/lib/utils';
+import { FileText, Folder } from 'lucide-react';
+
+type WikiTopicListItemProps = {
+ topic: IArtifactTopic;
+ onSelect: (topic: IArtifactTopic) => void;
+};
+
+export function WikiTopicListItem({ topic, onSelect }: WikiTopicListItemProps) {
+ const handleClick = () => {
+ onSelect(topic);
+ };
+
+ return (
+
+
+ {topic.title}
+
+ );
+}
+
+type WikiArtifactListItemProps = {
+ item: IArtifact;
+ isSelected: boolean;
+ onSelect: (artifact: IArtifact) => void;
+};
+
+export function WikiArtifactListItem({
+ item,
+ isSelected,
+ onSelect,
+}: WikiArtifactListItemProps) {
+ const handleClick = () => {
+ onSelect(item);
+ };
+
+ return (
+
+
+
+ {item.title}
+
+ {item.page_type && (
+
+ {item.page_type}
+
+ )}
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx
new file mode 100644
index 0000000000..a36daf7ded
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx
@@ -0,0 +1,171 @@
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
+} from '@/components/ui/breadcrumb';
+import { Button } from '@/components/ui/button';
+import { SearchInput } from '@/components/ui/input';
+import { IArtifact } from '@/interfaces/database/dataset';
+import { cn } from '@/lib/utils';
+import { Plus } from 'lucide-react';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { CreateDirectoryDialog } from '../create-directory-dialog';
+import { useCreateDirectory } from '../hooks/use-create-directory';
+import { useWikiNavigation, WikiPageType } from './hooks/use-wiki-navigation';
+import { WikiArtifactList } from './wiki-artifact-list';
+import { WikiTopicList } from './wiki-topic-list';
+
+type PageTypeFilterProps = {
+ value: WikiPageType;
+ onChange: (value: WikiPageType) => void;
+};
+
+function PageTypeFilter({ value, onChange }: PageTypeFilterProps) {
+ const { t } = useTranslation();
+
+ const handleConceptClick = useCallback(() => {
+ onChange('concept');
+ }, [onChange]);
+
+ const handleEntityClick = useCallback(() => {
+ onChange('entity');
+ }, [onChange]);
+
+ return (
+
+
+
+
+ );
+}
+
+type WikiNavBarProps = {
+ selectedArtifact: IArtifact | null;
+ onSelectArtifact: (artifact: IArtifact) => void;
+};
+
+export function WikiNavBar({
+ selectedArtifact,
+ onSelectArtifact,
+}: WikiNavBarProps) {
+ const { t } = useTranslation();
+ const {
+ scrollRef,
+ searchString,
+ selectedTopic,
+ pageType,
+ topics,
+ artifacts,
+ loading,
+ hasMore,
+ handleSearchChange,
+ handleSelectTopic,
+ handleBackToTopics,
+ handlePageTypeChange,
+ handleScroll,
+ } = useWikiNavigation();
+ const {
+ open,
+ loading: createLoading,
+ form,
+ showModal: handleShowCreateDialog,
+ hideModal: handleHideCreateDialog,
+ handleOk: handleCreateOk,
+ } = useCreateDirectory();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {t('knowledgeDetails.topics')}
+
+
+ {selectedTopic && (
+ <>
+
+
+
+ {selectedTopic.title}
+
+
+ >
+ )}
+
+
+ {!selectedTopic && (
+
+ )}
+
+
+
+
+ {selectedTopic ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-topic-list.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-topic-list.tsx
new file mode 100644
index 0000000000..fcce86507a
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-topic-list.tsx
@@ -0,0 +1,45 @@
+import { Spin } from '@/components/ui/spin';
+import { IArtifactTopic } from '@/interfaces/database/dataset';
+import { useTranslation } from 'react-i18next';
+
+import { WikiTopicListItem } from './wiki-list-item';
+
+type WikiTopicListProps = {
+ topics: IArtifactTopic[];
+ loading: boolean;
+ hasMore: boolean;
+ onSelectTopic: (topic: IArtifactTopic) => void;
+};
+
+export function WikiTopicList({
+ topics,
+ loading,
+ hasMore,
+ onSelectTopic,
+}: WikiTopicListProps) {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {topics.map((topic) => (
+
+ ))}
+
+ {loading && (
+
+
+
+ )}
+ {!loading && !hasMore && topics.length > 0 && (
+
+ {t('knowledgeList.noMoreData')}
+
+ )}
+ >
+ );
+}
diff --git a/web/src/pages/dataset/compilation/wiki-version-diff-panel.tsx b/web/src/pages/dataset/compilation/wiki-version-diff-panel.tsx
new file mode 100644
index 0000000000..2391434471
--- /dev/null
+++ b/web/src/pages/dataset/compilation/wiki-version-diff-panel.tsx
@@ -0,0 +1,58 @@
+import { cn } from '@/lib/utils';
+import { useTranslation } from 'react-i18next';
+import { WikiDiffLine } from './interface';
+import { getWikiDiffChanges, parseWikiDiff } from './utils/parse-wiki-diff';
+
+type WikiVersionDiffPanelProps = {
+ diff: string;
+ title?: string;
+};
+
+type DiffLineProps = {
+ line: WikiDiffLine;
+};
+
+function DiffLine({ line }: DiffLineProps) {
+ return (
+
+ {line.content.length === 0 ?
: line.content}
+
+ );
+}
+
+export function WikiVersionDiffPanel({
+ diff,
+ title,
+}: WikiVersionDiffPanelProps) {
+ const { t } = useTranslation();
+ const changes = getWikiDiffChanges(parseWikiDiff(diff));
+
+ return (
+
+
+
+ {title || t('knowledgeDetails.versionDiff')}
+
+
+
+ {changes.length === 0 ? (
+
+ {t('knowledgeDetails.noDiffAvailable')}
+
+ ) : (
+
+ {changes.map((line, index) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/dataset/dataset-overview/dataset-common.ts b/web/src/pages/dataset/dataset-overview/dataset-common.ts
index 566416dccd..aecc2225aa 100644
--- a/web/src/pages/dataset/dataset-overview/dataset-common.ts
+++ b/web/src/pages/dataset/dataset-overview/dataset-common.ts
@@ -6,10 +6,14 @@ export enum LogTabs {
export enum ProcessingType {
knowledgeGraph = 'Graph',
raptor = 'RAPTOR',
+ artifact = 'Artifact',
+ skill = 'Skill',
}
export const ProcessingTypeMap = {
[ProcessingType.knowledgeGraph]: 'Knowledge Graph',
[ProcessingType.raptor]: 'RAPTOR',
+ [ProcessingType.artifact]: 'Artifact',
+ [ProcessingType.skill]: 'Skill',
GraphRAG: 'Knowledge Graph',
};
diff --git a/web/src/pages/dataset/dataset-setting/configuration/naive.tsx b/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
index d69319970e..2886fbd40c 100644
--- a/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
+++ b/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
@@ -3,6 +3,7 @@ import {
AutoQuestionsFormField,
} from '@/components/auto-keywords-form-field';
import { ChildrenDelimiterForm } from '@/components/children-delimiter-form';
+import { CompilationTemplateFormField } from '@/components/compilation-template-form-field';
import { DelimiterFormField } from '@/components/delimiter-form-field';
import { ExcelToHtmlFormField } from '@/components/excel-to-html-form-field';
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
@@ -22,6 +23,7 @@ export function NaiveConfiguration() {
return (
+
();
const [sourceData, setSourceData] = useState();
- const [graphRagGenerateData, setGraphRagGenerateData] =
- useState();
- const [raptorGenerateData, setRaptorGenerateData] =
- useState();
useEffect(() => {
if (knowledgeDetails) {
@@ -153,14 +144,6 @@ export default function DatasetSettings() {
setSourceData(source_data);
- setGraphRagGenerateData({
- finish_at: knowledgeDetails.graphrag_task_finish_at,
- task_id: knowledgeDetails.graphrag_task_id,
- } as IGenerateLogButtonProps);
- setRaptorGenerateData({
- finish_at: knowledgeDetails.raptor_task_finish_at,
- task_id: knowledgeDetails.raptor_task_id,
- } as IGenerateLogButtonProps);
form.setValue(
'parse_type',
knowledgeDetails.pipeline_id ? ParseType.Pipeline : ParseType.BuiltIn,
@@ -206,20 +189,6 @@ export default function DatasetSettings() {
}
};
- const handleDeletePipelineTask = (type: GenerateType) => {
- if (type === GenerateType.KnowledgeGraph) {
- setGraphRagGenerateData({
- finish_at: '',
- task_id: '',
- } as IGenerateLogButtonProps);
- } else if (type === GenerateType.Raptor) {
- setRaptorGenerateData({
- finish_at: '',
- task_id: '',
- } as IGenerateLogButtonProps);
- }
- };
-
const parseType = useWatch({
control: form.control,
name: 'parse_type',
@@ -327,7 +296,6 @@ export default function DatasetSettings() {
/>
)}
- {/* */}
{parseType === ParseType.BuiltIn && }
{/*
-
-
- {t('knowledgeConfiguration.globalIndex')}
-
-
- handleDeletePipelineTask(GenerateType.KnowledgeGraph)
- }
- >
-
-
- handleDeletePipelineTask(GenerateType.Raptor)
- }
- >
diff --git a/web/src/pages/dataset/dataset/generate-button/constants.ts b/web/src/pages/dataset/dataset/generate-button/constants.ts
new file mode 100644
index 0000000000..83b7ae7730
--- /dev/null
+++ b/web/src/pages/dataset/dataset/generate-button/constants.ts
@@ -0,0 +1,36 @@
+import { ProcessingType } from '../../dataset-overview/dataset-common';
+
+export const GenerateStatus = {
+ running: 'running',
+ completed: 'completed',
+ start: 'start',
+ failed: 'failed',
+};
+
+export enum GenerateType {
+ KnowledgeGraph = 'KnowledgeGraph',
+ Raptor = 'Raptor',
+ Artifact = 'Artifact',
+ ToSkills = 'ToSkills',
+}
+
+export enum TraceType {
+ Graph = 'graph',
+ Raptor = 'raptor',
+ Artifact = 'artifact',
+ Skill = 'skill',
+}
+
+export const GenerateTypeMap = {
+ [GenerateType.KnowledgeGraph]: ProcessingType.knowledgeGraph,
+ [GenerateType.Raptor]: ProcessingType.raptor,
+ [GenerateType.Artifact]: ProcessingType.artifact,
+ [GenerateType.ToSkills]: ProcessingType.skill,
+};
+
+export const IconKeyMap = {
+ KnowledgeGraph: 'knowledgegraph',
+ Raptor: 'dataflow-01',
+ Artifact: 'book-open-01',
+ ToSkills: 'spark',
+};
diff --git a/web/src/pages/dataset/dataset/generate-button/generate-log-button.tsx b/web/src/pages/dataset/dataset/generate-button/generate-log-button.tsx
new file mode 100644
index 0000000000..f7bd610a26
--- /dev/null
+++ b/web/src/pages/dataset/dataset/generate-button/generate-log-button.tsx
@@ -0,0 +1,116 @@
+import { Button } from '@/components/ui/button';
+import { Modal } from '@/components/ui/modal/modal';
+import { cn } from '@/lib/utils';
+import { formatDate } from '@/utils/date';
+import { lowerFirst } from 'lodash';
+import { Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { GenerateType, GenerateTypeMap } from './constants';
+import { useUnBindTask } from './hook';
+
+export type IGenerateLogButtonProps = {
+ finish_at: string;
+ task_id: string;
+};
+
+export type IGenerateLogProps = IGenerateLogButtonProps & {
+ id?: string;
+ status: 0 | 1;
+ message?: string;
+ created_at?: string;
+ updated_at?: string;
+ type?: GenerateType;
+ className?: string;
+ onDelete?: () => void;
+};
+
+export function GenerateLogButton(props: IGenerateLogProps) {
+ const { t } = useTranslation();
+ const { message, finish_at, type, onDelete } = props;
+
+ const { handleUnbindTask } = useUnBindTask();
+
+ const handleDeleteFunc = async () => {
+ const data = await handleUnbindTask({
+ type: GenerateTypeMap[type as GenerateType],
+ });
+ Modal.destroy();
+ if (data.code === 0) {
+ onDelete?.();
+ }
+ };
+
+ const typeLabel = type
+ ? t(`knowledgeDetails.${lowerFirst(type)}`)
+ : t('knowledgeDetails.raptor');
+
+ const handleDelete = () => {
+ Modal.show({
+ visible: true,
+ className: '!w-[560px]',
+ title: t('common.delete') + ' ' + typeLabel,
+ children: (
+
+ ),
+ onVisibleChange: () => {
+ Modal.destroy();
+ },
+ footer: (
+
+
+
+
+ ),
+ });
+ };
+
+ return (
+
+
+ {finish_at && (
+ <>
+
+ {message || t('knowledgeDetails.generatedOn')}
+ {formatDate(finish_at)}
+
+
{
+ handleDelete();
+ e.stopPropagation();
+ }}
+ />
+ >
+ )}
+ {!finish_at && {t('knowledgeDetails.notGenerated')}
}
+
+
+ );
+}
+
+export default GenerateLogButton;
diff --git a/web/src/pages/dataset/dataset/generate-button/generate.tsx b/web/src/pages/dataset/dataset/generate-button/generate.tsx
deleted file mode 100644
index cfc5c4f87d..0000000000
--- a/web/src/pages/dataset/dataset/generate-button/generate.tsx
+++ /dev/null
@@ -1,355 +0,0 @@
-import { IconFontFill } from '@/components/icon-font';
-import { Button } from '@/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import { Modal } from '@/components/ui/modal/modal';
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from '@/components/ui/tooltip';
-import { cn } from '@/lib/utils';
-import { toFixed } from '@/utils/common-util';
-import { formatDate } from '@/utils/date';
-import { UseMutateAsyncFunction } from '@tanstack/react-query';
-import { t } from 'i18next';
-import { lowerFirst } from 'lodash';
-import { CirclePause, Trash2, WandSparkles } from 'lucide-react';
-import { useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { ProcessingType } from '../../dataset-overview/dataset-common';
-import { replaceText } from '../../process-log-modal';
-import {
- ITraceInfo,
- generateStatus,
- useDatasetGenerate,
- useTraceGenerate,
- useUnBindTask,
-} from './hook';
-
-export enum GenerateType {
- KnowledgeGraph = 'KnowledgeGraph',
- Raptor = 'Raptor',
-}
-export const GenerateTypeMap = {
- [GenerateType.KnowledgeGraph]: ProcessingType.knowledgeGraph,
- [GenerateType.Raptor]: ProcessingType.raptor,
-};
-const MenuItem: React.FC<{
- name: GenerateType;
- data: ITraceInfo;
- pauseGenerate: ({
- task_id,
- type,
- }: {
- task_id: string;
- type: GenerateType;
- }) => void;
- runGenerate: UseMutateAsyncFunction<
- any,
- Error,
- {
- type: GenerateType;
- },
- unknown
- >;
-}> = ({ name: type, runGenerate, data, pauseGenerate }) => {
- const iconKeyMap = {
- KnowledgeGraph: 'knowledgegraph',
- Raptor: 'dataflow-01',
- };
- const status = useMemo(() => {
- if (!data) {
- return generateStatus.start;
- }
- if (data.progress >= 1) {
- return generateStatus.completed;
- } else if (!data.progress && data.progress !== 0) {
- return generateStatus.start;
- } else if (data.progress < 0) {
- return generateStatus.failed;
- } else if (data.progress < 1) {
- return generateStatus.running;
- }
- }, [data]);
-
- const percent =
- status === generateStatus.failed
- ? 100
- : status === generateStatus.running
- ? data.progress * 100
- : 0;
-
- return (
- {
- e.preventDefault();
- }}
- onClick={(e) => {
- e.stopPropagation();
- }}
- >
- {
- if (
- status === generateStatus.start ||
- status === generateStatus.completed
- ) {
- runGenerate({ type });
- }
- }}
- >
-
-
- {t(`knowledgeDetails.${lowerFirst(type)}`)}
-
- {(status === generateStatus.start ||
- status === generateStatus.completed) && (
-
- {t(`knowledgeDetails.generate${type}`)}
-
- )}
- {(status === generateStatus.running ||
- status === generateStatus.failed) && (
-
-
- {status === generateStatus.running && (
-
{(toFixed(percent) as string) + '%'}
- )}
- {status === generateStatus.failed && (
-
{
- e.stopPropagation();
- runGenerate({ type });
- }}
- >
-
-
- )}
- {status !== generateStatus.failed && (
-
{
- e.stopPropagation();
- pauseGenerate({ task_id: data.id, type });
- }}
- >
-
-
- )}
-
- )}
- {status !== generateStatus.start &&
- status !== generateStatus.completed && (
-
- {replaceText(data?.progress_msg || '')}
-
- )}
-
-
- );
-};
-
-type GenerateProps = {
- disabled?: boolean;
-};
-const Generate: React.FC = (props) => {
- const { disabled = false } = props;
- const [open, setOpen] = useState(false);
- const { graphRunData, raptorRunData } = useTraceGenerate({ open });
- const { runGenerate, pauseGenerate } = useDatasetGenerate();
- const handleOpenChange = (isOpen: boolean) => {
- setOpen(isOpen);
- console.log('Dropdown is now', isOpen ? 'open' : 'closed');
- };
-
- return (
-
-
-
-
-
-
-
-
- {t('knowledgeDetails.generate')}
-
-
-
-
- {Object.values(GenerateType).map((name) => {
- const data = (
- name === GenerateType.KnowledgeGraph ? graphRunData : raptorRunData
- ) as ITraceInfo;
- return (
-
-
-
- );
- })}
-
-
- );
-};
-
-export default Generate;
-
-export type IGenerateLogButtonProps = {
- finish_at: string;
- task_id: string;
-};
-
-export type IGenerateLogProps = IGenerateLogButtonProps & {
- id?: string;
- status: 0 | 1;
- message?: string;
- created_at?: string;
- updated_at?: string;
- type?: GenerateType;
- className?: string;
- onDelete?: () => void;
-};
-export const GenerateLogButton = (props: IGenerateLogProps) => {
- const { t } = useTranslation();
- const { message, finish_at, type, onDelete } = props;
-
- const { handleUnbindTask } = useUnBindTask();
-
- const handleDeleteFunc = async () => {
- const data = await handleUnbindTask({
- type: GenerateTypeMap[type as GenerateType],
- });
- Modal.destroy();
- console.log('handleUnbindTask', data);
- if (data.code === 0) {
- onDelete?.();
- }
- };
-
- const handleDelete = () => {
- Modal.show({
- visible: true,
- className: '!w-[560px]',
- title:
- t('common.delete') +
- ' ' +
- (type === GenerateType.KnowledgeGraph
- ? t('knowledgeDetails.knowledgeGraph')
- : t('knowledgeDetails.raptor')),
- children: (
-
- ),
- onVisibleChange: () => {
- Modal.destroy();
- },
- footer: (
-
-
-
-
- ),
- });
- };
-
- return (
-
-
- {finish_at && (
- <>
-
- {message || t('knowledgeDetails.generatedOn')}
- {formatDate(finish_at)}
-
-
{
- console.log('delete');
- handleDelete();
- e.stopPropagation();
- }}
- />
- >
- )}
- {!finish_at && {t('knowledgeDetails.notGenerated')}
}
-
-
- );
-};
diff --git a/web/src/pages/dataset/dataset/generate-button/hook.ts b/web/src/pages/dataset/dataset/generate-button/hook.ts
index a79dd47a8a..34bb5dc2dc 100644
--- a/web/src/pages/dataset/dataset/generate-button/hook.ts
+++ b/web/src/pages/dataset/dataset/generate-button/hook.ts
@@ -6,23 +6,24 @@ import {
traceIndex,
} from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { t } from 'i18next';
-import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { ProcessingType } from '../../dataset-overview/dataset-common';
-import { GenerateType, GenerateTypeMap } from './generate';
-export const generateStatus = {
- running: 'running',
- completed: 'completed',
- start: 'start',
- failed: 'failed',
-};
+import { GenerateType, GenerateTypeMap, TraceType } from './constants';
enum DatasetKey {
generate = 'generate',
pauseGenerate = 'pauseGenerate',
}
+const PollIntervalMs = 5000;
+
+const DatasetGenerateKeys = {
+ trace: (type: GenerateType, id?: string, open?: boolean) =>
+ [type, id, open] as const,
+ traceById: (type: GenerateType, id?: string) => [type, id] as const,
+};
+
export interface ITraceInfo {
begin_at: string;
chunk_ids: string;
@@ -43,69 +44,71 @@ export interface ITraceInfo {
update_time: number;
}
+const useTraceQuery = (
+ type: GenerateType,
+ traceType: TraceType,
+ open: boolean,
+ id?: string,
+) => {
+ return useQuery({
+ queryKey: DatasetGenerateKeys.trace(type, id, open),
+ gcTime: 0,
+ refetchInterval: (query) => {
+ const progress = query.state.data?.progress;
+ return progress != null && progress >= 0 && progress < 1
+ ? PollIntervalMs
+ : false;
+ },
+ retry: 3,
+ retryDelay: 1000,
+ enabled: open && !!id,
+ queryFn: async () => {
+ const { data } = await traceIndex(id!, traceType);
+ return data?.data ?? {};
+ },
+ });
+};
+
export const useTraceGenerate = ({ open }: { open: boolean }) => {
const { id } = useParams();
- const [isLoopGraphRun, setLoopGraphRun] = useState(false);
- const [isLoopRaptorRun, setLoopRaptorRun] = useState(false);
- const { data: graphRunData, isFetching: graphRunloading } =
- useQuery({
- queryKey: [GenerateType.KnowledgeGraph, id, open],
- // initialData: {},
- gcTime: 0,
- refetchInterval: isLoopGraphRun ? 5000 : false,
- retry: 3,
- retryDelay: 1000,
- enabled: open,
- queryFn: async () => {
- const { data } = await traceIndex(id, 'graph');
- return data?.data || {};
- },
- });
+ const { data: graphRunData, isFetching: graphRunLoading } = useTraceQuery(
+ GenerateType.KnowledgeGraph,
+ TraceType.Graph,
+ open,
+ id,
+ );
+ const { data: raptorRunData, isFetching: raptorRunLoading } = useTraceQuery(
+ GenerateType.Raptor,
+ TraceType.Raptor,
+ open,
+ id,
+ );
- const { data: raptorRunData, isFetching: raptorRunloading } =
- useQuery({
- queryKey: [GenerateType.Raptor, id, open],
- // initialData: {},
- gcTime: 0,
- refetchInterval: isLoopRaptorRun ? 5000 : false,
- retry: 3,
- retryDelay: 1000,
- enabled: open,
- queryFn: async () => {
- const { data } = await traceIndex(id, 'raptor');
- return data?.data || {};
- },
- });
+ const { data: artifactRunData, isFetching: artifactRunLoading } =
+ useTraceQuery(GenerateType.Artifact, TraceType.Artifact, open, id);
+ const { data: skillRunData, isFetching: skillRunLoading } = useTraceQuery(
+ GenerateType.ToSkills,
+ TraceType.Skill,
+ open,
+ id,
+ );
- useEffect(() => {
- setLoopGraphRun(
- !!(
- (graphRunData?.progress || graphRunData?.progress === 0) &&
- graphRunData?.progress < 1 &&
- graphRunData?.progress >= 0
- ),
- );
- }, [graphRunData?.progress]);
-
- useEffect(() => {
- setLoopRaptorRun(
- !!(
- (raptorRunData?.progress || raptorRunData?.progress === 0) &&
- raptorRunData?.progress < 1 &&
- raptorRunData?.progress >= 0
- ),
- );
- }, [raptorRunData?.progress]);
return {
graphRunData,
- graphRunloading,
+ graphRunLoading,
raptorRunData,
- raptorRunloading,
+ raptorRunLoading,
+ artifactRunData,
+ artifactRunLoading,
+ skillRunData,
+ skillRunLoading,
};
};
export const useUnBindTask = () => {
const { id } = useParams();
+ const { t } = useTranslation();
+
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({
@@ -122,9 +125,6 @@ export const useUnBindTask = () => {
});
if (data.code === 0) {
message.success(t('message.operated'));
- // queryClient.invalidateQueries({
- // queryKey: [type],
- // });
}
return data;
},
@@ -135,6 +135,8 @@ export const useDatasetGenerate = () => {
const queryClient = useQueryClient();
const { id } = useParams();
const { handleUnbindTask } = useUnBindTask();
+ const { t } = useTranslation();
+
const {
data,
isPending: loading,
@@ -143,21 +145,24 @@ export const useDatasetGenerate = () => {
mutationKey: [DatasetKey.generate],
mutationFn: async ({ type }: { type: GenerateType }) => {
const indexType =
- type === GenerateType.KnowledgeGraph ? 'graph' : 'raptor';
- const { data } = await runIndex(id, indexType);
+ type === GenerateType.KnowledgeGraph
+ ? TraceType.Graph
+ : type === GenerateType.Artifact
+ ? TraceType.Artifact
+ : type === GenerateType.ToSkills
+ ? TraceType.Skill
+ : TraceType.Raptor;
+ const { data } = await runIndex(id!, indexType);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({
- queryKey: [type],
+ queryKey: DatasetGenerateKeys.traceById(type, id),
});
}
return data;
},
});
- // const pauseGenerate = useCallback(() => {
- // // TODO: pause generate
- // console.log('pause generate');
- // }, []);
+
const { mutateAsync: pauseGenerate } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({
@@ -178,9 +183,8 @@ export const useDatasetGenerate = () => {
wipe: type === GenerateType.KnowledgeGraph ? false : undefined,
});
if (data.code === 0 && unbindData.code === 0) {
- // message.success(t('message.operated'));
queryClient.invalidateQueries({
- queryKey: [type],
+ queryKey: DatasetGenerateKeys.traceById(type, id),
});
}
return data;
diff --git a/web/src/pages/dataset/dataset/generate-button/use-generate-status.ts b/web/src/pages/dataset/dataset/generate-button/use-generate-status.ts
new file mode 100644
index 0000000000..e58833cee9
--- /dev/null
+++ b/web/src/pages/dataset/dataset/generate-button/use-generate-status.ts
@@ -0,0 +1,33 @@
+import { useMemo } from 'react';
+
+import { GenerateStatus } from './constants';
+import { ITraceInfo } from './hook';
+
+export function useGenerateStatus(data?: ITraceInfo) {
+ const status = useMemo(() => {
+ if (!data) {
+ return GenerateStatus.start;
+ }
+ if (data.progress >= 1) {
+ return GenerateStatus.completed;
+ } else if (!data.progress && data.progress !== 0) {
+ return GenerateStatus.start;
+ } else if (data.progress < 0) {
+ return GenerateStatus.failed;
+ } else if (data.progress < 1) {
+ return GenerateStatus.running;
+ }
+ return GenerateStatus.start;
+ }, [data]);
+
+ const percent = useMemo(() => {
+ if (status === GenerateStatus.failed) {
+ return 100;
+ } else if (status === GenerateStatus.running) {
+ return data!.progress * 100;
+ }
+ return 0;
+ }, [status, data]);
+
+ return { status, percent };
+}
diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx
index cfe388e810..c00e352179 100644
--- a/web/src/pages/dataset/dataset/index.tsx
+++ b/web/src/pages/dataset/dataset/index.tsx
@@ -19,7 +19,6 @@ import {
useSelectedIds,
} from '@/hooks/logic-hooks/use-row-selection';
import { useFetchDocumentList } from '@/hooks/use-document-request';
-import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { LucidePlus } from 'lucide-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -28,7 +27,6 @@ import { useManageMetadata } from '../components/metedata/hooks/use-manage-modal
import { ManageMetadataModal } from '../components/metedata/manage-modal';
import { useKnowledgeBaseContext } from '../contexts/knowledge-base-context';
import { DatasetTable } from './dataset-table';
-import Generate from './generate-button/generate';
import { ReparseDialog } from './reparse-dialog';
import { useBulkOperateDataset } from './use-bulk-operate-dataset';
import { useCreateEmptyDocument } from './use-create-empty-document';
@@ -57,8 +55,6 @@ export default function Dataset() {
checkValue,
} = useFetchDocumentList();
- const { data: dataSetData } = useFetchKnowledgeBaseConfiguration();
-
const { filters, onOpenChange, filterGroup } = useSelectDatasetFilters();
const {
@@ -120,9 +116,6 @@ export default function Dataset() {
{t('knowledgeDetails.metadata.manageMetadata')}
- {/*
- {t('knowledgeDetails.metadata.manageMetadataForDataset')}
-
*/}
),
documentIds: selectedRowKeys,
@@ -164,38 +157,6 @@ export default function Dataset() {
}
- preChildren={ 0)} />}
- // preChildren={
- //
- // }
>
diff --git a/web/src/pages/dataset/sidebar/index.tsx b/web/src/pages/dataset/sidebar/index.tsx
index 15c25cb39c..6df9c2f9f3 100644
--- a/web/src/pages/dataset/sidebar/index.tsx
+++ b/web/src/pages/dataset/sidebar/index.tsx
@@ -1,20 +1,17 @@
-import { isEmpty } from 'lodash';
-
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
+ LucideBookText,
LucideFolderOpen,
LucideLogs,
LucideSettings,
LucideTextSearch,
} from 'lucide-react';
-import { IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook';
-import { useFetchKnowledgeGraph } from '@/hooks/use-knowledge-request';
import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date';
@@ -30,7 +27,6 @@ type PropType = {
export function SideBar({ dataset: data }: PropType) {
const pathName = useSecondPathName();
const { id } = useParams();
- const { data: routerData } = useFetchKnowledgeGraph();
const { t } = useTranslation();
const items = useMemo(() => {
@@ -55,18 +51,15 @@ export function SideBar({ dataset: data }: PropType) {
label: t(`knowledgeDetails.configuration`),
key: Routes.DataSetSetting,
},
+ {
+ icon: ,
+ label: t(`knowledgeDetails.compilation`),
+ key: Routes.Compilation,
+ },
];
- if (!isEmpty(routerData?.graph)) {
- list.push({
- icon: ,
- label: t(`knowledgeDetails.knowledgeGraph`),
- key: Routes.KnowledgeGraph,
- });
- }
-
return list;
- }, [t, routerData]);
+ }, [t]);
return (