@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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { GraphNode, GraphLink } from '../types';
|
|
3
|
+
import { X, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { buildWikiUrl } from '../utils/wikiUtils';
|
|
5
|
+
|
|
6
|
+
interface SidebarProps {
|
|
7
|
+
selectedNode: GraphNode | null;
|
|
8
|
+
selectedLink?: GraphLink | null;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onCollapseChange?: (collapsed: boolean) => void;
|
|
11
|
+
externalToggleSignal?: number;
|
|
12
|
+
isAdminMode?: boolean;
|
|
13
|
+
forceExpanded?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose, onCollapseChange, externalToggleSignal, isAdminMode, forceExpanded }) => {
|
|
17
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
18
|
+
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
|
19
|
+
const [showFullSummary, setShowFullSummary] = useState(false);
|
|
20
|
+
const userManuallyCollapsedRef = useRef(false);
|
|
21
|
+
const lastToggleSignalRef = useRef<number | undefined>(undefined);
|
|
22
|
+
|
|
23
|
+
const isRedundant = (s1?: string, s2?: string) => {
|
|
24
|
+
if (!s1 || !s2) return false;
|
|
25
|
+
const clean = (s: string) => s.toLowerCase().replace(/[^\w\s]/g, '').trim();
|
|
26
|
+
const c1 = clean(s1);
|
|
27
|
+
const c2 = clean(s2);
|
|
28
|
+
if (c1 === c2) return true;
|
|
29
|
+
if (c1.length > 10 && c2.includes(c1)) return true;
|
|
30
|
+
if (c2.length > 10 && c1.includes(c2)) return true;
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
|
36
|
+
window.addEventListener('resize', handleResize);
|
|
37
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (onCollapseChange) {
|
|
42
|
+
onCollapseChange(isCollapsed);
|
|
43
|
+
}
|
|
44
|
+
}, [isCollapsed, onCollapseChange]);
|
|
45
|
+
|
|
46
|
+
// Auto-expand logic: Only auto-expand on desktop if user hasn't manually collapsed it
|
|
47
|
+
// On mobile, keep it collapsed so it doesn't block the graph.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!selectedNode && !selectedLink) return;
|
|
50
|
+
if (forceExpanded) {
|
|
51
|
+
setIsCollapsed(false);
|
|
52
|
+
setShowFullSummary(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isMobile && !userManuallyCollapsedRef.current) {
|
|
56
|
+
setIsCollapsed(false);
|
|
57
|
+
} else {
|
|
58
|
+
setIsCollapsed(true);
|
|
59
|
+
}
|
|
60
|
+
setShowFullSummary(false);
|
|
61
|
+
}, [selectedNode, selectedLink, isMobile, forceExpanded]);
|
|
62
|
+
|
|
63
|
+
// External toggle (from header button)
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (externalToggleSignal === undefined) return;
|
|
66
|
+
if (lastToggleSignalRef.current === undefined) {
|
|
67
|
+
lastToggleSignalRef.current = externalToggleSignal;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (externalToggleSignal !== lastToggleSignalRef.current) {
|
|
71
|
+
lastToggleSignalRef.current = externalToggleSignal;
|
|
72
|
+
handleToggleCollapse();
|
|
73
|
+
}
|
|
74
|
+
}, [externalToggleSignal]);
|
|
75
|
+
|
|
76
|
+
const handleToggleCollapse = () => {
|
|
77
|
+
const newCollapsed = !isCollapsed;
|
|
78
|
+
setIsCollapsed(newCollapsed);
|
|
79
|
+
// Track that user manually collapsed it
|
|
80
|
+
userManuallyCollapsedRef.current = newCollapsed;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!selectedNode && !selectedLink) return null;
|
|
84
|
+
|
|
85
|
+
const nonPersonTypes = ['Movie', 'Event', 'Battle', 'Project', 'Company', 'Organization', 'Album', 'Song', 'Book', 'War', 'Treaty', 'Administration'];
|
|
86
|
+
const isPerson = selectedNode ? (selectedNode.is_atomic === true || selectedNode.is_person === true || (selectedNode.type.toLowerCase() === 'person' || selectedNode.type.toLowerCase() === 'actor')) : false;
|
|
87
|
+
|
|
88
|
+
// Unified side panel styling - slides right on both mobile and desktop
|
|
89
|
+
// Side panel styling - always slides right.
|
|
90
|
+
// When collapsed, we translate most of it away but leave 24px (1.5rem-ish) for the handle.
|
|
91
|
+
const effectiveMobile = forceExpanded ? false : isMobile;
|
|
92
|
+
const panelWidth = effectiveMobile ? 'calc(100vw - 1.5rem)' : '26rem';
|
|
93
|
+
const panelClasses = `fixed top-16 right-0 z-50 transition-transform duration-300 ease-in-out ${isCollapsed ? 'translate-x-[calc(100%-24px)]' : 'translate-x-0'}`;
|
|
94
|
+
const panelStyle = { width: panelWidth, maxWidth: '28rem', paddingRight: effectiveMobile ? '0.75rem' : '1rem' };
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<div className={panelClasses} style={panelStyle}>
|
|
99
|
+
<div className="bg-slate-900/95 backdrop-blur-xl rounded-xl border border-slate-700 shadow-2xl relative pointer-events-auto flex flex-col p-6 h-[calc(100vh-6rem)] overflow-visible">
|
|
100
|
+
{/* Persistent Toggle Handle */}
|
|
101
|
+
<button
|
|
102
|
+
onClick={handleToggleCollapse}
|
|
103
|
+
className={`absolute top-1/2 -translate-y-1/2 -left-8 w-8 h-24 bg-slate-800 border border-slate-700 border-r-0 rounded-l-xl flex flex-col items-center justify-center text-slate-400 hover:text-white transition-all group shadow-xl ${isCollapsed ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
|
104
|
+
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
105
|
+
>
|
|
106
|
+
{isCollapsed ? <ChevronLeft size={16} /> : <ChevronRight size={16} />}
|
|
107
|
+
<div className="[writing-mode:vertical-lr] text-[9px] uppercase tracking-tighter mt-1 font-bold">Details</div>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<div className="flex-1 overflow-visible">
|
|
111
|
+
<div className="flex justify-between items-start mb-4">
|
|
112
|
+
<h2 className="text-xl font-bold text-white leading-tight">
|
|
113
|
+
{selectedNode ? selectedNode.title : "Connection Details"}
|
|
114
|
+
</h2>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="space-y-4 overflow-y-auto pr-1">
|
|
118
|
+
{/* Selected Edge Evidence (when user clicks an edge) */}
|
|
119
|
+
{selectedLink && (
|
|
120
|
+
<div className="p-3 bg-slate-800/40 rounded-lg border border-slate-600/40">
|
|
121
|
+
<div className="flex items-center justify-between mb-1">
|
|
122
|
+
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-300">
|
|
123
|
+
Edge Selected
|
|
124
|
+
</span>
|
|
125
|
+
{selectedLink.evidence?.url && (
|
|
126
|
+
<a
|
|
127
|
+
href={selectedLink.evidence.url}
|
|
128
|
+
target="_blank"
|
|
129
|
+
rel="noopener noreferrer"
|
|
130
|
+
className="text-xs text-amber-300 hover:text-amber-200"
|
|
131
|
+
>
|
|
132
|
+
View Source
|
|
133
|
+
</a>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
{selectedLink.label && (
|
|
137
|
+
<div className="text-xs font-semibold text-slate-200 mb-2">
|
|
138
|
+
{selectedLink.label}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
{selectedLink.evidence?.pageTitle && (
|
|
142
|
+
<div className="text-xs font-semibold text-slate-200 mb-2">
|
|
143
|
+
From: {selectedLink.evidence.pageTitle}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
{selectedLink.evidence?.snippet && selectedLink.evidence.kind !== 'none' ? (
|
|
147
|
+
<p className="text-[11px] text-slate-300 leading-relaxed whitespace-pre-wrap">
|
|
148
|
+
“{selectedLink.evidence.snippet}”
|
|
149
|
+
</p>
|
|
150
|
+
) : (
|
|
151
|
+
<p className="text-[11px] text-slate-400 leading-relaxed">
|
|
152
|
+
No evidence snippet available for this edge yet.
|
|
153
|
+
</p>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* AI Classification Info (Admin only) */}
|
|
159
|
+
{isAdminMode && selectedNode && (selectedNode.atomic_type || selectedNode.composite_type) && (
|
|
160
|
+
<div className="p-3 bg-blue-900/20 rounded-lg border border-blue-500/20">
|
|
161
|
+
<div className="flex items-center gap-2 mb-1">
|
|
162
|
+
<span className="text-[10px] font-bold uppercase tracking-widest text-blue-400 px-1.5 py-0.5 bg-blue-500/10 rounded">
|
|
163
|
+
AI Classification
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="text-xs font-semibold text-blue-200 mb-2">
|
|
167
|
+
{selectedNode.atomic_type} ↔ {selectedNode.composite_type}
|
|
168
|
+
</div>
|
|
169
|
+
{selectedNode.classification_reasoning && (
|
|
170
|
+
<p className="text-[11px] text-blue-300 italic leading-relaxed">
|
|
171
|
+
"{selectedNode.classification_reasoning}"
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* Display type for events only (not for persons) */}
|
|
178
|
+
{selectedNode && !isPerson && selectedNode.type && (
|
|
179
|
+
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
|
180
|
+
<div>
|
|
181
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">Type</span>
|
|
182
|
+
<p className="text-blue-400 font-medium">{selectedNode.type}</p>
|
|
183
|
+
</div>
|
|
184
|
+
{selectedNode.year && selectedNode.year !== 0 && (
|
|
185
|
+
<div>
|
|
186
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">Date</span>
|
|
187
|
+
<p className="text-amber-400 font-medium">{selectedNode.year}</p>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{selectedNode && isPerson && selectedNode.year && selectedNode.year !== 0 && (
|
|
194
|
+
<div>
|
|
195
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">Active Around</span>
|
|
196
|
+
<p className="text-amber-400 font-medium">{selectedNode.year}</p>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{selectedNode && selectedNode.description && !isRedundant(selectedNode.description, selectedNode.wikiSummary) && (
|
|
201
|
+
<div>
|
|
202
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">Description</span>
|
|
203
|
+
<p className="text-slate-300 text-sm leading-relaxed mt-1 whitespace-pre-wrap">{selectedNode.description}</p>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{selectedNode && selectedNode.wikiSummary && (
|
|
208
|
+
<div>
|
|
209
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">Wikipedia Summary</span>
|
|
210
|
+
<p className="text-slate-200 text-sm leading-relaxed mt-1 whitespace-pre-wrap">
|
|
211
|
+
{showFullSummary || (selectedNode.wikiSummary || '').length <= 600
|
|
212
|
+
? selectedNode.wikiSummary
|
|
213
|
+
: `${(selectedNode.wikiSummary || '').slice(0, 600)}…`}
|
|
214
|
+
</p>
|
|
215
|
+
{selectedNode.wikiSummary && selectedNode.wikiSummary.length > 600 && (
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => setShowFullSummary(!showFullSummary)}
|
|
218
|
+
className="mt-1 text-xs text-amber-300 hover:text-amber-200"
|
|
219
|
+
>
|
|
220
|
+
{showFullSummary ? 'Show less' : 'Show more'}
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
{/* Action Buttons */}
|
|
228
|
+
{selectedNode && (
|
|
229
|
+
<div className="pt-4 border-t border-slate-800 flex flex-col gap-2">
|
|
230
|
+
{(selectedNode as any)?.meta?.openAlexUrl && (
|
|
231
|
+
<a
|
|
232
|
+
href={(selectedNode as any).meta.openAlexUrl}
|
|
233
|
+
target="_blank"
|
|
234
|
+
rel="noopener noreferrer"
|
|
235
|
+
className="flex items-center justify-center gap-2 w-full bg-slate-800 hover:bg-slate-700 text-slate-300 py-2.5 rounded-lg font-medium transition-colors text-sm"
|
|
236
|
+
>
|
|
237
|
+
<ExternalLink size={16} />
|
|
238
|
+
<span>View on OpenAlex</span>
|
|
239
|
+
</a>
|
|
240
|
+
)}
|
|
241
|
+
{(selectedNode as any)?.meta?.doi && (
|
|
242
|
+
<a
|
|
243
|
+
href={`https://doi.org/${String((selectedNode as any).meta.doi).replace(/^https?:\/\/doi\.org\//i, '')}`}
|
|
244
|
+
target="_blank"
|
|
245
|
+
rel="noopener noreferrer"
|
|
246
|
+
className="flex items-center justify-center gap-2 w-full bg-slate-800 hover:bg-slate-700 text-slate-300 py-2.5 rounded-lg font-medium transition-colors text-sm"
|
|
247
|
+
>
|
|
248
|
+
<ExternalLink size={16} />
|
|
249
|
+
<span>View DOI</span>
|
|
250
|
+
</a>
|
|
251
|
+
)}
|
|
252
|
+
<a
|
|
253
|
+
href={buildWikiUrl(selectedNode.title, selectedNode.wikipedia_id)}
|
|
254
|
+
target="_blank"
|
|
255
|
+
rel="noopener noreferrer"
|
|
256
|
+
className="flex items-center justify-center gap-2 w-full bg-slate-800 hover:bg-slate-700 text-slate-300 py-2.5 rounded-lg font-medium transition-colors text-sm"
|
|
257
|
+
>
|
|
258
|
+
<ExternalLink size={16} />
|
|
259
|
+
<span>Read on Wikipedia</span>
|
|
260
|
+
</a>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default Sidebar;
|