|
@@ -1,1994 +0,0 @@
|
|
|
-<!DOCTYPE html>
|
|
|
|
|
-<html lang="en">
|
|
|
|
|
-<head>
|
|
|
|
|
- <meta charset="UTF-8" />
|
|
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
- <title>CodeGraph Explorer</title>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Cytoscape.js + Dagre layout -->
|
|
|
|
|
- <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.4/cytoscape.min.js"></script>
|
|
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Highlight.js for code syntax highlighting -->
|
|
|
|
|
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
|
|
|
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
|
|
|
-
|
|
|
|
|
- <style>
|
|
|
|
|
- /* ====================================================================
|
|
|
|
|
- CSS Reset & Base
|
|
|
|
|
- ==================================================================== */
|
|
|
|
|
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
-
|
|
|
|
|
- :root {
|
|
|
|
|
- --bg-primary: #0d1117;
|
|
|
|
|
- --bg-secondary: #161b22;
|
|
|
|
|
- --bg-tertiary: #1c2128;
|
|
|
|
|
- --bg-hover: #1f2937;
|
|
|
|
|
- --border: #30363d;
|
|
|
|
|
- --border-light: #3d444d;
|
|
|
|
|
- --text-primary: #e6edf3;
|
|
|
|
|
- --text-secondary: #8b949e;
|
|
|
|
|
- --text-muted: #656d76;
|
|
|
|
|
- --accent: #58a6ff;
|
|
|
|
|
- --accent-hover: #79c0ff;
|
|
|
|
|
- --green: #3fb950;
|
|
|
|
|
- --purple: #d2a8ff;
|
|
|
|
|
- --orange: #ffa657;
|
|
|
|
|
- --red: #ff7b72;
|
|
|
|
|
- --yellow: #d29922;
|
|
|
|
|
- --pink: #f778ba;
|
|
|
|
|
- --cyan: #76e3ea;
|
|
|
|
|
- --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
|
|
|
|
|
- --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
|
|
|
- --sidebar-width: 320px;
|
|
|
|
|
- --panel-width: 460px;
|
|
|
|
|
- --header-height: 52px;
|
|
|
|
|
- --radius: 8px;
|
|
|
|
|
- --radius-sm: 6px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- html, body {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* ====================================================================
|
|
|
|
|
- Layout
|
|
|
|
|
- ==================================================================== */
|
|
|
|
|
- #app {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- height: 100vh;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Header */
|
|
|
|
|
- #header {
|
|
|
|
|
- height: var(--header-height);
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 0 16px;
|
|
|
|
|
- gap: 16px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #header .logo {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #header .logo span.icon { font-size: 20px; }
|
|
|
|
|
-
|
|
|
|
|
- #search-container {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- max-width: 560px;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #search-input {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 34px;
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- padding: 0 12px 0 34px;
|
|
|
|
|
- outline: none;
|
|
|
|
|
- transition: border-color 0.15s;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #search-input:focus { border-color: var(--accent); }
|
|
|
|
|
-
|
|
|
|
|
- #search-container .search-icon {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- left: 10px;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- transform: translateY(-50%);
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #search-results-dropdown {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 100%;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
- max-height: 400px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
- z-index: 100;
|
|
|
|
|
- display: none;
|
|
|
|
|
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #search-results-dropdown.visible { display: block; }
|
|
|
|
|
-
|
|
|
|
|
- .search-result-item {
|
|
|
|
|
- padding: 8px 12px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
- transition: background 0.1s;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .search-result-item:last-child { border-bottom: none; }
|
|
|
|
|
- .search-result-item:hover { background: var(--bg-hover); }
|
|
|
|
|
-
|
|
|
|
|
- .search-result-item .kind-badge {
|
|
|
|
|
- font-size: 10px;
|
|
|
|
|
- padding: 2px 6px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .search-result-item .name {
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .search-result-item .file-path {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- text-overflow: ellipsis;
|
|
|
|
|
- max-width: 200px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #header-stats {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #header-stats .stat { display: flex; align-items: center; gap: 4px; }
|
|
|
|
|
- #header-stats .stat-value { color: var(--text-secondary); font-weight: 500; }
|
|
|
|
|
-
|
|
|
|
|
- /* Main content */
|
|
|
|
|
- #main {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Sidebar */
|
|
|
|
|
- #sidebar {
|
|
|
|
|
- width: var(--sidebar-width);
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border-right: 1px solid var(--border);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .sidebar-section {
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .sidebar-header {
|
|
|
|
|
- padding: 10px 14px;
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- letter-spacing: 0.5px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- user-select: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .sidebar-header:hover { color: var(--text-secondary); }
|
|
|
|
|
-
|
|
|
|
|
- .sidebar-content {
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
- max-height: 300px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #file-tree {
|
|
|
|
|
- padding: 4px 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .file-item {
|
|
|
|
|
- padding: 5px 14px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- transition: background 0.1s;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- text-overflow: ellipsis;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .file-item:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
|
|
|
- .file-item.active { background: var(--bg-hover); color: var(--accent); }
|
|
|
|
|
-
|
|
|
|
|
- .file-item .file-icon { font-size: 12px; flex-shrink: 0; }
|
|
|
|
|
-
|
|
|
|
|
- /* Graph legend */
|
|
|
|
|
- #legend {
|
|
|
|
|
- padding: 10px 14px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .legend-item {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- padding: 3px 0;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .legend-dot {
|
|
|
|
|
- width: 10px;
|
|
|
|
|
- height: 10px;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Graph toolbar */
|
|
|
|
|
- #graph-toolbar {
|
|
|
|
|
- padding: 8px 14px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .toolbar-btn {
|
|
|
|
|
- padding: 4px 10px;
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: all 0.15s;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .toolbar-btn:hover {
|
|
|
|
|
- background: var(--bg-hover);
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- border-color: var(--border-light);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .toolbar-btn.active {
|
|
|
|
|
- background: var(--accent);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- border-color: var(--accent);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Graph canvas */
|
|
|
|
|
- #graph-container {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #cy {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #graph-overlay {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #graph-overlay .overlay-icon { font-size: 48px; margin-bottom: 12px; }
|
|
|
|
|
- #graph-overlay .overlay-title { font-size: 18px; font-weight: 500; margin-bottom: 6px; }
|
|
|
|
|
- #graph-overlay .overlay-subtitle { font-size: 13px; }
|
|
|
|
|
-
|
|
|
|
|
- .example-btn {
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- padding: 6px 16px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: all 0.15s;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- pointer-events: auto;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .example-btn:hover {
|
|
|
|
|
- background: var(--bg-hover);
|
|
|
|
|
- color: var(--accent);
|
|
|
|
|
- border-color: var(--accent);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Breadcrumbs */
|
|
|
|
|
- #breadcrumbs {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 10px;
|
|
|
|
|
- left: 10px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- z-index: 5;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- padding: 6px 10px;
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- transition: opacity 0.2s;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #breadcrumbs.visible { opacity: 1; pointer-events: auto; }
|
|
|
|
|
-
|
|
|
|
|
- .breadcrumb-item {
|
|
|
|
|
- color: var(--accent);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .breadcrumb-item:hover { text-decoration: underline; }
|
|
|
|
|
- .breadcrumb-sep { color: var(--text-muted); }
|
|
|
|
|
-
|
|
|
|
|
- /* Graph controls */
|
|
|
|
|
- #graph-controls {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 14px;
|
|
|
|
|
- right: 14px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- z-index: 5;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .graph-ctrl-btn {
|
|
|
|
|
- width: 32px;
|
|
|
|
|
- height: 32px;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- transition: all 0.15s;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .graph-ctrl-btn:hover {
|
|
|
|
|
- background: var(--bg-hover);
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Context menu */
|
|
|
|
|
- #context-menu {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- padding: 4px 0;
|
|
|
|
|
- z-index: 200;
|
|
|
|
|
- display: none;
|
|
|
|
|
- box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
|
|
|
- min-width: 180px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #context-menu.visible { display: block; }
|
|
|
|
|
-
|
|
|
|
|
- .ctx-item {
|
|
|
|
|
- padding: 7px 14px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- transition: background 0.1s;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .ctx-item:hover { background: var(--bg-hover); }
|
|
|
|
|
- .ctx-item .ctx-icon { font-size: 14px; width: 18px; text-align: center; }
|
|
|
|
|
- .ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
|
|
|
|
-
|
|
|
|
|
- /* Detail panel */
|
|
|
|
|
- #detail-panel {
|
|
|
|
|
- width: 0;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border-left: 1px solid var(--border);
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- transition: width 0.2s ease;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #detail-panel.open { width: var(--panel-width); }
|
|
|
|
|
-
|
|
|
|
|
- #detail-header {
|
|
|
|
|
- padding: 12px 16px;
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: flex-start;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #detail-header .node-title {
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- word-break: break-all;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #detail-header .close-btn {
|
|
|
|
|
- background: none;
|
|
|
|
|
- border: none;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- padding: 0 4px;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #detail-header .close-btn:hover { color: var(--text-primary); }
|
|
|
|
|
-
|
|
|
|
|
- #detail-body {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .detail-section {
|
|
|
|
|
- padding: 12px 16px;
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .detail-section-title {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- letter-spacing: 0.5px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .detail-meta {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: auto 1fr;
|
|
|
|
|
- gap: 4px 12px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .detail-meta .label { color: var(--text-muted); }
|
|
|
|
|
- .detail-meta .value { color: var(--text-secondary); word-break: break-all; }
|
|
|
|
|
- .detail-meta .value.accent { color: var(--accent); }
|
|
|
|
|
-
|
|
|
|
|
- /* Code block */
|
|
|
|
|
- .code-block {
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- overflow-x: auto;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 1.5;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .code-block pre {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- padding: 12px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .code-block code {
|
|
|
|
|
- font-family: var(--font-mono);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Relations list */
|
|
|
|
|
- .relation-list { list-style: none; }
|
|
|
|
|
-
|
|
|
|
|
- .relation-item {
|
|
|
|
|
- padding: 5px 0;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: color 0.1s;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .relation-item:hover { color: var(--accent); }
|
|
|
|
|
-
|
|
|
|
|
- .relation-item .rel-badge {
|
|
|
|
|
- font-size: 9px;
|
|
|
|
|
- padding: 1px 5px;
|
|
|
|
|
- border-radius: 3px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* Loading spinner */
|
|
|
|
|
- .spinner {
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
- width: 16px;
|
|
|
|
|
- height: 16px;
|
|
|
|
|
- border: 2px solid var(--border);
|
|
|
|
|
- border-top-color: var(--accent);
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- animation: spin 0.6s linear infinite;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
|
-
|
|
|
|
|
- /* Scrollbar */
|
|
|
|
|
- ::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
|
|
|
- ::-webkit-scrollbar-track { background: transparent; }
|
|
|
|
|
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
|
|
|
- ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
|
|
|
|
|
-
|
|
|
|
|
- /* Tooltip */
|
|
|
|
|
- .cy-tooltip {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- padding: 6px 10px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- z-index: 50;
|
|
|
|
|
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
|
|
|
- max-width: 300px;
|
|
|
|
|
- display: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .cy-tooltip .tip-kind {
|
|
|
|
|
- font-size: 10px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- margin-bottom: 2px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .cy-tooltip .tip-name { font-weight: 500; }
|
|
|
|
|
- .cy-tooltip .tip-file { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
|
|
|
-
|
|
|
|
|
- /* Notification toast */
|
|
|
|
|
- #toast {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- bottom: 20px;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translateX(-50%) translateY(80px);
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- padding: 10px 20px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- z-index: 300;
|
|
|
|
|
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
|
|
|
- transition: transform 0.3s ease;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #toast.visible { transform: translateX(-50%) translateY(0); }
|
|
|
|
|
-
|
|
|
|
|
- /* Embeddings setup dialog */
|
|
|
|
|
- #dialog-overlay {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- inset: 0;
|
|
|
|
|
- background: rgba(0,0,0,0.7);
|
|
|
|
|
- z-index: 500;
|
|
|
|
|
- display: none;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- backdrop-filter: blur(4px);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-overlay.visible { display: flex; }
|
|
|
|
|
-
|
|
|
|
|
- #dialog {
|
|
|
|
|
- background: var(--bg-secondary);
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 32px;
|
|
|
|
|
- max-width: 480px;
|
|
|
|
|
- width: 90%;
|
|
|
|
|
- box-shadow: 0 16px 48px rgba(0,0,0,0.5);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .dialog-icon { font-size: 36px; margin-bottom: 16px; }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .dialog-title {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .dialog-body {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .dialog-body strong { color: var(--text-primary); }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-progress {
|
|
|
|
|
- display: none;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-progress .progress-bar-track {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 8px;
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-progress .progress-bar-fill {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- background: var(--accent);
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- width: 0%;
|
|
|
|
|
- transition: width 0.2s ease;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-progress .progress-text {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog-progress .progress-percent {
|
|
|
|
|
- float: right;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .dialog-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- justify-content: flex-end;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .btn {
|
|
|
|
|
- padding: 8px 20px;
|
|
|
|
|
- border-radius: var(--radius-sm);
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- border: 1px solid var(--border);
|
|
|
|
|
- transition: all 0.15s;
|
|
|
|
|
- font-family: var(--font-sans);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .btn-primary {
|
|
|
|
|
- background: var(--accent);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- border-color: var(--accent);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .btn-primary:hover { background: var(--accent-hover); }
|
|
|
|
|
- #dialog .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .btn-secondary {
|
|
|
|
|
- background: var(--bg-primary);
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- #dialog .btn-secondary:hover { color: var(--text-primary); }
|
|
|
|
|
- </style>
|
|
|
|
|
-</head>
|
|
|
|
|
-<body>
|
|
|
|
|
- <div id="app">
|
|
|
|
|
- <!-- Header -->
|
|
|
|
|
- <div id="header">
|
|
|
|
|
- <div class="logo">
|
|
|
|
|
- <span class="icon">🔮</span>
|
|
|
|
|
- <span>CodeGraph</span>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div id="search-container">
|
|
|
|
|
- <span class="search-icon">🔍</span>
|
|
|
|
|
- <input id="search-input" type="text" placeholder="Search symbols... (Ctrl+K)" autocomplete="off" spellcheck="false" />
|
|
|
|
|
- <div id="search-results-dropdown"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div id="header-stats">
|
|
|
|
|
- <div class="stat"><span>Nodes:</span> <span class="stat-value" id="stat-nodes">-</span></div>
|
|
|
|
|
- <div class="stat"><span>Edges:</span> <span class="stat-value" id="stat-edges">-</span></div>
|
|
|
|
|
- <div class="stat"><span>Files:</span> <span class="stat-value" id="stat-files">-</span></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Main content -->
|
|
|
|
|
- <div id="main">
|
|
|
|
|
- <!-- Sidebar -->
|
|
|
|
|
- <div id="sidebar">
|
|
|
|
|
- <!-- Graph controls section -->
|
|
|
|
|
- <div class="sidebar-section">
|
|
|
|
|
- <div class="sidebar-header">
|
|
|
|
|
- <span>Graph Actions</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="graph-toolbar">
|
|
|
|
|
- <button class="toolbar-btn" onclick="loadOverview()" title="Show top-level symbols">Overview</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="clearGraph()" title="Clear the graph">Clear</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="runLayout()" title="Re-run layout">Layout</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="fitGraph()" title="Fit graph to view">Fit</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Legend -->
|
|
|
|
|
- <div class="sidebar-section">
|
|
|
|
|
- <div class="sidebar-header" onclick="toggleSection(this)">
|
|
|
|
|
- <span>Legend</span>
|
|
|
|
|
- <span>▶</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="sidebar-content" id="legend" style="display:none;"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Files -->
|
|
|
|
|
- <div class="sidebar-section" style="flex:1; overflow:hidden; display:flex; flex-direction:column;">
|
|
|
|
|
- <div class="sidebar-header" onclick="toggleSection(this)">
|
|
|
|
|
- <span>Files</span>
|
|
|
|
|
- <span>▼</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="sidebar-content" id="file-tree" style="flex:1; max-height:none;"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Graph area -->
|
|
|
|
|
- <div id="graph-container">
|
|
|
|
|
- <div id="cy"></div>
|
|
|
|
|
- <div id="graph-overlay">
|
|
|
|
|
- <div class="overlay-icon">🔮</div>
|
|
|
|
|
- <div class="overlay-title">Search for a starting point</div>
|
|
|
|
|
- <div class="overlay-subtitle">Type a symbol name, pick it, and trace its call chain</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="breadcrumbs"></div>
|
|
|
|
|
- <div id="graph-controls">
|
|
|
|
|
- <button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() * 1.3); cy.center()" title="Zoom in">+</button>
|
|
|
|
|
- <button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() / 1.3); cy.center()" title="Zoom out">−</button>
|
|
|
|
|
- <button class="graph-ctrl-btn" onclick="fitGraph()" title="Fit to view">⤢</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Detail panel -->
|
|
|
|
|
- <div id="detail-panel">
|
|
|
|
|
- <div id="detail-header">
|
|
|
|
|
- <div>
|
|
|
|
|
- <div class="node-title" id="detail-title">-</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button class="close-btn" onclick="closeDetailPanel()">×</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="detail-body"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Embeddings setup dialog -->
|
|
|
|
|
- <div id="dialog-overlay">
|
|
|
|
|
- <div id="dialog">
|
|
|
|
|
- <div class="dialog-icon">🧠</div>
|
|
|
|
|
- <div class="dialog-title" id="dialog-title">Enable Semantic Search</div>
|
|
|
|
|
- <div class="dialog-body" id="dialog-body">
|
|
|
|
|
- CodeGraph Explorer uses <strong>semantic embeddings</strong> to understand your code by meaning, not just keywords.
|
|
|
|
|
- This lets you ask questions like "how does authentication work?" and get accurate results.
|
|
|
|
|
- <br><br>
|
|
|
|
|
- This is a <strong>one-time setup</strong> that generates a local embedding model for this project. No data leaves your machine.
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="dialog-progress">
|
|
|
|
|
- <div class="progress-bar-track">
|
|
|
|
|
- <div class="progress-bar-fill" id="dialog-progress-fill"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="progress-text">
|
|
|
|
|
- <span id="dialog-progress-text">Preparing...</span>
|
|
|
|
|
- <span class="progress-percent" id="dialog-progress-percent">0%</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="dialog-actions" id="dialog-actions">
|
|
|
|
|
- <button class="btn btn-secondary" id="dialog-skip" onclick="closeDialog()">Skip for now</button>
|
|
|
|
|
- <button class="btn btn-primary" id="dialog-enable" onclick="startEmbeddings()">Enable Semantic Search</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Context menu -->
|
|
|
|
|
- <div id="context-menu">
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('expand-callees')"><span class="ctx-icon">→</span> Expand Callees</div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('expand-callers')"><span class="ctx-icon">←</span> Expand Callers</div>
|
|
|
|
|
- <div class="ctx-sep"></div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('callgraph')"><span class="ctx-icon">🌐</span> Full Call Graph</div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('impact')"><span class="ctx-icon">💥</span> Impact Analysis</div>
|
|
|
|
|
- <div class="ctx-sep"></div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('children')"><span class="ctx-icon">📂</span> Show Children</div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('details')"><span class="ctx-icon">📋</span> View Details</div>
|
|
|
|
|
- <div class="ctx-sep"></div>
|
|
|
|
|
- <div class="ctx-item" onclick="ctxAction('remove')"><span class="ctx-icon">✖</span> Remove from Graph</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Tooltip -->
|
|
|
|
|
- <div class="cy-tooltip" id="tooltip"></div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Toast -->
|
|
|
|
|
- <div id="toast"></div>
|
|
|
|
|
-
|
|
|
|
|
- <script>
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // State
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- let cy;
|
|
|
|
|
- let ctxNodeId = null;
|
|
|
|
|
- let searchDebounce = null;
|
|
|
|
|
- const expandedSets = { callers: new Set(), callees: new Set() };
|
|
|
|
|
-
|
|
|
|
|
- // Node kind → color mapping
|
|
|
|
|
- const kindColors = {
|
|
|
|
|
- 'function': '#79c0ff',
|
|
|
|
|
- 'method': '#7ee787',
|
|
|
|
|
- 'class': '#d2a8ff',
|
|
|
|
|
- 'interface': '#ffa657',
|
|
|
|
|
- 'struct': '#ffa657',
|
|
|
|
|
- 'trait': '#ffa657',
|
|
|
|
|
- 'protocol': '#ffa657',
|
|
|
|
|
- 'component': '#f778ba',
|
|
|
|
|
- 'enum': '#d29922',
|
|
|
|
|
- 'enum_member': '#d29922',
|
|
|
|
|
- 'type_alias': '#d2a8ff',
|
|
|
|
|
- 'variable': '#ff7b72',
|
|
|
|
|
- 'constant': '#ff7b72',
|
|
|
|
|
- 'property': '#76e3ea',
|
|
|
|
|
- 'field': '#76e3ea',
|
|
|
|
|
- 'file': '#8b949e',
|
|
|
|
|
- 'module': '#8b949e',
|
|
|
|
|
- 'namespace': '#8b949e',
|
|
|
|
|
- 'import': '#f0883e',
|
|
|
|
|
- 'export': '#3fb950',
|
|
|
|
|
- 'route': '#f778ba',
|
|
|
|
|
- 'parameter': '#8b949e',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const kindShapes = {
|
|
|
|
|
- 'class': 'round-rectangle',
|
|
|
|
|
- 'interface': 'round-diamond',
|
|
|
|
|
- 'struct': 'round-rectangle',
|
|
|
|
|
- 'trait': 'round-diamond',
|
|
|
|
|
- 'protocol': 'round-diamond',
|
|
|
|
|
- 'enum': 'round-hexagon',
|
|
|
|
|
- 'component': 'round-pentagon',
|
|
|
|
|
- 'file': 'round-rectangle',
|
|
|
|
|
- 'module': 'round-rectangle',
|
|
|
|
|
- 'namespace': 'round-rectangle',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const edgeColors = {
|
|
|
|
|
- 'calls': '#58a6ff',
|
|
|
|
|
- 'imports': '#f0883e',
|
|
|
|
|
- 'extends': '#d2a8ff',
|
|
|
|
|
- 'implements': '#ffa657',
|
|
|
|
|
- 'references': '#8b949e',
|
|
|
|
|
- 'contains': '#3d444d',
|
|
|
|
|
- 'type_of': '#76e3ea',
|
|
|
|
|
- 'returns': '#76e3ea',
|
|
|
|
|
- 'instantiates': '#f778ba',
|
|
|
|
|
- 'overrides': '#d29922',
|
|
|
|
|
- 'decorates': '#f778ba',
|
|
|
|
|
- 'exports': '#3fb950',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // API Client
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- const api = {
|
|
|
|
|
- async get(path) {
|
|
|
|
|
- const res = await fetch('/api/' + path);
|
|
|
|
|
- if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
|
|
|
- return res.json();
|
|
|
|
|
- },
|
|
|
|
|
- embeddingsStatus: () => api.get('embeddings/status'),
|
|
|
|
|
- status: () => api.get('status'),
|
|
|
|
|
- search: (q, kind, limit) => api.get(`search?q=${encodeURIComponent(q)}${kind ? '&kind='+kind : ''}&limit=${limit||30}`),
|
|
|
|
|
- explore: (q) => api.get(`explore?q=${encodeURIComponent(q)}`),
|
|
|
|
|
- overview: (limit) => api.get(`overview?limit=${limit||60}`),
|
|
|
|
|
- files: () => api.get('files'),
|
|
|
|
|
- fileNodes: (p) => api.get(`file-nodes?path=${encodeURIComponent(p)}`),
|
|
|
|
|
- node: (id) => api.get(`node/${encodeURIComponent(id)}`),
|
|
|
|
|
- callers: (id, d) => api.get(`node/${encodeURIComponent(id)}/callers?depth=${d||1}`),
|
|
|
|
|
- callees: (id, d) => api.get(`node/${encodeURIComponent(id)}/callees?depth=${d||1}`),
|
|
|
|
|
- children: (id) => api.get(`node/${encodeURIComponent(id)}/children`),
|
|
|
|
|
- impact: (id, d) => api.get(`node/${encodeURIComponent(id)}/impact?depth=${d||2}`),
|
|
|
|
|
- callgraph: (id, d) => api.get(`node/${encodeURIComponent(id)}/callgraph?depth=${d||2}`),
|
|
|
|
|
- context: (id) => api.get(`node/${encodeURIComponent(id)}/context`),
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Cytoscape Initialization
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function initCytoscape() {
|
|
|
|
|
- cy = cytoscape({
|
|
|
|
|
- container: document.getElementById('cy'),
|
|
|
|
|
- style: [
|
|
|
|
|
- // Nodes
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'node',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'label': 'data(label)',
|
|
|
|
|
- 'text-valign': 'center',
|
|
|
|
|
- 'text-halign': 'center',
|
|
|
|
|
- 'font-size': '12px',
|
|
|
|
|
- 'font-weight': 'bold',
|
|
|
|
|
- 'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
|
|
|
- 'color': '#ffffff',
|
|
|
|
|
- 'text-outline-color': '#000000',
|
|
|
|
|
- 'text-outline-width': 2,
|
|
|
|
|
- 'text-outline-opacity': 0.6,
|
|
|
|
|
- 'background-color': 'data(color)',
|
|
|
|
|
- 'background-opacity': 0.85,
|
|
|
|
|
- 'border-width': 2,
|
|
|
|
|
- 'border-color': 'data(color)',
|
|
|
|
|
- 'border-opacity': 0.7,
|
|
|
|
|
- 'width': 'label',
|
|
|
|
|
- 'height': 'label',
|
|
|
|
|
- 'padding': '12px',
|
|
|
|
|
- 'shape': 'data(shape)',
|
|
|
|
|
- 'text-wrap': 'wrap',
|
|
|
|
|
- 'text-max-width': '180px',
|
|
|
|
|
- 'transition-property': 'background-opacity, border-color, border-opacity, opacity, text-opacity',
|
|
|
|
|
- 'transition-duration': '0.2s',
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Selected node
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'node:selected',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'border-width': 3,
|
|
|
|
|
- 'border-color': '#ffffff',
|
|
|
|
|
- 'border-opacity': 1,
|
|
|
|
|
- 'background-opacity': 1,
|
|
|
|
|
- 'z-index': 10,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Hovered node
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'node.hover',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'border-width': 3,
|
|
|
|
|
- 'border-color': '#ffffff',
|
|
|
|
|
- 'border-opacity': 0.9,
|
|
|
|
|
- 'background-opacity': 1,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Faded node — keep text readable
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'node.faded',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'background-opacity': 0.3,
|
|
|
|
|
- 'border-opacity': 0.2,
|
|
|
|
|
- 'text-opacity': 0.7,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Highlighted node
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'node.highlighted',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'border-width': 3,
|
|
|
|
|
- 'border-color': '#f0e68c',
|
|
|
|
|
- 'border-opacity': 1,
|
|
|
|
|
- 'z-index': 10,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Edges
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'edge',
|
|
|
|
|
- style: {
|
|
|
|
|
- 'width': 1.5,
|
|
|
|
|
- 'line-color': 'data(color)',
|
|
|
|
|
- 'target-arrow-color': 'data(color)',
|
|
|
|
|
- 'target-arrow-shape': 'triangle',
|
|
|
|
|
- 'arrow-scale': 0.8,
|
|
|
|
|
- 'curve-style': 'bezier',
|
|
|
|
|
- 'opacity': 0.6,
|
|
|
|
|
- 'label': 'data(label)',
|
|
|
|
|
- 'font-size': '9px',
|
|
|
|
|
- 'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
|
|
|
- 'color': '#656d76',
|
|
|
|
|
- 'text-rotation': 'autorotate',
|
|
|
|
|
- 'text-margin-y': -8,
|
|
|
|
|
- 'text-outline-color': '#0d1117',
|
|
|
|
|
- 'text-outline-width': 2,
|
|
|
|
|
- 'transition-property': 'opacity, line-color',
|
|
|
|
|
- 'transition-duration': '0.15s',
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- // Selected edge
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'edge:selected',
|
|
|
|
|
- style: { 'opacity': 1, 'width': 2.5 }
|
|
|
|
|
- },
|
|
|
|
|
- // Faded edge
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'edge.faded',
|
|
|
|
|
- style: { 'opacity': 0.15 }
|
|
|
|
|
- },
|
|
|
|
|
- // Highlighted edge
|
|
|
|
|
- {
|
|
|
|
|
- selector: 'edge.highlighted',
|
|
|
|
|
- style: { 'opacity': 1, 'width': 2.5 }
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- layout: { name: 'preset' },
|
|
|
|
|
- minZoom: 0.1,
|
|
|
|
|
- maxZoom: 4,
|
|
|
|
|
- wheelSensitivity: 0.3,
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Event handlers
|
|
|
|
|
- cy.on('tap', 'node', (e) => {
|
|
|
|
|
- const nodeId = e.target.data('nodeId');
|
|
|
|
|
- if (nodeId) showNodeDetails(nodeId);
|
|
|
|
|
- highlightNeighborhood(e.target);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- cy.on('cxttap', 'node', (e) => {
|
|
|
|
|
- e.originalEvent.preventDefault();
|
|
|
|
|
- ctxNodeId = e.target.data('nodeId');
|
|
|
|
|
- showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- cy.on('tap', (e) => {
|
|
|
|
|
- if (e.target === cy) {
|
|
|
|
|
- clearHighlights();
|
|
|
|
|
- hideContextMenu();
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- cy.on('mouseover', 'node', (e) => {
|
|
|
|
|
- e.target.addClass('hover');
|
|
|
|
|
- showTooltip(e);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- cy.on('mouseout', 'node', (e) => {
|
|
|
|
|
- e.target.removeClass('hover');
|
|
|
|
|
- hideTooltip();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- cy.on('dblclick', 'node', (e) => {
|
|
|
|
|
- const nodeId = e.target.data('nodeId');
|
|
|
|
|
- if (nodeId) expandCallees(nodeId);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Click outside to close context menu
|
|
|
|
|
- document.addEventListener('click', (e) => {
|
|
|
|
|
- if (!e.target.closest('#context-menu')) hideContextMenu();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('contextmenu', (e) => {
|
|
|
|
|
- if (e.target.closest('#cy')) e.preventDefault();
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Graph Operations
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- const kindLabels = {
|
|
|
|
|
- 'function': 'fn', 'method': 'method', 'class': 'class', 'interface': 'iface',
|
|
|
|
|
- 'component': 'comp', 'route': 'route', 'enum': 'enum', 'type_alias': 'type',
|
|
|
|
|
- 'struct': 'struct', 'trait': 'trait', 'variable': 'var', 'constant': 'const',
|
|
|
|
|
- 'property': 'prop', 'field': 'field', 'file': 'file', 'module': 'mod',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- function addNodeToGraph(node) {
|
|
|
|
|
- if (cy.getElementById(node.id).length > 0) return;
|
|
|
|
|
- const color = kindColors[node.kind] || '#8b949e';
|
|
|
|
|
- const shape = kindShapes[node.kind] || 'round-rectangle';
|
|
|
|
|
- const kindLabel = kindLabels[node.kind] || node.kind;
|
|
|
|
|
- cy.add({
|
|
|
|
|
- group: 'nodes',
|
|
|
|
|
- data: {
|
|
|
|
|
- id: node.id,
|
|
|
|
|
- nodeId: node.id,
|
|
|
|
|
- label: `${node.name}\n${kindLabel}`,
|
|
|
|
|
- color: color,
|
|
|
|
|
- shape: shape,
|
|
|
|
|
- kind: node.kind,
|
|
|
|
|
- filePath: node.filePath,
|
|
|
|
|
- signature: node.signature || '',
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function addEdgeToGraph(edge) {
|
|
|
|
|
- const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
|
|
|
|
|
- if (cy.getElementById(edgeId).length > 0) return;
|
|
|
|
|
- // Don't add edge if source or target not in graph
|
|
|
|
|
- if (cy.getElementById(edge.source).length === 0 || cy.getElementById(edge.target).length === 0) return;
|
|
|
|
|
- const color = edgeColors[edge.kind] || '#8b949e';
|
|
|
|
|
- cy.add({
|
|
|
|
|
- group: 'edges',
|
|
|
|
|
- data: {
|
|
|
|
|
- id: edgeId,
|
|
|
|
|
- source: edge.source,
|
|
|
|
|
- target: edge.target,
|
|
|
|
|
- kind: edge.kind,
|
|
|
|
|
- label: edge.kind,
|
|
|
|
|
- color: color,
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function addSubgraph(nodes, edges) {
|
|
|
|
|
- const batchElements = [];
|
|
|
|
|
- for (const node of nodes) {
|
|
|
|
|
- if (cy.getElementById(node.id).length > 0) continue;
|
|
|
|
|
- const color = kindColors[node.kind] || '#8b949e';
|
|
|
|
|
- const shape = kindShapes[node.kind] || 'round-rectangle';
|
|
|
|
|
- batchElements.push({
|
|
|
|
|
- group: 'nodes',
|
|
|
|
|
- data: {
|
|
|
|
|
- id: node.id,
|
|
|
|
|
- nodeId: node.id,
|
|
|
|
|
- label: node.name,
|
|
|
|
|
- color: color,
|
|
|
|
|
- shape: shape,
|
|
|
|
|
- kind: node.kind,
|
|
|
|
|
- filePath: node.filePath,
|
|
|
|
|
- signature: node.signature || '',
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- for (const edge of edges) {
|
|
|
|
|
- const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
|
|
|
|
|
- if (cy.getElementById(edgeId).length > 0) continue;
|
|
|
|
|
- // Check source/target will exist
|
|
|
|
|
- const srcExists = cy.getElementById(edge.source).length > 0 || batchElements.some(e => e.data.id === edge.source);
|
|
|
|
|
- const tgtExists = cy.getElementById(edge.target).length > 0 || batchElements.some(e => e.data.id === edge.target);
|
|
|
|
|
- if (!srcExists || !tgtExists) continue;
|
|
|
|
|
- const color = edgeColors[edge.kind] || '#8b949e';
|
|
|
|
|
- batchElements.push({
|
|
|
|
|
- group: 'edges',
|
|
|
|
|
- data: {
|
|
|
|
|
- id: edgeId,
|
|
|
|
|
- source: edge.source,
|
|
|
|
|
- target: edge.target,
|
|
|
|
|
- kind: edge.kind,
|
|
|
|
|
- label: edge.kind,
|
|
|
|
|
- color: color,
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- if (batchElements.length > 0) {
|
|
|
|
|
- cy.add(batchElements);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function clearGraph() {
|
|
|
|
|
- cy.elements().remove();
|
|
|
|
|
- expandedSets.callers.clear();
|
|
|
|
|
- expandedSets.callees.clear();
|
|
|
|
|
- hideOverlay(false);
|
|
|
|
|
- closeDetailPanel();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function runLayout() {
|
|
|
|
|
- if (cy.nodes().length === 0) return;
|
|
|
|
|
- const layout = cy.layout({
|
|
|
|
|
- name: 'dagre',
|
|
|
|
|
- rankDir: 'LR',
|
|
|
|
|
- nodeSep: 50,
|
|
|
|
|
- rankSep: 80,
|
|
|
|
|
- edgeSep: 20,
|
|
|
|
|
- animate: true,
|
|
|
|
|
- animationDuration: 300,
|
|
|
|
|
- fit: true,
|
|
|
|
|
- padding: 40,
|
|
|
|
|
- });
|
|
|
|
|
- layout.run();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function fitGraph() {
|
|
|
|
|
- if (cy.nodes().length > 0) {
|
|
|
|
|
- cy.animate({ fit: { eles: cy.elements(), padding: 40 } }, { duration: 300 });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function highlightNeighborhood(node) {
|
|
|
|
|
- clearHighlights();
|
|
|
|
|
- const neighborhood = node.closedNeighborhood();
|
|
|
|
|
- cy.elements().not(neighborhood).addClass('faded');
|
|
|
|
|
- neighborhood.edges().addClass('highlighted');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function clearHighlights() {
|
|
|
|
|
- cy.elements().removeClass('faded highlighted');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function hideOverlay(hide = true) {
|
|
|
|
|
- const overlay = document.getElementById('graph-overlay');
|
|
|
|
|
- overlay.style.display = hide ? 'none' : 'block';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Data Loading
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- async function loadOverview() {
|
|
|
|
|
- showToast('Loading overview...');
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.overview(60);
|
|
|
|
|
- if (data.nodes.length === 0) {
|
|
|
|
|
- showToast('No symbols found. Is the project indexed?');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- clearGraph();
|
|
|
|
|
- hideOverlay();
|
|
|
|
|
- for (const node of data.nodes) addNodeToGraph(node);
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Loaded ${data.nodes.length} symbols`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function expandCallers(nodeId) {
|
|
|
|
|
- if (expandedSets.callers.has(nodeId)) return;
|
|
|
|
|
- expandedSets.callers.add(nodeId);
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.callers(nodeId, 1);
|
|
|
|
|
- if (data.items.length === 0) {
|
|
|
|
|
- showToast('No callers found');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- for (const item of data.items) {
|
|
|
|
|
- addNodeToGraph(item.node);
|
|
|
|
|
- addEdgeToGraph(item.edge);
|
|
|
|
|
- }
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Found ${data.items.length} callers`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function expandCallees(nodeId) {
|
|
|
|
|
- if (expandedSets.callees.has(nodeId)) return;
|
|
|
|
|
- expandedSets.callees.add(nodeId);
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.callees(nodeId, 1);
|
|
|
|
|
- if (data.items.length === 0) {
|
|
|
|
|
- showToast('No callees found');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- for (const item of data.items) {
|
|
|
|
|
- addNodeToGraph(item.node);
|
|
|
|
|
- addEdgeToGraph(item.edge);
|
|
|
|
|
- }
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Found ${data.items.length} callees`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadCallGraph(nodeId) {
|
|
|
|
|
- showToast('Loading call graph...');
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.callgraph(nodeId, 2);
|
|
|
|
|
- addSubgraph(data.nodes, data.edges);
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Loaded call graph: ${data.nodes.length} nodes`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadImpact(nodeId) {
|
|
|
|
|
- showToast('Analyzing impact...');
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.impact(nodeId, 2);
|
|
|
|
|
- addSubgraph(data.nodes, data.edges);
|
|
|
|
|
- runLayout();
|
|
|
|
|
- // Highlight the root
|
|
|
|
|
- const rootEle = cy.getElementById(nodeId);
|
|
|
|
|
- if (rootEle.length > 0) {
|
|
|
|
|
- rootEle.addClass('highlighted');
|
|
|
|
|
- }
|
|
|
|
|
- showToast(`Impact: ${data.nodes.length} nodes potentially affected`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadChildren(nodeId) {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.children(nodeId);
|
|
|
|
|
- if (data.children.length === 0) {
|
|
|
|
|
- showToast('No children found');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- for (const child of data.children) {
|
|
|
|
|
- addNodeToGraph(child);
|
|
|
|
|
- // Add contains edge
|
|
|
|
|
- addEdgeToGraph({ source: nodeId, target: child.id, kind: 'contains' });
|
|
|
|
|
- }
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Found ${data.children.length} children`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadFileNodes(filePath) {
|
|
|
|
|
- showToast('Loading file symbols...');
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.fileNodes(filePath);
|
|
|
|
|
- if (data.nodes.length === 0) {
|
|
|
|
|
- showToast('No symbols in this file');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- clearGraph();
|
|
|
|
|
- hideOverlay();
|
|
|
|
|
- for (const node of data.nodes) addNodeToGraph(node);
|
|
|
|
|
- runLayout();
|
|
|
|
|
- showToast(`Loaded ${data.nodes.length} symbols from file`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Explore — natural language question → graph
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- async function exploreQuery(question) {
|
|
|
|
|
- hideSearchDropdown();
|
|
|
|
|
- clearGraph();
|
|
|
|
|
- hideOverlay();
|
|
|
|
|
- document.getElementById('search-input').value = question;
|
|
|
|
|
-
|
|
|
|
|
- showToast('Finding entry point...');
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.explore(question);
|
|
|
|
|
- if (data.nodes.length === 0) {
|
|
|
|
|
- showToast('No relevant code found. Try searching for a specific symbol.');
|
|
|
|
|
- hideOverlay(false);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- addSubgraph(data.nodes, data.edges);
|
|
|
|
|
- runLayout();
|
|
|
|
|
-
|
|
|
|
|
- // Center on entry point
|
|
|
|
|
- if (data.entryPoint) {
|
|
|
|
|
- const entryEle = cy.getElementById(data.entryPoint);
|
|
|
|
|
- if (entryEle.length > 0) {
|
|
|
|
|
- entryEle.select();
|
|
|
|
|
- entryEle.addClass('highlighted');
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- cy.animate({ center: { eles: entryEle } }, { duration: 400 });
|
|
|
|
|
- showNodeDetails(data.entryPoint);
|
|
|
|
|
- }, 350);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const source = data.usedClaude ? ' (via Claude)' : '';
|
|
|
|
|
- showToast(`Traced ${data.nodes.length} symbols from entry point${source}`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Search
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function onSearchInput(e) {
|
|
|
|
|
- const query = e.target.value.trim();
|
|
|
|
|
- clearTimeout(searchDebounce);
|
|
|
|
|
- if (!query) {
|
|
|
|
|
- hideSearchDropdown();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- searchDebounce = setTimeout(async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.search(query, null, 20);
|
|
|
|
|
- showSearchResults(data.results);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Search error:', err);
|
|
|
|
|
- }
|
|
|
|
|
- }, 200);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function onSearchKeydown(e) {
|
|
|
|
|
- if (e.key === 'Enter') {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- const query = e.target.value.trim();
|
|
|
|
|
- if (!query) return;
|
|
|
|
|
-
|
|
|
|
|
- // If dropdown is visible and has results, select the first one
|
|
|
|
|
- const dropdown = document.getElementById('search-results-dropdown');
|
|
|
|
|
- const firstItem = dropdown.querySelector('.search-result-item');
|
|
|
|
|
- if (dropdown.classList.contains('visible') && firstItem) {
|
|
|
|
|
- firstItem.click();
|
|
|
|
|
- } else {
|
|
|
|
|
- // Trigger a search and auto-select first result
|
|
|
|
|
- (async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.search(query, null, 10);
|
|
|
|
|
- if (data.results.length > 0) {
|
|
|
|
|
- selectSearchResult(data.results[0].node.id);
|
|
|
|
|
- } else {
|
|
|
|
|
- showToast('No symbols found. Try a different search.');
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Search error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- })();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function showSearchResults(results) {
|
|
|
|
|
- const dropdown = document.getElementById('search-results-dropdown');
|
|
|
|
|
- if (results.length === 0) {
|
|
|
|
|
- dropdown.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:13px;">No results found</div>';
|
|
|
|
|
- dropdown.classList.add('visible');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- dropdown.innerHTML = results.map(r => `
|
|
|
|
|
- <div class="search-result-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
|
|
|
- <span class="kind-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
|
|
|
- <span class="name">${escapeHtml(r.node.name)}</span>
|
|
|
|
|
- <span class="file-path">${escapeHtml(r.node.filePath)}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- `).join('');
|
|
|
|
|
- dropdown.classList.add('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function hideSearchDropdown() {
|
|
|
|
|
- document.getElementById('search-results-dropdown').classList.remove('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function selectSearchResult(nodeId) {
|
|
|
|
|
- hideSearchDropdown();
|
|
|
|
|
- document.getElementById('search-input').value = '';
|
|
|
|
|
- hideOverlay();
|
|
|
|
|
- clearGraph();
|
|
|
|
|
- hideOverlay();
|
|
|
|
|
-
|
|
|
|
|
- showToast('Tracing call chain...');
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // Load the call graph from this entry point (depth 3 forward)
|
|
|
|
|
- const data = await api.callgraph(nodeId, 3);
|
|
|
|
|
- if (data.nodes.length === 0) {
|
|
|
|
|
- // Fallback: just show the node
|
|
|
|
|
- const nodeData = await api.node(nodeId);
|
|
|
|
|
- if (nodeData.node) addNodeToGraph(nodeData.node);
|
|
|
|
|
- } else {
|
|
|
|
|
- addSubgraph(data.nodes, data.edges);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- runLayout();
|
|
|
|
|
-
|
|
|
|
|
- // Select and center on the entry point
|
|
|
|
|
- const ele = cy.getElementById(nodeId);
|
|
|
|
|
- if (ele.length > 0) {
|
|
|
|
|
- ele.select();
|
|
|
|
|
- ele.addClass('highlighted');
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- cy.animate({ center: { eles: ele } }, { duration: 300 });
|
|
|
|
|
- }, 350);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- showNodeDetails(nodeId);
|
|
|
|
|
- showToast(`Traced ${data.nodes.length} symbols from entry point`);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- showToast('Error: ' + err.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Detail Panel
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- async function showNodeDetails(nodeId) {
|
|
|
|
|
- const panel = document.getElementById('detail-panel');
|
|
|
|
|
- const body = document.getElementById('detail-body');
|
|
|
|
|
- const title = document.getElementById('detail-title');
|
|
|
|
|
-
|
|
|
|
|
- panel.classList.add('open');
|
|
|
|
|
-
|
|
|
|
|
- body.innerHTML = '<div style="padding:20px;text-align:center;"><div class="spinner"></div></div>';
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const [nodeData, contextData] = await Promise.all([
|
|
|
|
|
- api.node(nodeId),
|
|
|
|
|
- api.context(nodeId),
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- const node = nodeData.node;
|
|
|
|
|
- const code = nodeData.code;
|
|
|
|
|
- const ancestors = nodeData.ancestors || [];
|
|
|
|
|
- const ctx = contextData.context;
|
|
|
|
|
-
|
|
|
|
|
- title.textContent = node.name;
|
|
|
|
|
-
|
|
|
|
|
- let html = '';
|
|
|
|
|
-
|
|
|
|
|
- // Quick actions
|
|
|
|
|
- html += `<div class="detail-section" style="padding:8px 16px;">
|
|
|
|
|
- <div style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
|
|
|
- <button class="toolbar-btn" onclick="expandCallees('${escapeAttr(node.id)}')" style="font-size:12px;">Expand Callees →</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="expandCallers('${escapeAttr(node.id)}')" style="font-size:12px;">← Expand Callers</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="loadCallGraph('${escapeAttr(node.id)}')" style="font-size:12px;">Full Call Graph</button>
|
|
|
|
|
- <button class="toolbar-btn" onclick="loadImpact('${escapeAttr(node.id)}')" style="font-size:12px;">Impact Analysis</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
-
|
|
|
|
|
- // Meta info
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Info</div>
|
|
|
|
|
- <div class="detail-meta">
|
|
|
|
|
- <span class="label">Kind</span>
|
|
|
|
|
- <span class="value"><span class="kind-badge" style="background:${kindColors[node.kind] || '#8b949e'}22;color:${kindColors[node.kind] || '#8b949e'};font-size:10px;padding:1px 5px;border-radius:3px;">${node.kind}</span></span>
|
|
|
|
|
- <span class="label">File</span>
|
|
|
|
|
- <span class="value accent">${escapeHtml(node.filePath)}</span>
|
|
|
|
|
- <span class="label">Lines</span>
|
|
|
|
|
- <span class="value">${node.startLine} - ${node.endLine}</span>
|
|
|
|
|
- ${node.signature ? `<span class="label">Signature</span><span class="value" style="font-family:var(--font-mono);font-size:11px;">${escapeHtml(node.signature)}</span>` : ''}
|
|
|
|
|
- ${node.visibility ? `<span class="label">Visibility</span><span class="value">${node.visibility}</span>` : ''}
|
|
|
|
|
- ${node.isExported ? `<span class="label">Exported</span><span class="value">Yes</span>` : ''}
|
|
|
|
|
- ${node.isAsync ? `<span class="label">Async</span><span class="value">Yes</span>` : ''}
|
|
|
|
|
- ${node.decorators && node.decorators.length ? `<span class="label">Decorators</span><span class="value">${escapeHtml(node.decorators.join(', '))}</span>` : ''}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
-
|
|
|
|
|
- // Breadcrumb ancestors
|
|
|
|
|
- if (ancestors.length > 0) {
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Hierarchy</div>
|
|
|
|
|
- <div style="font-size:12px;color:var(--text-secondary);">
|
|
|
|
|
- ${ancestors.map(a => `<span class="relation-item" onclick="selectSearchResult('${escapeAttr(a.id)}')" style="display:inline;cursor:pointer;color:var(--accent);">${escapeHtml(a.name)}</span>`).join(' <span style="color:var(--text-muted);">›</span> ')} <span style="color:var(--text-muted);">›</span> <strong>${escapeHtml(node.name)}</strong>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Source code
|
|
|
|
|
- if (code) {
|
|
|
|
|
- const lang = langForHighlight(node.language);
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Source Code</div>
|
|
|
|
|
- <div class="code-block"><pre><code class="language-${lang}">${escapeHtml(code)}</code></pre></div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Callers
|
|
|
|
|
- if (ctx.incomingRefs && ctx.incomingRefs.length > 0) {
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Called By (${ctx.incomingRefs.length})</div>
|
|
|
|
|
- <ul class="relation-list">
|
|
|
|
|
- ${ctx.incomingRefs.slice(0, 20).map(r => `
|
|
|
|
|
- <li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
|
|
|
- <span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
|
|
|
- ${escapeHtml(r.node.name)}
|
|
|
|
|
- <span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
|
|
|
|
|
- </li>
|
|
|
|
|
- `).join('')}
|
|
|
|
|
- </ul>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Callees
|
|
|
|
|
- if (ctx.outgoingRefs && ctx.outgoingRefs.length > 0) {
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Calls (${ctx.outgoingRefs.length})</div>
|
|
|
|
|
- <ul class="relation-list">
|
|
|
|
|
- ${ctx.outgoingRefs.slice(0, 20).map(r => `
|
|
|
|
|
- <li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
|
|
|
- <span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
|
|
|
- ${escapeHtml(r.node.name)}
|
|
|
|
|
- <span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
|
|
|
|
|
- </li>
|
|
|
|
|
- `).join('')}
|
|
|
|
|
- </ul>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Children
|
|
|
|
|
- if (ctx.children && ctx.children.length > 0) {
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Contains (${ctx.children.length})</div>
|
|
|
|
|
- <ul class="relation-list">
|
|
|
|
|
- ${ctx.children.slice(0, 30).map(c => `
|
|
|
|
|
- <li class="relation-item" onclick="selectSearchResult('${escapeAttr(c.id)}')">
|
|
|
|
|
- <span class="rel-badge" style="background:${kindColors[c.kind] || '#8b949e'}22;color:${kindColors[c.kind] || '#8b949e'}">${c.kind}</span>
|
|
|
|
|
- ${escapeHtml(c.name)}
|
|
|
|
|
- </li>
|
|
|
|
|
- `).join('')}
|
|
|
|
|
- </ul>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Docstring
|
|
|
|
|
- if (node.docstring) {
|
|
|
|
|
- html += `<div class="detail-section">
|
|
|
|
|
- <div class="detail-section-title">Documentation</div>
|
|
|
|
|
- <div style="font-size:12px;color:var(--text-secondary);white-space:pre-wrap;font-family:var(--font-mono);line-height:1.5;">${escapeHtml(node.docstring)}</div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- body.innerHTML = html;
|
|
|
|
|
-
|
|
|
|
|
- // Apply syntax highlighting
|
|
|
|
|
- body.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
|
|
|
|
-
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- body.innerHTML = `<div style="padding:20px;color:var(--red);">Error loading details: ${escapeHtml(err.message)}</div>`;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function closeDetailPanel() {
|
|
|
|
|
- document.getElementById('detail-panel').classList.remove('open');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Context Menu
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function showContextMenu(x, y) {
|
|
|
|
|
- const menu = document.getElementById('context-menu');
|
|
|
|
|
- menu.style.left = x + 'px';
|
|
|
|
|
- menu.style.top = y + 'px';
|
|
|
|
|
- menu.classList.add('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function hideContextMenu() {
|
|
|
|
|
- document.getElementById('context-menu').classList.remove('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function ctxAction(action) {
|
|
|
|
|
- hideContextMenu();
|
|
|
|
|
- if (!ctxNodeId) return;
|
|
|
|
|
- switch (action) {
|
|
|
|
|
- case 'expand-callees': expandCallees(ctxNodeId); break;
|
|
|
|
|
- case 'expand-callers': expandCallers(ctxNodeId); break;
|
|
|
|
|
- case 'callgraph': loadCallGraph(ctxNodeId); break;
|
|
|
|
|
- case 'impact': loadImpact(ctxNodeId); break;
|
|
|
|
|
- case 'children': loadChildren(ctxNodeId); break;
|
|
|
|
|
- case 'details': showNodeDetails(ctxNodeId); break;
|
|
|
|
|
- case 'remove':
|
|
|
|
|
- cy.getElementById(ctxNodeId).remove();
|
|
|
|
|
- expandedSets.callers.delete(ctxNodeId);
|
|
|
|
|
- expandedSets.callees.delete(ctxNodeId);
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Tooltip
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function showTooltip(e) {
|
|
|
|
|
- const node = e.target;
|
|
|
|
|
- const tip = document.getElementById('tooltip');
|
|
|
|
|
- const pos = e.originalEvent;
|
|
|
|
|
- tip.innerHTML = `
|
|
|
|
|
- <div class="tip-kind">${node.data('kind')}</div>
|
|
|
|
|
- <div class="tip-name">${escapeHtml(node.data('label'))}</div>
|
|
|
|
|
- ${node.data('signature') ? `<div class="tip-file" style="font-family:var(--font-mono);">${escapeHtml(node.data('signature'))}</div>` : ''}
|
|
|
|
|
- <div class="tip-file">${escapeHtml(node.data('filePath') || '')}</div>
|
|
|
|
|
- `;
|
|
|
|
|
- tip.style.left = (pos.clientX + 12) + 'px';
|
|
|
|
|
- tip.style.top = (pos.clientY + 12) + 'px';
|
|
|
|
|
- tip.style.display = 'block';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function hideTooltip() {
|
|
|
|
|
- document.getElementById('tooltip').style.display = 'none';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Toast notifications
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function showToast(msg) {
|
|
|
|
|
- const toast = document.getElementById('toast');
|
|
|
|
|
- toast.textContent = msg;
|
|
|
|
|
- toast.classList.add('visible');
|
|
|
|
|
- clearTimeout(toast._timeout);
|
|
|
|
|
- toast._timeout = setTimeout(() => toast.classList.remove('visible'), 2500);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Sidebar
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function toggleSection(header) {
|
|
|
|
|
- const content = header.nextElementSibling;
|
|
|
|
|
- const arrow = header.querySelector('span:last-child');
|
|
|
|
|
- if (content.style.display === 'none') {
|
|
|
|
|
- content.style.display = '';
|
|
|
|
|
- arrow.innerHTML = '▼';
|
|
|
|
|
- } else {
|
|
|
|
|
- content.style.display = 'none';
|
|
|
|
|
- arrow.innerHTML = '▶';
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function loadFileTree() {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.files();
|
|
|
|
|
- const tree = document.getElementById('file-tree');
|
|
|
|
|
- if (data.files.length === 0) {
|
|
|
|
|
- tree.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:12px;">No files indexed</div>';
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- // Group by directory
|
|
|
|
|
- const dirs = {};
|
|
|
|
|
- for (const f of data.files) {
|
|
|
|
|
- const dir = f.filePath.split('/').slice(0, -1).join('/') || '.';
|
|
|
|
|
- if (!dirs[dir]) dirs[dir] = [];
|
|
|
|
|
- dirs[dir].push(f);
|
|
|
|
|
- }
|
|
|
|
|
- let html = '';
|
|
|
|
|
- const sortedDirs = Object.keys(dirs).sort();
|
|
|
|
|
- for (const dir of sortedDirs) {
|
|
|
|
|
- html += `<div class="file-item" style="color:var(--text-muted);font-weight:500;padding-top:8px;" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? '' : 'none'">
|
|
|
|
|
- <span class="file-icon">📁</span> ${escapeHtml(dir)}/
|
|
|
|
|
- </div><div>`;
|
|
|
|
|
- for (const f of dirs[dir].sort((a, b) => a.filePath.localeCompare(b.filePath))) {
|
|
|
|
|
- const fileName = f.filePath.split('/').pop();
|
|
|
|
|
- html += `<div class="file-item" style="padding-left:28px;" onclick="loadFileNodes('${escapeAttr(f.filePath)}')">
|
|
|
|
|
- <span class="file-icon">📄</span> ${escapeHtml(fileName)}
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
- html += '</div>';
|
|
|
|
|
- }
|
|
|
|
|
- tree.innerHTML = html;
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Failed to load file tree:', err);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function buildLegend() {
|
|
|
|
|
- const legendEl = document.getElementById('legend');
|
|
|
|
|
- const kinds = ['function', 'method', 'class', 'interface', 'component', 'enum', 'variable', 'constant', 'property', 'type_alias', 'import', 'export'];
|
|
|
|
|
- legendEl.innerHTML = kinds.map(k => `
|
|
|
|
|
- <div class="legend-item">
|
|
|
|
|
- <div class="legend-dot" style="background:${kindColors[k]};"></div>
|
|
|
|
|
- <span>${k.replace('_', ' ')}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- `).join('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Helpers
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function escapeHtml(str) {
|
|
|
|
|
- if (!str) return '';
|
|
|
|
|
- return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function escapeAttr(str) {
|
|
|
|
|
- if (!str) return '';
|
|
|
|
|
- return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function langForHighlight(lang) {
|
|
|
|
|
- const map = {
|
|
|
|
|
- 'typescript': 'typescript', 'javascript': 'javascript', 'tsx': 'typescript',
|
|
|
|
|
- 'jsx': 'javascript', 'python': 'python', 'go': 'go', 'rust': 'rust',
|
|
|
|
|
- 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'csharp': 'csharp',
|
|
|
|
|
- 'php': 'php', 'ruby': 'ruby', 'swift': 'swift', 'kotlin': 'kotlin',
|
|
|
|
|
- 'dart': 'dart', 'svelte': 'xml', 'liquid': 'xml', 'pascal': 'delphi',
|
|
|
|
|
- };
|
|
|
|
|
- return map[lang] || 'plaintext';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Keyboard Shortcuts
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- document.addEventListener('keydown', (e) => {
|
|
|
|
|
- // Ctrl+K or Cmd+K → focus search
|
|
|
|
|
- if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- document.getElementById('search-input').focus();
|
|
|
|
|
- }
|
|
|
|
|
- // Escape → close things
|
|
|
|
|
- if (e.key === 'Escape') {
|
|
|
|
|
- hideSearchDropdown();
|
|
|
|
|
- hideContextMenu();
|
|
|
|
|
- closeDetailPanel();
|
|
|
|
|
- clearHighlights();
|
|
|
|
|
- document.getElementById('search-input').blur();
|
|
|
|
|
- }
|
|
|
|
|
- // Delete/Backspace → remove selected nodes
|
|
|
|
|
- if ((e.key === 'Delete' || e.key === 'Backspace') && document.activeElement.tagName !== 'INPUT') {
|
|
|
|
|
- const selected = cy.$(':selected');
|
|
|
|
|
- if (selected.length > 0) {
|
|
|
|
|
- selected.remove();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Embeddings Setup Dialog
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- function showDialog() {
|
|
|
|
|
- document.getElementById('dialog-overlay').classList.add('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function closeDialog() {
|
|
|
|
|
- document.getElementById('dialog-overlay').classList.remove('visible');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async function checkEmbeddings() {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.embeddingsStatus();
|
|
|
|
|
- if (!data.isReady) {
|
|
|
|
|
- showDialog();
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Failed to check embeddings status:', err);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function startEmbeddings() {
|
|
|
|
|
- const btnEnable = document.getElementById('dialog-enable');
|
|
|
|
|
- const btnSkip = document.getElementById('dialog-skip');
|
|
|
|
|
- const progress = document.getElementById('dialog-progress');
|
|
|
|
|
- const progressFill = document.getElementById('dialog-progress-fill');
|
|
|
|
|
- const progressText = document.getElementById('dialog-progress-text');
|
|
|
|
|
- const progressPercent = document.getElementById('dialog-progress-percent');
|
|
|
|
|
- const title = document.getElementById('dialog-title');
|
|
|
|
|
- const body = document.getElementById('dialog-body');
|
|
|
|
|
-
|
|
|
|
|
- // Update UI to progress mode
|
|
|
|
|
- btnEnable.disabled = true;
|
|
|
|
|
- btnEnable.textContent = 'Setting up...';
|
|
|
|
|
- btnSkip.style.display = 'none';
|
|
|
|
|
- progress.style.display = 'block';
|
|
|
|
|
- body.innerHTML = 'Setting up semantic search for your project. This only needs to happen once.';
|
|
|
|
|
-
|
|
|
|
|
- const evtSource = new EventSource('/api/embeddings/generate');
|
|
|
|
|
-
|
|
|
|
|
- evtSource.addEventListener('status', (e) => {
|
|
|
|
|
- const data = JSON.parse(e.data);
|
|
|
|
|
- progressText.textContent = data.message;
|
|
|
|
|
- title.textContent = data.phase === 'model' ? 'Downloading Model...' :
|
|
|
|
|
- data.phase === 'embedding' ? 'Generating Embeddings...' :
|
|
|
|
|
- 'Setting Up...';
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- evtSource.addEventListener('progress', (e) => {
|
|
|
|
|
- const data = JSON.parse(e.data);
|
|
|
|
|
- progressFill.style.width = data.percent + '%';
|
|
|
|
|
- progressPercent.textContent = data.percent + '%';
|
|
|
|
|
- progressText.textContent = data.nodeName
|
|
|
|
|
- ? `Embedding: ${data.nodeName} (${data.current}/${data.total})`
|
|
|
|
|
- : `Processing ${data.current} of ${data.total}...`;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- evtSource.addEventListener('complete', (e) => {
|
|
|
|
|
- const data = JSON.parse(e.data);
|
|
|
|
|
- evtSource.close();
|
|
|
|
|
- title.textContent = 'Ready!';
|
|
|
|
|
- body.innerHTML = `<strong>${data.message}</strong><br><br>Semantic search is now active. Your explore queries will understand code meaning, not just keywords.`;
|
|
|
|
|
- progressFill.style.width = '100%';
|
|
|
|
|
- progressPercent.textContent = '100%';
|
|
|
|
|
- progressText.textContent = 'Complete';
|
|
|
|
|
-
|
|
|
|
|
- // Change actions to just a close button
|
|
|
|
|
- document.getElementById('dialog-actions').innerHTML =
|
|
|
|
|
- '<button class="btn btn-primary" onclick="closeDialog()">Start Exploring</button>';
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- evtSource.addEventListener('error', (e) => {
|
|
|
|
|
- let msg = 'An error occurred during setup.';
|
|
|
|
|
- try {
|
|
|
|
|
- const data = JSON.parse(e.data);
|
|
|
|
|
- msg = data.message || msg;
|
|
|
|
|
- } catch {}
|
|
|
|
|
- evtSource.close();
|
|
|
|
|
- title.textContent = 'Setup Error';
|
|
|
|
|
- body.innerHTML = `<span style="color:var(--red);">${escapeHtml(msg)}</span><br><br>You can still use the explorer with keyword-based search.`;
|
|
|
|
|
- document.getElementById('dialog-actions').innerHTML =
|
|
|
|
|
- '<button class="btn btn-secondary" onclick="closeDialog()">Close</button>';
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // SSE connection error (different from app error event)
|
|
|
|
|
- evtSource.onerror = () => {
|
|
|
|
|
- // EventSource reconnects automatically; if it closes, we handle via the error event above
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- // Init
|
|
|
|
|
- // ====================================================================
|
|
|
|
|
- async function init() {
|
|
|
|
|
- initCytoscape();
|
|
|
|
|
- buildLegend();
|
|
|
|
|
-
|
|
|
|
|
- // Load stats
|
|
|
|
|
- try {
|
|
|
|
|
- const data = await api.status();
|
|
|
|
|
- document.getElementById('stat-nodes').textContent = data.stats.nodeCount.toLocaleString();
|
|
|
|
|
- document.getElementById('stat-edges').textContent = data.stats.edgeCount.toLocaleString();
|
|
|
|
|
- document.getElementById('stat-files').textContent = data.stats.fileCount.toLocaleString();
|
|
|
|
|
- document.title = `CodeGraph - ${data.projectName}`;
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Failed to load status:', err);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Load file tree
|
|
|
|
|
- loadFileTree();
|
|
|
|
|
-
|
|
|
|
|
- // Embeddings dialog available but not auto-shown
|
|
|
|
|
- // (local embedding model quality is insufficient for natural language queries)
|
|
|
|
|
-
|
|
|
|
|
- // Search input
|
|
|
|
|
- document.getElementById('search-input').addEventListener('input', onSearchInput);
|
|
|
|
|
- document.getElementById('search-input').addEventListener('keydown', onSearchKeydown);
|
|
|
|
|
- document.getElementById('search-input').addEventListener('focus', () => {
|
|
|
|
|
- if (document.getElementById('search-input').value.trim()) {
|
|
|
|
|
- document.getElementById('search-results-dropdown').classList.add('visible');
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Click outside search dropdown to close
|
|
|
|
|
- document.addEventListener('click', (e) => {
|
|
|
|
|
- if (!e.target.closest('#search-container')) hideSearchDropdown();
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Start
|
|
|
|
|
- init();
|
|
|
|
|
- </script>
|
|
|
|
|
-</body>
|
|
|
|
|
-</html>
|
|
|