@weirdfingers/boards 0.2.1 → 0.3.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.
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Utilities for parsing generator JSON Schemas into structured data
3
+ * suitable for dynamic UI generation.
4
+ */
5
+
6
+ import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
7
+ import type {
8
+ ParsedGeneratorSchema,
9
+ ArtifactSlot,
10
+ PromptField,
11
+ SettingsField,
12
+ } from "../types/generatorSchema";
13
+
14
+ /**
15
+ * Checks if a JSON Schema property references an artifact type.
16
+ *
17
+ * Artifacts are identified by $ref paths containing "Artifact" in their name,
18
+ * e.g., "#/$defs/AudioArtifact" or "#/$defs/VideoArtifact".
19
+ *
20
+ * @param property - The JSON Schema property to check
21
+ * @returns True if the property references an artifact type
22
+ */
23
+ export function isArtifactReference(
24
+ property: JSONSchema7 | JSONSchema7Definition | undefined
25
+ ): boolean {
26
+ if (!property || typeof property === "boolean") {
27
+ return false;
28
+ }
29
+
30
+ // Direct $ref to artifact
31
+ if (property.$ref && property.$ref.includes("Artifact")) {
32
+ return true;
33
+ }
34
+
35
+ // Array with items that reference an artifact
36
+ if (
37
+ property.type === "array" &&
38
+ property.items &&
39
+ typeof property.items === "object" &&
40
+ !Array.isArray(property.items)
41
+ ) {
42
+ const items = property.items as JSONSchema7;
43
+ return !!(items.$ref && items.$ref.includes("Artifact"));
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Extracts the artifact type from a $ref path.
51
+ *
52
+ * Examples:
53
+ * - "#/$defs/AudioArtifact" -> "audio"
54
+ * - "#/$defs/VideoArtifact" -> "video"
55
+ * - "#/$defs/ImageArtifact" -> "image"
56
+ * - "#/$defs/TextArtifact" -> "text"
57
+ *
58
+ * @param ref - The $ref string from the JSON Schema
59
+ * @returns The artifact type in lowercase
60
+ */
61
+ export function getArtifactType(
62
+ ref: string
63
+ ): "audio" | "video" | "image" | "text" {
64
+ const match = ref.match(/(Audio|Video|Image|Text)Artifact/);
65
+ if (match) {
66
+ return match[1].toLowerCase() as "audio" | "video" | "image" | "text";
67
+ }
68
+
69
+ // Fallback to "image" if pattern doesn't match
70
+ return "image";
71
+ }
72
+
73
+ /**
74
+ * Parses an artifact property into an ArtifactSlot structure.
75
+ *
76
+ * @param name - The property name from the schema
77
+ * @param property - The JSON Schema property definition
78
+ * @param required - Whether this field is in the required array
79
+ * @returns Parsed artifact slot information
80
+ */
81
+ export function parseArtifactSlot(
82
+ name: string,
83
+ property: JSONSchema7,
84
+ required: boolean
85
+ ): ArtifactSlot {
86
+ const title = property.title || name;
87
+ const description = property.description;
88
+
89
+ // Check if this is an array of artifacts
90
+ if (property.type === "array" && property.items) {
91
+ const items =
92
+ typeof property.items === "object" && !Array.isArray(property.items)
93
+ ? (property.items as JSONSchema7)
94
+ : undefined;
95
+
96
+ const artifactType = items?.$ref ? getArtifactType(items.$ref) : "image";
97
+
98
+ return {
99
+ name: title,
100
+ fieldName: name,
101
+ artifactType,
102
+ required,
103
+ description,
104
+ isArray: true,
105
+ minItems: property.minItems,
106
+ maxItems: property.maxItems,
107
+ };
108
+ }
109
+
110
+ // Single artifact
111
+ const artifactType = property.$ref ? getArtifactType(property.$ref) : "image";
112
+
113
+ return {
114
+ name: title,
115
+ fieldName: name,
116
+ artifactType,
117
+ required,
118
+ description,
119
+ isArray: false,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Determines if a numeric property should be rendered as a slider.
125
+ *
126
+ * A property is considered a slider if it's a number or integer type
127
+ * and has both minimum and maximum values defined.
128
+ *
129
+ * @param property - The JSON Schema property to check
130
+ * @returns True if this should be a slider
131
+ */
132
+ function isSlider(property: JSONSchema7): boolean {
133
+ return (
134
+ (property.type === "number" || property.type === "integer") &&
135
+ property.minimum !== undefined &&
136
+ property.maximum !== undefined
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Parses a settings field into its appropriate type (slider, dropdown, text, number).
142
+ *
143
+ * @param name - The property name from the schema
144
+ * @param property - The JSON Schema property definition
145
+ * @returns Parsed settings field information
146
+ */
147
+ export function parseSettingsField(
148
+ name: string,
149
+ property: JSONSchema7
150
+ ): SettingsField | null {
151
+ const title = property.title || name;
152
+ const description = property.description;
153
+
154
+ // Dropdown (enum)
155
+ if (property.enum && Array.isArray(property.enum)) {
156
+ const options = property.enum.map((val) => String(val));
157
+ const defaultValue =
158
+ property.default !== undefined ? String(property.default) : undefined;
159
+
160
+ return {
161
+ type: "dropdown",
162
+ fieldName: name,
163
+ title,
164
+ description,
165
+ options,
166
+ default: defaultValue,
167
+ };
168
+ }
169
+
170
+ // Slider (number/integer with min/max)
171
+ if (isSlider(property)) {
172
+ const isInteger = property.type === "integer";
173
+ return {
174
+ type: "slider",
175
+ fieldName: name,
176
+ title,
177
+ description,
178
+ min: property.minimum as number,
179
+ max: property.maximum as number,
180
+ step: property.multipleOf,
181
+ default:
182
+ property.default !== undefined
183
+ ? (property.default as number)
184
+ : undefined,
185
+ isInteger,
186
+ };
187
+ }
188
+
189
+ // Number input (without slider constraints)
190
+ if (property.type === "number" || property.type === "integer") {
191
+ const isInteger = property.type === "integer";
192
+ return {
193
+ type: "number",
194
+ fieldName: name,
195
+ title,
196
+ description,
197
+ default:
198
+ property.default !== undefined
199
+ ? (property.default as number)
200
+ : undefined,
201
+ min: property.minimum as number | undefined,
202
+ max: property.maximum as number | undefined,
203
+ isInteger,
204
+ };
205
+ }
206
+
207
+ // Text input
208
+ if (property.type === "string") {
209
+ return {
210
+ type: "text",
211
+ fieldName: name,
212
+ title,
213
+ description,
214
+ default:
215
+ property.default !== undefined ? String(property.default) : undefined,
216
+ pattern: property.pattern,
217
+ };
218
+ }
219
+
220
+ // Unsupported type
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Parses a complete generator JSON Schema into structured data for UI generation.
226
+ *
227
+ * This function categorizes schema properties into:
228
+ * - Artifact slots (for selecting existing artifacts)
229
+ * - Prompt field (special text input for generation prompts)
230
+ * - Settings fields (sliders, dropdowns, text inputs, etc.)
231
+ *
232
+ * @param schema - The JSON Schema from the generator's inputSchema field
233
+ * @returns Parsed schema structure ready for dynamic UI generation
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const generator = generators[0];
238
+ * const parsed = parseGeneratorSchema(generator.inputSchema);
239
+ *
240
+ * // Render artifact slots
241
+ * parsed.artifactSlots.forEach(slot => {
242
+ * console.log(`${slot.name}: ${slot.artifactType} (required: ${slot.required})`);
243
+ * });
244
+ *
245
+ * // Render prompt field
246
+ * if (parsed.promptField) {
247
+ * console.log(`Prompt: ${parsed.promptField.description}`);
248
+ * }
249
+ *
250
+ * // Render settings
251
+ * parsed.settingsFields.forEach(field => {
252
+ * if (field.type === 'slider') {
253
+ * console.log(`${field.title}: ${field.min} - ${field.max}`);
254
+ * }
255
+ * });
256
+ * ```
257
+ */
258
+ export function parseGeneratorSchema(
259
+ schema: JSONSchema7
260
+ ): ParsedGeneratorSchema {
261
+ const artifactSlots: ArtifactSlot[] = [];
262
+ const settingsFields: SettingsField[] = [];
263
+ let promptField: PromptField | null = null;
264
+
265
+ if (!schema.properties) {
266
+ return { artifactSlots, promptField, settingsFields };
267
+ }
268
+
269
+ const required = schema.required || [];
270
+
271
+ for (const [name, propertyDef] of Object.entries(schema.properties)) {
272
+ if (typeof propertyDef === "boolean") {
273
+ continue;
274
+ }
275
+
276
+ const property = propertyDef as JSONSchema7;
277
+ const isRequired = required.includes(name);
278
+
279
+ // Check if this is an artifact reference
280
+ if (isArtifactReference(property)) {
281
+ const slot = parseArtifactSlot(name, property, isRequired);
282
+ artifactSlots.push(slot);
283
+ continue;
284
+ }
285
+
286
+ // Check if this is the prompt field
287
+ if (name === "prompt" && property.type === "string") {
288
+ promptField = {
289
+ fieldName: name,
290
+ description: property.description,
291
+ required: isRequired,
292
+ default:
293
+ property.default !== undefined ? String(property.default) : undefined,
294
+ };
295
+ continue;
296
+ }
297
+
298
+ // Everything else goes to settings
299
+ const settingsField = parseSettingsField(name, property);
300
+ if (settingsField) {
301
+ settingsFields.push(settingsField);
302
+ }
303
+ }
304
+
305
+ return {
306
+ artifactSlots,
307
+ promptField,
308
+ settingsFields,
309
+ };
310
+ }