@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
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/lib.example-shared-ui",
|
|
3
|
+
"version": "0.0.0-canary-20260113170453",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
10
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
11
|
+
"build": "bun build:types && bun build:bundle",
|
|
12
|
+
"build:bundle": "tsdown",
|
|
13
|
+
"build:types": "tsc --noEmit",
|
|
14
|
+
"dev": "bun build:bundle --watch",
|
|
15
|
+
"clean": "rimraf dist .turbo",
|
|
16
|
+
"lint": "bun lint:fix",
|
|
17
|
+
"lint:fix": "eslint src --fix",
|
|
18
|
+
"lint:check": "eslint src",
|
|
19
|
+
"test": "bun test"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@apollo/client": "^4.0.12",
|
|
27
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260113170453",
|
|
28
|
+
"@contractspec/lib.design-system": "0.0.0-canary-20260113170453",
|
|
29
|
+
"@contractspec/lib.ui-kit-web": "0.0.0-canary-20260113170453",
|
|
30
|
+
"@tanstack/react-query": "^5.90.16",
|
|
31
|
+
"framer-motion": "^12.26.1",
|
|
32
|
+
"lucide-react": "^0.300.0",
|
|
33
|
+
"react": "19.2.3",
|
|
34
|
+
"react-dom": "19.2.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@contractspec/tool.typescript": "0.0.0-canary-20260113170453",
|
|
38
|
+
"@types/react": "^19.2.8",
|
|
39
|
+
"@types/react-dom": "^19.2.2",
|
|
40
|
+
"eslint": "^9.39.2",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { Button, LoaderBlock } 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 OptimizationHint,
|
|
10
|
+
type SpecAnomaly,
|
|
11
|
+
type SpecSuggestion,
|
|
12
|
+
type SpecUsageStats,
|
|
13
|
+
useEvolution,
|
|
14
|
+
} from './hooks/useEvolution';
|
|
15
|
+
|
|
16
|
+
export interface EvolutionDashboardProps {
|
|
17
|
+
templateId: TemplateId;
|
|
18
|
+
onLog?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dashboard for AI-powered spec evolution.
|
|
23
|
+
* Shows usage statistics, anomalies, AI suggestions, and optimization hints.
|
|
24
|
+
*/
|
|
25
|
+
export function EvolutionDashboard({
|
|
26
|
+
templateId,
|
|
27
|
+
onLog,
|
|
28
|
+
}: EvolutionDashboardProps) {
|
|
29
|
+
const {
|
|
30
|
+
usageStats,
|
|
31
|
+
anomalies,
|
|
32
|
+
suggestions,
|
|
33
|
+
hints,
|
|
34
|
+
loading,
|
|
35
|
+
trackOperation,
|
|
36
|
+
analyzeUsage,
|
|
37
|
+
generateSuggestions,
|
|
38
|
+
approveSuggestion,
|
|
39
|
+
rejectSuggestion,
|
|
40
|
+
clear,
|
|
41
|
+
operationCount,
|
|
42
|
+
} = useEvolution(templateId);
|
|
43
|
+
|
|
44
|
+
// Simulate operations for demo
|
|
45
|
+
const handleSimulateOperations = useCallback(() => {
|
|
46
|
+
// Simulate various operations with different outcomes
|
|
47
|
+
const operations = [
|
|
48
|
+
{ name: `${templateId}.list`, duration: 150, success: true },
|
|
49
|
+
{ name: `${templateId}.list`, duration: 180, success: true },
|
|
50
|
+
{ name: `${templateId}.create`, duration: 350, success: true },
|
|
51
|
+
{
|
|
52
|
+
name: `${templateId}.create`,
|
|
53
|
+
duration: 420,
|
|
54
|
+
success: false,
|
|
55
|
+
error: 'VALIDATION_ERROR',
|
|
56
|
+
},
|
|
57
|
+
{ name: `${templateId}.list`, duration: 200, success: true },
|
|
58
|
+
{ name: `${templateId}.get`, duration: 80, success: true },
|
|
59
|
+
{ name: `${templateId}.update`, duration: 280, success: true },
|
|
60
|
+
{ name: `${templateId}.list`, duration: 950, success: true }, // Slow
|
|
61
|
+
{
|
|
62
|
+
name: `${templateId}.delete`,
|
|
63
|
+
duration: 150,
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'NOT_FOUND',
|
|
66
|
+
},
|
|
67
|
+
{ name: `${templateId}.create`, duration: 380, success: true },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const op of operations) {
|
|
71
|
+
trackOperation(op.name, op.duration, op.success, op.error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onLog?.(`Simulated ${operations.length} operations`);
|
|
75
|
+
// Auto-analyze after simulation
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
analyzeUsage();
|
|
78
|
+
onLog?.('Analysis complete');
|
|
79
|
+
}, 100);
|
|
80
|
+
}, [templateId, trackOperation, analyzeUsage, onLog]);
|
|
81
|
+
|
|
82
|
+
const handleGenerateSuggestions = useCallback(async () => {
|
|
83
|
+
await generateSuggestions();
|
|
84
|
+
onLog?.('AI suggestions generated');
|
|
85
|
+
}, [generateSuggestions, onLog]);
|
|
86
|
+
|
|
87
|
+
const handleApproveSuggestion = useCallback(
|
|
88
|
+
(id: string) => {
|
|
89
|
+
approveSuggestion(id);
|
|
90
|
+
onLog?.(`Suggestion ${id.slice(0, 8)} approved`);
|
|
91
|
+
},
|
|
92
|
+
[approveSuggestion, onLog]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const handleRejectSuggestion = useCallback(
|
|
96
|
+
(id: string) => {
|
|
97
|
+
rejectSuggestion(id);
|
|
98
|
+
onLog?.(`Suggestion ${id.slice(0, 8)} rejected`);
|
|
99
|
+
},
|
|
100
|
+
[rejectSuggestion, onLog]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const handleClear = useCallback(() => {
|
|
104
|
+
clear();
|
|
105
|
+
onLog?.('Evolution data cleared');
|
|
106
|
+
}, [clear, onLog]);
|
|
107
|
+
|
|
108
|
+
const pendingSuggestions = useMemo(
|
|
109
|
+
() => suggestions.filter((s) => s.status === 'pending'),
|
|
110
|
+
[suggestions]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-6">
|
|
115
|
+
{/* Header */}
|
|
116
|
+
<div className="flex items-center justify-between">
|
|
117
|
+
<div>
|
|
118
|
+
<h2 className="text-xl font-semibold">AI Evolution Engine</h2>
|
|
119
|
+
<p className="text-muted-foreground text-sm">
|
|
120
|
+
Analyze usage patterns and get AI-powered suggestions
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-2">
|
|
124
|
+
<Badge variant="secondary">{operationCount} ops tracked</Badge>
|
|
125
|
+
<Button variant="ghost" size="sm" onPress={handleClear}>
|
|
126
|
+
Clear
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Actions */}
|
|
132
|
+
<Card className="p-4">
|
|
133
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
134
|
+
<Button
|
|
135
|
+
variant="default"
|
|
136
|
+
size="sm"
|
|
137
|
+
onPress={handleSimulateOperations}
|
|
138
|
+
>
|
|
139
|
+
Simulate Operations
|
|
140
|
+
</Button>
|
|
141
|
+
<Button
|
|
142
|
+
variant="outline"
|
|
143
|
+
size="sm"
|
|
144
|
+
onPress={analyzeUsage}
|
|
145
|
+
disabled={operationCount < 5}
|
|
146
|
+
>
|
|
147
|
+
Analyze Usage
|
|
148
|
+
</Button>
|
|
149
|
+
<Button
|
|
150
|
+
variant="outline"
|
|
151
|
+
size="sm"
|
|
152
|
+
onPress={handleGenerateSuggestions}
|
|
153
|
+
disabled={anomalies.length === 0 || loading}
|
|
154
|
+
>
|
|
155
|
+
{loading ? 'Generating...' : 'Generate AI Suggestions'}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
<p className="text-muted-foreground mt-2 text-xs">
|
|
159
|
+
Simulate sandbox operations, analyze patterns, and generate AI
|
|
160
|
+
improvement suggestions.
|
|
161
|
+
</p>
|
|
162
|
+
</Card>
|
|
163
|
+
|
|
164
|
+
{loading && <LoaderBlock label="Generating AI suggestions..." />}
|
|
165
|
+
|
|
166
|
+
{/* Usage Statistics */}
|
|
167
|
+
{usageStats.length > 0 && (
|
|
168
|
+
<Card className="p-4">
|
|
169
|
+
<h3 className="mb-3 font-semibold">Usage Statistics</h3>
|
|
170
|
+
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
171
|
+
{usageStats.map((stat) => (
|
|
172
|
+
<UsageStatCard key={stat.operation.name} stat={stat} />
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</Card>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Anomalies */}
|
|
179
|
+
{anomalies.length > 0 && (
|
|
180
|
+
<Card className="p-4">
|
|
181
|
+
<div className="mb-3 flex items-center justify-between">
|
|
182
|
+
<h3 className="font-semibold">Detected Anomalies</h3>
|
|
183
|
+
<Badge
|
|
184
|
+
variant="secondary"
|
|
185
|
+
className="border-amber-500/30 bg-amber-500/20 text-amber-400"
|
|
186
|
+
>
|
|
187
|
+
{anomalies.length} issues
|
|
188
|
+
</Badge>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="space-y-2">
|
|
191
|
+
{anomalies.map((anomaly, index) => (
|
|
192
|
+
<AnomalyCard
|
|
193
|
+
key={`${anomaly.operation.name}-${index}`}
|
|
194
|
+
anomaly={anomaly}
|
|
195
|
+
/>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</Card>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* AI Suggestions */}
|
|
202
|
+
{suggestions.length > 0 && (
|
|
203
|
+
<Card className="p-4">
|
|
204
|
+
<div className="mb-3 flex items-center justify-between">
|
|
205
|
+
<h3 className="font-semibold">AI Suggestions</h3>
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
{pendingSuggestions.length > 0 && (
|
|
208
|
+
<Badge
|
|
209
|
+
variant="secondary"
|
|
210
|
+
className="border-amber-500/30 bg-amber-500/20 text-amber-400"
|
|
211
|
+
>
|
|
212
|
+
{pendingSuggestions.length} pending
|
|
213
|
+
</Badge>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="space-y-3">
|
|
218
|
+
{suggestions.map((suggestion) => (
|
|
219
|
+
<SuggestionCard
|
|
220
|
+
key={suggestion.id}
|
|
221
|
+
suggestion={suggestion}
|
|
222
|
+
onApprove={handleApproveSuggestion}
|
|
223
|
+
onReject={handleRejectSuggestion}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</Card>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Optimization Hints */}
|
|
231
|
+
{hints.length > 0 && (
|
|
232
|
+
<Card className="p-4">
|
|
233
|
+
<h3 className="mb-3 font-semibold">Optimization Hints</h3>
|
|
234
|
+
<div className="space-y-2">
|
|
235
|
+
{hints.map((hint, index) => (
|
|
236
|
+
<HintCard key={`${hint.operation.name}-${index}`} hint={hint} />
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
</Card>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Empty State */}
|
|
243
|
+
{usageStats.length === 0 &&
|
|
244
|
+
anomalies.length === 0 &&
|
|
245
|
+
suggestions.length === 0 && (
|
|
246
|
+
<Card className="p-8 text-center">
|
|
247
|
+
<p className="text-muted-foreground">
|
|
248
|
+
Click "Simulate Operations" to generate sample data for
|
|
249
|
+
analysis.
|
|
250
|
+
</p>
|
|
251
|
+
</Card>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Usage statistics card component
|
|
259
|
+
*/
|
|
260
|
+
function UsageStatCard({ stat }: { stat: SpecUsageStats }) {
|
|
261
|
+
return (
|
|
262
|
+
<div className="rounded-lg border border-violet-500/20 bg-violet-500/5 p-3">
|
|
263
|
+
<div className="mb-2 flex items-center justify-between">
|
|
264
|
+
<span className="font-mono text-sm font-medium">
|
|
265
|
+
{stat.operation.name}
|
|
266
|
+
</span>
|
|
267
|
+
<Badge
|
|
268
|
+
variant={stat.errorRate > 0.1 ? 'destructive' : 'default'}
|
|
269
|
+
className={
|
|
270
|
+
stat.errorRate > 0.1
|
|
271
|
+
? ''
|
|
272
|
+
: 'border-green-500/30 bg-green-500/20 text-green-400'
|
|
273
|
+
}
|
|
274
|
+
>
|
|
275
|
+
{((1 - stat.errorRate) * 100).toFixed(0)}% success
|
|
276
|
+
</Badge>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
279
|
+
<div>
|
|
280
|
+
<span className="text-muted-foreground">Total Calls:</span>{' '}
|
|
281
|
+
<span className="font-medium">{stat.totalCalls}</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div>
|
|
284
|
+
<span className="text-muted-foreground">Avg Latency:</span>{' '}
|
|
285
|
+
<span className="font-medium">
|
|
286
|
+
{stat.averageLatencyMs.toFixed(0)}ms
|
|
287
|
+
</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div>
|
|
290
|
+
<span className="text-muted-foreground">P95:</span>{' '}
|
|
291
|
+
<span className="font-medium">{stat.p95LatencyMs.toFixed(0)}ms</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div>
|
|
294
|
+
<span className="text-muted-foreground">P99:</span>{' '}
|
|
295
|
+
<span className="font-medium">{stat.p99LatencyMs.toFixed(0)}ms</span>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Anomaly card component
|
|
304
|
+
*/
|
|
305
|
+
function AnomalyCard({ anomaly }: { anomaly: SpecAnomaly }) {
|
|
306
|
+
const severityColors = {
|
|
307
|
+
low: 'text-amber-400',
|
|
308
|
+
medium: 'text-orange-400',
|
|
309
|
+
high: 'text-red-400',
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className="flex items-center justify-between rounded-lg border border-amber-500/30 bg-amber-500/5 p-3">
|
|
314
|
+
<div className="flex items-center gap-3">
|
|
315
|
+
<span
|
|
316
|
+
className={`text-lg ${severityColors[anomaly.severity]}`}
|
|
317
|
+
title={`${anomaly.severity} severity`}
|
|
318
|
+
>
|
|
319
|
+
{anomaly.severity === 'high'
|
|
320
|
+
? '🔴'
|
|
321
|
+
: anomaly.severity === 'medium'
|
|
322
|
+
? '🟠'
|
|
323
|
+
: '🟡'}
|
|
324
|
+
</span>
|
|
325
|
+
<div>
|
|
326
|
+
<p className="text-sm font-medium">{anomaly.description}</p>
|
|
327
|
+
<p className="text-muted-foreground text-xs">
|
|
328
|
+
{anomaly.operation.name} • {anomaly.metric}
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<Badge
|
|
333
|
+
variant={anomaly.severity === 'high' ? 'destructive' : 'secondary'}
|
|
334
|
+
className={
|
|
335
|
+
anomaly.severity === 'medium'
|
|
336
|
+
? 'border-amber-500/30 bg-amber-500/20 text-amber-400'
|
|
337
|
+
: ''
|
|
338
|
+
}
|
|
339
|
+
>
|
|
340
|
+
{anomaly.severity}
|
|
341
|
+
</Badge>
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Suggestion card component
|
|
348
|
+
*/
|
|
349
|
+
function SuggestionCard({
|
|
350
|
+
suggestion,
|
|
351
|
+
onApprove,
|
|
352
|
+
onReject,
|
|
353
|
+
}: {
|
|
354
|
+
suggestion: SpecSuggestion;
|
|
355
|
+
onApprove: (id: string) => void;
|
|
356
|
+
onReject: (id: string) => void;
|
|
357
|
+
}) {
|
|
358
|
+
const getStatusStyles = (status: string) => {
|
|
359
|
+
switch (status) {
|
|
360
|
+
case 'pending':
|
|
361
|
+
return {
|
|
362
|
+
variant: 'secondary' as const,
|
|
363
|
+
className: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
|
364
|
+
};
|
|
365
|
+
case 'approved':
|
|
366
|
+
return {
|
|
367
|
+
variant: 'default' as const,
|
|
368
|
+
className: 'bg-green-500/20 text-green-400 border-green-500/30',
|
|
369
|
+
};
|
|
370
|
+
case 'rejected':
|
|
371
|
+
return { variant: 'destructive' as const, className: '' };
|
|
372
|
+
default:
|
|
373
|
+
return { variant: 'secondary' as const, className: '' };
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const statusStyle = getStatusStyles(suggestion.status);
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className="rounded-lg border border-violet-500/30 bg-violet-500/5 p-4">
|
|
381
|
+
<div className="mb-2 flex items-start justify-between">
|
|
382
|
+
<div className="flex-1">
|
|
383
|
+
<div className="flex items-center gap-2">
|
|
384
|
+
<span className="text-lg">
|
|
385
|
+
{suggestion.intent.type === 'latency-regression'
|
|
386
|
+
? '⚡'
|
|
387
|
+
: suggestion.intent.type === 'error-spike'
|
|
388
|
+
? '🔥'
|
|
389
|
+
: '📉'}
|
|
390
|
+
</span>
|
|
391
|
+
<h4 className="font-medium">{suggestion.proposal.summary}</h4>
|
|
392
|
+
</div>
|
|
393
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
394
|
+
{suggestion.proposal.rationale}
|
|
395
|
+
</p>
|
|
396
|
+
</div>
|
|
397
|
+
<Badge variant={statusStyle.variant} className={statusStyle.className}>
|
|
398
|
+
{suggestion.status}
|
|
399
|
+
</Badge>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{suggestion.proposal.recommendedActions &&
|
|
403
|
+
suggestion.proposal.recommendedActions.length > 0 && (
|
|
404
|
+
<div className="mt-3">
|
|
405
|
+
<p className="mb-1 text-xs font-semibold text-violet-400 uppercase">
|
|
406
|
+
Recommended Actions
|
|
407
|
+
</p>
|
|
408
|
+
<ul className="list-inside list-disc space-y-1 text-xs">
|
|
409
|
+
{suggestion.proposal.recommendedActions
|
|
410
|
+
.slice(0, 3)
|
|
411
|
+
.map((action, i) => (
|
|
412
|
+
<li key={i}>{action}</li>
|
|
413
|
+
))}
|
|
414
|
+
</ul>
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
<div className="mt-3 flex items-center justify-between">
|
|
419
|
+
<div className="flex items-center gap-2 text-xs">
|
|
420
|
+
<span className="text-muted-foreground">
|
|
421
|
+
Confidence: {(suggestion.confidence * 100).toFixed(0)}%
|
|
422
|
+
</span>
|
|
423
|
+
<span className="text-muted-foreground">•</span>
|
|
424
|
+
<Badge variant="secondary">{suggestion.priority}</Badge>
|
|
425
|
+
</div>
|
|
426
|
+
{suggestion.status === 'pending' && (
|
|
427
|
+
<div className="flex items-center gap-2">
|
|
428
|
+
<Button
|
|
429
|
+
variant="outline"
|
|
430
|
+
size="sm"
|
|
431
|
+
onPress={() => onReject(suggestion.id)}
|
|
432
|
+
>
|
|
433
|
+
Reject
|
|
434
|
+
</Button>
|
|
435
|
+
<Button
|
|
436
|
+
variant="default"
|
|
437
|
+
size="sm"
|
|
438
|
+
onPress={() => onApprove(suggestion.id)}
|
|
439
|
+
>
|
|
440
|
+
Approve
|
|
441
|
+
</Button>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Optimization hint card component
|
|
451
|
+
*/
|
|
452
|
+
function HintCard({ hint }: { hint: OptimizationHint }) {
|
|
453
|
+
const categoryIcons = {
|
|
454
|
+
schema: '📐',
|
|
455
|
+
policy: '🔒',
|
|
456
|
+
performance: '⚡',
|
|
457
|
+
'error-handling': '🛡️',
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<div className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-3">
|
|
462
|
+
<div className="flex items-start gap-3">
|
|
463
|
+
<span className="text-lg">{categoryIcons[hint.category]}</span>
|
|
464
|
+
<div className="flex-1">
|
|
465
|
+
<p className="font-medium">{hint.summary}</p>
|
|
466
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
467
|
+
{hint.justification}
|
|
468
|
+
</p>
|
|
469
|
+
{hint.recommendedActions.length > 0 && (
|
|
470
|
+
<ul className="mt-2 list-inside list-disc text-xs">
|
|
471
|
+
{hint.recommendedActions.slice(0, 2).map((action, i) => (
|
|
472
|
+
<li key={i}>{action}</li>
|
|
473
|
+
))}
|
|
474
|
+
</ul>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|