@contractspec/lib.example-shared-ui 0.0.0-canary-20260113170453
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/.turbo/turbo-build$colon$bundle.log +9 -0
- package/.turbo/turbo-build.log +11 -0
- package/CHANGELOG.md +34 -0
- package/dist/index.mjs +3121 -0
- package/package.json +43 -0
- package/src/EvolutionDashboard.tsx +480 -0
- package/src/EvolutionSidebar.tsx +282 -0
- package/src/LocalDataIndicator.tsx +39 -0
- package/src/MarkdownView.tsx +389 -0
- package/src/OverlayContextProvider.tsx +341 -0
- package/src/PersonalizationInsights.tsx +293 -0
- package/src/SaveToStudioButton.tsx +64 -0
- package/src/SpecEditorPanel.tsx +165 -0
- package/src/TemplateShell.tsx +63 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useBehaviorTracking.ts +327 -0
- package/src/hooks/useEvolution.ts +501 -0
- package/src/hooks/useRegistryTemplates.ts +49 -0
- package/src/hooks/useSpecContent.ts +243 -0
- package/src/hooks/useWorkflowComposer.ts +670 -0
- package/src/index.ts +15 -0
- package/src/lib/component-registry.tsx +64 -0
- package/src/lib/runtime-context.tsx +54 -0
- package/src/lib/types.ts +84 -0
- package/src/overlay-types.ts +25 -0
- package/src/utils/fetchPresentationData.ts +48 -0
- package/src/utils/generateSpecFromTemplate.ts +458 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useContext, useMemo } from 'react';
|
|
5
|
+
import type { TemplateId } from './lib/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Overlay modification operation types (for OverlayContextProvider)
|
|
9
|
+
*/
|
|
10
|
+
export type OverlayModificationOp =
|
|
11
|
+
| { op: 'hide' }
|
|
12
|
+
| { op: 'relabel'; label: string }
|
|
13
|
+
| { op: 'reorder'; position: number }
|
|
14
|
+
| { op: 'restyle'; className?: string; variant?: string }
|
|
15
|
+
| { op: 'set-default'; value: unknown };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Overlay spec for a field or component
|
|
19
|
+
*/
|
|
20
|
+
export interface OverlaySpec {
|
|
21
|
+
id: string;
|
|
22
|
+
target: string; // path to the field/component
|
|
23
|
+
modifications: OverlayModificationOp[];
|
|
24
|
+
conditions?: {
|
|
25
|
+
role?: string[];
|
|
26
|
+
device?: 'mobile' | 'desktop' | 'any';
|
|
27
|
+
featureFlags?: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Context value for overlay engine
|
|
33
|
+
*/
|
|
34
|
+
export interface OverlayContextValue {
|
|
35
|
+
/** Current overlays */
|
|
36
|
+
overlays: OverlaySpec[];
|
|
37
|
+
/** Apply overlays to a component */
|
|
38
|
+
applyOverlay: <T extends Record<string, unknown>>(
|
|
39
|
+
path: string,
|
|
40
|
+
target: T
|
|
41
|
+
) => T;
|
|
42
|
+
/** Check if a field should be hidden */
|
|
43
|
+
isHidden: (path: string) => boolean;
|
|
44
|
+
/** Get relabeled text */
|
|
45
|
+
getLabel: (path: string, defaultLabel: string) => string;
|
|
46
|
+
/** Get position for reordering */
|
|
47
|
+
getPosition: (path: string, defaultPosition: number) => number;
|
|
48
|
+
/** Get style modifications */
|
|
49
|
+
getStyle: (path: string) => { className?: string; variant?: string };
|
|
50
|
+
/** Get default value */
|
|
51
|
+
getDefault: <T>(path: string, defaultValue: T) => T;
|
|
52
|
+
/** Current user role */
|
|
53
|
+
role: string;
|
|
54
|
+
/** Current device type */
|
|
55
|
+
device: 'mobile' | 'desktop';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const OverlayContext = React.createContext<OverlayContextValue | null>(null);
|
|
59
|
+
|
|
60
|
+
export interface OverlayContextProviderProps extends React.PropsWithChildren {
|
|
61
|
+
templateId: TemplateId;
|
|
62
|
+
role?: string;
|
|
63
|
+
device?: 'mobile' | 'desktop';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Provider for overlay engine context.
|
|
68
|
+
* Loads template-specific overlays and provides helper functions.
|
|
69
|
+
*/
|
|
70
|
+
export function OverlayContextProvider({
|
|
71
|
+
templateId,
|
|
72
|
+
role = 'user',
|
|
73
|
+
device = 'desktop',
|
|
74
|
+
children,
|
|
75
|
+
}: OverlayContextProviderProps) {
|
|
76
|
+
// Load template-specific overlays
|
|
77
|
+
const overlays = useMemo(
|
|
78
|
+
() => getTemplateOverlays(templateId, role),
|
|
79
|
+
[templateId, role]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Filter overlays based on current context
|
|
83
|
+
const activeOverlays = useMemo(() => {
|
|
84
|
+
return overlays.filter((overlay) => {
|
|
85
|
+
const conditions = overlay.conditions;
|
|
86
|
+
if (!conditions) return true;
|
|
87
|
+
|
|
88
|
+
if (conditions.role && !conditions.role.includes(role)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
conditions.device &&
|
|
94
|
+
conditions.device !== 'any' &&
|
|
95
|
+
conditions.device !== device
|
|
96
|
+
) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}, [overlays, role, device]);
|
|
103
|
+
|
|
104
|
+
// Create overlay map for quick lookups
|
|
105
|
+
const overlayMap = useMemo(() => {
|
|
106
|
+
const map = new Map<string, OverlaySpec>();
|
|
107
|
+
for (const overlay of activeOverlays) {
|
|
108
|
+
map.set(overlay.target, overlay);
|
|
109
|
+
}
|
|
110
|
+
return map;
|
|
111
|
+
}, [activeOverlays]);
|
|
112
|
+
|
|
113
|
+
// Apply overlay to a target object
|
|
114
|
+
const applyOverlay = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
<T extends Record<string, unknown>>(path: string, target: T): T => {
|
|
117
|
+
const overlay = overlayMap.get(path);
|
|
118
|
+
if (!overlay) return target;
|
|
119
|
+
|
|
120
|
+
let result = { ...target };
|
|
121
|
+
for (const mod of overlay.modifications) {
|
|
122
|
+
switch (mod.op) {
|
|
123
|
+
case 'hide':
|
|
124
|
+
result = { ...result, hidden: true };
|
|
125
|
+
break;
|
|
126
|
+
case 'relabel':
|
|
127
|
+
result = { ...result, label: mod.label };
|
|
128
|
+
break;
|
|
129
|
+
case 'reorder':
|
|
130
|
+
result = { ...result, position: mod.position };
|
|
131
|
+
break;
|
|
132
|
+
case 'restyle':
|
|
133
|
+
result = {
|
|
134
|
+
...result,
|
|
135
|
+
className: mod.className,
|
|
136
|
+
variant: mod.variant,
|
|
137
|
+
};
|
|
138
|
+
break;
|
|
139
|
+
case 'set-default':
|
|
140
|
+
result = { ...result, defaultValue: mod.value };
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
},
|
|
146
|
+
[overlayMap]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Check if field is hidden
|
|
150
|
+
const isHidden = useMemo(
|
|
151
|
+
() =>
|
|
152
|
+
(path: string): boolean => {
|
|
153
|
+
const overlay = overlayMap.get(path);
|
|
154
|
+
return overlay?.modifications.some((m) => m.op === 'hide') ?? false;
|
|
155
|
+
},
|
|
156
|
+
[overlayMap]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Get relabeled text
|
|
160
|
+
const getLabel = useMemo(
|
|
161
|
+
() =>
|
|
162
|
+
(path: string, defaultLabel: string): string => {
|
|
163
|
+
const overlay = overlayMap.get(path);
|
|
164
|
+
const relabel = overlay?.modifications.find((m) => m.op === 'relabel');
|
|
165
|
+
return relabel && relabel.op === 'relabel'
|
|
166
|
+
? relabel.label
|
|
167
|
+
: defaultLabel;
|
|
168
|
+
},
|
|
169
|
+
[overlayMap]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Get position for reordering
|
|
173
|
+
const getPosition = useMemo(
|
|
174
|
+
() =>
|
|
175
|
+
(path: string, defaultPosition: number): number => {
|
|
176
|
+
const overlay = overlayMap.get(path);
|
|
177
|
+
const reorder = overlay?.modifications.find((m) => m.op === 'reorder');
|
|
178
|
+
return reorder && reorder.op === 'reorder'
|
|
179
|
+
? reorder.position
|
|
180
|
+
: defaultPosition;
|
|
181
|
+
},
|
|
182
|
+
[overlayMap]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Get style modifications
|
|
186
|
+
const getStyle = useMemo(
|
|
187
|
+
() =>
|
|
188
|
+
(path: string): { className?: string; variant?: string } => {
|
|
189
|
+
const overlay = overlayMap.get(path);
|
|
190
|
+
const restyle = overlay?.modifications.find((m) => m.op === 'restyle');
|
|
191
|
+
if (restyle && restyle.op === 'restyle') {
|
|
192
|
+
return {
|
|
193
|
+
className: restyle.className,
|
|
194
|
+
variant: restyle.variant,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
},
|
|
199
|
+
[overlayMap]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Get default value
|
|
203
|
+
const getDefault = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
<T,>(path: string, defaultValue: T): T => {
|
|
206
|
+
const overlay = overlayMap.get(path);
|
|
207
|
+
const setDefault = overlay?.modifications.find(
|
|
208
|
+
(m) => m.op === 'set-default'
|
|
209
|
+
);
|
|
210
|
+
return setDefault && setDefault.op === 'set-default'
|
|
211
|
+
? (setDefault.value as T)
|
|
212
|
+
: defaultValue;
|
|
213
|
+
},
|
|
214
|
+
[overlayMap]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const value = useMemo<OverlayContextValue>(
|
|
218
|
+
() => ({
|
|
219
|
+
overlays: activeOverlays,
|
|
220
|
+
applyOverlay,
|
|
221
|
+
isHidden,
|
|
222
|
+
getLabel,
|
|
223
|
+
getPosition,
|
|
224
|
+
getStyle,
|
|
225
|
+
getDefault,
|
|
226
|
+
role,
|
|
227
|
+
device,
|
|
228
|
+
}),
|
|
229
|
+
[
|
|
230
|
+
activeOverlays,
|
|
231
|
+
applyOverlay,
|
|
232
|
+
isHidden,
|
|
233
|
+
getLabel,
|
|
234
|
+
getPosition,
|
|
235
|
+
getStyle,
|
|
236
|
+
getDefault,
|
|
237
|
+
role,
|
|
238
|
+
device,
|
|
239
|
+
]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<OverlayContext.Provider value={value}>{children}</OverlayContext.Provider>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Hook to access overlay context
|
|
249
|
+
*/
|
|
250
|
+
export function useOverlayContext(): OverlayContextValue {
|
|
251
|
+
const context = useContext(OverlayContext);
|
|
252
|
+
if (!context) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
'useOverlayContext must be used within an OverlayContextProvider'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return context;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Hook to check if within overlay context
|
|
262
|
+
*/
|
|
263
|
+
export function useIsInOverlayContext(): boolean {
|
|
264
|
+
return useContext(OverlayContext) !== null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get template-specific overlays
|
|
269
|
+
*/
|
|
270
|
+
function getTemplateOverlays(
|
|
271
|
+
templateId: TemplateId,
|
|
272
|
+
_role: string
|
|
273
|
+
): OverlaySpec[] {
|
|
274
|
+
// Demo overlays for each template
|
|
275
|
+
const templateOverlays: Record<string, OverlaySpec[]> = {
|
|
276
|
+
'crm-pipeline': [
|
|
277
|
+
{
|
|
278
|
+
id: 'crm-hide-internal-fields',
|
|
279
|
+
target: 'deal.internalNotes',
|
|
280
|
+
modifications: [{ op: 'hide' }],
|
|
281
|
+
conditions: { role: ['viewer', 'user'] },
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'crm-relabel-value',
|
|
285
|
+
target: 'deal.value',
|
|
286
|
+
modifications: [{ op: 'relabel', label: 'Deal Amount' }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
'saas-boilerplate': [
|
|
290
|
+
{
|
|
291
|
+
id: 'saas-hide-billing',
|
|
292
|
+
target: 'settings.billing',
|
|
293
|
+
modifications: [{ op: 'hide' }],
|
|
294
|
+
conditions: { role: ['viewer'] },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'saas-restyle-plan',
|
|
298
|
+
target: 'settings.plan',
|
|
299
|
+
modifications: [{ op: 'restyle', variant: 'premium' }],
|
|
300
|
+
conditions: { role: ['admin'] },
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
'agent-console': [
|
|
304
|
+
{
|
|
305
|
+
id: 'agent-hide-cost',
|
|
306
|
+
target: 'run.cost',
|
|
307
|
+
modifications: [{ op: 'hide' }],
|
|
308
|
+
conditions: { role: ['viewer'] },
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'agent-relabel-tokens',
|
|
312
|
+
target: 'run.tokens',
|
|
313
|
+
modifications: [{ op: 'relabel', label: 'Token Usage' }],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
'todos-app': [
|
|
317
|
+
{
|
|
318
|
+
id: 'todos-hide-assignee',
|
|
319
|
+
target: 'task.assignee',
|
|
320
|
+
modifications: [{ op: 'hide' }],
|
|
321
|
+
conditions: { device: 'mobile' },
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
'messaging-app': [
|
|
325
|
+
{
|
|
326
|
+
id: 'messaging-reorder-timestamp',
|
|
327
|
+
target: 'message.timestamp',
|
|
328
|
+
modifications: [{ op: 'reorder', position: 0 }],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
'recipe-app-i18n': [
|
|
332
|
+
{
|
|
333
|
+
id: 'recipe-relabel-servings',
|
|
334
|
+
target: 'recipe.servings',
|
|
335
|
+
modifications: [{ op: 'relabel', label: 'Portions' }],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return templateOverlays[templateId] ?? [];
|
|
341
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { Button } from '@contractspec/lib.design-system';
|
|
5
|
+
import { Card } from '@contractspec/lib.ui-kit-web/ui/card';
|
|
6
|
+
import { Badge } from '@contractspec/lib.ui-kit-web/ui/badge';
|
|
7
|
+
import type { TemplateId } from './lib/types';
|
|
8
|
+
import {
|
|
9
|
+
type BehaviorSummary,
|
|
10
|
+
useBehaviorTracking,
|
|
11
|
+
} from './hooks/useBehaviorTracking';
|
|
12
|
+
|
|
13
|
+
export interface PersonalizationInsightsProps {
|
|
14
|
+
templateId: TemplateId;
|
|
15
|
+
/** Whether to show in collapsed mode */
|
|
16
|
+
collapsed?: boolean;
|
|
17
|
+
/** Toggle collapsed state */
|
|
18
|
+
onToggle?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Component showing personalization insights based on user behavior.
|
|
23
|
+
* Displays usage patterns, recommendations, and feature adoption.
|
|
24
|
+
*/
|
|
25
|
+
export function PersonalizationInsights({
|
|
26
|
+
templateId,
|
|
27
|
+
collapsed = false,
|
|
28
|
+
onToggle,
|
|
29
|
+
}: PersonalizationInsightsProps) {
|
|
30
|
+
const { getSummary, eventCount, clear, sessionStart } =
|
|
31
|
+
useBehaviorTracking(templateId);
|
|
32
|
+
|
|
33
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
34
|
+
|
|
35
|
+
const summary = useMemo(() => getSummary(), [getSummary]);
|
|
36
|
+
|
|
37
|
+
const formatDuration = useCallback((ms: number) => {
|
|
38
|
+
const seconds = Math.floor(ms / 1000);
|
|
39
|
+
const minutes = Math.floor(seconds / 60);
|
|
40
|
+
const hours = Math.floor(minutes / 60);
|
|
41
|
+
|
|
42
|
+
if (hours > 0) {
|
|
43
|
+
return `${hours}h ${minutes % 60}m`;
|
|
44
|
+
}
|
|
45
|
+
if (minutes > 0) {
|
|
46
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
47
|
+
}
|
|
48
|
+
return `${seconds}s`;
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handleClear = useCallback(() => {
|
|
52
|
+
clear();
|
|
53
|
+
}, [clear]);
|
|
54
|
+
|
|
55
|
+
// Collapsed view
|
|
56
|
+
if (collapsed) {
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
onClick={onToggle}
|
|
60
|
+
className="flex items-center gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 text-sm transition hover:bg-blue-500/20"
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
<span>📊</span>
|
|
64
|
+
<span>Insights</span>
|
|
65
|
+
<Badge variant="secondary">{eventCount}</Badge>
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Card className="overflow-hidden">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div className="flex items-center justify-between border-b border-blue-500/20 bg-blue-500/5 px-4 py-3">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span>📊</span>
|
|
76
|
+
<span className="font-semibold">Personalization Insights</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<Button
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="sm"
|
|
82
|
+
onPress={() => setShowDetails(!showDetails)}
|
|
83
|
+
>
|
|
84
|
+
{showDetails ? 'Hide Details' : 'Show Details'}
|
|
85
|
+
</Button>
|
|
86
|
+
{onToggle && (
|
|
87
|
+
<button
|
|
88
|
+
onClick={onToggle}
|
|
89
|
+
className="text-muted-foreground hover:text-foreground p-1"
|
|
90
|
+
type="button"
|
|
91
|
+
title="Collapse"
|
|
92
|
+
>
|
|
93
|
+
✕
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="p-4">
|
|
100
|
+
{/* Quick Stats */}
|
|
101
|
+
<div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
102
|
+
<StatCard
|
|
103
|
+
label="Session Time"
|
|
104
|
+
value={formatDuration(summary.sessionDuration)}
|
|
105
|
+
icon="⏱️"
|
|
106
|
+
/>
|
|
107
|
+
<StatCard
|
|
108
|
+
label="Events Tracked"
|
|
109
|
+
value={summary.totalEvents.toString()}
|
|
110
|
+
icon="📈"
|
|
111
|
+
/>
|
|
112
|
+
<StatCard
|
|
113
|
+
label="Features Used"
|
|
114
|
+
value={`${summary.featuresUsed.length}/${summary.featuresUsed.length + summary.unusedFeatures.length}`}
|
|
115
|
+
icon="✨"
|
|
116
|
+
/>
|
|
117
|
+
<StatCard
|
|
118
|
+
label="Errors"
|
|
119
|
+
value={summary.errorCount.toString()}
|
|
120
|
+
icon="⚠️"
|
|
121
|
+
variant={summary.errorCount > 0 ? 'warning' : 'success'}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Recommendations */}
|
|
126
|
+
{summary.recommendations.length > 0 && (
|
|
127
|
+
<div className="mb-4">
|
|
128
|
+
<h4 className="mb-2 text-xs font-semibold text-blue-400 uppercase">
|
|
129
|
+
Recommendations
|
|
130
|
+
</h4>
|
|
131
|
+
<ul className="space-y-1">
|
|
132
|
+
{summary.recommendations.map((rec, index) => (
|
|
133
|
+
<li key={index} className="flex items-start gap-2 text-sm">
|
|
134
|
+
<span className="text-blue-400">💡</span>
|
|
135
|
+
<span>{rec}</span>
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{/* Unused Features */}
|
|
143
|
+
{summary.unusedFeatures.length > 0 && (
|
|
144
|
+
<div className="mb-4">
|
|
145
|
+
<h4 className="mb-2 text-xs font-semibold text-blue-400 uppercase">
|
|
146
|
+
Try These Features
|
|
147
|
+
</h4>
|
|
148
|
+
<div className="flex flex-wrap gap-2">
|
|
149
|
+
{summary.unusedFeatures.slice(0, 5).map((feature) => (
|
|
150
|
+
<Badge key={feature} variant="secondary">
|
|
151
|
+
{formatFeatureName(feature)}
|
|
152
|
+
</Badge>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Detailed View */}
|
|
159
|
+
{showDetails && (
|
|
160
|
+
<DetailedInsights summary={summary} sessionStart={sessionStart} />
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{/* Footer Actions */}
|
|
164
|
+
<div className="mt-4 flex justify-end border-t border-blue-500/10 pt-4">
|
|
165
|
+
<Button variant="ghost" size="sm" onPress={handleClear}>
|
|
166
|
+
Clear Data
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</Card>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Stat card component
|
|
176
|
+
*/
|
|
177
|
+
function StatCard({
|
|
178
|
+
label,
|
|
179
|
+
value,
|
|
180
|
+
icon,
|
|
181
|
+
variant = 'default',
|
|
182
|
+
}: {
|
|
183
|
+
label: string;
|
|
184
|
+
value: string;
|
|
185
|
+
icon: string;
|
|
186
|
+
variant?: 'default' | 'warning' | 'success';
|
|
187
|
+
}) {
|
|
188
|
+
const bgColors = {
|
|
189
|
+
default: 'bg-blue-500/5 border-blue-500/20',
|
|
190
|
+
warning: 'bg-amber-500/5 border-amber-500/20',
|
|
191
|
+
success: 'bg-green-500/5 border-green-500/20',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className={`rounded-lg border p-3 text-center ${bgColors[variant]}`}>
|
|
196
|
+
<div className="mb-1 text-lg">{icon}</div>
|
|
197
|
+
<div className="text-lg font-bold">{value}</div>
|
|
198
|
+
<div className="text-muted-foreground text-xs">{label}</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Detailed insights section
|
|
205
|
+
*/
|
|
206
|
+
function DetailedInsights({
|
|
207
|
+
summary,
|
|
208
|
+
sessionStart,
|
|
209
|
+
}: {
|
|
210
|
+
summary: BehaviorSummary;
|
|
211
|
+
sessionStart: Date;
|
|
212
|
+
}) {
|
|
213
|
+
return (
|
|
214
|
+
<div className="mt-4 space-y-4 border-t border-blue-500/10 pt-4">
|
|
215
|
+
{/* Most Used Templates */}
|
|
216
|
+
{summary.mostUsedTemplates.length > 0 && (
|
|
217
|
+
<div>
|
|
218
|
+
<h4 className="mb-2 text-xs font-semibold text-blue-400 uppercase">
|
|
219
|
+
Most Used Templates
|
|
220
|
+
</h4>
|
|
221
|
+
<div className="space-y-1">
|
|
222
|
+
{summary.mostUsedTemplates.map(({ templateId, count }) => (
|
|
223
|
+
<div
|
|
224
|
+
key={templateId}
|
|
225
|
+
className="flex items-center justify-between text-sm"
|
|
226
|
+
>
|
|
227
|
+
<span>{formatTemplateId(templateId)}</span>
|
|
228
|
+
<span className="text-muted-foreground">{count} events</span>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Most Used Modes */}
|
|
236
|
+
{summary.mostUsedModes.length > 0 && (
|
|
237
|
+
<div>
|
|
238
|
+
<h4 className="mb-2 text-xs font-semibold text-blue-400 uppercase">
|
|
239
|
+
Mode Usage
|
|
240
|
+
</h4>
|
|
241
|
+
<div className="space-y-1">
|
|
242
|
+
{summary.mostUsedModes.map(({ mode, count }) => (
|
|
243
|
+
<div
|
|
244
|
+
key={mode}
|
|
245
|
+
className="flex items-center justify-between text-sm"
|
|
246
|
+
>
|
|
247
|
+
<span>{formatFeatureName(mode)}</span>
|
|
248
|
+
<span className="text-muted-foreground">{count} switches</span>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{/* Features Used */}
|
|
256
|
+
<div>
|
|
257
|
+
<h4 className="mb-2 text-xs font-semibold text-blue-400 uppercase">
|
|
258
|
+
Features Used
|
|
259
|
+
</h4>
|
|
260
|
+
<div className="flex flex-wrap gap-2">
|
|
261
|
+
{summary.featuresUsed.map((feature) => (
|
|
262
|
+
<Badge
|
|
263
|
+
key={feature}
|
|
264
|
+
variant="default"
|
|
265
|
+
className="border-green-500/30 bg-green-500/20 text-green-400"
|
|
266
|
+
>
|
|
267
|
+
✓ {formatFeatureName(feature)}
|
|
268
|
+
</Badge>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Session Info */}
|
|
274
|
+
<div className="text-muted-foreground text-xs">
|
|
275
|
+
Session started: {sessionStart.toLocaleString()}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Format feature name for display
|
|
283
|
+
*/
|
|
284
|
+
function formatFeatureName(feature: string): string {
|
|
285
|
+
return feature.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Format template ID for display
|
|
290
|
+
*/
|
|
291
|
+
function formatTemplateId(id: string): string {
|
|
292
|
+
return id.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
293
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Sparkles } from 'lucide-react';
|
|
5
|
+
import { useTemplateRuntime } from './lib/runtime-context';
|
|
6
|
+
|
|
7
|
+
export interface SaveToStudioButtonProps {
|
|
8
|
+
organizationId?: string;
|
|
9
|
+
projectName?: string;
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SaveToStudioButton({
|
|
15
|
+
organizationId = 'demo-org',
|
|
16
|
+
projectName,
|
|
17
|
+
endpoint,
|
|
18
|
+
token,
|
|
19
|
+
}: SaveToStudioButtonProps) {
|
|
20
|
+
const { installer, templateId, template } = useTemplateRuntime();
|
|
21
|
+
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>(
|
|
22
|
+
'idle'
|
|
23
|
+
);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
const handleSave = async () => {
|
|
27
|
+
setStatus('saving');
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
await installer.saveToStudio({
|
|
31
|
+
templateId,
|
|
32
|
+
projectName: projectName ?? `${template.name} demo`,
|
|
33
|
+
organizationId,
|
|
34
|
+
endpoint,
|
|
35
|
+
token,
|
|
36
|
+
});
|
|
37
|
+
setStatus('saved');
|
|
38
|
+
setTimeout(() => setStatus('idle'), 3000);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setStatus('error');
|
|
41
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col items-end gap-1">
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
className="btn-primary inline-flex items-center gap-2 text-sm"
|
|
50
|
+
onClick={handleSave}
|
|
51
|
+
disabled={status === 'saving'}
|
|
52
|
+
>
|
|
53
|
+
<Sparkles className="h-4 w-4" />
|
|
54
|
+
{status === 'saving' ? 'Publishing…' : 'Save to Studio'}
|
|
55
|
+
</button>
|
|
56
|
+
{status === 'error' && error ? (
|
|
57
|
+
<p className="text-destructive text-xs">{error}</p>
|
|
58
|
+
) : null}
|
|
59
|
+
{status === 'saved' ? (
|
|
60
|
+
<p className="text-xs text-emerald-400">Template sent to Studio.</p>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|