@basementstudio/sanity-ai-image-plugin 0.0.0 → 0.1.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.
Files changed (110) hide show
  1. package/README.md +281 -257
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/presets/article-featured-image.d.ts +3 -0
  7. package/dist/presets/article-featured-image.d.ts.map +1 -0
  8. package/dist/presets/article-featured-image.js +17 -0
  9. package/dist/presets/article-featured-image.js.map +1 -0
  10. package/dist/server/constants.d.ts +6 -0
  11. package/dist/server/constants.d.ts.map +1 -0
  12. package/{src/server/constants.ts → dist/server/constants.js} +17 -20
  13. package/dist/server/constants.js.map +1 -0
  14. package/dist/server/handle-request.d.ts +5 -0
  15. package/dist/server/handle-request.d.ts.map +1 -0
  16. package/dist/server/handle-request.js +122 -0
  17. package/dist/server/handle-request.js.map +1 -0
  18. package/dist/server/types.d.ts +28 -0
  19. package/dist/server/types.d.ts.map +1 -0
  20. package/dist/server/types.js +2 -0
  21. package/dist/server/types.js.map +1 -0
  22. package/dist/server/utils.d.ts +50 -0
  23. package/dist/server/utils.d.ts.map +1 -0
  24. package/dist/server/utils.js +274 -0
  25. package/dist/server/utils.js.map +1 -0
  26. package/dist/server.d.ts +3 -0
  27. package/dist/server.d.ts.map +1 -0
  28. package/dist/server.js +3 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/studio/components/asset-source.d.ts +4 -0
  31. package/dist/studio/components/asset-source.d.ts.map +1 -0
  32. package/dist/studio/components/asset-source.js +163 -0
  33. package/dist/studio/components/asset-source.js.map +1 -0
  34. package/dist/studio/components/generate-button-input.d.ts +8 -0
  35. package/dist/studio/components/generate-button-input.d.ts.map +1 -0
  36. package/dist/studio/components/generate-button-input.js +186 -0
  37. package/dist/studio/components/generate-button-input.js.map +1 -0
  38. package/dist/studio/components/input-router.d.ts +7 -0
  39. package/dist/studio/components/input-router.d.ts.map +1 -0
  40. package/dist/studio/components/input-router.js +21 -0
  41. package/dist/studio/components/input-router.js.map +1 -0
  42. package/dist/studio/files.d.ts +9 -0
  43. package/dist/studio/files.d.ts.map +1 -0
  44. package/dist/studio/files.js +86 -0
  45. package/dist/studio/files.js.map +1 -0
  46. package/dist/studio/plugin.d.ts +3 -0
  47. package/dist/studio/plugin.d.ts.map +1 -0
  48. package/dist/studio/plugin.js +47 -0
  49. package/dist/studio/plugin.js.map +1 -0
  50. package/dist/studio/settings/schema.d.ts +8 -0
  51. package/dist/studio/settings/schema.d.ts.map +1 -0
  52. package/dist/studio/settings/schema.js +110 -0
  53. package/dist/studio/settings/schema.js.map +1 -0
  54. package/dist/studio/settings/tool.d.ts +3 -0
  55. package/dist/studio/settings/tool.d.ts.map +1 -0
  56. package/dist/studio/settings/tool.js +292 -0
  57. package/dist/studio/settings/tool.js.map +1 -0
  58. package/dist/studio/settings-data.d.ts +24 -0
  59. package/dist/studio/settings-data.d.ts.map +1 -0
  60. package/dist/studio/settings-data.js +99 -0
  61. package/dist/studio/settings-data.js.map +1 -0
  62. package/dist/studio/shared-secret.d.ts +9 -0
  63. package/dist/studio/shared-secret.d.ts.map +1 -0
  64. package/dist/studio/shared-secret.js +37 -0
  65. package/dist/studio/shared-secret.js.map +1 -0
  66. package/dist/utils/config.d.ts +3 -0
  67. package/dist/utils/config.d.ts.map +1 -0
  68. package/dist/utils/config.js +41 -0
  69. package/dist/utils/config.js.map +1 -0
  70. package/dist/utils/context-fields.d.ts +16 -0
  71. package/dist/utils/context-fields.d.ts.map +1 -0
  72. package/dist/utils/context-fields.js +104 -0
  73. package/dist/utils/context-fields.js.map +1 -0
  74. package/dist/utils/document-paths.d.ts +10 -0
  75. package/dist/utils/document-paths.d.ts.map +1 -0
  76. package/dist/utils/document-paths.js +33 -0
  77. package/dist/utils/document-paths.js.map +1 -0
  78. package/dist/utils/models.d.ts +22 -0
  79. package/dist/utils/models.d.ts.map +1 -0
  80. package/dist/utils/models.js +76 -0
  81. package/dist/utils/models.js.map +1 -0
  82. package/dist/utils/prompts.d.ts +2 -0
  83. package/dist/utils/prompts.d.ts.map +1 -0
  84. package/dist/utils/prompts.js +7 -0
  85. package/dist/utils/prompts.js.map +1 -0
  86. package/dist/utils/shared.d.ts +82 -0
  87. package/dist/utils/shared.d.ts.map +1 -0
  88. package/dist/utils/shared.js +13 -0
  89. package/dist/utils/shared.js.map +1 -0
  90. package/package.json +53 -47
  91. package/src/index.ts +0 -23
  92. package/src/presets/article-featured-image.ts +0 -23
  93. package/src/server/handle-request.ts +0 -207
  94. package/src/server/types.ts +0 -30
  95. package/src/server/utils.ts +0 -395
  96. package/src/server.ts +0 -14
  97. package/src/studio/components/asset-source.tsx +0 -297
  98. package/src/studio/components/generate-button-input.tsx +0 -380
  99. package/src/studio/components/input-router.tsx +0 -41
  100. package/src/studio/files.ts +0 -114
  101. package/src/studio/plugin.tsx +0 -54
  102. package/src/studio/settings/schema.ts +0 -122
  103. package/src/studio/settings/tool.tsx +0 -587
  104. package/src/studio/settings-data.ts +0 -172
  105. package/src/utils/config.ts +0 -55
  106. package/src/utils/context-fields.ts +0 -172
  107. package/src/utils/document-paths.ts +0 -51
  108. package/src/utils/models.ts +0 -126
  109. package/src/utils/prompts.ts +0 -6
  110. package/src/utils/shared.ts +0 -88
@@ -1,587 +0,0 @@
1
- "use client"
2
-
3
- import {
4
- Box,
5
- Button,
6
- Card,
7
- Flex,
8
- Grid,
9
- Select,
10
- Stack,
11
- Text,
12
- TextArea,
13
- } from "@sanity/ui"
14
- import { type ChangeEvent, useEffect, useMemo, useState } from "react"
15
- import { useClient } from "sanity"
16
- import {
17
- getDefaultAllowedAiImageModel,
18
- getSupportedAiImageModelOptions,
19
- type SupportedAiImageModelId,
20
- } from "../../utils/models"
21
- import {
22
- MAX_REFERENCE_IMAGES,
23
- SETTINGS_DOCUMENT_ID,
24
- SETTINGS_SCHEMA_TYPE,
25
- type ResolvedOptions,
26
- } from "../../utils/shared"
27
- import {
28
- appendReferenceImages,
29
- createImageReferenceValue,
30
- createToolReferenceImageFromStoredImage,
31
- fetchSettingsDocument,
32
- getErrorMessage,
33
- resolveSettingsModel,
34
- type ToolReferenceImage,
35
- } from "../settings-data"
36
- import { convertImageFileToPng, formatFileSize } from "../files"
37
-
38
- type TargetEditorState = {
39
- prompt: string
40
- referenceImages: ToolReferenceImage[]
41
- }
42
-
43
- function SettingsReferenceImageCard({
44
- image,
45
- onRemove,
46
- }: {
47
- image: ToolReferenceImage
48
- onRemove: (imageId: string) => void
49
- }) {
50
- const [localPreviewUrl, setLocalPreviewUrl] = useState<string | null>(null)
51
-
52
- useEffect(() => {
53
- if (image.previewUrl || !image.sourceFile) {
54
- setLocalPreviewUrl(null)
55
- return
56
- }
57
-
58
- const objectUrl = URL.createObjectURL(image.sourceFile)
59
- setLocalPreviewUrl(objectUrl)
60
-
61
- return () => {
62
- URL.revokeObjectURL(objectUrl)
63
- }
64
- }, [image.previewUrl, image.sourceFile])
65
-
66
- const previewUrl = image.previewUrl || localPreviewUrl
67
-
68
- return (
69
- <Card padding={3} radius={2} tone="transparent">
70
- <Flex align="center" gap={3} justify="space-between">
71
- <Flex align="center" flex={1} gap={3}>
72
- <Card
73
- overflow="hidden"
74
- radius={2}
75
- style={{ flexShrink: 0, height: 128, width: 128 }}
76
- tone="default"
77
- >
78
- {previewUrl ? (
79
- <img
80
- alt={image.name}
81
- src={previewUrl}
82
- style={{
83
- display: "block",
84
- height: "100%",
85
- objectFit: "cover",
86
- width: "100%",
87
- }}
88
- />
89
- ) : (
90
- <Flex align="center" justify="center" style={{ height: "100%" }}>
91
- <Text muted size={1}>
92
- No preview
93
- </Text>
94
- </Flex>
95
- )}
96
- </Card>
97
-
98
- <Box flex={1}>
99
- <Text size={1} weight="medium">
100
- {image.name}
101
- </Text>
102
- <Text muted size={1}>
103
- {image.sourceFile
104
- ? formatFileSize(image.sourceFile.size)
105
- : "Stored in Sanity"}
106
- </Text>
107
- </Box>
108
- </Flex>
109
-
110
- <Button mode="bleed" onClick={() => onRemove(image.id)} text="Remove" />
111
- </Flex>
112
- </Card>
113
- )
114
- }
115
-
116
- function buildDefaultTargetState(
117
- options: ResolvedOptions
118
- ): Record<string, TargetEditorState> {
119
- return Object.fromEntries(
120
- [
121
- ...(options.assetSourceTarget ? [options.assetSourceTarget] : []),
122
- ...options.targets,
123
- ].map((target) => [
124
- target.id,
125
- {
126
- prompt: "",
127
- referenceImages: [],
128
- },
129
- ])
130
- )
131
- }
132
-
133
- function SettingsReferenceImagesEditor({
134
- images,
135
- label,
136
- onAdd,
137
- onRemove,
138
- }: {
139
- images: ToolReferenceImage[]
140
- label: string
141
- onAdd: (files: File[]) => void
142
- onRemove: (imageId: string) => void
143
- }) {
144
- return (
145
- <Stack space={3}>
146
- <Stack space={1}>
147
- <Text size={1} weight="medium">
148
- {label}
149
- </Text>
150
- <Text muted size={1}>
151
- Up to {MAX_REFERENCE_IMAGES} images.
152
- </Text>
153
- </Stack>
154
- <input
155
- accept="image/*"
156
- multiple
157
- onChange={(event) => {
158
- const files = Array.from(event.currentTarget.files || [])
159
-
160
- if (files.length > 0) {
161
- onAdd(files)
162
- }
163
-
164
- event.currentTarget.value = ""
165
- }}
166
- type="file"
167
- />
168
-
169
- {images.length > 0 ? (
170
- <Stack space={2}>
171
- {images.map((image) => (
172
- <SettingsReferenceImageCard
173
- image={image}
174
- key={image.id}
175
- onRemove={onRemove}
176
- />
177
- ))}
178
- </Stack>
179
- ) : (
180
- <Text muted size={1}>
181
- No reference images yet.
182
- </Text>
183
- )}
184
- </Stack>
185
- )
186
- }
187
-
188
- export function createSettingsTool(options: ResolvedOptions) {
189
- return function SettingsTool() {
190
- const client = useClient({ apiVersion: options.apiVersion })
191
- const [isLoading, setIsLoading] = useState(true)
192
- const [isSaving, setIsSaving] = useState(false)
193
- const [saveMessage, setSaveMessage] = useState<string | null>(null)
194
- const [error, setError] = useState<string | null>(null)
195
- const [globalModel, setGlobalModel] = useState<SupportedAiImageModelId>(
196
- getDefaultAllowedAiImageModel(options.allowedModels)
197
- )
198
- const [globalPrompt, setGlobalPrompt] = useState("")
199
- const [globalReferenceImages, setGlobalReferenceImages] = useState<
200
- ToolReferenceImage[]
201
- >([])
202
- const [targetState, setTargetState] = useState<
203
- Record<string, TargetEditorState>
204
- >(() => buildDefaultTargetState(options))
205
-
206
- useEffect(() => {
207
- let isMounted = true
208
-
209
- async function loadSettings() {
210
- try {
211
- const settings = await fetchSettingsDocument(
212
- client,
213
- SETTINGS_DOCUMENT_ID,
214
- SETTINGS_SCHEMA_TYPE
215
- )
216
-
217
- if (!isMounted) {
218
- return
219
- }
220
-
221
- setGlobalModel(
222
- resolveSettingsModel({
223
- allowedModels: options.allowedModels,
224
- settings,
225
- })
226
- )
227
- setGlobalPrompt(settings?.globalPrompt || "")
228
- setGlobalReferenceImages(
229
- (settings?.globalReferenceImages || [])
230
- .map(createToolReferenceImageFromStoredImage)
231
- .filter((image): image is ToolReferenceImage => image !== null)
232
- )
233
- setTargetState((currentState) => {
234
- const nextState = buildDefaultTargetState(options)
235
-
236
- for (const [targetId, state] of Object.entries(currentState)) {
237
- nextState[targetId] = state
238
- }
239
-
240
- for (const targetConfig of settings?.targetConfigs || []) {
241
- if (!targetConfig.targetId || !nextState[targetConfig.targetId]) {
242
- continue
243
- }
244
-
245
- nextState[targetConfig.targetId] = {
246
- prompt: targetConfig.prompt || "",
247
- referenceImages: (targetConfig.referenceImages || [])
248
- .map(createToolReferenceImageFromStoredImage)
249
- .filter((image): image is ToolReferenceImage => image !== null),
250
- }
251
- }
252
-
253
- return nextState
254
- })
255
- } catch (nextError) {
256
- if (!isMounted) {
257
- return
258
- }
259
-
260
- setError(
261
- getErrorMessage(nextError, "Unable to load AI Image Plugin settings.")
262
- )
263
- } finally {
264
- if (isMounted) {
265
- setIsLoading(false)
266
- }
267
- }
268
- }
269
-
270
- void loadSettings()
271
-
272
- return () => {
273
- isMounted = false
274
- }
275
- }, [client, options])
276
-
277
- const allTargets = useMemo(
278
- () => [
279
- ...(options.assetSourceTarget ? [options.assetSourceTarget] : []),
280
- ...options.targets,
281
- ],
282
- [options]
283
- )
284
- const allowedModelOptions = useMemo(
285
- () => getSupportedAiImageModelOptions(options.allowedModels),
286
- [options.allowedModels]
287
- )
288
-
289
- async function uploadReferenceImages(
290
- images: ToolReferenceImage[]
291
- ): Promise<ToolReferenceImage[]> {
292
- return Promise.all(
293
- images.map(async (image) => {
294
- if (image.assetId || !image.sourceFile) {
295
- return image
296
- }
297
-
298
- const normalizedFile = await convertImageFileToPng(image.sourceFile)
299
- const uploadedAsset = await client.assets.upload("image", normalizedFile, {
300
- filename: normalizedFile.name,
301
- })
302
-
303
- return {
304
- id: uploadedAsset._id,
305
- assetId: uploadedAsset._id,
306
- name: uploadedAsset.originalFilename || normalizedFile.name,
307
- previewUrl: uploadedAsset.url,
308
- }
309
- })
310
- )
311
- }
312
-
313
- async function handleSave() {
314
- setError(null)
315
- setSaveMessage(null)
316
- setIsSaving(true)
317
-
318
- try {
319
- const uploadedGlobalReferenceImages = await uploadReferenceImages(
320
- globalReferenceImages
321
- )
322
- const uploadedTargets = await Promise.all(
323
- allTargets.map(async (target) => {
324
- const currentTargetState = targetState[target.id] || {
325
- prompt: "",
326
- referenceImages: [],
327
- }
328
- const uploadedReferenceImages = await uploadReferenceImages(
329
- currentTargetState.referenceImages
330
- )
331
-
332
- return {
333
- targetId: target.id,
334
- prompt: currentTargetState.prompt.trim() || undefined,
335
- referenceImages: uploadedReferenceImages
336
- .filter((image) => image.assetId)
337
- .map((image) => createImageReferenceValue(image.assetId!)),
338
- uploadedReferenceImages,
339
- }
340
- })
341
- )
342
-
343
- await client.createOrReplace({
344
- _id: SETTINGS_DOCUMENT_ID,
345
- _type: SETTINGS_SCHEMA_TYPE,
346
- title: "AI Image Plugin Settings",
347
- globalModel,
348
- globalPrompt: globalPrompt.trim() || undefined,
349
- globalReferenceImages: uploadedGlobalReferenceImages
350
- .filter((image) => image.assetId)
351
- .map((image) => createImageReferenceValue(image.assetId!)),
352
- targetConfigs: uploadedTargets.map((target) => ({
353
- _type: "object",
354
- targetId: target.targetId,
355
- prompt: target.prompt,
356
- referenceImages: target.referenceImages,
357
- })),
358
- })
359
-
360
- setGlobalReferenceImages(uploadedGlobalReferenceImages)
361
- setTargetState((currentState) =>
362
- Object.fromEntries(
363
- Object.entries(currentState).map(([targetId, state]) => {
364
- const uploadedTarget = uploadedTargets.find(
365
- (target) => target.targetId === targetId
366
- )
367
-
368
- if (!uploadedTarget) {
369
- return [targetId, state]
370
- }
371
-
372
- return [
373
- targetId,
374
- {
375
- ...state,
376
- referenceImages: uploadedTarget.uploadedReferenceImages,
377
- },
378
- ]
379
- })
380
- )
381
- )
382
- setSaveMessage("Saved AI Image Plugin settings.")
383
- } catch (nextError) {
384
- setError(
385
- getErrorMessage(nextError, "Unable to save AI Image Plugin settings.")
386
- )
387
- } finally {
388
- setIsSaving(false)
389
- }
390
- }
391
-
392
- if (isLoading) {
393
- return (
394
- <Box padding={4}>
395
- <Text>Loading AI Image Plugin settings...</Text>
396
- </Box>
397
- )
398
- }
399
-
400
- return (
401
- <Box padding={4}>
402
- <Stack space={4}>
403
- <Card padding={4} radius={2} tone="primary">
404
- <Stack space={3}>
405
- <Text size={3} weight="semibold">
406
- AI Image Plugin
407
- </Text>
408
- <Text size={1}>
409
- Configure reusable prompts and reference images for every AI image
410
- generation entry point without wiring extra schema or structure
411
- files into each project.
412
- </Text>
413
- </Stack>
414
- </Card>
415
-
416
- <Card padding={4} radius={2}>
417
- <Stack space={4}>
418
- <Text size={2} weight="semibold">
419
- Global Defaults
420
- </Text>
421
-
422
- <Box>
423
- <Text size={1} weight="medium">
424
- Default model
425
- </Text>
426
- <Box marginTop={2}>
427
- <Select
428
- onChange={(event) =>
429
- setGlobalModel(
430
- event.currentTarget.value as SupportedAiImageModelId
431
- )
432
- }
433
- value={globalModel}
434
- >
435
- {allowedModelOptions.map((modelOption) => (
436
- <option key={modelOption.value} value={modelOption.value}>
437
- {modelOption.title}
438
- </option>
439
- ))}
440
- </Select>
441
- </Box>
442
- </Box>
443
-
444
- <Box>
445
- <Text size={1} weight="medium">
446
- Global prompt
447
- </Text>
448
- <Box marginTop={2}>
449
- <TextArea
450
- onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
451
- setGlobalPrompt(event.currentTarget.value)
452
- }
453
- placeholder="High-level prompt instructions that should apply to every generation."
454
- rows={8}
455
- value={globalPrompt}
456
- />
457
- </Box>
458
- </Box>
459
-
460
- <SettingsReferenceImagesEditor
461
- images={globalReferenceImages}
462
- label="Global reference images"
463
- onAdd={(files) =>
464
- setGlobalReferenceImages((currentImages) =>
465
- appendReferenceImages(currentImages, files)
466
- )
467
- }
468
- onRemove={(imageId) =>
469
- setGlobalReferenceImages((currentImages) =>
470
- currentImages.filter((image) => image.id !== imageId)
471
- )
472
- }
473
- />
474
- </Stack>
475
- </Card>
476
-
477
- <Grid columns={[1, 1, 2]} gap={4}>
478
- {allTargets.map((target) => {
479
- const state = targetState[target.id] || {
480
- prompt: "",
481
- referenceImages: [],
482
- }
483
-
484
- return (
485
- <Card key={target.id} padding={4} radius={2}>
486
- <Stack space={4}>
487
- <Stack space={2}>
488
- <Text size={2} weight="semibold">
489
- {target.title || target.id}
490
- </Text>
491
- <Text muted size={1}>
492
- {"fieldPath" in target
493
- ? `${target.documentType}.${target.fieldPath}`
494
- : "Shared asset picker configuration"}
495
- </Text>
496
- {"fieldPath" in target &&
497
- target.suggestedContextFieldPaths?.length ? (
498
- <Text muted size={1}>
499
- Suggested tags:{" "}
500
- {target.suggestedContextFieldPaths.join(", ")}
501
- </Text>
502
- ) : null}
503
- {target.description ? (
504
- <Text size={1}>{target.description}</Text>
505
- ) : null}
506
- </Stack>
507
-
508
- <Box>
509
- <Text size={1} weight="medium">
510
- Target prompt
511
- </Text>
512
- <Box marginTop={2}>
513
- <TextArea
514
- onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
515
- setTargetState((currentState) => ({
516
- ...currentState,
517
- [target.id]: {
518
- ...state,
519
- prompt: event.currentTarget.value,
520
- },
521
- }))
522
- }
523
- placeholder="Optional prompt layered on top of the global defaults for this specific target."
524
- rows={6}
525
- value={state.prompt}
526
- />
527
- </Box>
528
- </Box>
529
-
530
- <SettingsReferenceImagesEditor
531
- images={state.referenceImages}
532
- label="Target reference images"
533
- onAdd={(files) =>
534
- setTargetState((currentState) => ({
535
- ...currentState,
536
- [target.id]: {
537
- ...state,
538
- referenceImages: appendReferenceImages(
539
- state.referenceImages,
540
- files
541
- ),
542
- },
543
- }))
544
- }
545
- onRemove={(imageId) =>
546
- setTargetState((currentState) => ({
547
- ...currentState,
548
- [target.id]: {
549
- ...state,
550
- referenceImages: state.referenceImages.filter(
551
- (image) => image.id !== imageId
552
- ),
553
- },
554
- }))
555
- }
556
- />
557
- </Stack>
558
- </Card>
559
- )
560
- })}
561
- </Grid>
562
-
563
- {error ? (
564
- <Card padding={3} radius={2} tone="critical">
565
- <Text size={1}>{error}</Text>
566
- </Card>
567
- ) : null}
568
-
569
- {saveMessage ? (
570
- <Card padding={3} radius={2} tone="positive">
571
- <Text size={1}>{saveMessage}</Text>
572
- </Card>
573
- ) : null}
574
-
575
- <Flex justify="flex-end">
576
- <Button
577
- disabled={isSaving}
578
- onClick={() => void handleSave()}
579
- text={isSaving ? "Saving..." : "Save settings"}
580
- tone="primary"
581
- />
582
- </Flex>
583
- </Stack>
584
- </Box>
585
- )
586
- }
587
- }