An orbiting point cloud of all 8,424 indexed passages, with the 262 highlighted ones lit and threaded in the order they were marked. Data comes from library-rag (chunks + embeddings) projected to 3D with UMAP; the page never touches Postgres. Projection is `airy` (n_neighbors=50, min_dist=0.8) and the palette is `frost` -- both chosen by eye against the alternatives. Note that airy shows the underlying book-territory structure LESS than the tight projection the pre-flight measured, so the 0.805 k-NN purity figure describes that one, not this. Page weight: the wrapper adds 321 B gzip to the homepage's blocking load. `three` (127 KB gz) and the payload (56 KB gz) are a dynamic chunk fetched after render. It fails closed -- no WebGL, no payload, or a dead fetch leaves a plain page. The payload is a static snapshot; the 5-minute auto-build rebuilds the site, not the data. New reading reaches this map only when export.py is re-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.0 KiB
Plaintext
84 lines
3.0 KiB
Plaintext
---
|
|
/**
|
|
* The reading map — an orbiting point cloud of every passage read, traced in the
|
|
* order it was highlighted.
|
|
*
|
|
* import ReadingMap from '../components/ReadingMap.astro';
|
|
* <ReadingMap />
|
|
*
|
|
* Pieces:
|
|
* src/components/ReadingMap.astro ← this file
|
|
* src/lib/reading-map.js ← the renderer (shared with the standalone preview
|
|
* in c0smere_devops/library-rag/umap/web)
|
|
* src/styles/reading-map.css ← the chrome
|
|
* public/reading_umap.json ← `uv run umap/export.py -o …`
|
|
*
|
|
* ⚠️ The payload is a STATIC SNAPSHOT. Nothing on cyrion regenerates it — the 5-minute
|
|
* auto-build rebuilds the site, not the data. New reading only reaches this map when
|
|
* someone re-runs `export.py` and commits the result. See library-rag/umap/PLAN.md §7.
|
|
*/
|
|
import '../styles/reading-map.css';
|
|
|
|
interface Props {
|
|
/** Payload URL. Same-origin: a static file, not an API call. */
|
|
src?: string;
|
|
/** Caption under the canvas. Pass `null` to omit. */
|
|
caption?: string | null;
|
|
}
|
|
|
|
const {
|
|
src = '/reading_umap.json',
|
|
caption = 'Every passage I have read, arranged by what it is about — traced in the order I highlighted it.',
|
|
} = Astro.props;
|
|
---
|
|
|
|
<figure class="reading-map-figure">
|
|
<div class="reading-map" data-src={src}></div>
|
|
{caption && <figcaption>{caption}</figcaption>}
|
|
</figure>
|
|
|
|
<script>
|
|
// Dynamic import so `three` lands in its own chunk rather than the page bundle, and so a
|
|
// browser that never gets here (no WebGL, blocked script) pays nothing for it.
|
|
//
|
|
// ⚠️ Do NOT gate this on IntersectionObserver. `.reading-map:not([data-ready])` is
|
|
// `display: none` — that is the fail-closed rule — and a `display: none` element has no
|
|
// box, so it never intersects and the observer would never fire. Load-on-visible here
|
|
// deadlocks: hidden until mounted, never mounted because hidden. The renderer runs its
|
|
// own IntersectionObserver internally to park the animation loop offscreen, which is the
|
|
// part that actually matters for cost.
|
|
const containers = document.querySelectorAll<HTMLElement>('.reading-map');
|
|
if (containers.length) {
|
|
try {
|
|
const { mount } = await import('../lib/reading-map.js');
|
|
for (const el of containers) {
|
|
mount(el, {
|
|
src: el.dataset.src!,
|
|
// Fail closed, like the garden's other widgets: a missing payload or a dead GPU
|
|
// leaves a plain page rather than an empty bordered box.
|
|
onError: (err: unknown) => console.warn('reading map unavailable:', err),
|
|
});
|
|
}
|
|
} catch (err) {
|
|
/* chunk failed to load: the figure stays hidden */
|
|
console.warn('reading map unavailable:', err);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.reading-map-figure {
|
|
margin: 0 0 2rem;
|
|
}
|
|
.reading-map-figure figcaption {
|
|
margin-top: 0.6rem;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
opacity: 0.72;
|
|
}
|
|
/* Hide the caption too when the map itself failed to mount. */
|
|
.reading-map-figure:not(:has([data-ready])) figcaption {
|
|
display: none;
|
|
}
|
|
</style>
|