@johndimm/constellations 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.tsx +480 -0
- package/FullPageConstellations.tsx +74 -0
- package/FullPageConstellationsHostShell.tsx +27 -0
- package/README.md +116 -0
- package/components/AppConfirmDialog.tsx +46 -0
- package/components/AppHeader.tsx +73 -0
- package/components/AppNotifications.tsx +21 -0
- package/components/BrowsePeople.tsx +832 -0
- package/components/ControlPanel.tsx +1023 -0
- package/components/Graph.tsx +1525 -0
- package/components/HelpOverlay.tsx +168 -0
- package/components/NodeContextMenu.tsx +160 -0
- package/components/PeopleBrowserSidebar.tsx +690 -0
- package/components/Sidebar.tsx +271 -0
- package/components/TimelineView.tsx +4 -0
- package/hooks/useExpansion.ts +889 -0
- package/hooks/useGraphActions.ts +325 -0
- package/hooks/useGraphState.ts +414 -0
- package/hooks/useKioskMode.ts +47 -0
- package/hooks/useNodeClickHandler.ts +172 -0
- package/hooks/useSearchHandlers.ts +369 -0
- package/host.ts +16 -0
- package/index.css +101 -0
- package/index.tsx +16 -0
- package/kioskDomains.ts +307 -0
- package/package.json +78 -0
- package/services/aiUtils.ts +364 -0
- package/services/cacheService.ts +76 -0
- package/services/crossrefService.ts +107 -0
- package/services/geminiService.ts +1359 -0
- package/services/get-local-graphs.js +5 -0
- package/services/graphUtils.ts +347 -0
- package/services/imageService.ts +39 -0
- package/services/llmClient.ts +194 -0
- package/services/openAlexService.ts +173 -0
- package/services/wikipediaImage.ts +40 -0
- package/services/wikipediaService.ts +1175 -0
- package/sessionHandoff.ts +132 -0
- package/types.ts +99 -0
- package/useFullPageConstellationsHost.ts +116 -0
- package/utils/evidenceUtils.ts +107 -0
- package/utils/graphLogicUtils.ts +32 -0
- package/utils/graphNodeToChannelNotes.ts +71 -0
- package/utils/wikiUtils.ts +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 🌌 Constellations
|
|
2
|
+
|
|
3
|
+
**Universal, AI-powered bipartite knowledge graphs.**
|
|
4
|
+
|
|
5
|
+
[](https://news.ycombinator.com/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**[Live Demo](https://constellations-delta.vercel.app/)** | **[LinkedIn Post](https://www.linkedin.com/feed/update/urn:li:activity:7409328608910946304/)**
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
<video src="https://github.com/user-attachments/assets/8c9fb0b0-967e-4285-a71c-c922de8b245a" width="100%" autoplay muted controls playsinline></video>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## The Evolution: From History to Everything
|
|
15
|
+
|
|
16
|
+
Originally designed to map world history, **Constellations** has evolved into a **Universal Bipartite Explorer**. It uses LLMs to identify the fundamental "Atomic" building blocks and the "Composite" collections that connect them in any domain.
|
|
17
|
+
|
|
18
|
+
### Locked bipartite pairs (chosen once at the start)
|
|
19
|
+
Constellations now **locks a single bipartite pair per graph**, chosen from the **first input node** (and then **does not switch**):
|
|
20
|
+
- **Person ↔ Event**
|
|
21
|
+
- **Ingredient ↔ Recipe**
|
|
22
|
+
- **Symptom ↔ Disease**
|
|
23
|
+
- **Author ↔ Paper**
|
|
24
|
+
|
|
25
|
+
This makes exploration more reliable (no mid-graph “ontology drift”), while still supporting multiple domains.
|
|
26
|
+
|
|
27
|
+
### Domains + text input (single unified UI)
|
|
28
|
+
Constellations supports both free text search and curated domain seed lists in the same UI:
|
|
29
|
+
- **Pick a domain** and tap a seed to begin quickly.
|
|
30
|
+
- **Or type** any query to start from an arbitrary entity.
|
|
31
|
+
- **Curate domains/seeds in-app**: add `?admin=1`
|
|
32
|
+
|
|
33
|
+
### Universal Examples:
|
|
34
|
+
| Culinary (Beef) | Sports (LeBron James) | Medicine (Sore Throat) |
|
|
35
|
+
| :---: | :---: | :---: |
|
|
36
|
+
|  |  |  |
|
|
37
|
+
|
|
38
|
+
## Philosophy & Design
|
|
39
|
+
|
|
40
|
+
The core idea is to create collaboration graphs on the fly with **zero pre-computed database**. The graph constructs a local neighborhood around a given node using live LLM queries and expands outward.
|
|
41
|
+
|
|
42
|
+
### The Bipartite Realization
|
|
43
|
+
The app strictly follows a bipartite structure:
|
|
44
|
+
- **Atomic Nodes** (Circles): Fundamental entities (People, Ingredients, Symptoms).
|
|
45
|
+
- **Composite Nodes** (Cards): Collections or events that bring Atomics together (Movies, Recipes, Diseases).
|
|
46
|
+
- **Edges**: Only connect Atomics to the Composites they belong to.
|
|
47
|
+
|
|
48
|
+
This prevents "hallucinated connections" and forces a logical structure onto the space. The AI explains its classification reasoning in the sidebar for every node, and the chosen bipartite pair is locked at the start of each graph.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
## Vibe Coding: The Journey
|
|
53
|
+
|
|
54
|
+
This project was 100% "vibe coded" from start to finish—presenting problems to AI agents and iterating on their solutions. The journey took me through the current landscape of AI development tools:
|
|
55
|
+
|
|
56
|
+
1. **Google AI Studio**: A great start, but I pushed it too far and it took a wrong turn. When it couldn't revert a day's worth of work, I had to "liberate" the project.
|
|
57
|
+
2. **Antigravity**: My second stop, which worked well until I hit my quota.
|
|
58
|
+
3. **Cursor**: Where I spent the bulk of the time. It’s a powerful tool but occasionally the "weakest link"—prone to doom loops and finding the best solution only after exhausting every possible alternative.
|
|
59
|
+
4. **Codex**: When Cursor stalled on complex save/restore and export/import logic, Codex stepped in to finish the job. It was a step up in logic, though not cheap!
|
|
60
|
+
|
|
61
|
+
**Methodology**: I tried to present the agents with problems and kept my own implementation ideas to myself until I saw what they came up with. I've learned that the "Aha!" moment often comes from the agent's alternative path.
|
|
62
|
+
|
|
63
|
+
## Key Views
|
|
64
|
+
|
|
65
|
+
### Network & Timeline
|
|
66
|
+
The graph engine is built on **D3.js**, using a custom force-directed layout (`forceSimulation`, `forceLink`, `forceManyBody`, `forceCenter`, and collision detection). You can switch between the network view and a chronological timeline with one click.
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
### Path-seeking
|
|
71
|
+
Trace connections across history. Here is a path from **John von Neumann** to **Geoffrey Hinton**:
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
### Browse People
|
|
76
|
+
For inspiration, I scored the 5,000 "top" biographies from Simple Wikipedia based on log article length and link density. It's a fascinating, if occasionally quirky, cross-section of world history.
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
## Technical Architecture
|
|
81
|
+
|
|
82
|
+
- **Live Queries**: Uses **Gemini** models to identify connections on the fly (configurable via `VITE_GEMINI_MODEL`).
|
|
83
|
+
- **Multi-source context + metadata**: Uses **Wikipedia/Wikidata** plus **academic corpora/metadata APIs** (currently OpenAlex, with Crossref/DOI metadata as a fallback) when helpful.
|
|
84
|
+
- **Image Logic**: Currently queries **Wikipedia Commons** for images rather than the LLM (which the agents claimed would be too error-prone, though the current way has its own quirks!).
|
|
85
|
+
- **Persistence**: Saved graphs and LLM responses are cached in a **PostgreSQL (Supabase)** database to reduce tokens and speed up recurring paths.
|
|
86
|
+
- **Frontend**: React 19 + Tailwind CSS.
|
|
87
|
+
|
|
88
|
+
## Getting Started
|
|
89
|
+
|
|
90
|
+
1. **Clone & Install**:
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/johndimm/Constellations.git
|
|
93
|
+
cd Constellations
|
|
94
|
+
npm install
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
2. **Setup Env**:
|
|
98
|
+
Create a `.env` file with your Gemini API key and Supabase URL.
|
|
99
|
+
```env
|
|
100
|
+
VITE_GEMINI_API_KEY=your_key_here
|
|
101
|
+
# Optional:
|
|
102
|
+
VITE_GEMINI_MODEL=gemini-2.5-flash
|
|
103
|
+
VITE_GEMINI_MODEL_CLASSIFY=gemini-2.5-pro
|
|
104
|
+
DATABASE_URL=your_postgres_url
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
3. **Run**:
|
|
108
|
+
```bash
|
|
109
|
+
npm run start:cache # Starts the backend (cache/db)
|
|
110
|
+
npm run dev # Starts the frontend
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
*Built with ❤️ and a small army of AI agents.*
|
|
116
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface AppConfirmDialogProps {
|
|
4
|
+
confirmDialog: {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
} | null;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AppConfirmDialog: React.FC<AppConfirmDialogProps> = ({ confirmDialog, onClose }) => {
|
|
13
|
+
if (!confirmDialog || !confirmDialog.isOpen) return null;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="fixed inset-0 z-[100] flex items-end justify-center pb-20 sm:items-center sm:pb-0 px-4">
|
|
17
|
+
<div
|
|
18
|
+
className="absolute inset-0 bg-slate-950/40 backdrop-blur-sm animate-fade-in"
|
|
19
|
+
onClick={onClose}
|
|
20
|
+
></div>
|
|
21
|
+
<div className="bg-slate-900 text-white px-6 py-5 rounded-2xl border border-slate-700 shadow-2xl max-w-sm w-full relative animate-scale-in">
|
|
22
|
+
<h3 className="text-lg font-bold mb-2">Confirm Action</h3>
|
|
23
|
+
<p className="text-sm text-slate-300 mb-6">{confirmDialog.message}</p>
|
|
24
|
+
<div className="flex justify-end gap-3 text-sm">
|
|
25
|
+
<button
|
|
26
|
+
onClick={onClose}
|
|
27
|
+
className="px-4 py-2 rounded-xl text-slate-300 hover:bg-slate-800 transition-colors font-medium"
|
|
28
|
+
>
|
|
29
|
+
Cancel
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => {
|
|
33
|
+
confirmDialog.onConfirm();
|
|
34
|
+
onClose();
|
|
35
|
+
}}
|
|
36
|
+
className="px-6 py-2 rounded-xl bg-red-600 hover:bg-red-500 text-white transition-colors font-bold shadow-lg shadow-red-900/20"
|
|
37
|
+
>
|
|
38
|
+
Confirm
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default AppConfirmDialog;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight, ChevronLeft, Key } from 'lucide-react';
|
|
3
|
+
import { GraphNode } from '../types';
|
|
4
|
+
|
|
5
|
+
interface AppHeaderProps {
|
|
6
|
+
showHeader: boolean;
|
|
7
|
+
panelCollapsed: boolean;
|
|
8
|
+
setPanelCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
+
showBrowse: boolean;
|
|
10
|
+
handleOpenPeopleBrowser: () => void;
|
|
11
|
+
selectedNode: GraphNode | null;
|
|
12
|
+
sidebarCollapsed: boolean;
|
|
13
|
+
setSidebarCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
14
|
+
setSidebarToggleSignal: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
+
onReset: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AppHeader: React.FC<AppHeaderProps> = ({
|
|
19
|
+
showHeader,
|
|
20
|
+
panelCollapsed,
|
|
21
|
+
setPanelCollapsed,
|
|
22
|
+
showBrowse,
|
|
23
|
+
handleOpenPeopleBrowser,
|
|
24
|
+
selectedNode,
|
|
25
|
+
sidebarCollapsed,
|
|
26
|
+
setSidebarCollapsed,
|
|
27
|
+
setSidebarToggleSignal,
|
|
28
|
+
onReset
|
|
29
|
+
}) => {
|
|
30
|
+
if (!showHeader) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<header className="fixed top-0 left-0 right-0 z-50 min-h-14 bg-slate-900/95 backdrop-blur border-b border-slate-800 flex items-center justify-between px-2 sm:px-3 py-2 gap-2 overflow-x-hidden max-w-full">
|
|
34
|
+
<div className="flex items-center gap-1.5 sm:gap-2 min-w-0">
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => setPanelCollapsed(c => !c)}
|
|
37
|
+
className="w-9 h-9 sm:w-10 sm:h-10 bg-slate-800/80 border border-slate-700 rounded-lg flex items-center justify-center text-slate-300 hover:text-white transition flex-shrink-0"
|
|
38
|
+
title={panelCollapsed ? "Show controls" : "Hide controls"}
|
|
39
|
+
>
|
|
40
|
+
{panelCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
onClick={(e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
window.location.href = window.location.origin + window.location.pathname;
|
|
46
|
+
}}
|
|
47
|
+
className="text-base sm:text-lg font-bold text-red-500 whitespace-nowrap hover:text-red-400 transition-colors"
|
|
48
|
+
>
|
|
49
|
+
Constellations
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex items-center gap-3 sm:gap-4 flex-shrink-0 mr-2">
|
|
53
|
+
<button
|
|
54
|
+
onClick={handleOpenPeopleBrowser}
|
|
55
|
+
className={`text-sm font-bold uppercase tracking-widest transition-colors ${showBrowse ? 'text-red-500' : 'text-slate-400 hover:text-white'}`}
|
|
56
|
+
>
|
|
57
|
+
People
|
|
58
|
+
</button>
|
|
59
|
+
{selectedNode && (
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => { setSidebarCollapsed(c => !c); setSidebarToggleSignal(s => s + 1); }}
|
|
62
|
+
className="w-9 h-9 sm:w-10 sm:h-10 bg-slate-800/80 border border-slate-700 rounded-lg flex items-center justify-center text-slate-300 hover:text-white transition flex-shrink-0"
|
|
63
|
+
title="Toggle details"
|
|
64
|
+
>
|
|
65
|
+
{sidebarCollapsed ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
|
|
66
|
+
</button>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</header>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default AppHeader;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface AppNotificationsProps {
|
|
4
|
+
notification: {
|
|
5
|
+
message: string;
|
|
6
|
+
type: 'success' | 'error';
|
|
7
|
+
} | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const AppNotifications: React.FC<AppNotificationsProps> = ({ notification }) => {
|
|
11
|
+
if (!notification) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white px-6 py-3 rounded-lg shadow-2xl border border-slate-700 z-50 flex items-center animate-fade-in-up">
|
|
15
|
+
<div className={`w-3 h-3 rounded-full mr-3 ${notification.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
|
16
|
+
<span className="font-medium">{notification.message}</span>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default AppNotifications;
|