sunpeak 0.7.11 → 0.8.4
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/README.md +2 -1
- package/bin/commands/deploy.mjs +18 -8
- package/bin/commands/dev.mjs +60 -4
- package/bin/commands/login.mjs +73 -55
- package/bin/commands/logout.mjs +26 -12
- package/bin/commands/mcp.mjs +1 -1
- package/bin/commands/pull.mjs +60 -39
- package/bin/commands/push.mjs +73 -49
- package/bin/commands/upgrade.mjs +203 -0
- package/bin/sunpeak.js +68 -35
- package/dist/chatgpt/chatgpt-simulator.d.ts +2 -1
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -14
- package/dist/index.js.map +1 -1
- package/dist/mcp/entry.cjs +41 -9
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js +42 -10
- package/dist/mcp/entry.js.map +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/{server-CziiHU7V.cjs → server-B9YgCQdS.cjs} +3 -2
- package/dist/{server-CziiHU7V.cjs.map → server-B9YgCQdS.cjs.map} +1 -1
- package/dist/{server-D8kyzuiq.js → server-DVmTC-SF.js} +3 -2
- package/dist/{server-D8kyzuiq.js.map → server-DVmTC-SF.js.map} +1 -1
- package/dist/style.css +62 -0
- package/dist/types/simulation.d.ts +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +78 -15
- package/template/.sunpeak/vite-env.d.ts +1 -0
- package/template/README.md +35 -20
- package/template/dist/albums.js +1 -1
- package/template/dist/albums.json +3 -2
- package/template/dist/carousel.js +1 -1
- package/template/dist/carousel.json +3 -2
- package/template/dist/confirmation.js +49 -0
- package/template/dist/confirmation.json +16 -0
- package/template/dist/counter.js +1 -1
- package/template/dist/counter.json +7 -2
- package/template/dist/map.js +1 -1
- package/template/dist/map.json +6 -3
- package/template/node_modules/.vite/deps/_metadata.json +19 -19
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/components/map/map-view.test.tsx +1 -1
- package/template/src/components/map/map-view.tsx +1 -1
- package/template/src/components/map/map.tsx +1 -1
- package/template/src/components/map/place-card.test.tsx +1 -1
- package/template/src/components/map/place-card.tsx +1 -1
- package/template/src/components/map/place-carousel.test.tsx +1 -1
- package/template/src/components/map/place-carousel.tsx +1 -1
- package/template/src/components/map/place-inspector.test.tsx +1 -1
- package/template/src/components/map/place-inspector.tsx +1 -1
- package/template/src/components/map/place-list.test.tsx +1 -1
- package/template/src/components/map/place-list.tsx +1 -1
- package/template/src/components/map/types.ts +18 -0
- package/template/src/resources/albums-resource.json +1 -1
- package/template/src/resources/carousel-resource.json +1 -1
- package/template/src/resources/confirmation-resource.json +12 -0
- package/template/src/resources/confirmation-resource.tsx +479 -0
- package/template/src/resources/counter-resource.json +4 -1
- package/template/src/resources/index.ts +39 -4
- package/template/src/resources/map-resource.json +7 -2
- package/template/src/simulations/albums-show-simulation.json +131 -0
- package/template/src/simulations/carousel-show-simulation.json +68 -0
- package/template/src/simulations/confirmation-diff-simulation.json +80 -0
- package/template/src/simulations/confirmation-post-simulation.json +56 -0
- package/template/src/simulations/confirmation-purchase-simulation.json +88 -0
- package/template/src/simulations/counter-show-simulation.json +20 -0
- package/template/src/simulations/index.ts +17 -12
- package/template/src/simulations/map-show-simulation.json +123 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +1 -1
- package/template/src/simulations/albums-simulation.ts +0 -147
- package/template/src/simulations/carousel-simulation.ts +0 -84
- package/template/src/simulations/counter-simulation.ts +0 -34
- package/template/src/simulations/map-simulation.ts +0 -154
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useWidgetProps,
|
|
4
|
+
useWidgetState,
|
|
5
|
+
useSafeArea,
|
|
6
|
+
useMaxHeight,
|
|
7
|
+
useUserAgent,
|
|
8
|
+
useDisplayMode,
|
|
9
|
+
useWidgetAPI,
|
|
10
|
+
} from 'sunpeak';
|
|
11
|
+
import { Button } from '@openai/apps-sdk-ui/components/Button';
|
|
12
|
+
import { ExpandLg } from '@openai/apps-sdk-ui/components/Icon';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Production-ready Confirmation Resource
|
|
16
|
+
*
|
|
17
|
+
* A flexible confirmation dialog that adapts to various use cases:
|
|
18
|
+
* - Purchase confirmations (items, totals, payment)
|
|
19
|
+
* - Code change confirmations (file changes with diffs)
|
|
20
|
+
* - Social media post confirmations (content preview)
|
|
21
|
+
* - Booking confirmations (details, dates, prices)
|
|
22
|
+
* - Generic action confirmations (simple approve/reject)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Type Definitions
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/** A key-value detail row */
|
|
30
|
+
interface Detail {
|
|
31
|
+
label: string;
|
|
32
|
+
value: string;
|
|
33
|
+
/** Optional sublabel/description */
|
|
34
|
+
sublabel?: string;
|
|
35
|
+
/** Highlight this row (e.g., for totals) */
|
|
36
|
+
emphasis?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** An item with optional image and metadata (for purchases, lists) */
|
|
40
|
+
interface Item {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
subtitle?: string;
|
|
44
|
+
/** Image URL */
|
|
45
|
+
image?: string;
|
|
46
|
+
/** Right-aligned value (e.g., price, quantity) */
|
|
47
|
+
value?: string;
|
|
48
|
+
/** Small badge text (e.g., "New", "Sale") */
|
|
49
|
+
badge?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A code/file change entry */
|
|
53
|
+
interface Change {
|
|
54
|
+
id: string;
|
|
55
|
+
type: 'create' | 'modify' | 'delete' | 'action';
|
|
56
|
+
/** File path or identifier */
|
|
57
|
+
path?: string;
|
|
58
|
+
description: string;
|
|
59
|
+
details?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Alert/warning message */
|
|
63
|
+
interface Alert {
|
|
64
|
+
type: 'info' | 'warning' | 'error' | 'success';
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Content section - supports multiple display types */
|
|
69
|
+
interface Section {
|
|
70
|
+
/** Optional section title */
|
|
71
|
+
title?: string;
|
|
72
|
+
/** Section content type */
|
|
73
|
+
type: 'details' | 'items' | 'changes' | 'preview' | 'summary';
|
|
74
|
+
/** Content data (type depends on section type) */
|
|
75
|
+
content: Detail[] | Item[] | Change[] | string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Tool call configuration for domain-specific confirmation actions */
|
|
79
|
+
interface ConfirmationTool {
|
|
80
|
+
/** Tool name to call (e.g., "complete_purchase", "publish_post") */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Additional arguments to pass to the tool */
|
|
83
|
+
arguments?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ConfirmationData extends Record<string, unknown> {
|
|
87
|
+
/** Main title */
|
|
88
|
+
title: string;
|
|
89
|
+
/** Optional description below title */
|
|
90
|
+
description?: string;
|
|
91
|
+
/** Content sections */
|
|
92
|
+
sections?: Section[];
|
|
93
|
+
/** Alert messages to display */
|
|
94
|
+
alerts?: Alert[];
|
|
95
|
+
/** Accept button label */
|
|
96
|
+
acceptLabel?: string;
|
|
97
|
+
/** Reject button label */
|
|
98
|
+
rejectLabel?: string;
|
|
99
|
+
/** Use danger styling for accept button (for destructive actions) */
|
|
100
|
+
acceptDanger?: boolean;
|
|
101
|
+
/** Message shown after accepting */
|
|
102
|
+
acceptedMessage?: string;
|
|
103
|
+
/** Message shown after rejecting */
|
|
104
|
+
rejectedMessage?: string;
|
|
105
|
+
/** Domain-specific tool to call on confirmation */
|
|
106
|
+
confirmationTool?: ConfirmationTool;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ConfirmationState extends Record<string, unknown> {
|
|
110
|
+
decision?: 'accepted' | 'rejected' | null;
|
|
111
|
+
decidedAt?: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Section Renderers
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
const changeTypeConfig = {
|
|
119
|
+
create: { icon: '+', color: '#16a34a', bg: '#f0fdf4' },
|
|
120
|
+
modify: { icon: '~', color: '#ca8a04', bg: '#fefce8' },
|
|
121
|
+
delete: { icon: '−', color: '#dc2626', bg: '#fef2f2' },
|
|
122
|
+
action: { icon: '→', color: '#2563eb', bg: '#eff6ff' },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const alertTypeConfig = {
|
|
126
|
+
info: { icon: 'ℹ', bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af' },
|
|
127
|
+
warning: { icon: '⚠', bg: '#fefce8', border: '#fde047', text: '#a16207' },
|
|
128
|
+
error: { icon: '✕', bg: '#fef2f2', border: '#fecaca', text: '#b91c1c' },
|
|
129
|
+
success: { icon: '✓', bg: '#f0fdf4', border: '#bbf7d0', text: '#15803d' },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function DetailsSection({ content }: { content: Detail[] }) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
{content.map((detail, i) => (
|
|
136
|
+
<div
|
|
137
|
+
key={i}
|
|
138
|
+
className={`flex justify-between items-start gap-4 ${
|
|
139
|
+
detail.emphasis ? 'font-semibold pt-2 border-t border-subtle' : ''
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
<div className="flex-1 min-w-0">
|
|
143
|
+
<span className={detail.emphasis ? 'text-primary' : 'text-secondary'}>
|
|
144
|
+
{detail.label}
|
|
145
|
+
</span>
|
|
146
|
+
{detail.sublabel && <p className="text-xs text-secondary mt-0.5">{detail.sublabel}</p>}
|
|
147
|
+
</div>
|
|
148
|
+
<span className="text-primary flex-shrink-0">{detail.value}</span>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ItemsSection({ content }: { content: Item[] }) {
|
|
156
|
+
return (
|
|
157
|
+
<div className="space-y-3">
|
|
158
|
+
{content.map((item) => (
|
|
159
|
+
<div key={item.id} className="flex items-center gap-3 p-2 rounded-lg bg-surface-secondary">
|
|
160
|
+
{item.image && (
|
|
161
|
+
<img
|
|
162
|
+
src={item.image}
|
|
163
|
+
alt={item.title}
|
|
164
|
+
className="w-12 h-12 rounded-lg object-cover flex-shrink-0"
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
<div className="flex-1 min-w-0">
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<span className="text-sm font-medium text-primary truncate">{item.title}</span>
|
|
170
|
+
{item.badge && (
|
|
171
|
+
<span className="px-1.5 py-0.5 text-xs rounded bg-primary text-on-primary">
|
|
172
|
+
{item.badge}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
{item.subtitle && <p className="text-xs text-secondary truncate">{item.subtitle}</p>}
|
|
177
|
+
</div>
|
|
178
|
+
{item.value && (
|
|
179
|
+
<span className="text-sm font-medium text-primary flex-shrink-0">{item.value}</span>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ChangesSection({ content }: { content: Change[] }) {
|
|
188
|
+
return (
|
|
189
|
+
<ul className="space-y-2">
|
|
190
|
+
{content.map((change) => {
|
|
191
|
+
const config = changeTypeConfig[change.type];
|
|
192
|
+
return (
|
|
193
|
+
<li
|
|
194
|
+
key={change.id}
|
|
195
|
+
className="rounded-lg border border-subtle p-3"
|
|
196
|
+
style={{ backgroundColor: config.bg }}
|
|
197
|
+
>
|
|
198
|
+
<div className="flex items-start gap-3">
|
|
199
|
+
<span
|
|
200
|
+
className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded font-mono font-bold bg-white"
|
|
201
|
+
style={{
|
|
202
|
+
color: config.color,
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderStyle: 'solid',
|
|
205
|
+
borderColor: config.color,
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{config.icon}
|
|
209
|
+
</span>
|
|
210
|
+
<div className="flex-1 min-w-0">
|
|
211
|
+
{change.path && (
|
|
212
|
+
<code className="block text-xs text-secondary font-mono truncate mb-1">
|
|
213
|
+
{change.path}
|
|
214
|
+
</code>
|
|
215
|
+
)}
|
|
216
|
+
<p className="text-sm text-[#000000]">{change.description}</p>
|
|
217
|
+
{change.details && <p className="mt-1 text-xs text-secondary">{change.details}</p>}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</li>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</ul>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function PreviewSection({ content }: { content: string }) {
|
|
228
|
+
return (
|
|
229
|
+
<div className="p-4 rounded-lg bg-surface-secondary border border-subtle">
|
|
230
|
+
<p className="text-sm text-primary whitespace-pre-wrap">{content}</p>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function SummarySection({ content }: { content: Detail[] }) {
|
|
236
|
+
return (
|
|
237
|
+
<div className="p-3 rounded-lg bg-surface-secondary space-y-1">
|
|
238
|
+
{content.map((item, i) => (
|
|
239
|
+
<div
|
|
240
|
+
key={i}
|
|
241
|
+
className={`flex justify-between items-center ${
|
|
242
|
+
item.emphasis ? 'font-semibold text-lg pt-2 border-t border-subtle mt-2' : 'text-sm'
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
<span className={item.emphasis ? 'text-primary' : 'text-secondary'}>{item.label}</span>
|
|
246
|
+
<span className="text-primary">{item.value}</span>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function Section({ section }: { section: Section }) {
|
|
254
|
+
const renderContent = () => {
|
|
255
|
+
switch (section.type) {
|
|
256
|
+
case 'details':
|
|
257
|
+
return <DetailsSection content={section.content as Detail[]} />;
|
|
258
|
+
case 'items':
|
|
259
|
+
return <ItemsSection content={section.content as Item[]} />;
|
|
260
|
+
case 'changes':
|
|
261
|
+
return <ChangesSection content={section.content as Change[]} />;
|
|
262
|
+
case 'preview':
|
|
263
|
+
return <PreviewSection content={section.content as string} />;
|
|
264
|
+
case 'summary':
|
|
265
|
+
return <SummarySection content={section.content as Detail[]} />;
|
|
266
|
+
default:
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
{section.title && (
|
|
274
|
+
<h2 className="text-sm font-medium text-secondary uppercase tracking-wide">
|
|
275
|
+
{section.title}
|
|
276
|
+
</h2>
|
|
277
|
+
)}
|
|
278
|
+
{renderContent()}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function AlertBanner({ alert }: { alert: Alert }) {
|
|
284
|
+
const config = alertTypeConfig[alert.type];
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
className="flex items-start gap-2 p-3 rounded-lg"
|
|
288
|
+
style={{
|
|
289
|
+
backgroundColor: config.bg,
|
|
290
|
+
borderWidth: 1,
|
|
291
|
+
borderStyle: 'solid',
|
|
292
|
+
borderColor: config.border,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<span className="flex-shrink-0" style={{ color: config.text }}>
|
|
296
|
+
{config.icon}
|
|
297
|
+
</span>
|
|
298
|
+
<p className="text-sm" style={{ color: config.text }}>
|
|
299
|
+
{alert.message}
|
|
300
|
+
</p>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// Main Component
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
export const ConfirmationResource = React.forwardRef<HTMLDivElement>((_props, ref) => {
|
|
310
|
+
const data = useWidgetProps<ConfirmationData>(() => ({
|
|
311
|
+
title: 'Confirm',
|
|
312
|
+
sections: [],
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const [widgetState, setWidgetState] = useWidgetState<ConfirmationState>(() => ({
|
|
316
|
+
decision: null,
|
|
317
|
+
decidedAt: null,
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const safeArea = useSafeArea();
|
|
321
|
+
const maxHeight = useMaxHeight();
|
|
322
|
+
const userAgent = useUserAgent();
|
|
323
|
+
const displayMode = useDisplayMode();
|
|
324
|
+
const api = useWidgetAPI();
|
|
325
|
+
|
|
326
|
+
const hasTouch = userAgent?.capabilities.touch ?? false;
|
|
327
|
+
const decision = widgetState?.decision ?? null;
|
|
328
|
+
const isFullscreen = displayMode === 'fullscreen';
|
|
329
|
+
|
|
330
|
+
const handleRequestFullscreen = () => {
|
|
331
|
+
api?.requestDisplayMode?.({ mode: 'fullscreen' });
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleAccept = () => {
|
|
335
|
+
const decidedAt = new Date().toISOString();
|
|
336
|
+
setWidgetState({
|
|
337
|
+
decision: 'accepted',
|
|
338
|
+
decidedAt,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const tool = data.confirmationTool;
|
|
342
|
+
if (tool) {
|
|
343
|
+
console.log('callTool', {
|
|
344
|
+
name: tool.name,
|
|
345
|
+
arguments: {
|
|
346
|
+
...tool.arguments,
|
|
347
|
+
confirmed: true,
|
|
348
|
+
decidedAt,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const handleReject = () => {
|
|
355
|
+
const decidedAt = new Date().toISOString();
|
|
356
|
+
setWidgetState({
|
|
357
|
+
decision: 'rejected',
|
|
358
|
+
decidedAt,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const tool = data.confirmationTool;
|
|
362
|
+
if (tool) {
|
|
363
|
+
console.log('callTool', {
|
|
364
|
+
name: tool.name,
|
|
365
|
+
arguments: {
|
|
366
|
+
...tool.arguments,
|
|
367
|
+
confirmed: false,
|
|
368
|
+
decidedAt,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const acceptLabel = data.acceptLabel ?? 'Confirm';
|
|
375
|
+
const rejectLabel = data.rejectLabel ?? 'Cancel';
|
|
376
|
+
const acceptedMessage = data.acceptedMessage ?? 'Confirmed';
|
|
377
|
+
const rejectedMessage = data.rejectedMessage ?? 'Cancelled';
|
|
378
|
+
const sections = data.sections ?? [];
|
|
379
|
+
const alerts = data.alerts ?? [];
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<div
|
|
383
|
+
ref={ref}
|
|
384
|
+
className="flex flex-col"
|
|
385
|
+
style={{
|
|
386
|
+
paddingTop: `${safeArea?.insets.top ?? 0}px`,
|
|
387
|
+
paddingBottom: `${safeArea?.insets.bottom ?? 0}px`,
|
|
388
|
+
paddingLeft: `${safeArea?.insets.left ?? 0}px`,
|
|
389
|
+
paddingRight: `${safeArea?.insets.right ?? 0}px`,
|
|
390
|
+
maxHeight: maxHeight ?? undefined,
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
{/* Header */}
|
|
394
|
+
<div className="px-4 pt-4 pb-3 border-b border-subtle">
|
|
395
|
+
<div className="flex items-start justify-between gap-2">
|
|
396
|
+
<div className="flex-1 min-w-0">
|
|
397
|
+
<h1 className="text-xl font-semibold text-primary">{data.title}</h1>
|
|
398
|
+
{data.description && <p className="mt-1 text-sm text-secondary">{data.description}</p>}
|
|
399
|
+
</div>
|
|
400
|
+
{!isFullscreen && (
|
|
401
|
+
<Button
|
|
402
|
+
variant="ghost"
|
|
403
|
+
color="secondary"
|
|
404
|
+
size="sm"
|
|
405
|
+
onClick={handleRequestFullscreen}
|
|
406
|
+
aria-label="Enter fullscreen"
|
|
407
|
+
className="flex-shrink-0"
|
|
408
|
+
>
|
|
409
|
+
<ExpandLg className="h-4 w-4" aria-hidden="true" />
|
|
410
|
+
</Button>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{/* Content */}
|
|
416
|
+
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-4">
|
|
417
|
+
{/* Alerts */}
|
|
418
|
+
{alerts.length > 0 && (
|
|
419
|
+
<div className="space-y-2">
|
|
420
|
+
{alerts.map((alert, i) => (
|
|
421
|
+
<AlertBanner key={i} alert={alert} />
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{/* Sections */}
|
|
427
|
+
{sections.length === 0 ? (
|
|
428
|
+
<p className="text-secondary text-center py-8">Nothing to confirm</p>
|
|
429
|
+
) : (
|
|
430
|
+
sections.map((section, i) => <Section key={i} section={section} />)
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{/* Footer with Actions */}
|
|
435
|
+
<div className="px-4 py-3 border-t border-subtle bg-surface">
|
|
436
|
+
{decision === null ? (
|
|
437
|
+
<div className="flex gap-3">
|
|
438
|
+
<Button
|
|
439
|
+
variant="outline"
|
|
440
|
+
color="secondary"
|
|
441
|
+
onClick={handleReject}
|
|
442
|
+
size={hasTouch ? 'lg' : 'md'}
|
|
443
|
+
className="flex-1"
|
|
444
|
+
>
|
|
445
|
+
{rejectLabel}
|
|
446
|
+
</Button>
|
|
447
|
+
<Button
|
|
448
|
+
variant="solid"
|
|
449
|
+
color={data.acceptDanger ? 'danger' : 'primary'}
|
|
450
|
+
onClick={handleAccept}
|
|
451
|
+
size={hasTouch ? 'lg' : 'md'}
|
|
452
|
+
className="flex-1"
|
|
453
|
+
>
|
|
454
|
+
{acceptLabel}
|
|
455
|
+
</Button>
|
|
456
|
+
</div>
|
|
457
|
+
) : (
|
|
458
|
+
<div className="flex flex-col items-center gap-1">
|
|
459
|
+
<div
|
|
460
|
+
className="flex items-center justify-center gap-2"
|
|
461
|
+
style={{ color: decision === 'accepted' ? '#16a34a' : '#dc2626' }}
|
|
462
|
+
>
|
|
463
|
+
<span className="text-lg">{decision === 'accepted' ? '✓' : '✗'}</span>
|
|
464
|
+
<span className="font-medium">
|
|
465
|
+
{decision === 'accepted' ? acceptedMessage : rejectedMessage}
|
|
466
|
+
</span>
|
|
467
|
+
</div>
|
|
468
|
+
{widgetState?.decidedAt && (
|
|
469
|
+
<span className="text-xs text-secondary">
|
|
470
|
+
{new Date(widgetState.decidedAt).toLocaleString()}
|
|
471
|
+
</span>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
ConfirmationResource.displayName = 'ConfirmationResource';
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"description": "Show a simple counter tool widget",
|
|
5
5
|
"mimeType": "text/html+skybridge",
|
|
6
6
|
"_meta": {
|
|
7
|
-
"openai/widgetDomain": "https://sunpeak.ai"
|
|
7
|
+
"openai/widgetDomain": "https://sunpeak.ai",
|
|
8
|
+
"openai/widgetCSP": {
|
|
9
|
+
"resource_domains": ["https://cdn.openai.com"]
|
|
10
|
+
}
|
|
8
11
|
}
|
|
9
12
|
}
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discovers and re-exports all resource components.
|
|
3
|
+
*
|
|
4
|
+
* Discovers all *-resource.tsx files and exports their component
|
|
5
|
+
* with a PascalCase name (e.g., counter-resource.tsx -> CounterResource).
|
|
6
|
+
*
|
|
7
|
+
* Supports both export styles:
|
|
8
|
+
* - Default export: export default MyComponent
|
|
9
|
+
* - Named export: export const CounterResource = ...
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Auto-discover all resource component files
|
|
13
|
+
const resourceModules = import.meta.glob('./*-resource.tsx', { eager: true });
|
|
14
|
+
|
|
15
|
+
// Build exports object from discovered files
|
|
16
|
+
const resources: Record<string, React.ComponentType> = {};
|
|
17
|
+
|
|
18
|
+
for (const [path, module] of Object.entries(resourceModules)) {
|
|
19
|
+
// Extract key from path: './counter-resource.tsx' -> 'counter'
|
|
20
|
+
const match = path.match(/\.\/(.+)-resource\.tsx$/);
|
|
21
|
+
const key = match?.[1];
|
|
22
|
+
if (!key) continue;
|
|
23
|
+
|
|
24
|
+
// Convert to PascalCase and append 'Resource': 'counter' -> 'CounterResource'
|
|
25
|
+
const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
|
|
26
|
+
const exportName = `${pascalKey}Resource`;
|
|
27
|
+
|
|
28
|
+
const mod = module as Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
// Try default export first, then named export matching the expected name
|
|
31
|
+
const component = mod.default ?? mod[exportName];
|
|
32
|
+
|
|
33
|
+
// Accept functions (regular components) or objects (forwardRef/memo components)
|
|
34
|
+
if (component && (typeof component === 'function' || typeof component === 'object')) {
|
|
35
|
+
resources[exportName] = component as React.ComponentType;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default resources;
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"_meta": {
|
|
7
7
|
"openai/widgetDomain": "https://sunpeak.ai",
|
|
8
8
|
"openai/widgetCSP": {
|
|
9
|
-
"connect_domains": ["https://api.mapbox.com"],
|
|
10
|
-
"resource_domains": [
|
|
9
|
+
"connect_domains": ["https://api.mapbox.com", "https://events.mapbox.com"],
|
|
10
|
+
"resource_domains": [
|
|
11
|
+
"https://*.oaistatic.com",
|
|
12
|
+
"https://cdn.openai.com",
|
|
13
|
+
"https://api.mapbox.com",
|
|
14
|
+
"https://events.mapbox.com"
|
|
15
|
+
]
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"userMessage": "Pizza time",
|
|
3
|
+
"tool": {
|
|
4
|
+
"name": "show-albums",
|
|
5
|
+
"description": "Show photo albums",
|
|
6
|
+
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
|
|
7
|
+
"title": "Show Albums",
|
|
8
|
+
"annotations": { "readOnlyHint": true },
|
|
9
|
+
"_meta": {
|
|
10
|
+
"openai/toolInvocation/invoking": "Loading albums",
|
|
11
|
+
"openai/toolInvocation/invoked": "Album loaded",
|
|
12
|
+
"openai/widgetAccessible": true,
|
|
13
|
+
"openai/resultCanProduceWidget": true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"toolCall": {
|
|
17
|
+
"structuredContent": {
|
|
18
|
+
"albums": [
|
|
19
|
+
{
|
|
20
|
+
"id": "summer-escape",
|
|
21
|
+
"title": "Summer Slice",
|
|
22
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
|
|
23
|
+
"photos": [
|
|
24
|
+
{
|
|
25
|
+
"id": "s1",
|
|
26
|
+
"title": "Waves",
|
|
27
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "s2",
|
|
31
|
+
"title": "Palm trees",
|
|
32
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "s3",
|
|
36
|
+
"title": "Sunset",
|
|
37
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "city-lights",
|
|
43
|
+
"title": "Pepperoni Nights",
|
|
44
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png",
|
|
45
|
+
"photos": [
|
|
46
|
+
{
|
|
47
|
+
"id": "c1",
|
|
48
|
+
"title": "Downtown",
|
|
49
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "c2",
|
|
53
|
+
"title": "Neon",
|
|
54
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "c3",
|
|
58
|
+
"title": "Streets",
|
|
59
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "into-the-woods",
|
|
65
|
+
"title": "Truffle Forest",
|
|
66
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png",
|
|
67
|
+
"photos": [
|
|
68
|
+
{
|
|
69
|
+
"id": "n1",
|
|
70
|
+
"title": "Forest path",
|
|
71
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "n2",
|
|
75
|
+
"title": "Misty",
|
|
76
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "n3",
|
|
80
|
+
"title": "Waterfall",
|
|
81
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "pizza-tour",
|
|
87
|
+
"title": "Pizza tour",
|
|
88
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
|
|
89
|
+
"photos": [
|
|
90
|
+
{
|
|
91
|
+
"id": "tonys-pizza-napoletana",
|
|
92
|
+
"title": "Tony's Pizza Napoletana",
|
|
93
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "golden-boy-pizza",
|
|
97
|
+
"title": "Golden Boy Pizza",
|
|
98
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "pizzeria-delfina-mission",
|
|
102
|
+
"title": "Pizzeria Delfina (Mission)",
|
|
103
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "ragazza",
|
|
107
|
+
"title": "Ragazza",
|
|
108
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "del-popolo",
|
|
112
|
+
"title": "Del Popolo",
|
|
113
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "square-pie-guys",
|
|
117
|
+
"title": "Square Pie Guys",
|
|
118
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "zero-zero",
|
|
122
|
+
"title": "Zero Zero",
|
|
123
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"_meta": {}
|
|
130
|
+
}
|
|
131
|
+
}
|