Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions packages/arkham-shard-shell/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/arkham-shard-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@
"@fontsource/jetbrains-mono": "^5.0.0",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.5",
"cytoscape": "^3.33.1",
"cytoscape-context-menus": "^4.2.1",
"cytoscape-dagre": "^2.5.0",
"cytoscape-expand-collapse": "^4.1.1",
"cytoscape-fcose": "^2.2.0",
"cytoscape-svg": "^0.4.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dompurify": "^3.3.1",
"lucide-react": "^0.294.0",
"maplibre-gl": "^5.16.0",
"react": "^18.2.0",
"react-cytoscapejs": "^2.0.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.0",
"react-force-graph-2d": "^1.29.0",
Expand All @@ -28,6 +35,7 @@
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@types/cytoscape": "^3.21.9",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/arkham-shard-shell/src/pages/graph/GraphPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -4380,3 +4380,19 @@
padding: 1rem;
overflow: auto;
}

/* Cytoscape View Container */
.cytoscape-view-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-primary);
position: relative;
}

.cytoscape-graph-main {
flex: 1;
width: 100%;
min-height: 0;
}
91 changes: 88 additions & 3 deletions packages/arkham-shard-shell/src/pages/graph/GraphPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { useFetch } from '../../hooks/useFetch';
import { useGraphSettings } from './hooks/useGraphSettings';
import { useUrlParams } from './hooks/useUrlParams';
import { GraphControls, DataSourcesPanel, LayoutModeControls, AssociationMatrix, MatrixControls, SankeyDiagram, SankeyControls, GeoGraphView, GeoGraphControls, ArgumentationView, ArgumentationControls, CausalGraphView, CausalGraphControls } from './components';
import { CytoscapeGraph, CytoscapeControls } from './components/cytoscape';
import type { CytoscapeGraphRef } from './components/cytoscape';
import './components/cytoscape/CytoscapeGraph.css';
import type { FlowData } from './components';
import { EgoMetricsPanel } from './components/EgoMetricsPanel';
import { fetchScores, type EntityScore } from './api';
Expand Down Expand Up @@ -106,11 +109,12 @@ const ENTITY_TYPE_COLORS: Record<string, string> = {
unknown: '#718096',
};

type TabId = 'graph' | 'matrix' | 'sankey' | 'geo' | 'argumentation' | 'causal' | 'controls' | 'sources';
type TabId = 'graph' | 'cytoscape' | 'matrix' | 'sankey' | 'geo' | 'argumentation' | 'causal' | 'controls' | 'sources';

export function GraphPage() {
const { toast } = useToast();
const graphRef = useRef<ForceGraphMethods | null>(null);
const cytoscapeRef = useRef<CytoscapeGraphRef | null>(null);
const graphSettings = useGraphSettings();

// State
Expand Down Expand Up @@ -173,6 +177,10 @@ export function GraphPage() {
const [causalShowLabels, setCausalShowLabels] = useState(true);
const [causalShowStrength, setCausalShowStrength] = useState(true);

// Cytoscape view state
const [cyLayout, setCyLayout] = useState<string>('fcose');
const [cyShowEdgeLabels, setCyShowEdgeLabels] = useState(false);

// Container ref for responsive sizing
const containerRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -1099,7 +1107,15 @@ export function GraphPage() {
onClick={() => setActiveTab('graph')}
>
<Icon name="Network" size={16} />
Graph View
Force Graph
</button>
<button
className={`graph-tab ${activeTab === 'cytoscape' ? 'active' : ''}`}
onClick={() => setActiveTab('cytoscape')}
title="Cytoscape.js - Professional OSINT-grade visualization"
>
<Icon name="Hexagon" size={16} />
Cytoscape
</button>
<button
className={`graph-tab ${activeTab === 'matrix' ? 'active' : ''}`}
Expand Down Expand Up @@ -1155,7 +1171,7 @@ export function GraphPage() {

<div className="graph-layout">
{/* Sidebar - content changes based on tab */}
<aside className={`graph-sidebar ${(activeTab !== 'graph' && activeTab !== 'matrix' && activeTab !== 'sankey' && activeTab !== 'geo' && activeTab !== 'argumentation' && activeTab !== 'causal') ? 'wide' : ''} ${sidebarCollapsed ? 'collapsed' : ''}`}>
<aside className={`graph-sidebar ${(activeTab !== 'graph' && activeTab !== 'cytoscape' && activeTab !== 'matrix' && activeTab !== 'sankey' && activeTab !== 'geo' && activeTab !== 'argumentation' && activeTab !== 'causal') ? 'wide' : ''} ${sidebarCollapsed ? 'collapsed' : ''}`}>
{/* Collapse toggle button */}
<button
className="sidebar-collapse-toggle"
Expand Down Expand Up @@ -1210,6 +1226,19 @@ export function GraphPage() {
showStrength={causalShowStrength}
onShowStrengthChange={setCausalShowStrength}
/>
) : activeTab === 'cytoscape' ? (
<CytoscapeControls
layout={cyLayout}
onLayoutChange={(layout) => {
setCyLayout(layout);
cytoscapeRef.current?.runLayout(layout);
}}
showEdgeLabels={cyShowEdgeLabels}
onShowEdgeLabelsChange={setCyShowEdgeLabels}
onZoomToFit={() => cytoscapeRef.current?.zoomToFit()}
onExpandAll={() => {}}
onCollapseAll={() => {}}
/>
) : activeTab === 'matrix' ? (
<MatrixControls
sortBy={matrixSortBy}
Expand Down Expand Up @@ -1551,6 +1580,62 @@ export function GraphPage() {
</div>
)}

{/* Cytoscape View - Professional OSINT-grade visualization */}
{activeTab === 'cytoscape' && (
loading ? (
<div className="graph-loading">
<Icon name="Loader2" size={48} className="spin" />
<span>Loading graph...</span>
</div>
) : error ? (
<div className="graph-error">
<Icon name="AlertCircle" size={48} />
<span>Failed to load graph</span>
<button className="btn btn-secondary" onClick={() => refetch()}>
Retry
</button>
</div>
) : forceGraphData.nodes.length > 0 ? (
<div className="cytoscape-view-container">
<CytoscapeGraph
ref={cytoscapeRef}
nodes={forceGraphData.nodes}
edges={forceGraphData.links}
layout={cyLayout}
onNodeClick={(_nodeId, node) => {
setSelectedNode(node as GraphNode);
}}
onNodeRightClick={(_nodeId, _position) => {
// Could show context menu here
const node = forceGraphData.nodes.find(n => n.id === _nodeId);
if (node) setSelectedNode(node);
}}
onEdgeClick={(_edgeId, edge) => {
toast.info(`Edge: ${edge.relationship_type || edge.type || 'related'}`);
}}
onBackgroundClick={() => {
setSelectedNode(null);
setHighlightedPath(new Set());
}}
selectedNodeId={selectedNode?.id}
highlightedPath={highlightedPath}
showEdgeLabels={cyShowEdgeLabels}
className="cytoscape-graph-main"
/>
</div>
) : (
<div className="graph-empty">
<Icon name="Hexagon" size={64} />
<h3>No Graph Data</h3>
<p>Build a graph to visualize with Cytoscape</p>
<button className="btn btn-primary" onClick={buildGraph}>
<Icon name="GitBranch" size={16} />
Build Graph
</button>
</div>
)
)}

{/* Graph View */}
{activeTab === 'graph' || activeTab === 'controls' || activeTab === 'sources' ? (
loading ? (
Expand Down
Loading