Skip to content

Commit 8216f33

Browse files
committed
feat: balance root layout and add project attribution
1 parent 99f322c commit 8216f33

12 files changed

Lines changed: 282 additions & 43 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
- Read `docs/deployment.md` and `docs/release-checklist.md` before changing CI or release behavior.
1111
- When touching the new layout/mobile flow, read `docs/ux-effects-plan.md`.
1212
- When touching the React Flow renderer or alternate view modes, read `docs/react-flow-structured-view-plan.md`.
13+
- When touching public attribution, Wikimedia references, or API-identification behavior, also check `README.md` and `NOTICE`.
1314

1415
## Working Rules
1516
- Read the existing code and docs before changing behavior.
1617
- Prefer minimal, local changes over broad rewrites.
1718
- 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/`.
1819
- Do not commit generated local artifacts such as `.preview.*` or `.playwright-cli/`.
1920
- If instructions or release steps seem stale, call that out explicitly in the handoff.
21+
- For graph-layout changes, sanity-check first-load behavior with at least two root topics in `web` mode before calling the UX done.
22+
- For Wikimedia/API wording, verify against official Wikimedia policy pages and keep the UI notice, `README.md`, and `NOTICE` aligned.
2023

2124
## Verification
2225
- Install dependencies with `npm ci`.

NOTICE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Copyright 2025 Monroe
55
This product includes software developed by the WikiWebMap contributors.
66

77
Wikipedia content and page metadata are provided by the Wikimedia Foundation and are
8-
licensed under CC BY-SA 4.0. This project is not affiliated with Wikimedia Foundation.
8+
licensed under CC BY-SA 4.0. This project is not affiliated with or endorsed by the Wikimedia Foundation.
9+
Wikipedia is a trademark of the Wikimedia Foundation.

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Explore Wikipedia topics as an interactive graph: expand nodes, search for connections between topics, and visualize link context.
44

55
Live: `https://wikiconnectionsmap.web.app/`
6+
Website: `https://monroes.tech`
7+
GitHub: `https://github.com/StoneHub/WikiWebMap`
68

79
## Quickstart
810

@@ -48,5 +50,12 @@ Live: `https://wikiconnectionsmap.web.app/`
4850
## License
4951
Apache-2.0. See `LICENSE`.
5052

53+
## Ownership, API, and Wikimedia Notice
54+
- This repo is currently Apache-2.0. That means attribution and branding help establish authorship, but they do not prevent someone else from reusing or forking the code under the license terms.
55+
- Set `VITE_WIKI_API_CONTACT_EMAIL` so requests send a descriptive `Api-User-Agent` header to Wikipedia.
56+
- The app rate-limits and caches Wikipedia requests, and `VITE_RECAPTCHA_SITE_KEY` can be used to reduce automated abuse on the client side.
57+
- Wikipedia is a trademark of the Wikimedia Foundation. WikiWebMap is an independent project and is not affiliated with or endorsed by the Wikimedia Foundation.
58+
5159
## Attribution
52-
- Wikipedia/Wikimedia content is licensed under CC BY-SA 4.0. This project is not affiliated with the Wikimedia Foundation.
60+
- Wikipedia/Wikimedia content is licensed under CC BY-SA 4.0.
61+
- This project is not affiliated with or endorsed by the Wikimedia Foundation.

docs/development-plan.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ This repo is now in a safer release-ready state than it was at clone time:
99
- Mobile guidance and destructive actions are safer.
1010
- The repo now has unit coverage for `WikiService`, `runPathfinder`, and `useGraphState` reset/restore behavior.
1111
- Session diagnostics now include both connection logs and captured client runtime errors.
12+
- `web` mode now seeds root topics from root-count-aware positions instead of total-node-count drift, which keeps first-load layouts more balanced.
13+
- 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.
14+
- Neutral placeholder art now avoids leaning on Wikimedia-looking fallback branding when a topic has no thumbnail.
1215

1316
Renderer planning note:
1417
- The next radically different visualization experiment is a React Flow-based `Structured View`; see `docs/react-flow-structured-view-plan.md`.
@@ -115,6 +118,12 @@ Recommended order:
115118
| P6 | UX | Improve onboarding and mobile drawer polish | Increases usability and product trust | Medium | Low | Phase 5 |
116119
| P7 | Observability | Add remote client error reporting | Makes live issues easier to diagnose after release | Medium | Low | Phase 5 |
117120

121+
## Immediate next improvements
122+
123+
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+
118127
## Branch strategy
119128

120129
- Use a release PR to merge the current hardening work to `main`.

docs/ux-effects-plan.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Core principles:
3131
- New users do not get a strong explanation of what to do first.
3232
- Suggested paths help, but the screen still relies on inference.
3333

34+
### 1a. First-load spatial rhythm still needs refinement
35+
- Root topics now seed more intelligently, but web mode still needs stronger first-load balance when multiple major topics are added quickly.
36+
- The most successful layouts still happen after a user drags roots outward and lets inner topics settle between them.
37+
3438
### 2. Mobile feels functional, not polished
3539
- The mobile sheets are safer now, but they still feel like adapted desktop UI.
3640
- Important actions are present, but the experience is not yet elegant.
@@ -39,6 +43,10 @@ Core principles:
3943
- The search box, graph, connection context, settings, and diagnostics all compete.
4044
- There is not yet a strong “primary task” rhythm on load.
4145

46+
### 3a. Topic visuals are improving but still have room to mature
47+
- Nodes now have softer halos and better root emphasis, but the graph still does not fully communicate connection importance at a glance.
48+
- Link weight and branch significance are still more present in the data than in the visuals.
49+
4250
### 4. Motion is mostly utilitarian
4351
- The graph has energy, but UI transitions and contextual reveals are still basic.
4452
- There is room for effects that make cause-and-effect easier to understand.
@@ -48,6 +56,23 @@ Core principles:
4856

4957
## Recommended rollout
5058

59+
### Phase 0: Space and trust pass
60+
Goal:
61+
- Make the first-loaded graph feel more balanced and make project ownership/independence clearer without crowding the main panel.
62+
63+
Changes:
64+
- Keep root topics on soft perimeter anchors in `web` mode instead of letting total node count fling them outward.
65+
- Preserve the lower-left authorship/trust strip as part of the graph tool area rather than the intro panel.
66+
- Continue using neutral custom placeholder artwork instead of anything that could read as Wikimedia branding.
67+
68+
Suggested UI work:
69+
- Tune soft-anchor strength and perimeter radius logic in [GraphManager.ts](/C:/Users/monro/Codex/WikiWebMap/src/GraphManager.ts)
70+
- Tune first-seed and multi-seed spawn logic in [useGraphState.ts](/C:/Users/monro/Codex/WikiWebMap/src/hooks/useGraphState.ts)
71+
- Keep attribution and external-link surfaces coordinated between [GraphControls.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/GraphControls.tsx), [SearchOverlay.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/SearchOverlay.tsx), and [README.md](/C:/Users/monro/Codex/WikiWebMap/README.md)
72+
73+
Risk:
74+
- Low
75+
5176
### Phase 1: Clarity pass
5277
Goal:
5378
- Make the product easier to understand in the first minute.
@@ -151,11 +176,11 @@ Risk:
151176
## High-impact UX tasks
152177

153178
### Top 5 to do next
154-
1. Add a stronger first-run search/onboarding block in [SearchOverlay.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/SearchOverlay.tsx)
179+
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
155180
2. Convert connection context into a more deliberate mobile/desktop drawer pattern in [ConnectionStatusBar.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/ConnectionStatusBar.tsx)
156-
3. Add motion rules and shared transitions in [index.css](/C:/Users/monro/Codex/WikiWebMap/src/index.css)
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)
157182
4. Polish the node details sheet in [NodeDetailsPanel.tsx](/C:/Users/monro/Codex/WikiWebMap/src/components/NodeDetailsPanel.tsx)
158-
5. Add a more expressive path-result reveal in [GraphManager.ts](/C:/Users/monro/Codex/WikiWebMap/src/GraphManager.ts)
183+
5. Add motion rules and shared transitions in [index.css](/C:/Users/monro/Codex/WikiWebMap/src/index.css)
159184

160185
## Safe implementation order
161186

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>WikiWeb Explorer - Interactive Wikipedia Knowledge Graph</title>
7+
<title>WikiWebMap - Interactive Graph for Wikipedia Topics</title>
88
</head>
99
<body class="m-0 p-0 overflow-hidden">
1010
<div id="root"></div>

scripts/smoke.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ try {
2828
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
2929
await slowWikipediaRequests(page);
3030
await page.goto(baseUrl, { waitUntil: 'networkidle' });
31+
await page.getByRole('link', { name: 'monroes.tech' }).waitFor({ timeout: 15000 });
32+
await page.getByRole('link', { name: 'GitHub' }).waitFor({ timeout: 15000 });
3133

3234
await page.getByRole('textbox', { name: 'Search a Wikipedia topic' }).fill('Physics');
3335
await page.getByRole('button', { name: 'Start Exploration' }).click();

src/GraphManager.ts

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -419,15 +419,43 @@ export class GraphManager {
419419
private getCollisionRadius(node: Node) {
420420
const connections = this.degreeById.get(node.id) || 0;
421421
const meta = this.getMetadata(node.id);
422-
const layoutBoost = this.usesGuidedTreeLayout() && meta.colorRole === 'root'
423-
? (this.isStructuredMapMode() ? 8 : 6)
422+
const layoutBoost = meta.colorRole === 'root'
423+
? (this.usesGuidedTreeLayout() ? (this.isStructuredMapMode() ? 8 : 6) : 10)
424424
: 0;
425425
return (Math.min(30 + connections * 0.5, 60) * this.nodeSizeScale) + layoutBoost;
426426
}
427427

428+
private getWebRootAnchor(nodeId: string) {
429+
const rootIds = this.nodes
430+
.filter((candidate) => !this.hiddenNodeIds.has(candidate.id) && this.getMetadata(candidate.id).colorRole === 'root')
431+
.map((candidate) => candidate.id);
432+
const index = rootIds.indexOf(nodeId);
433+
const center = { x: this.width / 2, y: this.height / 2 };
434+
435+
if (index === -1 || rootIds.length <= 1) return center;
436+
437+
const count = rootIds.length;
438+
const startAngle = count === 2 ? Math.PI : -Math.PI / 2;
439+
const angle = startAngle + (index / count) * Math.PI * 2;
440+
const radiusX = count === 2
441+
? Math.max(180, Math.min(this.width * 0.22, 320))
442+
: Math.max(180, Math.min(this.width * 0.28, 360));
443+
const radiusY = count === 2
444+
? Math.max(24, Math.min(this.height * 0.04, 48))
445+
: Math.max(120, Math.min(this.height * 0.24, 260));
446+
447+
return {
448+
x: center.x + Math.cos(angle) * radiusX,
449+
y: center.y + Math.sin(angle) * radiusY,
450+
};
451+
}
452+
428453
private getChargeStrength(node: Node) {
429-
if (this.layoutMode === 'web') return -500;
430454
const meta = this.getMetadata(node.id);
455+
if (this.layoutMode === 'web') {
456+
if (meta.colorRole === 'root') return -920;
457+
return -340;
458+
}
431459
if (this.isStructuredMapMode()) {
432460
if (meta.colorRole === 'root') return -340;
433461
if (meta.layoutDepth !== undefined && meta.layoutDepth > 2) return -190;
@@ -437,24 +465,31 @@ export class GraphManager {
437465
}
438466

439467
private getLayoutTarget(node: Node) {
440-
if (!this.usesGuidedTreeLayout()) {
441-
return { x: this.width / 2, y: this.height / 2 };
442-
}
443-
444468
const meta = this.getMetadata(node.id);
445469
if (meta.isPinned && meta.manualPosition) {
446470
return meta.manualPosition;
447471
}
448472

473+
if (!this.usesGuidedTreeLayout()) {
474+
if (meta.colorRole === 'root') {
475+
return this.getWebRootAnchor(node.id);
476+
}
477+
return { x: this.width / 2, y: this.height / 2 };
478+
}
479+
449480
return this.forestTargets.get(node.id) || {
450481
x: node.x ?? this.width / 2,
451482
y: node.y ?? this.height / 2,
452483
};
453484
}
454485

455486
private getTargetStrength(node: Node) {
456-
if (!this.usesGuidedTreeLayout()) return 0.02;
457487
const meta = this.getMetadata(node.id);
488+
if (!this.usesGuidedTreeLayout()) {
489+
if (meta.isPinned) return 0.36;
490+
if (meta.colorRole === 'root') return 0.1;
491+
return 0.008;
492+
}
458493
if (meta.isPinned) return 0.4;
459494
if (this.isStructuredMapMode()) {
460495
if (meta.colorRole === 'root') return 0.24;
@@ -465,7 +500,15 @@ export class GraphManager {
465500
}
466501

467502
private getLinkDistance(link: Link) {
468-
if (!this.usesGuidedTreeLayout()) return this.nodeSpacing;
503+
if (!this.usesGuidedTreeLayout()) {
504+
const sourceId = typeof link.source === 'object' ? link.source.id : link.source;
505+
const targetId = typeof link.target === 'object' ? link.target.id : link.target;
506+
const sourceMeta = this.getMetadata(sourceId);
507+
const targetMeta = this.getMetadata(targetId);
508+
const touchesRoot = sourceMeta.colorRole === 'root' || targetMeta.colorRole === 'root';
509+
if (link.layoutRole === 'cross') return Math.max(this.nodeSpacing * 0.82, 108);
510+
return touchesRoot ? Math.max(this.nodeSpacing * 0.92, 124) : Math.max(this.nodeSpacing * 0.8, 104);
511+
}
469512
if (this.isStructuredMapMode()) {
470513
return link.layoutRole === 'cross'
471514
? Math.max(this.nodeSpacing * 0.6, 96)
@@ -1502,8 +1545,35 @@ export class GraphManager {
15021545

15031546
const radius = this.getCollisionRadius(d) - (this.usesGuidedTreeLayout() && meta.colorRole === 'root' ? 6 : 0);
15041547
const focusScale = this.getFocusScale(meta);
1548+
const nodeColor = this.getNodeColor(d.id, meta);
15051549
inner.attr('transform', `scale(${focusScale})`);
15061550

1551+
const auraData: Array<{
1552+
radius: number;
1553+
fill: string;
1554+
opacity: number;
1555+
}> = meta.isFocusTarget
1556+
? [{ radius: radius + 16, fill: '#22d3ee', opacity: 0.12 }]
1557+
: meta.isPathEndpoint
1558+
? [{ radius: radius + 14, fill: '#f59e0b', opacity: 0.14 }]
1559+
: meta.isPinned
1560+
? [{ radius: radius + 13, fill: '#e2e8f0', opacity: 0.1 }]
1561+
: meta.colorRole === 'root'
1562+
? [{ radius: radius + 12, fill: nodeColor, opacity: 0.16 }]
1563+
: [];
1564+
1565+
inner
1566+
.selectAll<SVGCircleElement, typeof auraData[number]>('circle.node-aura')
1567+
.data(auraData)
1568+
.join(
1569+
(enterSelection) => enterSelection.append('circle').attr('class', 'node-aura'),
1570+
(updateSelection) => updateSelection,
1571+
(exitSelection) => exitSelection.remove()
1572+
)
1573+
.attr('r', (aura) => aura.radius)
1574+
.attr('fill', (aura) => aura.fill)
1575+
.attr('fill-opacity', (aura) => aura.opacity);
1576+
15071577
const outerRingData: Array<{
15081578
radius: number;
15091579
stroke: string;
@@ -1551,12 +1621,39 @@ export class GraphManager {
15511621
(updateSelection) => updateSelection
15521622
)
15531623
.attr('r', radius)
1554-
.attr('fill', this.getNodeColor(d.id, meta))
1624+
.attr('fill', nodeColor)
15551625
.attr('fill-opacity', meta.thumbnail ? 0.3 : 1)
15561626
.attr('stroke', this.getNodeStroke(meta))
15571627
.attr('stroke-width', this.getNodeStrokeWidth(meta))
15581628
.attr('filter', meta.isFocusTarget ? 'url(#focus-glow)' : meta.isFocusNeighbor ? 'url(#neighbor-glow)' : null);
15591629

1630+
inner
1631+
.selectAll<SVGCircleElement, Node>('circle.node-inner-ring')
1632+
.data([d])
1633+
.join(
1634+
(enterSelection) => enterSelection.append('circle').attr('class', 'node-inner-ring'),
1635+
(updateSelection) => updateSelection
1636+
)
1637+
.attr('r', Math.max(8, radius * 0.74))
1638+
.attr('fill', 'none')
1639+
.attr('stroke', 'rgba(255,255,255,0.22)')
1640+
.attr('stroke-width', Math.max(0.9, 1.1 * this.nodeSizeScale))
1641+
.attr('stroke-opacity', meta.thumbnail ? 0.12 : meta.isDimmed ? 0.1 : 0.24);
1642+
1643+
inner
1644+
.selectAll<SVGEllipseElement, Node>('ellipse.node-sheen')
1645+
.data([d])
1646+
.join(
1647+
(enterSelection) => enterSelection.append('ellipse').attr('class', 'node-sheen'),
1648+
(updateSelection) => updateSelection
1649+
)
1650+
.attr('rx', Math.max(6, radius * 0.38))
1651+
.attr('ry', Math.max(4, radius * 0.22))
1652+
.attr('cx', -radius * 0.16)
1653+
.attr('cy', -radius * 0.22)
1654+
.attr('fill', 'rgba(255,255,255,0.16)')
1655+
.attr('fill-opacity', meta.thumbnail ? 0.08 : meta.isDimmed ? 0.06 : 0.16);
1656+
15601657
this.addTextLabel(inner as any, d.title, radius);
15611658
});
15621659

0 commit comments

Comments
 (0)