Expand raw packet feed and have Hide UI option

This commit is contained in:
Jack Kingsman
2026-01-19 16:48:50 -08:00
parent 5b38e7735b
commit 8ccc620cac
8 changed files with 666 additions and 645 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -13,8 +13,8 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-D05AJxJe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DgiuGBKg.css">
<script type="module" crossorigin src="/assets/index-qvx9bWcn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ty42XaBr.css">
</head>
<body>
<div id="root"></div>
+112 -88
View File
@@ -857,9 +857,11 @@ interface PacketVisualizerProps {
packets: RawPacket[];
contacts: Contact[];
config: RadioConfig | null;
fullScreen?: boolean;
onFullScreenChange?: (fullScreen: boolean) => void;
}
export function PacketVisualizer({ packets, contacts, config }: PacketVisualizerProps) {
export function PacketVisualizer({ packets, contacts, config, fullScreen, onFullScreenChange }: PacketVisualizerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
@@ -870,6 +872,7 @@ export function PacketVisualizer({ packets, contacts, config }: PacketVisualizer
const [chargeStrength, setChargeStrength] = useState(-200);
const [filterOldRepeaters, setFilterOldRepeaters] = useState(false);
const [letEmDrift, setLetEmDrift] = useState(true);
const [hideUI, setHideUI] = useState(false);
// Pan/zoom
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
@@ -1057,100 +1060,121 @@ export function PacketVisualizer({ packets, contacts, config }: PacketVisualizer
/>
{/* Legend */}
<div className="absolute bottom-4 left-4 bg-background/80 backdrop-blur-sm rounded-lg p-3 text-xs border border-border">
<div className="flex gap-6">
<div className="flex flex-col gap-1.5">
<div className="text-muted-foreground font-medium mb-1">Nodes</div>
{LEGEND_ITEMS.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<span className={item.size}>{item.emoji}</span>
<span>{item.label}</span>
</div>
))}
</div>
<div className="flex flex-col gap-1.5">
<div className="text-muted-foreground font-medium mb-1">Packets</div>
{PACKET_LEGEND_ITEMS.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<div
className="w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold text-white"
style={{ backgroundColor: item.color }}
>
{item.label}
{!hideUI && (
<div className="absolute bottom-4 left-4 bg-background/80 backdrop-blur-sm rounded-lg p-3 text-xs border border-border">
<div className="flex gap-6">
<div className="flex flex-col gap-1.5">
<div className="text-muted-foreground font-medium mb-1">Nodes</div>
{LEGEND_ITEMS.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<span className={item.size}>{item.emoji}</span>
<span>{item.label}</span>
</div>
<span>{item.description}</span>
</div>
))}
))}
</div>
<div className="flex flex-col gap-1.5">
<div className="text-muted-foreground font-medium mb-1">Packets</div>
{PACKET_LEGEND_ITEMS.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<div
className="w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold text-white"
style={{ backgroundColor: item.color }}
>
{item.label}
</div>
<span>{item.description}</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
{/* Options */}
<div className="absolute top-4 right-4 bg-background/80 backdrop-blur-sm rounded-lg p-3 text-xs border border-border">
<div className="flex flex-col gap-2">
<div>Nodes: {data.stats.nodes}</div>
<div>Links: {data.stats.links}</div>
<div className="text-muted-foreground">
Processed: {data.stats.processed} | Animated: {data.stats.animated}
</div>
<div className="border-t border-border pt-2 mt-1 flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={showAmbiguousPaths}
onCheckedChange={(c) => setShowAmbiguousPaths(c === true)}
/>
<span title="Show placeholder nodes for repeaters when the 1-byte prefix matches multiple contacts">
Ambiguous repeaters
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={showAmbiguousNodes}
onCheckedChange={(c) => setShowAmbiguousNodes(c === true)}
/>
<span title="Show placeholder nodes for senders/recipients when only a 1-byte prefix is known">
Ambiguous sender/recipient
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={filterOldRepeaters}
onCheckedChange={(c) => setFilterOldRepeaters(c === true)}
/>
<span title="Hide repeaters not heard within the last 48 hours">
Hide repeaters &gt;48hrs heard
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox checked={letEmDrift} onCheckedChange={(c) => setLetEmDrift(c === true)} />
<span title="When enabled, the graph continuously reorganizes itself into a better layout">
Let &apos;em drift
</span>
</label>
<div className="flex flex-col gap-1 mt-1">
<label
className="text-muted-foreground"
title="How strongly nodes repel each other. Higher values spread nodes out more."
>
Repulsion: {Math.abs(chargeStrength)}
</label>
<input
type="range"
min="50"
max="2500"
value={Math.abs(chargeStrength)}
onChange={(e) => setChargeStrength(-parseInt(e.target.value))}
className="w-full h-2 bg-border rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<button
onClick={data.randomizePositions}
className="mt-2 px-3 py-1.5 bg-primary/20 hover:bg-primary/30 text-primary rounded text-xs transition-colors"
title="Randomize node positions and let the simulation settle into a new layout"
>
Shuffle layout
</button>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox checked={hideUI} onCheckedChange={(c) => setHideUI(c === true)} />
<span title="Hide legends and controls for a cleaner view">Hide UI</span>
</label>
{!hideUI && (
<>
<div className="border-t border-border pt-2 mt-1">
<div>Nodes: {data.stats.nodes}</div>
<div>Links: {data.stats.links}</div>
<div className="text-muted-foreground">
Processed: {data.stats.processed} | Animated: {data.stats.animated}
</div>
</div>
<div className="border-t border-border pt-2 mt-1 flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={showAmbiguousPaths}
onCheckedChange={(c) => setShowAmbiguousPaths(c === true)}
/>
<span title="Show placeholder nodes for repeaters when the 1-byte prefix matches multiple contacts">
Ambiguous repeaters
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={showAmbiguousNodes}
onCheckedChange={(c) => setShowAmbiguousNodes(c === true)}
/>
<span title="Show placeholder nodes for senders/recipients when only a 1-byte prefix is known">
Ambiguous sender/recipient
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={filterOldRepeaters}
onCheckedChange={(c) => setFilterOldRepeaters(c === true)}
/>
<span title="Hide repeaters not heard within the last 48 hours">
Hide repeaters &gt;48hrs heard
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox checked={letEmDrift} onCheckedChange={(c) => setLetEmDrift(c === true)} />
<span title="When enabled, the graph continuously reorganizes itself into a better layout">
Let &apos;em drift
</span>
</label>
<div className="flex flex-col gap-1 mt-1">
<label
className="text-muted-foreground"
title="How strongly nodes repel each other. Higher values spread nodes out more."
>
Repulsion: {Math.abs(chargeStrength)}
</label>
<input
type="range"
min="50"
max="2500"
value={Math.abs(chargeStrength)}
onChange={(e) => setChargeStrength(-parseInt(e.target.value))}
className="w-full h-2 bg-border rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<button
onClick={data.randomizePositions}
className="mt-2 px-3 py-1.5 bg-primary/20 hover:bg-primary/30 text-primary rounded text-xs transition-colors"
title="Randomize node positions and let the simulation settle into a new layout"
>
Shuffle layout
</button>
{onFullScreenChange && (
<label className="flex items-center gap-2 cursor-pointer mt-2 pt-2 border-t border-border">
<Checkbox
checked={fullScreen}
onCheckedChange={(c) => onFullScreenChange(c === true)}
/>
<span title="Hide the packet feed panel">Full screen</span>
</label>
)}
</div>
</>
)}
</div>
</div>
</div>
+8 -11
View File
@@ -3,7 +3,6 @@ import type { Contact, RawPacket, RadioConfig } from '../types';
import { PacketVisualizer } from './PacketVisualizer';
import { RawPacketList } from './RawPacketList';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Checkbox } from './ui/checkbox';
import { cn } from '@/lib/utils';
interface VisualizerViewProps {
@@ -20,14 +19,6 @@ export function VisualizerView({ packets, contacts, config }: VisualizerViewProp
{/* Header */}
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
<span>Mesh Visualizer</span>
{/* Full screen toggle - only show on larger screens */}
<label className="hidden md:flex items-center gap-2 text-sm font-normal cursor-pointer">
<Checkbox
checked={fullScreen}
onCheckedChange={(checked) => setFullScreen(checked === true)}
/>
<span>Full screen</span>
</label>
</div>
{/* Mobile: Tabbed interface */}
@@ -55,14 +46,20 @@ export function VisualizerView({ packets, contacts, config }: VisualizerViewProp
fullScreen ? 'flex-1' : 'flex-1 border-r border-border'
)}
>
<PacketVisualizer packets={packets} contacts={contacts} config={config} />
<PacketVisualizer
packets={packets}
contacts={contacts}
config={config}
fullScreen={fullScreen}
onFullScreenChange={setFullScreen}
/>
</div>
{/* Packet feed panel - hidden when full screen */}
<div
className={cn(
'overflow-hidden transition-all duration-200',
fullScreen ? 'w-0' : 'w-[45rem] lg:w-[54rem]'
fullScreen ? 'w-0' : 'w-[31rem] lg:w-[38rem]'
)}
>
<div className="h-full flex flex-col">