@johndimm/constellations 1.0.0 → 1.0.2
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 +352 -70
- package/FullPageConstellations.tsx +7 -5
- package/components/AppConfirmDialog.tsx +1 -0
- package/components/AppHeader.tsx +69 -29
- package/components/AppNotifications.tsx +1 -0
- package/components/BrowsePeople.tsx +3 -0
- package/components/ControlPanel.tsx +46 -371
- package/components/Graph.tsx +251 -87
- package/components/HelpOverlay.tsx +1 -0
- 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/embedded.css +38 -0
- package/hooks/useExpansion.ts +61 -229
- 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 +57 -19
- package/host.ts +1 -1
- package/index.css +17 -3
- package/package.json +4 -1
- package/services/aiService.ts +23 -0
- package/services/aiUtils.ts +216 -207
- package/services/cacheService.ts +1 -0
- package/services/crossrefService.ts +1 -0
- package/services/deepseekService.ts +467 -0
- package/services/geminiService.ts +532 -733
- 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 +56 -46
- package/types.ts +3 -0
- package/utils/evidenceUtils.ts +1 -0
- package/utils/graphLogicUtils.ts +1 -0
- package/utils/wikiUtils.ts +14 -2
package/components/AppHeader.tsx
CHANGED
|
@@ -1,70 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import { ChevronRight, ChevronLeft,
|
|
3
|
+
import { ChevronRight, ChevronLeft, X } from 'lucide-react';
|
|
3
4
|
import { GraphNode } from '../types';
|
|
4
5
|
|
|
5
6
|
interface AppHeaderProps {
|
|
6
7
|
showHeader: boolean;
|
|
7
8
|
panelCollapsed: boolean;
|
|
8
9
|
setPanelCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
-
showBrowse: boolean;
|
|
10
|
-
handleOpenPeopleBrowser: () => void;
|
|
11
10
|
selectedNode: GraphNode | null;
|
|
12
11
|
sidebarCollapsed: boolean;
|
|
13
|
-
setSidebarCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
14
12
|
setSidebarToggleSignal: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
-
|
|
13
|
+
/** When set, shows a top-right control that leaves full-screen (e.g. back to player). */
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* When set (e.g. `/` or `/player`), the close control is a real `href` link so navigation works
|
|
17
|
+
* even if pointer-event layering blocked the old button. `onClick` can still run for cleanup.
|
|
18
|
+
*/
|
|
19
|
+
closeHref?: string;
|
|
20
|
+
/**
|
|
21
|
+
* When the host app shows its own top bar (e.g. Trailer Vision nav, ~44px), set so this header
|
|
22
|
+
* does not sit at viewport top:0 and steal clicks from the host nav. Use `top-11` for 2.75rem.
|
|
23
|
+
*/
|
|
24
|
+
offsetTopClass?: string;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
const AppHeader: React.FC<AppHeaderProps> = ({
|
|
19
28
|
showHeader,
|
|
20
29
|
panelCollapsed,
|
|
21
30
|
setPanelCollapsed,
|
|
22
|
-
showBrowse,
|
|
23
|
-
handleOpenPeopleBrowser,
|
|
24
31
|
selectedNode,
|
|
25
32
|
sidebarCollapsed,
|
|
26
|
-
setSidebarCollapsed,
|
|
27
33
|
setSidebarToggleSignal,
|
|
28
|
-
|
|
34
|
+
onClose,
|
|
35
|
+
closeHref,
|
|
36
|
+
offsetTopClass = "top-0",
|
|
29
37
|
}) => {
|
|
30
38
|
if (!showHeader) return null;
|
|
31
39
|
|
|
40
|
+
const showLeave = closeHref || onClose;
|
|
41
|
+
|
|
32
42
|
return (
|
|
33
|
-
<header
|
|
34
|
-
|
|
43
|
+
<header
|
|
44
|
+
className={`absolute left-0 right-0 z-[200] h-14 max-h-14 min-h-14 shrink-0 pointer-events-auto bg-slate-900/95 backdrop-blur flex items-center justify-between px-2 sm:px-3 py-2 gap-2 overflow-x-hidden max-w-full ${offsetTopClass}`}
|
|
45
|
+
>
|
|
46
|
+
<div className="pointer-events-auto flex min-w-0 items-center gap-1.5 sm:gap-2">
|
|
35
47
|
<button
|
|
48
|
+
type="button"
|
|
36
49
|
onClick={() => setPanelCollapsed(c => !c)}
|
|
37
50
|
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={
|
|
51
|
+
title={
|
|
52
|
+
panelCollapsed
|
|
53
|
+
? "Show left panel — search, save/load, graph options"
|
|
54
|
+
: "Hide left panel"
|
|
55
|
+
}
|
|
56
|
+
aria-label={panelCollapsed ? "Show control panel" : "Hide control panel"}
|
|
39
57
|
>
|
|
40
58
|
{panelCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
|
41
59
|
</button>
|
|
42
|
-
<
|
|
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
|
-
>
|
|
60
|
+
<span className="text-base sm:text-lg font-bold text-red-500 whitespace-nowrap">
|
|
49
61
|
Constellations
|
|
50
|
-
</
|
|
62
|
+
</span>
|
|
51
63
|
</div>
|
|
52
|
-
<div className="flex items-center gap-
|
|
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>
|
|
64
|
+
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0 mr-1">
|
|
59
65
|
{selectedNode && (
|
|
60
66
|
<button
|
|
61
|
-
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => {
|
|
69
|
+
setSidebarToggleSignal((s) => s + 1);
|
|
70
|
+
}}
|
|
62
71
|
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=
|
|
72
|
+
title={
|
|
73
|
+
sidebarCollapsed
|
|
74
|
+
? "Show right details (selected node on graph)"
|
|
75
|
+
: "Hide right details"
|
|
76
|
+
}
|
|
77
|
+
aria-label={sidebarCollapsed ? "Show details panel" : "Hide details panel"}
|
|
64
78
|
>
|
|
65
79
|
{sidebarCollapsed ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
|
|
66
80
|
</button>
|
|
67
81
|
)}
|
|
82
|
+
{showLeave && closeHref && (
|
|
83
|
+
<a
|
|
84
|
+
href={closeHref}
|
|
85
|
+
onClick={onClose}
|
|
86
|
+
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 hover:border-slate-600 transition flex-shrink-0"
|
|
87
|
+
title="Return to the main app"
|
|
88
|
+
aria-label="Return to the main app"
|
|
89
|
+
>
|
|
90
|
+
<X size={20} strokeWidth={2} />
|
|
91
|
+
</a>
|
|
92
|
+
)}
|
|
93
|
+
{showLeave && !closeHref && onClose && (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={(e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
onClose();
|
|
100
|
+
}}
|
|
101
|
+
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 hover:border-slate-600 transition flex-shrink-0"
|
|
102
|
+
title="Leave full screen and return to Trailer Vision"
|
|
103
|
+
aria-label="Close constellations"
|
|
104
|
+
>
|
|
105
|
+
<X size={20} strokeWidth={2} />
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
68
108
|
</div>
|
|
69
109
|
</header>
|
|
70
110
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
3
|
import { Search, ChevronLeft, ChevronRight, X, Filter } from 'lucide-react';
|
|
3
4
|
|
|
@@ -124,6 +125,8 @@ const BrowsePeople: React.FC<BrowsePeopleProps> = ({ baseUrl = '', onSelect, exp
|
|
|
124
125
|
'South African', 'New Zealander', 'Israeli', 'Saudi Arabian', 'Korean', 'Thai', 'Vietnamese', 'Indonesian'
|
|
125
126
|
];
|
|
126
127
|
|
|
128
|
+
const isPureBrowse = useCallback(() => !searchTerm.trim() && !occupation && !nationality, [searchTerm, occupation, nationality]);
|
|
129
|
+
|
|
127
130
|
const buildSearchQuery = useCallback(() => {
|
|
128
131
|
const parts: string[] = [];
|
|
129
132
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Search,
|
|
3
|
-
import type { LlmProviderId } from '../services/aiUtils';
|
|
4
|
-
import { getBrowserLlmOverride, setBrowserLlmOverride, getEnvCacheUrl } from '../services/aiUtils';
|
|
3
|
+
import { Search, Minimize2, Maximize2, Calendar, Network, X, Link as LinkIcon, ArrowRight, Type, ChevronLeft, ChevronRight, ChevronDown, Settings } from 'lucide-react';
|
|
5
4
|
import { DEFAULT_KIOSK_DOMAINS, saveKioskDomains, saveSelectedKioskDomainId } from '../kioskDomains';
|
|
6
5
|
import type { KioskDomain } from '../kioskDomains';
|
|
7
6
|
|
|
@@ -23,9 +22,6 @@ interface ControlPanelProps {
|
|
|
23
22
|
selectedKioskDomainId?: string;
|
|
24
23
|
onSelectKioskDomain?: (domainId: string) => void;
|
|
25
24
|
onUpdateKioskDomains?: (domains: KioskDomain[]) => void;
|
|
26
|
-
onClear: () => void;
|
|
27
|
-
onClearCache?: () => void;
|
|
28
|
-
onExpandAllLeafNodes?: () => void;
|
|
29
25
|
isProcessing: boolean;
|
|
30
26
|
isCompact: boolean;
|
|
31
27
|
onToggleCompact: () => void;
|
|
@@ -33,20 +29,23 @@ interface ControlPanelProps {
|
|
|
33
29
|
onToggleTimeline: () => void;
|
|
34
30
|
isTextOnly: boolean;
|
|
35
31
|
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
32
|
isCollapsed: boolean;
|
|
46
33
|
onSetCollapsed: (val: boolean) => void;
|
|
47
34
|
onOpenPeopleBrowser?: () => void;
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
/** When set, shows a link to the graph settings page at the top of the panel. */
|
|
36
|
+
settingsHref?: string;
|
|
37
|
+
/** Fixed top offset (viewport). Default `top-14` = below constellations header. Use `top-[6.25rem]` when a host app nav (~44px) sits above. */
|
|
38
|
+
offsetTopClass?: string;
|
|
39
|
+
/**
|
|
40
|
+
* When true (e.g. embedded with `hideHeader`), the rail is `top-2 bottom-2` and inner heights
|
|
41
|
+
* use the graph column instead of `100vh` / `60vh` so the panel does not “fall” to the viewport.
|
|
42
|
+
*/
|
|
43
|
+
constrainToParentHeight?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* When true, use `position: fixed` and viewport-based width so the rail matches full-screen
|
|
46
|
+
* overlay hosts (same family as a `fixed` details sidebar). Ignored for normal in-layout absolute rails.
|
|
47
|
+
*/
|
|
48
|
+
pinToViewport?: boolean;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
@@ -67,9 +66,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
67
66
|
selectedKioskDomainId,
|
|
68
67
|
onSelectKioskDomain,
|
|
69
68
|
onUpdateKioskDomains,
|
|
70
|
-
onClear,
|
|
71
|
-
onClearCache,
|
|
72
|
-
onExpandAllLeafNodes,
|
|
73
69
|
isProcessing,
|
|
74
70
|
isCompact,
|
|
75
71
|
onToggleCompact,
|
|
@@ -77,20 +73,13 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
77
73
|
onToggleTimeline,
|
|
78
74
|
isTextOnly,
|
|
79
75
|
onToggleTextOnly,
|
|
80
|
-
onPrune,
|
|
81
|
-
error,
|
|
82
|
-
onSave,
|
|
83
|
-
onLoad,
|
|
84
|
-
onDeleteGraph,
|
|
85
|
-
onImport,
|
|
86
|
-
savedGraphs,
|
|
87
|
-
helpHover,
|
|
88
|
-
onHelpHoverChange,
|
|
89
76
|
isCollapsed,
|
|
90
77
|
onSetCollapsed,
|
|
91
78
|
onOpenPeopleBrowser,
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
settingsHref,
|
|
80
|
+
offsetTopClass = "top-14",
|
|
81
|
+
constrainToParentHeight = false,
|
|
82
|
+
pinToViewport = false,
|
|
94
83
|
}) => {
|
|
95
84
|
const [hasStarted, setHasStarted] = useState(false);
|
|
96
85
|
const [isHovered, setIsHovered] = useState(false);
|
|
@@ -103,16 +92,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
103
92
|
// Collapsible sections state - combined toggle for examples section
|
|
104
93
|
const [showExamples, setShowExamples] = useState(false);
|
|
105
94
|
|
|
106
|
-
const [llmSelectValue, setLlmSelectValue] = useState<"env" | LlmProviderId>(() =>
|
|
107
|
-
getBrowserLlmOverride() ?? "env"
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Save/Load/Share State
|
|
111
|
-
const [showSave, setShowSave] = useState(false);
|
|
112
|
-
const [showLoad, setShowLoad] = useState(false);
|
|
113
|
-
const [showShare, setShowShare] = useState(false);
|
|
114
|
-
const [saveName, setSaveName] = useState('');
|
|
115
|
-
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
116
95
|
const domainsImportRef = React.useRef<HTMLInputElement>(null);
|
|
117
96
|
|
|
118
97
|
const handleSubmit = (e: React.FormEvent) => {
|
|
@@ -132,59 +111,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
132
111
|
}
|
|
133
112
|
};
|
|
134
113
|
|
|
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
114
|
const EXAMPLES = [
|
|
189
115
|
"The Godfather",
|
|
190
116
|
"Watergate Scandal",
|
|
@@ -219,12 +145,24 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
219
145
|
<>
|
|
220
146
|
{headerActions}
|
|
221
147
|
<div
|
|
222
|
-
className={
|
|
223
|
-
style={
|
|
148
|
+
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}`}
|
|
149
|
+
style={
|
|
150
|
+
pinToViewport
|
|
151
|
+
? { width: "min(28rem, calc(100vw - 1.5rem))", maxWidth: "28rem" }
|
|
152
|
+
: { width: "calc(100% - 1.5rem)", maxWidth: "28rem" }
|
|
153
|
+
}
|
|
224
154
|
>
|
|
225
|
-
<div
|
|
155
|
+
<div
|
|
156
|
+
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
|
|
157
|
+
? "h-full min-h-0 max-h-full"
|
|
158
|
+
: "max-h-[calc(100vh-64px)]"}`}
|
|
159
|
+
>
|
|
226
160
|
{/* Scrollable area for everything above the Start Here list if it gets too tall (e.g. Help open) */}
|
|
227
|
-
<div
|
|
161
|
+
<div
|
|
162
|
+
className={`overflow-y-auto overflow-x-hidden custom-scrollbar ${constrainToParentHeight
|
|
163
|
+
? "min-h-0 max-h-[min(14rem,45%)] flex-shrink-0"
|
|
164
|
+
: "flex-shrink-0 max-h-[60vh]"}`}
|
|
165
|
+
>
|
|
228
166
|
{/* Persistent Toggle Handle */}
|
|
229
167
|
<button
|
|
230
168
|
onClick={() => onSetCollapsed?.(!isCollapsed)}
|
|
@@ -235,64 +173,18 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
235
173
|
<div className="[writing-mode:vertical-lr] text-[9px] uppercase tracking-tighter mt-1 font-bold">Controls</div>
|
|
236
174
|
</button>
|
|
237
175
|
|
|
238
|
-
{/*
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
</
|
|
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>
|
|
293
|
-
</div>
|
|
176
|
+
{/* Settings link */}
|
|
177
|
+
{settingsHref && (
|
|
178
|
+
<div className="mb-3">
|
|
179
|
+
<a href={settingsHref} className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors">
|
|
180
|
+
<Settings size={12} />
|
|
181
|
+
Settings
|
|
182
|
+
</a>
|
|
294
183
|
</div>
|
|
184
|
+
)}
|
|
295
185
|
|
|
186
|
+
{/* Button Groups */}
|
|
187
|
+
<div className="space-y-4 mb-4">
|
|
296
188
|
{/* Group: View */}
|
|
297
189
|
<div>
|
|
298
190
|
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
|
|
@@ -328,223 +220,8 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
328
220
|
</button>
|
|
329
221
|
</div>
|
|
330
222
|
</div>
|
|
331
|
-
|
|
332
|
-
{/* Group: LLM — browser override; with cache proxy, sent as llmProvider on each request */}
|
|
333
|
-
<div>
|
|
334
|
-
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
|
|
335
|
-
<Cpu size={10} /> LLM
|
|
336
|
-
</div>
|
|
337
|
-
<select
|
|
338
|
-
value={llmSelectValue}
|
|
339
|
-
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
|
-
}
|
|
348
|
-
}}
|
|
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
|
-
title="Model provider for AI calls from this tab"
|
|
351
|
-
>
|
|
352
|
-
<option value="env">Default (from .env)</option>
|
|
353
|
-
<option value="gemini">Google Gemini</option>
|
|
354
|
-
<option value="openai">OpenAI</option>
|
|
355
|
-
<option value="deepseek">DeepSeek</option>
|
|
356
|
-
<option value="anthropic">Anthropic</option>
|
|
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'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>
|
|
374
|
-
</div>
|
|
375
|
-
|
|
376
|
-
{/* 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>
|
|
418
|
-
</div>
|
|
419
|
-
</div>
|
|
420
223
|
</div>
|
|
421
224
|
|
|
422
|
-
{/* Help Dialog moved to shared App-level HelpOverlay */}
|
|
423
|
-
|
|
424
|
-
{/* Share Dialog */}
|
|
425
|
-
{showShare && (
|
|
426
|
-
<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
|
-
<div className="flex justify-between items-center mb-3">
|
|
428
|
-
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
|
429
|
-
<Share2 size={14} /> Share Graph
|
|
430
|
-
</h3>
|
|
431
|
-
<button onClick={() => setShowShare(false)}><X size={14} className="text-slate-400" /></button>
|
|
432
|
-
</div>
|
|
433
|
-
<div className="grid grid-cols-3 gap-2">
|
|
434
|
-
<button
|
|
435
|
-
onClick={() => onSave('__COPY_LINK__')}
|
|
436
|
-
className="flex flex-col items-center justify-center gap-2 bg-slate-700 hover:bg-slate-600 text-white p-3 rounded-lg transition-colors border border-slate-600"
|
|
437
|
-
>
|
|
438
|
-
<LinkIcon size={20} className="text-orange-400" />
|
|
439
|
-
<span className="text-[10px] font-bold uppercase tracking-wider text-center">Copy Link</span>
|
|
440
|
-
</button>
|
|
441
|
-
<button
|
|
442
|
-
onClick={() => onSave('__COPY__')}
|
|
443
|
-
className="flex flex-col items-center justify-center gap-2 bg-slate-700 hover:bg-slate-600 text-white p-3 rounded-lg transition-colors border border-slate-600"
|
|
444
|
-
>
|
|
445
|
-
<Copy size={20} className="text-purple-400" />
|
|
446
|
-
<span className="text-[10px] font-bold uppercase tracking-wider text-center">Copy JSON</span>
|
|
447
|
-
</button>
|
|
448
|
-
<button
|
|
449
|
-
onClick={() => onSave('__EXPORT__')}
|
|
450
|
-
className="flex flex-col items-center justify-center gap-2 bg-slate-700 hover:bg-slate-600 text-white p-3 rounded-lg transition-colors border border-slate-600"
|
|
451
|
-
>
|
|
452
|
-
<Download size={20} className="text-indigo-400" />
|
|
453
|
-
<span className="text-[10px] font-bold uppercase tracking-wider text-center">Download File</span>
|
|
454
|
-
</button>
|
|
455
|
-
</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
|
-
</div>
|
|
460
|
-
)}
|
|
461
|
-
|
|
462
|
-
{/* Save Dialog */}
|
|
463
|
-
{showSave && (
|
|
464
|
-
<div className="mb-4 bg-slate-800 p-3 rounded-lg border border-slate-600">
|
|
465
|
-
<div className="flex justify-between items-center mb-2">
|
|
466
|
-
<h3 className="text-sm font-bold text-white">Save Graph</h3>
|
|
467
|
-
<button onClick={() => setShowSave(false)}><X size={14} className="text-slate-400" /></button>
|
|
468
|
-
</div>
|
|
469
|
-
<form onSubmit={handleSaveSubmit} className="flex gap-2">
|
|
470
|
-
<input
|
|
471
|
-
type="text"
|
|
472
|
-
value={saveName}
|
|
473
|
-
onChange={(e) => setSaveName(e.target.value)}
|
|
474
|
-
placeholder="Graph Name..."
|
|
475
|
-
className="flex-1 bg-slate-900 border border-slate-700 text-white px-2 py-1 rounded text-sm focus:outline-none focus:border-indigo-500"
|
|
476
|
-
autoFocus
|
|
477
|
-
/>
|
|
478
|
-
<button type="submit" className="bg-green-600 hover:bg-green-500 text-white px-3 py-1 rounded text-sm font-medium">
|
|
479
|
-
Save
|
|
480
|
-
</button>
|
|
481
|
-
{/* Export Button (Downloads current as JSON) */}
|
|
482
|
-
<button
|
|
483
|
-
type="button"
|
|
484
|
-
onClick={() => onSave('__EXPORT__')} // Special signal to export
|
|
485
|
-
className="bg-indigo-600 hover:bg-indigo-500 text-white px-2 py-1 rounded text-sm font-medium flex items-center"
|
|
486
|
-
title="Export as JSON"
|
|
487
|
-
>
|
|
488
|
-
<Download size={14} />
|
|
489
|
-
</button>
|
|
490
|
-
</form>
|
|
491
|
-
</div>
|
|
492
|
-
)}
|
|
493
|
-
|
|
494
|
-
{/* Load Dialog */}
|
|
495
|
-
{showLoad && (
|
|
496
|
-
<div className="mb-4 bg-slate-800 p-3 rounded-lg border border-slate-600 max-h-60 overflow-y-auto">
|
|
497
|
-
<div className="flex justify-between items-center mb-2">
|
|
498
|
-
<h3 className="text-sm font-bold text-white">Load Graph</h3>
|
|
499
|
-
<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>
|
|
507
|
-
<button onClick={() => setShowLoad(false)}><X size={14} className="text-slate-400" /></button>
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
510
|
-
<input
|
|
511
|
-
type="file"
|
|
512
|
-
ref={fileInputRef}
|
|
513
|
-
onChange={handleImportFile}
|
|
514
|
-
accept=".json"
|
|
515
|
-
className="hidden"
|
|
516
|
-
/>
|
|
517
|
-
|
|
518
|
-
{savedGraphs.length === 0 ? (
|
|
519
|
-
<p className="text-slate-400 text-xs italic">No saved graphs.</p>
|
|
520
|
-
) : (
|
|
521
|
-
<div className="space-y-1">
|
|
522
|
-
{savedGraphs.map(name => (
|
|
523
|
-
<div key={name} className="flex justify-between items-center bg-slate-900 p-2 rounded hover:bg-slate-700 group transition-colors">
|
|
524
|
-
<button
|
|
525
|
-
onClick={() => { onLoad(name); setShowLoad(false); }}
|
|
526
|
-
className="text-white text-sm text-left flex-1"
|
|
527
|
-
>
|
|
528
|
-
{name}
|
|
529
|
-
</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>
|
|
541
|
-
</div>
|
|
542
|
-
))}
|
|
543
|
-
</div>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
)}
|
|
547
|
-
|
|
548
225
|
</div>
|
|
549
226
|
|
|
550
227
|
<div onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
@@ -673,8 +350,6 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
|
|
|
673
350
|
</div>
|
|
674
351
|
</form>
|
|
675
352
|
|
|
676
|
-
{error && <p className="text-red-400 text-xs mb-3">{error}</p>}
|
|
677
|
-
|
|
678
353
|
<div className="space-y-2 flex-1 min-h-0 flex flex-col">
|
|
679
354
|
{showExamples && (
|
|
680
355
|
<div className="flex flex-wrap gap-1.5 overflow-y-auto overflow-x-hidden pr-1 flex-1 min-h-0">
|