deepspace 0.3.9 → 0.3.11

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.
Files changed (120) hide show
  1. package/dist/cli.js +96 -18
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +283 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +172 -2
  7. package/dist/server.js +581 -1
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +252 -2
  10. package/dist/worker.js +588 -1
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. package/scripts/add-feature.cjs +654 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Integration Test Page — browse and test all integration endpoints.
3
+ *
4
+ * Fetches the catalog from the API worker, renders typed form fields
5
+ * from JSON Schema, and shows results.
6
+ *
7
+ * data-testid attributes for Playwright:
8
+ * integration-endpoint, integration-submit, integration-result,
9
+ * integration-error, integration-loading, integration-catalog
10
+ */
11
+
12
+ import { useState, useEffect, useMemo } from 'react'
13
+ import { integration } from 'deepspace'
14
+ import { useAuth } from 'deepspace'
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ interface CatalogEntry {
21
+ endpoint: string
22
+ billing: { model: string; baseCost: number; currency: string }
23
+ inputSchema?: JsonSchema
24
+ example?: Record<string, unknown>
25
+ }
26
+
27
+ interface JsonSchema {
28
+ type?: string
29
+ properties?: Record<string, JsonSchemaProperty>
30
+ required?: string[]
31
+ }
32
+
33
+ interface JsonSchemaProperty {
34
+ type?: string
35
+ enum?: unknown[]
36
+ default?: unknown
37
+ description?: string
38
+ minimum?: number
39
+ maximum?: number
40
+ minLength?: number
41
+ maxLength?: number
42
+ items?: JsonSchemaProperty
43
+ }
44
+
45
+ type Catalog = Record<string, CatalogEntry[]>
46
+
47
+ // ============================================================================
48
+ // Example Generation — build a complete example from schema + catalog defaults
49
+ // ============================================================================
50
+
51
+ function buildFullExample(schema?: JsonSchema | null, catalogExample?: Record<string, unknown>): Record<string, unknown> {
52
+ const example: Record<string, unknown> = { ...catalogExample }
53
+ if (!schema?.properties) return example
54
+
55
+ for (const [key, prop] of Object.entries(schema.properties)) {
56
+ if (example[key] !== undefined) continue // already has a value from catalog
57
+
58
+ if (prop.default !== undefined) {
59
+ example[key] = prop.default
60
+ } else if (prop.type === 'array' && prop.items) {
61
+ example[key] = [buildExampleItem(prop.items)]
62
+ } else if (prop.type === 'string' && prop.enum) {
63
+ example[key] = prop.enum[0]
64
+ }
65
+ // Skip optional fields without defaults — user fills them in
66
+ }
67
+ return example
68
+ }
69
+
70
+ function buildExampleItem(items: any): unknown {
71
+ if (items.type === 'string') return 'example'
72
+ if (items.type === 'number' || items.type === 'integer') return 0
73
+ if (items.type === 'object' && items.properties) {
74
+ const obj: Record<string, unknown> = {}
75
+ for (const [k, v] of Object.entries(items.properties as Record<string, any>)) {
76
+ if (v.default !== undefined) obj[k] = v.default
77
+ else if (v.enum) obj[k] = v.enum[0]
78
+ else if (v.type === 'string') obj[k] = k
79
+ else if (v.type === 'number' || v.type === 'integer') obj[k] = v.minimum ?? 0
80
+ else if (v.type === 'boolean') obj[k] = false
81
+ else if (v.anyOf) {
82
+ const str = v.anyOf.find((a: any) => a.type === 'string')
83
+ obj[k] = str ? k : null
84
+ }
85
+ }
86
+ return obj
87
+ }
88
+ return null
89
+ }
90
+
91
+ // ============================================================================
92
+ // Schema Form — auto-generates form fields from JSON Schema
93
+ // ============================================================================
94
+
95
+ function SchemaForm({
96
+ schema,
97
+ values,
98
+ onChange,
99
+ }: {
100
+ schema: JsonSchema
101
+ values: Record<string, unknown>
102
+ onChange: (values: Record<string, unknown>) => void
103
+ }) {
104
+ const properties = schema.properties ?? {}
105
+ const required = new Set(schema.required ?? [])
106
+
107
+ const updateField = (key: string, value: unknown) => {
108
+ onChange({ ...values, [key]: value })
109
+ }
110
+
111
+ return (
112
+ <div className="space-y-3">
113
+ {Object.entries(properties).map(([key, prop]) => (
114
+ <div key={key}>
115
+ <label className="block text-sm font-medium text-foreground mb-1">
116
+ {key}
117
+ {required.has(key) && <span className="text-destructive ml-0.5">*</span>}
118
+ {prop.type && <span className="ml-1 text-xs text-muted-foreground">({prop.type})</span>}
119
+ </label>
120
+ <FieldInput
121
+ prop={prop}
122
+ value={values[key]}
123
+ onChange={(v) => updateField(key, v)}
124
+ />
125
+ </div>
126
+ ))}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ function FieldInput({
132
+ prop,
133
+ value,
134
+ onChange,
135
+ }: {
136
+ prop: JsonSchemaProperty
137
+ value: unknown
138
+ onChange: (v: unknown) => void
139
+ }) {
140
+ // Enum → select
141
+ if (prop.enum) {
142
+ return (
143
+ <select
144
+ value={String(value ?? prop.default ?? '')}
145
+ onChange={(e) => onChange(e.target.value)}
146
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground"
147
+ >
148
+ {prop.enum.map((v) => (
149
+ <option key={String(v)} value={String(v)}>{String(v)}</option>
150
+ ))}
151
+ </select>
152
+ )
153
+ }
154
+
155
+ // Boolean → checkbox
156
+ if (prop.type === 'boolean') {
157
+ return (
158
+ <input
159
+ type="checkbox"
160
+ checked={Boolean(value ?? prop.default ?? false)}
161
+ onChange={(e) => onChange(e.target.checked)}
162
+ className="rounded border-border"
163
+ />
164
+ )
165
+ }
166
+
167
+ // Number → number input
168
+ if (prop.type === 'number' || prop.type === 'integer') {
169
+ return (
170
+ <input
171
+ type="number"
172
+ value={value != null ? String(value) : String(prop.default ?? '')}
173
+ min={prop.minimum}
174
+ max={prop.maximum}
175
+ onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
176
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground"
177
+ />
178
+ )
179
+ }
180
+
181
+ // Array / Object → JSON textarea
182
+ if (prop.type === 'array' || prop.type === 'object') {
183
+ // Generate a meaningful placeholder for arrays of objects
184
+ let defaultValue = prop.default ?? (prop.type === 'array' ? [] : {})
185
+ if (prop.type === 'array' && Array.isArray(defaultValue) && defaultValue.length === 0 && prop.items?.properties) {
186
+ // Build an example object from the items schema
187
+ const exampleItem: Record<string, unknown> = {}
188
+ for (const [k, v] of Object.entries(prop.items.properties as Record<string, any>)) {
189
+ if (v.default !== undefined) exampleItem[k] = v.default
190
+ else if (v.enum) exampleItem[k] = v.enum[0]
191
+ else if (v.type === 'string') exampleItem[k] = k
192
+ else if (v.type === 'number' || v.type === 'integer') exampleItem[k] = v.minimum ?? 0
193
+ else if (v.type === 'boolean') exampleItem[k] = false
194
+ else if (v.anyOf) {
195
+ // Pick the simplest type from anyOf
196
+ const stringType = v.anyOf.find((a: any) => a.type === 'string')
197
+ if (stringType) exampleItem[k] = k
198
+ else exampleItem[k] = null
199
+ }
200
+ }
201
+ defaultValue = [exampleItem]
202
+ }
203
+ const strValue = typeof value === 'string' ? value : JSON.stringify(value ?? defaultValue, null, 2)
204
+ return (
205
+ <textarea
206
+ value={strValue}
207
+ onChange={(e) => {
208
+ try { onChange(JSON.parse(e.target.value)) } catch { onChange(e.target.value) }
209
+ }}
210
+ rows={Math.min(strValue.split('\n').length + 1, 8)}
211
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground font-mono"
212
+ />
213
+ )
214
+ }
215
+
216
+ // Default → text input
217
+ return (
218
+ <input
219
+ type="text"
220
+ value={String(value ?? prop.default ?? '')}
221
+ onChange={(e) => onChange(e.target.value || undefined)}
222
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground"
223
+ placeholder={prop.description ?? `Enter ${prop.type ?? 'value'}`}
224
+ />
225
+ )
226
+ }
227
+
228
+ // ============================================================================
229
+ // Page
230
+ // ============================================================================
231
+
232
+ export default function IntegrationTestPage() {
233
+ const { isSignedIn } = useAuth()
234
+ const [catalog, setCatalog] = useState<Catalog | null>(null)
235
+ const [endpoint, setEndpoint] = useState('openai/chat-completion')
236
+ const [formValues, setFormValues] = useState<Record<string, unknown>>({})
237
+ const [rawMode, setRawMode] = useState(false)
238
+ const [rawBody, setRawBody] = useState('{}')
239
+ const [result, setResult] = useState<any>(null)
240
+ const [loading, setLoading] = useState(false)
241
+ const [error, setError] = useState<string | null>(null)
242
+
243
+ // Fetch catalog on mount
244
+ useEffect(() => {
245
+ integration.get<{ integrations: Catalog }>('').then((res) => {
246
+ if (res.success && res.data) {
247
+ setCatalog((res.data as any).integrations ?? res.data)
248
+ }
249
+ })
250
+ }, [])
251
+
252
+ // Find current endpoint's catalog entry
253
+ const currentEntry = useMemo(() => {
254
+ if (!catalog) return null
255
+ const [name, ep] = endpoint.split('/')
256
+ return catalog[name]?.find((e) => e.endpoint === ep) ?? null
257
+ }, [catalog, endpoint])
258
+
259
+ const selectEndpoint = (name: string, ep: string) => {
260
+ setEndpoint(`${name}/${ep}`)
261
+ setResult(null)
262
+ setError(null)
263
+ const entry = catalog?.[name]?.find((e) => e.endpoint === ep)
264
+ const fullExample = buildFullExample(entry?.inputSchema, entry?.example ?? {})
265
+ setFormValues(fullExample)
266
+ setRawBody(JSON.stringify(fullExample, null, 2))
267
+ }
268
+
269
+ const handleSubmit = async () => {
270
+ setLoading(true)
271
+ setError(null)
272
+ setResult(null)
273
+
274
+ try {
275
+ const body = rawMode ? JSON.parse(rawBody) : formValues
276
+ const res = await integration.post(endpoint, body)
277
+ setResult(res)
278
+ } catch (err: any) {
279
+ setError(err.message)
280
+ } finally {
281
+ setLoading(false)
282
+ }
283
+ }
284
+
285
+ return (
286
+ <div className="mx-auto max-w-4xl px-6 py-10">
287
+ <h1 className="text-2xl font-bold text-foreground mb-2">Integration Tester</h1>
288
+ <p className="text-sm text-muted-foreground mb-6">
289
+ {isSignedIn ? 'Signed in — API calls will use your JWT.' : 'Not signed in — developer billing will be used.'}
290
+ </p>
291
+
292
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
293
+ {/* Catalog */}
294
+ <div data-testid="integration-catalog" className="md:col-span-1 space-y-3 max-h-[70vh] overflow-y-auto">
295
+ {catalog ? (
296
+ Object.entries(catalog).sort(([a], [b]) => a.localeCompare(b)).map(([name, endpoints]) => (
297
+ <div key={name}>
298
+ <div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">{name}</div>
299
+ <div className="space-y-0.5">
300
+ {endpoints.map((ep) => (
301
+ <button
302
+ key={ep.endpoint}
303
+ onClick={() => selectEndpoint(name, ep.endpoint)}
304
+ className={`block w-full text-left px-2 py-1 rounded text-sm transition-colors ${
305
+ endpoint === `${name}/${ep.endpoint}`
306
+ ? 'bg-primary/20 text-primary'
307
+ : 'text-foreground hover:bg-secondary'
308
+ }`}
309
+ >
310
+ {ep.endpoint}
311
+ <span className="ml-1 text-xs text-muted-foreground">
312
+ {ep.billing.model === 'per_request' ? `$${ep.billing.baseCost}` : ep.billing.model}
313
+ </span>
314
+ </button>
315
+ ))}
316
+ </div>
317
+ </div>
318
+ ))
319
+ ) : (
320
+ <div className="text-sm text-muted-foreground">Loading catalog...</div>
321
+ )}
322
+ </div>
323
+
324
+ {/* Request / Response */}
325
+ <div className="md:col-span-2 space-y-4">
326
+ <div className="flex items-center justify-between">
327
+ <div className="flex-1">
328
+ <label className="block text-sm font-medium text-foreground mb-1">Endpoint</label>
329
+ <input
330
+ data-testid="integration-endpoint"
331
+ type="text"
332
+ value={endpoint}
333
+ onChange={(e) => setEndpoint(e.target.value)}
334
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground"
335
+ />
336
+ </div>
337
+ <button
338
+ onClick={() => {
339
+ setRawMode(!rawMode)
340
+ if (!rawMode) setRawBody(JSON.stringify(formValues, null, 2))
341
+ else try { setFormValues(JSON.parse(rawBody)) } catch {}
342
+ }}
343
+ className="ml-3 mt-5 rounded-lg border border-border px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
344
+ >
345
+ {rawMode ? 'Form' : 'JSON'}
346
+ </button>
347
+ </div>
348
+
349
+ {rawMode ? (
350
+ <textarea
351
+ data-testid="integration-body"
352
+ value={rawBody}
353
+ onChange={(e) => setRawBody(e.target.value)}
354
+ rows={10}
355
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground font-mono"
356
+ />
357
+ ) : currentEntry?.inputSchema ? (
358
+ <SchemaForm
359
+ schema={currentEntry.inputSchema}
360
+ values={formValues}
361
+ onChange={setFormValues}
362
+ />
363
+ ) : (
364
+ <textarea
365
+ data-testid="integration-body"
366
+ value={rawBody}
367
+ onChange={(e) => setRawBody(e.target.value)}
368
+ rows={8}
369
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground font-mono"
370
+ placeholder="{}"
371
+ />
372
+ )}
373
+
374
+ <button
375
+ data-testid="integration-submit"
376
+ onClick={handleSubmit}
377
+ disabled={loading}
378
+ className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
379
+ >
380
+ {loading ? 'Calling...' : 'Send Request'}
381
+ </button>
382
+
383
+ {loading && (
384
+ <div data-testid="integration-loading" className="text-sm text-muted-foreground">
385
+ Loading...
386
+ </div>
387
+ )}
388
+
389
+ {error && (
390
+ <div data-testid="integration-error" className="rounded-lg bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive">
391
+ {error}
392
+ </div>
393
+ )}
394
+
395
+ {result && !result.success && result.issues && (
396
+ <div className="rounded-lg bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm space-y-1">
397
+ <div className="font-medium text-destructive">{result.error}</div>
398
+ {result.issues.map((issue: any, i: number) => (
399
+ <div key={i} className="text-destructive/80">
400
+ <span className="font-mono">{issue.path?.join('.')}</span>: {issue.message}
401
+ </div>
402
+ ))}
403
+ </div>
404
+ )}
405
+
406
+ {result && (
407
+ <div className="space-y-2">
408
+ <div data-testid="integration-status" className="text-sm font-medium text-foreground">
409
+ {result.success ? '✓ Success' : '✗ Failed'}
410
+ </div>
411
+ <pre
412
+ data-testid="integration-result"
413
+ className="rounded-lg bg-card border border-border px-4 py-3 text-xs text-foreground font-mono overflow-auto max-h-96"
414
+ >
415
+ {JSON.stringify(result, null, 2)}
416
+ </pre>
417
+ </div>
418
+ )}
419
+ </div>
420
+ </div>
421
+ </div>
422
+ )
423
+ }
@@ -0,0 +1,42 @@
1
+ import { test, expect } from '@playwright/test'
2
+ import { signUp } from './helpers/auth'
3
+
4
+ async function waitForApp(page: import('@playwright/test').Page) {
5
+ await page.waitForSelector('[data-testid="app-navigation"]', { timeout: 15000 })
6
+ }
7
+
8
+ test.describe('Integration API', () => {
9
+ test('authenticated user can call OpenAI via integration proxy', async ({ page }) => {
10
+ await signUp(page, `integ-${Date.now()}@test.local`, { name: 'Integration Tester' })
11
+ await waitForApp(page)
12
+
13
+ await page.goto('/integration-test')
14
+ await page.waitForSelector('[data-testid="integration-submit"]', { timeout: 10000 })
15
+
16
+ // Default is openai/chat-completion — click submit
17
+ await page.getByTestId('integration-submit').click()
18
+
19
+ // Wait for result or error
20
+ await page.waitForSelector('[data-testid="integration-result"], [data-testid="integration-error"]', { timeout: 30000 })
21
+
22
+ const resultEl = page.getByTestId('integration-result')
23
+ const errorEl = page.getByTestId('integration-error')
24
+
25
+ if (await resultEl.isVisible()) {
26
+ const text = await resultEl.textContent()
27
+ const result = JSON.parse(text!)
28
+ expect(result.success).toBeTruthy()
29
+ expect(result.data).toBeDefined()
30
+ } else {
31
+ const errorText = await errorEl.textContent()
32
+ // Acceptable errors: no API key (dev), insufficient credits, etc.
33
+ expect(errorText).toBeTruthy()
34
+ }
35
+ })
36
+
37
+ test('integration test page loads for anonymous user', async ({ page }) => {
38
+ await page.goto('/integration-test')
39
+ await page.waitForSelector('[data-testid="app-navigation"]', { timeout: 15000 })
40
+ await expect(page.getByTestId('integration-submit')).toBeVisible()
41
+ })
42
+ })
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "items",
3
+ "name": "Items",
4
+ "category": "data",
5
+ "description": "CRUD items with ownership, status toggling, and role-based access",
6
+ "details": "Reference implementation for basic CRUD with useQuery and useMutations. Items have owner-based permissions (own items editable, others read-only), active/archived status toggle, and role-based create access.",
7
+ "files": [
8
+ {
9
+ "src": "src/ItemsPage.tsx",
10
+ "dest": "src/pages/items.tsx"
11
+ },
12
+ {
13
+ "src": "src/items-schema.ts",
14
+ "dest": "src/schemas/items-schema.ts"
15
+ },
16
+ {
17
+ "src": "src/items-constants.ts",
18
+ "dest": "src/components/items/items-constants.ts"
19
+ }
20
+ ],
21
+ "schema": {
22
+ "exportName": "itemsSchema",
23
+ "importPath": "./schemas/items-schema",
24
+ "spreadOperator": false
25
+ },
26
+ "route": {
27
+ "path": "/items",
28
+ "protected": true
29
+ },
30
+ "requires": [],
31
+ "instructions": [],
32
+ "patterns": []
33
+ }