alita-sdk 0.3.257__py3-none-any.whl → 0.3.584__py3-none-any.whl
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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +215 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3794 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +11 -0
- alita_sdk/configurations/ado.py +148 -2
- alita_sdk/configurations/azure_search.py +1 -1
- alita_sdk/configurations/bigquery.py +1 -1
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/browser.py +18 -0
- alita_sdk/configurations/carrier.py +19 -0
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/delta_lake.py +1 -1
- alita_sdk/configurations/figma.py +76 -5
- alita_sdk/configurations/github.py +65 -1
- alita_sdk/configurations/gitlab.py +81 -0
- alita_sdk/configurations/google_places.py +17 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +323 -0
- alita_sdk/configurations/postman.py +1 -1
- alita_sdk/configurations/qtest.py +72 -3
- alita_sdk/configurations/report_portal.py +115 -0
- alita_sdk/configurations/salesforce.py +19 -0
- alita_sdk/configurations/service_now.py +1 -12
- alita_sdk/configurations/sharepoint.py +167 -0
- alita_sdk/configurations/sonar.py +18 -0
- alita_sdk/configurations/sql.py +20 -0
- alita_sdk/configurations/testio.py +101 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +94 -1
- alita_sdk/configurations/zephyr_enterprise.py +94 -1
- alita_sdk/configurations/zephyr_essential.py +95 -0
- alita_sdk/runtime/clients/artifact.py +21 -4
- alita_sdk/runtime/clients/client.py +458 -67
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +352 -0
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +183 -43
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
- alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
- alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
- alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
- alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
- alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
- alita_sdk/runtime/langchain/langraph_agent.py +493 -105
- alita_sdk/runtime/langchain/utils.py +118 -8
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +28 -0
- alita_sdk/runtime/toolkits/application.py +14 -4
- alita_sdk/runtime/toolkits/artifact.py +25 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +782 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +11 -6
- alita_sdk/runtime/toolkits/tools.py +314 -70
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +24 -0
- alita_sdk/runtime/tools/application.py +16 -4
- alita_sdk/runtime/tools/artifact.py +367 -33
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +100 -4
- alita_sdk/runtime/tools/graph.py +81 -0
- alita_sdk/runtime/tools/image_generation.py +218 -0
- alita_sdk/runtime/tools/llm.py +1032 -177
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -1
- alita_sdk/runtime/tools/sandbox.py +375 -0
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +69 -65
- alita_sdk/runtime/tools/vectorstore_base.py +163 -90
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/streamlit.py +41 -14
- alita_sdk/runtime/utils/toolkit_utils.py +28 -9
- alita_sdk/runtime/utils/utils.py +48 -0
- alita_sdk/tools/__init__.py +135 -37
- alita_sdk/tools/ado/__init__.py +2 -2
- alita_sdk/tools/ado/repos/__init__.py +16 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +27 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +28 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +28 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +14 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +28 -19
- alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
- alita_sdk/tools/browser/__init__.py +41 -16
- alita_sdk/tools/browser/crawler.py +3 -1
- alita_sdk/tools/browser/utils.py +15 -6
- alita_sdk/tools/carrier/__init__.py +18 -17
- alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
- alita_sdk/tools/carrier/excel_reporter.py +8 -4
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/codeparser.py +1 -1
- alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +12 -7
- alita_sdk/tools/cloud/azure/__init__.py +12 -7
- alita_sdk/tools/cloud/gcp/__init__.py +12 -7
- alita_sdk/tools/cloud/k8s/__init__.py +12 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +21 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +22 -14
- alita_sdk/tools/confluence/api_wrapper.py +197 -58
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +546 -64
- alita_sdk/tools/figma/__init__.py +60 -11
- alita_sdk/tools/figma/api_wrapper.py +1400 -167
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +18 -17
- alita_sdk/tools/github/api_wrapper.py +9 -26
- alita_sdk/tools/github/github_client.py +81 -12
- alita_sdk/tools/github/schemas.py +2 -1
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +19 -13
- alita_sdk/tools/gitlab/api_wrapper.py +256 -80
- alita_sdk/tools/gitlab_org/__init__.py +14 -10
- alita_sdk/tools/google/bigquery/__init__.py +14 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +21 -11
- alita_sdk/tools/jira/__init__.py +22 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +38 -14
- alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +491 -106
- alita_sdk/tools/openapi/api_wrapper.py +1357 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +40 -45
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/postman/api_wrapper.py +19 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +11 -10
- alita_sdk/tools/qtest/__init__.py +22 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +13 -10
- alita_sdk/tools/report_portal/__init__.py +23 -16
- alita_sdk/tools/salesforce/__init__.py +22 -16
- alita_sdk/tools/servicenow/__init__.py +21 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +17 -14
- alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +13 -8
- alita_sdk/tools/sql/__init__.py +22 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +21 -13
- alita_sdk/tools/testrail/__init__.py +13 -11
- alita_sdk/tools/testrail/api_wrapper.py +214 -46
- alita_sdk/tools/utils/__init__.py +28 -4
- alita_sdk/tools/utils/content_parser.py +241 -55
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +18 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +12 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +16 -10
- alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
- alita_sdk/tools/zephyr_essential/client.py +6 -4
- alita_sdk/tools/zephyr_scale/__init__.py +13 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +12 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
- alita_sdk-0.3.584.dist-info/RECORD +452 -0
- alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
- alita_sdk/tools/bitbucket/tools.py +0 -304
- alita_sdk-0.3.257.dist-info/RECORD +0 -343
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge Graph Visualization Utility.
|
|
3
|
+
|
|
4
|
+
Generates an interactive HTML visualization of the knowledge graph
|
|
5
|
+
using D3.js force-directed graph layout.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# From CLI
|
|
9
|
+
alita inventory visualize --graph ./graph.json --output ./graph.html
|
|
10
|
+
|
|
11
|
+
# From Python
|
|
12
|
+
from alita_sdk.community.inventory.visualize import generate_visualization
|
|
13
|
+
generate_visualization("./graph.json", "./graph.html")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Dict, Any, List
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Color palette for entity types based on ENTITY_TAXONOMY layers
|
|
24
|
+
# Each layer has its own distinct hue range to prevent overlap
|
|
25
|
+
TYPE_COLORS = {
|
|
26
|
+
# =========================================================================
|
|
27
|
+
# PRODUCT LAYER - Red/Pink/Orange hues (0-30° and 330-360°)
|
|
28
|
+
# =========================================================================
|
|
29
|
+
"epic": "#D32F2F", # Deep Red
|
|
30
|
+
"feature": "#E53935", # Red
|
|
31
|
+
"user_story": "#F44336", # Lighter Red
|
|
32
|
+
"screen": "#FF5722", # Deep Orange
|
|
33
|
+
"ux_flow": "#FF7043", # Orange
|
|
34
|
+
"ui_component": "#FF8A65", # Light Orange
|
|
35
|
+
"ui_field": "#FFAB91", # Peach
|
|
36
|
+
|
|
37
|
+
# =========================================================================
|
|
38
|
+
# DOMAIN LAYER - Green hues (90-150°)
|
|
39
|
+
# =========================================================================
|
|
40
|
+
"domain_entity": "#2E7D32", # Dark Green
|
|
41
|
+
"attribute": "#388E3C", # Green
|
|
42
|
+
"business_rule": "#43A047", # Medium Green
|
|
43
|
+
"business_event": "#4CAF50", # Light Green
|
|
44
|
+
"glossary_term": "#66BB6A", # Pale Green
|
|
45
|
+
"workflow": "#81C784", # Very Light Green
|
|
46
|
+
|
|
47
|
+
# =========================================================================
|
|
48
|
+
# SERVICE LAYER - Cyan/Teal hues (160-200°)
|
|
49
|
+
# =========================================================================
|
|
50
|
+
"service": "#00695C", # Dark Teal
|
|
51
|
+
"api": "#00796B", # Teal
|
|
52
|
+
"endpoint": "#00897B", # Medium Teal
|
|
53
|
+
"payload": "#009688", # Light Teal
|
|
54
|
+
"integration": "#26A69A", # Pale Teal
|
|
55
|
+
|
|
56
|
+
# =========================================================================
|
|
57
|
+
# CODE LAYER - Blue hues (200-240°)
|
|
58
|
+
# =========================================================================
|
|
59
|
+
"module": "#1565C0", # Dark Blue
|
|
60
|
+
"class": "#1976D2", # Blue
|
|
61
|
+
"function": "#1E88E5", # Medium Blue
|
|
62
|
+
"interface": "#2196F3", # Light Blue
|
|
63
|
+
"constant": "#42A5F5", # Pale Blue
|
|
64
|
+
"configuration": "#64B5F6", # Very Light Blue
|
|
65
|
+
|
|
66
|
+
# =========================================================================
|
|
67
|
+
# DATA LAYER - Brown/Amber hues (30-60°)
|
|
68
|
+
# =========================================================================
|
|
69
|
+
"database": "#E65100", # Dark Orange/Brown
|
|
70
|
+
"table": "#EF6C00", # Orange
|
|
71
|
+
"column": "#F57C00", # Medium Orange
|
|
72
|
+
"constraint": "#FB8C00", # Light Orange
|
|
73
|
+
"index": "#FF9800", # Amber
|
|
74
|
+
"migration": "#FFA726", # Light Amber
|
|
75
|
+
"enum": "#FFB74D", # Pale Amber
|
|
76
|
+
|
|
77
|
+
# =========================================================================
|
|
78
|
+
# TESTING LAYER - Purple/Violet hues (260-290°)
|
|
79
|
+
# =========================================================================
|
|
80
|
+
"test_suite": "#4527A0", # Deep Purple
|
|
81
|
+
"test_case": "#512DA8", # Dark Purple
|
|
82
|
+
"test_step": "#5E35B1", # Purple
|
|
83
|
+
"assertion": "#673AB7", # Medium Purple
|
|
84
|
+
"test_data": "#7E57C2", # Light Purple
|
|
85
|
+
"defect": "#B71C1C", # Dark Red (stands out - critical)
|
|
86
|
+
"incident": "#C62828", # Red (stands out - critical)
|
|
87
|
+
|
|
88
|
+
# =========================================================================
|
|
89
|
+
# DELIVERY LAYER - Lime/Yellow-Green hues (60-90°)
|
|
90
|
+
# =========================================================================
|
|
91
|
+
"release": "#827717", # Dark Lime
|
|
92
|
+
"sprint": "#9E9D24", # Olive
|
|
93
|
+
"commit": "#AFB42B", # Lime
|
|
94
|
+
"pull_request": "#C0CA33", # Light Lime
|
|
95
|
+
"ticket": "#CDDC39", # Yellow-Lime
|
|
96
|
+
"deployment": "#D4E157", # Pale Lime
|
|
97
|
+
|
|
98
|
+
# =========================================================================
|
|
99
|
+
# ORGANIZATION LAYER - Magenta/Pink hues (290-330°)
|
|
100
|
+
# =========================================================================
|
|
101
|
+
"team": "#AD1457", # Dark Magenta
|
|
102
|
+
"owner": "#C2185B", # Magenta
|
|
103
|
+
"stakeholder": "#D81B60", # Pink-Magenta
|
|
104
|
+
"repository": "#E91E63", # Pink
|
|
105
|
+
"documentation": "#EC407A", # Light Pink
|
|
106
|
+
|
|
107
|
+
# =========================================================================
|
|
108
|
+
# GENERIC/COMMON TYPES - Distinct colors for frequently used types
|
|
109
|
+
# =========================================================================
|
|
110
|
+
# Documents & Content
|
|
111
|
+
"document": "#5C6BC0", # Indigo
|
|
112
|
+
"section": "#7986CB", # Light Indigo
|
|
113
|
+
"chapter": "#9FA8DA", # Pale Indigo
|
|
114
|
+
"paragraph": "#C5CAE9", # Very Light Indigo
|
|
115
|
+
"text": "#E8EAF6", # Faint Indigo
|
|
116
|
+
|
|
117
|
+
# Process & Actions
|
|
118
|
+
"process": "#00ACC1", # Cyan
|
|
119
|
+
"action": "#26C6DA", # Light Cyan
|
|
120
|
+
"step": "#4DD0E1", # Pale Cyan
|
|
121
|
+
"task": "#80DEEA", # Very Light Cyan
|
|
122
|
+
"activity": "#B2EBF2", # Faint Cyan
|
|
123
|
+
|
|
124
|
+
# Tools & Scripts
|
|
125
|
+
"tool": "#8D6E63", # Brown
|
|
126
|
+
"toolkit": "#A1887F", # Light Brown
|
|
127
|
+
"script": "#BCAAA4", # Pale Brown
|
|
128
|
+
"utility": "#D7CCC8", # Very Light Brown
|
|
129
|
+
"mcp_server": "#6D4C41", # Dark Brown (MCP)
|
|
130
|
+
"mcp_tool": "#795548", # Medium Brown (MCP)
|
|
131
|
+
"mcp_resource": "#8D6E63", # Brown (MCP)
|
|
132
|
+
"connector": "#5D4037", # Deep Brown
|
|
133
|
+
|
|
134
|
+
# Structure & Organization
|
|
135
|
+
"structure": "#78909C", # Blue Grey
|
|
136
|
+
"component": "#90A4AE", # Light Blue Grey
|
|
137
|
+
"element": "#B0BEC5", # Pale Blue Grey
|
|
138
|
+
"item": "#CFD8DC", # Very Light Blue Grey
|
|
139
|
+
|
|
140
|
+
# Resources & References
|
|
141
|
+
"resource": "#546E7A", # Dark Blue Grey
|
|
142
|
+
"reference": "#607D8B", # Blue Grey
|
|
143
|
+
"link": "#78909C", # Light Blue Grey
|
|
144
|
+
|
|
145
|
+
# Requirements & Specs
|
|
146
|
+
"requirement": "#F06292", # Pink
|
|
147
|
+
"specification": "#F48FB1", # Light Pink
|
|
148
|
+
"criteria": "#F8BBD9", # Pale Pink
|
|
149
|
+
|
|
150
|
+
# Issues & Problems
|
|
151
|
+
"issue": "#EF5350", # Red
|
|
152
|
+
"bug": "#E57373", # Light Red
|
|
153
|
+
"error": "#EF9A9A", # Pale Red
|
|
154
|
+
"problem": "#FFCDD2", # Very Light Red
|
|
155
|
+
"troubleshooting": "#FFEBEE", # Faint Red
|
|
156
|
+
|
|
157
|
+
# States & Status
|
|
158
|
+
"state": "#AB47BC", # Purple
|
|
159
|
+
"status": "#BA68C8", # Light Purple
|
|
160
|
+
"condition": "#CE93D8", # Pale Purple
|
|
161
|
+
|
|
162
|
+
# Misc common types
|
|
163
|
+
"entity": "#26A69A", # Teal
|
|
164
|
+
"object": "#4DB6AC", # Light Teal
|
|
165
|
+
"concept": "#80CBC4", # Pale Teal
|
|
166
|
+
"idea": "#B2DFDB", # Very Light Teal
|
|
167
|
+
|
|
168
|
+
"checklist": "#FFD54F", # Amber
|
|
169
|
+
"list": "#FFE082", # Light Amber
|
|
170
|
+
"collection": "#FFECB3", # Pale Amber
|
|
171
|
+
|
|
172
|
+
"project": "#7B1FA2", # Deep Purple
|
|
173
|
+
"program": "#8E24AA", # Purple
|
|
174
|
+
"initiative": "#9C27B0", # Light Purple
|
|
175
|
+
|
|
176
|
+
"platform": "#0097A7", # Dark Cyan
|
|
177
|
+
"system": "#00ACC1", # Cyan
|
|
178
|
+
"application": "#00BCD4", # Light Cyan
|
|
179
|
+
"app": "#26C6DA", # Pale Cyan
|
|
180
|
+
|
|
181
|
+
"value": "#689F38", # Light Green
|
|
182
|
+
"property": "#7CB342", # Pale Green
|
|
183
|
+
"setting": "#8BC34A", # Very Light Green
|
|
184
|
+
|
|
185
|
+
"agent": "#FF7043", # Deep Orange
|
|
186
|
+
"agent_type": "#FF8A65", # Orange
|
|
187
|
+
"bot": "#FFAB91", # Light Orange
|
|
188
|
+
|
|
189
|
+
"category": "#5D4037", # Brown
|
|
190
|
+
"type": "#6D4C41", # Light Brown
|
|
191
|
+
"kind": "#795548", # Medium Brown
|
|
192
|
+
"group": "#8D6E63", # Pale Brown
|
|
193
|
+
|
|
194
|
+
"file": "#455A64", # Dark Blue Grey
|
|
195
|
+
"folder": "#546E7A", # Blue Grey
|
|
196
|
+
"directory": "#607D8B", # Light Blue Grey
|
|
197
|
+
"path": "#78909C", # Pale Blue Grey
|
|
198
|
+
|
|
199
|
+
"method": "#1E88E5", # Blue (code related)
|
|
200
|
+
"parameter": "#42A5F5", # Light Blue
|
|
201
|
+
"argument": "#64B5F6", # Pale Blue
|
|
202
|
+
"variable": "#90CAF9", # Very Light Blue
|
|
203
|
+
|
|
204
|
+
"event": "#7E57C2", # Purple
|
|
205
|
+
"trigger": "#9575CD", # Light Purple
|
|
206
|
+
"handler": "#B39DDB", # Pale Purple
|
|
207
|
+
"callback": "#D1C4E9", # Very Light Purple
|
|
208
|
+
|
|
209
|
+
"rule": "#43A047", # Green (business related)
|
|
210
|
+
"policy": "#66BB6A", # Light Green
|
|
211
|
+
"guideline": "#81C784", # Pale Green
|
|
212
|
+
"standard": "#A5D6A7", # Very Light Green
|
|
213
|
+
|
|
214
|
+
"user": "#EC407A", # Pink (organization related)
|
|
215
|
+
"role": "#F06292", # Light Pink
|
|
216
|
+
"permission": "#F48FB1", # Pale Pink
|
|
217
|
+
"access": "#F8BBD9", # Very Light Pink
|
|
218
|
+
|
|
219
|
+
# Default fallback
|
|
220
|
+
"default": "#9E9E9E", # Grey
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Relation type colors
|
|
224
|
+
RELATION_COLORS = {
|
|
225
|
+
"contains": "#4CAF50",
|
|
226
|
+
"extends": "#2196F3",
|
|
227
|
+
"implements": "#9C27B0",
|
|
228
|
+
"imports": "#FF9800",
|
|
229
|
+
"calls": "#F44336",
|
|
230
|
+
"triggers": "#E91E63",
|
|
231
|
+
"depends_on": "#673AB7",
|
|
232
|
+
"uses": "#00BCD4",
|
|
233
|
+
"stores_in": "#795548",
|
|
234
|
+
"reads_from": "#607D8B",
|
|
235
|
+
"transforms": "#FFC107",
|
|
236
|
+
"maps_to": "#8BC34A",
|
|
237
|
+
"default": "#9E9E9E",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
242
|
+
<html lang="en">
|
|
243
|
+
<head>
|
|
244
|
+
<meta charset="UTF-8">
|
|
245
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
246
|
+
<title>Knowledge Graph Visualization</title>
|
|
247
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
248
|
+
<style>
|
|
249
|
+
* {
|
|
250
|
+
margin: 0;
|
|
251
|
+
padding: 0;
|
|
252
|
+
box-sizing: border-box;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
body {
|
|
256
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
257
|
+
background: #1a1a2e;
|
|
258
|
+
color: #eee;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#container {
|
|
263
|
+
display: flex;
|
|
264
|
+
height: 100vh;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#graph-container {
|
|
268
|
+
flex: 1;
|
|
269
|
+
position: relative;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#sidebar {
|
|
273
|
+
width: 350px;
|
|
274
|
+
background: #16213e;
|
|
275
|
+
border-left: 1px solid #0f3460;
|
|
276
|
+
padding: 20px;
|
|
277
|
+
overflow-y: auto;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#controls {
|
|
281
|
+
position: absolute;
|
|
282
|
+
top: 20px;
|
|
283
|
+
left: 20px;
|
|
284
|
+
background: rgba(22, 33, 62, 0.95);
|
|
285
|
+
padding: 15px;
|
|
286
|
+
border-radius: 8px;
|
|
287
|
+
border: 1px solid #0f3460;
|
|
288
|
+
z-index: 100;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#controls h3 {
|
|
292
|
+
margin-bottom: 10px;
|
|
293
|
+
color: #e94560;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#controls label {
|
|
297
|
+
display: block;
|
|
298
|
+
margin: 8px 0;
|
|
299
|
+
font-size: 13px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#controls input[type="range"] {
|
|
303
|
+
width: 150px;
|
|
304
|
+
margin-left: 10px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#controls input[type="text"] {
|
|
308
|
+
width: 200px;
|
|
309
|
+
padding: 5px 10px;
|
|
310
|
+
border: 1px solid #0f3460;
|
|
311
|
+
background: #1a1a2e;
|
|
312
|
+
color: #eee;
|
|
313
|
+
border-radius: 4px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#search-results {
|
|
317
|
+
margin-top: 10px;
|
|
318
|
+
max-height: 200px;
|
|
319
|
+
overflow-y: auto;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.search-result {
|
|
323
|
+
padding: 5px 10px;
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
border-radius: 4px;
|
|
326
|
+
margin: 2px 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.search-result:hover {
|
|
330
|
+
background: #0f3460;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#sidebar h2 {
|
|
334
|
+
color: #e94560;
|
|
335
|
+
margin-bottom: 15px;
|
|
336
|
+
border-bottom: 1px solid #0f3460;
|
|
337
|
+
padding-bottom: 10px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#sidebar h3 {
|
|
341
|
+
color: #eee;
|
|
342
|
+
margin: 15px 0 10px;
|
|
343
|
+
font-size: 14px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#entity-details {
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
line-height: 1.6;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.detail-label {
|
|
352
|
+
color: #888;
|
|
353
|
+
font-size: 11px;
|
|
354
|
+
text-transform: uppercase;
|
|
355
|
+
margin-top: 10px;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.detail-value {
|
|
359
|
+
color: #eee;
|
|
360
|
+
word-break: break-word;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.entity-type-badge {
|
|
364
|
+
display: inline-block;
|
|
365
|
+
padding: 3px 8px;
|
|
366
|
+
border-radius: 4px;
|
|
367
|
+
font-size: 11px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
text-transform: uppercase;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.citation-link {
|
|
373
|
+
color: #64b5f6;
|
|
374
|
+
text-decoration: none;
|
|
375
|
+
font-family: monospace;
|
|
376
|
+
font-size: 12px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.citation-link:hover {
|
|
380
|
+
text-decoration: underline;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Context Menu */
|
|
384
|
+
#context-menu {
|
|
385
|
+
display: none;
|
|
386
|
+
position: fixed;
|
|
387
|
+
background: #1a1a2e;
|
|
388
|
+
border: 1px solid #0f3460;
|
|
389
|
+
border-radius: 6px;
|
|
390
|
+
padding: 5px 0;
|
|
391
|
+
min-width: 180px;
|
|
392
|
+
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
|
|
393
|
+
z-index: 1000;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.context-menu-item {
|
|
397
|
+
padding: 8px 15px;
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
color: #eee;
|
|
400
|
+
font-size: 13px;
|
|
401
|
+
display: flex;
|
|
402
|
+
align-items: center;
|
|
403
|
+
gap: 8px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.context-menu-item:hover {
|
|
407
|
+
background: #0f3460;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.context-menu-item.danger {
|
|
411
|
+
color: #e94560;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.context-menu-separator {
|
|
415
|
+
height: 1px;
|
|
416
|
+
background: #0f3460;
|
|
417
|
+
margin: 5px 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Focus Mode Styling */
|
|
421
|
+
.node.faded circle {
|
|
422
|
+
opacity: 0.1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.node.faded text {
|
|
426
|
+
opacity: 0.1;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.link.faded {
|
|
430
|
+
opacity: 0.05;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.node.focused circle {
|
|
434
|
+
stroke: #e94560;
|
|
435
|
+
stroke-width: 3px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.node.neighbor circle {
|
|
439
|
+
stroke: #64b5f6;
|
|
440
|
+
stroke-width: 2px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Focus Mode Banner */
|
|
444
|
+
#focus-banner {
|
|
445
|
+
display: none;
|
|
446
|
+
position: fixed;
|
|
447
|
+
top: 10px;
|
|
448
|
+
left: 50%;
|
|
449
|
+
transform: translateX(-50%);
|
|
450
|
+
background: #e94560;
|
|
451
|
+
color: white;
|
|
452
|
+
padding: 8px 20px;
|
|
453
|
+
border-radius: 20px;
|
|
454
|
+
font-size: 13px;
|
|
455
|
+
z-index: 100;
|
|
456
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#focus-banner button {
|
|
460
|
+
background: white;
|
|
461
|
+
color: #e94560;
|
|
462
|
+
border: none;
|
|
463
|
+
padding: 3px 10px;
|
|
464
|
+
border-radius: 10px;
|
|
465
|
+
margin-left: 10px;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
font-weight: 600;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#legend {
|
|
471
|
+
margin-top: 20px;
|
|
472
|
+
padding-top: 15px;
|
|
473
|
+
border-top: 1px solid #0f3460;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.legend-item {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
margin: 5px 0;
|
|
480
|
+
font-size: 12px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.legend-color {
|
|
484
|
+
width: 12px;
|
|
485
|
+
height: 12px;
|
|
486
|
+
border-radius: 50%;
|
|
487
|
+
margin-right: 8px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#stats {
|
|
491
|
+
background: #0f3460;
|
|
492
|
+
padding: 10px 15px;
|
|
493
|
+
border-radius: 6px;
|
|
494
|
+
margin-bottom: 15px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#stats span {
|
|
498
|
+
display: inline-block;
|
|
499
|
+
margin-right: 15px;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.stat-value {
|
|
503
|
+
color: #e94560;
|
|
504
|
+
font-weight: 600;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
svg {
|
|
508
|
+
width: 100%;
|
|
509
|
+
height: 100%;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.node {
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.node circle {
|
|
517
|
+
stroke: #fff;
|
|
518
|
+
stroke-width: 1.5px;
|
|
519
|
+
transition: r 0.2s;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.node:hover circle {
|
|
523
|
+
stroke-width: 3px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.node.selected circle {
|
|
527
|
+
stroke: #e94560;
|
|
528
|
+
stroke-width: 3px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.node text {
|
|
532
|
+
font-size: 10px;
|
|
533
|
+
fill: #ccc;
|
|
534
|
+
pointer-events: none;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.link {
|
|
538
|
+
stroke-opacity: 0.4;
|
|
539
|
+
stroke-width: 1.5px;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.link:hover {
|
|
543
|
+
stroke-opacity: 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.link-label {
|
|
547
|
+
font-size: 9px;
|
|
548
|
+
fill: #888;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Tooltip */
|
|
552
|
+
.tooltip {
|
|
553
|
+
position: absolute;
|
|
554
|
+
background: rgba(22, 33, 62, 0.95);
|
|
555
|
+
border: 1px solid #0f3460;
|
|
556
|
+
border-radius: 6px;
|
|
557
|
+
padding: 10px;
|
|
558
|
+
font-size: 12px;
|
|
559
|
+
pointer-events: none;
|
|
560
|
+
z-index: 1000;
|
|
561
|
+
max-width: 300px;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.tooltip h4 {
|
|
565
|
+
color: #e94560;
|
|
566
|
+
margin-bottom: 5px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* Filter panel */
|
|
570
|
+
#type-filters {
|
|
571
|
+
max-height: 400px;
|
|
572
|
+
overflow-y: auto;
|
|
573
|
+
margin-top: 10px;
|
|
574
|
+
border: 1px solid #333;
|
|
575
|
+
border-radius: 4px;
|
|
576
|
+
padding: 5px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#type-filter-controls {
|
|
580
|
+
display: flex;
|
|
581
|
+
gap: 5px;
|
|
582
|
+
margin-bottom: 8px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#type-filter-controls button {
|
|
586
|
+
flex: 1;
|
|
587
|
+
padding: 4px 8px;
|
|
588
|
+
font-size: 11px;
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
background: #333;
|
|
591
|
+
color: #fff;
|
|
592
|
+
border: 1px solid #555;
|
|
593
|
+
border-radius: 3px;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
#type-filter-controls button:hover {
|
|
597
|
+
background: #444;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#type-filter-search {
|
|
601
|
+
width: 100%;
|
|
602
|
+
padding: 5px;
|
|
603
|
+
margin-bottom: 8px;
|
|
604
|
+
background: #1a1a2e;
|
|
605
|
+
color: #fff;
|
|
606
|
+
border: 1px solid #333;
|
|
607
|
+
border-radius: 3px;
|
|
608
|
+
font-size: 12px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.type-filter {
|
|
612
|
+
display: flex;
|
|
613
|
+
align-items: center;
|
|
614
|
+
margin: 3px 0;
|
|
615
|
+
font-size: 12px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.type-filter input {
|
|
619
|
+
margin-right: 8px;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.type-filter .count {
|
|
623
|
+
color: #888;
|
|
624
|
+
margin-left: auto;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.type-filter.hidden {
|
|
628
|
+
display: none;
|
|
629
|
+
}
|
|
630
|
+
</style>
|
|
631
|
+
</head>
|
|
632
|
+
<body>
|
|
633
|
+
<div id="container">
|
|
634
|
+
<div id="graph-container">
|
|
635
|
+
<div id="controls">
|
|
636
|
+
<h3>🔍 Search & Filter</h3>
|
|
637
|
+
<input type="text" id="search-input" placeholder="Search entities...">
|
|
638
|
+
<div id="search-results"></div>
|
|
639
|
+
|
|
640
|
+
<h3 style="margin-top: 15px;">⚙️ Layout</h3>
|
|
641
|
+
<label>
|
|
642
|
+
Link Distance
|
|
643
|
+
<input type="range" id="link-distance" min="50" max="300" value="120">
|
|
644
|
+
</label>
|
|
645
|
+
<label>
|
|
646
|
+
Charge Strength
|
|
647
|
+
<input type="range" id="charge-strength" min="-500" max="-50" value="-200">
|
|
648
|
+
</label>
|
|
649
|
+
<label>
|
|
650
|
+
<input type="checkbox" id="show-labels" checked> Show Labels
|
|
651
|
+
</label>
|
|
652
|
+
<label>
|
|
653
|
+
<input type="checkbox" id="show-orphans" checked> Show Orphan Nodes
|
|
654
|
+
</label>
|
|
655
|
+
|
|
656
|
+
<h3 style="margin-top: 15px;">📊 Filter by Type <span id="type-count-display" style="color: #888; font-size: 12px;"></span></h3>
|
|
657
|
+
<input type="text" id="type-filter-search" placeholder="Search types...">
|
|
658
|
+
<div id="type-filter-controls">
|
|
659
|
+
<button id="select-all-types">All</button>
|
|
660
|
+
<button id="select-none-types">None</button>
|
|
661
|
+
<button id="select-top10-types">Top 10</button>
|
|
662
|
+
</div>
|
|
663
|
+
<div id="type-filters"></div>
|
|
664
|
+
</div>
|
|
665
|
+
<svg id="graph"></svg>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<div id="sidebar">
|
|
669
|
+
<h2>📋 Entity Details</h2>
|
|
670
|
+
<div id="stats">
|
|
671
|
+
<span>Entities: <span class="stat-value" id="entity-count">0</span></span>
|
|
672
|
+
<span>Relations: <span class="stat-value" id="relation-count">0</span></span>
|
|
673
|
+
</div>
|
|
674
|
+
<div id="entity-details">
|
|
675
|
+
<p style="color: #888;">Click on a node to see details</p>
|
|
676
|
+
</div>
|
|
677
|
+
<div id="legend">
|
|
678
|
+
<h3>🎨 Legend</h3>
|
|
679
|
+
<div id="legend-items"></div>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div class="tooltip" id="tooltip" style="display: none;"></div>
|
|
685
|
+
|
|
686
|
+
<!-- Context Menu -->
|
|
687
|
+
<div id="context-menu">
|
|
688
|
+
<div class="context-menu-item" id="ctx-focus-1">🔍 Focus 1 level</div>
|
|
689
|
+
<div class="context-menu-item" id="ctx-focus-2">🔍 Focus 2 levels</div>
|
|
690
|
+
<div class="context-menu-item" id="ctx-focus-3">🔍 Focus 3 levels</div>
|
|
691
|
+
<div class="context-menu-item" id="ctx-focus-5">🔍 Focus 5 levels</div>
|
|
692
|
+
<div class="context-menu-separator"></div>
|
|
693
|
+
<div class="context-menu-item" id="ctx-expand">📈 Expand neighbors</div>
|
|
694
|
+
<div class="context-menu-item" id="ctx-hide">👁️ Hide this node</div>
|
|
695
|
+
<div class="context-menu-separator"></div>
|
|
696
|
+
<div class="context-menu-item danger" id="ctx-reset">✖ Clear focus</div>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<!-- Focus Mode Banner -->
|
|
700
|
+
<div id="focus-banner">
|
|
701
|
+
<span id="focus-info">Focus mode: showing X levels from "Node"</span>
|
|
702
|
+
<button id="clear-focus">Clear Focus</button>
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
<script>
|
|
706
|
+
// Graph data injected from Python
|
|
707
|
+
const graphData = GRAPH_DATA_PLACEHOLDER;
|
|
708
|
+
const typeColors = TYPE_COLORS_PLACEHOLDER;
|
|
709
|
+
const relationColors = RELATION_COLORS_PLACEHOLDER;
|
|
710
|
+
|
|
711
|
+
// Process data
|
|
712
|
+
const nodes = graphData.nodes.map(n => {
|
|
713
|
+
// Handle both 'citations' (list) and legacy 'citation' (single)
|
|
714
|
+
let citations = n.citations || [];
|
|
715
|
+
if (n.citation && !citations.length) {
|
|
716
|
+
citations = [n.citation];
|
|
717
|
+
}
|
|
718
|
+
// Get file_path from first citation if not on node directly
|
|
719
|
+
const file_path = n.file_path || (citations[0] ? citations[0].file_path : null);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
id: n.id,
|
|
723
|
+
name: n.name,
|
|
724
|
+
type: n.type,
|
|
725
|
+
citations: citations,
|
|
726
|
+
properties: n.properties || {},
|
|
727
|
+
source_toolkit: n.source_toolkit || (citations[0] ? citations[0].source_toolkit : null),
|
|
728
|
+
file_path: file_path
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const links = graphData.links.map(l => ({
|
|
733
|
+
source: l.source,
|
|
734
|
+
target: l.target,
|
|
735
|
+
type: l.relation_type || 'related',
|
|
736
|
+
source_toolkit: l.source_toolkit,
|
|
737
|
+
discovered_in_file: l.discovered_in_file,
|
|
738
|
+
...l
|
|
739
|
+
}));
|
|
740
|
+
|
|
741
|
+
// Update stats
|
|
742
|
+
document.getElementById('entity-count').textContent = nodes.length;
|
|
743
|
+
document.getElementById('relation-count').textContent = links.length;
|
|
744
|
+
|
|
745
|
+
// Count types
|
|
746
|
+
const typeCounts = {};
|
|
747
|
+
nodes.forEach(n => {
|
|
748
|
+
typeCounts[n.type] = (typeCounts[n.type] || 0) + 1;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Build type filters
|
|
752
|
+
const typeFiltersDiv = document.getElementById('type-filters');
|
|
753
|
+
const enabledTypes = new Set(Object.keys(typeCounts));
|
|
754
|
+
const sortedTypes = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
|
755
|
+
|
|
756
|
+
// Display type count
|
|
757
|
+
document.getElementById('type-count-display').textContent = `(${sortedTypes.length} types)`;
|
|
758
|
+
|
|
759
|
+
sortedTypes.forEach(([type, count]) => {
|
|
760
|
+
const div = document.createElement('div');
|
|
761
|
+
div.className = 'type-filter';
|
|
762
|
+
div.dataset.typeName = type.toLowerCase();
|
|
763
|
+
div.innerHTML = `
|
|
764
|
+
<input type="checkbox" checked data-type="${type}">
|
|
765
|
+
<span class="legend-color" style="background: ${getColor(type)}"></span>
|
|
766
|
+
${type}
|
|
767
|
+
<span class="count">${count}</span>
|
|
768
|
+
`;
|
|
769
|
+
typeFiltersDiv.appendChild(div);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Type filter search
|
|
773
|
+
document.getElementById('type-filter-search').addEventListener('input', (e) => {
|
|
774
|
+
const searchTerm = e.target.value.toLowerCase();
|
|
775
|
+
document.querySelectorAll('.type-filter').forEach(div => {
|
|
776
|
+
const typeName = div.dataset.typeName;
|
|
777
|
+
div.classList.toggle('hidden', !typeName.includes(searchTerm));
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// Select all/none/top10 buttons
|
|
782
|
+
function updateGraphVisibility() {
|
|
783
|
+
node.style('display', d => enabledTypes.has(d.type) ? 'block' : 'none');
|
|
784
|
+
link.style('display', d => {
|
|
785
|
+
const sourceType = typeof d.source === 'object' ? d.source.type : nodes.find(n => n.id === d.source)?.type;
|
|
786
|
+
const targetType = typeof d.target === 'object' ? d.target.type : nodes.find(n => n.id === d.target)?.type;
|
|
787
|
+
return enabledTypes.has(sourceType) && enabledTypes.has(targetType) ? 'block' : 'none';
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
document.getElementById('select-all-types').addEventListener('click', () => {
|
|
792
|
+
document.querySelectorAll('.type-filter input[type="checkbox"]').forEach(cb => {
|
|
793
|
+
cb.checked = true;
|
|
794
|
+
enabledTypes.add(cb.dataset.type);
|
|
795
|
+
});
|
|
796
|
+
updateGraphVisibility();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
document.getElementById('select-none-types').addEventListener('click', () => {
|
|
800
|
+
document.querySelectorAll('.type-filter input[type="checkbox"]').forEach(cb => {
|
|
801
|
+
cb.checked = false;
|
|
802
|
+
enabledTypes.delete(cb.dataset.type);
|
|
803
|
+
});
|
|
804
|
+
updateGraphVisibility();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
document.getElementById('select-top10-types').addEventListener('click', () => {
|
|
808
|
+
const top10Types = new Set(sortedTypes.slice(0, 10).map(([type]) => type));
|
|
809
|
+
document.querySelectorAll('.type-filter input[type="checkbox"]').forEach(cb => {
|
|
810
|
+
const isTop10 = top10Types.has(cb.dataset.type);
|
|
811
|
+
cb.checked = isTop10;
|
|
812
|
+
if (isTop10) {
|
|
813
|
+
enabledTypes.add(cb.dataset.type);
|
|
814
|
+
} else {
|
|
815
|
+
enabledTypes.delete(cb.dataset.type);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
updateGraphVisibility();
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// Build legend
|
|
822
|
+
const legendDiv = document.getElementById('legend-items');
|
|
823
|
+
Object.entries(typeCounts)
|
|
824
|
+
.sort((a, b) => b[1] - a[1])
|
|
825
|
+
.slice(0, 10)
|
|
826
|
+
.forEach(([type, count]) => {
|
|
827
|
+
const div = document.createElement('div');
|
|
828
|
+
div.className = 'legend-item';
|
|
829
|
+
div.innerHTML = `
|
|
830
|
+
<span class="legend-color" style="background: ${getColor(type)}"></span>
|
|
831
|
+
${type} (${count})
|
|
832
|
+
`;
|
|
833
|
+
legendDiv.appendChild(div);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Generate a consistent color from a string hash (fallback for unknown types)
|
|
837
|
+
function stringToColor(str) {
|
|
838
|
+
let hash = 0;
|
|
839
|
+
for (let i = 0; i < str.length; i++) {
|
|
840
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
841
|
+
}
|
|
842
|
+
// Use golden ratio to spread hues evenly, avoiding muddy colors
|
|
843
|
+
const h = Math.abs(hash * 137.508) % 360;
|
|
844
|
+
const s = 55 + (Math.abs(hash >> 8) % 25); // 55-80% saturation
|
|
845
|
+
const l = 45 + (Math.abs(hash >> 16) % 20); // 45-65% lightness
|
|
846
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function getColor(type) {
|
|
850
|
+
// First try exact match in predefined colors
|
|
851
|
+
if (typeColors[type]) return typeColors[type];
|
|
852
|
+
// Try lowercase match
|
|
853
|
+
const lower = type.toLowerCase();
|
|
854
|
+
if (typeColors[lower]) return typeColors[lower];
|
|
855
|
+
// Try with underscores/spaces/dashes replaced (e.g., "UserStory" -> "userstory")
|
|
856
|
+
const normalized = lower.replace(/[_\\s-]/g, '');
|
|
857
|
+
for (const [key, color] of Object.entries(typeColors)) {
|
|
858
|
+
if (key.toLowerCase().replace(/[_\\s-]/g, '') === normalized) {
|
|
859
|
+
return color;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// Fallback: generate consistent color from type name
|
|
863
|
+
return stringToColor(type);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function getRelationColor(type) {
|
|
867
|
+
return relationColors[type] || relationColors['default'];
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// SVG setup
|
|
871
|
+
const svg = d3.select('#graph');
|
|
872
|
+
const container = document.getElementById('graph-container');
|
|
873
|
+
const width = container.clientWidth;
|
|
874
|
+
const height = container.clientHeight;
|
|
875
|
+
|
|
876
|
+
svg.attr('viewBox', [0, 0, width, height]);
|
|
877
|
+
|
|
878
|
+
// Create zoom behavior
|
|
879
|
+
const zoom = d3.zoom()
|
|
880
|
+
.scaleExtent([0.1, 4])
|
|
881
|
+
.on('zoom', (event) => {
|
|
882
|
+
g.attr('transform', event.transform);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
svg.call(zoom);
|
|
886
|
+
|
|
887
|
+
// Main group for zoom/pan
|
|
888
|
+
const g = svg.append('g');
|
|
889
|
+
|
|
890
|
+
// Arrow markers for directed edges
|
|
891
|
+
svg.append('defs').selectAll('marker')
|
|
892
|
+
.data(['arrow'])
|
|
893
|
+
.join('marker')
|
|
894
|
+
.attr('id', d => d)
|
|
895
|
+
.attr('viewBox', '0 -5 10 10')
|
|
896
|
+
.attr('refX', 20)
|
|
897
|
+
.attr('refY', 0)
|
|
898
|
+
.attr('markerWidth', 6)
|
|
899
|
+
.attr('markerHeight', 6)
|
|
900
|
+
.attr('orient', 'auto')
|
|
901
|
+
.append('path')
|
|
902
|
+
.attr('fill', '#888')
|
|
903
|
+
.attr('d', 'M0,-5L10,0L0,5');
|
|
904
|
+
|
|
905
|
+
// Force simulation
|
|
906
|
+
const simulation = d3.forceSimulation(nodes)
|
|
907
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(120))
|
|
908
|
+
.force('charge', d3.forceManyBody().strength(-200))
|
|
909
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
910
|
+
.force('collision', d3.forceCollide().radius(30));
|
|
911
|
+
|
|
912
|
+
// Draw links
|
|
913
|
+
const link = g.append('g')
|
|
914
|
+
.attr('class', 'links')
|
|
915
|
+
.selectAll('line')
|
|
916
|
+
.data(links)
|
|
917
|
+
.join('line')
|
|
918
|
+
.attr('class', 'link')
|
|
919
|
+
.attr('stroke', d => getRelationColor(d.type))
|
|
920
|
+
.attr('marker-end', 'url(#arrow)')
|
|
921
|
+
.on('mouseover', function(event, d) {
|
|
922
|
+
// Show tooltip for relations
|
|
923
|
+
tooltip
|
|
924
|
+
.style('display', 'block')
|
|
925
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
926
|
+
.style('top', (event.pageY + 10) + 'px')
|
|
927
|
+
.html(`
|
|
928
|
+
<h4>${d.type}</h4>
|
|
929
|
+
${d.source_toolkit ? `<div><strong>Source:</strong> ${d.source_toolkit}</div>` : ''}
|
|
930
|
+
${d.discovered_in_file ? `<div><strong>File:</strong> ${d.discovered_in_file}</div>` : ''}
|
|
931
|
+
${d.confidence ? `<div><strong>Confidence:</strong> ${(d.confidence * 100).toFixed(0)}%</div>` : ''}
|
|
932
|
+
`);
|
|
933
|
+
// Highlight the link
|
|
934
|
+
d3.select(this).attr('stroke-width', 3).attr('stroke-opacity', 1);
|
|
935
|
+
})
|
|
936
|
+
.on('mouseout', function() {
|
|
937
|
+
tooltip.style('display', 'none');
|
|
938
|
+
// Reset link appearance
|
|
939
|
+
d3.select(this).attr('stroke-width', 1.5).attr('stroke-opacity', 0.4);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Draw nodes
|
|
943
|
+
const node = g.append('g')
|
|
944
|
+
.attr('class', 'nodes')
|
|
945
|
+
.selectAll('g')
|
|
946
|
+
.data(nodes)
|
|
947
|
+
.join('g')
|
|
948
|
+
.attr('class', 'node')
|
|
949
|
+
.call(d3.drag()
|
|
950
|
+
.on('start', dragstarted)
|
|
951
|
+
.on('drag', dragged)
|
|
952
|
+
.on('end', dragended));
|
|
953
|
+
|
|
954
|
+
// Node circles
|
|
955
|
+
node.append('circle')
|
|
956
|
+
.attr('r', d => 8 + Math.min(5, (typeCounts[d.type] || 1) / 2))
|
|
957
|
+
.attr('fill', d => getColor(d.type));
|
|
958
|
+
|
|
959
|
+
// Node labels
|
|
960
|
+
const labels = node.append('text')
|
|
961
|
+
.attr('dx', 12)
|
|
962
|
+
.attr('dy', 4)
|
|
963
|
+
.text(d => d.name.length > 25 ? d.name.substring(0, 25) + '...' : d.name);
|
|
964
|
+
|
|
965
|
+
// Tooltip
|
|
966
|
+
const tooltip = d3.select('#tooltip');
|
|
967
|
+
|
|
968
|
+
node.on('mouseover', (event, d) => {
|
|
969
|
+
tooltip
|
|
970
|
+
.style('display', 'block')
|
|
971
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
972
|
+
.style('top', (event.pageY + 10) + 'px')
|
|
973
|
+
.html(`
|
|
974
|
+
<h4>${d.name}</h4>
|
|
975
|
+
<div><strong>Type:</strong> ${d.type}</div>
|
|
976
|
+
<div><strong>File:</strong> ${d.file_path || 'N/A'}</div>
|
|
977
|
+
${d.citations && d.citations.length > 0 ? `<div><strong>Files:</strong> ${d.citations.length} reference${d.citations.length > 1 ? 's' : ''}</div>` : ''}
|
|
978
|
+
`);
|
|
979
|
+
})
|
|
980
|
+
.on('mouseout', () => {
|
|
981
|
+
tooltip.style('display', 'none');
|
|
982
|
+
})
|
|
983
|
+
.on('click', (event, d) => {
|
|
984
|
+
// Deselect all
|
|
985
|
+
node.classed('selected', false);
|
|
986
|
+
// Select clicked
|
|
987
|
+
d3.select(event.currentTarget).classed('selected', true);
|
|
988
|
+
// Show details
|
|
989
|
+
showEntityDetails(d);
|
|
990
|
+
})
|
|
991
|
+
.on('contextmenu', (event, d) => {
|
|
992
|
+
event.preventDefault();
|
|
993
|
+
showContextMenu(event, d);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Context Menu
|
|
997
|
+
const contextMenu = document.getElementById('context-menu');
|
|
998
|
+
let contextMenuNode = null;
|
|
999
|
+
|
|
1000
|
+
function showContextMenu(event, d) {
|
|
1001
|
+
contextMenuNode = d;
|
|
1002
|
+
contextMenu.style.display = 'block';
|
|
1003
|
+
contextMenu.style.left = event.pageX + 'px';
|
|
1004
|
+
contextMenu.style.top = event.pageY + 'px';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function hideContextMenu() {
|
|
1008
|
+
contextMenu.style.display = 'none';
|
|
1009
|
+
contextMenuNode = null;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Hide context menu on click elsewhere
|
|
1013
|
+
document.addEventListener('click', hideContextMenu);
|
|
1014
|
+
document.addEventListener('contextmenu', (e) => {
|
|
1015
|
+
if (!e.target.closest('.node')) {
|
|
1016
|
+
hideContextMenu();
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// Build adjacency list for BFS
|
|
1021
|
+
const adjacency = new Map();
|
|
1022
|
+
nodes.forEach(n => adjacency.set(n.id, new Set()));
|
|
1023
|
+
links.forEach(l => {
|
|
1024
|
+
const srcId = typeof l.source === 'object' ? l.source.id : l.source;
|
|
1025
|
+
const tgtId = typeof l.target === 'object' ? l.target.id : l.target;
|
|
1026
|
+
adjacency.get(srcId)?.add(tgtId);
|
|
1027
|
+
adjacency.get(tgtId)?.add(srcId);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// Get nodes within N levels using BFS
|
|
1031
|
+
function getNodesWithinLevels(startId, maxLevels) {
|
|
1032
|
+
const visited = new Map(); // nodeId -> level
|
|
1033
|
+
const queue = [[startId, 0]];
|
|
1034
|
+
visited.set(startId, 0);
|
|
1035
|
+
|
|
1036
|
+
while (queue.length > 0) {
|
|
1037
|
+
const [currentId, level] = queue.shift();
|
|
1038
|
+
if (level >= maxLevels) continue;
|
|
1039
|
+
|
|
1040
|
+
const neighbors = adjacency.get(currentId) || new Set();
|
|
1041
|
+
for (const neighborId of neighbors) {
|
|
1042
|
+
if (!visited.has(neighborId)) {
|
|
1043
|
+
visited.set(neighborId, level + 1);
|
|
1044
|
+
queue.push([neighborId, level + 1]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return visited;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Focus on node with N levels
|
|
1053
|
+
function focusOnNode(nodeData, levels) {
|
|
1054
|
+
const focusedNodes = getNodesWithinLevels(nodeData.id, levels);
|
|
1055
|
+
|
|
1056
|
+
// Apply styling
|
|
1057
|
+
node.classed('faded', d => !focusedNodes.has(d.id))
|
|
1058
|
+
.classed('focused', d => d.id === nodeData.id)
|
|
1059
|
+
.classed('neighbor', d => focusedNodes.has(d.id) && d.id !== nodeData.id);
|
|
1060
|
+
|
|
1061
|
+
link.classed('faded', d => {
|
|
1062
|
+
const srcId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
1063
|
+
const tgtId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
1064
|
+
return !focusedNodes.has(srcId) || !focusedNodes.has(tgtId);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// Show focus banner
|
|
1068
|
+
const banner = document.getElementById('focus-banner');
|
|
1069
|
+
const info = document.getElementById('focus-info');
|
|
1070
|
+
info.textContent = `Focus: ${levels} level${levels > 1 ? 's' : ''} from "${nodeData.name}" (${focusedNodes.size} nodes)`;
|
|
1071
|
+
banner.style.display = 'block';
|
|
1072
|
+
|
|
1073
|
+
// Select the focused node
|
|
1074
|
+
node.classed('selected', d => d.id === nodeData.id);
|
|
1075
|
+
showEntityDetails(nodeData);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Clear focus
|
|
1079
|
+
function clearFocus() {
|
|
1080
|
+
node.classed('faded', false)
|
|
1081
|
+
.classed('focused', false)
|
|
1082
|
+
.classed('neighbor', false);
|
|
1083
|
+
link.classed('faded', false);
|
|
1084
|
+
document.getElementById('focus-banner').style.display = 'none';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Context menu actions
|
|
1088
|
+
document.getElementById('ctx-focus-1').onclick = () => {
|
|
1089
|
+
if (contextMenuNode) focusOnNode(contextMenuNode, 1);
|
|
1090
|
+
hideContextMenu();
|
|
1091
|
+
};
|
|
1092
|
+
document.getElementById('ctx-focus-2').onclick = () => {
|
|
1093
|
+
if (contextMenuNode) focusOnNode(contextMenuNode, 2);
|
|
1094
|
+
hideContextMenu();
|
|
1095
|
+
};
|
|
1096
|
+
document.getElementById('ctx-focus-3').onclick = () => {
|
|
1097
|
+
if (contextMenuNode) focusOnNode(contextMenuNode, 3);
|
|
1098
|
+
hideContextMenu();
|
|
1099
|
+
};
|
|
1100
|
+
document.getElementById('ctx-focus-5').onclick = () => {
|
|
1101
|
+
if (contextMenuNode) focusOnNode(contextMenuNode, 5);
|
|
1102
|
+
hideContextMenu();
|
|
1103
|
+
};
|
|
1104
|
+
document.getElementById('ctx-expand').onclick = () => {
|
|
1105
|
+
if (contextMenuNode) {
|
|
1106
|
+
// Just clear faded state for this node and neighbors
|
|
1107
|
+
const neighbors = adjacency.get(contextMenuNode.id) || new Set();
|
|
1108
|
+
node.filter(d => d.id === contextMenuNode.id || neighbors.has(d.id))
|
|
1109
|
+
.classed('faded', false);
|
|
1110
|
+
}
|
|
1111
|
+
hideContextMenu();
|
|
1112
|
+
};
|
|
1113
|
+
document.getElementById('ctx-hide').onclick = () => {
|
|
1114
|
+
if (contextMenuNode) {
|
|
1115
|
+
node.filter(d => d.id === contextMenuNode.id).style('display', 'none');
|
|
1116
|
+
link.filter(d => {
|
|
1117
|
+
const srcId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
1118
|
+
const tgtId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
1119
|
+
return srcId === contextMenuNode.id || tgtId === contextMenuNode.id;
|
|
1120
|
+
}).style('display', 'none');
|
|
1121
|
+
}
|
|
1122
|
+
hideContextMenu();
|
|
1123
|
+
};
|
|
1124
|
+
document.getElementById('ctx-reset').onclick = () => {
|
|
1125
|
+
clearFocus();
|
|
1126
|
+
// Also show all hidden nodes
|
|
1127
|
+
node.style('display', 'block');
|
|
1128
|
+
link.style('display', 'block');
|
|
1129
|
+
hideContextMenu();
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
// Clear focus button in banner
|
|
1133
|
+
document.getElementById('clear-focus').onclick = clearFocus;
|
|
1134
|
+
|
|
1135
|
+
// Tick function
|
|
1136
|
+
simulation.on('tick', () => {
|
|
1137
|
+
link
|
|
1138
|
+
.attr('x1', d => d.source.x)
|
|
1139
|
+
.attr('y1', d => d.source.y)
|
|
1140
|
+
.attr('x2', d => d.target.x)
|
|
1141
|
+
.attr('y2', d => d.target.y);
|
|
1142
|
+
|
|
1143
|
+
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// Drag functions
|
|
1147
|
+
function dragstarted(event, d) {
|
|
1148
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1149
|
+
d.fx = d.x;
|
|
1150
|
+
d.fy = d.y;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function dragged(event, d) {
|
|
1154
|
+
d.fx = event.x;
|
|
1155
|
+
d.fy = event.y;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function dragended(event, d) {
|
|
1159
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
1160
|
+
d.fx = null;
|
|
1161
|
+
d.fy = null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Show entity details in sidebar
|
|
1165
|
+
function showEntityDetails(d) {
|
|
1166
|
+
const details = document.getElementById('entity-details');
|
|
1167
|
+
const propsHtml = Object.entries(d.properties || {})
|
|
1168
|
+
.map(([k, v]) => `
|
|
1169
|
+
<div class="detail-label">${k}</div>
|
|
1170
|
+
<div class="detail-value">${typeof v === 'object' ? JSON.stringify(v, null, 2) : v}</div>
|
|
1171
|
+
`).join('');
|
|
1172
|
+
|
|
1173
|
+
details.innerHTML = `
|
|
1174
|
+
<h3>${d.name}</h3>
|
|
1175
|
+
<div style="margin: 10px 0;">
|
|
1176
|
+
<span class="entity-type-badge" style="background: ${getColor(d.type)}">${d.type}</span>
|
|
1177
|
+
</div>
|
|
1178
|
+
|
|
1179
|
+
<div class="detail-label">ID</div>
|
|
1180
|
+
<div class="detail-value" style="font-family: monospace; font-size: 11px;">${d.id}</div>
|
|
1181
|
+
|
|
1182
|
+
${d.citations && d.citations.length > 0 ? `
|
|
1183
|
+
<div class="detail-label">Citations (${d.citations.length})</div>
|
|
1184
|
+
<div class="detail-value">
|
|
1185
|
+
${d.citations.map(c => `
|
|
1186
|
+
<div class="citation-link" style="margin-bottom: 4px;">
|
|
1187
|
+
${c.file_path}${c.line_start ? `:${c.line_start}-${c.line_end}` : ''}
|
|
1188
|
+
</div>
|
|
1189
|
+
`).join('')}
|
|
1190
|
+
</div>
|
|
1191
|
+
` : ''}
|
|
1192
|
+
|
|
1193
|
+
<div class="detail-label">Source</div>
|
|
1194
|
+
<div class="detail-value">${d.source_toolkit || 'N/A'}</div>
|
|
1195
|
+
|
|
1196
|
+
${propsHtml ? `
|
|
1197
|
+
<div class="detail-label" style="margin-top: 15px;">Properties</div>
|
|
1198
|
+
${propsHtml}
|
|
1199
|
+
` : ''}
|
|
1200
|
+
`;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Search functionality
|
|
1204
|
+
const searchInput = document.getElementById('search-input');
|
|
1205
|
+
const searchResults = document.getElementById('search-results');
|
|
1206
|
+
|
|
1207
|
+
searchInput.addEventListener('input', (e) => {
|
|
1208
|
+
const query = e.target.value.toLowerCase();
|
|
1209
|
+
searchResults.innerHTML = '';
|
|
1210
|
+
|
|
1211
|
+
if (query.length < 2) return;
|
|
1212
|
+
|
|
1213
|
+
const matches = nodes.filter(n =>
|
|
1214
|
+
n.name.toLowerCase().includes(query) ||
|
|
1215
|
+
n.type.toLowerCase().includes(query)
|
|
1216
|
+
).slice(0, 10);
|
|
1217
|
+
|
|
1218
|
+
matches.forEach(n => {
|
|
1219
|
+
const div = document.createElement('div');
|
|
1220
|
+
div.className = 'search-result';
|
|
1221
|
+
div.innerHTML = `<span class="legend-color" style="background: ${getColor(n.type)}; display: inline-block; vertical-align: middle;"></span> ${n.name}`;
|
|
1222
|
+
div.onclick = () => {
|
|
1223
|
+
// Center on node
|
|
1224
|
+
const transform = d3.zoomIdentity
|
|
1225
|
+
.translate(width / 2 - n.x, height / 2 - n.y);
|
|
1226
|
+
svg.transition().duration(500).call(zoom.transform, transform);
|
|
1227
|
+
|
|
1228
|
+
// Select node
|
|
1229
|
+
node.classed('selected', d => d.id === n.id);
|
|
1230
|
+
showEntityDetails(n);
|
|
1231
|
+
};
|
|
1232
|
+
searchResults.appendChild(div);
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// Controls
|
|
1237
|
+
document.getElementById('link-distance').addEventListener('input', (e) => {
|
|
1238
|
+
simulation.force('link').distance(+e.target.value);
|
|
1239
|
+
simulation.alpha(0.3).restart();
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
document.getElementById('charge-strength').addEventListener('input', (e) => {
|
|
1243
|
+
simulation.force('charge').strength(+e.target.value);
|
|
1244
|
+
simulation.alpha(0.3).restart();
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
document.getElementById('show-labels').addEventListener('change', (e) => {
|
|
1248
|
+
labels.style('display', e.target.checked ? 'block' : 'none');
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
document.getElementById('show-orphans').addEventListener('change', (e) => {
|
|
1252
|
+
const connectedIds = new Set();
|
|
1253
|
+
links.forEach(l => {
|
|
1254
|
+
connectedIds.add(typeof l.source === 'object' ? l.source.id : l.source);
|
|
1255
|
+
connectedIds.add(typeof l.target === 'object' ? l.target.id : l.target);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
node.style('display', d => {
|
|
1259
|
+
if (e.target.checked) return 'block';
|
|
1260
|
+
return connectedIds.has(d.id) ? 'block' : 'none';
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
// Type filters
|
|
1265
|
+
typeFiltersDiv.addEventListener('change', (e) => {
|
|
1266
|
+
if (e.target.type === 'checkbox') {
|
|
1267
|
+
const type = e.target.dataset.type;
|
|
1268
|
+
if (e.target.checked) {
|
|
1269
|
+
enabledTypes.add(type);
|
|
1270
|
+
} else {
|
|
1271
|
+
enabledTypes.delete(type);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
node.style('display', d => enabledTypes.has(d.type) ? 'block' : 'none');
|
|
1275
|
+
link.style('display', d => {
|
|
1276
|
+
const sourceType = typeof d.source === 'object' ? d.source.type : nodes.find(n => n.id === d.source)?.type;
|
|
1277
|
+
const targetType = typeof d.target === 'object' ? d.target.type : nodes.find(n => n.id === d.target)?.type;
|
|
1278
|
+
return enabledTypes.has(sourceType) && enabledTypes.has(targetType) ? 'block' : 'none';
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// Initial zoom to fit
|
|
1284
|
+
const initialScale = 0.8;
|
|
1285
|
+
svg.call(zoom.transform, d3.zoomIdentity
|
|
1286
|
+
.translate(width * (1 - initialScale) / 2, height * (1 - initialScale) / 2)
|
|
1287
|
+
.scale(initialScale));
|
|
1288
|
+
</script>
|
|
1289
|
+
</body>
|
|
1290
|
+
</html>
|
|
1291
|
+
'''
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def generate_visualization(
|
|
1295
|
+
graph_path: str,
|
|
1296
|
+
output_path: str,
|
|
1297
|
+
title: str = "Knowledge Graph"
|
|
1298
|
+
) -> str:
|
|
1299
|
+
"""
|
|
1300
|
+
Generate an interactive HTML visualization of the knowledge graph.
|
|
1301
|
+
|
|
1302
|
+
Args:
|
|
1303
|
+
graph_path: Path to the knowledge graph JSON file
|
|
1304
|
+
output_path: Path to write the HTML file
|
|
1305
|
+
title: Title for the visualization
|
|
1306
|
+
|
|
1307
|
+
Returns:
|
|
1308
|
+
Path to the generated HTML file
|
|
1309
|
+
"""
|
|
1310
|
+
# Load graph
|
|
1311
|
+
graph_path = Path(graph_path)
|
|
1312
|
+
if not graph_path.exists():
|
|
1313
|
+
raise FileNotFoundError(f"Graph not found: {graph_path}")
|
|
1314
|
+
|
|
1315
|
+
with open(graph_path, 'r') as f:
|
|
1316
|
+
graph_data = json.load(f)
|
|
1317
|
+
|
|
1318
|
+
# Handle NetworkX 3.5+ compatibility: may have "edges" instead of "links"
|
|
1319
|
+
if 'edges' in graph_data and 'links' not in graph_data:
|
|
1320
|
+
graph_data['links'] = graph_data.pop('edges')
|
|
1321
|
+
|
|
1322
|
+
# Prepare data for JavaScript - properly escape for embedding in HTML
|
|
1323
|
+
# Convert to JSON strings that will be valid JavaScript
|
|
1324
|
+
graph_json = json.dumps(graph_data, default=str, ensure_ascii=True)
|
|
1325
|
+
type_colors_json = json.dumps(TYPE_COLORS, ensure_ascii=True)
|
|
1326
|
+
relation_colors_json = json.dumps(RELATION_COLORS, ensure_ascii=True)
|
|
1327
|
+
|
|
1328
|
+
# Escape special HTML characters that could break the script tag
|
|
1329
|
+
# This prevents issues with large JSON data containing HTML-like content
|
|
1330
|
+
graph_json = graph_json.replace('</', '<\\/')
|
|
1331
|
+
type_colors_json = type_colors_json.replace('</', '<\\/')
|
|
1332
|
+
relation_colors_json = relation_colors_json.replace('</', '<\\/')
|
|
1333
|
+
|
|
1334
|
+
# Generate HTML - build valid HTML structure first
|
|
1335
|
+
html = HTML_TEMPLATE.replace('<title>Knowledge Graph Visualization</title>',
|
|
1336
|
+
f'<title>{title}</title>')
|
|
1337
|
+
|
|
1338
|
+
# Now inject the properly escaped JSON data into the script placeholders
|
|
1339
|
+
html = html.replace('GRAPH_DATA_PLACEHOLDER', graph_json)
|
|
1340
|
+
html = html.replace('TYPE_COLORS_PLACEHOLDER', type_colors_json)
|
|
1341
|
+
html = html.replace('RELATION_COLORS_PLACEHOLDER', relation_colors_json)
|
|
1342
|
+
|
|
1343
|
+
# Write output
|
|
1344
|
+
output_path = Path(output_path)
|
|
1345
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1346
|
+
|
|
1347
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
1348
|
+
f.write(html)
|
|
1349
|
+
|
|
1350
|
+
logger.info(f"Generated visualization: {output_path} ({len(html)} bytes)")
|
|
1351
|
+
return str(output_path)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def main():
|
|
1355
|
+
"""CLI entry point."""
|
|
1356
|
+
import argparse
|
|
1357
|
+
|
|
1358
|
+
parser = argparse.ArgumentParser(description='Generate knowledge graph visualization')
|
|
1359
|
+
parser.add_argument('--graph', '-g', required=True, help='Path to graph JSON file')
|
|
1360
|
+
parser.add_argument('--output', '-o', default='graph.html', help='Output HTML file')
|
|
1361
|
+
parser.add_argument('--title', '-t', default='Knowledge Graph', help='Visualization title')
|
|
1362
|
+
|
|
1363
|
+
args = parser.parse_args()
|
|
1364
|
+
|
|
1365
|
+
output = generate_visualization(args.graph, args.output, args.title)
|
|
1366
|
+
print(f"Generated: {output}")
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
if __name__ == '__main__':
|
|
1370
|
+
main()
|