Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
53 changes: 53 additions & 0 deletions crates/shrimpk-viz/web/package-lock.json

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

21 changes: 12 additions & 9 deletions crates/shrimpk-viz/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@
"preview": "vite preview"
},
"dependencies": {
"@react-sigma/core": "^4.0.3",
"@sigma/utils": "^3.0.0",
"d3-scale-chromatic": "^3.1.0",
"graphology": "^0.25.4",
"graphology-communities-louvain": "^2.0.1",
"graphology-layout-forceatlas2": "^0.10.1",
"lucide-react": "^0.475.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sigma": "^3.0.0",
"graphology": "^0.25.4",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-communities-louvain": "^2.0.1",
"@react-sigma/core": "^4.0.3",
"zustand": "^5.0.0",
"lucide-react": "^0.475.0"
"zustand": "^5.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/d3-scale-chromatic": "^3.1.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"tailwindcss": "^4.0.0"
"vite": "^6.0.0"
}
}
29 changes: 8 additions & 21 deletions crates/shrimpk-viz/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { AppLayout } from "./layouts/AppLayout";
import { useGraphStore } from "./stores/graphStore";
import { checkHealth } from "./api/client";
import { LoadingState, ErrorState } from "@/components/ui/StateDisplay";

export function App() {
const loadOverview = useGraphStore((s) => s.loadOverview);
Expand Down Expand Up @@ -35,37 +36,23 @@ export function App() {

if (checking) {
return (
<div className="h-screen flex items-center justify-center bg-zinc-950">
<div className="text-center space-y-3">
<div className="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-sm text-zinc-500">
Connecting to ShrimPK daemon...
</p>
</div>
<div className="h-screen flex items-center justify-center bg-canvas">
<LoadingState message="Connecting to ShrimPK daemon..." />
</div>
);
}

if (!daemonOnline) {
return (
<div className="h-screen flex items-center justify-center bg-zinc-950">
<div className="text-center space-y-3 max-w-sm">
<div className="w-10 h-10 bg-red-950 rounded-full flex items-center justify-center mx-auto">
<span className="text-red-400 text-lg">!</span>
</div>
<h2 className="text-lg font-medium text-zinc-200">
Daemon Offline
</h2>
<p className="text-sm text-zinc-500">
ShrimPK daemon is not running on localhost:11435.
</p>
<code className="block text-xs text-zinc-600 bg-zinc-900 px-3 py-2 rounded">
<div className="h-screen flex items-center justify-center bg-canvas">
<ErrorState message="Daemon Offline" detail="ShrimPK daemon is not running on localhost:11435.">
<code className="block text-xs text-text-disabled bg-base px-3 py-2 rounded">
shrimpk-daemon
</code>
<p className="text-xs text-zinc-600">
<p className="text-xs text-text-disabled">
Retrying automatically...
</p>
</div>
</ErrorState>
</div>
);
}
Expand Down
50 changes: 50 additions & 0 deletions crates/shrimpk-viz/web/src/components/CommunityLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useGraphStore } from "@/stores/graphStore";
import { Panel } from "@/components/ui/Panel";
import { communityColor } from "@/lib/categoryColors";

export function CommunityLegend() {
const communityMap = useGraphStore((s) => s.communityMap);
const zoomLevel = useGraphStore((s) => s.zoomLevel);
const clusters = useGraphStore((s) => s.clusters);
const visible = zoomLevel !== "galaxy" && communityMap.size > 0;

const entries = Array.from(communityMap.entries())
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 10);

/** Try to match a community's members to a cluster with a summary. */
function labelForCommunity(members: string[], index: number): string {
for (const cluster of clusters) {
if (!cluster.summary) continue;
const memberSet = new Set(members);
const hasOverlap = cluster.top_members.some((m) => memberSet.has(m.id));
if (hasOverlap) {
return cluster.summary.length > 20
? cluster.summary.slice(0, 20) + "\u2026"
: cluster.summary;
}
}
return `Group ${String.fromCharCode(65 + index)}`;
}

return (
<Panel
variant="legend"
aria-hidden={!visible}
className={`bottom-4 left-4 flex flex-wrap gap-2 max-w-xs transition-opacity duration-panel ease-out motion-reduce:transition-none ${visible ? "opacity-100" : "opacity-0 pointer-events-none"}`}
>
{entries.map(([id, members], index) => (
<span
key={id}
className="inline-flex items-center gap-1.5 text-text-secondary text-micro"
>
<span
className="w-3 h-3 rounded-full shrink-0"
style={{ backgroundColor: communityColor(id) }}
/>
{labelForCommunity(members, index)} ({members.length})
</span>
))}
</Panel>
);
}
Loading
Loading