@basementstudio/sanity-ai-image-plugin 0.0.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 +257 -0
- package/package.json +67 -0
- package/src/index.ts +23 -0
- package/src/presets/article-featured-image.ts +23 -0
- package/src/server/constants.ts +20 -0
- package/src/server/handle-request.ts +207 -0
- package/src/server/types.ts +30 -0
- package/src/server/utils.ts +395 -0
- package/src/server.ts +14 -0
- package/src/studio/components/asset-source.tsx +297 -0
- package/src/studio/components/generate-button-input.tsx +380 -0
- package/src/studio/components/input-router.tsx +41 -0
- package/src/studio/files.ts +114 -0
- package/src/studio/plugin.tsx +54 -0
- package/src/studio/settings/schema.ts +122 -0
- package/src/studio/settings/tool.tsx +587 -0
- package/src/studio/settings-data.ts +172 -0
- package/src/utils/config.ts +55 -0
- package/src/utils/context-fields.ts +172 -0
- package/src/utils/document-paths.ts +51 -0
- package/src/utils/models.ts +126 -0
- package/src/utils/prompts.ts +6 -0
- package/src/utils/shared.ts +88 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ImageValue, SanityClient } from "sanity"
|
|
2
|
+
import {
|
|
3
|
+
resolveAllowedAiImageModel,
|
|
4
|
+
type SupportedAiImageModelId,
|
|
5
|
+
} from "../utils/models"
|
|
6
|
+
import {
|
|
7
|
+
MAX_REFERENCE_IMAGES,
|
|
8
|
+
type SettingsTargetConfigValue,
|
|
9
|
+
type SettingsValue,
|
|
10
|
+
type StoredImage,
|
|
11
|
+
} from "../utils/shared"
|
|
12
|
+
import { convertImageFileToPng, fileExtensionFromMimeType } from "./files"
|
|
13
|
+
|
|
14
|
+
export type ToolReferenceImage = {
|
|
15
|
+
assetId?: string
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
previewUrl?: string
|
|
19
|
+
sourceFile?: File
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createImageReferenceValue(assetId: string): ImageValue {
|
|
23
|
+
return {
|
|
24
|
+
_type: "image",
|
|
25
|
+
asset: {
|
|
26
|
+
_type: "reference",
|
|
27
|
+
_ref: assetId,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getErrorMessage(
|
|
33
|
+
error: unknown,
|
|
34
|
+
fallback = "Unable to generate an image right now."
|
|
35
|
+
): string {
|
|
36
|
+
if (error instanceof Error && error.message.trim()) {
|
|
37
|
+
return error.message
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fallback
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function findTargetSettings(
|
|
44
|
+
settings: SettingsValue | null,
|
|
45
|
+
targetId: string
|
|
46
|
+
): SettingsTargetConfigValue | undefined {
|
|
47
|
+
return settings?.targetConfigs?.find((target) => target.targetId === targetId)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveSettingsModel(options: {
|
|
51
|
+
allowedModels: SupportedAiImageModelId[]
|
|
52
|
+
settings: SettingsValue | null
|
|
53
|
+
}): SupportedAiImageModelId {
|
|
54
|
+
return resolveAllowedAiImageModel(
|
|
55
|
+
options.settings?.globalModel,
|
|
56
|
+
options.allowedModels
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchSettingsDocument(
|
|
61
|
+
client: SanityClient,
|
|
62
|
+
documentId: string,
|
|
63
|
+
schemaType: string
|
|
64
|
+
): Promise<SettingsValue | null> {
|
|
65
|
+
return client.fetch<SettingsValue | null>(
|
|
66
|
+
`*[_type == $type && _id == $id][0]{
|
|
67
|
+
globalModel,
|
|
68
|
+
globalPrompt,
|
|
69
|
+
globalReferenceImages[]{
|
|
70
|
+
"asset": {
|
|
71
|
+
"_ref": asset._ref,
|
|
72
|
+
"_id": asset->_id,
|
|
73
|
+
"originalFilename": asset->originalFilename,
|
|
74
|
+
"url": asset->url
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
targetConfigs[]{
|
|
78
|
+
targetId,
|
|
79
|
+
prompt,
|
|
80
|
+
referenceImages[]{
|
|
81
|
+
"asset": {
|
|
82
|
+
"_ref": asset._ref,
|
|
83
|
+
"_id": asset->_id,
|
|
84
|
+
"originalFilename": asset->originalFilename,
|
|
85
|
+
"url": asset->url
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}`,
|
|
90
|
+
{
|
|
91
|
+
id: documentId,
|
|
92
|
+
type: schemaType,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function downloadReferenceImageAsPng(
|
|
98
|
+
image: StoredImage,
|
|
99
|
+
index: number
|
|
100
|
+
): Promise<File | null> {
|
|
101
|
+
if (!image.asset?.url) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const response = await fetch(image.asset.url)
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error("Failed to download AI Image Plugin reference images.")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const blob = await response.blob()
|
|
112
|
+
const fallbackName = `reference-${index + 1}.${fileExtensionFromMimeType(
|
|
113
|
+
blob.type || "image/png"
|
|
114
|
+
)}`
|
|
115
|
+
const filename = image.asset.originalFilename || fallbackName
|
|
116
|
+
const file = new File([blob], filename, { type: blob.type || "image/png" })
|
|
117
|
+
|
|
118
|
+
return convertImageFileToPng(file)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function buildSettingsReferenceImages(
|
|
122
|
+
settings: SettingsValue | null,
|
|
123
|
+
targetId?: string,
|
|
124
|
+
maxReferenceImages = MAX_REFERENCE_IMAGES
|
|
125
|
+
): Promise<File[]> {
|
|
126
|
+
const targetSettings = targetId ? findTargetSettings(settings, targetId) : null
|
|
127
|
+
const images = [
|
|
128
|
+
...(settings?.globalReferenceImages || []),
|
|
129
|
+
...(targetSettings?.referenceImages || []),
|
|
130
|
+
].slice(0, maxReferenceImages)
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
await Promise.all(images.map(downloadReferenceImageAsPng))
|
|
134
|
+
).filter((image): image is File => image instanceof File)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createToolReferenceImageFromStoredImage(
|
|
138
|
+
image: StoredImage,
|
|
139
|
+
index: number
|
|
140
|
+
): ToolReferenceImage | null {
|
|
141
|
+
const asset = image.asset
|
|
142
|
+
const assetId = asset?._ref || asset?._id
|
|
143
|
+
|
|
144
|
+
if (!assetId) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
id: assetId,
|
|
150
|
+
assetId,
|
|
151
|
+
name: asset?.originalFilename || `reference-${index + 1}.png`,
|
|
152
|
+
...(asset?.url ? { previewUrl: asset.url } : {}),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createToolReferenceImageFromFile(file: File): ToolReferenceImage {
|
|
157
|
+
return {
|
|
158
|
+
id: `${file.name}-${file.size}-${file.lastModified}`,
|
|
159
|
+
name: file.name,
|
|
160
|
+
sourceFile: file,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function appendReferenceImages(
|
|
165
|
+
currentImages: ToolReferenceImage[],
|
|
166
|
+
nextFiles: File[]
|
|
167
|
+
): ToolReferenceImage[] {
|
|
168
|
+
return [...currentImages, ...nextFiles.map(createToolReferenceImageFromFile)].slice(
|
|
169
|
+
0,
|
|
170
|
+
MAX_REFERENCE_IMAGES
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { normalizeAllowedAiImageModels } from "./models"
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_API_ENDPOINT,
|
|
4
|
+
DEFAULT_ASSET_SOURCE_TARGET_ID,
|
|
5
|
+
SETTINGS_TOOL_NAME,
|
|
6
|
+
SETTINGS_TOOL_TITLE,
|
|
7
|
+
type PluginOptions,
|
|
8
|
+
type ResolvedOptions,
|
|
9
|
+
} from "./shared"
|
|
10
|
+
|
|
11
|
+
export function normalizeOptions(options: PluginOptions): ResolvedOptions {
|
|
12
|
+
const assetSource =
|
|
13
|
+
options.assetSource === false ||
|
|
14
|
+
(typeof options.assetSource === "object" &&
|
|
15
|
+
options.assetSource.enabled === false)
|
|
16
|
+
? null
|
|
17
|
+
: {
|
|
18
|
+
id:
|
|
19
|
+
typeof options.assetSource === "object"
|
|
20
|
+
? options.assetSource.id || DEFAULT_ASSET_SOURCE_TARGET_ID
|
|
21
|
+
: DEFAULT_ASSET_SOURCE_TARGET_ID,
|
|
22
|
+
type: "assetSource" as const,
|
|
23
|
+
title:
|
|
24
|
+
typeof options.assetSource === "object"
|
|
25
|
+
? options.assetSource.title || "Image Asset Source"
|
|
26
|
+
: "Image Asset Source",
|
|
27
|
+
...(typeof options.assetSource === "object" &&
|
|
28
|
+
options.assetSource.description
|
|
29
|
+
? { description: options.assetSource.description }
|
|
30
|
+
: {
|
|
31
|
+
description: "Adds AI Image Plugin to the image asset picker.",
|
|
32
|
+
}),
|
|
33
|
+
promptLabel:
|
|
34
|
+
typeof options.assetSource === "object"
|
|
35
|
+
? options.assetSource.promptLabel || "Prompt"
|
|
36
|
+
: "Prompt",
|
|
37
|
+
promptPlaceholder:
|
|
38
|
+
typeof options.assetSource === "object"
|
|
39
|
+
? options.assetSource.promptPlaceholder ||
|
|
40
|
+
"Turn these references into a clean homepage hero image with warm daylight and subtle depth."
|
|
41
|
+
: "Turn these references into a clean homepage hero image with warm daylight and subtle depth.",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
apiEndpoint: options.apiEndpoint || DEFAULT_API_ENDPOINT,
|
|
46
|
+
apiVersion: options.apiVersion,
|
|
47
|
+
allowedModels: normalizeAllowedAiImageModels(options.allowedModels),
|
|
48
|
+
assetSourceTarget: assetSource,
|
|
49
|
+
registerSettingsSchemaType:
|
|
50
|
+
options.settingsTool?.registerSchemaType === true,
|
|
51
|
+
settingsToolName: options.settingsTool?.name || SETTINGS_TOOL_NAME,
|
|
52
|
+
settingsToolTitle: options.settingsTool?.title || SETTINGS_TOOL_TITLE,
|
|
53
|
+
targets: options.targets || [],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { getValueAtPath } from "./document-paths"
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_CONTEXT_FIELD_TYPES = new Set([
|
|
4
|
+
"string",
|
|
5
|
+
"text",
|
|
6
|
+
"number",
|
|
7
|
+
"boolean",
|
|
8
|
+
"date",
|
|
9
|
+
"datetime",
|
|
10
|
+
"slug",
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
type DocumentSchemaFieldMember = {
|
|
14
|
+
name?: string
|
|
15
|
+
type?: {
|
|
16
|
+
name?: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type DocumentSchemaField = {
|
|
21
|
+
name?: string
|
|
22
|
+
title?: string
|
|
23
|
+
type?: {
|
|
24
|
+
name?: string
|
|
25
|
+
of?: DocumentSchemaFieldMember[]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SelectableContextField = {
|
|
30
|
+
label: string
|
|
31
|
+
path: string
|
|
32
|
+
typeName: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDocumentSchemaFields(documentSchema: unknown): DocumentSchemaField[] {
|
|
36
|
+
if (!documentSchema || typeof documentSchema !== "object") {
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fields = (documentSchema as { fields?: unknown }).fields
|
|
41
|
+
|
|
42
|
+
return Array.isArray(fields) ? (fields as DocumentSchemaField[]) : []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatFieldNameAsLabel(fieldName: string): string {
|
|
46
|
+
const normalizedFieldName = fieldName
|
|
47
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
48
|
+
.replace(/[_-]+/g, " ")
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/\s+/g, " ")
|
|
51
|
+
|
|
52
|
+
if (!normalizedFieldName) {
|
|
53
|
+
return fieldName
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
normalizedFieldName.charAt(0).toUpperCase() +
|
|
58
|
+
normalizedFieldName.slice(1).toLowerCase()
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isSimpleStringArrayField(field: DocumentSchemaField): boolean {
|
|
63
|
+
if (field.type?.name !== "array" || !Array.isArray(field.type.of)) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (field.type.of.length === 0) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return field.type.of.every((member) => {
|
|
72
|
+
const memberTypeName = member?.type?.name || member?.name
|
|
73
|
+
|
|
74
|
+
return memberTypeName === "string"
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getSelectableContextFields(
|
|
79
|
+
documentSchema: unknown
|
|
80
|
+
): SelectableContextField[] {
|
|
81
|
+
return getDocumentSchemaFields(documentSchema).flatMap((field) => {
|
|
82
|
+
if (!field?.name || !field.type?.name) {
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isSupportedScalarField = SUPPORTED_CONTEXT_FIELD_TYPES.has(
|
|
87
|
+
field.type.name
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if (!isSupportedScalarField && !isSimpleStringArrayField(field)) {
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
label: field.title || formatFieldNameAsLabel(field.name),
|
|
97
|
+
path: field.name,
|
|
98
|
+
typeName: field.type.name,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getDefaultSelectedContextFieldPaths(options: {
|
|
105
|
+
selectableContextFields: SelectableContextField[]
|
|
106
|
+
suggestedContextFieldPaths: string[] | undefined
|
|
107
|
+
}): string[] {
|
|
108
|
+
const suggestedFieldPaths = new Set(
|
|
109
|
+
(options.suggestedContextFieldPaths || []).filter(Boolean)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return options.selectableContextFields
|
|
113
|
+
.filter((field) => suggestedFieldPaths.has(field.path))
|
|
114
|
+
.map((field) => field.path)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function serializeContextFieldValue(value: unknown): string | null {
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
const trimmedValue = value.trim()
|
|
120
|
+
|
|
121
|
+
return trimmedValue || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
125
|
+
return String(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
const serializedValues = value
|
|
130
|
+
.map((item) => (typeof item === "string" ? item.trim() : null))
|
|
131
|
+
.filter((item): item is string => Boolean(item))
|
|
132
|
+
|
|
133
|
+
return serializedValues.length > 0 ? serializedValues.join(", ") : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (value && typeof value === "object") {
|
|
137
|
+
const currentValue = (value as Record<string, unknown>).current
|
|
138
|
+
|
|
139
|
+
if (typeof currentValue === "string") {
|
|
140
|
+
const trimmedValue = currentValue.trim()
|
|
141
|
+
|
|
142
|
+
return trimmedValue || null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildContextFieldPrompt(options: {
|
|
150
|
+
contextFieldPaths: string[]
|
|
151
|
+
documentValue: Record<string, unknown> | undefined
|
|
152
|
+
}): string | null {
|
|
153
|
+
if (!options.documentValue || options.contextFieldPaths.length === 0) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const promptLines = options.contextFieldPaths
|
|
158
|
+
.map((fieldPath) => {
|
|
159
|
+
const serializedValue = serializeContextFieldValue(
|
|
160
|
+
getValueAtPath(options.documentValue, fieldPath)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if (!serializedValue) {
|
|
164
|
+
return `The field titled "${fieldPath}" is empty.`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `The value of field "${fieldPath}" is '${serializedValue}'.`
|
|
168
|
+
})
|
|
169
|
+
.filter((line): line is string => Boolean(line))
|
|
170
|
+
|
|
171
|
+
return promptLines.length > 0 ? promptLines.join("\n") : null
|
|
172
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Path } from "sanity"
|
|
2
|
+
import type { GenerateButtonTarget } from "./shared"
|
|
3
|
+
|
|
4
|
+
export function pathToFieldPath(path: Path): string {
|
|
5
|
+
return path
|
|
6
|
+
.map((segment) => {
|
|
7
|
+
if (typeof segment === "string" || typeof segment === "number") {
|
|
8
|
+
return String(segment)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (segment && typeof segment === "object" && "_key" in segment) {
|
|
12
|
+
return String(segment._key)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return ""
|
|
16
|
+
})
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(".")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getValueAtPath(
|
|
22
|
+
value: Record<string, unknown> | undefined,
|
|
23
|
+
path: string | undefined
|
|
24
|
+
): unknown {
|
|
25
|
+
if (!value || !path) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return path.split(".").reduce<unknown>((currentValue, segment) => {
|
|
30
|
+
if (!currentValue || typeof currentValue !== "object") {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (currentValue as Record<string, unknown>)[segment]
|
|
35
|
+
}, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function doesTargetMatchField(options: {
|
|
39
|
+
documentType: string | undefined
|
|
40
|
+
path: Path
|
|
41
|
+
target: GenerateButtonTarget
|
|
42
|
+
}): boolean {
|
|
43
|
+
if (!options.documentType) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
options.documentType === options.target.documentType &&
|
|
49
|
+
pathToFieldPath(options.path) === options.target.fieldPath
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const SUPPORTED_AI_IMAGE_MODELS = [
|
|
2
|
+
"gemini-2.5-flash-image",
|
|
3
|
+
"gemini-3.1-flash-image-preview",
|
|
4
|
+
"gpt-image-1",
|
|
5
|
+
] as const
|
|
6
|
+
|
|
7
|
+
export type SupportedAiImageModelId = (typeof SUPPORTED_AI_IMAGE_MODELS)[number]
|
|
8
|
+
|
|
9
|
+
export type SupportedAiImageProvider = "google" | "openai"
|
|
10
|
+
|
|
11
|
+
export type SupportedAiImageModelDefinition = {
|
|
12
|
+
defaultAspectRatio?: string
|
|
13
|
+
defaultSize?: string
|
|
14
|
+
id: SupportedAiImageModelId
|
|
15
|
+
provider: SupportedAiImageProvider
|
|
16
|
+
supportsReferenceImages: boolean
|
|
17
|
+
title: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_SUPPORTED_AI_IMAGE_MODEL: SupportedAiImageModelId =
|
|
21
|
+
"gemini-2.5-flash-image"
|
|
22
|
+
|
|
23
|
+
const SUPPORTED_AI_IMAGE_MODEL_DEFINITIONS = [
|
|
24
|
+
{
|
|
25
|
+
defaultAspectRatio: "1:1",
|
|
26
|
+
id: "gemini-2.5-flash-image",
|
|
27
|
+
provider: "google",
|
|
28
|
+
supportsReferenceImages: true,
|
|
29
|
+
title: "Google Gemini 2.5 Flash Image",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
defaultAspectRatio: "1:1",
|
|
33
|
+
id: "gemini-3.1-flash-image-preview",
|
|
34
|
+
provider: "google",
|
|
35
|
+
supportsReferenceImages: true,
|
|
36
|
+
title: "Google Gemini 3.1 Flash Image Preview",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
defaultSize: "1024x1024",
|
|
40
|
+
id: "gpt-image-1",
|
|
41
|
+
provider: "openai",
|
|
42
|
+
supportsReferenceImages: true,
|
|
43
|
+
title: "OpenAI GPT Image 1",
|
|
44
|
+
},
|
|
45
|
+
] as const satisfies readonly SupportedAiImageModelDefinition[]
|
|
46
|
+
|
|
47
|
+
const SUPPORTED_AI_IMAGE_MODEL_DEFINITION_MAP = Object.fromEntries(
|
|
48
|
+
SUPPORTED_AI_IMAGE_MODEL_DEFINITIONS.map((modelDefinition) => [
|
|
49
|
+
modelDefinition.id,
|
|
50
|
+
modelDefinition,
|
|
51
|
+
])
|
|
52
|
+
) as Record<SupportedAiImageModelId, SupportedAiImageModelDefinition>
|
|
53
|
+
|
|
54
|
+
export function getSupportedAiImageModelDefinition(
|
|
55
|
+
modelId: SupportedAiImageModelId
|
|
56
|
+
): SupportedAiImageModelDefinition {
|
|
57
|
+
return SUPPORTED_AI_IMAGE_MODEL_DEFINITION_MAP[modelId]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getSupportedAiImageModelOptions(
|
|
61
|
+
modelIds: readonly SupportedAiImageModelId[]
|
|
62
|
+
): Array<{ title: string; value: SupportedAiImageModelId }> {
|
|
63
|
+
return modelIds.map((modelId) => {
|
|
64
|
+
const modelDefinition = getSupportedAiImageModelDefinition(modelId)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
title: modelDefinition.title,
|
|
68
|
+
value: modelDefinition.id,
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isSupportedAiImageModelId(
|
|
74
|
+
value: string
|
|
75
|
+
): value is SupportedAiImageModelId {
|
|
76
|
+
return (
|
|
77
|
+
SUPPORTED_AI_IMAGE_MODELS as readonly string[]
|
|
78
|
+
).includes(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeAllowedAiImageModels(
|
|
82
|
+
allowedModels?: readonly string[]
|
|
83
|
+
): SupportedAiImageModelId[] {
|
|
84
|
+
if (!allowedModels) {
|
|
85
|
+
return [DEFAULT_SUPPORTED_AI_IMAGE_MODEL]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (allowedModels.length === 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"AI Image Plugin requires at least one allowed model in allowedModels."
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const normalizedAllowedModels: SupportedAiImageModelId[] = []
|
|
95
|
+
|
|
96
|
+
for (const allowedModel of allowedModels) {
|
|
97
|
+
if (!isSupportedAiImageModelId(allowedModel)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`AI Image Plugin does not support the model "${allowedModel}".`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!normalizedAllowedModels.includes(allowedModel)) {
|
|
104
|
+
normalizedAllowedModels.push(allowedModel)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalizedAllowedModels
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getDefaultAllowedAiImageModel(
|
|
112
|
+
allowedModels: readonly SupportedAiImageModelId[]
|
|
113
|
+
): SupportedAiImageModelId {
|
|
114
|
+
return allowedModels[0] ?? DEFAULT_SUPPORTED_AI_IMAGE_MODEL
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveAllowedAiImageModel(
|
|
118
|
+
modelId: string | null | undefined,
|
|
119
|
+
allowedModels: readonly SupportedAiImageModelId[]
|
|
120
|
+
): SupportedAiImageModelId {
|
|
121
|
+
if (modelId && allowedModels.includes(modelId as SupportedAiImageModelId)) {
|
|
122
|
+
return modelId as SupportedAiImageModelId
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return getDefaultAllowedAiImageModel(allowedModels)
|
|
126
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { SupportedAiImageModelId } from "./models"
|
|
2
|
+
|
|
3
|
+
export const SOURCE_NAME = "ai-image-plugin"
|
|
4
|
+
export const SOURCE_TITLE = "AI Image Plugin"
|
|
5
|
+
export const SETTINGS_DOCUMENT_ID = "aiImagePlugin.settings"
|
|
6
|
+
export const SETTINGS_SCHEMA_TYPE = "aiImagePluginSettings"
|
|
7
|
+
export const SETTINGS_TOOL_NAME = "ai-image-plugin-settings"
|
|
8
|
+
export const SETTINGS_TOOL_TITLE = "AI Image Plugin"
|
|
9
|
+
export const DEFAULT_API_ENDPOINT = "/api/ai-image-plugin"
|
|
10
|
+
export const DEFAULT_ASSET_SOURCE_TARGET_ID = "ai-image-plugin-asset-source"
|
|
11
|
+
export const MAX_REFERENCE_IMAGES = 5
|
|
12
|
+
|
|
13
|
+
export type StoredImage = {
|
|
14
|
+
asset?: {
|
|
15
|
+
_id?: string
|
|
16
|
+
_ref?: string
|
|
17
|
+
originalFilename?: string
|
|
18
|
+
url?: string
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SettingsTargetConfigValue = {
|
|
23
|
+
prompt?: string
|
|
24
|
+
referenceImages?: StoredImage[]
|
|
25
|
+
targetId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SettingsValue = {
|
|
29
|
+
globalModel?: SupportedAiImageModelId
|
|
30
|
+
globalPrompt?: string
|
|
31
|
+
globalReferenceImages?: StoredImage[]
|
|
32
|
+
targetConfigs?: SettingsTargetConfigValue[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type GenerateButtonTarget = {
|
|
36
|
+
description?: string
|
|
37
|
+
dialogTitle?: string
|
|
38
|
+
documentType: string
|
|
39
|
+
fieldPath: string
|
|
40
|
+
id: string
|
|
41
|
+
promptLabel?: string
|
|
42
|
+
promptPlaceholder?: string
|
|
43
|
+
suggestedContextFieldPaths?: string[]
|
|
44
|
+
title?: string
|
|
45
|
+
type: "generateButton"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type AssetSourceTarget = {
|
|
49
|
+
description?: string
|
|
50
|
+
id: string
|
|
51
|
+
promptLabel?: string
|
|
52
|
+
promptPlaceholder?: string
|
|
53
|
+
title?: string
|
|
54
|
+
type: "assetSource"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PluginOptions = {
|
|
58
|
+
apiEndpoint?: string
|
|
59
|
+
apiVersion: string
|
|
60
|
+
allowedModels?: SupportedAiImageModelId[]
|
|
61
|
+
assetSource?:
|
|
62
|
+
| boolean
|
|
63
|
+
| {
|
|
64
|
+
description?: string
|
|
65
|
+
enabled?: boolean
|
|
66
|
+
id?: string
|
|
67
|
+
promptLabel?: string
|
|
68
|
+
promptPlaceholder?: string
|
|
69
|
+
title?: string
|
|
70
|
+
}
|
|
71
|
+
settingsTool?: {
|
|
72
|
+
name?: string
|
|
73
|
+
registerSchemaType?: boolean
|
|
74
|
+
title?: string
|
|
75
|
+
}
|
|
76
|
+
targets?: GenerateButtonTarget[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ResolvedOptions = {
|
|
80
|
+
apiEndpoint: string
|
|
81
|
+
apiVersion: string
|
|
82
|
+
allowedModels: SupportedAiImageModelId[]
|
|
83
|
+
assetSourceTarget: AssetSourceTarget | null
|
|
84
|
+
registerSettingsSchemaType: boolean
|
|
85
|
+
settingsToolName: string
|
|
86
|
+
settingsToolTitle: string
|
|
87
|
+
targets: GenerateButtonTarget[]
|
|
88
|
+
}
|