@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.
- package/App.tsx +360 -66
- package/FullPageConstellations.tsx +7 -4
- package/components/AppConfirmDialog.tsx +1 -0
- package/components/AppHeader.tsx +67 -30
- package/components/AppNotifications.tsx +1 -0
- package/components/BrowsePeople.tsx +3 -0
- package/components/ControlPanel.tsx +229 -250
- package/components/Graph.tsx +251 -87
- package/components/HelpOverlay.tsx +2 -1
- package/components/NodeContextMenu.tsx +123 -3
- package/components/PeopleBrowserSidebar.tsx +15 -6
- package/components/Sidebar.tsx +46 -19
- package/components/TimelineView.tsx +1 -0
- package/hooks/useExpansion.ts +85 -230
- package/hooks/useGraphActions.ts +1 -0
- package/hooks/useGraphState.ts +75 -40
- package/hooks/useKioskMode.ts +1 -0
- package/hooks/useNodeClickHandler.ts +23 -15
- package/hooks/useSearchHandlers.ts +60 -21
- package/host.ts +1 -1
- package/index.css +17 -3
- package/index.tsx +5 -3
- package/package.json +4 -2
- package/services/aiService.ts +27 -0
- package/services/aiUtils.ts +285 -195
- package/services/cacheService.ts +1 -0
- package/services/crossrefService.ts +1 -0
- package/services/deepseekService.ts +479 -0
- package/services/geminiService.ts +543 -736
- package/services/graphUtils.ts +128 -18
- package/services/imageService.ts +18 -0
- package/services/openAlexService.ts +1 -0
- package/services/resolveImageForTitle.ts +458 -0
- package/services/wikipediaImage.ts +1 -0
- package/services/wikipediaService.ts +79 -49
- package/sessionHandoff.ts +26 -0
- package/types.ts +3 -0
- package/utils/evidenceUtils.ts +1 -0
- package/utils/graphLogicUtils.ts +1 -0
- package/utils/wikiUtils.ts +14 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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 =
|
|
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={
|
|
223
|
-
style={
|
|
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
|
|
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
|
|
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
|
-
|
|
242
|
-
<div
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
defaultName
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
setShowSave(false);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
378
|
-
<div
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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__')}
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
e.stopPropagation();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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">
|