@basementstudio/sanity-ai-image-plugin 0.0.1 → 0.1.1
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 +300 -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 +169 -0
- package/dist/studio/components/asset-source.js.map +1 -0
- package/dist/studio/components/frontend-rate-limit.d.ts +29 -0
- package/dist/studio/components/frontend-rate-limit.d.ts.map +1 -0
- package/dist/studio/components/frontend-rate-limit.js +197 -0
- package/dist/studio/components/frontend-rate-limit.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 +194 -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/frontend-rate-limit.d.ts +29 -0
- package/dist/studio/frontend-rate-limit.d.ts.map +1 -0
- package/dist/studio/frontend-rate-limit.js +197 -0
- package/dist/studio/frontend-rate-limit.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 +59 -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 +96 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +17 -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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSecrets } from "@sanity/studio-secrets";
|
|
4
|
+
import { Box, Button, Card, Flex, Select, Stack, Text, TextArea, } from "@sanity/ui";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { useClient, } from "sanity";
|
|
7
|
+
import { getDefaultAllowedAiImageModel, getSupportedAiImageModelOptions, } from "../../utils/models";
|
|
8
|
+
import { MAX_REFERENCE_IMAGES, SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE, SOURCE_NAME, SOURCE_TITLE, } from "../../utils/shared";
|
|
9
|
+
import { getMissingSharedSecretMessage, getSharedSecretValue, SHARED_SECRET_HEADER_NAME, SHARED_SECRET_NAMESPACE, } from "../shared-secret";
|
|
10
|
+
import { composePrompt } from "../../utils/prompts";
|
|
11
|
+
import { buildSettingsReferenceImages, fetchSettingsDocument, findTargetSettings, getErrorMessage, resolveSettingsModel, } from "../settings-data";
|
|
12
|
+
import { formatRateLimitRemainingTime, useFrontendRateLimit, } from "./frontend-rate-limit";
|
|
13
|
+
import { convertImageFileToPng, createGeneratedFile, formatFileSize, } from "../files";
|
|
14
|
+
function getGenerationPhaseLabel(phase) {
|
|
15
|
+
switch (phase) {
|
|
16
|
+
case "preparing":
|
|
17
|
+
return "Preparing...";
|
|
18
|
+
case "generating":
|
|
19
|
+
return "Generating...";
|
|
20
|
+
case "uploading":
|
|
21
|
+
return "Uploading to Sanity...";
|
|
22
|
+
case "updating":
|
|
23
|
+
return "Updating field...";
|
|
24
|
+
default:
|
|
25
|
+
return "Generate with AI Image Plugin";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function createAssetSource(options) {
|
|
29
|
+
const assetSourceTarget = options.assetSourceTarget;
|
|
30
|
+
function AssetSourceComponent({ onClose, onSelect, }) {
|
|
31
|
+
const client = useClient({ apiVersion: options.apiVersion });
|
|
32
|
+
const { loading: isLoadingSharedSecret, secrets } = useSecrets(SHARED_SECRET_NAMESPACE);
|
|
33
|
+
const [prompt, setPrompt] = useState("");
|
|
34
|
+
const [selectedModel, setSelectedModel] = useState(getDefaultAllowedAiImageModel(options.allowedModels));
|
|
35
|
+
const [referenceImages, setReferenceImages] = useState([]);
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const [generationPhase, setGenerationPhase] = useState("idle");
|
|
38
|
+
const isGenerating = generationPhase !== "idle";
|
|
39
|
+
const sharedSecret = getSharedSecretValue(secrets);
|
|
40
|
+
const hasCustomSelectedModelRef = useRef(false);
|
|
41
|
+
const frontendRateLimit = useFrontendRateLimit(options.frontendRateLimit);
|
|
42
|
+
const allowedModelOptions = useMemo(() => getSupportedAiImageModelOptions(options.allowedModels), [options.allowedModels]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let isMounted = true;
|
|
45
|
+
setSelectedModel(getDefaultAllowedAiImageModel(options.allowedModels));
|
|
46
|
+
hasCustomSelectedModelRef.current = false;
|
|
47
|
+
async function loadDefaultModel() {
|
|
48
|
+
try {
|
|
49
|
+
const settings = await fetchSettingsDocument(client, SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE);
|
|
50
|
+
if (!isMounted || hasCustomSelectedModelRef.current) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setSelectedModel(resolveSettingsModel({
|
|
54
|
+
allowedModels: options.allowedModels,
|
|
55
|
+
settings,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Keep the first allowed model selected if settings loading fails.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
void loadDefaultModel();
|
|
63
|
+
return () => {
|
|
64
|
+
isMounted = false;
|
|
65
|
+
};
|
|
66
|
+
}, [client, options.allowedModels]);
|
|
67
|
+
function handleReferenceImageChange(event) {
|
|
68
|
+
const nextFiles = Array.from(event.currentTarget.files ?? []);
|
|
69
|
+
if (nextFiles.length === 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setReferenceImages((currentFiles) => [...currentFiles, ...nextFiles].slice(0, MAX_REFERENCE_IMAGES));
|
|
73
|
+
event.currentTarget.value = "";
|
|
74
|
+
}
|
|
75
|
+
function handleRemoveReferenceImage(indexToRemove) {
|
|
76
|
+
setReferenceImages((currentFiles) => currentFiles.filter((_, index) => index !== indexToRemove));
|
|
77
|
+
}
|
|
78
|
+
async function handleGenerate() {
|
|
79
|
+
setError(null);
|
|
80
|
+
setGenerationPhase("preparing");
|
|
81
|
+
try {
|
|
82
|
+
if (isLoadingSharedSecret) {
|
|
83
|
+
throw new Error("Loading AI Image Plugin shared secret...");
|
|
84
|
+
}
|
|
85
|
+
if (!sharedSecret) {
|
|
86
|
+
throw new Error(getMissingSharedSecretMessage());
|
|
87
|
+
}
|
|
88
|
+
const settings = await fetchSettingsDocument(client, SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE);
|
|
89
|
+
const targetSettings = assetSourceTarget
|
|
90
|
+
? findTargetSettings(settings, assetSourceTarget.id)
|
|
91
|
+
: null;
|
|
92
|
+
const composedPrompt = composePrompt([
|
|
93
|
+
settings?.globalPrompt
|
|
94
|
+
? `Global direction:\n${settings.globalPrompt}`
|
|
95
|
+
: null,
|
|
96
|
+
targetSettings?.prompt
|
|
97
|
+
? `Asset source direction:\n${targetSettings.prompt}`
|
|
98
|
+
: null,
|
|
99
|
+
prompt.trim() ? `Editor prompt:\n${prompt.trim()}` : null,
|
|
100
|
+
]);
|
|
101
|
+
if (!composedPrompt) {
|
|
102
|
+
throw new Error("Add a prompt or configure a default prompt first.");
|
|
103
|
+
}
|
|
104
|
+
const formData = new FormData();
|
|
105
|
+
const normalizedReferenceImages = await Promise.all(referenceImages.map(convertImageFileToPng));
|
|
106
|
+
const settingsReferenceImages = await buildSettingsReferenceImages(settings, assetSourceTarget?.id);
|
|
107
|
+
const allReferenceImages = [
|
|
108
|
+
...settingsReferenceImages,
|
|
109
|
+
...normalizedReferenceImages,
|
|
110
|
+
].slice(0, MAX_REFERENCE_IMAGES);
|
|
111
|
+
formData.set("prompt", composedPrompt);
|
|
112
|
+
formData.set("model", selectedModel);
|
|
113
|
+
for (const referenceImage of allReferenceImages) {
|
|
114
|
+
formData.append("references", referenceImage);
|
|
115
|
+
}
|
|
116
|
+
const rateLimitSnapshot = frontendRateLimit.consumeAttempt();
|
|
117
|
+
if (!rateLimitSnapshot.allowed) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setGenerationPhase("generating");
|
|
121
|
+
const response = await fetch(options.apiEndpoint, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
body: formData,
|
|
124
|
+
headers: {
|
|
125
|
+
[SHARED_SECRET_HEADER_NAME]: sharedSecret,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const payload = (await response.json());
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error("error" in payload && payload.error
|
|
131
|
+
? payload.error
|
|
132
|
+
: "AI image generation failed.");
|
|
133
|
+
}
|
|
134
|
+
if (!("data" in payload) ||
|
|
135
|
+
typeof payload.data !== "string" ||
|
|
136
|
+
typeof payload.mimeType !== "string") {
|
|
137
|
+
throw new Error("AI Image Plugin returned an invalid image payload.");
|
|
138
|
+
}
|
|
139
|
+
const generatedFile = createGeneratedFile(payload, prompt, "ai-image-plugin");
|
|
140
|
+
setGenerationPhase("uploading");
|
|
141
|
+
const uploadedAsset = await client.assets.upload("image", generatedFile, {
|
|
142
|
+
filename: generatedFile.name,
|
|
143
|
+
});
|
|
144
|
+
setGenerationPhase("updating");
|
|
145
|
+
onSelect([{ kind: "assetDocumentId", value: uploadedAsset._id }]);
|
|
146
|
+
onClose();
|
|
147
|
+
}
|
|
148
|
+
catch (nextError) {
|
|
149
|
+
setError(getErrorMessage(nextError));
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
setGenerationPhase("idle");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return (_jsx(Card, { padding: 4, children: _jsxs(Stack, { space: 4, children: [_jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 2, weight: "semibold", children: "Generate an image with AI Image Plugin and drop it straight into this field." }), _jsx(Text, { muted: true, size: 1, children: "Project defaults from the AI Image Plugin settings tool are included automatically when configured." })] }), _jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "medium", children: "Model" }), _jsx(Box, { marginTop: 2, children: _jsx(Select, { onChange: (event) => {
|
|
156
|
+
hasCustomSelectedModelRef.current = true;
|
|
157
|
+
setSelectedModel(event.currentTarget.value);
|
|
158
|
+
}, value: selectedModel, children: allowedModelOptions.map((modelOption) => (_jsx("option", { value: modelOption.value, children: modelOption.title }, modelOption.value))) }) })] }), _jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "medium", children: assetSourceTarget?.promptLabel || "Prompt" }), _jsx(Box, { marginTop: 2, children: _jsx(TextArea, { onChange: (event) => setPrompt(event.currentTarget.value), placeholder: assetSourceTarget?.promptPlaceholder ||
|
|
159
|
+
"Turn these references into a clean homepage hero image with warm daylight and subtle depth.", rows: 6, value: prompt }) })] }), _jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "medium", children: "Reference images" }), _jsx(Box, { marginTop: 2, children: _jsx("input", { accept: "image/*", multiple: true, onChange: handleReferenceImageChange, type: "file" }) }), referenceImages.length > 0 ? (_jsx(Box, { marginTop: 3, children: _jsx(Stack, { space: 2, children: referenceImages.map((referenceImage, index) => (_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Flex, { align: "center", gap: 3, justify: "space-between", children: [_jsxs(Box, { flex: 1, children: [_jsx(Text, { size: 1, weight: "medium", children: referenceImage.name }), _jsx(Text, { muted: true, size: 1, children: formatFileSize(referenceImage.size) })] }), _jsx(Button, { mode: "bleed", onClick: () => handleRemoveReferenceImage(index), text: "Remove" })] }) }, `${referenceImage.name}-${index}`))) }) })) : null] }), error ? (_jsx(Card, { padding: 3, radius: 2, tone: "critical", children: _jsx(Text, { size: 1, children: error }) })) : null, frontendRateLimit.blocked && !isGenerating ? (_jsx(Card, { padding: 3, radius: 2, tone: "caution", children: _jsxs(Text, { size: 1, children: ["Please wait", " ", formatRateLimitRemainingTime(frontendRateLimit.remainingMs), " ", "before generating again."] }) })) : null, isGenerating ? (_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Text, { size: 1, children: [getGenerationPhaseLabel(generationPhase), " This can take a minute or two for slower models."] }) })) : null, _jsxs(Flex, { gap: 3, justify: "flex-end", children: [_jsx(Button, { mode: "ghost", onClick: onClose, text: "Cancel" }), _jsx(Button, { disabled: isGenerating ||
|
|
160
|
+
isLoadingSharedSecret ||
|
|
161
|
+
frontendRateLimit.blocked, onClick: () => void handleGenerate(), text: getGenerationPhaseLabel(generationPhase), tone: "primary" })] })] }) }));
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
name: SOURCE_NAME,
|
|
165
|
+
title: SOURCE_TITLE,
|
|
166
|
+
component: AssetSourceComponent,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=asset-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-source.js","sourceRoot":"","sources":["../../../src/studio/components/asset-source.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EACL,GAAG,EACH,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,KAAK,EACL,IAAI,EACJ,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,OAAO,EAAoB,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC/E,OAAO,EAGL,SAAS,GACV,MAAM,QAAQ,CAAC;AAChB,OAAO,EACL,6BAA6B,EAC7B,+BAA+B,GAEhC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,YAAY,GAEb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,6BAA6B,EAC7B,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,GACf,MAAM,UAAU,CAAC;AAelB,SAAS,uBAAuB,CAAC,KAAsB;IACrD,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,WAAW;YACd,OAAO,cAAc,CAAC;QACxB,KAAK,YAAY;YACf,OAAO,eAAe,CAAC;QACzB,KAAK,WAAW;YACd,OAAO,wBAAwB,CAAC;QAClC,KAAK,UAAU;YACb,OAAO,mBAAmB,CAAC;QAC7B;YACE,OAAO,+BAA+B,CAAC;IAC3C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAwB;IACxD,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAEpD,SAAS,oBAAoB,CAAC,EAC5B,OAAO,EACP,QAAQ,GACkB;QAC1B,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QAC7D,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,OAAO,EAAE,GAC/C,UAAU,CAAuB,uBAAuB,CAAC,CAAC;QAC5D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAChD,6BAA6B,CAAC,OAAO,CAAC,aAAa,CAAC,CACrD,CAAC;QACF,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAS,EAAE,CAAC,CAAC;QACnE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;QACxD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GACzC,QAAQ,CAAkB,MAAM,CAAC,CAAC;QACpC,MAAM,YAAY,GAAG,eAAe,KAAK,MAAM,CAAC;QAChD,MAAM,YAAY,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,yBAAyB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,iBAAiB,GAAG,oBAAoB,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC1E,MAAM,mBAAmB,GAAG,OAAO,CACjC,GAAG,EAAE,CAAC,+BAA+B,CAAC,OAAO,CAAC,aAAa,CAAC,EAC5D,CAAC,OAAO,CAAC,aAAa,CAAC,CACxB,CAAC;QAEF,SAAS,CAAC,GAAG,EAAE;YACb,IAAI,SAAS,GAAG,IAAI,CAAC;YAErB,gBAAgB,CAAC,6BAA6B,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;YACvE,yBAAyB,CAAC,OAAO,GAAG,KAAK,CAAC;YAE1C,KAAK,UAAU,gBAAgB;gBAC7B,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAC1C,MAAM,EACN,oBAAoB,EACpB,oBAAoB,CACrB,CAAC;oBAEF,IAAI,CAAC,SAAS,IAAI,yBAAyB,CAAC,OAAO,EAAE,CAAC;wBACpD,OAAO;oBACT,CAAC;oBAED,gBAAgB,CACd,oBAAoB,CAAC;wBACnB,aAAa,EAAE,OAAO,CAAC,aAAa;wBACpC,QAAQ;qBACT,CAAC,CACH,CAAC;gBACJ,CAAC;gBAAC,MAAM,CAAC;oBACP,mEAAmE;gBACrE,CAAC;YACH,CAAC;YAED,KAAK,gBAAgB,EAAE,CAAC;YAExB,OAAO,GAAG,EAAE;gBACV,SAAS,GAAG,KAAK,CAAC;YACpB,CAAC,CAAC;QACJ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;QAEpC,SAAS,0BAA0B,CAAC,KAAoC;YACtE,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;YAE9D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,OAAO;YACT,CAAC;YAED,kBAAkB,CAAC,CAAC,YAAY,EAAE,EAAE,CAClC,CAAC,GAAG,YAAY,EAAE,GAAG,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAC/D,CAAC;YACF,KAAK,CAAC,aAAa,CAAC,KAAK,GAAG,EAAE,CAAC;QACjC,CAAC;QAED,SAAS,0BAA0B,CAAC,aAAqB;YACvD,kBAAkB,CAAC,CAAC,YAAY,EAAE,EAAE,CAClC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,aAAa,CAAC,CAC3D,CAAC;QACJ,CAAC;QAED,KAAK,UAAU,cAAc;YAC3B,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,kBAAkB,CAAC,WAAW,CAAC,CAAC;YAEhC,IAAI,CAAC;gBACH,IAAI,qBAAqB,EAAE,CAAC;oBAC1B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;gBAC9D,CAAC;gBAED,IAAI,CAAC,YAAY,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,6BAA6B,EAAE,CAAC,CAAC;gBACnD,CAAC;gBAED,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAC1C,MAAM,EACN,oBAAoB,EACpB,oBAAoB,CACrB,CAAC;gBACF,MAAM,cAAc,GAAG,iBAAiB;oBACtC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE,CAAC;oBACpD,CAAC,CAAC,IAAI,CAAC;gBACT,MAAM,cAAc,GAAG,aAAa,CAAC;oBACnC,QAAQ,EAAE,YAAY;wBACpB,CAAC,CAAC,sBAAsB,QAAQ,CAAC,YAAY,EAAE;wBAC/C,CAAC,CAAC,IAAI;oBACR,cAAc,EAAE,MAAM;wBACpB,CAAC,CAAC,4BAA4B,cAAc,CAAC,MAAM,EAAE;wBACrD,CAAC,CAAC,IAAI;oBACR,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,mBAAmB,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI;iBAC1D,CAAC,CAAC;gBAEH,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;gBACvE,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;gBAChC,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,GAAG,CACjD,eAAe,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAC3C,CAAC;gBACF,MAAM,uBAAuB,GAAG,MAAM,4BAA4B,CAChE,QAAQ,EACR,iBAAiB,EAAE,EAAE,CACtB,CAAC;gBACF,MAAM,kBAAkB,GAAG;oBACzB,GAAG,uBAAuB;oBAC1B,GAAG,yBAAyB;iBAC7B,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC;gBAEjC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;gBACvC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;gBAErC,KAAK,MAAM,cAAc,IAAI,kBAAkB,EAAE,CAAC;oBAChD,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;gBAChD,CAAC;gBAED,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,cAAc,EAAE,CAAC;gBAE7D,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,CAAC;oBAC/B,OAAO;gBACT,CAAC;gBAED,kBAAkB,CAAC,YAAY,CAAC,CAAC;gBACjC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE;oBAChD,MAAM,EAAE,MAAM;oBACd,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE;wBACP,CAAC,yBAAyB,CAAC,EAAE,YAAY;qBAC1C;iBACF,CAAC,CAAC;gBACH,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAEhB,CAAC;gBAEvB,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;oBACjB,MAAM,IAAI,KAAK,CACb,OAAO,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK;wBACjC,CAAC,CAAC,OAAO,CAAC,KAAK;wBACf,CAAC,CAAC,6BAA6B,CAClC,CAAC;gBACJ,CAAC;gBAED,IACE,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC;oBACpB,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;oBAChC,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,EACpC,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;gBACxE,CAAC;gBAED,MAAM,aAAa,GAAG,mBAAmB,CACvC,OAAO,EACP,MAAM,EACN,iBAAiB,CAClB,CAAC;gBACF,kBAAkB,CAAC,WAAW,CAAC,CAAC;gBAChC,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAC9C,OAAO,EACP,aAAa,EACb;oBACE,QAAQ,EAAE,aAAa,CAAC,IAAI;iBAC7B,CACF,CAAC;gBAEF,kBAAkB,CAAC,UAAU,CAAC,CAAC;gBAC/B,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAClE,OAAO,EAAE,CAAC;YACZ,CAAC;YAAC,OAAO,SAAS,EAAE,CAAC;gBACnB,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC;YACvC,CAAC;oBAAS,CAAC;gBACT,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,OAAO,CACL,KAAC,IAAI,IAAC,OAAO,EAAE,CAAC,YACd,MAAC,KAAK,IAAC,KAAK,EAAE,CAAC,aACb,MAAC,KAAK,IAAC,KAAK,EAAE,CAAC,aACb,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAC,UAAU,6FAGzB,EACP,KAAC,IAAI,IAAC,KAAK,QAAC,IAAI,EAAE,CAAC,oHAGZ,IACD,EAER,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAC,QAAQ,sBAEvB,EACP,KAAC,GAAG,IAAC,SAAS,EAAE,CAAC,YACf,KAAC,MAAM,IACL,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;wCAClB,yBAAyB,CAAC,OAAO,GAAG,IAAI,CAAC;wCACzC,gBAAgB,CACd,KAAK,CAAC,aAAa,CAAC,KAAgC,CACrD,CAAC;oCACJ,CAAC,EACD,KAAK,EAAE,aAAa,YAEnB,mBAAmB,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACxC,iBAAgC,KAAK,EAAE,WAAW,CAAC,KAAK,YACrD,WAAW,CAAC,KAAK,IADP,WAAW,CAAC,KAAK,CAErB,CACV,CAAC,GACK,GACL,IACF,EAEN,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAC,QAAQ,YAC3B,iBAAiB,EAAE,WAAW,IAAI,QAAQ,GACtC,EACP,KAAC,GAAG,IAAC,SAAS,EAAE,CAAC,YACf,KAAC,QAAQ,IACP,QAAQ,EAAE,CAAC,KAAuC,EAAE,EAAE,CACpD,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,EAEtC,WAAW,EACT,iBAAiB,EAAE,iBAAiB;wCACpC,6FAA6F,EAE/F,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,MAAM,GACb,GACE,IACF,EAEN,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAC,QAAQ,iCAEvB,EACP,KAAC,GAAG,IAAC,SAAS,EAAE,CAAC,YACf,gBACE,MAAM,EAAC,SAAS,EAChB,QAAQ,QACR,QAAQ,EAAE,0BAA0B,EACpC,IAAI,EAAC,MAAM,GACX,GACE,EACL,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAC5B,KAAC,GAAG,IAAC,SAAS,EAAE,CAAC,YACf,KAAC,KAAK,IAAC,KAAK,EAAE,CAAC,YACZ,eAAe,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,CAAC,CAC9C,KAAC,IAAI,IAEH,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,CAAC,EACT,IAAI,EAAC,aAAa,YAElB,MAAC,IAAI,IAAC,KAAK,EAAC,QAAQ,EAAC,GAAG,EAAE,CAAC,EAAE,OAAO,EAAC,eAAe,aAClD,MAAC,GAAG,IAAC,IAAI,EAAE,CAAC,aACV,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAC,QAAQ,YAC3B,cAAc,CAAC,IAAI,GACf,EACP,KAAC,IAAI,IAAC,KAAK,QAAC,IAAI,EAAE,CAAC,YAChB,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,GAC/B,IACH,EACN,KAAC,MAAM,IACL,IAAI,EAAC,OAAO,EACZ,OAAO,EAAE,GAAG,EAAE,CAAC,0BAA0B,CAAC,KAAK,CAAC,EAChD,IAAI,EAAC,QAAQ,GACb,IACG,IAnBF,GAAG,cAAc,CAAC,IAAI,IAAI,KAAK,EAAE,CAoBjC,CACR,CAAC,GACI,GACJ,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,EAEL,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,IAAI,IAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAC,UAAU,YAC1C,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,YAAG,KAAK,GAAQ,GACxB,CACR,CAAC,CAAC,CAAC,IAAI,EAEP,iBAAiB,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAC5C,KAAC,IAAI,IAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAC,SAAS,YACzC,MAAC,IAAI,IAAC,IAAI,EAAE,CAAC,4BACC,GAAG,EACd,4BAA4B,CAAC,iBAAiB,CAAC,WAAW,CAAC,EAAE,GAAG,gCAE5D,GACF,CACR,CAAC,CAAC,CAAC,IAAI,EAEP,YAAY,CAAC,CAAC,CAAC,CACd,KAAC,IAAI,IAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAC,aAAa,YAC7C,MAAC,IAAI,IAAC,IAAI,EAAE,CAAC,aACV,uBAAuB,CAAC,eAAe,CAAC,yDAEpC,GACF,CACR,CAAC,CAAC,CAAC,IAAI,EAER,MAAC,IAAI,IAAC,GAAG,EAAE,CAAC,EAAE,OAAO,EAAC,UAAU,aAC9B,KAAC,MAAM,IAAC,IAAI,EAAC,OAAO,EAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAC,QAAQ,GAAG,EACvD,KAAC,MAAM,IACL,QAAQ,EACN,YAAY;oCACZ,qBAAqB;oCACrB,iBAAiB,CAAC,OAAO,EAE3B,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,cAAc,EAAE,EACpC,IAAI,EAAE,uBAAuB,CAAC,eAAe,CAAC,EAC9C,IAAI,EAAC,SAAS,GACd,IACG,IACD,GACH,CACR,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,YAAY;QACnB,SAAS,EAAE,oBAAoB;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolvedFrontendRateLimitOptions } from "../../utils/shared";
|
|
2
|
+
export declare const FRONTEND_RATE_LIMIT_STORAGE_KEY = "ai-image-plugin.frontend-rate-limit.v1";
|
|
3
|
+
type FrontendRateLimitState = {
|
|
4
|
+
attempts: number[];
|
|
5
|
+
};
|
|
6
|
+
type FrontendRateLimitEnvironment = {
|
|
7
|
+
fallbackState?: FrontendRateLimitState;
|
|
8
|
+
storage?: Pick<Storage, "getItem" | "setItem"> | null;
|
|
9
|
+
};
|
|
10
|
+
export type FrontendRateLimitSnapshot = {
|
|
11
|
+
attempts: number[];
|
|
12
|
+
blocked: boolean;
|
|
13
|
+
remainingMs: number;
|
|
14
|
+
};
|
|
15
|
+
export type FrontendRateLimitAttemptResult = FrontendRateLimitSnapshot & {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function createFrontendRateLimiter(options: ResolvedFrontendRateLimitOptions | false, environment?: FrontendRateLimitEnvironment): {
|
|
19
|
+
consumeAttempt(now?: number): FrontendRateLimitAttemptResult;
|
|
20
|
+
getSnapshot(now?: number): FrontendRateLimitSnapshot;
|
|
21
|
+
};
|
|
22
|
+
export declare function formatRateLimitRemainingTime(remainingMs: number): string;
|
|
23
|
+
export declare function useFrontendRateLimit(options: ResolvedFrontendRateLimitOptions | false): {
|
|
24
|
+
blocked: boolean;
|
|
25
|
+
consumeAttempt: () => FrontendRateLimitAttemptResult;
|
|
26
|
+
remainingMs: number;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=frontend-rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-rate-limit.d.ts","sourceRoot":"","sources":["../../../src/studio/components/frontend-rate-limit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,oBAAoB,CAAC;AAE3E,eAAO,MAAM,+BAA+B,2CACF,CAAC;AAI3C,KAAK,sBAAsB,GAAG;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG,yBAAyB,GAAG;IACvE,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAiKF,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GAAG,KAAK,EACjD,WAAW,CAAC,EAAE,4BAA4B;kCAGN,8BAA8B;+BAsBjC,yBAAyB;EAI3D;AAED,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAMxE;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,gCAAgC,GAAG,KAAK;;0BAuDtB,8BAA8B;;EAa1D"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
export const FRONTEND_RATE_LIMIT_STORAGE_KEY = "ai-image-plugin.frontend-rate-limit.v1";
|
|
4
|
+
const FRONTEND_RATE_LIMIT_SYNC_EVENT = "ai-image-plugin:frontend-rate-limit";
|
|
5
|
+
const memoryFallbackState = {
|
|
6
|
+
attempts: [],
|
|
7
|
+
};
|
|
8
|
+
function createEmptySnapshot() {
|
|
9
|
+
return {
|
|
10
|
+
attempts: [],
|
|
11
|
+
blocked: false,
|
|
12
|
+
remainingMs: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function getWindowMs(options) {
|
|
16
|
+
return options.windowSecs * 1000;
|
|
17
|
+
}
|
|
18
|
+
function hasSameAttempts(left, right) {
|
|
19
|
+
if (left.length !== right.length) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return left.every((value, index) => value === right[index]);
|
|
23
|
+
}
|
|
24
|
+
function pruneAttempts(attempts, windowMs, now) {
|
|
25
|
+
return attempts.filter((attempt) => now - attempt < windowMs);
|
|
26
|
+
}
|
|
27
|
+
function parseAttempts(value) {
|
|
28
|
+
if (!value) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const parsedValue = JSON.parse(value);
|
|
33
|
+
if (!Array.isArray(parsedValue.attempts)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return parsedValue.attempts
|
|
37
|
+
.filter((attempt) => typeof attempt === "number" && Number.isFinite(attempt))
|
|
38
|
+
.sort((left, right) => left - right);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveStorage(storage) {
|
|
45
|
+
if (storage !== undefined) {
|
|
46
|
+
return storage;
|
|
47
|
+
}
|
|
48
|
+
if (typeof window === "undefined") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return window.localStorage;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readAttempts(storage, fallbackState) {
|
|
59
|
+
if (!storage) {
|
|
60
|
+
return [...fallbackState.attempts];
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return parseAttempts(storage.getItem(FRONTEND_RATE_LIMIT_STORAGE_KEY));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [...fallbackState.attempts];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function persistAttempts(attempts, storage, fallbackState) {
|
|
70
|
+
fallbackState.attempts = [...attempts];
|
|
71
|
+
if (!storage) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
storage.setItem(FRONTEND_RATE_LIMIT_STORAGE_KEY, JSON.stringify({ attempts }));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Keep the in-memory fallback current when storage writes fail.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function notifyAttemptListeners() {
|
|
82
|
+
if (typeof window === "undefined") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
window.dispatchEvent(new Event(FRONTEND_RATE_LIMIT_SYNC_EVENT));
|
|
86
|
+
}
|
|
87
|
+
function buildSnapshot(attempts, options, now) {
|
|
88
|
+
const windowMs = getWindowMs(options);
|
|
89
|
+
const blocked = attempts.length >= options.maxRequests;
|
|
90
|
+
const relevantAttempt = blocked
|
|
91
|
+
? attempts[attempts.length - options.maxRequests]
|
|
92
|
+
: null;
|
|
93
|
+
return {
|
|
94
|
+
attempts,
|
|
95
|
+
blocked,
|
|
96
|
+
remainingMs: relevantAttempt === null
|
|
97
|
+
? 0
|
|
98
|
+
: Math.max(0, relevantAttempt + windowMs - now),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function readSnapshot(options, environment, now = Date.now()) {
|
|
102
|
+
if (!options) {
|
|
103
|
+
return createEmptySnapshot();
|
|
104
|
+
}
|
|
105
|
+
const fallbackState = environment?.fallbackState || memoryFallbackState;
|
|
106
|
+
const storage = resolveStorage(environment?.storage);
|
|
107
|
+
const storedAttempts = readAttempts(storage, fallbackState);
|
|
108
|
+
const attempts = pruneAttempts(storedAttempts, getWindowMs(options), now);
|
|
109
|
+
if (!hasSameAttempts(storedAttempts, attempts)) {
|
|
110
|
+
persistAttempts(attempts, storage, fallbackState);
|
|
111
|
+
}
|
|
112
|
+
return buildSnapshot(attempts, options, now);
|
|
113
|
+
}
|
|
114
|
+
export function createFrontendRateLimiter(options, environment) {
|
|
115
|
+
return {
|
|
116
|
+
consumeAttempt(now = Date.now()) {
|
|
117
|
+
const currentSnapshot = readSnapshot(options, environment, now);
|
|
118
|
+
if (!options || currentSnapshot.blocked) {
|
|
119
|
+
return {
|
|
120
|
+
...currentSnapshot,
|
|
121
|
+
allowed: !currentSnapshot.blocked,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const fallbackState = environment?.fallbackState || memoryFallbackState;
|
|
125
|
+
const storage = resolveStorage(environment?.storage);
|
|
126
|
+
const attempts = [...currentSnapshot.attempts, now];
|
|
127
|
+
persistAttempts(attempts, storage, fallbackState);
|
|
128
|
+
notifyAttemptListeners();
|
|
129
|
+
return {
|
|
130
|
+
...buildSnapshot(attempts, options, now),
|
|
131
|
+
allowed: true,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
getSnapshot(now = Date.now()) {
|
|
135
|
+
return readSnapshot(options, environment, now);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function formatRateLimitRemainingTime(remainingMs) {
|
|
140
|
+
const totalSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
|
|
141
|
+
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
|
|
142
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
143
|
+
return `${minutes}:${seconds}`;
|
|
144
|
+
}
|
|
145
|
+
export function useFrontendRateLimit(options) {
|
|
146
|
+
const maxRequests = options ? options.maxRequests : 0;
|
|
147
|
+
const windowSecs = options ? options.windowSecs : 0;
|
|
148
|
+
const [snapshot, setSnapshot] = useState(() => createFrontendRateLimiter(options).getSnapshot());
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
setSnapshot(createFrontendRateLimiter(options).getSnapshot());
|
|
151
|
+
}, [maxRequests, windowSecs]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (typeof window === "undefined") {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
function syncSnapshot() {
|
|
157
|
+
setSnapshot(createFrontendRateLimiter(options).getSnapshot());
|
|
158
|
+
}
|
|
159
|
+
function handleStorage(event) {
|
|
160
|
+
if (!event.key || event.key === FRONTEND_RATE_LIMIT_STORAGE_KEY) {
|
|
161
|
+
syncSnapshot();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function handleSync() {
|
|
165
|
+
syncSnapshot();
|
|
166
|
+
}
|
|
167
|
+
syncSnapshot();
|
|
168
|
+
window.addEventListener("storage", handleStorage);
|
|
169
|
+
window.addEventListener(FRONTEND_RATE_LIMIT_SYNC_EVENT, handleSync);
|
|
170
|
+
return () => {
|
|
171
|
+
window.removeEventListener("storage", handleStorage);
|
|
172
|
+
window.removeEventListener(FRONTEND_RATE_LIMIT_SYNC_EVENT, handleSync);
|
|
173
|
+
};
|
|
174
|
+
}, [maxRequests, windowSecs]);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!snapshot.blocked || typeof window === "undefined") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const intervalId = window.setInterval(() => {
|
|
180
|
+
setSnapshot(createFrontendRateLimiter(options).getSnapshot());
|
|
181
|
+
}, 1000);
|
|
182
|
+
return () => {
|
|
183
|
+
window.clearInterval(intervalId);
|
|
184
|
+
};
|
|
185
|
+
}, [snapshot.blocked, maxRequests, windowSecs]);
|
|
186
|
+
function consumeAttempt() {
|
|
187
|
+
const nextSnapshot = createFrontendRateLimiter(options).consumeAttempt();
|
|
188
|
+
setSnapshot(nextSnapshot);
|
|
189
|
+
return nextSnapshot;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
blocked: snapshot.blocked,
|
|
193
|
+
consumeAttempt,
|
|
194
|
+
remainingMs: snapshot.remainingMs,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=frontend-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-rate-limit.js","sourceRoot":"","sources":["../../../src/studio/components/frontend-rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ImageValue, type ObjectInputProps } from "sanity";
|
|
2
|
+
import { type GenerateButtonTarget, type ResolvedOptions } from "../../utils/shared";
|
|
3
|
+
export declare function GenerateButtonInput({ options, props, target, }: {
|
|
4
|
+
options: ResolvedOptions;
|
|
5
|
+
props: ObjectInputProps<ImageValue>;
|
|
6
|
+
target: GenerateButtonTarget;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=generate-button-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-button-input.d.ts","sourceRoot":"","sources":["../../../src/studio/components/generate-button-input.tsx"],"names":[],"mappings":"AAeA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,gBAAgB,EAMtB,MAAM,QAAQ,CAAC;AAkBhB,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AA0C5B,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,KAAK,EACL,MAAM,GACP,EAAE;IACD,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,EAAE,oBAAoB,CAAC;CAC9B,2CA6ZA"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Box, Button, Card, Dialog, Flex, Select, Stack, Text, TextArea, } from "@sanity/ui";
|
|
4
|
+
import { useSecrets } from "@sanity/studio-secrets";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { PatchEvent, set, setIfMissing, useClient, useFormValue, useSchema, } from "sanity";
|
|
7
|
+
import { getMissingSharedSecretMessage, getSharedSecretValue, SHARED_SECRET_HEADER_NAME, SHARED_SECRET_NAMESPACE, } from "../shared-secret";
|
|
8
|
+
import { buildContextFieldPrompt, getDefaultSelectedContextFieldPaths, getSelectableContextFields, } from "../../utils/context-fields";
|
|
9
|
+
import { getDefaultAllowedAiImageModel, getSupportedAiImageModelOptions, } from "../../utils/models";
|
|
10
|
+
import { SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE, } from "../../utils/shared";
|
|
11
|
+
import { composePrompt } from "../../utils/prompts";
|
|
12
|
+
import { buildSettingsReferenceImages, fetchSettingsDocument, findTargetSettings, getErrorMessage, resolveSettingsModel, } from "../settings-data";
|
|
13
|
+
import { formatRateLimitRemainingTime, useFrontendRateLimit, } from "./frontend-rate-limit";
|
|
14
|
+
import { createGeneratedFile } from "../files";
|
|
15
|
+
function getGenerationPhaseLabel(phase) {
|
|
16
|
+
switch (phase) {
|
|
17
|
+
case "preparing":
|
|
18
|
+
return "Preparing...";
|
|
19
|
+
case "generating":
|
|
20
|
+
return "Generating...";
|
|
21
|
+
case "uploading":
|
|
22
|
+
return "Uploading to Sanity...";
|
|
23
|
+
case "updating":
|
|
24
|
+
return "Updating field...";
|
|
25
|
+
default:
|
|
26
|
+
return "Generate image";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function GenerateButtonInput({ options, props, target, }) {
|
|
30
|
+
const client = useClient({ apiVersion: options.apiVersion });
|
|
31
|
+
const { loading: isLoadingSharedSecret, secrets } = useSecrets(SHARED_SECRET_NAMESPACE);
|
|
32
|
+
const schema = useSchema();
|
|
33
|
+
const documentValue = useFormValue([]);
|
|
34
|
+
const documentType = useFormValue(["_type"]);
|
|
35
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
36
|
+
const [userPrompt, setUserPrompt] = useState("");
|
|
37
|
+
const [selectedModel, setSelectedModel] = useState(getDefaultAllowedAiImageModel(options.allowedModels));
|
|
38
|
+
const [selectedContextFieldPaths, setSelectedContextFieldPaths] = useState([]);
|
|
39
|
+
const [error, setError] = useState(null);
|
|
40
|
+
const [generationPhase, setGenerationPhase] = useState("idle");
|
|
41
|
+
const isGenerating = generationPhase !== "idle";
|
|
42
|
+
const currentAssetRef = props.value?.asset?._ref || "empty";
|
|
43
|
+
const hasCustomSelectedModelRef = useRef(false);
|
|
44
|
+
const frontendRateLimit = useFrontendRateLimit(options.frontendRateLimit);
|
|
45
|
+
const selectableContextFields = useMemo(() => getSelectableContextFields(documentType ? schema.get(documentType) : undefined), [documentType, schema]);
|
|
46
|
+
const defaultSelectedContextFieldPaths = useMemo(() => getDefaultSelectedContextFieldPaths({
|
|
47
|
+
selectableContextFields,
|
|
48
|
+
suggestedContextFieldPaths: target.suggestedContextFieldPaths,
|
|
49
|
+
}), [selectableContextFields, target.suggestedContextFieldPaths]);
|
|
50
|
+
const runtimePrompt = buildContextFieldPrompt({
|
|
51
|
+
contextFieldPaths: selectedContextFieldPaths,
|
|
52
|
+
documentValue,
|
|
53
|
+
});
|
|
54
|
+
const sharedSecret = getSharedSecretValue(secrets);
|
|
55
|
+
const allowedModelOptions = useMemo(() => getSupportedAiImageModelOptions(options.allowedModels), [options.allowedModels]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isDialogOpen) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let isMounted = true;
|
|
61
|
+
setSelectedModel(getDefaultAllowedAiImageModel(options.allowedModels));
|
|
62
|
+
hasCustomSelectedModelRef.current = false;
|
|
63
|
+
async function loadDefaultModel() {
|
|
64
|
+
try {
|
|
65
|
+
const settings = await fetchSettingsDocument(client, SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE);
|
|
66
|
+
if (!isMounted || hasCustomSelectedModelRef.current) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setSelectedModel(resolveSettingsModel({
|
|
70
|
+
allowedModels: options.allowedModels,
|
|
71
|
+
settings,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Keep the first allowed model selected if settings loading fails.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
void loadDefaultModel();
|
|
79
|
+
return () => {
|
|
80
|
+
isMounted = false;
|
|
81
|
+
};
|
|
82
|
+
}, [client, isDialogOpen, options.allowedModels]);
|
|
83
|
+
function toggleContextFieldPath(fieldPath) {
|
|
84
|
+
setSelectedContextFieldPaths((currentFieldPaths) => currentFieldPaths.includes(fieldPath)
|
|
85
|
+
? currentFieldPaths.filter((currentFieldPath) => currentFieldPath !== fieldPath)
|
|
86
|
+
: [...currentFieldPaths, fieldPath]);
|
|
87
|
+
}
|
|
88
|
+
async function handleGenerate() {
|
|
89
|
+
setError(null);
|
|
90
|
+
setGenerationPhase("preparing");
|
|
91
|
+
try {
|
|
92
|
+
if (isLoadingSharedSecret) {
|
|
93
|
+
throw new Error("Loading AI Image Plugin shared secret...");
|
|
94
|
+
}
|
|
95
|
+
if (!sharedSecret) {
|
|
96
|
+
throw new Error(getMissingSharedSecretMessage());
|
|
97
|
+
}
|
|
98
|
+
const settings = await fetchSettingsDocument(client, SETTINGS_DOCUMENT_ID, SETTINGS_SCHEMA_TYPE);
|
|
99
|
+
const targetSettings = findTargetSettings(settings, target.id);
|
|
100
|
+
const prompt = composePrompt([
|
|
101
|
+
settings?.globalPrompt
|
|
102
|
+
? `Global direction:\n${settings.globalPrompt}`
|
|
103
|
+
: null,
|
|
104
|
+
targetSettings?.prompt
|
|
105
|
+
? `Target direction:\n${targetSettings.prompt}`
|
|
106
|
+
: null,
|
|
107
|
+
runtimePrompt ? `Document context:\n${runtimePrompt}` : null,
|
|
108
|
+
userPrompt.trim()
|
|
109
|
+
? `Additional editor instructions:\n${userPrompt.trim()}`
|
|
110
|
+
: null,
|
|
111
|
+
]);
|
|
112
|
+
if (!prompt) {
|
|
113
|
+
throw new Error("Add a prompt or configure target defaults first.");
|
|
114
|
+
}
|
|
115
|
+
const referenceImages = await buildSettingsReferenceImages(settings, target.id);
|
|
116
|
+
const formData = new FormData();
|
|
117
|
+
formData.set("prompt", prompt);
|
|
118
|
+
formData.set("model", selectedModel);
|
|
119
|
+
for (const referenceImage of referenceImages) {
|
|
120
|
+
formData.append("references", referenceImage);
|
|
121
|
+
}
|
|
122
|
+
const rateLimitSnapshot = frontendRateLimit.consumeAttempt();
|
|
123
|
+
if (!rateLimitSnapshot.allowed) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setGenerationPhase("generating");
|
|
127
|
+
const response = await fetch(options.apiEndpoint, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: formData,
|
|
130
|
+
headers: {
|
|
131
|
+
[SHARED_SECRET_HEADER_NAME]: sharedSecret,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const payload = (await response.json());
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new Error("error" in payload && payload.error
|
|
137
|
+
? payload.error
|
|
138
|
+
: "AI image generation failed.");
|
|
139
|
+
}
|
|
140
|
+
if (!("data" in payload) ||
|
|
141
|
+
typeof payload.data !== "string" ||
|
|
142
|
+
typeof payload.mimeType !== "string") {
|
|
143
|
+
throw new Error("AI Image Plugin returned an invalid image payload.");
|
|
144
|
+
}
|
|
145
|
+
const basename = String(documentValue?.title ||
|
|
146
|
+
documentValue?._type ||
|
|
147
|
+
target.title ||
|
|
148
|
+
"generated").trim() || "generated";
|
|
149
|
+
const generatedFile = createGeneratedFile(payload, basename, "ai-image-plugin-field");
|
|
150
|
+
setGenerationPhase("uploading");
|
|
151
|
+
const uploadedAsset = await client.assets.upload("image", generatedFile, {
|
|
152
|
+
filename: generatedFile.name,
|
|
153
|
+
});
|
|
154
|
+
setGenerationPhase("updating");
|
|
155
|
+
props.onChange(PatchEvent.from([
|
|
156
|
+
setIfMissing({ _type: "image" }),
|
|
157
|
+
set({ _type: "reference", _ref: uploadedAsset._id }, ["asset"]),
|
|
158
|
+
]));
|
|
159
|
+
setIsDialogOpen(false);
|
|
160
|
+
setUserPrompt("");
|
|
161
|
+
}
|
|
162
|
+
catch (nextError) {
|
|
163
|
+
setError(getErrorMessage(nextError, "Unable to generate an image for this field."));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setGenerationPhase("idle");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return (_jsxs(Stack, { space: 4, children: [_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 1, children: target.description ||
|
|
170
|
+
"Generate an image with AI Image Plugin for this field." }), _jsx(Flex, { gap: 3, children: _jsx(Button, { onClick: () => {
|
|
171
|
+
setError(null);
|
|
172
|
+
setSelectedContextFieldPaths(defaultSelectedContextFieldPaths);
|
|
173
|
+
setIsDialogOpen(true);
|
|
174
|
+
}, text: "Generate", tone: "primary" }) })] }) }), _jsx(Box, { children: props.renderDefault(props) }, currentAssetRef), isDialogOpen ? (_jsx(Dialog, { header: target.dialogTitle || "Generate Image", id: `${target.id}-generate-dialog`, onClose: () => {
|
|
175
|
+
if (isGenerating) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
setError(null);
|
|
179
|
+
setIsDialogOpen(false);
|
|
180
|
+
}, width: 1, children: _jsx(Box, { padding: 4, children: _jsxs(Stack, { space: 4, children: [_jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "medium", children: "Model" }), _jsx(Box, { marginTop: 2, children: _jsx(Select, { onChange: (event) => {
|
|
181
|
+
hasCustomSelectedModelRef.current = true;
|
|
182
|
+
setSelectedModel(event.currentTarget.value);
|
|
183
|
+
}, value: selectedModel, children: allowedModelOptions.map((modelOption) => (_jsx("option", { value: modelOption.value, children: modelOption.title }, modelOption.value))) }) })] }), _jsxs(Box, { children: [_jsx(Text, { size: 1, weight: "medium", children: target.promptLabel || "Custom prompt" }), _jsx(Box, { marginTop: 2, children: _jsx(TextArea, { onChange: (event) => setUserPrompt(event.currentTarget.value), placeholder: target.promptPlaceholder ||
|
|
184
|
+
"Optional custom instructions for this image.", rows: 6, value: userPrompt }) })] }), selectableContextFields.length > 0 ? (_jsx(Box, { children: _jsx(Stack, { space: 3, children: _jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 1, weight: "medium", children: "Include document fields" }), _jsx(Flex, { gap: 2, style: { flexWrap: "wrap" }, children: selectableContextFields.map((field) => {
|
|
185
|
+
const isSelected = selectedContextFieldPaths.includes(field.path);
|
|
186
|
+
return (_jsx(Button, { mode: isSelected ? "default" : "ghost", onClick: () => toggleContextFieldPath(field.path), text: field.label, tone: isSelected ? "primary" : "default" }, field.path));
|
|
187
|
+
}) })] }) }) })) : (_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 1, weight: "medium", children: "Document context" }), _jsx(Text, { muted: true, size: 1, children: "No supported top-level document fields are available for this type yet." })] }) })), selectableContextFields.length > 0 ? (_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Stack, { space: 2, children: [_jsx(Text, { size: 1, weight: "medium", children: "Document context preview" }), runtimePrompt ? (_jsx(Text, { size: 1, children: runtimePrompt })) : (_jsx(Text, { muted: true, size: 1, children: "No document fields are selected with a usable value yet." }))] }) })) : null, error ? (_jsx(Card, { padding: 3, radius: 2, tone: "critical", children: _jsx(Text, { size: 1, children: error }) })) : null, frontendRateLimit.blocked && !isGenerating ? (_jsx(Card, { padding: 3, radius: 2, tone: "caution", children: _jsxs(Text, { size: 1, children: ["Please wait", " ", formatRateLimitRemainingTime(frontendRateLimit.remainingMs), " ", "before generating again."] }) })) : null, isGenerating ? (_jsx(Card, { padding: 3, radius: 2, tone: "transparent", children: _jsxs(Text, { size: 1, children: [getGenerationPhaseLabel(generationPhase), " This can take a minute or two for slower models."] }) })) : null, _jsxs(Flex, { gap: 3, justify: "flex-end", children: [_jsx(Button, { disabled: isGenerating, mode: "ghost", onClick: () => {
|
|
188
|
+
setError(null);
|
|
189
|
+
setIsDialogOpen(false);
|
|
190
|
+
}, text: "Cancel" }), _jsx(Button, { disabled: isGenerating ||
|
|
191
|
+
isLoadingSharedSecret ||
|
|
192
|
+
frontendRateLimit.blocked, onClick: () => void handleGenerate(), text: getGenerationPhaseLabel(generationPhase), tone: "primary" })] })] }) }) })) : null] }));
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=generate-button-input.js.map
|