Browse Source

Enable publication -> visualize view, tag depth

master
limina1 10 months ago
parent
commit
61b44548f2
  1. 1
      src/lib/components/cards/BlogHeader.svelte
  2. 10
      src/lib/navigator/EventNetwork/Legend.svelte
  3. 45
      src/lib/navigator/EventNetwork/Settings.svelte
  4. 132
      src/lib/navigator/EventNetwork/index.svelte
  5. 8
      src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts
  6. 86
      src/routes/visualize/+page.svelte

1
src/lib/components/cards/BlogHeader.svelte

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing";
import CardActions from "$components/util/CardActions.svelte";
import { getMatchingTags } from '$lib/utils/nostrUtils';
const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: string, event: NDKEvent, onBlogUpdate?: any, active: boolean }>();

10
src/lib/navigator/EventNetwork/Legend.svelte

@ -12,12 +12,14 @@ @@ -12,12 +12,14 @@
starMode = false,
showTags = false,
tagAnchors = [],
eventCounts = {},
} = $props<{
collapsedOnInteraction: boolean;
className: string;
starMode?: boolean;
showTags?: boolean;
tagAnchors?: any[];
eventCounts?: { [kind: number]: number };
}>();
let expanded = $state(true);
@ -65,7 +67,7 @@ @@ -65,7 +67,7 @@
</span>
</div>
<span class="legend-text"
>Index events (kind 30040) - Star centers with unique colors</span
>{eventCounts[30040] || 0} Index events (kind 30040) - Star centers with unique colors</span
>
</li>
@ -77,7 +79,7 @@ @@ -77,7 +79,7 @@
</span>
</div>
<span class="legend-text"
>Content nodes (kind 30041) - Arranged around star centers</span
>{eventCounts[30041] || 0} Content nodes (kind 30041) - Arranged around star centers</span
>
</li>
@ -107,7 +109,7 @@ @@ -107,7 +109,7 @@
</span>
</div>
<span class="legend-text"
>Index events (kind 30040) - Each with a unique pastel color</span
>{eventCounts[30040] || 0} Index events (kind 30040) - Each with a unique pastel color</span
>
</li>
@ -119,7 +121,7 @@ @@ -119,7 +121,7 @@
</span>
</div>
<span class="legend-text"
>Content events (kinds 30041, 30818) - Publication sections</span
>{(eventCounts[30041] || 0) + (eventCounts[30818] || 0)} Content events (kinds 30041, 30818) - Publication sections</span
>
</li>

45
src/lib/navigator/EventNetwork/Settings.svelte

@ -17,12 +17,14 @@ @@ -17,12 +17,14 @@
starVisualization = $bindable(true),
showTagAnchors = $bindable(false),
selectedTagType = $bindable("t"),
tagExpansionDepth = $bindable(0),
} = $props<{
count: number;
onupdate: () => void;
starVisualization?: boolean;
showTagAnchors?: boolean;
selectedTagType?: string;
tagExpansionDepth?: number;
}>();
let expanded = $state(false);
@ -36,6 +38,17 @@ @@ -36,6 +38,17 @@
function handleLimitUpdate() {
onupdate();
}
function handleDepthInput(event: Event) {
const input = event.target as HTMLInputElement;
const value = parseInt(input.value);
// Ensure value is between 0 and 10
if (!isNaN(value) && value >= 0 && value <= 10) {
tagExpansionDepth = value;
} else if (input.value === "") {
tagExpansionDepth = 0;
}
}
</script>
<div class="leather-legend sm:!right-1 sm:!left-auto">
@ -87,10 +100,12 @@ @@ -87,10 +100,12 @@
</p>
{#if showTagAnchors}
<div class="mt-2">
<div class="mt-2 space-y-3">
<div>
<label
for="tag-type-select"
class="text-xs text-gray-600 dark:text-gray-400">Tag Type:</label
class="text-xs text-gray-600 dark:text-gray-400"
>Tag Type:</label
>
<Select
id="tag-type-select"
@ -107,6 +122,32 @@ @@ -107,6 +122,32 @@
<option value="summary">Summaries</option>
</Select>
</div>
<div class="space-y-1">
<div class="flex items-center gap-2">
<label
for="tag-depth-input"
class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap"
>Expansion Depth:</label
>
<input
type="number"
id="tag-depth-input"
min="0"
max="10"
value={tagExpansionDepth}
oninput={handleDepthInput}
class="w-16 text-xs bg-primary-0 dark:bg-primary-1000 border border-gray-300 dark:border-gray-700 rounded-md px-2 py-1 dark:text-white"
/>
<span class="text-xs text-gray-500 dark:text-gray-400">
(0-10)
</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
Fetch publications sharing tags
</p>
</div>
</div>
{/if}
</div>

132
src/lib/navigator/EventNetwork/index.svelte

@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
type Selection = any;
// Configuration
const DEBUG = false; // Set to true to enable debug logging
const DEBUG = true; // Set to true to enable debug logging
const NODE_RADIUS = 20;
const LINK_DISTANCE = 10;
const ARROW_DISTANCE = 10;
@ -58,9 +58,10 @@ @@ -58,9 +58,10 @@
}
// Component props
let { events = [], onupdate } = $props<{
let { events = [], onupdate, onTagExpansionChange } = $props<{
events?: NDKEvent[];
onupdate: () => void;
onTagExpansionChange?: (depth: number, tags: string[]) => void;
}>();
// Error state
@ -95,6 +96,9 @@ @@ -95,6 +96,9 @@
let zoomBehavior: any;
let svgElement: Selection;
// Position cache to preserve node positions across updates
let nodePositions = new Map<string, { x: number; y: number; vx?: number; vy?: number }>();
// Track current render level
let currentLevels = $derived(levelsToRender);
@ -105,6 +109,14 @@ @@ -105,6 +109,14 @@
let showTagAnchors = $state(false);
let selectedTagType = $state("t"); // Default to hashtags
let tagAnchorInfo = $state<any[]>([]);
let tagExpansionDepth = $state(0); // Default to no expansion
// Store initial state to detect if component is being recreated
let componentId = Math.random();
debug("Component created with ID:", componentId);
// Event counts by kind
let eventCounts = $state<{ [kind: number]: number }>({});
// Debug function - call from browser console: window.debugTagAnchors()
if (typeof window !== "undefined") {
@ -224,6 +236,13 @@ @@ -224,6 +236,13 @@
// Enhance with tag anchors if enabled
if (showTagAnchors) {
debug("Enhancing graph with tags", {
selectedTagType,
eventCount: events.length,
width,
height
});
graphData = enhanceGraphWithTags(
graphData,
events,
@ -233,9 +252,14 @@ @@ -233,9 +252,14 @@
);
// Extract tag anchor info for legend
tagAnchorInfo = graphData.nodes
.filter((n) => n.isTagAnchor)
.map((n) => ({
const tagAnchors = graphData.nodes.filter((n) => n.isTagAnchor);
debug("Tag anchors created", {
count: tagAnchors.length,
anchors: tagAnchors
});
tagAnchorInfo = tagAnchors.map((n) => ({
type: n.tagType,
label: n.title,
count: n.connectedNodes?.length || 0,
@ -245,12 +269,48 @@ @@ -245,12 +269,48 @@
tagAnchorInfo = [];
}
// Save current node positions before updating
if (simulation && nodes.length > 0) {
nodes.forEach(node => {
if (node.x != null && node.y != null) {
nodePositions.set(node.id, {
x: node.x,
y: node.y,
vx: node.vx,
vy: node.vy
});
}
});
debug("Saved positions for", nodePositions.size, "nodes");
}
nodes = graphData.nodes;
links = graphData.links;
// Count events by kind
const counts: { [kind: number]: number } = {};
events.forEach(event => {
counts[event.kind] = (counts[event.kind] || 0) + 1;
});
eventCounts = counts;
// Restore positions for existing nodes
let restoredCount = 0;
nodes.forEach(node => {
const savedPos = nodePositions.get(node.id);
if (savedPos && !node.isTagAnchor) { // Don't restore tag anchor positions as they're fixed
node.x = savedPos.x;
node.y = savedPos.y;
node.vx = savedPos.vx || 0;
node.vy = savedPos.vy || 0;
restoredCount++;
}
});
debug("Generated graph data", {
nodeCount: nodes.length,
linkCount: links.length,
restoredPositions: restoredCount
});
if (!nodes.length) {
@ -265,16 +325,25 @@ @@ -265,16 +325,25 @@
// Create new simulation
debug("Creating new simulation");
const hasRestoredPositions = restoredCount > 0;
if (starVisualization) {
// Use star-specific simulation
simulation = createStarSimulation(nodes, links, width, height);
// Apply initial star positioning
// Apply initial star positioning only if we don't have restored positions
if (!hasRestoredPositions) {
applyInitialStarPositions(nodes, links, width, height);
}
} else {
// Use regular simulation
simulation = createSimulation(nodes, links, NODE_RADIUS, LINK_DISTANCE);
}
// Use gentler alpha for updates with restored positions
if (hasRestoredPositions) {
simulation.alpha(0.3); // Gentler restart
}
// Center the nodes when the simulation is done
if (simulation) {
simulation.on("end", () => {
@ -643,6 +712,55 @@ @@ -643,6 +712,55 @@
}
});
// Track previous values to avoid unnecessary calls
let previousDepth = $state(0);
let previousTagType = $state(selectedTagType);
let isInitialized = $state(false);
// Mark as initialized after first render
$effect(() => {
if (!isInitialized && svg) {
isInitialized = true;
}
});
/**
* Watch for tag expansion depth changes
*/
$effect(() => {
// Skip if not initialized or no callback
if (!isInitialized || !onTagExpansionChange) return;
// Check if we need to trigger expansion
const depthChanged = tagExpansionDepth !== previousDepth;
const tagTypeChanged = selectedTagType !== previousTagType;
const shouldExpand = showTagAnchors && (depthChanged || tagTypeChanged);
if (shouldExpand) {
previousDepth = tagExpansionDepth;
previousTagType = selectedTagType;
// Extract unique tags from current events
const tags = new Set<string>();
events.forEach(event => {
const eventTags = event.getMatchingTags(selectedTagType);
eventTags.forEach(tag => {
if (tag[1]) tags.add(tag[1]);
});
});
debug("Tag expansion requested", {
depth: tagExpansionDepth,
tagType: selectedTagType,
tags: Array.from(tags),
depthChanged,
tagTypeChanged
});
onTagExpansionChange(tagExpansionDepth, Array.from(tags));
}
});
/**
* Handles tooltip close event
*/
@ -724,6 +842,7 @@ @@ -724,6 +842,7 @@
starMode={starVisualization}
showTags={showTagAnchors}
tagAnchors={tagAnchorInfo}
eventCounts={eventCounts}
/>
<!-- Settings Panel (shown when settings button is clicked) -->
@ -733,6 +852,7 @@ @@ -733,6 +852,7 @@
bind:starVisualization
bind:showTagAnchors
bind:selectedTagType
bind:tagExpansionDepth
/>
<!-- svelte-ignore a11y_click_events_have_key_events -->

8
src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts

@ -74,6 +74,8 @@ export function extractUniqueTagsForType( @@ -74,6 +74,8 @@ export function extractUniqueTagsForType(
// Map of tagValue -> Set of event IDs
const tagMap = new Map<string, Set<string>>();
console.log(`[TagBuilder] Extracting tags of type: ${tagType} from ${events.length} events`);
events.forEach((event) => {
if (!event.tags || !event.id) return;
@ -93,6 +95,8 @@ export function extractUniqueTagsForType( @@ -93,6 +95,8 @@ export function extractUniqueTagsForType(
});
});
console.log(`[TagBuilder] Found ${tagMap.size} unique tags of type ${tagType}:`, Array.from(tagMap.keys()));
return tagMap;
}
@ -108,8 +112,10 @@ export function createTagAnchorNodes( @@ -108,8 +112,10 @@ export function createTagAnchorNodes(
const anchorNodes: NetworkNode[] = [];
// Calculate positions for tag anchors randomly within radius
// For single publication view, show all tags. For network view, only show tags with 2+ events
const minEventCount = tagMap.size <= 10 ? 1 : 2;
const validTags = Array.from(tagMap.entries()).filter(
([_, eventIds]) => eventIds.size >= 2,
([_, eventIds]) => eventIds.size >= minEventCount,
);
if (validTags.length === 0) return [];

86
src/routes/visualize/+page.svelte

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
import { networkFetchLimit } from "$lib/state";
// Configuration
const DEBUG = false; // Set to true to enable debug logging
const DEBUG = true; // Set to true to enable debug logging
const INDEX_EVENT_KIND = 30040;
const CONTENT_EVENT_KINDS = [30041, 30818];
@ -31,6 +31,8 @@ @@ -31,6 +31,8 @@
let loading = true;
let error: string | null = null;
let showSettings = false;
let tagExpansionDepth = 0;
let baseEvents: NDKEvent[] = []; // Store original events before expansion
/**
* Fetches events from the Nostr network
@ -95,6 +97,7 @@ @@ -95,6 +97,7 @@
// Step 5: Combine both sets of events
events = [...Array.from(validIndexEvents), ...Array.from(contentEvents)];
baseEvents = [...events]; // Store base events for tag expansion
debug("Total events for visualization:", events.length);
} catch (e) {
console.error("Error fetching events:", e);
@ -105,6 +108,85 @@ @@ -105,6 +108,85 @@
}
/**
* Handles tag expansion to fetch related publications
*/
async function handleTagExpansion(depth: number, tags: string[]) {
debug("Handling tag expansion", { depth, tags });
if (depth === 0 || tags.length === 0) {
// Reset to base events only
events = [...baseEvents];
return;
}
try {
// Don't show loading spinner for incremental updates
error = null;
// Keep track of existing event IDs to avoid duplicates
const existingEventIds = new Set(baseEvents.map(e => e.id));
// Fetch publications that have any of the specified tags
const taggedPublications = await $ndkInstance.fetchEvents({
kinds: [INDEX_EVENT_KIND],
"#t": tags, // Match any of these tags
limit: 30 * depth // Reasonable limit based on depth
});
debug("Found tagged publications:", taggedPublications.size);
// Filter to avoid duplicates
const newPublications = Array.from(taggedPublications).filter(
event => !existingEventIds.has(event.id)
);
// Extract content event IDs from new publications
const contentEventIds = new Set<string>();
const existingContentIds = new Set(
baseEvents.filter(e => CONTENT_EVENT_KINDS.includes(e.kind)).map(e => e.id)
);
newPublications.forEach((event) => {
const aTags = event.getMatchingTags("a");
aTags.forEach((tag) => {
const eventId = tag[3];
if (eventId && !existingContentIds.has(eventId)) {
contentEventIds.add(eventId);
}
});
});
// Fetch the content events
let newContentEvents: NDKEvent[] = [];
if (contentEventIds.size > 0) {
const contentEventsSet = await $ndkInstance.fetchEvents({
kinds: CONTENT_EVENT_KINDS,
ids: Array.from(contentEventIds),
});
newContentEvents = Array.from(contentEventsSet);
}
// Combine all events: base events + new publications + new content
events = [
...baseEvents,
...newPublications,
...newContentEvents
];
debug("Events after expansion:", {
base: baseEvents.length,
newPubs: newPublications.length,
newContent: newContentEvents.length,
total: events.length
});
} catch (e) {
console.error("Error expanding tags:", e);
error = e instanceof Error ? e.message : String(e);
}
}
// Fetch events when component mounts
onMount(() => {
debug("Component mounted");
@ -159,6 +241,6 @@ @@ -159,6 +241,6 @@
<!-- Network visualization -->
{:else}
<!-- Event network visualization -->
<EventNetwork {events} onupdate={fetchEvents} />
<EventNetwork {events} onupdate={fetchEvents} onTagExpansionChange={handleTagExpansion} />
{/if}
</div>

Loading…
Cancel
Save