+ {intro}
+
+ {/* Metric tiles */}
+
+ {[
+ { label: t('graphReach.metricEntities'), value: result.nodeCount },
+ { label: t('graphReach.metricDiameter'), value: result.diameter },
+ { label: t('graphReach.metricRadius'), value: result.radius },
+ ].map(tile => (
+
+
+ {tile.label}
+
+
+ {tile.value}
+
+
+ ))}
+
+
+ {result.componentCount === 1
+ ? result.giantComponentSize === 1
+ ? t('graphReach.summaryCaptionOneAndOne')
+ : t('graphReach.summaryCaptionOne').replace(
+ '{giant}',
+ String(result.giantComponentSize)
+ )
+ : t('graphReach.summaryCaption')
+ .replace('{components}', String(result.componentCount))
+ .replace('{giant}', String(result.giantComponentSize))}
+
+
+ {/* Ranked most-central entities */}
+
+
+ {t('graphReach.rankedHeading')}
+
+
+
+
+ |
+ {t('graphReach.colRank')}
+ |
+
+ {t('graphReach.colEntity')}
+ |
+
+ {t('graphReach.colEccentricity')}
+ |
+
+ {t('graphReach.colLinks')}
+ |
+
+
+
+ {rows.map((node, i) => {
+ const localDiameter = componentDiameter.get(node.componentId) ?? 0;
+ const barWidth = localDiameter === 0 ? 0 : (node.eccentricity / localDiameter) * 100;
+ return (
+
+ | {i + 1} |
+
+ {node.id}
+ {node.isCenter && (
+
+ {t('graphReach.centerBadge')}
+
+ )}
+ |
+
+
+
+
+ {node.eccentricity}
+
+
+ |
+
+ {node.degree}
+ |
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+export default GraphReachPanel;
diff --git a/app/src/components/intelligence/GraphReachTab.test.tsx b/app/src/components/intelligence/GraphReachTab.test.tsx
new file mode 100644
index 0000000000..65171266b1
--- /dev/null
+++ b/app/src/components/intelligence/GraphReachTab.test.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeGraphReach } from '../../lib/memory/graphReach';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import GraphReachTab from './GraphReachTab';
+
+const mockLoadReach = vi.fn();
+const mockLoadNamespaces = vi.fn();
+
+vi.mock('../../services/api/graphReachApi', () => ({
+ loadReach: (...args: unknown[]) => mockLoadReach(...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 = computeGraphReach([rel('A', 'B'), rel('B', 'C'), rel('C', 'D')]);
+
+describe('