@johndimm/constellations 1.0.1 → 1.0.3

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.
Files changed (40) hide show
  1. package/App.tsx +360 -66
  2. package/FullPageConstellations.tsx +7 -4
  3. package/components/AppConfirmDialog.tsx +1 -0
  4. package/components/AppHeader.tsx +67 -30
  5. package/components/AppNotifications.tsx +1 -0
  6. package/components/BrowsePeople.tsx +3 -0
  7. package/components/ControlPanel.tsx +229 -250
  8. package/components/Graph.tsx +251 -87
  9. package/components/HelpOverlay.tsx +2 -1
  10. package/components/NodeContextMenu.tsx +123 -3
  11. package/components/PeopleBrowserSidebar.tsx +15 -6
  12. package/components/Sidebar.tsx +46 -19
  13. package/components/TimelineView.tsx +1 -0
  14. package/hooks/useExpansion.ts +85 -230
  15. package/hooks/useGraphActions.ts +1 -0
  16. package/hooks/useGraphState.ts +75 -40
  17. package/hooks/useKioskMode.ts +1 -0
  18. package/hooks/useNodeClickHandler.ts +23 -15
  19. package/hooks/useSearchHandlers.ts +60 -21
  20. package/host.ts +1 -1
  21. package/index.css +17 -3
  22. package/index.tsx +5 -3
  23. package/package.json +4 -2
  24. package/services/aiService.ts +27 -0
  25. package/services/aiUtils.ts +285 -195
  26. package/services/cacheService.ts +1 -0
  27. package/services/crossrefService.ts +1 -0
  28. package/services/deepseekService.ts +479 -0
  29. package/services/geminiService.ts +543 -736
  30. package/services/graphUtils.ts +128 -18
  31. package/services/imageService.ts +18 -0
  32. package/services/openAlexService.ts +1 -0
  33. package/services/resolveImageForTitle.ts +458 -0
  34. package/services/wikipediaImage.ts +1 -0
  35. package/services/wikipediaService.ts +79 -49
  36. package/sessionHandoff.ts +26 -0
  37. package/types.ts +3 -0
  38. package/utils/evidenceUtils.ts +1 -0
  39. package/utils/graphLogicUtils.ts +1 -0
  40. package/utils/wikiUtils.ts +14 -2
@@ -1,5 +1,6 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Search, Github, HelpCircle, Minimize2, Maximize2, Maximize, Plus, AlertCircle, Scissors, Calendar, Network, X, Link as LinkIcon, ArrowRight, Type, Trash2, ChevronLeft, ChevronRight, ChevronDown, Download, Upload, Share2, Copy, Users, Cpu } from 'lucide-react';
1
+ "use client";
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import { Search, Minimize2, Maximize2, Maximize, Calendar, Network, X, Link as LinkIcon, ArrowRight, Type, ChevronLeft, ChevronRight, ChevronDown, Settings, Download, Trash2, Share2, Copy, Upload, Plus, HelpCircle, Cpu } from 'lucide-react';
3
4
  import type { LlmProviderId } from '../services/aiUtils';
4
5
  import { getBrowserLlmOverride, setBrowserLlmOverride, getEnvCacheUrl } from '../services/aiUtils';
5
6
  import { DEFAULT_KIOSK_DOMAINS, saveKioskDomains, saveSelectedKioskDomainId } from '../kioskDomains';
@@ -23,9 +24,18 @@ interface ControlPanelProps {
23
24
  selectedKioskDomainId?: string;
24
25
  onSelectKioskDomain?: (domainId: string) => void;
25
26
  onUpdateKioskDomains?: (domains: KioskDomain[]) => void;
26
- onClear: () => void;
27
+ onClear?: () => void;
27
28
  onClearCache?: () => void;
28
29
  onExpandAllLeafNodes?: () => void;
30
+ onSave?: (name: string) => void;
31
+ onLoad?: (name: string) => void;
32
+ onDeleteGraph?: (name: string) => void;
33
+ onImport?: (data: any) => void;
34
+ savedGraphs?: string[];
35
+ helpHover?: string | null;
36
+ onHelpHoverChange?: (value: string | null) => void;
37
+ onToggleHelp?: () => void;
38
+ showHelp?: boolean;
29
39
  isProcessing: boolean;
30
40
  isCompact: boolean;
31
41
  onToggleCompact: () => void;
@@ -33,20 +43,23 @@ interface ControlPanelProps {
33
43
  onToggleTimeline: () => void;
34
44
  isTextOnly: boolean;
35
45
  onToggleTextOnly: () => void;
36
- onPrune?: () => void;
37
- error?: string | null;
38
- onSave: (name: string) => void;
39
- onLoad: (name: string) => void;
40
- onDeleteGraph: (name: string) => void;
41
- onImport: (data: any) => void; // New prop for importing
42
- savedGraphs: string[];
43
- helpHover: string | null;
44
- onHelpHoverChange: (value: string | null) => void;
45
46
  isCollapsed: boolean;
46
47
  onSetCollapsed: (val: boolean) => void;
47
48
  onOpenPeopleBrowser?: () => void;
48
- onToggleHelp: () => void;
49
- showHelp?: boolean;
49
+ /** When set, shows a link to the graph settings page at the top of the panel. */
50
+ settingsHref?: string;
51
+ /** Fixed top offset (viewport). Default `top-14` = below constellations header. Use `top-[6.25rem]` when a host app nav (~44px) sits above. */
52
+ offsetTopClass?: string;
53
+ /**
54
+ * When true (e.g. embedded with `hideHeader`), the rail is `top-2 bottom-2` and inner heights
55
+ * use the graph column instead of `100vh` / `60vh` so the panel does not “fall” to the viewport.
56
+ */
57
+ constrainToParentHeight?: boolean;
58
+ /**
59
+ * When true, use `position: fixed` and viewport-based width so the rail matches full-screen
60
+ * overlay hosts (same family as a `fixed` details sidebar). Ignored for normal in-layout absolute rails.
61
+ */
62
+ pinToViewport?: boolean;
50
63
  }
51
64
 
52
65
  const ControlPanel: React.FC<ControlPanelProps> = ({
@@ -70,6 +83,15 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
70
83
  onClear,
71
84
  onClearCache,
72
85
  onExpandAllLeafNodes,
86
+ onSave,
87
+ onLoad,
88
+ onDeleteGraph,
89
+ onImport,
90
+ savedGraphs = [],
91
+ helpHover,
92
+ onHelpHoverChange,
93
+ onToggleHelp,
94
+ showHelp,
73
95
  isProcessing,
74
96
  isCompact,
75
97
  onToggleCompact,
@@ -77,20 +99,13 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
77
99
  onToggleTimeline,
78
100
  isTextOnly,
79
101
  onToggleTextOnly,
80
- onPrune,
81
- error,
82
- onSave,
83
- onLoad,
84
- onDeleteGraph,
85
- onImport,
86
- savedGraphs,
87
- helpHover,
88
- onHelpHoverChange,
89
102
  isCollapsed,
90
103
  onSetCollapsed,
91
104
  onOpenPeopleBrowser,
92
- onToggleHelp,
93
- showHelp = false
105
+ settingsHref,
106
+ offsetTopClass = "top-14",
107
+ constrainToParentHeight = false,
108
+ pinToViewport = false,
94
109
  }) => {
95
110
  const [hasStarted, setHasStarted] = useState(false);
96
111
  const [isHovered, setIsHovered] = useState(false);
@@ -99,22 +114,42 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
99
114
  const [newDomainLabel, setNewDomainLabel] = useState('');
100
115
  const [newTerm, setNewTerm] = useState('');
101
116
  const [bulkTerms, setBulkTerms] = useState('');
102
-
103
- // Collapsible sections state - combined toggle for examples section
104
- const [showExamples, setShowExamples] = useState(false);
105
-
106
- const [llmSelectValue, setLlmSelectValue] = useState<"env" | LlmProviderId>(() =>
107
- getBrowserLlmOverride() ?? "env"
108
- );
109
-
110
- // Save/Load/Share State
111
117
  const [showSave, setShowSave] = useState(false);
112
118
  const [showLoad, setShowLoad] = useState(false);
113
119
  const [showShare, setShowShare] = useState(false);
114
120
  const [saveName, setSaveName] = useState('');
115
- const fileInputRef = React.useRef<HTMLInputElement>(null);
121
+ const fileInputRef = useRef<HTMLInputElement>(null);
122
+ const [llmSelectValue, setLlmSelectValue] = useState<'env' | LlmProviderId>(() => getBrowserLlmOverride() ?? 'env');
123
+
124
+ // Collapsible sections state - combined toggle for examples section
125
+ const [showExamples, setShowExamples] = useState(false);
126
+
116
127
  const domainsImportRef = React.useRef<HTMLInputElement>(null);
117
128
 
129
+ const handleSaveSubmit = (e: React.FormEvent) => {
130
+ e.preventDefault();
131
+ if (saveName.trim() && onSave) {
132
+ onSave(saveName.trim());
133
+ setShowSave(false);
134
+ setSaveName('');
135
+ }
136
+ };
137
+
138
+ const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
139
+ const file = e.target.files?.[0];
140
+ if (!file || !onImport) return;
141
+ const reader = new FileReader();
142
+ reader.onload = (ev) => {
143
+ try {
144
+ const data = JSON.parse(ev.target?.result as string);
145
+ onImport(data);
146
+ setShowLoad(false);
147
+ } catch { alert('Invalid JSON file'); }
148
+ finally { e.target.value = ''; }
149
+ };
150
+ reader.readAsText(file);
151
+ };
152
+
118
153
  const handleSubmit = (e: React.FormEvent) => {
119
154
  e.preventDefault();
120
155
  if (searchMode === 'explore') {
@@ -132,59 +167,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
132
167
  }
133
168
  };
134
169
 
135
- const handleSaveSubmit = (e: React.FormEvent) => {
136
- e.preventDefault();
137
- if (saveName.trim()) {
138
- onSave(saveName.trim());
139
- setSaveName('');
140
- setShowSave(false);
141
- }
142
- };
143
-
144
- const handleExport = () => {
145
- // We need the current graph data. Ideally passed down, but we can grab from what we know or ask parent.
146
- // Actually, onSave usually saves *current* state.
147
- // To export, we probably need access to the current `nodes` and `links` or a way to get them.
148
- // BUT we don't have them in props here.
149
- // Solution: Let the PARENT handle the export triggered by a callback, OR pass the data down.
150
- // Adding `onExport` prop is safer.
151
- // Wait, the prompt says "Export as JSON and send it".
152
- // I can modify `onSave` to optionally accept an "export" flag? Or just add `onExport` prop.
153
- // Let's add `onExportRequest` prop to `ControlPanel` and implement it in `App`.
154
-
155
- // Changing approach slightly: I will add `onExport` to props in the NEXT step (App.tsx updates),
156
- // but for now I will structure this file to expect it.
157
- // Actually I can keep local logic if I pass the data down? No, passing all nodes/links to ControlPanel causes rerenders.
158
- // Best: `onExport` callback.
159
- };
160
-
161
- // Re-thinking export: User clicks "Export", App.tsx gathers data and downloads it.
162
- // So I need an `onExport` prop. I will add it to the interface above in a sec (or assume it exists and fix App later).
163
- // Actually, I can fix the interface now.
164
-
165
- const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
166
- const file = e.target.files?.[0];
167
- if (!file) return;
168
-
169
- const reader = new FileReader();
170
- reader.onload = (event) => {
171
- try {
172
- const json = JSON.parse(event.target?.result as string);
173
- // Basic validation
174
- if (json.nodes && json.links) {
175
- onImport(json);
176
- setShowLoad(false);
177
- } else {
178
- alert("Invalid graph JSON");
179
- }
180
- } catch (err) {
181
- console.error(err);
182
- alert("Failed to parse JSON");
183
- }
184
- };
185
- reader.readAsText(file);
186
- };
187
-
188
170
  const EXAMPLES = [
189
171
  "The Godfather",
190
172
  "Watergate Scandal",
@@ -219,12 +201,24 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
219
201
  <>
220
202
  {headerActions}
221
203
  <div
222
- className={`absolute left-0 z-40 flex flex-col gap-2 transition-transform duration-300 ease-in-out pointer-events-none ${isCollapsed ? '-translate-x-[calc(100%-24px)]' : 'translate-x-[12px] sm:translate-x-[16px]'} top-16`}
223
- style={{ width: 'calc(100% - 1.5rem)', maxWidth: '28rem' }}
204
+ className={`${pinToViewport ? "fixed" : "absolute"} left-0 z-50 flex flex-col gap-2 transition-transform duration-300 ease-in-out pointer-events-none ${isCollapsed ? "-translate-x-[calc(100%-24px)]" : "translate-x-[12px] sm:translate-x-[16px]"} ${offsetTopClass}`}
205
+ style={
206
+ pinToViewport
207
+ ? { width: "min(28rem, calc(100vw - 1.5rem))", maxWidth: "28rem" }
208
+ : { width: "calc(100% - 1.5rem)", maxWidth: "28rem" }
209
+ }
224
210
  >
225
- <div className="bg-slate-900/95 backdrop-blur-xl p-4 rounded-xl border border-slate-700 shadow-2xl pointer-events-auto relative overflow-hidden flex flex-col max-h-[calc(100vh-64px)]">
211
+ <div
212
+ className={`bg-slate-900/95 backdrop-blur-xl p-4 rounded-xl border border-slate-700 shadow-2xl pointer-events-auto relative overflow-hidden flex flex-col ${constrainToParentHeight
213
+ ? "h-full min-h-0 max-h-full"
214
+ : "max-h-[calc(100vh-64px)]"}`}
215
+ >
226
216
  {/* Scrollable area for everything above the Start Here list if it gets too tall (e.g. Help open) */}
227
- <div className="overflow-y-auto overflow-x-hidden flex-shrink-0 custom-scrollbar max-h-[60vh]">
217
+ <div
218
+ className={`overflow-y-auto overflow-x-hidden custom-scrollbar ${constrainToParentHeight
219
+ ? "min-h-0 max-h-[min(14rem,45%)] flex-shrink-0"
220
+ : "flex-shrink-0 max-h-[60vh]"}`}
221
+ >
228
222
  {/* Persistent Toggle Handle */}
229
223
  <button
230
224
  onClick={() => onSetCollapsed?.(!isCollapsed)}
@@ -235,63 +229,74 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
235
229
  <div className="[writing-mode:vertical-lr] text-[9px] uppercase tracking-tighter mt-1 font-bold">Controls</div>
236
230
  </button>
237
231
 
232
+ {/* Settings link */}
233
+ {settingsHref && (
234
+ <div className="mb-3">
235
+ <a href={settingsHref} className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors">
236
+ <Settings size={12} />
237
+ Settings
238
+ </a>
239
+ </div>
240
+ )}
241
+
242
+ {/* Dev docs links */}
243
+ <div className="mb-3 flex gap-3">
244
+ <a href="/doc/help.html" target="_blank" rel="noreferrer"
245
+ className="text-[10px] text-slate-500 hover:text-slate-300 transition-colors">
246
+ Help
247
+ </a>
248
+ <a href="/doc/prompt.html" target="_blank" rel="noreferrer"
249
+ className="text-[10px] text-slate-500 hover:text-slate-300 transition-colors">
250
+ Prompt
251
+ </a>
252
+ <a href="/doc/journal.html" target="_blank" rel="noreferrer"
253
+ className="text-[10px] text-slate-500 hover:text-slate-300 transition-colors">
254
+ Dev Journal
255
+ </a>
256
+ </div>
257
+
238
258
  {/* Button Groups */}
239
259
  <div className="space-y-4 mb-4">
240
260
  {/* Group: File */}
241
- <div>
242
- <div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
243
- <Download size={10} /> File
244
- </div>
245
- <div className="flex flex-wrap gap-2 text-xs">
246
- <button
247
- onClick={() => {
248
- let defaultName = "";
249
- if (searchMode === 'explore' && exploreTerm) {
250
- defaultName = exploreTerm;
251
- } else if (searchMode === 'connect' && pathStart && pathEnd) {
252
- defaultName = `${pathStart} to ${pathEnd}`;
253
- } else {
254
- defaultName = `Graph ${new Date().toLocaleTimeString()}`;
255
- }
256
- setSaveName(defaultName);
257
- setShowSave(!showSave);
258
- setShowLoad(false);
259
- setShowShare(false);
260
- if (showHelp) onToggleHelp();
261
- onHelpHoverChange(null);
262
- }}
263
- className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'save' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
264
- title="Save Graph"
265
- >
266
- SAVE
267
- </button>
268
- <button
269
- onClick={() => {
270
- setShowLoad(!showLoad);
271
- setShowSave(false);
272
- setShowShare(false);
273
- if (showHelp) onToggleHelp();
274
- }}
275
- className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'load' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
276
- title="Load Graph"
277
- >
278
- LOAD
279
- </button>
280
- <button
281
- onClick={() => {
282
- setShowShare(!showShare);
283
- setShowSave(false);
284
- setShowLoad(false);
285
- if (showHelp) onToggleHelp();
286
- onHelpHoverChange(null);
287
- }}
288
- className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'share' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
289
- title="Share Graph"
290
- >
291
- SHARE
292
- </button>
261
+ {onSave && (
262
+ <div>
263
+ <div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
264
+ <Download size={10} /> File
265
+ </div>
266
+ <div className="flex flex-wrap gap-2 text-xs">
267
+ <button
268
+ onClick={() => {
269
+ let defaultName = searchMode === 'explore' && exploreTerm ? exploreTerm
270
+ : searchMode === 'connect' && pathStart && pathEnd ? `${pathStart} to ${pathEnd}`
271
+ : `Graph ${new Date().toLocaleTimeString()}`;
272
+ setSaveName(defaultName);
273
+ setShowSave(!showSave);
274
+ setShowLoad(false);
275
+ setShowShare(false);
276
+ onHelpHoverChange?.(null);
277
+ }}
278
+ className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'save' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
279
+ title="Save Graph"
280
+ >
281
+ SAVE
282
+ </button>
283
+ <button
284
+ onClick={() => { setShowLoad(!showLoad); setShowSave(false); setShowShare(false); }}
285
+ className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'load' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
286
+ title="Load Graph"
287
+ >
288
+ LOAD
289
+ </button>
290
+ <button
291
+ onClick={() => { setShowShare(!showShare); setShowSave(false); setShowLoad(false); onHelpHoverChange?.(null); }}
292
+ className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-amber-300 transition-colors ${helpHover === 'share' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
293
+ title="Share Graph"
294
+ >
295
+ SHARE
296
+ </button>
297
+ </div>
293
298
  </div>
294
- </div>
299
+ )}
295
300
 
296
301
  {/* Group: View */}
297
302
  <div>
@@ -329,7 +334,7 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
329
334
  </div>
330
335
  </div>
331
336
 
332
- {/* Group: LLM — browser override; with cache proxy, sent as llmProvider on each request */}
337
+ {/* Group: LLM */}
333
338
  <div>
334
339
  <div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
335
340
  <Cpu size={10} /> LLM
@@ -337,14 +342,9 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
337
342
  <select
338
343
  value={llmSelectValue}
339
344
  onChange={(e) => {
340
- const v = e.target.value as "env" | LlmProviderId;
341
- if (v === "env") {
342
- setBrowserLlmOverride(null);
343
- setLlmSelectValue("env");
344
- } else {
345
- setBrowserLlmOverride(v);
346
- setLlmSelectValue(v);
347
- }
345
+ const v = e.target.value as 'env' | LlmProviderId;
346
+ setBrowserLlmOverride(v === 'env' ? null : v);
347
+ setLlmSelectValue(v);
348
348
  }}
349
349
  className="w-full bg-slate-900 border border-slate-600 text-slate-100 text-xs rounded-md px-2 py-2 focus:outline-none focus:ring-1 focus:ring-indigo-500"
350
350
  title="Model provider for AI calls from this tab"
@@ -355,74 +355,66 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
355
355
  <option value="deepseek">DeepSeek</option>
356
356
  <option value="anthropic">Anthropic</option>
357
357
  </select>
358
- <p className="text-[9px] text-slate-500 mt-1 leading-snug">
359
- {getEnvCacheUrl() ? (
360
- <>
361
- Proxied requests include <span className="text-slate-400">llmProvider</span> when you pick a provider
362
- (Default uses the cache server&apos;s <span className="text-slate-400">LLM_PROVIDER</span>). Keys live on
363
- the server (e.g. Render).
364
- </>
365
- ) : (
366
- <>
367
- Overrides <span className="text-slate-400">VITE_LLM_PROVIDER</span> for this tab. Keys:{" "}
368
- <span className="text-slate-400">VITE_GEMINI_API_KEY</span>,{" "}
369
- <span className="text-slate-400">VITE_OPENAI_API_KEY</span>, etc. in{" "}
370
- <span className="text-slate-400">.env.local</span>.
371
- </>
372
- )}
373
- </p>
358
+ {getEnvCacheUrl() && (
359
+ <p className="text-[9px] text-slate-500 mt-1 leading-snug">
360
+ Proxied — keys live on the server. Default uses{' '}
361
+ <span className="text-slate-400">LLM_PROVIDER</span> env var.
362
+ </p>
363
+ )}
374
364
  </div>
375
365
 
376
366
  {/* Group: Actions */}
377
- <div>
378
- <div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
379
- <Plus size={10} /> Actions
380
- </div>
381
- <div className="flex flex-wrap gap-2 text-xs">
382
- <button
383
- onClick={onClear}
384
- className="text-slate-300 hover:text-red-300 p-1.5 rounded-md border border-slate-700 bg-slate-800/80 transition-colors"
385
- title="Clear graph"
386
- >
387
- <Trash2 size={16} />
388
- </button>
389
- {onClearCache && (
390
- <button
391
- onClick={onClearCache}
392
- className="px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-orange-300 transition-colors text-xs"
393
- title="Clear API cache (forces fresh data from LLM)"
394
- >
395
- CLEAR CACHE
396
- </button>
397
- )}
398
- {onExpandAllLeafNodes && (
399
- <button
400
- onClick={onExpandAllLeafNodes}
401
- disabled={isProcessing}
402
- className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-emerald-300 inline-flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed transition-colors`}
403
- title="Expand everything reachable from the current graph frontier"
404
- >
405
- <Maximize size={14} className="text-emerald-400" />
406
- EXPAND ALL
407
- </button>
408
- )}
409
- <button
410
- onClick={() => {
411
- onToggleHelp();
412
- }}
413
- className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-white flex items-center gap-1 transition-colors ${helpHover === 'help' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
414
- title="Help & Info"
415
- >
416
- <HelpCircle size={14} /> HELP
417
- </button>
367
+ {(onClear || onClearCache || onExpandAllLeafNodes || onToggleHelp) && (
368
+ <div>
369
+ <div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
370
+ <Plus size={10} /> Actions
371
+ </div>
372
+ <div className="flex flex-wrap gap-2 text-xs">
373
+ {onClear && (
374
+ <button
375
+ onClick={onClear}
376
+ className="text-slate-300 hover:text-red-300 p-1.5 rounded-md border border-slate-700 bg-slate-800/80 transition-colors"
377
+ title="Clear graph"
378
+ >
379
+ <Trash2 size={16} />
380
+ </button>
381
+ )}
382
+ {onClearCache && (
383
+ <button
384
+ onClick={onClearCache}
385
+ className="px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-orange-300 transition-colors"
386
+ title="Clear API cache (forces fresh data from LLM)"
387
+ >
388
+ CLEAR CACHE
389
+ </button>
390
+ )}
391
+ {onExpandAllLeafNodes && (
392
+ <button
393
+ onClick={onExpandAllLeafNodes}
394
+ disabled={isProcessing}
395
+ className="px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-emerald-300 inline-flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
396
+ title="Expand all leaf nodes"
397
+ >
398
+ <Maximize size={14} className="text-emerald-400" />
399
+ EXPAND ALL
400
+ </button>
401
+ )}
402
+ {onToggleHelp && (
403
+ <button
404
+ onClick={onToggleHelp}
405
+ className={`px-3 py-1.5 rounded-md border border-slate-700 bg-slate-800/80 text-slate-200 hover:text-white flex items-center gap-1 transition-colors ${helpHover === 'help' ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-slate-900' : ''}`}
406
+ title="Help & Info"
407
+ >
408
+ <HelpCircle size={14} /> HELP
409
+ </button>
410
+ )}
411
+ </div>
418
412
  </div>
419
- </div>
413
+ )}
420
414
  </div>
421
415
 
422
- {/* Help Dialog moved to shared App-level HelpOverlay */}
423
-
424
416
  {/* Share Dialog */}
425
- {showShare && (
417
+ {showShare && onSave && (
426
418
  <div className="mb-4 bg-slate-800 p-3 rounded-lg border border-slate-600 animate-in fade-in slide-in-from-top-2 duration-200">
427
419
  <div className="flex justify-between items-center mb-3">
428
420
  <h3 className="text-sm font-bold text-white flex items-center gap-2">
@@ -453,14 +445,11 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
453
445
  <span className="text-[10px] font-bold uppercase tracking-wider text-center">Download File</span>
454
446
  </button>
455
447
  </div>
456
- <p className="mt-3 text-[10px] text-slate-400 text-center italic">
457
- Share the JSON data with others to let them view your graph.
458
- </p>
459
448
  </div>
460
449
  )}
461
450
 
462
451
  {/* Save Dialog */}
463
- {showSave && (
452
+ {showSave && onSave && (
464
453
  <div className="mb-4 bg-slate-800 p-3 rounded-lg border border-slate-600">
465
454
  <div className="flex justify-between items-center mb-2">
466
455
  <h3 className="text-sm font-bold text-white">Save Graph</h3>
@@ -478,10 +467,9 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
478
467
  <button type="submit" className="bg-green-600 hover:bg-green-500 text-white px-3 py-1 rounded text-sm font-medium">
479
468
  Save
480
469
  </button>
481
- {/* Export Button (Downloads current as JSON) */}
482
470
  <button
483
471
  type="button"
484
- onClick={() => onSave('__EXPORT__')} // Special signal to export
472
+ onClick={() => onSave('__EXPORT__')}
485
473
  className="bg-indigo-600 hover:bg-indigo-500 text-white px-2 py-1 rounded text-sm font-medium flex items-center"
486
474
  title="Export as JSON"
487
475
  >
@@ -492,29 +480,24 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
492
480
  )}
493
481
 
494
482
  {/* Load Dialog */}
495
- {showLoad && (
483
+ {showLoad && onLoad && (
496
484
  <div className="mb-4 bg-slate-800 p-3 rounded-lg border border-slate-600 max-h-60 overflow-y-auto">
497
485
  <div className="flex justify-between items-center mb-2">
498
486
  <h3 className="text-sm font-bold text-white">Load Graph</h3>
499
487
  <div className="flex items-center gap-2">
500
- <button
501
- onClick={() => fileInputRef.current?.click()}
502
- className="text-slate-400 hover:text-blue-400 flex items-center gap-1 text-xs"
503
- title="Import JSON"
504
- >
505
- <Upload size={14} /> Import
506
- </button>
488
+ {onImport && (
489
+ <button
490
+ onClick={() => fileInputRef.current?.click()}
491
+ className="text-slate-400 hover:text-blue-400 flex items-center gap-1 text-xs"
492
+ title="Import JSON"
493
+ >
494
+ <Upload size={14} /> Import
495
+ </button>
496
+ )}
507
497
  <button onClick={() => setShowLoad(false)}><X size={14} className="text-slate-400" /></button>
508
498
  </div>
509
499
  </div>
510
- <input
511
- type="file"
512
- ref={fileInputRef}
513
- onChange={handleImportFile}
514
- accept=".json"
515
- className="hidden"
516
- />
517
-
500
+ <input type="file" ref={fileInputRef} onChange={handleImportFile} accept=".json" className="hidden" />
518
501
  {savedGraphs.length === 0 ? (
519
502
  <p className="text-slate-400 text-xs italic">No saved graphs.</p>
520
503
  ) : (
@@ -527,17 +510,15 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
527
510
  >
528
511
  {name}
529
512
  </button>
530
- <button
531
- onClick={(e) => {
532
- e.stopPropagation();
533
- onDeleteGraph(name);
534
- setShowLoad(false);
535
- }}
536
- className="text-slate-400 hover:text-red-400 transition-colors p-1.5 rounded-md hover:bg-slate-800"
537
- title="Delete Graph"
538
- >
539
- <Trash2 size={14} />
540
- </button>
513
+ {onDeleteGraph && (
514
+ <button
515
+ onClick={(e) => { e.stopPropagation(); onDeleteGraph(name); setShowLoad(false); }}
516
+ className="text-slate-400 hover:text-red-400 transition-colors p-1.5 rounded-md hover:bg-slate-800"
517
+ title="Delete Graph"
518
+ >
519
+ <Trash2 size={14} />
520
+ </button>
521
+ )}
541
522
  </div>
542
523
  ))}
543
524
  </div>
@@ -673,8 +654,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
673
654
  </div>
674
655
  </form>
675
656
 
676
- {error && <p className="text-red-400 text-xs mb-3">{error}</p>}
677
-
678
657
  <div className="space-y-2 flex-1 min-h-0 flex flex-col">
679
658
  {showExamples && (
680
659
  <div className="flex flex-wrap gap-1.5 overflow-y-auto overflow-x-hidden pr-1 flex-1 min-h-0">