@diegonogueiradev_/mcp-graph 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/api/middleware/error-handler.d.ts +3 -0
- package/dist/api/middleware/error-handler.d.ts.map +1 -0
- package/dist/api/middleware/error-handler.js +35 -0
- package/dist/api/middleware/error-handler.js.map +1 -0
- package/dist/api/middleware/validate.d.ts +5 -0
- package/dist/api/middleware/validate.d.ts.map +1 -0
- package/dist/api/middleware/validate.js +23 -0
- package/dist/api/middleware/validate.js.map +1 -0
- package/dist/api/router.d.ts +11 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +41 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/routes/capture.d.ts +3 -0
- package/dist/api/routes/capture.d.ts.map +1 -0
- package/dist/api/routes/capture.js +31 -0
- package/dist/api/routes/capture.js.map +1 -0
- package/dist/api/routes/context.d.ts +4 -0
- package/dist/api/routes/context.d.ts.map +1 -0
- package/dist/api/routes/context.js +25 -0
- package/dist/api/routes/context.js.map +1 -0
- package/dist/api/routes/docs-cache.d.ts +4 -0
- package/dist/api/routes/docs-cache.d.ts.map +1 -0
- package/dist/api/routes/docs-cache.js +79 -0
- package/dist/api/routes/docs-cache.js.map +1 -0
- package/dist/api/routes/edges.d.ts +4 -0
- package/dist/api/routes/edges.d.ts.map +1 -0
- package/dist/api/routes/edges.js +50 -0
- package/dist/api/routes/edges.js.map +1 -0
- package/dist/api/routes/events.d.ts +4 -0
- package/dist/api/routes/events.d.ts.map +1 -0
- package/dist/api/routes/events.js +37 -0
- package/dist/api/routes/events.js.map +1 -0
- package/dist/api/routes/graph.d.ts +4 -0
- package/dist/api/routes/graph.d.ts.map +1 -0
- package/dist/api/routes/graph.js +39 -0
- package/dist/api/routes/graph.js.map +1 -0
- package/dist/api/routes/import.d.ts +4 -0
- package/dist/api/routes/import.d.ts.map +1 -0
- package/dist/api/routes/import.js +92 -0
- package/dist/api/routes/import.js.map +1 -0
- package/dist/api/routes/insights.d.ts +4 -0
- package/dist/api/routes/insights.d.ts.map +1 -0
- package/dist/api/routes/insights.js +40 -0
- package/dist/api/routes/insights.js.map +1 -0
- package/dist/api/routes/integrations.d.ts +4 -0
- package/dist/api/routes/integrations.d.ts.map +1 -0
- package/dist/api/routes/integrations.js +56 -0
- package/dist/api/routes/integrations.js.map +1 -0
- package/dist/api/routes/nodes.d.ts +4 -0
- package/dist/api/routes/nodes.d.ts.map +1 -0
- package/dist/api/routes/nodes.js +123 -0
- package/dist/api/routes/nodes.js.map +1 -0
- package/dist/api/routes/project.d.ts +4 -0
- package/dist/api/routes/project.d.ts.map +1 -0
- package/dist/api/routes/project.js +33 -0
- package/dist/api/routes/project.js.map +1 -0
- package/dist/api/routes/search.d.ts +4 -0
- package/dist/api/routes/search.d.ts.map +1 -0
- package/dist/api/routes/search.js +25 -0
- package/dist/api/routes/search.js.map +1 -0
- package/dist/api/routes/skills.d.ts +3 -0
- package/dist/api/routes/skills.d.ts.map +1 -0
- package/dist/api/routes/skills.js +16 -0
- package/dist/api/routes/skills.js.map +1 -0
- package/dist/api/routes/stats.d.ts +4 -0
- package/dist/api/routes/stats.d.ts.map +1 -0
- package/dist/api/routes/stats.js +14 -0
- package/dist/api/routes/stats.js.map +1 -0
- package/dist/cli/commands/import-cmd.d.ts +3 -0
- package/dist/cli/commands/import-cmd.d.ts.map +1 -0
- package/dist/cli/commands/import-cmd.js +38 -0
- package/dist/cli/commands/import-cmd.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +55 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +3 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +18 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +3 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +39 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/capture/content-extractor.d.ts +21 -0
- package/dist/core/capture/content-extractor.d.ts.map +1 -0
- package/dist/core/capture/content-extractor.js +74 -0
- package/dist/core/capture/content-extractor.js.map +1 -0
- package/dist/core/capture/web-capture.d.ts +20 -0
- package/dist/core/capture/web-capture.d.ts.map +1 -0
- package/dist/core/capture/web-capture.js +51 -0
- package/dist/core/capture/web-capture.js.map +1 -0
- package/dist/core/config/config-loader.d.ts +3 -0
- package/dist/core/config/config-loader.d.ts.map +1 -0
- package/dist/core/config/config-loader.js +43 -0
- package/dist/core/config/config-loader.js.map +1 -0
- package/dist/core/config/config-schema.d.ts +11 -0
- package/dist/core/config/config-schema.d.ts.map +1 -0
- package/dist/core/config/config-schema.js +12 -0
- package/dist/core/config/config-schema.js.map +1 -0
- package/dist/core/docs/docs-cache-store.d.ts +24 -0
- package/dist/core/docs/docs-cache-store.d.ts.map +1 -0
- package/dist/core/docs/docs-cache-store.js +61 -0
- package/dist/core/docs/docs-cache-store.js.map +1 -0
- package/dist/core/docs/docs-syncer.d.ts +13 -0
- package/dist/core/docs/docs-syncer.d.ts.map +1 -0
- package/dist/core/docs/docs-syncer.js +38 -0
- package/dist/core/docs/docs-syncer.js.map +1 -0
- package/dist/core/events/event-bus.d.ts +26 -0
- package/dist/core/events/event-bus.d.ts.map +1 -0
- package/dist/core/events/event-bus.js +47 -0
- package/dist/core/events/event-bus.js.map +1 -0
- package/dist/core/events/event-types.d.ts +57 -0
- package/dist/core/events/event-types.d.ts.map +1 -0
- package/dist/core/events/event-types.js +2 -0
- package/dist/core/events/event-types.js.map +1 -0
- package/dist/core/graph/mermaid-export.d.ts +9 -0
- package/dist/core/graph/mermaid-export.d.ts.map +1 -0
- package/dist/core/graph/mermaid-export.js +80 -0
- package/dist/core/graph/mermaid-export.js.map +1 -0
- package/dist/core/importer/prd-to-graph.d.ts.map +1 -1
- package/dist/core/importer/prd-to-graph.js +7 -0
- package/dist/core/importer/prd-to-graph.js.map +1 -1
- package/dist/core/insights/bottleneck-detector.d.ts +31 -0
- package/dist/core/insights/bottleneck-detector.d.ts.map +1 -0
- package/dist/core/insights/bottleneck-detector.js +69 -0
- package/dist/core/insights/bottleneck-detector.js.map +1 -0
- package/dist/core/insights/metrics-calculator.d.ts +31 -0
- package/dist/core/insights/metrics-calculator.d.ts.map +1 -0
- package/dist/core/insights/metrics-calculator.js +78 -0
- package/dist/core/insights/metrics-calculator.js.map +1 -0
- package/dist/core/insights/skill-recommender.d.ts +21 -0
- package/dist/core/insights/skill-recommender.d.ts.map +1 -0
- package/dist/core/insights/skill-recommender.js +129 -0
- package/dist/core/insights/skill-recommender.js.map +1 -0
- package/dist/core/integrations/serena-reader.d.ts +18 -0
- package/dist/core/integrations/serena-reader.d.ts.map +1 -0
- package/dist/core/integrations/serena-reader.js +50 -0
- package/dist/core/integrations/serena-reader.js.map +1 -0
- package/dist/core/integrations/tool-status.d.ts +18 -0
- package/dist/core/integrations/tool-status.d.ts.map +1 -0
- package/dist/core/integrations/tool-status.js +92 -0
- package/dist/core/integrations/tool-status.js.map +1 -0
- package/dist/core/parser/file-reader.d.ts +13 -0
- package/dist/core/parser/file-reader.d.ts.map +1 -0
- package/dist/core/parser/file-reader.js +52 -0
- package/dist/core/parser/file-reader.js.map +1 -0
- package/dist/core/parser/read-html.d.ts +7 -0
- package/dist/core/parser/read-html.d.ts.map +1 -0
- package/dist/core/parser/read-html.js +51 -0
- package/dist/core/parser/read-html.js.map +1 -0
- package/dist/core/parser/read-pdf.d.ts +10 -0
- package/dist/core/parser/read-pdf.d.ts.map +1 -0
- package/dist/core/parser/read-pdf.js +16 -0
- package/dist/core/parser/read-pdf.js.map +1 -0
- package/dist/core/planner/next-task.d.ts.map +1 -1
- package/dist/core/planner/next-task.js +4 -1
- package/dist/core/planner/next-task.js.map +1 -1
- package/dist/core/search/fts-search.d.ts.map +1 -1
- package/dist/core/search/fts-search.js +6 -1
- package/dist/core/search/fts-search.js.map +1 -1
- package/dist/core/store/migrations.d.ts.map +1 -1
- package/dist/core/store/migrations.js +38 -0
- package/dist/core/store/migrations.js.map +1 -1
- package/dist/core/store/sqlite-store.d.ts +7 -0
- package/dist/core/store/sqlite-store.d.ts.map +1 -1
- package/dist/core/store/sqlite-store.js +28 -3
- package/dist/core/store/sqlite-store.js.map +1 -1
- package/dist/core/utils/logger.d.ts +1 -0
- package/dist/core/utils/logger.d.ts.map +1 -1
- package/dist/core/utils/logger.js +5 -0
- package/dist/core/utils/logger.js.map +1 -1
- package/dist/mcp/init-project.d.ts.map +1 -1
- package/dist/mcp/init-project.js +12 -16
- package/dist/mcp/init-project.js.map +1 -1
- package/dist/mcp/server.js +16 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.js +0 -0
- package/dist/mcp/tools/export-mermaid.d.ts +4 -0
- package/dist/mcp/tools/export-mermaid.d.ts.map +1 -0
- package/dist/mcp/tools/export-mermaid.js +27 -0
- package/dist/mcp/tools/export-mermaid.js.map +1 -0
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/web/dashboard/dist/assets/code-graph-tab-jvBo8Q9t.js +1 -0
- package/dist/web/dashboard/dist/assets/constants-CLJl-f3f.js +1 -0
- package/dist/web/dashboard/dist/assets/graph-tab-BoKfDlvO.js +1 -0
- package/dist/web/dashboard/dist/assets/graph-utils-BZV40eAE.css +1 -0
- package/dist/web/dashboard/dist/assets/graph-utils-uGOH4eMw.js +23 -0
- package/dist/web/dashboard/dist/assets/index-DM_LGeRr.css +1 -0
- package/dist/web/dashboard/dist/assets/index-DrHbgcp5.js +53 -0
- package/dist/web/dashboard/dist/assets/insights-tab-D7sHV2xV.js +1 -0
- package/dist/web/dashboard/dist/assets/prd-backlog-tab-C_Uq1Qte.js +1 -0
- package/dist/web/dashboard/dist/index.html +13 -0
- package/dist/web/public/css/styles.css +646 -0
- package/dist/web/public/index.html +209 -0
- package/dist/web/public/js/api-client.js +85 -0
- package/dist/web/public/js/app.js +112 -0
- package/dist/web/public/js/capture-form.js +196 -0
- package/dist/web/public/js/filters.js +94 -0
- package/dist/web/public/js/force-graph-renderer.js +498 -0
- package/dist/web/public/js/graph-renderer.js +62 -0
- package/dist/web/public/js/import-form.js +105 -0
- package/dist/web/public/js/node-detail.js +106 -0
- package/dist/web/public/js/tabs/code-graph-tab.js +66 -0
- package/dist/web/public/js/tabs/graph-tab.js +238 -0
- package/dist/web/public/js/tabs/insights-tab.js +236 -0
- package/dist/web/public/js/tabs/knowledge-tab.js +201 -0
- package/dist/web/public/js/tabs/prd-backlog-tab.js +167 -0
- package/dist/web/public/vendor/force-graph.min.js +5 -0
- package/dist/web/public/vendor/mermaid.min.js +2843 -0
- package/package.json +22 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter UI for graph visualization
|
|
3
|
+
*/
|
|
4
|
+
const Filters = (() => {
|
|
5
|
+
const STATUSES = ['backlog', 'ready', 'in_progress', 'blocked', 'done'];
|
|
6
|
+
const TYPES = ['epic', 'task', 'subtask', 'requirement', 'constraint', 'milestone', 'acceptance_criteria', 'risk', 'decision'];
|
|
7
|
+
const EDGE_TYPES = ['depends_on', 'parent_of', 'blocks', 'related_to', 'implements'];
|
|
8
|
+
|
|
9
|
+
const STATUS_LABELS = {
|
|
10
|
+
backlog: 'Backlog', ready: 'Ready', in_progress: 'In Progress',
|
|
11
|
+
blocked: 'Blocked', done: 'Done',
|
|
12
|
+
};
|
|
13
|
+
const TYPE_LABELS = {
|
|
14
|
+
epic: 'Epic', task: 'Task', subtask: 'Subtask', requirement: 'Requirement',
|
|
15
|
+
constraint: 'Constraint', milestone: 'Milestone', acceptance_criteria: 'AC',
|
|
16
|
+
risk: 'Risk', decision: 'Decision',
|
|
17
|
+
};
|
|
18
|
+
const EDGE_TYPE_LABELS = {
|
|
19
|
+
depends_on: 'Depends On', parent_of: 'Parent Of', blocks: 'Blocks',
|
|
20
|
+
related_to: 'Related To', implements: 'Implements',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function renderCheckboxes(containerId, items, labels, checked = false) {
|
|
24
|
+
const container = document.getElementById(containerId);
|
|
25
|
+
if (!container) return;
|
|
26
|
+
container.innerHTML = items.map(item => `
|
|
27
|
+
<label class="filter-check">
|
|
28
|
+
<input type="checkbox" value="${item}" ${checked ? 'checked' : ''}>
|
|
29
|
+
<span class="status-dot status-${item}"></span>
|
|
30
|
+
${labels[item] || item}
|
|
31
|
+
</label>
|
|
32
|
+
`).join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function init() {
|
|
36
|
+
renderCheckboxes('filter-status', STATUSES, STATUS_LABELS);
|
|
37
|
+
renderCheckboxes('filter-type', TYPES, TYPE_LABELS);
|
|
38
|
+
renderCheckboxes('filter-edge-types', EDGE_TYPES, EDGE_TYPE_LABELS, true);
|
|
39
|
+
|
|
40
|
+
// Depth slider label
|
|
41
|
+
const depthSlider = document.getElementById('filter-focus-depth');
|
|
42
|
+
const depthValue = document.getElementById('depth-value');
|
|
43
|
+
if (depthSlider && depthValue) {
|
|
44
|
+
depthSlider.addEventListener('input', () => {
|
|
45
|
+
depthValue.textContent = depthSlider.value === '0' ? 'All' : depthSlider.value;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getValues() {
|
|
51
|
+
const status = Array.from(document.querySelectorAll('#filter-status input:checked')).map(cb => cb.value);
|
|
52
|
+
const type = Array.from(document.querySelectorAll('#filter-type input:checked')).map(cb => cb.value);
|
|
53
|
+
const direction = document.getElementById('filter-direction')?.value || 'TD';
|
|
54
|
+
const format = document.getElementById('filter-format')?.value || 'flowchart';
|
|
55
|
+
const edgeTypes = Array.from(document.querySelectorAll('#filter-edge-types input:checked')).map(cb => cb.value);
|
|
56
|
+
const focusDepth = parseInt(document.getElementById('filter-focus-depth')?.value || '0', 10);
|
|
57
|
+
return { status, type, direction, format, edgeTypes, focusDepth };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clear() {
|
|
61
|
+
document.querySelectorAll('.filter-checks input').forEach(cb => { cb.checked = false; });
|
|
62
|
+
// Re-check all edge types on clear
|
|
63
|
+
document.querySelectorAll('#filter-edge-types input').forEach(cb => { cb.checked = true; });
|
|
64
|
+
const dirSelect = document.getElementById('filter-direction');
|
|
65
|
+
if (dirSelect) dirSelect.value = 'TD';
|
|
66
|
+
const fmtSelect = document.getElementById('filter-format');
|
|
67
|
+
if (fmtSelect) fmtSelect.value = 'flowchart';
|
|
68
|
+
const depthSlider = document.getElementById('filter-focus-depth');
|
|
69
|
+
if (depthSlider) depthSlider.value = '0';
|
|
70
|
+
const depthValue = document.getElementById('depth-value');
|
|
71
|
+
if (depthValue) depthValue.textContent = 'All';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toQueryParams() {
|
|
75
|
+
const vals = getValues();
|
|
76
|
+
const params = {};
|
|
77
|
+
if (vals.status.length) params.status = vals.status.join(',');
|
|
78
|
+
if (vals.type.length) params.type = vals.type.join(',');
|
|
79
|
+
params.direction = vals.direction;
|
|
80
|
+
params.format = vals.format;
|
|
81
|
+
return params;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Toggle visibility of Mermaid-specific vs Force-specific filters */
|
|
85
|
+
function setViewMode(mode) {
|
|
86
|
+
const mermaidFilters = document.querySelectorAll('.mermaid-filter');
|
|
87
|
+
const forceFilters = document.querySelectorAll('.force-filter');
|
|
88
|
+
|
|
89
|
+
mermaidFilters.forEach(el => el.classList.toggle('hidden', mode === 'force'));
|
|
90
|
+
forceFilters.forEach(el => el.classList.toggle('hidden', mode === 'mermaid'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { init, getValues, clear, toQueryParams, setViewMode };
|
|
94
|
+
})();
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Force-directed graph renderer using force-graph library
|
|
3
|
+
* Vanilla JS IIFE — canvas-based interactive visualization
|
|
4
|
+
*/
|
|
5
|
+
const ForceGraphRenderer = (() => {
|
|
6
|
+
// ── Color & Size Constants ────────────────────
|
|
7
|
+
const NODE_COLORS = {
|
|
8
|
+
epic: '#7c3aed', task: '#3b82f6', subtask: '#22c55e',
|
|
9
|
+
requirement: '#f59e0b', constraint: '#ef4444', milestone: '#8b5cf6',
|
|
10
|
+
acceptance_criteria: '#06b6d4', risk: '#f97316', decision: '#6366f1',
|
|
11
|
+
};
|
|
12
|
+
const NODE_SIZES = {
|
|
13
|
+
epic: 10, task: 7, subtask: 5, requirement: 6, constraint: 6,
|
|
14
|
+
milestone: 9, acceptance_criteria: 4, risk: 6, decision: 7,
|
|
15
|
+
};
|
|
16
|
+
const STATUS_COLORS = {
|
|
17
|
+
done: '#4caf50', in_progress: '#2196f3', blocked: '#f44336',
|
|
18
|
+
ready: '#ff9800', backlog: '#9e9e9e',
|
|
19
|
+
};
|
|
20
|
+
const EDGE_COLORS = {
|
|
21
|
+
depends_on: '#ff980088', parent_of: '#9e9e9e66', blocks: '#f4433688',
|
|
22
|
+
related_to: '#9e9e9e44', implements: '#7c3aed66',
|
|
23
|
+
};
|
|
24
|
+
const EDGE_DASH = {
|
|
25
|
+
related_to: [4, 2],
|
|
26
|
+
implements: [6, 3],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── State ─────────────────────────────────────
|
|
30
|
+
let graph = null;
|
|
31
|
+
let containerId = null;
|
|
32
|
+
let theme = 'default';
|
|
33
|
+
let hoveredNode = null;
|
|
34
|
+
let selectedNode = null;
|
|
35
|
+
let highlightNodes = new Set();
|
|
36
|
+
let highlightLinks = new Set();
|
|
37
|
+
let adjacencyMap = new Map();
|
|
38
|
+
let allNodes = [];
|
|
39
|
+
let allLinks = [];
|
|
40
|
+
let focusDepth = 0; // 0 = show all
|
|
41
|
+
let activeEdgeTypes = new Set();
|
|
42
|
+
let tooltipEl = null;
|
|
43
|
+
|
|
44
|
+
// ── Adjacency Map (O(1) neighbor lookup) ──────
|
|
45
|
+
function buildAdjacencyMap(links) {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
for (const link of links) {
|
|
48
|
+
const sId = typeof link.source === 'object' ? link.source.id : link.source;
|
|
49
|
+
const tId = typeof link.target === 'object' ? link.target.id : link.target;
|
|
50
|
+
if (!map.has(sId)) map.set(sId, []);
|
|
51
|
+
if (!map.has(tId)) map.set(tId, []);
|
|
52
|
+
map.get(sId).push({ nodeId: tId, link });
|
|
53
|
+
map.get(tId).push({ nodeId: sId, link });
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── N-hop BFS for focus depth ─────────────────
|
|
59
|
+
function getNeighborhood(startId, adjMap, maxDepth) {
|
|
60
|
+
const visited = new Set([startId]);
|
|
61
|
+
let frontier = [startId];
|
|
62
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
63
|
+
const next = [];
|
|
64
|
+
for (const id of frontier) {
|
|
65
|
+
const neighbors = adjMap.get(id);
|
|
66
|
+
if (!neighbors) continue;
|
|
67
|
+
for (const { nodeId } of neighbors) {
|
|
68
|
+
if (!visited.has(nodeId)) {
|
|
69
|
+
visited.add(nodeId);
|
|
70
|
+
next.push(nodeId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
frontier = next;
|
|
75
|
+
}
|
|
76
|
+
return visited;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Tooltip ───────────────────────────────────
|
|
80
|
+
function ensureTooltip() {
|
|
81
|
+
if (tooltipEl) return tooltipEl;
|
|
82
|
+
tooltipEl = document.createElement('div');
|
|
83
|
+
tooltipEl.className = 'graph-tooltip';
|
|
84
|
+
tooltipEl.style.display = 'none';
|
|
85
|
+
document.body.appendChild(tooltipEl);
|
|
86
|
+
return tooltipEl;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function showTooltip(node, event) {
|
|
90
|
+
const tip = ensureTooltip();
|
|
91
|
+
const statusClass = `status-${node.status || 'backlog'}`;
|
|
92
|
+
tip.innerHTML = `
|
|
93
|
+
<div class="tooltip-title">${escapeHtml(node.title || node.id)}</div>
|
|
94
|
+
<div class="tooltip-row"><span class="badge badge-type">${escapeHtml(node.type)}</span> <span class="badge ${statusClass}">${escapeHtml(node.status || 'backlog')}</span></div>
|
|
95
|
+
${node.priority !== undefined ? `<div class="tooltip-row text-muted">Priority: ${node.priority}</div>` : ''}
|
|
96
|
+
${node.description ? `<div class="tooltip-desc">${escapeHtml(truncate(node.description, 120))}</div>` : ''}
|
|
97
|
+
`;
|
|
98
|
+
tip.style.display = 'block';
|
|
99
|
+
positionTooltip(tip, event);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positionTooltip(tip, event) {
|
|
103
|
+
const x = event.clientX || 0;
|
|
104
|
+
const y = event.clientY || 0;
|
|
105
|
+
const pad = 12;
|
|
106
|
+
tip.style.left = (x + pad) + 'px';
|
|
107
|
+
tip.style.top = (y + pad) + 'px';
|
|
108
|
+
// Adjust if off-screen
|
|
109
|
+
const rect = tip.getBoundingClientRect();
|
|
110
|
+
if (rect.right > window.innerWidth) {
|
|
111
|
+
tip.style.left = (x - rect.width - pad) + 'px';
|
|
112
|
+
}
|
|
113
|
+
if (rect.bottom > window.innerHeight) {
|
|
114
|
+
tip.style.top = (y - rect.height - pad) + 'px';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hideTooltip() {
|
|
119
|
+
if (tooltipEl) tooltipEl.style.display = 'none';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Helpers ───────────────────────────────────
|
|
123
|
+
function escapeHtml(text) {
|
|
124
|
+
if (!text) return '';
|
|
125
|
+
const div = document.createElement('div');
|
|
126
|
+
div.textContent = String(text);
|
|
127
|
+
return div.innerHTML;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function truncate(str, len) {
|
|
131
|
+
if (!str) return '';
|
|
132
|
+
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function linkKey(link) {
|
|
136
|
+
const sId = typeof link.source === 'object' ? link.source.id : link.source;
|
|
137
|
+
const tId = typeof link.target === 'object' ? link.target.id : link.target;
|
|
138
|
+
return `${sId}-${tId}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Filter pipeline ───────────────────────────
|
|
142
|
+
function getFilteredData() {
|
|
143
|
+
let nodes = [...allNodes];
|
|
144
|
+
let links = [...allLinks];
|
|
145
|
+
|
|
146
|
+
// Edge type filter
|
|
147
|
+
if (activeEdgeTypes.size > 0) {
|
|
148
|
+
links = links.filter(l => activeEdgeTypes.has(l.relationType));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Focus depth (N-hop BFS from selected node)
|
|
152
|
+
if (selectedNode && focusDepth > 0) {
|
|
153
|
+
const adjMap = buildAdjacencyMap(links);
|
|
154
|
+
const reachable = getNeighborhood(selectedNode.id, adjMap, focusDepth);
|
|
155
|
+
nodes = nodes.filter(n => reachable.has(n.id));
|
|
156
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
157
|
+
links = links.filter(l => {
|
|
158
|
+
const sId = typeof l.source === 'object' ? l.source.id : l.source;
|
|
159
|
+
const tId = typeof l.target === 'object' ? l.target.id : l.target;
|
|
160
|
+
return nodeIds.has(sId) && nodeIds.has(tId);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { nodes, links };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Custom Node Painter ───────────────────────
|
|
168
|
+
function paintNode(node, ctx, globalScale) {
|
|
169
|
+
const radius = NODE_SIZES[node.type] || 6;
|
|
170
|
+
const color = NODE_COLORS[node.type] || '#6366f1';
|
|
171
|
+
const statusColor = STATUS_COLORS[node.status] || STATUS_COLORS.backlog;
|
|
172
|
+
const isHighlighted = highlightNodes.size === 0 || highlightNodes.has(node.id);
|
|
173
|
+
const isSelected = selectedNode && selectedNode.id === node.id;
|
|
174
|
+
|
|
175
|
+
// Selection glow
|
|
176
|
+
if (isSelected) {
|
|
177
|
+
ctx.save();
|
|
178
|
+
ctx.shadowColor = color;
|
|
179
|
+
ctx.shadowBlur = 15;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Status border ring
|
|
183
|
+
ctx.beginPath();
|
|
184
|
+
ctx.arc(node.x, node.y, radius + 1.5, 0, 2 * Math.PI);
|
|
185
|
+
ctx.fillStyle = isHighlighted ? statusColor : statusColor + '26';
|
|
186
|
+
ctx.fill();
|
|
187
|
+
|
|
188
|
+
// Main circle
|
|
189
|
+
ctx.beginPath();
|
|
190
|
+
ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI);
|
|
191
|
+
ctx.fillStyle = isHighlighted ? color : color + '26';
|
|
192
|
+
ctx.fill();
|
|
193
|
+
|
|
194
|
+
if (isSelected) {
|
|
195
|
+
ctx.restore();
|
|
196
|
+
// Selected ring
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
ctx.arc(node.x, node.y, radius + 3, 0, 2 * Math.PI);
|
|
199
|
+
ctx.strokeStyle = '#fff';
|
|
200
|
+
ctx.lineWidth = 1.5;
|
|
201
|
+
ctx.stroke();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Label (only at reasonable zoom)
|
|
205
|
+
if (globalScale > 0.5) {
|
|
206
|
+
const fontSize = Math.max(9, 11 / globalScale);
|
|
207
|
+
ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
208
|
+
ctx.textAlign = 'center';
|
|
209
|
+
ctx.textBaseline = 'top';
|
|
210
|
+
ctx.fillStyle = isHighlighted
|
|
211
|
+
? (theme === 'dark' ? '#e4e4e7' : '#212529')
|
|
212
|
+
: (theme === 'dark' ? '#71717a44' : '#21252944');
|
|
213
|
+
ctx.fillText(truncate(node.title || node.id, 25), node.x, node.y + radius + 3);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Custom Link Painter ───────────────────────
|
|
218
|
+
function paintLink(link, ctx) {
|
|
219
|
+
const sNode = link.source;
|
|
220
|
+
const tNode = link.target;
|
|
221
|
+
if (!sNode || !tNode || typeof sNode.x !== 'number') return;
|
|
222
|
+
|
|
223
|
+
const key = linkKey(link);
|
|
224
|
+
const isHighlighted = highlightLinks.size === 0 || highlightLinks.has(key);
|
|
225
|
+
const color = EDGE_COLORS[link.relationType] || '#9e9e9e44';
|
|
226
|
+
|
|
227
|
+
ctx.beginPath();
|
|
228
|
+
ctx.moveTo(sNode.x, sNode.y);
|
|
229
|
+
ctx.lineTo(tNode.x, tNode.y);
|
|
230
|
+
|
|
231
|
+
ctx.strokeStyle = isHighlighted ? color : color.slice(0, 7) + '1a';
|
|
232
|
+
ctx.lineWidth = isHighlighted ? 2 : 0.5;
|
|
233
|
+
|
|
234
|
+
// Dash pattern for certain edge types
|
|
235
|
+
const dash = EDGE_DASH[link.relationType];
|
|
236
|
+
if (dash) {
|
|
237
|
+
ctx.setLineDash(dash);
|
|
238
|
+
} else {
|
|
239
|
+
ctx.setLineDash([]);
|
|
240
|
+
}
|
|
241
|
+
ctx.stroke();
|
|
242
|
+
ctx.setLineDash([]);
|
|
243
|
+
|
|
244
|
+
// Arrow at target end
|
|
245
|
+
if (isHighlighted) {
|
|
246
|
+
const dx = tNode.x - sNode.x;
|
|
247
|
+
const dy = tNode.y - sNode.y;
|
|
248
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
249
|
+
if (dist < 1) return;
|
|
250
|
+
const targetRadius = NODE_SIZES[tNode.type] || 6;
|
|
251
|
+
const ratio = (dist - targetRadius - 2) / dist;
|
|
252
|
+
const ax = sNode.x + dx * ratio;
|
|
253
|
+
const ay = sNode.y + dy * ratio;
|
|
254
|
+
const angle = Math.atan2(dy, dx);
|
|
255
|
+
const arrowLen = 6;
|
|
256
|
+
const arrowWidth = 3;
|
|
257
|
+
|
|
258
|
+
ctx.beginPath();
|
|
259
|
+
ctx.moveTo(ax, ay);
|
|
260
|
+
ctx.lineTo(
|
|
261
|
+
ax - arrowLen * Math.cos(angle) + arrowWidth * Math.sin(angle),
|
|
262
|
+
ay - arrowLen * Math.sin(angle) - arrowWidth * Math.cos(angle)
|
|
263
|
+
);
|
|
264
|
+
ctx.lineTo(
|
|
265
|
+
ax - arrowLen * Math.cos(angle) - arrowWidth * Math.sin(angle),
|
|
266
|
+
ay - arrowLen * Math.sin(angle) + arrowWidth * Math.cos(angle)
|
|
267
|
+
);
|
|
268
|
+
ctx.closePath();
|
|
269
|
+
ctx.fillStyle = color;
|
|
270
|
+
ctx.fill();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Init ──────────────────────────────────────
|
|
275
|
+
function init(id) {
|
|
276
|
+
containerId = id;
|
|
277
|
+
ensureTooltip();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Render ────────────────────────────────────
|
|
281
|
+
function render(nodes, links) {
|
|
282
|
+
const container = document.getElementById(containerId);
|
|
283
|
+
if (!container) return;
|
|
284
|
+
|
|
285
|
+
// Store raw data
|
|
286
|
+
allNodes = nodes.map(n => ({ ...n }));
|
|
287
|
+
allLinks = links.map(l => ({ ...l }));
|
|
288
|
+
|
|
289
|
+
// Initialize all edge types if not set
|
|
290
|
+
if (activeEdgeTypes.size === 0) {
|
|
291
|
+
const types = new Set(links.map(l => l.relationType));
|
|
292
|
+
activeEdgeTypes = types;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Apply filters
|
|
296
|
+
const data = getFilteredData();
|
|
297
|
+
adjacencyMap = buildAdjacencyMap(data.links);
|
|
298
|
+
|
|
299
|
+
// Destroy previous graph
|
|
300
|
+
if (graph) {
|
|
301
|
+
graph._destructor && graph._destructor();
|
|
302
|
+
graph = null;
|
|
303
|
+
}
|
|
304
|
+
container.innerHTML = '';
|
|
305
|
+
|
|
306
|
+
if (data.nodes.length === 0) {
|
|
307
|
+
container.innerHTML = '<div class="empty-state"><p>No graph data. Import a PRD to get started.</p></div>';
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const bgColor = theme === 'dark' ? '#1a1b1e' : '#f8f9fa';
|
|
312
|
+
|
|
313
|
+
// Create force graph instance
|
|
314
|
+
graph = ForceGraph()(container)
|
|
315
|
+
.graphData({ nodes: data.nodes, links: data.links })
|
|
316
|
+
.backgroundColor(bgColor)
|
|
317
|
+
.nodeId('id')
|
|
318
|
+
.linkSource('source')
|
|
319
|
+
.linkTarget('target')
|
|
320
|
+
.nodeCanvasObject((node, ctx, globalScale) => paintNode(node, ctx, globalScale))
|
|
321
|
+
.nodePointerAreaPaint((node, color, ctx) => {
|
|
322
|
+
const r = NODE_SIZES[node.type] || 6;
|
|
323
|
+
ctx.beginPath();
|
|
324
|
+
ctx.arc(node.x, node.y, r + 2, 0, 2 * Math.PI);
|
|
325
|
+
ctx.fillStyle = color;
|
|
326
|
+
ctx.fill();
|
|
327
|
+
})
|
|
328
|
+
.linkCanvasObject((link, ctx) => paintLink(link, ctx))
|
|
329
|
+
.linkCanvasObjectMode(() => 'replace')
|
|
330
|
+
.onNodeHover((node, prevNode) => {
|
|
331
|
+
container.style.cursor = node ? 'pointer' : 'default';
|
|
332
|
+
hoveredNode = node;
|
|
333
|
+
|
|
334
|
+
if (!node) {
|
|
335
|
+
highlightNodes = new Set();
|
|
336
|
+
highlightLinks = new Set();
|
|
337
|
+
hideTooltip();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const connNodes = new Set([node.id]);
|
|
342
|
+
const connLinks = new Set();
|
|
343
|
+
const neighbors = adjacencyMap.get(node.id);
|
|
344
|
+
if (neighbors) {
|
|
345
|
+
for (const { nodeId, link } of neighbors) {
|
|
346
|
+
connNodes.add(nodeId);
|
|
347
|
+
connLinks.add(linkKey(link));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
highlightNodes = connNodes;
|
|
351
|
+
highlightLinks = connLinks;
|
|
352
|
+
})
|
|
353
|
+
.onNodeClick((node) => {
|
|
354
|
+
if (!node) return;
|
|
355
|
+
selectedNode = node;
|
|
356
|
+
// Highlight selected node row in table
|
|
357
|
+
highlightTableRow(node.id);
|
|
358
|
+
// Dispatch event for NodeDetail panel
|
|
359
|
+
document.dispatchEvent(new CustomEvent('node-clicked', { detail: { nodeId: node.id } }));
|
|
360
|
+
// Re-render if focus depth is active
|
|
361
|
+
if (focusDepth > 0) {
|
|
362
|
+
refreshGraph();
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
.d3VelocityDecay(0.4)
|
|
366
|
+
.warmupTicks(100)
|
|
367
|
+
.cooldownTime(3000);
|
|
368
|
+
|
|
369
|
+
// Configure forces
|
|
370
|
+
try {
|
|
371
|
+
graph.d3Force('charge').strength(-120);
|
|
372
|
+
graph.d3Force('center').strength(0.03);
|
|
373
|
+
graph.d3Force('link').distance(link => {
|
|
374
|
+
switch (link.relationType) {
|
|
375
|
+
case 'parent_of': return 40;
|
|
376
|
+
case 'depends_on': return 60;
|
|
377
|
+
case 'blocks': return 50;
|
|
378
|
+
default: return 80;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
} catch (e) {
|
|
382
|
+
// Forces may not be ready yet
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Track mouse for tooltip positioning
|
|
386
|
+
container.addEventListener('mousemove', (e) => {
|
|
387
|
+
if (hoveredNode) {
|
|
388
|
+
showTooltip(hoveredNode, e);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Refresh (re-render with same data) ────────
|
|
394
|
+
function refreshGraph() {
|
|
395
|
+
if (!graph) return;
|
|
396
|
+
const data = getFilteredData();
|
|
397
|
+
adjacencyMap = buildAdjacencyMap(data.links);
|
|
398
|
+
graph.graphData({ nodes: data.nodes, links: data.links });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Table row highlight ───────────────────────
|
|
402
|
+
function highlightTableRow(nodeId) {
|
|
403
|
+
document.querySelectorAll('#node-table-body .node-row').forEach(row => {
|
|
404
|
+
row.classList.toggle('selected-row', row.dataset.id === nodeId);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Zoom Controls ─────────────────────────────
|
|
409
|
+
function zoomIn() {
|
|
410
|
+
if (!graph) return;
|
|
411
|
+
const currentZoom = graph.zoom();
|
|
412
|
+
graph.zoom(currentZoom * 1.4, 300);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function zoomOut() {
|
|
416
|
+
if (!graph) return;
|
|
417
|
+
const currentZoom = graph.zoom();
|
|
418
|
+
graph.zoom(currentZoom / 1.4, 300);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function zoomFit() {
|
|
422
|
+
if (!graph) return;
|
|
423
|
+
graph.zoomToFit(400, 40);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Set Theme ─────────────────────────────────
|
|
427
|
+
function setTheme(newTheme) {
|
|
428
|
+
theme = newTheme;
|
|
429
|
+
if (graph) {
|
|
430
|
+
graph.backgroundColor(theme === 'dark' ? '#1a1b1e' : '#f8f9fa');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Set Filters ───────────────────────────────
|
|
435
|
+
function setFilters({ edgeTypes, depth }) {
|
|
436
|
+
if (edgeTypes !== undefined) {
|
|
437
|
+
activeEdgeTypes = new Set(edgeTypes);
|
|
438
|
+
}
|
|
439
|
+
if (depth !== undefined) {
|
|
440
|
+
focusDepth = depth;
|
|
441
|
+
}
|
|
442
|
+
refreshGraph();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Resize ────────────────────────────────────
|
|
446
|
+
function resize() {
|
|
447
|
+
if (!graph) return;
|
|
448
|
+
const container = document.getElementById(containerId);
|
|
449
|
+
if (container) {
|
|
450
|
+
graph.width(container.clientWidth);
|
|
451
|
+
graph.height(container.clientHeight);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── Select Node (programmatic) ────────────────
|
|
456
|
+
function selectNode(nodeId) {
|
|
457
|
+
if (!graph) return;
|
|
458
|
+
const node = allNodes.find(n => n.id === nodeId);
|
|
459
|
+
if (node) {
|
|
460
|
+
selectedNode = node;
|
|
461
|
+
highlightTableRow(nodeId);
|
|
462
|
+
if (focusDepth > 0) refreshGraph();
|
|
463
|
+
graph.centerAt(node.x, node.y, 500);
|
|
464
|
+
graph.zoom(2, 500);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getSelectedNode() {
|
|
469
|
+
return selectedNode;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── Destroy ───────────────────────────────────
|
|
473
|
+
function destroy() {
|
|
474
|
+
if (graph) {
|
|
475
|
+
graph._destructor && graph._destructor();
|
|
476
|
+
graph = null;
|
|
477
|
+
}
|
|
478
|
+
if (tooltipEl) {
|
|
479
|
+
tooltipEl.remove();
|
|
480
|
+
tooltipEl = null;
|
|
481
|
+
}
|
|
482
|
+
hoveredNode = null;
|
|
483
|
+
selectedNode = null;
|
|
484
|
+
highlightNodes = new Set();
|
|
485
|
+
highlightLinks = new Set();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Get all unique edge types from data ───────
|
|
489
|
+
function getEdgeTypes() {
|
|
490
|
+
return [...new Set(allLinks.map(l => l.relationType))];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
init, render, setTheme, resize, destroy,
|
|
495
|
+
setFilters, selectNode, getSelectedNode,
|
|
496
|
+
zoomIn, zoomOut, zoomFit, getEdgeTypes,
|
|
497
|
+
};
|
|
498
|
+
})();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid.js graph renderer
|
|
3
|
+
*/
|
|
4
|
+
const GraphRenderer = (() => {
|
|
5
|
+
let initialized = false;
|
|
6
|
+
|
|
7
|
+
function init(theme = 'default') {
|
|
8
|
+
mermaid.initialize({
|
|
9
|
+
startOnLoad: false,
|
|
10
|
+
theme: theme,
|
|
11
|
+
flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis' },
|
|
12
|
+
securityLevel: 'loose',
|
|
13
|
+
});
|
|
14
|
+
initialized = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function render(container, mermaidCode) {
|
|
18
|
+
if (!initialized) init();
|
|
19
|
+
|
|
20
|
+
if (!mermaidCode || mermaidCode.trim().length === 0) {
|
|
21
|
+
container.innerHTML = '<div class="empty-state"><p>No graph data to display.</p></div>';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Clear previous
|
|
27
|
+
container.innerHTML = '';
|
|
28
|
+
|
|
29
|
+
const id = 'mermaid-' + Date.now();
|
|
30
|
+
const { svg } = await mermaid.render(id, mermaidCode);
|
|
31
|
+
container.innerHTML = svg;
|
|
32
|
+
|
|
33
|
+
// Make nodes clickable
|
|
34
|
+
const svgEl = container.querySelector('svg');
|
|
35
|
+
if (svgEl) {
|
|
36
|
+
svgEl.querySelectorAll('.node').forEach(node => {
|
|
37
|
+
node.style.cursor = 'pointer';
|
|
38
|
+
node.addEventListener('click', () => {
|
|
39
|
+
const nodeId = node.id?.replace(/^flowchart-/, '').replace(/-\d+$/, '');
|
|
40
|
+
if (nodeId) {
|
|
41
|
+
document.dispatchEvent(new CustomEvent('node-clicked', { detail: { nodeId } }));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
container.innerHTML = `<div class="error-state"><p>Render error: ${err.message}</p><pre>${escapeHtml(mermaidCode)}</pre></div>`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setTheme(theme) {
|
|
52
|
+
init(theme);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeHtml(text) {
|
|
56
|
+
const div = document.createElement('div');
|
|
57
|
+
div.textContent = text;
|
|
58
|
+
return div.innerHTML;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { init, render, setTheme };
|
|
62
|
+
})();
|