deepspace 0.3.10 → 0.4.0
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/dist/cli.js +331 -115
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +199 -16
- package/dist/index.js +354 -166
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +242 -73
- package/dist/server.js +985 -374
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +322 -73
- package/dist/worker.js +961 -343
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- 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
|
+
}
|