nitrostack 1.0.71 → 1.0.72
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/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +3 -1
- package/src/studio/app/auth/callback/page.tsx +6 -6
- package/src/studio/app/chat/page.tsx +1099 -408
- package/src/studio/app/chat/page.tsx.backup +1046 -187
- package/src/studio/app/globals.css +361 -191
- package/src/studio/app/health/page.tsx +72 -76
- package/src/studio/app/layout.tsx +9 -11
- package/src/studio/app/logs/page.tsx +29 -30
- package/src/studio/app/page.tsx +134 -230
- package/src/studio/app/prompts/page.tsx +115 -97
- package/src/studio/app/resources/page.tsx +115 -124
- package/src/studio/app/settings/page.tsx +1080 -125
- package/src/studio/app/tools/page.tsx +343 -0
- package/src/studio/components/EnlargeModal.tsx +76 -65
- package/src/studio/components/LogMessage.tsx +5 -5
- package/src/studio/components/MarkdownRenderer.tsx +4 -4
- package/src/studio/components/Sidebar.tsx +150 -210
- package/src/studio/components/SplashScreen.tsx +109 -0
- package/src/studio/components/ToolCard.tsx +50 -41
- package/src/studio/components/VoiceOrbOverlay.tsx +469 -0
- package/src/studio/components/WidgetRenderer.tsx +8 -3
- package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
- package/src/studio/lib/store.ts +15 -0
- package/src/studio/package-lock.json +3303 -0
- package/src/studio/package.json +3 -1
- package/src/studio/public/NitroStudio Isotype Color.png +0 -0
- package/src/studio/tailwind.config.ts +63 -17
- package/src/studio/app/auth/page.tsx +0 -560
- package/src/studio/app/ping/page.tsx +0 -209
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
4
|
+
import ReactFlow, {
|
|
5
|
+
Background,
|
|
6
|
+
Controls,
|
|
7
|
+
Edge,
|
|
8
|
+
Node,
|
|
9
|
+
ReactFlowProvider,
|
|
10
|
+
useNodesState,
|
|
11
|
+
useEdgesState,
|
|
12
|
+
Handle,
|
|
13
|
+
Position,
|
|
14
|
+
MarkerType,
|
|
15
|
+
} from 'reactflow';
|
|
16
|
+
import 'reactflow/dist/style.css';
|
|
17
|
+
import { Tool, Resource, Prompt } from '@/lib/types';
|
|
18
|
+
import { WrenchScrewdriverIcon, BoltIcon, SparklesIcon, CubeIcon, DocumentTextIcon, GlobeAltIcon } from '@heroicons/react/24/outline';
|
|
19
|
+
|
|
20
|
+
interface ToolsCanvasProps {
|
|
21
|
+
tools: Tool[];
|
|
22
|
+
resources?: Resource[];
|
|
23
|
+
prompts?: Prompt[];
|
|
24
|
+
onToolClick?: (tool: Tool) => void;
|
|
25
|
+
onResourceClick?: (resource: Resource) => void;
|
|
26
|
+
onPromptClick?: (prompt: Prompt) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Custom Agent Node (Center)
|
|
30
|
+
const AgentNode = ({ data }: { data: { label: string } }) => {
|
|
31
|
+
return (
|
|
32
|
+
<div className="relative group">
|
|
33
|
+
{/* Glow Effect */}
|
|
34
|
+
<div className="absolute -inset-4 bg-primary/20 rounded-full blur-xl group-hover:bg-primary/30 transition-all duration-500"></div>
|
|
35
|
+
|
|
36
|
+
<div className="relative w-32 h-32 rounded-full border-2 border-primary bg-card/90 backdrop-blur-md shadow-2xl flex flex-col items-center justify-center z-10 animate-pulse-slow">
|
|
37
|
+
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-2">
|
|
38
|
+
<CubeIcon className="w-7 h-7 text-primary" />
|
|
39
|
+
</div>
|
|
40
|
+
<div className="text-[10px] font-mono text-primary uppercase tracking-widest mb-1">AGENT</div>
|
|
41
|
+
<div className="text-xs font-bold text-foreground text-center px-4 leading-tight">{data.label}</div>
|
|
42
|
+
|
|
43
|
+
<Handle type="source" position={Position.Top} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
|
|
44
|
+
<Handle type="source" position={Position.Right} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
|
|
45
|
+
<Handle type="source" position={Position.Bottom} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
|
|
46
|
+
<Handle type="source" position={Position.Left} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Custom Tool Node (Satellite)
|
|
53
|
+
const ToolNode = ({ data }: { data: { tool: Tool } }) => {
|
|
54
|
+
const Icon = data.tool.name.toLowerCase().includes('run') || data.tool.name.toLowerCase().includes('exec')
|
|
55
|
+
? BoltIcon
|
|
56
|
+
: data.tool.description?.toLowerCase().includes('ai')
|
|
57
|
+
? SparklesIcon
|
|
58
|
+
: WrenchScrewdriverIcon;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="w-[200px] rounded-lg border border-teal-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-teal-500/60 hover:shadow-teal-500/10 group cursor-pointer">
|
|
62
|
+
<Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
|
|
63
|
+
|
|
64
|
+
<div className="flex items-start gap-3">
|
|
65
|
+
<div className="w-8 h-8 rounded-lg bg-teal-500/10 flex items-center justify-center shrink-0 group-hover:bg-teal-500/20 transition-colors">
|
|
66
|
+
<Icon className="w-4 h-4 text-teal-500 group-hover:text-teal-400 transition-colors" />
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<div className="text-[10px] font-mono text-teal-500 uppercase tracking-wider mb-0.5">TOOL</div>
|
|
70
|
+
<div className="font-semibold text-foreground text-sm line-clamp-1">{data.tool.name}</div>
|
|
71
|
+
<div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.tool.description}</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Argument Pills */}
|
|
76
|
+
{data.tool.inputSchema?.properties && Object.keys(data.tool.inputSchema.properties).length > 0 && (
|
|
77
|
+
<div className="flex flex-wrap gap-1 mt-3">
|
|
78
|
+
{Object.keys(data.tool.inputSchema.properties).slice(0, 3).map(arg => (
|
|
79
|
+
<span key={arg} className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">
|
|
80
|
+
{arg}
|
|
81
|
+
</span>
|
|
82
|
+
))}
|
|
83
|
+
{Object.keys(data.tool.inputSchema.properties).length > 3 && (
|
|
84
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">+{Object.keys(data.tool.inputSchema.properties).length - 3}</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Custom Resource Node
|
|
93
|
+
const ResourceNode = ({ data }: { data: { resource: Resource } }) => {
|
|
94
|
+
return (
|
|
95
|
+
<div className="w-[200px] rounded-lg border border-indigo-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-indigo-500/60 hover:shadow-indigo-500/10 group cursor-pointer">
|
|
96
|
+
<Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
|
|
97
|
+
|
|
98
|
+
<div className="flex items-start gap-3">
|
|
99
|
+
<div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center shrink-0 group-hover:bg-indigo-500/20 transition-colors">
|
|
100
|
+
<CubeIcon className="w-4 h-4 text-indigo-500 group-hover:text-indigo-400 transition-colors" />
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<div className="text-[10px] font-mono text-indigo-500 uppercase tracking-wider mb-0.5">RESOURCE</div>
|
|
104
|
+
<div className="font-semibold text-foreground text-sm line-clamp-1">{data.resource.name}</div>
|
|
105
|
+
<div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.resource.uri}</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="flex flex-wrap gap-1 mt-3">
|
|
109
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] bg-indigo-500/5 text-indigo-500 border border-indigo-500/10">
|
|
110
|
+
{data.resource.mimeType || 'text/plain'}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Custom Prompt Node
|
|
118
|
+
const PromptNode = ({ data }: { data: { prompt: Prompt } }) => {
|
|
119
|
+
return (
|
|
120
|
+
<div className="w-[200px] rounded-lg border border-amber-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-amber-500/60 hover:shadow-amber-500/10 group cursor-pointer">
|
|
121
|
+
<Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
|
|
122
|
+
|
|
123
|
+
<div className="flex items-start gap-3">
|
|
124
|
+
<div className="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center shrink-0 group-hover:bg-amber-500/20 transition-colors">
|
|
125
|
+
<DocumentTextIcon className="w-4 h-4 text-amber-500 group-hover:text-amber-400 transition-colors" />
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<div className="text-[10px] font-mono text-amber-500 uppercase tracking-wider mb-0.5">PROMPT</div>
|
|
129
|
+
<div className="font-semibold text-foreground text-sm line-clamp-1">{data.prompt.name}</div>
|
|
130
|
+
<div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.prompt.description || 'No description'}</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Arguments */}
|
|
135
|
+
{data.prompt.arguments && data.prompt.arguments.length > 0 && (
|
|
136
|
+
<div className="flex flex-wrap gap-1 mt-3">
|
|
137
|
+
{data.prompt.arguments.slice(0, 3).map(arg => (
|
|
138
|
+
<span key={arg.name} className="px-1.5 py-0.5 rounded text-[10px] bg-amber-500/5 text-amber-500 border border-amber-500/10">
|
|
139
|
+
{arg.name}
|
|
140
|
+
</span>
|
|
141
|
+
))}
|
|
142
|
+
{data.prompt.arguments.length > 3 && (
|
|
143
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">+{data.prompt.arguments.length - 3}</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const nodeTypes = {
|
|
152
|
+
agent: AgentNode,
|
|
153
|
+
tool: ToolNode,
|
|
154
|
+
resource: ResourceNode,
|
|
155
|
+
prompt: PromptNode,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export function ToolsCanvas({ tools, resources = [], prompts = [], onToolClick, onResourceClick, onPromptClick }: ToolsCanvasProps) {
|
|
159
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
160
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
161
|
+
|
|
162
|
+
// Calculate Layout (Categorical Zones)
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const centerX = 400;
|
|
165
|
+
const centerY = 300;
|
|
166
|
+
const zoneRadius = 350; // Distance of zone centers from main center
|
|
167
|
+
|
|
168
|
+
const newNodes: Node[] = [
|
|
169
|
+
{
|
|
170
|
+
id: 'agent-root',
|
|
171
|
+
type: 'agent',
|
|
172
|
+
position: { x: centerX - 64, y: centerY - 64 }, // Center minus half width/height
|
|
173
|
+
data: { label: 'NitroStack Agent' },
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const newEdges: Edge[] = [];
|
|
178
|
+
|
|
179
|
+
// Helper to place items in a grid within a zone
|
|
180
|
+
const placeItemsInZone = (items: any[], type: string, startX: number, startY: number, cols: number = 3) => {
|
|
181
|
+
items.forEach((item, index) => {
|
|
182
|
+
const col = index % cols;
|
|
183
|
+
const row = Math.floor(index / cols);
|
|
184
|
+
|
|
185
|
+
// Grid spacing
|
|
186
|
+
const gapX = 240;
|
|
187
|
+
const gapY = 150;
|
|
188
|
+
|
|
189
|
+
const x = startX + (col * gapX);
|
|
190
|
+
const y = startY + (row * gapY);
|
|
191
|
+
|
|
192
|
+
let nodeId = '';
|
|
193
|
+
let nodeData = {};
|
|
194
|
+
let strokeColor = '#52525b';
|
|
195
|
+
|
|
196
|
+
if (type === 'tool') {
|
|
197
|
+
const tool = item as Tool;
|
|
198
|
+
nodeId = `tool-${tool.name}`;
|
|
199
|
+
nodeData = { tool };
|
|
200
|
+
strokeColor = '#14b8a6'; // Teal
|
|
201
|
+
} else if (type === 'resource') {
|
|
202
|
+
const resource = item as Resource;
|
|
203
|
+
nodeId = `resource-${resource.uri}`;
|
|
204
|
+
nodeData = { resource };
|
|
205
|
+
strokeColor = '#6366f1'; // Indigo
|
|
206
|
+
} else if (type === 'prompt') {
|
|
207
|
+
const prompt = item as Prompt;
|
|
208
|
+
nodeId = `prompt-${prompt.name}`;
|
|
209
|
+
nodeData = { prompt };
|
|
210
|
+
strokeColor = '#f59e0b'; // Amber
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
newNodes.push({
|
|
214
|
+
id: nodeId,
|
|
215
|
+
type: type,
|
|
216
|
+
position: { x, y },
|
|
217
|
+
data: nodeData,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
newEdges.push({
|
|
221
|
+
id: `e-root-${nodeId}`,
|
|
222
|
+
source: 'agent-root',
|
|
223
|
+
target: nodeId,
|
|
224
|
+
type: 'default',
|
|
225
|
+
animated: true,
|
|
226
|
+
style: { stroke: strokeColor, strokeWidth: 1.5, opacity: 0.3 },
|
|
227
|
+
markerEnd: {
|
|
228
|
+
type: MarkerType.ArrowClosed,
|
|
229
|
+
color: strokeColor,
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 1. Prompts (Top Left)
|
|
236
|
+
// Center of zone is approx (-zoneRadius, -zoneRadius) relative to main center
|
|
237
|
+
// We'll define start point for the grid
|
|
238
|
+
if (prompts && prompts.length > 0) {
|
|
239
|
+
const startX = centerX - 600;
|
|
240
|
+
const startY = centerY - 400;
|
|
241
|
+
placeItemsInZone(prompts, 'prompt', startX, startY, 2);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. Resources (Top Right)
|
|
245
|
+
if (resources && resources.length > 0) {
|
|
246
|
+
const startX = centerX + 200;
|
|
247
|
+
const startY = centerY - 400;
|
|
248
|
+
placeItemsInZone(resources, 'resource', startX, startY, 2);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 3. Tools (Bottom)
|
|
252
|
+
if (tools && tools.length > 0) {
|
|
253
|
+
// Center the tools grid at the bottom
|
|
254
|
+
const cols = 4;
|
|
255
|
+
const gridWidth = Math.min(tools.length, cols) * 240;
|
|
256
|
+
const startX = centerX - (gridWidth / 2) + 100; // Adjust to center
|
|
257
|
+
const startY = centerY + 200;
|
|
258
|
+
placeItemsInZone(tools, 'tool', startX, startY, cols);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setNodes(newNodes);
|
|
262
|
+
setEdges(newEdges);
|
|
263
|
+
}, [tools, resources, prompts, setNodes, setEdges]); // Re-calc when any data changes
|
|
264
|
+
|
|
265
|
+
const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
|
|
266
|
+
if (node.type === 'tool' && node.data.tool && onToolClick) {
|
|
267
|
+
onToolClick(node.data.tool);
|
|
268
|
+
} else if (node.type === 'resource' && node.data.resource && onResourceClick) {
|
|
269
|
+
onResourceClick(node.data.resource);
|
|
270
|
+
} else if (node.type === 'prompt' && node.data.prompt && onPromptClick) {
|
|
271
|
+
onPromptClick(node.data.prompt);
|
|
272
|
+
}
|
|
273
|
+
}, [onToolClick, onResourceClick, onPromptClick]);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div className="w-full h-full min-h-[600px] bg-[#09090b] rounded-xl overflow-hidden border border-border/50 shadow-inner relative group/canvas">
|
|
277
|
+
<ReactFlow
|
|
278
|
+
nodes={nodes}
|
|
279
|
+
edges={edges}
|
|
280
|
+
onNodesChange={onNodesChange}
|
|
281
|
+
onEdgesChange={onEdgesChange}
|
|
282
|
+
nodeTypes={nodeTypes}
|
|
283
|
+
onNodeClick={handleNodeClick}
|
|
284
|
+
fitView
|
|
285
|
+
className="bg-[#09090b]" // Zinc-950 dark background
|
|
286
|
+
>
|
|
287
|
+
<Background
|
|
288
|
+
color="#27272a" // Zinc-800 dots
|
|
289
|
+
gap={20}
|
|
290
|
+
size={1.5}
|
|
291
|
+
variant={"dots" as any}
|
|
292
|
+
/>
|
|
293
|
+
<Controls
|
|
294
|
+
position="bottom-right"
|
|
295
|
+
showInteractive={false}
|
|
296
|
+
className="!flex !flex-row !gap-1 !p-1 !bg-card/90 !backdrop-blur !border !border-border !rounded-lg !shadow-lg !m-4"
|
|
297
|
+
/>
|
|
298
|
+
</ReactFlow>
|
|
299
|
+
|
|
300
|
+
{/* Legend / Key */}
|
|
301
|
+
<div className="absolute top-4 left-4 flex gap-3 pointer-events-none">
|
|
302
|
+
<div className="bg-card/80 backdrop-blur px-3 py-1.5 rounded-lg border border-border text-xs text-muted-foreground">
|
|
303
|
+
Interactive Canvas
|
|
304
|
+
</div>
|
|
305
|
+
<div className="flex gap-2">
|
|
306
|
+
<span className="flex items-center gap-1.5 px-2 py-1 bg-teal-500/10 border border-teal-500/20 rounded-md text-[10px] text-teal-500 uppercase tracking-wide font-medium">
|
|
307
|
+
<WrenchScrewdriverIcon className="w-3 h-3" /> Tools
|
|
308
|
+
</span>
|
|
309
|
+
<span className="flex items-center gap-1.5 px-2 py-1 bg-indigo-500/10 border border-indigo-500/20 rounded-md text-[10px] text-indigo-500 uppercase tracking-wide font-medium">
|
|
310
|
+
<CubeIcon className="w-3 h-3" /> Resources
|
|
311
|
+
</span>
|
|
312
|
+
<span className="flex items-center gap-1.5 px-2 py-1 bg-amber-500/10 border border-amber-500/20 rounded-md text-[10px] text-amber-500 uppercase tracking-wide font-medium">
|
|
313
|
+
<DocumentTextIcon className="w-3 h-3" /> Prompts
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default function ToolsCanvasWrapper(props: ToolsCanvasProps) {
|
|
322
|
+
return (
|
|
323
|
+
<ReactFlowProvider>
|
|
324
|
+
<ToolsCanvas {...props} />
|
|
325
|
+
</ReactFlowProvider>
|
|
326
|
+
)
|
|
327
|
+
}
|
package/src/studio/lib/store.ts
CHANGED
|
@@ -46,6 +46,9 @@ interface StudioState {
|
|
|
46
46
|
apiKey: string | null;
|
|
47
47
|
setApiKey: (key: string | null) => void;
|
|
48
48
|
|
|
49
|
+
elevenLabsApiKey: string | null;
|
|
50
|
+
setElevenLabsApiKey: (key: string | null) => void;
|
|
51
|
+
|
|
49
52
|
oauthState: OAuthState;
|
|
50
53
|
setOAuthState: (state: Partial<OAuthState>) => void;
|
|
51
54
|
|
|
@@ -130,6 +133,18 @@ export const useStudioStore = create<StudioState>((set) => ({
|
|
|
130
133
|
set({ apiKey });
|
|
131
134
|
},
|
|
132
135
|
|
|
136
|
+
elevenLabsApiKey: typeof window !== 'undefined' ? localStorage.getItem('mcp_elevenlabs_key') : null,
|
|
137
|
+
setElevenLabsApiKey: (elevenLabsApiKey) => {
|
|
138
|
+
if (typeof window !== 'undefined') {
|
|
139
|
+
if (elevenLabsApiKey) {
|
|
140
|
+
localStorage.setItem('mcp_elevenlabs_key', elevenLabsApiKey);
|
|
141
|
+
} else {
|
|
142
|
+
localStorage.removeItem('mcp_elevenlabs_key');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
set({ elevenLabsApiKey });
|
|
146
|
+
},
|
|
147
|
+
|
|
133
148
|
oauthState: typeof window !== 'undefined'
|
|
134
149
|
? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
|
|
135
150
|
authServerUrl: null,
|