@basementstudio/sanity-ai-image-plugin 0.0.1 → 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.
- package/README.md +281 -257
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/article-featured-image.d.ts +3 -0
- package/dist/presets/article-featured-image.d.ts.map +1 -0
- package/dist/presets/article-featured-image.js +17 -0
- package/dist/presets/article-featured-image.js.map +1 -0
- package/dist/server/constants.d.ts +6 -0
- package/dist/server/constants.d.ts.map +1 -0
- package/{src/server/constants.ts → dist/server/constants.js} +17 -20
- package/dist/server/constants.js.map +1 -0
- package/dist/server/handle-request.d.ts +5 -0
- package/dist/server/handle-request.d.ts.map +1 -0
- package/dist/server/handle-request.js +122 -0
- package/dist/server/handle-request.js.map +1 -0
- package/dist/server/types.d.ts +28 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/utils.d.ts +50 -0
- package/dist/server/utils.d.ts.map +1 -0
- package/dist/server/utils.js +274 -0
- package/dist/server/utils.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -0
- package/dist/studio/components/asset-source.d.ts +4 -0
- package/dist/studio/components/asset-source.d.ts.map +1 -0
- package/dist/studio/components/asset-source.js +163 -0
- package/dist/studio/components/asset-source.js.map +1 -0
- package/dist/studio/components/generate-button-input.d.ts +8 -0
- package/dist/studio/components/generate-button-input.d.ts.map +1 -0
- package/dist/studio/components/generate-button-input.js +186 -0
- package/dist/studio/components/generate-button-input.js.map +1 -0
- package/dist/studio/components/input-router.d.ts +7 -0
- package/dist/studio/components/input-router.d.ts.map +1 -0
- package/dist/studio/components/input-router.js +21 -0
- package/dist/studio/components/input-router.js.map +1 -0
- package/dist/studio/files.d.ts +9 -0
- package/dist/studio/files.d.ts.map +1 -0
- package/dist/studio/files.js +86 -0
- package/dist/studio/files.js.map +1 -0
- package/dist/studio/plugin.d.ts +3 -0
- package/dist/studio/plugin.d.ts.map +1 -0
- package/dist/studio/plugin.js +47 -0
- package/dist/studio/plugin.js.map +1 -0
- package/dist/studio/settings/schema.d.ts +8 -0
- package/dist/studio/settings/schema.d.ts.map +1 -0
- package/dist/studio/settings/schema.js +110 -0
- package/dist/studio/settings/schema.js.map +1 -0
- package/dist/studio/settings/tool.d.ts +3 -0
- package/dist/studio/settings/tool.d.ts.map +1 -0
- package/dist/studio/settings/tool.js +292 -0
- package/dist/studio/settings/tool.js.map +1 -0
- package/dist/studio/settings-data.d.ts +24 -0
- package/dist/studio/settings-data.d.ts.map +1 -0
- package/dist/studio/settings-data.js +99 -0
- package/dist/studio/settings-data.js.map +1 -0
- package/dist/studio/shared-secret.d.ts +9 -0
- package/dist/studio/shared-secret.d.ts.map +1 -0
- package/dist/studio/shared-secret.js +37 -0
- package/dist/studio/shared-secret.js.map +1 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +41 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/context-fields.d.ts +16 -0
- package/dist/utils/context-fields.d.ts.map +1 -0
- package/dist/utils/context-fields.js +104 -0
- package/dist/utils/context-fields.js.map +1 -0
- package/dist/utils/document-paths.d.ts +10 -0
- package/dist/utils/document-paths.d.ts.map +1 -0
- package/dist/utils/document-paths.js +33 -0
- package/dist/utils/document-paths.js.map +1 -0
- package/dist/utils/models.d.ts +22 -0
- package/dist/utils/models.d.ts.map +1 -0
- package/dist/utils/models.js +76 -0
- package/dist/utils/models.js.map +1 -0
- package/dist/utils/prompts.d.ts +2 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +7 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shared.d.ts +82 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +13 -0
- package/dist/utils/shared.js.map +1 -0
- package/package.json +73 -67
- package/src/index.ts +0 -23
- package/src/presets/article-featured-image.ts +0 -23
- package/src/server/handle-request.ts +0 -207
- package/src/server/types.ts +0 -30
- package/src/server/utils.ts +0 -395
- package/src/server.ts +0 -14
- package/src/studio/components/asset-source.tsx +0 -297
- package/src/studio/components/generate-button-input.tsx +0 -380
- package/src/studio/components/input-router.tsx +0 -41
- package/src/studio/files.ts +0 -114
- package/src/studio/plugin.tsx +0 -54
- package/src/studio/settings/schema.ts +0 -122
- package/src/studio/settings/tool.tsx +0 -587
- package/src/studio/settings-data.ts +0 -172
- package/src/utils/config.ts +0 -55
- package/src/utils/context-fields.ts +0 -172
- package/src/utils/document-paths.ts +0 -51
- package/src/utils/models.ts +0 -126
- package/src/utils/prompts.ts +0 -6
- package/src/utils/shared.ts +0 -88
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Box,
|
|
5
|
-
Button,
|
|
6
|
-
Card,
|
|
7
|
-
Dialog,
|
|
8
|
-
Flex,
|
|
9
|
-
Stack,
|
|
10
|
-
Text,
|
|
11
|
-
TextArea,
|
|
12
|
-
} from "@sanity/ui"
|
|
13
|
-
import { type ChangeEvent, useMemo, useState } from "react"
|
|
14
|
-
import {
|
|
15
|
-
PatchEvent,
|
|
16
|
-
type ImageValue,
|
|
17
|
-
type ObjectInputProps,
|
|
18
|
-
set,
|
|
19
|
-
setIfMissing,
|
|
20
|
-
useClient,
|
|
21
|
-
useFormValue,
|
|
22
|
-
useSchema,
|
|
23
|
-
} from "sanity"
|
|
24
|
-
import {
|
|
25
|
-
buildContextFieldPrompt,
|
|
26
|
-
getDefaultSelectedContextFieldPaths,
|
|
27
|
-
getSelectableContextFields,
|
|
28
|
-
} from "../../utils/context-fields"
|
|
29
|
-
import {
|
|
30
|
-
SETTINGS_DOCUMENT_ID,
|
|
31
|
-
SETTINGS_SCHEMA_TYPE,
|
|
32
|
-
type GenerateButtonTarget,
|
|
33
|
-
type ResolvedOptions,
|
|
34
|
-
} from "../../utils/shared"
|
|
35
|
-
import { composePrompt } from "../../utils/prompts"
|
|
36
|
-
import {
|
|
37
|
-
buildSettingsReferenceImages,
|
|
38
|
-
fetchSettingsDocument,
|
|
39
|
-
findTargetSettings,
|
|
40
|
-
getErrorMessage,
|
|
41
|
-
resolveSettingsModel,
|
|
42
|
-
} from "../settings-data"
|
|
43
|
-
import { createGeneratedFile } from "../files"
|
|
44
|
-
|
|
45
|
-
type GenerateResponse = {
|
|
46
|
-
data: string
|
|
47
|
-
mimeType: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
type GenerationPhase = "idle" | "preparing" | "generating" | "uploading" | "updating"
|
|
51
|
-
|
|
52
|
-
function getGenerationPhaseLabel(phase: GenerationPhase): string {
|
|
53
|
-
switch (phase) {
|
|
54
|
-
case "preparing":
|
|
55
|
-
return "Preparing..."
|
|
56
|
-
case "generating":
|
|
57
|
-
return "Generating..."
|
|
58
|
-
case "uploading":
|
|
59
|
-
return "Uploading to Sanity..."
|
|
60
|
-
case "updating":
|
|
61
|
-
return "Updating field..."
|
|
62
|
-
default:
|
|
63
|
-
return "Generate image"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function GenerateButtonInput({
|
|
68
|
-
options,
|
|
69
|
-
props,
|
|
70
|
-
target,
|
|
71
|
-
}: {
|
|
72
|
-
options: ResolvedOptions
|
|
73
|
-
props: ObjectInputProps<ImageValue>
|
|
74
|
-
target: GenerateButtonTarget
|
|
75
|
-
}) {
|
|
76
|
-
const client = useClient({ apiVersion: options.apiVersion })
|
|
77
|
-
const schema = useSchema()
|
|
78
|
-
const documentValue = useFormValue([]) as Record<string, unknown> | undefined
|
|
79
|
-
const documentType = useFormValue(["_type"]) as string | undefined
|
|
80
|
-
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
|
81
|
-
const [userPrompt, setUserPrompt] = useState("")
|
|
82
|
-
const [selectedContextFieldPaths, setSelectedContextFieldPaths] = useState<
|
|
83
|
-
string[]
|
|
84
|
-
>([])
|
|
85
|
-
const [error, setError] = useState<string | null>(null)
|
|
86
|
-
const [generationPhase, setGenerationPhase] = useState<GenerationPhase>("idle")
|
|
87
|
-
const isGenerating = generationPhase !== "idle"
|
|
88
|
-
const currentAssetRef = props.value?.asset?._ref || "empty"
|
|
89
|
-
|
|
90
|
-
const selectableContextFields = useMemo(
|
|
91
|
-
() =>
|
|
92
|
-
getSelectableContextFields(
|
|
93
|
-
documentType ? schema.get(documentType) : undefined
|
|
94
|
-
),
|
|
95
|
-
[documentType, schema]
|
|
96
|
-
)
|
|
97
|
-
const defaultSelectedContextFieldPaths = useMemo(
|
|
98
|
-
() =>
|
|
99
|
-
getDefaultSelectedContextFieldPaths({
|
|
100
|
-
selectableContextFields,
|
|
101
|
-
suggestedContextFieldPaths: target.suggestedContextFieldPaths,
|
|
102
|
-
}),
|
|
103
|
-
[selectableContextFields, target.suggestedContextFieldPaths]
|
|
104
|
-
)
|
|
105
|
-
const runtimePrompt = buildContextFieldPrompt({
|
|
106
|
-
contextFieldPaths: selectedContextFieldPaths,
|
|
107
|
-
documentValue,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
function toggleContextFieldPath(fieldPath: string) {
|
|
111
|
-
setSelectedContextFieldPaths((currentFieldPaths) =>
|
|
112
|
-
currentFieldPaths.includes(fieldPath)
|
|
113
|
-
? currentFieldPaths.filter(
|
|
114
|
-
(currentFieldPath) => currentFieldPath !== fieldPath
|
|
115
|
-
)
|
|
116
|
-
: [...currentFieldPaths, fieldPath]
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function handleGenerate() {
|
|
121
|
-
setError(null)
|
|
122
|
-
setGenerationPhase("preparing")
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const settings = await fetchSettingsDocument(
|
|
126
|
-
client,
|
|
127
|
-
SETTINGS_DOCUMENT_ID,
|
|
128
|
-
SETTINGS_SCHEMA_TYPE
|
|
129
|
-
)
|
|
130
|
-
const targetSettings = findTargetSettings(settings, target.id)
|
|
131
|
-
const selectedModel = resolveSettingsModel({
|
|
132
|
-
allowedModels: options.allowedModels,
|
|
133
|
-
settings,
|
|
134
|
-
})
|
|
135
|
-
const prompt = composePrompt([
|
|
136
|
-
settings?.globalPrompt
|
|
137
|
-
? `Global direction:\n${settings.globalPrompt}`
|
|
138
|
-
: null,
|
|
139
|
-
targetSettings?.prompt
|
|
140
|
-
? `Target direction:\n${targetSettings.prompt}`
|
|
141
|
-
: null,
|
|
142
|
-
runtimePrompt ? `Document context:\n${runtimePrompt}` : null,
|
|
143
|
-
userPrompt.trim()
|
|
144
|
-
? `Additional editor instructions:\n${userPrompt.trim()}`
|
|
145
|
-
: null,
|
|
146
|
-
])
|
|
147
|
-
|
|
148
|
-
if (!prompt) {
|
|
149
|
-
throw new Error("Add a prompt or configure target defaults first.")
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const referenceImages = await buildSettingsReferenceImages(
|
|
153
|
-
settings,
|
|
154
|
-
target.id
|
|
155
|
-
)
|
|
156
|
-
const formData = new FormData()
|
|
157
|
-
|
|
158
|
-
formData.set("prompt", prompt)
|
|
159
|
-
formData.set("model", selectedModel)
|
|
160
|
-
|
|
161
|
-
for (const referenceImage of referenceImages) {
|
|
162
|
-
formData.append("references", referenceImage)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
setGenerationPhase("generating")
|
|
166
|
-
const response = await fetch(options.apiEndpoint, {
|
|
167
|
-
method: "POST",
|
|
168
|
-
body: formData,
|
|
169
|
-
})
|
|
170
|
-
const payload = (await response.json()) as
|
|
171
|
-
| GenerateResponse
|
|
172
|
-
| { error?: string }
|
|
173
|
-
|
|
174
|
-
if (!response.ok) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
"error" in payload && payload.error
|
|
177
|
-
? payload.error
|
|
178
|
-
: "AI image generation failed."
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
!("data" in payload) ||
|
|
184
|
-
typeof payload.data !== "string" ||
|
|
185
|
-
typeof payload.mimeType !== "string"
|
|
186
|
-
) {
|
|
187
|
-
throw new Error("AI Image Plugin returned an invalid image payload.")
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const basename =
|
|
191
|
-
String(
|
|
192
|
-
documentValue?.title ||
|
|
193
|
-
documentValue?._type ||
|
|
194
|
-
target.title ||
|
|
195
|
-
"generated"
|
|
196
|
-
).trim() || "generated"
|
|
197
|
-
const generatedFile = createGeneratedFile(
|
|
198
|
-
payload,
|
|
199
|
-
basename,
|
|
200
|
-
"ai-image-plugin-field"
|
|
201
|
-
)
|
|
202
|
-
setGenerationPhase("uploading")
|
|
203
|
-
const uploadedAsset = await client.assets.upload("image", generatedFile, {
|
|
204
|
-
filename: generatedFile.name,
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
setGenerationPhase("updating")
|
|
208
|
-
props.onChange(
|
|
209
|
-
PatchEvent.from([
|
|
210
|
-
setIfMissing({ _type: "image" }),
|
|
211
|
-
set({ _type: "reference", _ref: uploadedAsset._id }, ["asset"]),
|
|
212
|
-
])
|
|
213
|
-
)
|
|
214
|
-
setIsDialogOpen(false)
|
|
215
|
-
setUserPrompt("")
|
|
216
|
-
} catch (nextError) {
|
|
217
|
-
setError(
|
|
218
|
-
getErrorMessage(nextError, "Unable to generate an image for this field.")
|
|
219
|
-
)
|
|
220
|
-
} finally {
|
|
221
|
-
setGenerationPhase("idle")
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<Stack space={4}>
|
|
227
|
-
<Card padding={3} radius={2} tone="transparent">
|
|
228
|
-
<Stack space={3}>
|
|
229
|
-
<Text size={1}>
|
|
230
|
-
{target.description ||
|
|
231
|
-
"Generate an image with AI Image Plugin for this field."}
|
|
232
|
-
</Text>
|
|
233
|
-
<Flex gap={3}>
|
|
234
|
-
<Button
|
|
235
|
-
onClick={() => {
|
|
236
|
-
setError(null)
|
|
237
|
-
setSelectedContextFieldPaths(defaultSelectedContextFieldPaths)
|
|
238
|
-
setIsDialogOpen(true)
|
|
239
|
-
}}
|
|
240
|
-
text="Generate"
|
|
241
|
-
tone="primary"
|
|
242
|
-
/>
|
|
243
|
-
</Flex>
|
|
244
|
-
</Stack>
|
|
245
|
-
</Card>
|
|
246
|
-
|
|
247
|
-
<Box key={currentAssetRef}>{props.renderDefault(props)}</Box>
|
|
248
|
-
|
|
249
|
-
{isDialogOpen ? (
|
|
250
|
-
<Dialog
|
|
251
|
-
header={target.dialogTitle || "Generate Image"}
|
|
252
|
-
id={`${target.id}-generate-dialog`}
|
|
253
|
-
onClose={() => {
|
|
254
|
-
if (isGenerating) {
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
setError(null)
|
|
259
|
-
setIsDialogOpen(false)
|
|
260
|
-
}}
|
|
261
|
-
width={1}
|
|
262
|
-
>
|
|
263
|
-
<Box padding={4}>
|
|
264
|
-
<Stack space={4}>
|
|
265
|
-
<Box>
|
|
266
|
-
<Text size={1} weight="medium">
|
|
267
|
-
{target.promptLabel || "Custom prompt"}
|
|
268
|
-
</Text>
|
|
269
|
-
<Box marginTop={2}>
|
|
270
|
-
<TextArea
|
|
271
|
-
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
|
|
272
|
-
setUserPrompt(event.currentTarget.value)
|
|
273
|
-
}
|
|
274
|
-
placeholder={
|
|
275
|
-
target.promptPlaceholder ||
|
|
276
|
-
"Optional custom instructions for this image."
|
|
277
|
-
}
|
|
278
|
-
rows={6}
|
|
279
|
-
value={userPrompt}
|
|
280
|
-
/>
|
|
281
|
-
</Box>
|
|
282
|
-
</Box>
|
|
283
|
-
|
|
284
|
-
{selectableContextFields.length > 0 ? (
|
|
285
|
-
<Box>
|
|
286
|
-
<Stack space={3}>
|
|
287
|
-
<Stack space={2}>
|
|
288
|
-
<Text size={1} weight="medium">
|
|
289
|
-
Include document fields
|
|
290
|
-
</Text>
|
|
291
|
-
<Flex gap={2} style={{ flexWrap: "wrap" }}>
|
|
292
|
-
{selectableContextFields.map((field) => {
|
|
293
|
-
const isSelected = selectedContextFieldPaths.includes(
|
|
294
|
-
field.path
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
return (
|
|
298
|
-
<Button
|
|
299
|
-
key={field.path}
|
|
300
|
-
mode={isSelected ? "default" : "ghost"}
|
|
301
|
-
onClick={() => toggleContextFieldPath(field.path)}
|
|
302
|
-
text={field.label}
|
|
303
|
-
tone={isSelected ? "primary" : "default"}
|
|
304
|
-
/>
|
|
305
|
-
)
|
|
306
|
-
})}
|
|
307
|
-
</Flex>
|
|
308
|
-
</Stack>
|
|
309
|
-
</Stack>
|
|
310
|
-
</Box>
|
|
311
|
-
) : (
|
|
312
|
-
<Card padding={3} radius={2} tone="transparent">
|
|
313
|
-
<Stack space={2}>
|
|
314
|
-
<Text size={1} weight="medium">
|
|
315
|
-
Document context
|
|
316
|
-
</Text>
|
|
317
|
-
<Text muted size={1}>
|
|
318
|
-
No supported top-level document fields are available for
|
|
319
|
-
this type yet.
|
|
320
|
-
</Text>
|
|
321
|
-
</Stack>
|
|
322
|
-
</Card>
|
|
323
|
-
)}
|
|
324
|
-
|
|
325
|
-
{selectableContextFields.length > 0 ? (
|
|
326
|
-
<Card padding={3} radius={2} tone="transparent">
|
|
327
|
-
<Stack space={2}>
|
|
328
|
-
<Text size={1} weight="medium">
|
|
329
|
-
Document context preview
|
|
330
|
-
</Text>
|
|
331
|
-
{runtimePrompt ? (
|
|
332
|
-
<Text size={1}>{runtimePrompt}</Text>
|
|
333
|
-
) : (
|
|
334
|
-
<Text muted size={1}>
|
|
335
|
-
No document fields are selected with a usable value yet.
|
|
336
|
-
</Text>
|
|
337
|
-
)}
|
|
338
|
-
</Stack>
|
|
339
|
-
</Card>
|
|
340
|
-
) : null}
|
|
341
|
-
|
|
342
|
-
{error ? (
|
|
343
|
-
<Card padding={3} radius={2} tone="critical">
|
|
344
|
-
<Text size={1}>{error}</Text>
|
|
345
|
-
</Card>
|
|
346
|
-
) : null}
|
|
347
|
-
|
|
348
|
-
{isGenerating ? (
|
|
349
|
-
<Card padding={3} radius={2} tone="transparent">
|
|
350
|
-
<Text size={1}>
|
|
351
|
-
{getGenerationPhaseLabel(generationPhase)} This can take a
|
|
352
|
-
minute or two for slower models.
|
|
353
|
-
</Text>
|
|
354
|
-
</Card>
|
|
355
|
-
) : null}
|
|
356
|
-
|
|
357
|
-
<Flex gap={3} justify="flex-end">
|
|
358
|
-
<Button
|
|
359
|
-
disabled={isGenerating}
|
|
360
|
-
mode="ghost"
|
|
361
|
-
onClick={() => {
|
|
362
|
-
setError(null)
|
|
363
|
-
setIsDialogOpen(false)
|
|
364
|
-
}}
|
|
365
|
-
text="Cancel"
|
|
366
|
-
/>
|
|
367
|
-
<Button
|
|
368
|
-
disabled={isGenerating}
|
|
369
|
-
onClick={() => void handleGenerate()}
|
|
370
|
-
text={getGenerationPhaseLabel(generationPhase)}
|
|
371
|
-
tone="primary"
|
|
372
|
-
/>
|
|
373
|
-
</Flex>
|
|
374
|
-
</Stack>
|
|
375
|
-
</Box>
|
|
376
|
-
</Dialog>
|
|
377
|
-
) : null}
|
|
378
|
-
</Stack>
|
|
379
|
-
)
|
|
380
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useFormValue, type InputProps, type ObjectInputProps } from "sanity"
|
|
4
|
-
import type { ImageValue } from "sanity"
|
|
5
|
-
import { GenerateButtonInput } from "./generate-button-input"
|
|
6
|
-
import { doesTargetMatchField } from "../../utils/document-paths"
|
|
7
|
-
import type { ResolvedOptions } from "../../utils/shared"
|
|
8
|
-
|
|
9
|
-
export function InputRouter({
|
|
10
|
-
options,
|
|
11
|
-
props,
|
|
12
|
-
}: {
|
|
13
|
-
options: ResolvedOptions
|
|
14
|
-
props: InputProps
|
|
15
|
-
}) {
|
|
16
|
-
const documentType = useFormValue(["_type"]) as string | undefined
|
|
17
|
-
|
|
18
|
-
if (props.schemaType?.name !== "image") {
|
|
19
|
-
return props.renderDefault(props)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const target = options.targets.find((candidate) =>
|
|
23
|
-
doesTargetMatchField({
|
|
24
|
-
documentType,
|
|
25
|
-
path: props.path,
|
|
26
|
-
target: candidate,
|
|
27
|
-
})
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if (!target) {
|
|
31
|
-
return props.renderDefault(props)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<GenerateButtonInput
|
|
36
|
-
options={options}
|
|
37
|
-
props={props as ObjectInputProps<ImageValue>}
|
|
38
|
-
target={target}
|
|
39
|
-
/>
|
|
40
|
-
)
|
|
41
|
-
}
|
package/src/studio/files.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
function getPngFilename(filename: string): string {
|
|
4
|
-
const basename = filename.replace(/\.[^/.]+$/, "")
|
|
5
|
-
return `${basename || "reference"}.png`
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function convertImageFileToPng(file: File): Promise<File> {
|
|
9
|
-
if (file.type === "image/png") {
|
|
10
|
-
return file
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const imageUrl = URL.createObjectURL(file)
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
17
|
-
const nextImage = new Image()
|
|
18
|
-
nextImage.onload = () => resolve(nextImage)
|
|
19
|
-
nextImage.onerror = () =>
|
|
20
|
-
reject(new Error(`Unable to decode ${file.name} as an image.`))
|
|
21
|
-
nextImage.src = imageUrl
|
|
22
|
-
})
|
|
23
|
-
const canvas = document.createElement("canvas")
|
|
24
|
-
const width = image.naturalWidth || image.width
|
|
25
|
-
const height = image.naturalHeight || image.height
|
|
26
|
-
|
|
27
|
-
canvas.width = width
|
|
28
|
-
canvas.height = height
|
|
29
|
-
|
|
30
|
-
const context = canvas.getContext("2d")
|
|
31
|
-
|
|
32
|
-
if (!context) {
|
|
33
|
-
throw new Error("Unable to convert the reference image to PNG.")
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
context.drawImage(image, 0, 0, width, height)
|
|
37
|
-
|
|
38
|
-
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
39
|
-
canvas.toBlob(
|
|
40
|
-
(nextBlob) => {
|
|
41
|
-
if (nextBlob) {
|
|
42
|
-
resolve(nextBlob)
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
reject(new Error("Unable to convert the reference image to PNG."))
|
|
47
|
-
},
|
|
48
|
-
"image/png",
|
|
49
|
-
1
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
return new File([blob], getPngFilename(file.name), { type: "image/png" })
|
|
54
|
-
} finally {
|
|
55
|
-
URL.revokeObjectURL(imageUrl)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function base64ToBlob(base64: string, mimeType: string): Blob {
|
|
60
|
-
const binaryString = atob(base64)
|
|
61
|
-
const bytes = new Uint8Array(binaryString.length)
|
|
62
|
-
|
|
63
|
-
for (const [index, character] of Array.from(binaryString).entries()) {
|
|
64
|
-
bytes[index] = character.charCodeAt(0)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return new Blob([bytes], { type: mimeType })
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function fileExtensionFromMimeType(mimeType: string): string {
|
|
71
|
-
switch (mimeType) {
|
|
72
|
-
case "image/jpeg":
|
|
73
|
-
return "jpg"
|
|
74
|
-
case "image/webp":
|
|
75
|
-
return "webp"
|
|
76
|
-
default:
|
|
77
|
-
return "png"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function formatFileSize(bytes: number): string {
|
|
82
|
-
if (bytes < 1024) {
|
|
83
|
-
return `${bytes} B`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const kilobytes = bytes / 1024
|
|
87
|
-
|
|
88
|
-
if (kilobytes < 1024) {
|
|
89
|
-
return `${kilobytes.toFixed(1)} KB`
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return `${(kilobytes / 1024).toFixed(1)} MB`
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function createGeneratedFile(
|
|
96
|
-
result: { data: string; mimeType: string },
|
|
97
|
-
basename: string,
|
|
98
|
-
prefix = "ai-image-plugin"
|
|
99
|
-
): File {
|
|
100
|
-
const extension = fileExtensionFromMimeType(result.mimeType)
|
|
101
|
-
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, "-")
|
|
102
|
-
const safeBasename = basename
|
|
103
|
-
.trim()
|
|
104
|
-
.toLowerCase()
|
|
105
|
-
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
106
|
-
.replaceAll(/^-+|-+$/g, "")
|
|
107
|
-
.slice(0, 40)
|
|
108
|
-
const suffix = safeBasename || "generated"
|
|
109
|
-
const blob = base64ToBlob(result.data, result.mimeType)
|
|
110
|
-
|
|
111
|
-
return new File([blob], `${prefix}-${suffix}-${timestamp}.${extension}`, {
|
|
112
|
-
type: result.mimeType,
|
|
113
|
-
})
|
|
114
|
-
}
|
package/src/studio/plugin.tsx
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { definePlugin, type AssetSource, type InputProps } from "sanity"
|
|
2
|
-
import { createAssetSource } from "./components/asset-source"
|
|
3
|
-
import { InputRouter } from "./components/input-router"
|
|
4
|
-
import { createSettingsSchema } from "./settings/schema"
|
|
5
|
-
import { createSettingsTool } from "./settings/tool"
|
|
6
|
-
import { normalizeOptions } from "../utils/config"
|
|
7
|
-
import type { PluginOptions } from "../utils/shared"
|
|
8
|
-
|
|
9
|
-
export const aiImagePlugin = definePlugin<PluginOptions>((rawOptions) => {
|
|
10
|
-
const options = normalizeOptions(rawOptions)
|
|
11
|
-
const assetSource = options.assetSourceTarget
|
|
12
|
-
? createAssetSource(options)
|
|
13
|
-
: null
|
|
14
|
-
const settingsTool = createSettingsTool(options)
|
|
15
|
-
const settingsSchema = createSettingsSchema(options)
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
name: "ai-image-plugin",
|
|
19
|
-
...(options.registerSettingsSchemaType
|
|
20
|
-
? {
|
|
21
|
-
schema: {
|
|
22
|
-
types: [settingsSchema],
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
: {}),
|
|
26
|
-
tools: [
|
|
27
|
-
{
|
|
28
|
-
name: options.settingsToolName,
|
|
29
|
-
title: options.settingsToolTitle,
|
|
30
|
-
component: settingsTool,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
form: {
|
|
34
|
-
...(assetSource
|
|
35
|
-
? {
|
|
36
|
-
image: {
|
|
37
|
-
assetSources: (previousAssetSources: AssetSource[]) =>
|
|
38
|
-
previousAssetSources.some(
|
|
39
|
-
(previousAssetSource) =>
|
|
40
|
-
previousAssetSource.name === assetSource.name
|
|
41
|
-
)
|
|
42
|
-
? previousAssetSources
|
|
43
|
-
: [...previousAssetSources, assetSource],
|
|
44
|
-
},
|
|
45
|
-
}
|
|
46
|
-
: {}),
|
|
47
|
-
components: {
|
|
48
|
-
input: (props: InputProps) => (
|
|
49
|
-
<InputRouter options={options} props={props} />
|
|
50
|
-
),
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
})
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { defineArrayMember, defineField, defineType } from "sanity"
|
|
2
|
-
import {
|
|
3
|
-
getDefaultAllowedAiImageModel,
|
|
4
|
-
getSupportedAiImageModelOptions,
|
|
5
|
-
} from "../../utils/models"
|
|
6
|
-
import {
|
|
7
|
-
SETTINGS_DOCUMENT_ID,
|
|
8
|
-
SETTINGS_SCHEMA_TYPE,
|
|
9
|
-
type ResolvedOptions,
|
|
10
|
-
} from "../../utils/shared"
|
|
11
|
-
|
|
12
|
-
export function createSettingsSchema(options: ResolvedOptions) {
|
|
13
|
-
return defineType({
|
|
14
|
-
name: SETTINGS_SCHEMA_TYPE,
|
|
15
|
-
title: "AI Image Plugin Settings",
|
|
16
|
-
type: "document",
|
|
17
|
-
fields: [
|
|
18
|
-
defineField({
|
|
19
|
-
name: "title",
|
|
20
|
-
title: "Title",
|
|
21
|
-
type: "string",
|
|
22
|
-
initialValue: "AI Image Plugin Settings",
|
|
23
|
-
readOnly: true,
|
|
24
|
-
}),
|
|
25
|
-
defineField({
|
|
26
|
-
name: "globalPrompt",
|
|
27
|
-
title: "Global Prompt",
|
|
28
|
-
type: "text",
|
|
29
|
-
rows: 8,
|
|
30
|
-
description:
|
|
31
|
-
"Prompt instructions automatically included in every AI Image Plugin generation.",
|
|
32
|
-
}),
|
|
33
|
-
defineField({
|
|
34
|
-
name: "globalModel",
|
|
35
|
-
title: "Default Model",
|
|
36
|
-
type: "string",
|
|
37
|
-
initialValue: getDefaultAllowedAiImageModel(options.allowedModels),
|
|
38
|
-
description:
|
|
39
|
-
"The AI image model used by the plugin unless a different allowed default is configured later.",
|
|
40
|
-
options: {
|
|
41
|
-
list: getSupportedAiImageModelOptions(options.allowedModels),
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
defineField({
|
|
45
|
-
name: "globalReferenceImages",
|
|
46
|
-
title: "Global Reference Images",
|
|
47
|
-
type: "array",
|
|
48
|
-
description:
|
|
49
|
-
"Reference images automatically included in every AI Image Plugin generation.",
|
|
50
|
-
of: [
|
|
51
|
-
defineArrayMember({
|
|
52
|
-
type: "image",
|
|
53
|
-
options: { hotspot: true },
|
|
54
|
-
}),
|
|
55
|
-
],
|
|
56
|
-
}),
|
|
57
|
-
defineField({
|
|
58
|
-
name: "targetConfigs",
|
|
59
|
-
title: "Target Overrides",
|
|
60
|
-
type: "array",
|
|
61
|
-
description:
|
|
62
|
-
"Per-target prompt and reference images for specific AI Image Plugin entry points.",
|
|
63
|
-
of: [
|
|
64
|
-
defineArrayMember({
|
|
65
|
-
type: "object",
|
|
66
|
-
fields: [
|
|
67
|
-
defineField({
|
|
68
|
-
name: "targetId",
|
|
69
|
-
title: "Target ID",
|
|
70
|
-
type: "string",
|
|
71
|
-
validation: (Rule) => Rule.required(),
|
|
72
|
-
options: {
|
|
73
|
-
list: [
|
|
74
|
-
...(options.assetSourceTarget
|
|
75
|
-
? [
|
|
76
|
-
{
|
|
77
|
-
title:
|
|
78
|
-
options.assetSourceTarget.title ||
|
|
79
|
-
options.assetSourceTarget.id,
|
|
80
|
-
value: options.assetSourceTarget.id,
|
|
81
|
-
},
|
|
82
|
-
]
|
|
83
|
-
: []),
|
|
84
|
-
...options.targets.map((target) => ({
|
|
85
|
-
title: target.title || target.id,
|
|
86
|
-
value: target.id,
|
|
87
|
-
})),
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
defineField({
|
|
92
|
-
name: "prompt",
|
|
93
|
-
title: "Prompt",
|
|
94
|
-
type: "text",
|
|
95
|
-
rows: 6,
|
|
96
|
-
}),
|
|
97
|
-
defineField({
|
|
98
|
-
name: "referenceImages",
|
|
99
|
-
title: "Reference Images",
|
|
100
|
-
type: "array",
|
|
101
|
-
of: [
|
|
102
|
-
defineArrayMember({
|
|
103
|
-
type: "image",
|
|
104
|
-
options: { hotspot: true },
|
|
105
|
-
}),
|
|
106
|
-
],
|
|
107
|
-
}),
|
|
108
|
-
],
|
|
109
|
-
}),
|
|
110
|
-
],
|
|
111
|
-
}),
|
|
112
|
-
],
|
|
113
|
-
preview: {
|
|
114
|
-
prepare() {
|
|
115
|
-
return {
|
|
116
|
-
title: "AI Image Plugin Settings",
|
|
117
|
-
subtitle: SETTINGS_DOCUMENT_ID,
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
}
|