From 548f296eadf996c03c79d21d85006c9bdf3e778a Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 16 Jul 2026 20:54:58 +0800 Subject: [PATCH] Feat: Visualize session graph data using ArtifactForceGraph. (#16977) ### Summary Feat: Visualize session graph data using ArtifactForceGraph. --- lefthook.yml | 27 ++----- web/.prettierrc | 5 +- web/src/constants/compilation.ts | 2 + .../plugins/floating-selection-toolbar.tsx | 2 +- .../components/mindmap-g6-graph/index.tsx | 79 +++++++++++++------ .../components/representation-renderer.tsx | 32 ++++++++ .../components/representation-select.tsx | 20 +---- 7 files changed, 104 insertions(+), 63 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index a12aef81f7..54efbdd627 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -70,27 +70,10 @@ pre-commit: else gofmt -w {staged_files} fi - - name: web-deps - glob: "web/**/*.{css,less,json,js,jsx,ts,tsx}" - run: | - # Ensure web/node_modules is populated. CI runners don't run `npm install` - # before lefthook, and `npx` without local node_modules fetches the - # latest packages from the registry - which breaks because the pinned - # prettier plugins (prettier-plugin-organize-imports, - # prettier-plugin-packagejson) aren't auto-resolved and ESLint 10 - # requires eslint.config.js. - # Serialized via lefthook 'deps' from web-prettier/web-eslint instead of - # a mkdir mutex, which left stale locks on interrupted commits and - # caused subsequent commits to hang forever in 'while ! mkdir'. - if [ ! -f web/node_modules/.package-lock.json ]; then - echo '==> web/node_modules missing or incomplete; running npm ci --prefix web' - rm -rf web/node_modules - npm ci --prefix web --no-audit --no-fund - fi - name: web-prettier + # prettier plugins removed from .prettierrc, so this job needs no node_modules. glob: "web/**/*.{css,less,json,js,jsx,ts,tsx}" stage_fixed: true - deps: [web-deps] run: | if [ "${LEFTHOOK_CHECK_ONLY:-}" = "1" ]; then cd web && printf '%s\n' {staged_files} | sed 's|^web/||' | xargs npx prettier --check --ignore-unknown @@ -98,10 +81,16 @@ pre-commit: cd web && printf '%s\n' {staged_files} | sed 's|^web/||' | xargs npx prettier --write --ignore-unknown fi - name: web-eslint + # Only web-eslint needs node_modules (for @typescript-eslint/* plugins). + # web-prettier has no npm deps, so there is no race on node_modules. glob: "web/**/*.{js,jsx,ts,tsx}" stage_fixed: true - deps: [web-deps] run: | + if [ ! -f web/node_modules/.package-lock.json ]; then + echo '==> web/node_modules missing or incomplete; running npm ci --prefix web' + rm -rf web/node_modules + npm ci --prefix web --no-audit --no-fund + fi if [ "${LEFTHOOK_CHECK_ONLY:-}" = "1" ]; then cd web && printf '%s\n' {staged_files} | sed 's|^web/||' | xargs npx eslint else diff --git a/web/.prettierrc b/web/.prettierrc index bd49c75255..ea51f06ac9 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -4,9 +4,6 @@ "trailingComma": "all", "proseWrap": "never", "overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }], - "plugins": [ - "prettier-plugin-organize-imports", - "prettier-plugin-packagejson" - ], + "plugins": [], "endOfLine": "lf" } diff --git a/web/src/constants/compilation.ts b/web/src/constants/compilation.ts index 362092c1ce..daecbaed48 100644 --- a/web/src/constants/compilation.ts +++ b/web/src/constants/compilation.ts @@ -6,6 +6,8 @@ export const enum CompilationTemplateKind { Tree = 'tree', Empty = 'empty', MindMap = 'mind_map', + SessionGraph = 'session_graph', + SessionEssence = 'session_essence', } export const enum CompilationTemplateScope { diff --git a/web/src/lib/editor/plugins/floating-selection-toolbar.tsx b/web/src/lib/editor/plugins/floating-selection-toolbar.tsx index 2c25388cd8..9cb490b73d 100644 --- a/web/src/lib/editor/plugins/floating-selection-toolbar.tsx +++ b/web/src/lib/editor/plugins/floating-selection-toolbar.tsx @@ -233,7 +233,7 @@ export default function FloatingSelectionToolbar() { alignItems: 'center', gap: 2, padding: '4px 6px', - background: 'var(--nim-bg-secondary)', + background: 'var(--bg-base)', border: '1px solid var(--nim-border)', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.25)', diff --git a/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx b/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx index 926486e0ba..c2ea3a4bc8 100644 --- a/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx +++ b/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx @@ -1,6 +1,7 @@ +import { SkeletonCard } from '@/components/skeleton-card'; import { cn } from '@/lib/utils'; import { Graph, IElementEvent, NodeEvent, treeToGraphData } from '@antv/g6'; -import { memo, useEffect, useRef } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { adaptMindMapToIndentedTree } from '../../utils/adapters'; import { type MindMapG6GraphProps, type MindMapNodeValue } from './types'; @@ -17,10 +18,28 @@ function MindMapG6Graph({ }: MindMapG6GraphProps) { const containerRef = useRef(null); const graphRef = useRef(null); + const [loading, setLoading] = useState(true); + const [graphData, setGraphData] = useState | null>(null); + + // Defer heavy data transformation to avoid blocking the render phase. + useEffect(() => { + setLoading(true); + setGraphData(null); + + const rafId = requestAnimationFrame(() => { + setGraphData(treeToGraphData(adaptMindMapToIndentedTree(template))); + }); + + return () => { + cancelAnimationFrame(rafId); + }; + }, [template]); useEffect(() => { const container = containerRef.current; - if (!container) return; + if (!container || !graphData) return; const styles = window.getComputedStyle(container); const textPrimary = styles.getPropertyValue('--text-primary').trim(); @@ -41,7 +60,8 @@ function MindMapG6Graph({ width, height, autoFit: 'view', - data: treeToGraphData(adaptMindMapToIndentedTree(template)), + animation: false, + data: graphData, node: { style: { labelText: (datum) => @@ -64,8 +84,7 @@ function MindMapG6Graph({ direction: 'H', preLayout: false, getHeight: () => 32, - getWidth: (node: { id: string; data?: MindMapNodeData }) => - Math.max(120, String(node.data?.name ?? node.id).length * 7), + getWidth: () => 120, getVGap: () => 10, getHGap: () => 80, }, @@ -89,36 +108,50 @@ function MindMapG6Graph({ graph.on(NodeEvent.CLICK, handleNodeClick); graphRef.current = graph; - void graph.render(); + void graph.render().then(() => { + setLoading(false); + }); }; - observer = new ResizeObserver(() => { - if (!graph) { - createGraph(); - return; - } + // Defer graph creation so the browser paints the skeleton before the + // heavy G6 layout + render starts. + const rafId = requestAnimationFrame(() => { + observer = new ResizeObserver(() => { + if (!graph) { + createGraph(); + return; + } - const { width, height } = container.getBoundingClientRect(); - if (width > 0 && height > 0) { - graph.resize(width, height); - } + const { width, height } = container.getBoundingClientRect(); + if (width > 0 && height > 0) { + graph.resize(width, height); + } + }); + + observer.observe(container); + createGraph(); }); - observer.observe(container); - createGraph(); - return () => { + cancelAnimationFrame(rafId); observer?.disconnect(); graph?.destroy(); graphRef.current = null; }; - }, [template, onNodeClick]); + }, [graphData, onNodeClick]); return ( -
+
+ {loading && ( +
+ +
+ )} +
+
); } diff --git a/web/src/pages/chunk/representation/components/representation-renderer.tsx b/web/src/pages/chunk/representation/components/representation-renderer.tsx index 2aff5143fd..085831257b 100644 --- a/web/src/pages/chunk/representation/components/representation-renderer.tsx +++ b/web/src/pages/chunk/representation/components/representation-renderer.tsx @@ -170,6 +170,38 @@ export function RepresentationRenderer({ />
); + case CompilationTemplateKind.SessionGraph: + return ( +
+ +
+ ); + case CompilationTemplateKind.SessionEssence: + return ( +
+ +
+ ); + case CompilationTemplateKind.Empty: + return ( +
+ +
+ ); default: return ; } diff --git a/web/src/pages/chunk/representation/components/representation-select.tsx b/web/src/pages/chunk/representation/components/representation-select.tsx index dc8dc9bc5e..380e4b87f0 100644 --- a/web/src/pages/chunk/representation/components/representation-select.tsx +++ b/web/src/pages/chunk/representation/components/representation-select.tsx @@ -2,12 +2,9 @@ import { SelectWithSearch, type SelectWithSearchFlagOptionType, } from '@/components/originui/select-with-search'; -import { - type IStructureGraphTemplate, - type StructureTemplateKind, -} from '@/interfaces/database/document-structure'; +import { type IStructureGraphTemplate } from '@/interfaces/database/document-structure'; +import { formatKindLabel } from '@/utils/compilation-template-util'; import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; interface RepresentationSelectProps { templates: IStructureGraphTemplate[]; @@ -15,20 +12,11 @@ interface RepresentationSelectProps { onChange?: (value: string) => void; } -function getKindLabel( - t: (key: string) => string, - kind: StructureTemplateKind, -): string { - return t(`chunk.representationKinds.${kind}`); -} - export function RepresentationSelect({ templates, value, onChange, }: RepresentationSelectProps) { - const { t } = useTranslation(); - const options = useMemo(() => { return templates.map((template) => ({ value: template.template_id, @@ -36,13 +24,13 @@ export function RepresentationSelect({ {template.template_name} - {getKindLabel(t, template.kind)} + {formatKindLabel(template.kind)} ), keywords: [template.template_name, template.kind], })); - }, [templates, t]); + }, [templates]); return (