+ {intro}
+
+ {/* Metric tiles */}
+
+ {[
+ { label: t('graphCentrality.metricEntities'), value: result.nodeCount },
+ { label: t('graphCentrality.metricConnections'), value: result.edgeCount },
+ { label: t('graphCentrality.metricClusters'), value: result.componentCount },
+ ].map(tile => (
+
+
+ {tile.label}
+
+
+ {tile.value}
+
+
+ ))}
+
+
+ {t('graphCentrality.clustersCaption')
+ .replace('{components}', String(result.componentCount))
+ .replace('{largest}', String(largestCluster))}
+ {!result.converged && (
+
+ {t('graphCentrality.approximateBadge')}
+
+ )}
+
+
+ {/* Ranked hub table */}
+
+
+ {t('graphCentrality.rankedHeading')}
+
+
+
+
+ |
+ {t('graphCentrality.colRank')}
+ |
+
+ {t('graphCentrality.colEntity')}
+ |
+
+ {t('graphCentrality.colInfluence')}
+ |
+
+ {t('graphCentrality.colLinks')}
+ |
+
+
+
+ {rows.map((node, i) => (
+
+ | {i + 1} |
+
+ {node.id}
+ {bridgeIds.has(node.id) && (
+
+ {t('graphCentrality.bridgeBadge')}
+
+ )}
+ |
+
+
+
+
+ {node.pageRank.toFixed(3)}
+
+
+ |
+
+ {node.totalDegree}
+ |
+
+ ))}
+
+
+
+
+ );
+};
+
+export default GraphCentralityPanel;
diff --git a/app/src/components/intelligence/GraphCentralityTab.test.tsx b/app/src/components/intelligence/GraphCentralityTab.test.tsx
new file mode 100644
index 000000000..18a5b0598
--- /dev/null
+++ b/app/src/components/intelligence/GraphCentralityTab.test.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeGraphCentrality } from '../../lib/memory/graphCentrality';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import GraphCentralityTab from './GraphCentralityTab';
+
+const mockLoadCentrality = vi.fn();
+const mockLoadNamespaces = vi.fn();
+
+vi.mock('../../services/api/graphCentralityApi', () => ({
+ loadCentrality: (...args: unknown[]) => mockLoadCentrality(...args),
+ loadNamespaces: (...args: unknown[]) => mockLoadNamespaces(...args),
+}));
+
+function rel(subject: string, object: string): GraphRelation {
+ return {
+ namespace: 'n',
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+const result = computeGraphCentrality([rel('A', 'B'), rel('B', 'A')]);
+
+describe('