mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
Merge pull request #740 from rarepops/feat/graph-ui-routing-and-filter-ux
feat(graph-ui): URL routing, filter panel UX, and camera fly-to fix
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { colorForLabel } from "../lib/colors";
|
||||
import type { GraphData } from "../lib/types";
|
||||
|
||||
@@ -37,9 +38,9 @@ export function FilterPanel({
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3 border-b border-border/40 space-y-3">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col shrink-0 max-h-[45%] border-b border-border/40">
|
||||
{/* Header row — always visible */}
|
||||
<div className="flex items-center justify-between px-4 pt-3 pb-2 shrink-0">
|
||||
<span className="text-[11px] font-medium text-foreground/50 uppercase tracking-widest">
|
||||
Filters
|
||||
</span>
|
||||
@@ -50,66 +51,77 @@ export function FilterPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node labels */}
|
||||
<div>
|
||||
<p className="text-[10px] text-foreground/30 mb-1.5">Nodes</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{labelCounts.map(([label, count]) => {
|
||||
const on = enabledLabels.has(label);
|
||||
const c = colorForLabel(label);
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => onToggleLabel(label)}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-[3px] rounded-md text-[10px] font-medium transition-all border ${
|
||||
on ? "border-white/[0.08] bg-white/[0.04]" : "border-transparent opacity-25"
|
||||
}`}
|
||||
>
|
||||
<span className="w-[5px] h-[5px] rounded-full" style={{ backgroundColor: on ? c : "#444" }} />
|
||||
<span style={{ color: on ? c : "#555" }}>{label}</span>
|
||||
<span className="text-foreground/20 tabular-nums">{count.toLocaleString()}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Scrollable filter groups */}
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="px-4 pb-3 space-y-3">
|
||||
{/* Node types */}
|
||||
{labelCounts.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-foreground/40 mb-1.5 uppercase tracking-wider">Node types</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{labelCounts.map(([label, count]) => {
|
||||
const on = enabledLabels.has(label);
|
||||
const c = colorForLabel(label);
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => onToggleLabel(label)}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-[3px] rounded-md text-[10px] font-medium transition-all border ${
|
||||
on ? "border-white/[0.08] bg-white/[0.04]" : "border-transparent opacity-25"
|
||||
}`}
|
||||
>
|
||||
<span className="w-[5px] h-[5px] rounded-full" style={{ backgroundColor: on ? c : "#444" }} />
|
||||
<span style={{ color: on ? c : "#555" }}>{label}</span>
|
||||
<span className="text-foreground/20 tabular-nums">{count.toLocaleString()}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edge types */}
|
||||
<div>
|
||||
<p className="text-[10px] text-foreground/30 mb-1.5">Edges</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{edgeTypeCounts.map(([type, count]) => {
|
||||
const on = enabledEdgeTypes.has(type);
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => onToggleEdgeType(type)}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-[3px] rounded-md text-[10px] font-medium transition-all border ${
|
||||
on ? "border-white/[0.06] bg-white/[0.03] text-foreground/60" : "border-transparent opacity-20 text-foreground/30"
|
||||
}`}
|
||||
>
|
||||
{type.replace(/_/g, " ").toLowerCase()}
|
||||
<span className="text-foreground/15 tabular-nums">{count.toLocaleString()}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{/* Relationships */}
|
||||
{edgeTypeCounts.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-foreground/40 mb-1.5 uppercase tracking-wider">Relationships</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{edgeTypeCounts.map(([type, count]) => {
|
||||
const on = enabledEdgeTypes.has(type);
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => onToggleEdgeType(type)}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-[3px] rounded-md text-[10px] font-medium transition-all border ${
|
||||
on ? "border-white/[0.06] bg-white/[0.03] text-foreground/60" : "border-transparent opacity-20 text-foreground/30"
|
||||
}`}
|
||||
>
|
||||
{type.replace(/_/g, " ").toLowerCase()}
|
||||
<span className="text-foreground/15 tabular-nums">{count.toLocaleString()}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Show labels toggle */}
|
||||
<button
|
||||
onClick={onToggleShowLabels}
|
||||
className={`inline-flex items-center gap-1.5 text-[11px] font-medium transition-all ${
|
||||
showLabels ? "text-primary" : "text-foreground/30"
|
||||
}`}
|
||||
>
|
||||
<span className={`w-3.5 h-3.5 rounded border flex items-center justify-center transition-all ${
|
||||
showLabels ? "border-primary bg-primary/20" : "border-foreground/15"
|
||||
}`}>
|
||||
{showLabels && <span className="text-primary text-[9px]">✓</span>}
|
||||
</span>
|
||||
Show labels
|
||||
</button>
|
||||
{/* Display options — pinned footer */}
|
||||
<div className="px-4 py-2.5 border-t border-border/20 shrink-0">
|
||||
<button
|
||||
onClick={onToggleShowLabels}
|
||||
className={`inline-flex items-center gap-1.5 text-[11px] font-medium transition-all ${
|
||||
showLabels ? "text-primary" : "text-foreground/30"
|
||||
}`}
|
||||
>
|
||||
<span className={`w-3.5 h-3.5 rounded border flex items-center justify-center transition-all ${
|
||||
showLabels ? "border-primary bg-primary/20" : "border-foreground/15"
|
||||
}`}>
|
||||
{showLabels && <span className="text-primary text-[9px]">✓</span>}
|
||||
</span>
|
||||
Show labels
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@ interface CameraTarget {
|
||||
lookAt: THREE.Vector3;
|
||||
}
|
||||
|
||||
function CameraAnimator({ target }: { target: CameraTarget | null }) {
|
||||
function CameraAnimator({
|
||||
target,
|
||||
controlsRef,
|
||||
}: {
|
||||
target: CameraTarget | null;
|
||||
controlsRef: React.RefObject<OrbitControlsImpl | null>;
|
||||
}) {
|
||||
const { camera } = useThree();
|
||||
const targetRef = useRef<CameraTarget | null>(null);
|
||||
const progress = useRef(1);
|
||||
@@ -36,7 +42,17 @@ function CameraAnimator({ target }: { target: CameraTarget | null }) {
|
||||
const t = 1 - Math.pow(1 - progress.current, 3); /* ease-out cubic */
|
||||
|
||||
camera.position.lerp(targetRef.current.position, t * 0.08);
|
||||
camera.lookAt(targetRef.current.lookAt);
|
||||
|
||||
/* Move the OrbitControls pivot to the focus point as well. Otherwise the
|
||||
* controls keep their target at the origin and re-center the view on the
|
||||
* next frame, snapping the camera back to the middle after the fly-to. */
|
||||
const controls = controlsRef.current;
|
||||
if (controls) {
|
||||
controls.target.lerp(targetRef.current.lookAt, t * 0.08);
|
||||
controls.update();
|
||||
} else {
|
||||
camera.lookAt(targetRef.current.lookAt);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -179,7 +195,7 @@ export function GraphScene({
|
||||
|
||||
{hovered && <NodeTooltip node={hovered} />}
|
||||
|
||||
<CameraAnimator target={cameraTarget} />
|
||||
<CameraAnimator target={cameraTarget} controlsRef={controlsRef} />
|
||||
<IdleAutoRotate controlsRef={controlsRef} />
|
||||
|
||||
<EffectComposer multisampling={GRAPH_COMPOSER_MULTISAMPLING}>
|
||||
|
||||
@@ -181,7 +181,7 @@ export function GraphTab({ project }: GraphTabProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-white/30 text-sm">
|
||||
Select a project from the Stats tab
|
||||
Select a project from the Projects tab
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -312,7 +312,7 @@ export function GraphTab({ project }: GraphTabProps) {
|
||||
setCameraTarget(null);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
Clear selection
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -120,7 +120,12 @@ export function Sidebar({ nodes, onSelectPath, selectedPath }: SidebarProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="px-3 py-2.5 border-b border-border/30">
|
||||
<div className="px-4 pt-3 pb-2 shrink-0">
|
||||
<span className="text-[11px] font-medium text-foreground/50 uppercase tracking-widest">
|
||||
{t.graph.folders}
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-3 pb-2.5 border-b border-border/30 shrink-0">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
Reference in New Issue
Block a user