@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
package/src/server/utils.ts
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
3
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
|
-
import * as aiSdk from "ai";
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_SUPPORTED_AI_IMAGE_MODEL,
|
|
7
|
-
getDefaultAllowedAiImageModel,
|
|
8
|
-
getSupportedAiImageModelDefinition,
|
|
9
|
-
normalizeAllowedAiImageModels,
|
|
10
|
-
type SupportedAiImageModelId,
|
|
11
|
-
} from "../utils/models";
|
|
12
|
-
import { MAX_REFERENCE_IMAGES } from "../utils/shared";
|
|
13
|
-
import {
|
|
14
|
-
DEFAULT_MAX_REFERENCE_FILE_BYTES,
|
|
15
|
-
OPENAI_SUPPORTED_REFERENCE_IMAGE_TYPES,
|
|
16
|
-
SUPPORTED_REFERENCE_IMAGE_TYPES,
|
|
17
|
-
} from "./constants";
|
|
18
|
-
import type {
|
|
19
|
-
GenerateImageFunction,
|
|
20
|
-
GeneratedImagePayload,
|
|
21
|
-
RequestOptions,
|
|
22
|
-
} from "./types";
|
|
23
|
-
|
|
24
|
-
type ResolvedRequestOptions = {
|
|
25
|
-
allowedModels: SupportedAiImageModelId[];
|
|
26
|
-
defaultModel: SupportedAiImageModelId;
|
|
27
|
-
enforceSameOrigin: boolean;
|
|
28
|
-
maxReferenceFileBytes: number;
|
|
29
|
-
maxReferences: number;
|
|
30
|
-
maxTotalReferenceBytes: number;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type ValidationError = {
|
|
34
|
-
error: string;
|
|
35
|
-
status: number;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Small request-parsing helpers used directly by the main request handler.
|
|
39
|
-
export function jsonResponse(
|
|
40
|
-
body: Record<string, unknown>,
|
|
41
|
-
status = 200,
|
|
42
|
-
): Response {
|
|
43
|
-
return Response.json(body, { status });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function isFileEntry(value: FormDataEntryValue): value is File {
|
|
47
|
-
return typeof value !== "string";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function isMultipartFormData(contentType: string | null): boolean {
|
|
51
|
-
return typeof contentType === "string"
|
|
52
|
-
? contentType.toLowerCase().startsWith("multipart/form-data")
|
|
53
|
-
: false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function parseHeaderByteLength(value: string | null): number | null {
|
|
57
|
-
if (!value) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const parsedValue = Number(value);
|
|
62
|
-
|
|
63
|
-
return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Request-level validation helpers keep the handler readable while still
|
|
67
|
-
// returning precise HTTP errors for common failure modes.
|
|
68
|
-
export function getSameOriginValidationError(
|
|
69
|
-
request: Request,
|
|
70
|
-
): ValidationError | null {
|
|
71
|
-
const requestOrigin = getRequestOrigin(request);
|
|
72
|
-
const normalizedOrigin = getNormalizedOrigin(request.headers.get("origin"));
|
|
73
|
-
|
|
74
|
-
if (!requestOrigin) {
|
|
75
|
-
return {
|
|
76
|
-
error: "AI Image Plugin could not determine the request origin.",
|
|
77
|
-
status: 400,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!normalizedOrigin || normalizedOrigin !== requestOrigin) {
|
|
82
|
-
return {
|
|
83
|
-
error:
|
|
84
|
-
"AI Image Plugin only accepts same-origin browser requests from Sanity Studio.",
|
|
85
|
-
status: 403,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function resolveRequestOptions(
|
|
93
|
-
options: RequestOptions,
|
|
94
|
-
): ResolvedRequestOptions {
|
|
95
|
-
const allowedModels = normalizeAllowedModels(options);
|
|
96
|
-
const defaultModel = getDefaultModel(options, allowedModels);
|
|
97
|
-
const enforceSameOrigin = options.enforceSameOrigin !== false;
|
|
98
|
-
const maxReferenceFileBytes =
|
|
99
|
-
options.maxReferenceFileBytes ?? DEFAULT_MAX_REFERENCE_FILE_BYTES;
|
|
100
|
-
const maxReferences = options.maxReferences ?? MAX_REFERENCE_IMAGES;
|
|
101
|
-
const maxTotalReferenceBytes =
|
|
102
|
-
options.maxTotalReferenceBytes ?? maxReferences * maxReferenceFileBytes;
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
allowedModels,
|
|
106
|
-
defaultModel,
|
|
107
|
-
enforceSameOrigin,
|
|
108
|
-
maxReferenceFileBytes,
|
|
109
|
-
maxReferences,
|
|
110
|
-
maxTotalReferenceBytes,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function getFormRequestPayload(
|
|
115
|
-
formData: FormData,
|
|
116
|
-
maxReferences: number,
|
|
117
|
-
): {
|
|
118
|
-
prompt: string;
|
|
119
|
-
referenceImages: File[];
|
|
120
|
-
requestedModel: string;
|
|
121
|
-
} {
|
|
122
|
-
return {
|
|
123
|
-
prompt: String(formData.get("prompt") || "").trim(),
|
|
124
|
-
requestedModel: String(formData.get("model") || "").trim(),
|
|
125
|
-
referenceImages: formData
|
|
126
|
-
.getAll("references")
|
|
127
|
-
.filter(isFileEntry)
|
|
128
|
-
.slice(0, maxReferences),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Reference image rules depend on both configured byte limits and the
|
|
133
|
-
// capabilities of the selected model provider.
|
|
134
|
-
export function getReferenceImageValidationError(options: {
|
|
135
|
-
maxReferenceFileBytes: number;
|
|
136
|
-
maxTotalReferenceBytes: number;
|
|
137
|
-
model: SupportedAiImageModelId;
|
|
138
|
-
referenceImages: File[];
|
|
139
|
-
}): ValidationError | null {
|
|
140
|
-
const totalReferenceBytes = options.referenceImages.reduce(
|
|
141
|
-
(totalBytes, referenceImage) => totalBytes + referenceImage.size,
|
|
142
|
-
0,
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
if (totalReferenceBytes > options.maxTotalReferenceBytes) {
|
|
146
|
-
return {
|
|
147
|
-
error: `Reference images cannot exceed ${options.maxTotalReferenceBytes} bytes in total.`,
|
|
148
|
-
status: 413,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const modelDefinition = getSupportedAiImageModelDefinition(options.model);
|
|
153
|
-
|
|
154
|
-
for (const referenceImage of options.referenceImages) {
|
|
155
|
-
if (referenceImage.size > options.maxReferenceFileBytes) {
|
|
156
|
-
return {
|
|
157
|
-
error: `Reference images cannot exceed ${options.maxReferenceFileBytes} bytes each.`,
|
|
158
|
-
status: 413,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!SUPPORTED_REFERENCE_IMAGE_TYPES.has(referenceImage.type)) {
|
|
163
|
-
return {
|
|
164
|
-
error: "Reference images must be PNG, JPEG, WEBP, HEIC, or HEIF files.",
|
|
165
|
-
status: 400,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
modelDefinition.provider === "openai" &&
|
|
171
|
-
!OPENAI_SUPPORTED_REFERENCE_IMAGE_TYPES.has(referenceImage.type)
|
|
172
|
-
) {
|
|
173
|
-
return {
|
|
174
|
-
error:
|
|
175
|
-
"Reference images for OpenAI models must be PNG, JPEG, or WEBP files.",
|
|
176
|
-
status: 400,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Provider helpers centralize API-key selection and user-facing provider names
|
|
185
|
-
// so the request handler stays focused on control flow.
|
|
186
|
-
export function getProviderApiKey(
|
|
187
|
-
model: SupportedAiImageModelId,
|
|
188
|
-
options: RequestOptions,
|
|
189
|
-
): string | null {
|
|
190
|
-
const modelDefinition = getSupportedAiImageModelDefinition(model);
|
|
191
|
-
|
|
192
|
-
if (modelDefinition.provider === "google") {
|
|
193
|
-
return options.googleApiKey || options.apiKey || null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return options.openAiApiKey || null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function getProviderDisplayName(model: SupportedAiImageModelId): string {
|
|
200
|
-
const modelDefinition = getSupportedAiImageModelDefinition(model);
|
|
201
|
-
|
|
202
|
-
return modelDefinition.provider === "google" ? "Google" : "OpenAI";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function getMissingApiKeyErrorMessage(
|
|
206
|
-
model: SupportedAiImageModelId,
|
|
207
|
-
): string {
|
|
208
|
-
const modelDefinition = getSupportedAiImageModelDefinition(model);
|
|
209
|
-
|
|
210
|
-
if (modelDefinition.provider === "google") {
|
|
211
|
-
return "Missing Google API key. Add googleApiKey (or apiKey for backwards compatibility) before using AI Image Plugin.";
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return "Missing OpenAI API key. Add openAiApiKey before using AI Image Plugin.";
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function getErrorMessage(error: unknown): string | null {
|
|
218
|
-
if (!(error instanceof Error) || !error.message.trim()) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return error.message;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Provider SDKs do not share a single error shape, so we look through a few
|
|
226
|
-
// common status locations before falling back to a generic 502.
|
|
227
|
-
export function getErrorStatusCode(error: unknown): number | null {
|
|
228
|
-
if (!error || typeof error !== "object") {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
233
|
-
return error.statusCode;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if ("status" in error && typeof error.status === "number") {
|
|
237
|
-
return error.status;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
"response" in error &&
|
|
242
|
-
error.response &&
|
|
243
|
-
typeof error.response === "object" &&
|
|
244
|
-
"status" in error.response &&
|
|
245
|
-
typeof error.response.status === "number"
|
|
246
|
-
) {
|
|
247
|
-
return error.response.status;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// This wraps the AI SDK differences so the request handler can ask for "an
|
|
254
|
-
// image from model X" without caring which provider implementation is behind it.
|
|
255
|
-
export async function generateImageWithModel(options: {
|
|
256
|
-
apiKey: string;
|
|
257
|
-
geminiApiUrl?: string;
|
|
258
|
-
model: SupportedAiImageModelId;
|
|
259
|
-
prompt: string;
|
|
260
|
-
referenceImages: File[];
|
|
261
|
-
}): Promise<{ data: string; mimeType: string } | null> {
|
|
262
|
-
const generateImage = getGenerateImage();
|
|
263
|
-
const modelDefinition = getSupportedAiImageModelDefinition(options.model);
|
|
264
|
-
const prompt =
|
|
265
|
-
options.referenceImages.length > 0
|
|
266
|
-
? {
|
|
267
|
-
images: await Promise.all(options.referenceImages.map(toModelInput)),
|
|
268
|
-
text: options.prompt,
|
|
269
|
-
}
|
|
270
|
-
: options.prompt;
|
|
271
|
-
|
|
272
|
-
const model =
|
|
273
|
-
modelDefinition.provider === "google"
|
|
274
|
-
? createGoogleGenerativeAI({
|
|
275
|
-
apiKey: options.apiKey,
|
|
276
|
-
...(options.geminiApiUrl ? { baseURL: options.geminiApiUrl } : {}),
|
|
277
|
-
}).image(options.model)
|
|
278
|
-
: createOpenAI({
|
|
279
|
-
apiKey: options.apiKey,
|
|
280
|
-
}).image(options.model);
|
|
281
|
-
|
|
282
|
-
const result = await generateImage({
|
|
283
|
-
aspectRatio: modelDefinition.defaultAspectRatio ?? null,
|
|
284
|
-
model,
|
|
285
|
-
prompt,
|
|
286
|
-
size: modelDefinition.defaultSize ?? null,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
return getGeneratedImagePayload(result.image);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Internal helpers below support the exported request/provider helpers without
|
|
293
|
-
// needing to leak implementation details across the server module boundary.
|
|
294
|
-
function getGenerateImage(): GenerateImageFunction {
|
|
295
|
-
const aiModule = aiSdk as Record<string, unknown>;
|
|
296
|
-
const generateImage = (
|
|
297
|
-
"generateImage" in aiModule
|
|
298
|
-
? aiModule.generateImage
|
|
299
|
-
: aiModule.experimental_generateImage
|
|
300
|
-
) as GenerateImageFunction | undefined;
|
|
301
|
-
|
|
302
|
-
if (typeof generateImage !== "function") {
|
|
303
|
-
throw new Error(
|
|
304
|
-
'AI Image Plugin could not find generateImage in the installed "ai" package.',
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return generateImage;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function getNormalizedOrigin(value: string | null): string | null {
|
|
312
|
-
if (!value || value === "null") {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
return new URL(value).origin;
|
|
318
|
-
} catch {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function getRequestOrigin(request: Request): string | null {
|
|
324
|
-
try {
|
|
325
|
-
return new URL(request.url).origin;
|
|
326
|
-
} catch {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function normalizeAllowedModels(
|
|
332
|
-
options: RequestOptions,
|
|
333
|
-
): SupportedAiImageModelId[] {
|
|
334
|
-
if (options.allowedModels) {
|
|
335
|
-
return normalizeAllowedAiImageModels(options.allowedModels);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (options.model) {
|
|
339
|
-
return normalizeAllowedAiImageModels([options.model]);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return normalizeAllowedAiImageModels();
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getDefaultModel(
|
|
346
|
-
options: RequestOptions,
|
|
347
|
-
allowedModels: SupportedAiImageModelId[],
|
|
348
|
-
): SupportedAiImageModelId {
|
|
349
|
-
if (!options.model) {
|
|
350
|
-
return getDefaultAllowedAiImageModel(allowedModels);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!allowedModels.includes(options.model)) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
`AI Image Plugin server configuration must include "${options.model}" in allowedModels.`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return options.model;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function getGeneratedImagePayload(
|
|
363
|
-
image: unknown,
|
|
364
|
-
): { data: string; mimeType: string } | null {
|
|
365
|
-
if (!image || typeof image !== "object") {
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const generatedImage = image as GeneratedImagePayload;
|
|
370
|
-
const mimeType =
|
|
371
|
-
typeof generatedImage.mediaType === "string" &&
|
|
372
|
-
generatedImage.mediaType.trim()
|
|
373
|
-
? generatedImage.mediaType
|
|
374
|
-
: "image/png";
|
|
375
|
-
|
|
376
|
-
if (typeof generatedImage.base64 === "string" && generatedImage.base64) {
|
|
377
|
-
return {
|
|
378
|
-
data: generatedImage.base64.replace(/^data:[^;]+;base64,/, ""),
|
|
379
|
-
mimeType,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (generatedImage.uint8Array instanceof Uint8Array) {
|
|
384
|
-
return {
|
|
385
|
-
data: Buffer.from(generatedImage.uint8Array).toString("base64"),
|
|
386
|
-
mimeType,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function toModelInput(file: File): Promise<Buffer> {
|
|
394
|
-
return Buffer.from(await file.arrayBuffer());
|
|
395
|
-
}
|
package/src/server.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
DEFAULT_MODEL,
|
|
3
|
-
DEFAULT_MAX_REFERENCE_FILE_BYTES,
|
|
4
|
-
handleAiImageRequest,
|
|
5
|
-
} from "./server/handle-request";
|
|
6
|
-
export {
|
|
7
|
-
DEFAULT_PROVIDER_API_URL,
|
|
8
|
-
SUPPORTED_REFERENCE_IMAGE_TYPES,
|
|
9
|
-
} from "./server/constants";
|
|
10
|
-
export type { RequestOptions, SuccessResponse } from "./server/types";
|
|
11
|
-
export {
|
|
12
|
-
SUPPORTED_AI_IMAGE_MODELS,
|
|
13
|
-
type SupportedAiImageModelId,
|
|
14
|
-
} from "./utils/models";
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { Box, Button, Card, Flex, Stack, Text, TextArea } from "@sanity/ui"
|
|
4
|
-
import { type ChangeEvent, useState } from "react"
|
|
5
|
-
import {
|
|
6
|
-
type AssetSource,
|
|
7
|
-
type AssetSourceComponentProps,
|
|
8
|
-
useClient,
|
|
9
|
-
} from "sanity"
|
|
10
|
-
import {
|
|
11
|
-
MAX_REFERENCE_IMAGES,
|
|
12
|
-
SETTINGS_DOCUMENT_ID,
|
|
13
|
-
SETTINGS_SCHEMA_TYPE,
|
|
14
|
-
SOURCE_NAME,
|
|
15
|
-
SOURCE_TITLE,
|
|
16
|
-
type ResolvedOptions,
|
|
17
|
-
} from "../../utils/shared"
|
|
18
|
-
import { composePrompt } from "../../utils/prompts"
|
|
19
|
-
import {
|
|
20
|
-
buildSettingsReferenceImages,
|
|
21
|
-
fetchSettingsDocument,
|
|
22
|
-
findTargetSettings,
|
|
23
|
-
getErrorMessage,
|
|
24
|
-
resolveSettingsModel,
|
|
25
|
-
} from "../settings-data"
|
|
26
|
-
import {
|
|
27
|
-
convertImageFileToPng,
|
|
28
|
-
createGeneratedFile,
|
|
29
|
-
formatFileSize,
|
|
30
|
-
} from "../files"
|
|
31
|
-
|
|
32
|
-
type GenerateResponse = {
|
|
33
|
-
data: string
|
|
34
|
-
mimeType: string
|
|
35
|
-
model: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type GenerationPhase = "idle" | "preparing" | "generating" | "uploading" | "updating"
|
|
39
|
-
|
|
40
|
-
function getGenerationPhaseLabel(phase: GenerationPhase): string {
|
|
41
|
-
switch (phase) {
|
|
42
|
-
case "preparing":
|
|
43
|
-
return "Preparing..."
|
|
44
|
-
case "generating":
|
|
45
|
-
return "Generating..."
|
|
46
|
-
case "uploading":
|
|
47
|
-
return "Uploading to Sanity..."
|
|
48
|
-
case "updating":
|
|
49
|
-
return "Updating field..."
|
|
50
|
-
default:
|
|
51
|
-
return "Generate with AI Image Plugin"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function createAssetSource(options: ResolvedOptions): AssetSource {
|
|
56
|
-
const assetSourceTarget = options.assetSourceTarget
|
|
57
|
-
|
|
58
|
-
function AssetSourceComponent({
|
|
59
|
-
onClose,
|
|
60
|
-
onSelect,
|
|
61
|
-
}: AssetSourceComponentProps) {
|
|
62
|
-
const client = useClient({ apiVersion: options.apiVersion })
|
|
63
|
-
const [prompt, setPrompt] = useState("")
|
|
64
|
-
const [referenceImages, setReferenceImages] = useState<File[]>([])
|
|
65
|
-
const [error, setError] = useState<string | null>(null)
|
|
66
|
-
const [generationPhase, setGenerationPhase] = useState<GenerationPhase>("idle")
|
|
67
|
-
const isGenerating = generationPhase !== "idle"
|
|
68
|
-
|
|
69
|
-
function handleReferenceImageChange(event: ChangeEvent<HTMLInputElement>) {
|
|
70
|
-
const nextFiles = Array.from(event.currentTarget.files ?? [])
|
|
71
|
-
|
|
72
|
-
if (nextFiles.length === 0) {
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
setReferenceImages((currentFiles) =>
|
|
77
|
-
[...currentFiles, ...nextFiles].slice(0, MAX_REFERENCE_IMAGES)
|
|
78
|
-
)
|
|
79
|
-
event.currentTarget.value = ""
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function handleRemoveReferenceImage(indexToRemove: number) {
|
|
83
|
-
setReferenceImages((currentFiles) =>
|
|
84
|
-
currentFiles.filter((_, index) => index !== indexToRemove)
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function handleGenerate() {
|
|
89
|
-
setError(null)
|
|
90
|
-
setGenerationPhase("preparing")
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const settings = await fetchSettingsDocument(
|
|
94
|
-
client,
|
|
95
|
-
SETTINGS_DOCUMENT_ID,
|
|
96
|
-
SETTINGS_SCHEMA_TYPE
|
|
97
|
-
)
|
|
98
|
-
const targetSettings = assetSourceTarget
|
|
99
|
-
? findTargetSettings(settings, assetSourceTarget.id)
|
|
100
|
-
: null
|
|
101
|
-
const selectedModel = resolveSettingsModel({
|
|
102
|
-
allowedModels: options.allowedModels,
|
|
103
|
-
settings,
|
|
104
|
-
})
|
|
105
|
-
const composedPrompt = composePrompt([
|
|
106
|
-
settings?.globalPrompt
|
|
107
|
-
? `Global direction:\n${settings.globalPrompt}`
|
|
108
|
-
: null,
|
|
109
|
-
targetSettings?.prompt
|
|
110
|
-
? `Asset source direction:\n${targetSettings.prompt}`
|
|
111
|
-
: null,
|
|
112
|
-
prompt.trim()
|
|
113
|
-
? `Editor prompt:\n${prompt.trim()}`
|
|
114
|
-
: null,
|
|
115
|
-
])
|
|
116
|
-
|
|
117
|
-
if (!composedPrompt) {
|
|
118
|
-
throw new Error("Add a prompt or configure a default prompt first.")
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const formData = new FormData()
|
|
122
|
-
const normalizedReferenceImages = await Promise.all(
|
|
123
|
-
referenceImages.map(convertImageFileToPng)
|
|
124
|
-
)
|
|
125
|
-
const settingsReferenceImages = await buildSettingsReferenceImages(
|
|
126
|
-
settings,
|
|
127
|
-
assetSourceTarget?.id
|
|
128
|
-
)
|
|
129
|
-
const allReferenceImages = [
|
|
130
|
-
...settingsReferenceImages,
|
|
131
|
-
...normalizedReferenceImages,
|
|
132
|
-
].slice(0, MAX_REFERENCE_IMAGES)
|
|
133
|
-
|
|
134
|
-
formData.set("prompt", composedPrompt)
|
|
135
|
-
formData.set("model", selectedModel)
|
|
136
|
-
|
|
137
|
-
for (const referenceImage of allReferenceImages) {
|
|
138
|
-
formData.append("references", referenceImage)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setGenerationPhase("generating")
|
|
142
|
-
const response = await fetch(options.apiEndpoint, {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: formData,
|
|
145
|
-
})
|
|
146
|
-
const payload = (await response.json()) as
|
|
147
|
-
| GenerateResponse
|
|
148
|
-
| { error?: string }
|
|
149
|
-
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
"error" in payload && payload.error
|
|
153
|
-
? payload.error
|
|
154
|
-
: "AI image generation failed."
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
!("data" in payload) ||
|
|
160
|
-
typeof payload.data !== "string" ||
|
|
161
|
-
typeof payload.mimeType !== "string"
|
|
162
|
-
) {
|
|
163
|
-
throw new Error("AI Image Plugin returned an invalid image payload.")
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const generatedFile = createGeneratedFile(
|
|
167
|
-
payload,
|
|
168
|
-
prompt,
|
|
169
|
-
"ai-image-plugin"
|
|
170
|
-
)
|
|
171
|
-
setGenerationPhase("uploading")
|
|
172
|
-
const uploadedAsset = await client.assets.upload("image", generatedFile, {
|
|
173
|
-
filename: generatedFile.name,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
setGenerationPhase("updating")
|
|
177
|
-
onSelect([{ kind: "assetDocumentId", value: uploadedAsset._id }])
|
|
178
|
-
onClose()
|
|
179
|
-
} catch (nextError) {
|
|
180
|
-
setError(getErrorMessage(nextError))
|
|
181
|
-
} finally {
|
|
182
|
-
setGenerationPhase("idle")
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return (
|
|
187
|
-
<Card padding={4}>
|
|
188
|
-
<Stack space={4}>
|
|
189
|
-
<Stack space={2}>
|
|
190
|
-
<Text size={2} weight="semibold">
|
|
191
|
-
Generate an image with AI Image Plugin and drop it straight into this
|
|
192
|
-
field.
|
|
193
|
-
</Text>
|
|
194
|
-
<Text muted size={1}>
|
|
195
|
-
Project defaults from the AI Image Plugin settings tool are included
|
|
196
|
-
automatically when configured.
|
|
197
|
-
</Text>
|
|
198
|
-
</Stack>
|
|
199
|
-
|
|
200
|
-
<Box>
|
|
201
|
-
<Text size={1} weight="medium">
|
|
202
|
-
{assetSourceTarget?.promptLabel || "Prompt"}
|
|
203
|
-
</Text>
|
|
204
|
-
<Box marginTop={2}>
|
|
205
|
-
<TextArea
|
|
206
|
-
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
|
|
207
|
-
setPrompt(event.currentTarget.value)
|
|
208
|
-
}
|
|
209
|
-
placeholder={
|
|
210
|
-
assetSourceTarget?.promptPlaceholder ||
|
|
211
|
-
"Turn these references into a clean homepage hero image with warm daylight and subtle depth."
|
|
212
|
-
}
|
|
213
|
-
rows={6}
|
|
214
|
-
value={prompt}
|
|
215
|
-
/>
|
|
216
|
-
</Box>
|
|
217
|
-
</Box>
|
|
218
|
-
|
|
219
|
-
<Box>
|
|
220
|
-
<Text size={1} weight="medium">
|
|
221
|
-
Reference images
|
|
222
|
-
</Text>
|
|
223
|
-
<Box marginTop={2}>
|
|
224
|
-
<input
|
|
225
|
-
accept="image/*"
|
|
226
|
-
multiple
|
|
227
|
-
onChange={handleReferenceImageChange}
|
|
228
|
-
type="file"
|
|
229
|
-
/>
|
|
230
|
-
</Box>
|
|
231
|
-
{referenceImages.length > 0 ? (
|
|
232
|
-
<Box marginTop={3}>
|
|
233
|
-
<Stack space={2}>
|
|
234
|
-
{referenceImages.map((referenceImage, index) => (
|
|
235
|
-
<Card
|
|
236
|
-
key={`${referenceImage.name}-${index}`}
|
|
237
|
-
padding={3}
|
|
238
|
-
radius={2}
|
|
239
|
-
tone="transparent"
|
|
240
|
-
>
|
|
241
|
-
<Flex align="center" gap={3} justify="space-between">
|
|
242
|
-
<Box flex={1}>
|
|
243
|
-
<Text size={1} weight="medium">
|
|
244
|
-
{referenceImage.name}
|
|
245
|
-
</Text>
|
|
246
|
-
<Text muted size={1}>
|
|
247
|
-
{formatFileSize(referenceImage.size)}
|
|
248
|
-
</Text>
|
|
249
|
-
</Box>
|
|
250
|
-
<Button
|
|
251
|
-
mode="bleed"
|
|
252
|
-
onClick={() => handleRemoveReferenceImage(index)}
|
|
253
|
-
text="Remove"
|
|
254
|
-
/>
|
|
255
|
-
</Flex>
|
|
256
|
-
</Card>
|
|
257
|
-
))}
|
|
258
|
-
</Stack>
|
|
259
|
-
</Box>
|
|
260
|
-
) : null}
|
|
261
|
-
</Box>
|
|
262
|
-
|
|
263
|
-
{error ? (
|
|
264
|
-
<Card padding={3} radius={2} tone="critical">
|
|
265
|
-
<Text size={1}>{error}</Text>
|
|
266
|
-
</Card>
|
|
267
|
-
) : null}
|
|
268
|
-
|
|
269
|
-
{isGenerating ? (
|
|
270
|
-
<Card padding={3} radius={2} tone="transparent">
|
|
271
|
-
<Text size={1}>
|
|
272
|
-
{getGenerationPhaseLabel(generationPhase)} This can take a minute
|
|
273
|
-
or two for slower models.
|
|
274
|
-
</Text>
|
|
275
|
-
</Card>
|
|
276
|
-
) : null}
|
|
277
|
-
|
|
278
|
-
<Flex gap={3} justify="flex-end">
|
|
279
|
-
<Button mode="ghost" onClick={onClose} text="Cancel" />
|
|
280
|
-
<Button
|
|
281
|
-
disabled={isGenerating}
|
|
282
|
-
onClick={() => void handleGenerate()}
|
|
283
|
-
text={getGenerationPhaseLabel(generationPhase)}
|
|
284
|
-
tone="primary"
|
|
285
|
-
/>
|
|
286
|
-
</Flex>
|
|
287
|
-
</Stack>
|
|
288
|
-
</Card>
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
name: SOURCE_NAME,
|
|
294
|
-
title: SOURCE_TITLE,
|
|
295
|
-
component: AssetSourceComponent,
|
|
296
|
-
}
|
|
297
|
-
}
|