Skip to content

Commit 512e8df

Browse files
committed
feat: clarify link strength and search activity
1 parent 8216f33 commit 512e8df

9 files changed

Lines changed: 480 additions & 272 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Read the existing code and docs before changing behavior.
1717
- Prefer minimal, local changes over broad rewrites.
1818
- If a task spans multiple files, the most likely hotspots are `src/App.tsx`, `src/GraphManager.ts`, `src/components/*`, `src/features/structured-view/*`, `src/hooks/useGraphState.ts`, and matching tests under `src/`.
19+
- Search/pathfinding UX work most often lands in `src/components/SearchStatusOverlay.tsx`, `src/components/ConnectionStatusBar.tsx`, and the path/queue orchestration inside `src/App.tsx`.
1920
- Do not commit generated local artifacts such as `.preview.*` or `.playwright-cli/`.
2021
- If instructions or release steps seem stale, call that out explicitly in the handoff.
2122
- For graph-layout changes, sanity-check first-load behavior with at least two root topics in `web` mode before calling the UX done.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ GitHub: `https://github.com/StoneHub/WikiWebMap`
4545
- Drag canvas to pan; scroll to zoom.
4646
- Click a node to open details and actions.
4747
- Select up to 2 nodes to search for connections. On desktop you can Shift+Click nodes; on touch devices you can use the node details panel.
48+
- Queued bridge searches run automatically, and the `Search Activity` panel shows progress, pause/resume controls, and whether alternate-bridge search is enabled.
4849
- Alt/Option+Drag box-selects nodes for bulk actions on desktop.
4950

5051
## License

docs/development-plan.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This repo is now in a safer release-ready state than it was at clone time:
1212
- `web` mode now seeds root topics from root-count-aware positions instead of total-node-count drift, which keeps first-load layouts more balanced.
1313
- Project attribution and external links now live with the bottom-left graph tools on desktop, with matching ownership/Wikimedia notice language in the UI and docs.
1414
- Neutral placeholder art now avoids leaning on Wikimedia-looking fallback branding when a topic has no thumbnail.
15+
- Link-strength cues are now surfaced in both the legend and connection drawer, so “strong ties” are no longer hidden inside rendering math alone.
16+
- The old green `Search Terminal` is now a calmer `Search Activity` panel with automatic queue messaging, optional detail logs, and duplicate-search prevention.
1517

1618
Renderer planning note:
1719
- The next radically different visualization experiment is a React Flow-based `Structured View`; see `docs/react-flow-structured-view-plan.md`.
@@ -121,8 +123,9 @@ Recommended order:
121123
## Immediate next improvements
122124

123125
1. Add a one-click `Spread Roots` action so users can quickly recreate the “pull major topics apart, let the inner nodes congregate” layout they naturally discovered.
124-
2. Turn current link-strength scoring into clearer visual language with a legend entry for strong ties, shared-neighbor ties, and cross-branch bridges.
125-
3. Add a small server-side or edge cache/proxy layer for Wikipedia requests so abuse control, request budgets, and API identification are no longer purely client-enforced.
126+
2. Add a small server-side or edge cache/proxy layer for Wikipedia requests so abuse control, request budgets, and API identification are no longer purely client-enforced.
127+
3. Let users pin a `search recipe` from the activity panel, such as “find alternate bridges” or “pause after first result,” so repeated exploration sessions feel more intentional.
128+
4. Add a `connection lens` mode that temporarily brightens only the strongest ties around a focused topic.
126129

127130
## Branch strategy
128131

docs/ux-effects-plan.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Core principles:
4747
- Nodes now have softer halos and better root emphasis, but the graph still does not fully communicate connection importance at a glance.
4848
- Link weight and branch significance are still more present in the data than in the visuals.
4949

50+
### 3b. Search activity should feel like product guidance, not a debug console
51+
- The old terminal look is gone, but the activity panel can still get smarter about when to stay quiet, when to summarize, and when to reveal deeper detail.
52+
- Search controls should feel integrated with discovery, not like a separate developer tool floating above it.
53+
5054
### 4. Motion is mostly utilitarian
5155
- The graph has energy, but UI transitions and contextual reveals are still basic.
5256
- There is room for effects that make cause-and-effect easier to understand.
@@ -126,6 +130,7 @@ Suggested effects:
126130
- Search panel fade/slide on first load
127131
- Node details sheet spring-in on mobile
128132
- Context drawer expand/collapse animation
133+
- Search Activity panel state transitions that gracefully collapse from active search to standby summary
129134
- Path result pulse or trace effect in [GraphManager.ts](/C:/Users/monro/Codex/WikiWebMap/src/GraphManager.ts)
130135
- Softer background parallax or light-field drift in [LensingGridBackground.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/LensingGridBackground.tsx)
131136

@@ -178,7 +183,7 @@ Risk:
178183
### Top 5 to do next
179184
1. Add a “root spread” assist in [GraphManager.ts](/C:/Users/monro/Codex/WikiWebMap/src/GraphManager.ts) so major topics land in cleaner perimeter positions before the user drags them
180185
2. Convert connection context into a more deliberate mobile/desktop drawer pattern in [ConnectionStatusBar.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/ConnectionStatusBar.tsx)
181-
3. Add weighted link styling and legend language so strong/shared-topic bridges read immediately in [GraphManager.ts](/C:/Users/monro/Codex/WikiWebMap/src/GraphManager.ts) and [GraphControls.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/GraphControls.tsx)
186+
3. Make the `Search Activity` panel smarter about automatic collapse, session presets, and result summaries in [SearchStatusOverlay.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/SearchStatusOverlay.tsx)
182187
4. Polish the node details sheet in [NodeDetailsPanel.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/NodeDetailsPanel.tsx)
183188
5. Add motion rules and shared transitions in [index.css](/C:/Users/monro/Codex/WikiWebMap/src/index.css)
184189

src/App.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,8 @@ const WikiWebExplorer = () => {
702702
keepSearchingRef.current = keepSearching;
703703
}, [keepSearching]);
704704

705-
const findPath = (startInput: string, endInput: string) =>
706-
runPathfinder({
705+
const findPath = (startInput: string, endInput: string) =>
706+
runPathfinder({
707707
startInput,
708708
endInput,
709709
maxDepth: recursionDepth * 2,
@@ -720,19 +720,36 @@ const WikiWebExplorer = () => {
720720
onFoundPath: (found) => {
721721
setFoundPaths(prev => [...prev, found]);
722722
setSearchDockLinkId(prev => prev ?? found.triggerLinkId);
723-
},
724-
});
725-
726-
const enqueueSearch = async (from: string, to: string, source: 'suggested' | 'shift') => {
727-
const passedVerification = await RecaptchaService.verify('pathfinding');
728-
if (!passedVerification) {
729-
setError('Bot verification failed. Please try again.');
730-
return;
731-
}
732-
setSearchQueue(prev => {
733-
if (prev.length >= 3 || searchQueueRef.current.length >= 3) {
734-
setError('Search queue full (max 3).');
735-
return prev;
723+
},
724+
});
725+
726+
const getSearchKey = (from: string, to: string) =>
727+
`${from.trim().toLowerCase()}${to.trim().toLowerCase()}`;
728+
729+
const enqueueSearch = async (from: string, to: string, source: 'suggested' | 'shift') => {
730+
const passedVerification = await RecaptchaService.verify('pathfinding');
731+
if (!passedVerification) {
732+
setError('Bot verification failed. Please try again.');
733+
return;
734+
}
735+
736+
const requestedKey = getSearchKey(from, to);
737+
const activeKey = activeSearch ? getSearchKey(activeSearch.from, activeSearch.to) : null;
738+
const duplicateQueued = searchQueueRef.current.some((job) => getSearchKey(job.from, job.to) === requestedKey);
739+
if (activeKey === requestedKey || duplicateQueued) {
740+
setError('That search is already running or queued.');
741+
setSearchTerminalMinimized(false);
742+
return;
743+
}
744+
745+
setSearchQueue(prev => {
746+
if (prev.some((job) => getSearchKey(job.from, job.to) === requestedKey)) {
747+
setError('That search is already queued.');
748+
return prev;
749+
}
750+
if (prev.length >= 3 || searchQueueRef.current.length >= 3) {
751+
setError('Search queue full (max 3).');
752+
return prev;
736753
}
737754
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
738755
const next = [...prev, { id, from, to, source }];
@@ -824,6 +841,9 @@ const WikiWebExplorer = () => {
824841
const pinnedLinks = pinnedState.ids
825842
.map(id => graphManagerRef.current?.getLinkById(id))
826843
.filter((x): x is Link => Boolean(x));
844+
const displayedLinkInsight = displayedLinkId
845+
? graphManagerRef.current?.getLinkInsightSummary(displayedLinkId)
846+
: undefined;
827847
const clickedNodeMeta = clickedNode
828848
? graphManagerRef.current?.getNodeMetadata(clickedNode.id)
829849
: undefined;
@@ -973,6 +993,7 @@ const WikiWebExplorer = () => {
973993

974994
<ConnectionStatusBar
975995
link={displayedLink}
996+
linkInsight={displayedLinkInsight}
976997
pinnedLinks={pinnedLinks}
977998
selectedPinnedLinkId={pinnedState.selectedId}
978999
isTouchDevice={isTouchDevice}

src/GraphManager.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ type LinkInsight = {
116116
strength: number;
117117
};
118118

119+
export type LinkInsightSummary = {
120+
role: 'primary' | 'cross' | 'backlink' | 'path';
121+
tier: 'light' | 'moderate' | 'strong';
122+
sharedNeighbors: number;
123+
sharedNeighborRatio: number;
124+
isReciprocal: boolean;
125+
strength: number;
126+
};
127+
119128
/**
120129
* GraphManager - Imperative D3 graph management
121130
* Owns the simulation and DOM, provides methods for incremental updates
@@ -1948,6 +1957,40 @@ export class GraphManager {
19481957
return this.links.find(l => l.id === linkId);
19491958
}
19501959

1960+
getLinkInsightSummary(linkId: string): LinkInsightSummary | undefined {
1961+
const link = this.getLinkById(linkId);
1962+
if (!link) return undefined;
1963+
1964+
const insight = this.getLinkInsight(link);
1965+
const sourceId = typeof link.source === 'object' ? link.source.id : link.source;
1966+
const targetId = typeof link.target === 'object' ? link.target.id : link.target;
1967+
const sourceMeta = this.nodeMetadata.get(sourceId);
1968+
const targetMeta = this.nodeMetadata.get(targetId);
1969+
const isPathLink = Boolean(sourceMeta?.isInPath && targetMeta?.isInPath);
1970+
const isBacklink = typeof link.type === 'string' && link.type.includes('backlink');
1971+
const role: LinkInsightSummary['role'] = isPathLink
1972+
? 'path'
1973+
: isBacklink
1974+
? 'backlink'
1975+
: link.layoutRole === 'cross'
1976+
? 'cross'
1977+
: 'primary';
1978+
const tier: LinkInsightSummary['tier'] = insight.strength >= 0.72 || insight.sharedNeighbors >= 3
1979+
? 'strong'
1980+
: insight.strength >= 0.46 || insight.sharedNeighbors >= 1
1981+
? 'moderate'
1982+
: 'light';
1983+
1984+
return {
1985+
role,
1986+
tier,
1987+
sharedNeighbors: insight.sharedNeighbors,
1988+
sharedNeighborRatio: insight.sharedNeighborRatio,
1989+
isReciprocal: insight.isReciprocal,
1990+
strength: insight.strength,
1991+
};
1992+
}
1993+
19511994
getLensingNodes(): Array<{ x: number; y: number; mass: number }> {
19521995
const transform = d3.zoomTransform(this.svg.node()!);
19531996

src/components/ConnectionStatusBar.tsx

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useEffect, useState } from 'react';
2-
import type { Link } from '../GraphManager';
2+
import type { Link, LinkInsightSummary } from '../GraphManager';
33

44
export function ConnectionStatusBar(props: {
55
link: Link | null;
6+
linkInsight?: LinkInsightSummary;
67
pinnedLinks: Link[];
78
selectedPinnedLinkId: string | null;
89
isTouchDevice: boolean;
@@ -53,13 +54,47 @@ export function ConnectionStatusBar(props: {
5354

5455
const isPinned = Boolean(link && props.pinnedLinks.some(l => l.id === link.id));
5556
const hasLongContext = Boolean(link?.context && link.context.length > 180);
57+
const linkInsight = props.linkInsight;
5658
const wrapperClassName = props.isTouchDevice
5759
? 'fixed inset-x-3 bottom-[6.8rem] z-30 pointer-events-none'
5860
: 'fixed left-3 right-3 bottom-3 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-40 sm:w-[min(720px,calc(100vw-1.5rem))] pointer-events-none';
5961
const panelClassName = props.isTouchDevice
6062
? 'pointer-events-auto rounded-[1.75rem] border border-gray-700/60 bg-gray-900/90 px-4 py-4 shadow-[0_20px_60px_rgba(2,6,23,0.55)] backdrop-blur-md'
6163
: 'pointer-events-auto bg-gray-900/85 backdrop-blur-md border border-gray-700/60 rounded-2xl shadow-2xl px-4 py-3';
6264

65+
const relationshipLabel = (() => {
66+
if (!linkInsight) return null;
67+
if (linkInsight.role === 'path') return 'Path bridge';
68+
if (linkInsight.role === 'backlink') return linkInsight.isReciprocal ? 'Mutual backlink' : 'Incoming reference';
69+
if (linkInsight.role === 'cross') {
70+
return linkInsight.tier === 'strong' ? 'Strong bridge' : 'Bridge link';
71+
}
72+
return linkInsight.tier === 'strong'
73+
? 'Strong tie'
74+
: linkInsight.tier === 'moderate'
75+
? 'Related topics'
76+
: 'Light tie';
77+
})();
78+
79+
const relationshipCopy = (() => {
80+
if (!linkInsight) return null;
81+
if (linkInsight.role === 'path') {
82+
return 'This highlighted edge is part of the active bridge the pathfinder reconstructed.';
83+
}
84+
if (linkInsight.role === 'cross') {
85+
return linkInsight.sharedNeighbors > 0
86+
? `This bridge jumps between branches and still shares ${linkInsight.sharedNeighbors} nearby topic${linkInsight.sharedNeighbors === 1 ? '' : 's'}.`
87+
: 'This bridge jumps between branches even though the local neighborhoods stay fairly distinct.';
88+
}
89+
if (linkInsight.sharedNeighbors > 0) {
90+
return `These topics share ${linkInsight.sharedNeighbors} nearby Wikipedia topic${linkInsight.sharedNeighbors === 1 ? '' : 's'}, which is why this connection renders more strongly.`;
91+
}
92+
if (linkInsight.isReciprocal) {
93+
return 'These articles point back to each other, so the relationship gets a stronger visual treatment.';
94+
}
95+
return 'This is a lighter connection with less local overlap than the stronger highlighted ties.';
96+
})();
97+
6398
return (
6499
<div className={wrapperClassName}>
65100
<div className={panelClassName}>
@@ -124,23 +159,42 @@ export function ConnectionStatusBar(props: {
124159
)}
125160
</div>
126161
{link ? (
127-
<div className="mt-2 text-sm font-semibold text-white leading-relaxed">
128-
<button
129-
onClick={() => props.onFocusNode(source)}
130-
className="underline decoration-white/20 hover:decoration-white/60"
131-
title="Focus this topic in the map"
132-
>
133-
{source}
134-
</button>{' '}
135-
<span className="text-gray-300">{direction}</span>{' '}
136-
<button
137-
onClick={() => props.onFocusNode(target)}
138-
className="underline decoration-white/20 hover:decoration-white/60"
139-
title="Focus this topic in the map"
140-
>
141-
{target}
142-
</button>
143-
</div>
162+
<>
163+
<div className="mt-2 text-sm font-semibold text-white leading-relaxed">
164+
<button
165+
onClick={() => props.onFocusNode(source)}
166+
className="underline decoration-white/20 hover:decoration-white/60"
167+
title="Focus this topic in the map"
168+
>
169+
{source}
170+
</button>{' '}
171+
<span className="text-gray-300">{direction}</span>{' '}
172+
<button
173+
onClick={() => props.onFocusNode(target)}
174+
className="underline decoration-white/20 hover:decoration-white/60"
175+
title="Focus this topic in the map"
176+
>
177+
{target}
178+
</button>
179+
</div>
180+
{relationshipLabel && (
181+
<div className="mt-2 flex flex-wrap gap-2 text-[11px] text-slate-200">
182+
<span className="rounded-full border border-cyan-400/20 bg-cyan-400/10 px-2.5 py-1 text-cyan-100">
183+
{relationshipLabel}
184+
</span>
185+
{linkInsight && linkInsight.sharedNeighbors > 0 && (
186+
<span className="rounded-full border border-slate-700/70 bg-black/20 px-2.5 py-1 text-slate-200">
187+
Shared neighbors: {linkInsight.sharedNeighbors}
188+
</span>
189+
)}
190+
{linkInsight?.isReciprocal && (
191+
<span className="rounded-full border border-amber-400/20 bg-amber-400/10 px-2.5 py-1 text-amber-100">
192+
Mutual references
193+
</span>
194+
)}
195+
</div>
196+
)}
197+
</>
144198
) : (
145199
<div className="mt-2 text-sm font-semibold text-white">
146200
Select a pinned connection.
@@ -170,7 +224,7 @@ export function ConnectionStatusBar(props: {
170224
<div className="mt-3 bg-black/30 rounded-2xl border border-gray-700/50 px-3 py-3">
171225
<div className="mb-2 flex items-center justify-between gap-3">
172226
<div className="text-[10px] uppercase tracking-[0.22em] text-gray-400">
173-
Article Snippet
227+
Why These Topics Connect
174228
</div>
175229
{hasLongContext && !isLoading && (
176230
<button
@@ -195,8 +249,15 @@ export function ConnectionStatusBar(props: {
195249
</div>
196250
</div>
197251
) : (
198-
<div className={`text-[12px] text-gray-200 leading-relaxed italic ${showFullContext ? '' : 'line-clamp-3'}`}>
199-
{link?.context}
252+
<div className="space-y-3">
253+
{relationshipCopy && (
254+
<div className="rounded-xl border border-cyan-400/15 bg-cyan-400/5 px-3 py-2 text-[11px] leading-relaxed text-slate-200">
255+
{relationshipCopy}
256+
</div>
257+
)}
258+
<div className={`text-[12px] text-gray-200 leading-relaxed italic ${showFullContext ? '' : 'line-clamp-3'}`}>
259+
{link?.context}
260+
</div>
200261
</div>
201262
)}
202263
</div>

0 commit comments

Comments
 (0)