sanity-plugin-alt-text-generator 1.0.2 → 1.0.3
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 +14 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +70 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Generate alt text for your images inside Sanity CMS. You can generate alt text o
|
|
|
10
10
|
|
|
11
11
|
- **Per-field "Generate Alt Text" button** on any image field with an `alt` subfield
|
|
12
12
|
- **Bulk dashboard** — a dedicated "Alt Text Generator" tool in the Studio nav that scans your entire dataset for image fields, lets you filter by missing/all, generate one at a time or in bulk, review and edit before saving, and save every change in one transaction
|
|
13
|
+
- **Automatic mode** — optionally write alt text the moment an image is uploaded, when its alt text is empty
|
|
13
14
|
- **In-Studio Settings panel** — paste your API key directly in the Studio, no code or redeploy required; shows your remaining credit balance
|
|
14
15
|
- Automatically finds every image field with an `alt` subfield across your schema — no manual field configuration needed
|
|
15
16
|
|
|
@@ -60,6 +61,19 @@ Open the **Alt Text Generator** tool in the Studio nav and paste your key into i
|
|
|
60
61
|
altTextGenerator({apiKey: process.env.SANITY_STUDIO_ALT_TEXT_API_KEY})
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Generate alt text automatically on upload
|
|
65
|
+
|
|
66
|
+
Turn on **Write alt text automatically when an image is uploaded** in the tool's Settings panel, or in code:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
altTextGenerator({autoGenerate: true})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When someone uploads an image and its alt text is empty, the plugin writes it right away. If they swap an image for a different one, it writes new alt text for the new image. Each image uses one credit. Existing images are never changed just by opening a document. This needs the image field wired up with `AltTextField` or `AltTextImageInput` (see below). The Settings panel choice takes priority over the code option.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+

|
|
76
|
+
|
|
63
77
|
### Wiring up the per-field button
|
|
64
78
|
|
|
65
79
|
Any image field with an `alt` subfield is automatically picked up by the bulk dashboard. To also get a one-click "Generate Alt Text" button on that field, wire it in one of two ways:
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ interface AltTextGeneratorConfig {
|
|
|
12
12
|
apiEndpoint?: string;
|
|
13
13
|
/** Default language for generated alt text, e.g. "english", "spanish". */
|
|
14
14
|
language?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Write alt text automatically when an image is uploaded (if its alt field is
|
|
17
|
+
* empty) or swapped for a different image. Off by default because each image uses one credit. People can
|
|
18
|
+
* also switch it on in the plugin's Settings panel, which takes priority.
|
|
19
|
+
*/
|
|
20
|
+
autoGenerate?: boolean;
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
17
23
|
* Drop-in replacement for the default image input: renders the normal image
|
|
@@ -54,6 +60,10 @@ export declare function AltTextDashboard(): import("react").JSX.Element;
|
|
|
54
60
|
* at alttextgeneratorai.com). You can also pass a default `apiKey` here for
|
|
55
61
|
* local development/CI; the Settings-panel key takes priority once saved.
|
|
56
62
|
*
|
|
63
|
+
* To write alt text automatically when an image is uploaded (if its alt text
|
|
64
|
+
* is empty) or swapped for another image, tick the checkbox in the same Settings panel or pass
|
|
65
|
+
* `altTextGenerator({autoGenerate: true})`. Each image uses one credit.
|
|
66
|
+
*
|
|
57
67
|
* Then wire it into your schema in one of two ways:
|
|
58
68
|
*
|
|
59
69
|
* 1. On the `alt` string field itself (recommended — puts the "Generate Alt
|
package/dist/index.js
CHANGED
|
@@ -15,16 +15,26 @@ function useAltTextGeneratorConfig() {
|
|
|
15
15
|
* the Studio's schema, so this stays entirely internal to the plugin and
|
|
16
16
|
* never shows up in Content/search for the merchant.
|
|
17
17
|
*/
|
|
18
|
-
const SETTINGS_DOC_ID = "sanity-plugin-alt-text-generator.settings", API_VERSION$5 = "2024-01-01";
|
|
19
|
-
async function
|
|
20
|
-
|
|
18
|
+
const SETTINGS_DOC_ID = "sanity-plugin-alt-text-generator.settings", SETTINGS_DOC_TYPE = "sanityPluginAltTextGeneratorSettings", API_VERSION$5 = "2024-01-01";
|
|
19
|
+
async function getSettings(client) {
|
|
20
|
+
let doc = await client.withConfig({ apiVersion: API_VERSION$5 }).fetch("*[_id == $id][0]{apiKey, autoGenerate}", { id: SETTINGS_DOC_ID });
|
|
21
|
+
return {
|
|
22
|
+
apiKey: doc?.apiKey || void 0,
|
|
23
|
+
autoGenerate: typeof doc?.autoGenerate == "boolean" ? doc.autoGenerate : void 0
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
async function saveSettingsApiKey(client, apiKey) {
|
|
23
27
|
await client.withConfig({ apiVersion: API_VERSION$5 }).transaction().createIfNotExists({
|
|
24
28
|
_id: SETTINGS_DOC_ID,
|
|
25
|
-
_type:
|
|
29
|
+
_type: SETTINGS_DOC_TYPE
|
|
26
30
|
}).patch(SETTINGS_DOC_ID, (p) => p.set({ apiKey })).commit();
|
|
27
31
|
}
|
|
32
|
+
async function saveSettingsAutoGenerate(client, autoGenerate) {
|
|
33
|
+
await client.withConfig({ apiVersion: API_VERSION$5 }).transaction().createIfNotExists({
|
|
34
|
+
_id: SETTINGS_DOC_ID,
|
|
35
|
+
_type: SETTINGS_DOC_TYPE
|
|
36
|
+
}).patch(SETTINGS_DOC_ID, (p) => p.set({ autoGenerate })).commit();
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* Injects the plugin's config into React context for the whole Studio tree,
|
|
30
40
|
* via the `studio.components.layout` slot — this is how a plugin makes its
|
|
@@ -39,11 +49,11 @@ async function saveSettingsApiKey(client, apiKey) {
|
|
|
39
49
|
*/
|
|
40
50
|
function createAltTextGeneratorProvider(staticConfig) {
|
|
41
51
|
return function AltTextGeneratorProvider(props) {
|
|
42
|
-
let client = useClient({ apiVersion: "2024-01-01" }), [liveApiKey, setLiveApiKey] = useState(void 0);
|
|
52
|
+
let client = useClient({ apiVersion: "2024-01-01" }), [liveApiKey, setLiveApiKey] = useState(void 0), [liveAutoGenerate, setLiveAutoGenerate] = useState(void 0);
|
|
43
53
|
useEffect(() => {
|
|
44
54
|
let cancelled = !1;
|
|
45
|
-
return
|
|
46
|
-
cancelled || setLiveApiKey(
|
|
55
|
+
return getSettings(client).then((settings) => {
|
|
56
|
+
cancelled || (setLiveApiKey(settings.apiKey), setLiveAutoGenerate(settings.autoGenerate));
|
|
47
57
|
}).catch(() => {}), () => {
|
|
48
58
|
cancelled = !0;
|
|
49
59
|
};
|
|
@@ -52,8 +62,14 @@ function createAltTextGeneratorProvider(staticConfig) {
|
|
|
52
62
|
apiKey: liveApiKey || staticConfig.apiKey || "",
|
|
53
63
|
apiEndpoint: staticConfig.apiEndpoint,
|
|
54
64
|
language: staticConfig.language,
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
autoGenerate: liveAutoGenerate ?? staticConfig.autoGenerate ?? !1,
|
|
66
|
+
updateApiKey: setLiveApiKey,
|
|
67
|
+
updateAutoGenerate: setLiveAutoGenerate
|
|
68
|
+
}), [
|
|
69
|
+
liveApiKey,
|
|
70
|
+
liveAutoGenerate,
|
|
71
|
+
staticConfig
|
|
72
|
+
]);
|
|
57
73
|
return <AltTextGeneratorContext.Provider value={value}>
|
|
58
74
|
{props.renderDefault(props)}
|
|
59
75
|
</AltTextGeneratorContext.Provider>;
|
|
@@ -258,7 +274,16 @@ function SettingsPanel() {
|
|
|
258
274
|
keyInput,
|
|
259
275
|
config,
|
|
260
276
|
checkCredits
|
|
261
|
-
])
|
|
277
|
+
]), handleAutoGenerate = useCallback(async (checked) => {
|
|
278
|
+
setError(null);
|
|
279
|
+
let previous = config.autoGenerate;
|
|
280
|
+
config.updateAutoGenerate(checked);
|
|
281
|
+
try {
|
|
282
|
+
await saveSettingsAutoGenerate(client, checked);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
config.updateAutoGenerate(previous), setError(e instanceof Error ? e.message : "Failed to save.");
|
|
285
|
+
}
|
|
286
|
+
}, [client, config]);
|
|
262
287
|
return <Card padding={4} radius={2} border tone="transparent">
|
|
263
288
|
<Stack gap={3}>
|
|
264
289
|
<Text size={1} weight="medium">
|
|
@@ -276,6 +301,10 @@ function SettingsPanel() {
|
|
|
276
301
|
</Box>
|
|
277
302
|
<Button text={saving ? "Saving…" : "Save"} tone="primary" disabled={saving || keyInput.trim() === config.apiKey} onClick={handleSave} />
|
|
278
303
|
</Flex>
|
|
304
|
+
<Flex as="label" gap={2} align="center">
|
|
305
|
+
<Checkbox checked={config.autoGenerate} onChange={(e) => handleAutoGenerate(e.currentTarget.checked)} />
|
|
306
|
+
<Text size={1}>Write alt text automatically when an image is uploaded (uses 1 credit per image)</Text>
|
|
307
|
+
</Flex>
|
|
279
308
|
{justSaved && <Text size={1} style={{ color: "var(--card-positive-fg-color, #2a9d5c)" }}>
|
|
280
309
|
Saved.
|
|
281
310
|
</Text>}
|
|
@@ -509,6 +538,31 @@ function AltTextDashboard() {
|
|
|
509
538
|
</Container>;
|
|
510
539
|
}
|
|
511
540
|
/**
|
|
541
|
+
* Runs `generate` once when an image is uploaded or swapped, if auto-generate
|
|
542
|
+
* is switched on and an API key is set. A first upload only gets alt text when
|
|
543
|
+
* the field is empty. Swapping one image for another always writes new alt
|
|
544
|
+
* text, because the old text describes the old image. The asset the form opens
|
|
545
|
+
* with is remembered first, so existing images are never touched just by
|
|
546
|
+
* opening a document.
|
|
547
|
+
*/
|
|
548
|
+
function useAutoGenerate(assetRef, hasAlt, generate) {
|
|
549
|
+
let config = useAltTextGeneratorConfig(), lastRef = useRef(assetRef), latest = useRef({
|
|
550
|
+
generate,
|
|
551
|
+
hasAlt,
|
|
552
|
+
enabled: !1
|
|
553
|
+
});
|
|
554
|
+
latest.current = {
|
|
555
|
+
generate,
|
|
556
|
+
hasAlt,
|
|
557
|
+
enabled: config.autoGenerate && !!config.apiKey
|
|
558
|
+
}, useEffect(() => {
|
|
559
|
+
let previous = lastRef.current;
|
|
560
|
+
if (lastRef.current = assetRef, !assetRef || assetRef === previous) return;
|
|
561
|
+
let { generate: run, hasAlt: filled, enabled } = latest.current;
|
|
562
|
+
enabled && (previous || !filled) && run();
|
|
563
|
+
}, [assetRef]);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
512
566
|
* Drop-in replacement for the default image input: renders the normal image
|
|
513
567
|
* field (upload, crop, hotspot — via `renderDefault`) plus a "Generate Alt
|
|
514
568
|
* Text" button that sends the uploaded asset's CDN URL to the Alt Text
|
|
@@ -546,7 +600,7 @@ function AltTextImageInput(props) {
|
|
|
546
600
|
config,
|
|
547
601
|
onChange
|
|
548
602
|
]);
|
|
549
|
-
return <Stack gap={2}>
|
|
603
|
+
return useAutoGenerate(value?.asset?._ref, !!value?.alt, handleGenerate), <Stack gap={2}>
|
|
550
604
|
{props.renderDefault(props)}
|
|
551
605
|
{value?.asset && <Stack gap={2}>
|
|
552
606
|
<Flex>
|
|
@@ -601,7 +655,7 @@ function AltTextField(props) {
|
|
|
601
655
|
config,
|
|
602
656
|
onChange
|
|
603
657
|
]);
|
|
604
|
-
return <Stack gap={2}>
|
|
658
|
+
return useAutoGenerate(imageValue?.asset?._ref, !!value, handleGenerate), <Stack gap={2}>
|
|
605
659
|
{props.renderDefault(props)}
|
|
606
660
|
{imageValue?.asset && <Stack gap={2}>
|
|
607
661
|
<Flex>
|
|
@@ -634,6 +688,10 @@ function AltTextField(props) {
|
|
|
634
688
|
* at alttextgeneratorai.com). You can also pass a default `apiKey` here for
|
|
635
689
|
* local development/CI; the Settings-panel key takes priority once saved.
|
|
636
690
|
*
|
|
691
|
+
* To write alt text automatically when an image is uploaded (if its alt text
|
|
692
|
+
* is empty) or swapped for another image, tick the checkbox in the same Settings panel or pass
|
|
693
|
+
* `altTextGenerator({autoGenerate: true})`. Each image uses one credit.
|
|
694
|
+
*
|
|
637
695
|
* Then wire it into your schema in one of two ways:
|
|
638
696
|
*
|
|
639
697
|
* 1. On the `alt` string field itself (recommended — puts the "Generate Alt
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["API_VERSION","API_VERSION","DEFAULT_ENDPOINT","API_VERSION","API_VERSION","API_VERSION"],"sources":["../src/context.ts","../src/settingsStore.ts","../src/AltTextGeneratorProvider.tsx","../src/schemaScan.ts","../src/dashboardData.ts","../src/generateAltText.ts","../src/verifyCredits.ts","../src/SettingsPanel.tsx","../src/AltTextDashboard.tsx","../src/AltTextImageInput.tsx","../src/AltTextField.tsx","../src/index.ts"],"sourcesContent":["import {createContext, useContext} from 'react'\n\nexport interface AltTextGeneratorConfig {\n /**\n * Optional default API key from your Alt Text Generator account\n * (alttextgeneratorai.com). Not required — merchants can instead (or also)\n * paste their key into the plugin's own Settings panel in the Studio, which\n * takes priority when both are set. Passing one here is mainly useful for\n * local development or CI.\n */\n apiKey?: string\n /** Override the generation endpoint — mainly useful for local development. */\n apiEndpoint?: string\n /** Default language for generated alt text, e.g. \"english\", \"spanish\". */\n language?: string\n}\n\nexport interface AltTextGeneratorContextValue {\n apiKey: string\n apiEndpoint?: string\n language?: string\n /** Updates the live, in-memory API key immediately after a successful save from Settings. */\n updateApiKey: (apiKey: string) => void\n}\n\nexport const AltTextGeneratorContext = createContext<AltTextGeneratorContextValue | null>(null)\n\nexport function useAltTextGeneratorConfig(): AltTextGeneratorContextValue {\n const ctx = useContext(AltTextGeneratorContext)\n if (!ctx) {\n throw new Error(\n 'useAltTextGeneratorConfig must be used within the alt-text-generator plugin. ' +\n 'Did you forget to add altTextGenerator() to your sanity.config plugins array?',\n )\n }\n return ctx\n}\n","import type {SanityClient} from 'sanity'\n\n/**\n * A single, well-known document this plugin owns to store its own settings\n * (currently just the API key). Not registered in the consumer's schema —\n * Sanity's document store doesn't require a document's `_type` to be part of\n * the Studio's schema, so this stays entirely internal to the plugin and\n * never shows up in Content/search for the merchant.\n */\nexport const SETTINGS_DOC_ID = 'sanity-plugin-alt-text-generator.settings'\nexport const SETTINGS_DOC_TYPE = 'sanityPluginAltTextGeneratorSettings'\n\nconst API_VERSION = '2024-01-01'\n\nexport async function getSettingsApiKey(client: SanityClient): Promise<string | undefined> {\n const key = await client\n .withConfig({apiVersion: API_VERSION})\n .fetch<string | null>(`*[_id == $id][0].apiKey`, {id: SETTINGS_DOC_ID})\n return key || undefined\n}\n\nexport async function saveSettingsApiKey(client: SanityClient, apiKey: string): Promise<void> {\n await client\n .withConfig({apiVersion: API_VERSION})\n .transaction()\n .createIfNotExists({_id: SETTINGS_DOC_ID, _type: SETTINGS_DOC_TYPE})\n .patch(SETTINGS_DOC_ID, (p) => p.set({apiKey}))\n .commit()\n}\n","import {useEffect, useMemo, useState} from 'react'\nimport {useClient, type LayoutProps} from 'sanity'\nimport {AltTextGeneratorContext, type AltTextGeneratorConfig} from './context'\nimport {getSettingsApiKey} from './settingsStore'\n\nconst API_VERSION = '2024-01-01'\n\n/**\n * Injects the plugin's config into React context for the whole Studio tree,\n * via the `studio.components.layout` slot — this is how a plugin makes its\n * config reachable from an input component deep in a document form, without\n * the merchant having to repeat the API key on every field.\n *\n * The API key can come from two places: the static `apiKey` passed to\n * `altTextGenerator({...})` in sanity.config.ts, and/or the key saved through\n * the plugin's own Settings panel (stored in a document — see\n * settingsStore.ts). The Settings-panel key takes priority once loaded, so a\n * merchant can paste their key into the Studio UI without touching code.\n */\nexport function createAltTextGeneratorProvider(staticConfig: AltTextGeneratorConfig) {\n return function AltTextGeneratorProvider(props: LayoutProps) {\n const client = useClient({apiVersion: API_VERSION})\n const [liveApiKey, setLiveApiKey] = useState<string | undefined>(undefined)\n\n useEffect(() => {\n let cancelled = false\n getSettingsApiKey(client)\n .then((key) => {\n if (!cancelled) setLiveApiKey(key)\n })\n .catch(() => {\n // No settings doc yet, or no read access — fall back to static config silently.\n })\n return () => {\n cancelled = true\n }\n }, [client])\n\n const value = useMemo(\n () => ({\n apiKey: liveApiKey || staticConfig.apiKey || '',\n apiEndpoint: staticConfig.apiEndpoint,\n language: staticConfig.language,\n updateApiKey: setLiveApiKey,\n }),\n [liveApiKey, staticConfig],\n )\n\n return (\n <AltTextGeneratorContext.Provider value={value}>\n {props.renderDefault(props)}\n </AltTextGeneratorContext.Provider>\n )\n }\n}\n","import type {Schema, SchemaType} from 'sanity'\n\nexport interface AltTextFieldTarget {\n documentType: string\n /** e.g. ['mainImage'] or ['seo', 'ogImage'] — joined with '.' for GROQ/patch paths. */\n fieldPath: string[]\n}\n\nfunction chainHasName(type: SchemaType | undefined, name: string): boolean {\n let t: SchemaType | undefined = type\n while (t) {\n if (t.name === name) return true\n t = (t as {type?: SchemaType}).type\n }\n return false\n}\n\nfunction hasAltSubfield(imageType: SchemaType): boolean {\n const fields = (imageType as {fields?: {name: string}[]}).fields || []\n return fields.some((f) => f.name === 'alt')\n}\n\nconst MAX_NESTING_DEPTH = 2\n\nfunction walkFields(\n type: SchemaType,\n path: string[],\n documentType: string,\n out: AltTextFieldTarget[],\n seen: Set<string>,\n depth: number,\n) {\n const fields = (type as {fields?: {name: string; type: SchemaType}[]}).fields\n if (!fields) return\n\n for (const field of fields) {\n const fieldPath = [...path, field.name]\n const fieldType = field.type\n\n if (chainHasName(fieldType, 'image')) {\n if (hasAltSubfield(fieldType)) {\n const key = `${documentType}:${fieldPath.join('.')}`\n if (!seen.has(key)) {\n seen.add(key)\n out.push({documentType, fieldPath})\n }\n }\n continue\n }\n\n // Recurse into plain nested objects (e.g. `seo.ogImage`), skip arrays/references\n // to keep discovery predictable — deeply nested or array-of-image fields are a\n // known gap, not attempted here.\n if (fieldType.jsonType === 'object' && depth < MAX_NESTING_DEPTH) {\n walkFields(fieldType, fieldPath, documentType, out, seen, depth + 1)\n }\n }\n}\n\n/**\n * Walks every document type in the (fully merged, runtime) schema and finds\n * every image field that has an `alt` subfield — the convention this plugin's\n * own AltTextImageInput relies on, and the one most real-world Sanity schemas\n * already use for accessible images either way.\n */\nexport function findAltTextFields(schema: Schema): AltTextFieldTarget[] {\n const results: AltTextFieldTarget[] = []\n const seen = new Set<string>()\n\n for (const typeName of schema.getTypeNames()) {\n const type = schema.get(typeName)\n if (!type || !chainHasName(type, 'document')) continue\n walkFields(type, [], typeName, results, seen, 0)\n }\n\n return results\n}\n","import type {SanityClient} from 'sanity'\nimport type {AltTextFieldTarget} from './schemaScan'\n\nexport interface AltTextRow {\n key: string\n documentId: string\n documentType: string\n documentTitle: string\n fieldPath: string[]\n fieldLabel: string\n imageUrl: string | null\n alt: string\n}\n\nfunction fieldAlias(fieldPath: string[]): string {\n return 'f_' + fieldPath.join('_')\n}\n\nfunction groqPath(fieldPath: string[]): string {\n return fieldPath.join('.')\n}\n\n/**\n * One GROQ query per document type (fields are grouped by documentType first),\n * projecting every discovered image field on that type plus a best-effort title.\n */\nfunction buildQuery(documentType: string, fields: AltTextFieldTarget[]): string {\n const projections = fields\n .map((f) => {\n const alias = fieldAlias(f.fieldPath)\n const path = groqPath(f.fieldPath)\n return `\"${alias}\": ${path}{alt, \"url\": asset->url}`\n })\n .join(',\\n ')\n\n return `*[_type == \"${documentType}\"]{\n _id,\n _type,\n \"documentTitle\": coalesce(title, name, heading, label, _id),\n ${projections}\n }`\n}\n\nexport async function fetchAltTextRows(\n client: SanityClient,\n targets: AltTextFieldTarget[],\n): Promise<AltTextRow[]> {\n const byType = new Map<string, AltTextFieldTarget[]>()\n for (const t of targets) {\n const list = byType.get(t.documentType) || []\n list.push(t)\n byType.set(t.documentType, list)\n }\n\n const rows: AltTextRow[] = []\n\n for (const [documentType, fields] of byType) {\n const query = buildQuery(documentType, fields)\n const docs = await client.fetch<Record<string, unknown>[]>(query)\n\n for (const doc of docs) {\n for (const f of fields) {\n const alias = fieldAlias(f.fieldPath)\n const value = doc[alias] as {alt?: string; url?: string} | null | undefined\n if (!value || !value.url) continue // field empty on this doc — nothing to show\n\n rows.push({\n key: `${doc._id}:${f.fieldPath.join('.')}`,\n documentId: doc._id as string,\n documentType,\n documentTitle: (doc.documentTitle as string) || (doc._id as string),\n fieldPath: f.fieldPath,\n fieldLabel: f.fieldPath.join(' › '),\n imageUrl: value.url,\n alt: value.alt || '',\n })\n }\n }\n }\n\n return rows\n}\n\nexport interface AltTextUpdate {\n documentId: string\n fieldPath: string[]\n alt: string\n}\n\n/** Saves every update in one transaction — either all land or none do. */\nexport async function saveAltTextUpdates(client: SanityClient, updates: AltTextUpdate[]): Promise<void> {\n if (!updates.length) return\n\n let tx = client.transaction()\n for (const u of updates) {\n const patchKey = `${groqPath(u.fieldPath)}.alt`\n tx = tx.patch(u.documentId, (p) => p.set({[patchKey]: u.alt}))\n }\n await tx.commit()\n}\n","import type {AltTextGeneratorConfig} from './context'\n\nconst DEFAULT_ENDPOINT = 'https://alttextgeneratorai.com/api/sanity'\n\nexport type GenerateResult =\n | {ok: true; altText: string; creditsLeft: number | null}\n | {ok: false; message: string; tone: 'critical' | 'caution'}\n\nfunction messageForStatus(status: number, body: string): {message: string; tone: 'critical' | 'caution'} {\n if (status === 401) return {message: 'Invalid API key. Check it under the plugin config.', tone: 'critical'}\n if (status === 402) return {message: 'No credits left on this API key.', tone: 'caution'}\n return {message: body || `Generation failed (${status}).`, tone: 'critical'}\n}\n\nexport async function generateAltText(imageUrl: string, config: AltTextGeneratorConfig): Promise<GenerateResult> {\n try {\n const endpoint = config.apiEndpoint || DEFAULT_ENDPOINT\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n image: imageUrl,\n wpkey: config.apiKey,\n language: config.language || 'english',\n }),\n })\n\n const body = await res.text()\n\n if (!res.ok) {\n return {ok: false, ...messageForStatus(res.status, body)}\n }\n\n const creditsHeader = res.headers.get('X-Credits-Left')\n return {\n ok: true,\n altText: body.trim(),\n creditsLeft: creditsHeader === null ? null : Number(creditsHeader),\n }\n } catch (e) {\n return {\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n }\n }\n}\n","import type {AltTextGeneratorConfig} from './context'\n\nconst DEFAULT_ENDPOINT = 'https://alttextgeneratorai.com/api/sanity'\n\n/**\n * The credits-check endpoint isn't Sanity-specific — it's the same\n * `/api/verify` every plugin (Framer, WordPress, Magento...) already calls.\n * Derived from the generation endpoint so a local-dev `apiEndpoint` override\n * (e.g. http://localhost:3000/api/sanity) checks credits against the same\n * local backend instead of production.\n */\nfunction verifyEndpointFor(config: AltTextGeneratorConfig): string {\n const base = config.apiEndpoint || DEFAULT_ENDPOINT\n return base.replace(/\\/[^/]+$/, '/verify')\n}\n\nexport async function fetchCreditsRemaining(config: AltTextGeneratorConfig, apiKey: string): Promise<number | null> {\n if (!apiKey) return null\n try {\n const res = await fetch(verifyEndpointFor(config), {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({apiKey}),\n })\n if (!res.ok) return null\n const data = (await res.json()) as {freeRewritesLeft?: number}\n return typeof data.freeRewritesLeft === 'number' ? data.freeRewritesLeft : null\n } catch {\n return null\n }\n}\n","import {useCallback, useEffect, useState} from 'react'\nimport {Box, Button, Card, Flex, Stack, Text, TextInput} from '@sanity/ui'\nimport {useClient} from 'sanity'\nimport {useAltTextGeneratorConfig} from './context'\nimport {saveSettingsApiKey} from './settingsStore'\nimport {fetchCreditsRemaining} from './verifyCredits'\n\nconst API_VERSION = '2024-01-01'\nconst GET_KEY_URL = 'https://alttextgeneratorai.com/dashboard'\n\n/**\n * Lets a merchant paste their API key directly in the Studio, no code edit\n * required — saved to a plugin-internal settings document (settingsStore.ts)\n * and pushed straight into the shared context so every open field/tool picks\n * it up immediately. Also shows the current credit balance for the active key.\n */\nexport function SettingsPanel() {\n const client = useClient({apiVersion: API_VERSION})\n const config = useAltTextGeneratorConfig()\n\n const [keyInput, setKeyInput] = useState(config.apiKey)\n const [saving, setSaving] = useState(false)\n const [justSaved, setJustSaved] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [credits, setCredits] = useState<number | null>(null)\n const [checkingCredits, setCheckingCredits] = useState(false)\n\n // Keep the input in sync if the live key changes elsewhere (e.g. loaded after mount).\n useEffect(() => {\n setKeyInput(config.apiKey)\n }, [config.apiKey])\n\n const checkCredits = useCallback(\n async (apiKey: string) => {\n if (!apiKey) {\n setCredits(null)\n return\n }\n setCheckingCredits(true)\n const result = await fetchCreditsRemaining(config, apiKey)\n setCredits(result)\n setCheckingCredits(false)\n },\n [config],\n )\n\n useEffect(() => {\n checkCredits(config.apiKey)\n // Only re-check when the live key itself changes, not on every config identity change.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n const handleSave = useCallback(async () => {\n setSaving(true)\n setError(null)\n setJustSaved(false)\n try {\n const trimmed = keyInput.trim()\n await saveSettingsApiKey(client, trimmed)\n config.updateApiKey(trimmed)\n setJustSaved(true)\n checkCredits(trimmed)\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to save.')\n } finally {\n setSaving(false)\n }\n }, [client, keyInput, config, checkCredits])\n\n return (\n <Card padding={4} radius={2} border tone=\"transparent\">\n <Stack gap={3}>\n <Text size={1} weight=\"medium\">\n API Key\n </Text>\n <Text size={1} muted>\n Paste your Alt Text Generator API key below.{' '}\n <a href={GET_KEY_URL} target=\"_blank\" rel=\"noreferrer\">\n Get a free API key →\n </a>\n </Text>\n <Flex gap={2} align=\"center\" wrap=\"wrap\">\n <Box flex={1} style={{minWidth: 240}}>\n <TextInput\n value={keyInput}\n placeholder=\"Paste your API key\"\n onChange={(e) => setKeyInput(e.currentTarget.value)}\n />\n </Box>\n <Button\n text={saving ? 'Saving…' : 'Save'}\n tone=\"primary\"\n disabled={saving || keyInput.trim() === config.apiKey}\n onClick={handleSave}\n />\n </Flex>\n {justSaved && (\n <Text size={1} style={{color: 'var(--card-positive-fg-color, #2a9d5c)'}}>\n Saved.\n </Text>\n )}\n {error && (\n <Text size={1} style={{color: 'var(--card-critical-fg-color, #e02b27)'}}>\n {error}\n </Text>\n )}\n {!config.apiKey && (\n <Text size={1} style={{color: 'var(--card-caution-fg-color, #a56a0d)'}}>\n No API key configured yet — generation will fail until you add one.\n </Text>\n )}\n {config.apiKey && (\n <Text size={1} muted>\n {checkingCredits\n ? 'Checking credits…'\n : credits === null\n ? 'Could not check credits for this key.'\n : `${credits} credit${credits === 1 ? '' : 's'} remaining`}\n </Text>\n )}\n </Stack>\n </Card>\n )\n}\n","import {useCallback, useMemo, useRef, useState} from 'react'\nimport {\n Box,\n Button,\n Card,\n Checkbox,\n Container,\n Flex,\n Heading,\n Stack,\n Text,\n TextArea,\n Spinner,\n} from '@sanity/ui'\nimport {useClient, useSchema} from 'sanity'\nimport {findAltTextFields} from './schemaScan'\nimport {fetchAltTextRows, saveAltTextUpdates, type AltTextRow} from './dashboardData'\nimport {generateAltText} from './generateAltText'\nimport {useAltTextGeneratorConfig} from './context'\nimport {SettingsPanel} from './SettingsPanel'\n\nconst API_VERSION = '2024-01-01'\n\ntype Filter = 'missing' | 'all'\n\n/**\n * The bulk companion to AltTextImageInput: scans every document type in the\n * schema for image fields that have an `alt` subfield (see schemaScan.ts),\n * lists every image found across the dataset, and lets the user review,\n * generate, and save alt text for many images in one pass.\n */\nexport function AltTextDashboard() {\n const schema = useSchema()\n const client = useClient({apiVersion: API_VERSION})\n const config = useAltTextGeneratorConfig()\n\n const [scanning, setScanning] = useState(false)\n const [scanError, setScanError] = useState<string | null>(null)\n const [rows, setRows] = useState<AltTextRow[]>([])\n const [scannedTypes, setScannedTypes] = useState(0)\n\n const [filter, setFilter] = useState<Filter>('missing')\n const [overwrite, setOverwrite] = useState(false)\n const [selected, setSelected] = useState<Set<string>>(new Set())\n const [dirty, setDirty] = useState<Record<string, string>>({}) // key -> new alt text\n const [rowErrors, setRowErrors] = useState<Record<string, string>>({})\n\n const [bulkRunning, setBulkRunning] = useState(false)\n const [progress, setProgress] = useState('')\n const [credits, setCredits] = useState<number | null>(null)\n const [saving, setSaving] = useState(false)\n const stopRequested = useRef(false)\n\n const targets = useMemo(() => findAltTextFields(schema), [schema])\n\n const handleScan = useCallback(async () => {\n setScanning(true)\n setScanError(null)\n try {\n const found = await fetchAltTextRows(client, targets)\n setRows(found)\n setScannedTypes(new Set(targets.map((t) => t.documentType)).size)\n setDirty({})\n setSelected(new Set())\n setRowErrors({})\n } catch (e) {\n setScanError(e instanceof Error ? e.message : 'Scan failed.')\n } finally {\n setScanning(false)\n }\n }, [client, targets])\n\n const currentAlt = useCallback(\n (row: AltTextRow) => (row.key in dirty ? dirty[row.key] : row.alt),\n [dirty],\n )\n\n const visibleRows = useMemo(() => {\n return rows.filter((row) => {\n if (filter === 'all') return true\n const text = currentAlt(row)\n return !text || text.trim() === ''\n })\n }, [rows, filter, currentAlt])\n\n const isLocked = useCallback(\n (row: AltTextRow) => !overwrite && currentAlt(row).trim() !== '',\n [overwrite, currentAlt],\n )\n\n const setRowText = useCallback((key: string, text: string) => {\n setDirty((prev) => ({...prev, [key]: text}))\n }, [])\n\n const generateOne = useCallback(\n async (row: AltTextRow): Promise<{ok: boolean; message?: string}> => {\n setRowErrors((prev) => {\n const next = {...prev}\n delete next[row.key]\n return next\n })\n if (!row.imageUrl) return {ok: false, message: 'No image URL.'}\n\n const result = await generateAltText(row.imageUrl, config)\n if (!result.ok) {\n setRowErrors((prev) => ({...prev, [row.key]: result.message}))\n return {ok: false, message: result.message}\n }\n setRowText(row.key, result.altText)\n if (result.creditsLeft !== null) setCredits(result.creditsLeft)\n return {ok: true}\n },\n [config, setRowText],\n )\n\n const runBulk = useCallback(\n async (list: AltTextRow[]) => {\n setBulkRunning(true)\n stopRequested.current = false\n let done = 0\n let applied = 0\n let failed = 0\n let lastError = ''\n\n for (const row of list) {\n if (stopRequested.current) break\n const result = await generateOne(row)\n done++\n if (result.ok) {\n applied++\n } else {\n failed++\n lastError = result.message || ''\n if (lastError.toLowerCase().includes('credits')) {\n setProgress(`Stopped: out of credits. ${applied} generated.`)\n break\n }\n }\n setProgress(\n `${done} / ${list.length} · ${applied} generated` +\n (failed ? ` · ${failed} failed — last error: ${lastError}` : ''),\n )\n }\n\n setBulkRunning(false)\n },\n [generateOne],\n )\n\n const handleGenerateSelected = useCallback(() => {\n const list = visibleRows.filter((r) => selected.has(r.key) && !isLocked(r))\n if (!list.length) return\n runBulk(list)\n }, [visibleRows, selected, isLocked, runBulk])\n\n const handleGenerateAllMissing = useCallback(() => {\n const list = rows.filter((r) => currentAlt(r).trim() === '')\n if (!list.length) return\n runBulk(list)\n }, [rows, currentAlt, runBulk])\n\n const handleSave = useCallback(async () => {\n const updates = Object.entries(dirty).map(([key, alt]) => {\n const row = rows.find((r) => r.key === key)!\n return {documentId: row.documentId, fieldPath: row.fieldPath, alt}\n })\n if (!updates.length) return\n\n setSaving(true)\n try {\n await saveAltTextUpdates(client, updates)\n // Reflect saved values into the base rows and clear dirty state.\n setRows((prev) =>\n prev.map((r) => (r.key in dirty ? {...r, alt: dirty[r.key]} : r)),\n )\n setDirty({})\n setProgress(`Saved ${updates.length} image(s).`)\n } catch (e) {\n setProgress(`Save failed: ${e instanceof Error ? e.message : 'unknown error'}`)\n } finally {\n setSaving(false)\n }\n }, [dirty, rows, client])\n\n const toggleSelectAll = useCallback(\n (checked: boolean) => {\n setSelected(checked ? new Set(visibleRows.filter((r) => !isLocked(r)).map((r) => r.key)) : new Set())\n },\n [visibleRows, isLocked],\n )\n\n const missingCount = rows.filter((r) => currentAlt(r).trim() === '').length\n\n return (\n <Container width={4} padding={4}>\n <Stack gap={4}>\n <Heading size={2}>Alt Text Generator AI</Heading>\n\n <SettingsPanel />\n\n <Flex align=\"center\" gap={3}>\n <Button text={scanning ? 'Scanning…' : 'Scan Dataset'} tone=\"primary\" disabled={scanning} onClick={handleScan} />\n {rows.length > 0 && (\n <Text size={1} muted>\n {rows.length} image{rows.length === 1 ? '' : 's'} · {missingCount} missing alt text (scanned {scannedTypes}{' '}\n document type{scannedTypes === 1 ? '' : 's'})\n </Text>\n )}\n {scanning && <Spinner muted />}\n </Flex>\n\n {scanError && (\n <Card tone=\"critical\" padding={3} radius={2} border>\n <Text size={1}>{scanError}</Text>\n </Card>\n )}\n\n {targets.length === 0 && !scanning && (\n <Card tone=\"caution\" padding={3} radius={2} border>\n <Text size={1}>\n No image fields with an "alt" subfield were found in this Studio's schema. Add an{' '}\n <code>alt</code> string field to an image type (see the README) for it to show up here.\n </Text>\n </Card>\n )}\n\n {rows.length > 0 && (\n <Stack gap={4}>\n <Flex align=\"center\" gap={3}>\n <Button\n text=\"Missing\"\n mode={filter === 'missing' ? 'default' : 'ghost'}\n tone={filter === 'missing' ? 'primary' : 'default'}\n onClick={() => setFilter('missing')}\n />\n <Button\n text=\"All\"\n mode={filter === 'all' ? 'default' : 'ghost'}\n tone={filter === 'all' ? 'primary' : 'default'}\n onClick={() => setFilter('all')}\n />\n <Flex align=\"center\" gap={2}>\n <Checkbox checked={overwrite} onChange={(e) => setOverwrite(e.currentTarget.checked)} id=\"atg-overwrite\" />\n <Text as=\"label\" size={1} htmlFor=\"atg-overwrite\">\n Overwrite images that already have alt text\n </Text>\n </Flex>\n </Flex>\n\n <Flex align=\"center\" gap={2}>\n <Checkbox\n checked={visibleRows.length > 0 && visibleRows.every((r) => isLocked(r) || selected.has(r.key))}\n onChange={(e) => toggleSelectAll(e.currentTarget.checked)}\n id=\"atg-select-all\"\n />\n <Text as=\"label\" size={1} htmlFor=\"atg-select-all\">\n Select all visible {selected.size > 0 && `(${selected.size} selected)`}\n </Text>\n </Flex>\n\n <Card border radius={2} style={{maxHeight: 520, overflowY: 'auto'}}>\n <Stack gap={0}>\n {visibleRows.map((row) => {\n const locked = isLocked(row)\n const text = currentAlt(row)\n const error = rowErrors[row.key]\n return (\n <Flex key={row.key} padding={3} gap={3} align=\"flex-start\" style={{borderBottom: '1px solid var(--card-border-color)'}}>\n <Box paddingTop={1}>\n <Checkbox\n checked={selected.has(row.key)}\n disabled={locked}\n onChange={(e) => {\n // Extract the primitive before it reaches the state\n // updater — React can invoke that updater outside the\n // original synchronous event (Strict Mode double-invokes\n // it), and by then `e.currentTarget` is already null.\n const checked = e.currentTarget.checked\n setSelected((prev) => {\n const next = new Set(prev)\n if (checked) next.add(row.key)\n else next.delete(row.key)\n return next\n })\n }}\n />\n </Box>\n {row.imageUrl && (\n <img\n src={`${row.imageUrl}?w=80&h=80&fit=crop`}\n width={56}\n height={56}\n style={{objectFit: 'cover', borderRadius: 4, flexShrink: 0}}\n alt=\"\"\n />\n )}\n <Box style={{width: 220, flexShrink: 0}}>\n <Stack gap={2}>\n <Text size={1} weight=\"medium\">\n {row.documentTitle}\n </Text>\n <Text size={1} muted>\n {row.documentType} · {row.fieldLabel}\n </Text>\n </Stack>\n </Box>\n <Box flex={1}>\n <Stack gap={2}>\n <TextArea\n rows={2}\n value={text}\n disabled={locked}\n onChange={(e) => setRowText(row.key, e.currentTarget.value)}\n />\n {error && (\n <Text size={1} style={{color: 'var(--card-critical-fg-color, #e02b27)'}}>\n {error}\n </Text>\n )}\n </Stack>\n </Box>\n <Box paddingTop={1}>\n <Button\n text=\"Generate\"\n mode=\"ghost\"\n fontSize={1}\n disabled={locked || bulkRunning}\n onClick={() => generateOne(row)}\n />\n </Box>\n </Flex>\n )\n })}\n </Stack>\n </Card>\n\n <Flex align=\"center\" gap={3} wrap=\"wrap\">\n <Button\n text=\"Generate Selected\"\n tone=\"primary\"\n disabled={bulkRunning || selected.size === 0}\n onClick={handleGenerateSelected}\n />\n <Button\n text=\"Generate All Missing\"\n mode=\"ghost\"\n disabled={bulkRunning || missingCount === 0}\n onClick={handleGenerateAllMissing}\n />\n {bulkRunning && (\n <Button text=\"Stop\" mode=\"ghost\" tone=\"critical\" onClick={() => (stopRequested.current = true)} />\n )}\n <Button\n text={saving ? 'Saving…' : 'Save Changes'}\n tone=\"positive\"\n disabled={saving || Object.keys(dirty).length === 0}\n onClick={handleSave}\n />\n {credits !== null && (\n <Text size={1} muted>\n ({credits} credits remaining)\n </Text>\n )}\n </Flex>\n\n {progress && (\n <Text size={1} muted>\n {progress}\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n </Container>\n )\n}\n","import {useCallback, useState} from 'react'\nimport {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, useClient, type ImageInputProps} from 'sanity'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {useAltTextGeneratorConfig} from './context'\nimport {generateAltText, type GenerateResult} from './generateAltText'\n\nconst API_VERSION = '2024-01-01'\n\n/**\n * Drop-in replacement for the default image input: renders the normal image\n * field (upload, crop, hotspot — via `renderDefault`) plus a \"Generate Alt\n * Text\" button that sends the uploaded asset's CDN URL to the Alt Text\n * Generator API and writes the result into the image's `alt` field.\n */\nexport function AltTextImageInput(props: ImageInputProps) {\n const {value, onChange} = props\n const config = useAltTextGeneratorConfig()\n const client = useClient({apiVersion: API_VERSION})\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<Extract<GenerateResult, {ok: false}> | null>(null)\n\n const handleGenerate = useCallback(async () => {\n if (!value?.asset?._ref || !config.apiKey) return\n setLoading(true)\n setError(null)\n\n try {\n const {projectId, dataset} = client.config()\n if (!projectId || !dataset) {\n throw new Error('Could not determine the Sanity project/dataset for this Studio.')\n }\n const imageUrl = imageUrlBuilder({projectId, dataset}).image(value).url()\n\n const result = await generateAltText(imageUrl, config)\n if (!result.ok) {\n setError(result)\n return\n }\n onChange(set(result.altText, ['alt']))\n } catch (e) {\n setError({\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n })\n } finally {\n setLoading(false)\n }\n }, [value, client, config, onChange])\n\n return (\n <Stack gap={2}>\n {props.renderDefault(props)}\n {value?.asset && (\n <Stack gap={2}>\n <Flex>\n <Button\n text={loading ? 'Generating…' : 'Generate Alt Text'}\n tone=\"primary\"\n mode=\"ghost\"\n disabled={loading || !config.apiKey}\n onClick={handleGenerate}\n />\n </Flex>\n {error && (\n <Card tone={error.tone} padding={2} radius={2} border>\n <Text size={1}>{error.message}</Text>\n </Card>\n )}\n {!config.apiKey && (\n <Text size={1} muted>\n No API key configured — open the "Alt Text Generator" tool in the nav to add one.\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n )\n}\n","import {useCallback, useState} from 'react'\nimport {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, useClient, useFormValue, type StringInputProps} from 'sanity'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {useAltTextGeneratorConfig} from './context'\nimport {generateAltText, type GenerateResult} from './generateAltText'\n\nconst API_VERSION = '2024-01-01'\n\ntype ImageWithAsset = {asset?: {_ref: string}}\n\n/**\n * Attaches to the `alt` STRING field itself (not the whole image type), so\n * the \"Generate Alt Text\" button renders directly under the Alternative Text\n * input instead of below every other field on the image (e.g. Caption).\n *\n * Reads the sibling image value via useFormValue since a field-level input\n * component only gets its own value/path, not the parent object's.\n */\nexport function AltTextField(props: StringInputProps) {\n const {onChange, value = ''} = props\n const config = useAltTextGeneratorConfig()\n const client = useClient({apiVersion: API_VERSION})\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<Extract<GenerateResult, {ok: false}> | null>(null)\n\n // props.path is e.g. ['mainImage', 'alt'] — the image value lives one level up.\n const imagePath = props.path.slice(0, -1)\n const imageValue = useFormValue(imagePath) as ImageWithAsset | undefined\n\n const handleGenerate = useCallback(async () => {\n if (!imageValue?.asset?._ref || !config.apiKey) return\n setLoading(true)\n setError(null)\n\n try {\n const {projectId, dataset} = client.config()\n if (!projectId || !dataset) {\n throw new Error('Could not determine the Sanity project/dataset for this Studio.')\n }\n const imageUrl = imageUrlBuilder({projectId, dataset}).image(imageValue).url()\n\n const result = await generateAltText(imageUrl, config)\n if (!result.ok) {\n setError(result)\n return\n }\n onChange(set(result.altText))\n } catch (e) {\n setError({\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n })\n } finally {\n setLoading(false)\n }\n }, [imageValue, client, config, onChange])\n\n return (\n <Stack gap={2}>\n {props.renderDefault(props)}\n {imageValue?.asset && (\n <Stack gap={2}>\n <Flex>\n <Button\n text={loading ? 'Generating…' : 'Generate Alt Text'}\n tone=\"primary\"\n mode=\"ghost\"\n disabled={loading || !config.apiKey}\n onClick={handleGenerate}\n />\n </Flex>\n {error && (\n <Card tone={error.tone} padding={2} radius={2} border>\n <Text size={1}>{error.message}</Text>\n </Card>\n )}\n {!config.apiKey && (\n <Text size={1} muted>\n No API key configured — open the "Alt Text Generator" tool in the nav to add one.\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n )\n}\n","import {definePlugin} from 'sanity'\nimport type {AltTextGeneratorConfig} from './context'\nimport {createAltTextGeneratorProvider} from './AltTextGeneratorProvider'\nimport {AltTextDashboard} from './AltTextDashboard'\n\nexport {AltTextImageInput} from './AltTextImageInput'\nexport {AltTextField} from './AltTextField'\nexport {AltTextDashboard} from './AltTextDashboard'\nexport type {AltTextGeneratorConfig} from './context'\n\n/**\n * Usage in `sanity.config.ts`:\n *\n * ```ts\n * import {defineConfig} from 'sanity'\n * import {altTextGenerator} from 'sanity-plugin-alt-text-generator'\n *\n * export default defineConfig({\n * // ...\n * plugins: [altTextGenerator()],\n * })\n * ```\n *\n * No API key required at setup time — open the \"Alt Text Generator\" tool in\n * the Studio nav and paste your key into its Settings panel (get a free one\n * at alttextgeneratorai.com). You can also pass a default `apiKey` here for\n * local development/CI; the Settings-panel key takes priority once saved.\n *\n * Then wire it into your schema in one of two ways:\n *\n * 1. On the `alt` string field itself (recommended — puts the \"Generate Alt\n * Text\" button right under Alternative Text, above any other fields like\n * Caption):\n *\n * ```ts\n * defineField({\n * name: 'alt',\n * type: 'string',\n * components: {input: AltTextField},\n * })\n * ```\n *\n * 2. On the whole image field (button renders below all of the image's\n * fields at once):\n *\n * ```ts\n * defineField({\n * name: 'mainImage',\n * type: 'image',\n * components: {input: AltTextImageInput},\n * })\n * ```\n *\n * @public\n */\nexport const altTextGenerator = definePlugin<AltTextGeneratorConfig | void>((config) => {\n const staticConfig = config || {}\n\n return {\n name: 'sanity-plugin-alt-text-generator',\n studio: {\n components: {\n layout: createAltTextGeneratorProvider(staticConfig),\n },\n },\n tools: (prev) => [\n ...prev,\n {\n name: 'alt-text-generator',\n title: 'Alt Text Generator',\n component: AltTextDashboard,\n },\n ],\n }\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["API_VERSION","API_VERSION","DEFAULT_ENDPOINT","API_VERSION","API_VERSION","API_VERSION"],"sources":["../src/context.ts","../src/settingsStore.ts","../src/AltTextGeneratorProvider.tsx","../src/schemaScan.ts","../src/dashboardData.ts","../src/generateAltText.ts","../src/verifyCredits.ts","../src/SettingsPanel.tsx","../src/AltTextDashboard.tsx","../src/useAutoGenerate.ts","../src/AltTextImageInput.tsx","../src/AltTextField.tsx","../src/index.ts"],"sourcesContent":["import {createContext, useContext} from 'react'\n\nexport interface AltTextGeneratorConfig {\n /**\n * Optional default API key from your Alt Text Generator account\n * (alttextgeneratorai.com). Not required — merchants can instead (or also)\n * paste their key into the plugin's own Settings panel in the Studio, which\n * takes priority when both are set. Passing one here is mainly useful for\n * local development or CI.\n */\n apiKey?: string\n /** Override the generation endpoint — mainly useful for local development. */\n apiEndpoint?: string\n /** Default language for generated alt text, e.g. \"english\", \"spanish\". */\n language?: string\n /**\n * Write alt text automatically when an image is uploaded (if its alt field is\n * empty) or swapped for a different image. Off by default because each image uses one credit. People can\n * also switch it on in the plugin's Settings panel, which takes priority.\n */\n autoGenerate?: boolean\n}\n\nexport interface AltTextGeneratorContextValue {\n apiKey: string\n apiEndpoint?: string\n language?: string\n autoGenerate: boolean\n /** Updates the live, in-memory API key immediately after a successful save from Settings. */\n updateApiKey: (apiKey: string) => void\n /** Updates the live auto-generate switch immediately after it is saved from Settings. */\n updateAutoGenerate: (autoGenerate: boolean) => void\n}\n\nexport const AltTextGeneratorContext = createContext<AltTextGeneratorContextValue | null>(null)\n\nexport function useAltTextGeneratorConfig(): AltTextGeneratorContextValue {\n const ctx = useContext(AltTextGeneratorContext)\n if (!ctx) {\n throw new Error(\n 'useAltTextGeneratorConfig must be used within the alt-text-generator plugin. ' +\n 'Did you forget to add altTextGenerator() to your sanity.config plugins array?',\n )\n }\n return ctx\n}\n","import type {SanityClient} from 'sanity'\n\n/**\n * A single, well-known document this plugin owns to store its own settings\n * (currently just the API key). Not registered in the consumer's schema —\n * Sanity's document store doesn't require a document's `_type` to be part of\n * the Studio's schema, so this stays entirely internal to the plugin and\n * never shows up in Content/search for the merchant.\n */\nexport const SETTINGS_DOC_ID = 'sanity-plugin-alt-text-generator.settings'\nexport const SETTINGS_DOC_TYPE = 'sanityPluginAltTextGeneratorSettings'\n\nconst API_VERSION = '2024-01-01'\n\nexport type StoredSettings = {apiKey?: string; autoGenerate?: boolean}\n\nexport async function getSettings(client: SanityClient): Promise<StoredSettings> {\n const doc = await client\n .withConfig({apiVersion: API_VERSION})\n .fetch<{apiKey?: string | null; autoGenerate?: boolean | null} | null>(\n `*[_id == $id][0]{apiKey, autoGenerate}`,\n {id: SETTINGS_DOC_ID},\n )\n return {\n apiKey: doc?.apiKey || undefined,\n autoGenerate: typeof doc?.autoGenerate === 'boolean' ? doc.autoGenerate : undefined,\n }\n}\n\nexport async function saveSettingsApiKey(client: SanityClient, apiKey: string): Promise<void> {\n await client\n .withConfig({apiVersion: API_VERSION})\n .transaction()\n .createIfNotExists({_id: SETTINGS_DOC_ID, _type: SETTINGS_DOC_TYPE})\n .patch(SETTINGS_DOC_ID, (p) => p.set({apiKey}))\n .commit()\n}\n\nexport async function saveSettingsAutoGenerate(client: SanityClient, autoGenerate: boolean): Promise<void> {\n await client\n .withConfig({apiVersion: API_VERSION})\n .transaction()\n .createIfNotExists({_id: SETTINGS_DOC_ID, _type: SETTINGS_DOC_TYPE})\n .patch(SETTINGS_DOC_ID, (p) => p.set({autoGenerate}))\n .commit()\n}\n","import {useEffect, useMemo, useState} from 'react'\nimport {useClient, type LayoutProps} from 'sanity'\nimport {AltTextGeneratorContext, type AltTextGeneratorConfig} from './context'\nimport {getSettings} from './settingsStore'\n\nconst API_VERSION = '2024-01-01'\n\n/**\n * Injects the plugin's config into React context for the whole Studio tree,\n * via the `studio.components.layout` slot — this is how a plugin makes its\n * config reachable from an input component deep in a document form, without\n * the merchant having to repeat the API key on every field.\n *\n * The API key can come from two places: the static `apiKey` passed to\n * `altTextGenerator({...})` in sanity.config.ts, and/or the key saved through\n * the plugin's own Settings panel (stored in a document — see\n * settingsStore.ts). The Settings-panel key takes priority once loaded, so a\n * merchant can paste their key into the Studio UI without touching code.\n */\nexport function createAltTextGeneratorProvider(staticConfig: AltTextGeneratorConfig) {\n return function AltTextGeneratorProvider(props: LayoutProps) {\n const client = useClient({apiVersion: API_VERSION})\n const [liveApiKey, setLiveApiKey] = useState<string | undefined>(undefined)\n const [liveAutoGenerate, setLiveAutoGenerate] = useState<boolean | undefined>(undefined)\n\n useEffect(() => {\n let cancelled = false\n getSettings(client)\n .then((settings) => {\n if (cancelled) return\n setLiveApiKey(settings.apiKey)\n setLiveAutoGenerate(settings.autoGenerate)\n })\n .catch(() => {\n // No settings doc yet, or no read access — fall back to static config silently.\n })\n return () => {\n cancelled = true\n }\n }, [client])\n\n const value = useMemo(\n () => ({\n apiKey: liveApiKey || staticConfig.apiKey || '',\n apiEndpoint: staticConfig.apiEndpoint,\n language: staticConfig.language,\n autoGenerate: liveAutoGenerate ?? staticConfig.autoGenerate ?? false,\n updateApiKey: setLiveApiKey,\n updateAutoGenerate: setLiveAutoGenerate,\n }),\n [liveApiKey, liveAutoGenerate, staticConfig],\n )\n\n return (\n <AltTextGeneratorContext.Provider value={value}>\n {props.renderDefault(props)}\n </AltTextGeneratorContext.Provider>\n )\n }\n}\n","import type {Schema, SchemaType} from 'sanity'\n\nexport interface AltTextFieldTarget {\n documentType: string\n /** e.g. ['mainImage'] or ['seo', 'ogImage'] — joined with '.' for GROQ/patch paths. */\n fieldPath: string[]\n}\n\nfunction chainHasName(type: SchemaType | undefined, name: string): boolean {\n let t: SchemaType | undefined = type\n while (t) {\n if (t.name === name) return true\n t = (t as {type?: SchemaType}).type\n }\n return false\n}\n\nfunction hasAltSubfield(imageType: SchemaType): boolean {\n const fields = (imageType as {fields?: {name: string}[]}).fields || []\n return fields.some((f) => f.name === 'alt')\n}\n\nconst MAX_NESTING_DEPTH = 2\n\nfunction walkFields(\n type: SchemaType,\n path: string[],\n documentType: string,\n out: AltTextFieldTarget[],\n seen: Set<string>,\n depth: number,\n) {\n const fields = (type as {fields?: {name: string; type: SchemaType}[]}).fields\n if (!fields) return\n\n for (const field of fields) {\n const fieldPath = [...path, field.name]\n const fieldType = field.type\n\n if (chainHasName(fieldType, 'image')) {\n if (hasAltSubfield(fieldType)) {\n const key = `${documentType}:${fieldPath.join('.')}`\n if (!seen.has(key)) {\n seen.add(key)\n out.push({documentType, fieldPath})\n }\n }\n continue\n }\n\n // Recurse into plain nested objects (e.g. `seo.ogImage`), skip arrays/references\n // to keep discovery predictable — deeply nested or array-of-image fields are a\n // known gap, not attempted here.\n if (fieldType.jsonType === 'object' && depth < MAX_NESTING_DEPTH) {\n walkFields(fieldType, fieldPath, documentType, out, seen, depth + 1)\n }\n }\n}\n\n/**\n * Walks every document type in the (fully merged, runtime) schema and finds\n * every image field that has an `alt` subfield — the convention this plugin's\n * own AltTextImageInput relies on, and the one most real-world Sanity schemas\n * already use for accessible images either way.\n */\nexport function findAltTextFields(schema: Schema): AltTextFieldTarget[] {\n const results: AltTextFieldTarget[] = []\n const seen = new Set<string>()\n\n for (const typeName of schema.getTypeNames()) {\n const type = schema.get(typeName)\n if (!type || !chainHasName(type, 'document')) continue\n walkFields(type, [], typeName, results, seen, 0)\n }\n\n return results\n}\n","import type {SanityClient} from 'sanity'\nimport type {AltTextFieldTarget} from './schemaScan'\n\nexport interface AltTextRow {\n key: string\n documentId: string\n documentType: string\n documentTitle: string\n fieldPath: string[]\n fieldLabel: string\n imageUrl: string | null\n alt: string\n}\n\nfunction fieldAlias(fieldPath: string[]): string {\n return 'f_' + fieldPath.join('_')\n}\n\nfunction groqPath(fieldPath: string[]): string {\n return fieldPath.join('.')\n}\n\n/**\n * One GROQ query per document type (fields are grouped by documentType first),\n * projecting every discovered image field on that type plus a best-effort title.\n */\nfunction buildQuery(documentType: string, fields: AltTextFieldTarget[]): string {\n const projections = fields\n .map((f) => {\n const alias = fieldAlias(f.fieldPath)\n const path = groqPath(f.fieldPath)\n return `\"${alias}\": ${path}{alt, \"url\": asset->url}`\n })\n .join(',\\n ')\n\n return `*[_type == \"${documentType}\"]{\n _id,\n _type,\n \"documentTitle\": coalesce(title, name, heading, label, _id),\n ${projections}\n }`\n}\n\nexport async function fetchAltTextRows(\n client: SanityClient,\n targets: AltTextFieldTarget[],\n): Promise<AltTextRow[]> {\n const byType = new Map<string, AltTextFieldTarget[]>()\n for (const t of targets) {\n const list = byType.get(t.documentType) || []\n list.push(t)\n byType.set(t.documentType, list)\n }\n\n const rows: AltTextRow[] = []\n\n for (const [documentType, fields] of byType) {\n const query = buildQuery(documentType, fields)\n const docs = await client.fetch<Record<string, unknown>[]>(query)\n\n for (const doc of docs) {\n for (const f of fields) {\n const alias = fieldAlias(f.fieldPath)\n const value = doc[alias] as {alt?: string; url?: string} | null | undefined\n if (!value || !value.url) continue // field empty on this doc — nothing to show\n\n rows.push({\n key: `${doc._id}:${f.fieldPath.join('.')}`,\n documentId: doc._id as string,\n documentType,\n documentTitle: (doc.documentTitle as string) || (doc._id as string),\n fieldPath: f.fieldPath,\n fieldLabel: f.fieldPath.join(' › '),\n imageUrl: value.url,\n alt: value.alt || '',\n })\n }\n }\n }\n\n return rows\n}\n\nexport interface AltTextUpdate {\n documentId: string\n fieldPath: string[]\n alt: string\n}\n\n/** Saves every update in one transaction — either all land or none do. */\nexport async function saveAltTextUpdates(client: SanityClient, updates: AltTextUpdate[]): Promise<void> {\n if (!updates.length) return\n\n let tx = client.transaction()\n for (const u of updates) {\n const patchKey = `${groqPath(u.fieldPath)}.alt`\n tx = tx.patch(u.documentId, (p) => p.set({[patchKey]: u.alt}))\n }\n await tx.commit()\n}\n","import type {AltTextGeneratorConfig} from './context'\n\nconst DEFAULT_ENDPOINT = 'https://alttextgeneratorai.com/api/sanity'\n\nexport type GenerateResult =\n | {ok: true; altText: string; creditsLeft: number | null}\n | {ok: false; message: string; tone: 'critical' | 'caution'}\n\nfunction messageForStatus(status: number, body: string): {message: string; tone: 'critical' | 'caution'} {\n if (status === 401) return {message: 'Invalid API key. Check it under the plugin config.', tone: 'critical'}\n if (status === 402) return {message: 'No credits left on this API key.', tone: 'caution'}\n return {message: body || `Generation failed (${status}).`, tone: 'critical'}\n}\n\nexport async function generateAltText(imageUrl: string, config: AltTextGeneratorConfig): Promise<GenerateResult> {\n try {\n const endpoint = config.apiEndpoint || DEFAULT_ENDPOINT\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n image: imageUrl,\n wpkey: config.apiKey,\n language: config.language || 'english',\n }),\n })\n\n const body = await res.text()\n\n if (!res.ok) {\n return {ok: false, ...messageForStatus(res.status, body)}\n }\n\n const creditsHeader = res.headers.get('X-Credits-Left')\n return {\n ok: true,\n altText: body.trim(),\n creditsLeft: creditsHeader === null ? null : Number(creditsHeader),\n }\n } catch (e) {\n return {\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n }\n }\n}\n","import type {AltTextGeneratorConfig} from './context'\n\nconst DEFAULT_ENDPOINT = 'https://alttextgeneratorai.com/api/sanity'\n\n/**\n * The credits-check endpoint isn't Sanity-specific — it's the same\n * `/api/verify` every plugin (Framer, WordPress, Magento...) already calls.\n * Derived from the generation endpoint so a local-dev `apiEndpoint` override\n * (e.g. http://localhost:3000/api/sanity) checks credits against the same\n * local backend instead of production.\n */\nfunction verifyEndpointFor(config: AltTextGeneratorConfig): string {\n const base = config.apiEndpoint || DEFAULT_ENDPOINT\n return base.replace(/\\/[^/]+$/, '/verify')\n}\n\nexport async function fetchCreditsRemaining(config: AltTextGeneratorConfig, apiKey: string): Promise<number | null> {\n if (!apiKey) return null\n try {\n const res = await fetch(verifyEndpointFor(config), {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({apiKey}),\n })\n if (!res.ok) return null\n const data = (await res.json()) as {freeRewritesLeft?: number}\n return typeof data.freeRewritesLeft === 'number' ? data.freeRewritesLeft : null\n } catch {\n return null\n }\n}\n","import {useCallback, useEffect, useState} from 'react'\nimport {Box, Button, Card, Checkbox, Flex, Stack, Text, TextInput} from '@sanity/ui'\nimport {useClient} from 'sanity'\nimport {useAltTextGeneratorConfig} from './context'\nimport {saveSettingsApiKey, saveSettingsAutoGenerate} from './settingsStore'\nimport {fetchCreditsRemaining} from './verifyCredits'\n\nconst API_VERSION = '2024-01-01'\nconst GET_KEY_URL = 'https://alttextgeneratorai.com/dashboard'\n\n/**\n * Lets a merchant paste their API key directly in the Studio, no code edit\n * required — saved to a plugin-internal settings document (settingsStore.ts)\n * and pushed straight into the shared context so every open field/tool picks\n * it up immediately. Also shows the current credit balance for the active key.\n */\nexport function SettingsPanel() {\n const client = useClient({apiVersion: API_VERSION})\n const config = useAltTextGeneratorConfig()\n\n const [keyInput, setKeyInput] = useState(config.apiKey)\n const [saving, setSaving] = useState(false)\n const [justSaved, setJustSaved] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [credits, setCredits] = useState<number | null>(null)\n const [checkingCredits, setCheckingCredits] = useState(false)\n\n // Keep the input in sync if the live key changes elsewhere (e.g. loaded after mount).\n useEffect(() => {\n setKeyInput(config.apiKey)\n }, [config.apiKey])\n\n const checkCredits = useCallback(\n async (apiKey: string) => {\n if (!apiKey) {\n setCredits(null)\n return\n }\n setCheckingCredits(true)\n const result = await fetchCreditsRemaining(config, apiKey)\n setCredits(result)\n setCheckingCredits(false)\n },\n [config],\n )\n\n useEffect(() => {\n checkCredits(config.apiKey)\n // Only re-check when the live key itself changes, not on every config identity change.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n const handleSave = useCallback(async () => {\n setSaving(true)\n setError(null)\n setJustSaved(false)\n try {\n const trimmed = keyInput.trim()\n await saveSettingsApiKey(client, trimmed)\n config.updateApiKey(trimmed)\n setJustSaved(true)\n checkCredits(trimmed)\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to save.')\n } finally {\n setSaving(false)\n }\n }, [client, keyInput, config, checkCredits])\n\n const handleAutoGenerate = useCallback(\n async (checked: boolean) => {\n setError(null)\n const previous = config.autoGenerate\n config.updateAutoGenerate(checked)\n try {\n await saveSettingsAutoGenerate(client, checked)\n } catch (e) {\n config.updateAutoGenerate(previous)\n setError(e instanceof Error ? e.message : 'Failed to save.')\n }\n },\n [client, config],\n )\n\n return (\n <Card padding={4} radius={2} border tone=\"transparent\">\n <Stack gap={3}>\n <Text size={1} weight=\"medium\">\n API Key\n </Text>\n <Text size={1} muted>\n Paste your Alt Text Generator API key below.{' '}\n <a href={GET_KEY_URL} target=\"_blank\" rel=\"noreferrer\">\n Get a free API key →\n </a>\n </Text>\n <Flex gap={2} align=\"center\" wrap=\"wrap\">\n <Box flex={1} style={{minWidth: 240}}>\n <TextInput\n value={keyInput}\n placeholder=\"Paste your API key\"\n onChange={(e) => setKeyInput(e.currentTarget.value)}\n />\n </Box>\n <Button\n text={saving ? 'Saving…' : 'Save'}\n tone=\"primary\"\n disabled={saving || keyInput.trim() === config.apiKey}\n onClick={handleSave}\n />\n </Flex>\n <Flex as=\"label\" gap={2} align=\"center\">\n <Checkbox checked={config.autoGenerate} onChange={(e) => handleAutoGenerate(e.currentTarget.checked)} />\n <Text size={1}>Write alt text automatically when an image is uploaded (uses 1 credit per image)</Text>\n </Flex>\n {justSaved && (\n <Text size={1} style={{color: 'var(--card-positive-fg-color, #2a9d5c)'}}>\n Saved.\n </Text>\n )}\n {error && (\n <Text size={1} style={{color: 'var(--card-critical-fg-color, #e02b27)'}}>\n {error}\n </Text>\n )}\n {!config.apiKey && (\n <Text size={1} style={{color: 'var(--card-caution-fg-color, #a56a0d)'}}>\n No API key configured yet — generation will fail until you add one.\n </Text>\n )}\n {config.apiKey && (\n <Text size={1} muted>\n {checkingCredits\n ? 'Checking credits…'\n : credits === null\n ? 'Could not check credits for this key.'\n : `${credits} credit${credits === 1 ? '' : 's'} remaining`}\n </Text>\n )}\n </Stack>\n </Card>\n )\n}\n","import {useCallback, useMemo, useRef, useState} from 'react'\nimport {\n Box,\n Button,\n Card,\n Checkbox,\n Container,\n Flex,\n Heading,\n Stack,\n Text,\n TextArea,\n Spinner,\n} from '@sanity/ui'\nimport {useClient, useSchema} from 'sanity'\nimport {findAltTextFields} from './schemaScan'\nimport {fetchAltTextRows, saveAltTextUpdates, type AltTextRow} from './dashboardData'\nimport {generateAltText} from './generateAltText'\nimport {useAltTextGeneratorConfig} from './context'\nimport {SettingsPanel} from './SettingsPanel'\n\nconst API_VERSION = '2024-01-01'\n\ntype Filter = 'missing' | 'all'\n\n/**\n * The bulk companion to AltTextImageInput: scans every document type in the\n * schema for image fields that have an `alt` subfield (see schemaScan.ts),\n * lists every image found across the dataset, and lets the user review,\n * generate, and save alt text for many images in one pass.\n */\nexport function AltTextDashboard() {\n const schema = useSchema()\n const client = useClient({apiVersion: API_VERSION})\n const config = useAltTextGeneratorConfig()\n\n const [scanning, setScanning] = useState(false)\n const [scanError, setScanError] = useState<string | null>(null)\n const [rows, setRows] = useState<AltTextRow[]>([])\n const [scannedTypes, setScannedTypes] = useState(0)\n\n const [filter, setFilter] = useState<Filter>('missing')\n const [overwrite, setOverwrite] = useState(false)\n const [selected, setSelected] = useState<Set<string>>(new Set())\n const [dirty, setDirty] = useState<Record<string, string>>({}) // key -> new alt text\n const [rowErrors, setRowErrors] = useState<Record<string, string>>({})\n\n const [bulkRunning, setBulkRunning] = useState(false)\n const [progress, setProgress] = useState('')\n const [credits, setCredits] = useState<number | null>(null)\n const [saving, setSaving] = useState(false)\n const stopRequested = useRef(false)\n\n const targets = useMemo(() => findAltTextFields(schema), [schema])\n\n const handleScan = useCallback(async () => {\n setScanning(true)\n setScanError(null)\n try {\n const found = await fetchAltTextRows(client, targets)\n setRows(found)\n setScannedTypes(new Set(targets.map((t) => t.documentType)).size)\n setDirty({})\n setSelected(new Set())\n setRowErrors({})\n } catch (e) {\n setScanError(e instanceof Error ? e.message : 'Scan failed.')\n } finally {\n setScanning(false)\n }\n }, [client, targets])\n\n const currentAlt = useCallback(\n (row: AltTextRow) => (row.key in dirty ? dirty[row.key] : row.alt),\n [dirty],\n )\n\n const visibleRows = useMemo(() => {\n return rows.filter((row) => {\n if (filter === 'all') return true\n const text = currentAlt(row)\n return !text || text.trim() === ''\n })\n }, [rows, filter, currentAlt])\n\n const isLocked = useCallback(\n (row: AltTextRow) => !overwrite && currentAlt(row).trim() !== '',\n [overwrite, currentAlt],\n )\n\n const setRowText = useCallback((key: string, text: string) => {\n setDirty((prev) => ({...prev, [key]: text}))\n }, [])\n\n const generateOne = useCallback(\n async (row: AltTextRow): Promise<{ok: boolean; message?: string}> => {\n setRowErrors((prev) => {\n const next = {...prev}\n delete next[row.key]\n return next\n })\n if (!row.imageUrl) return {ok: false, message: 'No image URL.'}\n\n const result = await generateAltText(row.imageUrl, config)\n if (!result.ok) {\n setRowErrors((prev) => ({...prev, [row.key]: result.message}))\n return {ok: false, message: result.message}\n }\n setRowText(row.key, result.altText)\n if (result.creditsLeft !== null) setCredits(result.creditsLeft)\n return {ok: true}\n },\n [config, setRowText],\n )\n\n const runBulk = useCallback(\n async (list: AltTextRow[]) => {\n setBulkRunning(true)\n stopRequested.current = false\n let done = 0\n let applied = 0\n let failed = 0\n let lastError = ''\n\n for (const row of list) {\n if (stopRequested.current) break\n const result = await generateOne(row)\n done++\n if (result.ok) {\n applied++\n } else {\n failed++\n lastError = result.message || ''\n if (lastError.toLowerCase().includes('credits')) {\n setProgress(`Stopped: out of credits. ${applied} generated.`)\n break\n }\n }\n setProgress(\n `${done} / ${list.length} · ${applied} generated` +\n (failed ? ` · ${failed} failed — last error: ${lastError}` : ''),\n )\n }\n\n setBulkRunning(false)\n },\n [generateOne],\n )\n\n const handleGenerateSelected = useCallback(() => {\n const list = visibleRows.filter((r) => selected.has(r.key) && !isLocked(r))\n if (!list.length) return\n runBulk(list)\n }, [visibleRows, selected, isLocked, runBulk])\n\n const handleGenerateAllMissing = useCallback(() => {\n const list = rows.filter((r) => currentAlt(r).trim() === '')\n if (!list.length) return\n runBulk(list)\n }, [rows, currentAlt, runBulk])\n\n const handleSave = useCallback(async () => {\n const updates = Object.entries(dirty).map(([key, alt]) => {\n const row = rows.find((r) => r.key === key)!\n return {documentId: row.documentId, fieldPath: row.fieldPath, alt}\n })\n if (!updates.length) return\n\n setSaving(true)\n try {\n await saveAltTextUpdates(client, updates)\n // Reflect saved values into the base rows and clear dirty state.\n setRows((prev) =>\n prev.map((r) => (r.key in dirty ? {...r, alt: dirty[r.key]} : r)),\n )\n setDirty({})\n setProgress(`Saved ${updates.length} image(s).`)\n } catch (e) {\n setProgress(`Save failed: ${e instanceof Error ? e.message : 'unknown error'}`)\n } finally {\n setSaving(false)\n }\n }, [dirty, rows, client])\n\n const toggleSelectAll = useCallback(\n (checked: boolean) => {\n setSelected(checked ? new Set(visibleRows.filter((r) => !isLocked(r)).map((r) => r.key)) : new Set())\n },\n [visibleRows, isLocked],\n )\n\n const missingCount = rows.filter((r) => currentAlt(r).trim() === '').length\n\n return (\n <Container width={4} padding={4}>\n <Stack gap={4}>\n <Heading size={2}>Alt Text Generator AI</Heading>\n\n <SettingsPanel />\n\n <Flex align=\"center\" gap={3}>\n <Button text={scanning ? 'Scanning…' : 'Scan Dataset'} tone=\"primary\" disabled={scanning} onClick={handleScan} />\n {rows.length > 0 && (\n <Text size={1} muted>\n {rows.length} image{rows.length === 1 ? '' : 's'} · {missingCount} missing alt text (scanned {scannedTypes}{' '}\n document type{scannedTypes === 1 ? '' : 's'})\n </Text>\n )}\n {scanning && <Spinner muted />}\n </Flex>\n\n {scanError && (\n <Card tone=\"critical\" padding={3} radius={2} border>\n <Text size={1}>{scanError}</Text>\n </Card>\n )}\n\n {targets.length === 0 && !scanning && (\n <Card tone=\"caution\" padding={3} radius={2} border>\n <Text size={1}>\n No image fields with an "alt" subfield were found in this Studio's schema. Add an{' '}\n <code>alt</code> string field to an image type (see the README) for it to show up here.\n </Text>\n </Card>\n )}\n\n {rows.length > 0 && (\n <Stack gap={4}>\n <Flex align=\"center\" gap={3}>\n <Button\n text=\"Missing\"\n mode={filter === 'missing' ? 'default' : 'ghost'}\n tone={filter === 'missing' ? 'primary' : 'default'}\n onClick={() => setFilter('missing')}\n />\n <Button\n text=\"All\"\n mode={filter === 'all' ? 'default' : 'ghost'}\n tone={filter === 'all' ? 'primary' : 'default'}\n onClick={() => setFilter('all')}\n />\n <Flex align=\"center\" gap={2}>\n <Checkbox checked={overwrite} onChange={(e) => setOverwrite(e.currentTarget.checked)} id=\"atg-overwrite\" />\n <Text as=\"label\" size={1} htmlFor=\"atg-overwrite\">\n Overwrite images that already have alt text\n </Text>\n </Flex>\n </Flex>\n\n <Flex align=\"center\" gap={2}>\n <Checkbox\n checked={visibleRows.length > 0 && visibleRows.every((r) => isLocked(r) || selected.has(r.key))}\n onChange={(e) => toggleSelectAll(e.currentTarget.checked)}\n id=\"atg-select-all\"\n />\n <Text as=\"label\" size={1} htmlFor=\"atg-select-all\">\n Select all visible {selected.size > 0 && `(${selected.size} selected)`}\n </Text>\n </Flex>\n\n <Card border radius={2} style={{maxHeight: 520, overflowY: 'auto'}}>\n <Stack gap={0}>\n {visibleRows.map((row) => {\n const locked = isLocked(row)\n const text = currentAlt(row)\n const error = rowErrors[row.key]\n return (\n <Flex key={row.key} padding={3} gap={3} align=\"flex-start\" style={{borderBottom: '1px solid var(--card-border-color)'}}>\n <Box paddingTop={1}>\n <Checkbox\n checked={selected.has(row.key)}\n disabled={locked}\n onChange={(e) => {\n // Extract the primitive before it reaches the state\n // updater — React can invoke that updater outside the\n // original synchronous event (Strict Mode double-invokes\n // it), and by then `e.currentTarget` is already null.\n const checked = e.currentTarget.checked\n setSelected((prev) => {\n const next = new Set(prev)\n if (checked) next.add(row.key)\n else next.delete(row.key)\n return next\n })\n }}\n />\n </Box>\n {row.imageUrl && (\n <img\n src={`${row.imageUrl}?w=80&h=80&fit=crop`}\n width={56}\n height={56}\n style={{objectFit: 'cover', borderRadius: 4, flexShrink: 0}}\n alt=\"\"\n />\n )}\n <Box style={{width: 220, flexShrink: 0}}>\n <Stack gap={2}>\n <Text size={1} weight=\"medium\">\n {row.documentTitle}\n </Text>\n <Text size={1} muted>\n {row.documentType} · {row.fieldLabel}\n </Text>\n </Stack>\n </Box>\n <Box flex={1}>\n <Stack gap={2}>\n <TextArea\n rows={2}\n value={text}\n disabled={locked}\n onChange={(e) => setRowText(row.key, e.currentTarget.value)}\n />\n {error && (\n <Text size={1} style={{color: 'var(--card-critical-fg-color, #e02b27)'}}>\n {error}\n </Text>\n )}\n </Stack>\n </Box>\n <Box paddingTop={1}>\n <Button\n text=\"Generate\"\n mode=\"ghost\"\n fontSize={1}\n disabled={locked || bulkRunning}\n onClick={() => generateOne(row)}\n />\n </Box>\n </Flex>\n )\n })}\n </Stack>\n </Card>\n\n <Flex align=\"center\" gap={3} wrap=\"wrap\">\n <Button\n text=\"Generate Selected\"\n tone=\"primary\"\n disabled={bulkRunning || selected.size === 0}\n onClick={handleGenerateSelected}\n />\n <Button\n text=\"Generate All Missing\"\n mode=\"ghost\"\n disabled={bulkRunning || missingCount === 0}\n onClick={handleGenerateAllMissing}\n />\n {bulkRunning && (\n <Button text=\"Stop\" mode=\"ghost\" tone=\"critical\" onClick={() => (stopRequested.current = true)} />\n )}\n <Button\n text={saving ? 'Saving…' : 'Save Changes'}\n tone=\"positive\"\n disabled={saving || Object.keys(dirty).length === 0}\n onClick={handleSave}\n />\n {credits !== null && (\n <Text size={1} muted>\n ({credits} credits remaining)\n </Text>\n )}\n </Flex>\n\n {progress && (\n <Text size={1} muted>\n {progress}\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n </Container>\n )\n}\n","import {useEffect, useRef} from 'react'\nimport {useAltTextGeneratorConfig} from './context'\n\n/**\n * Runs `generate` once when an image is uploaded or swapped, if auto-generate\n * is switched on and an API key is set. A first upload only gets alt text when\n * the field is empty. Swapping one image for another always writes new alt\n * text, because the old text describes the old image. The asset the form opens\n * with is remembered first, so existing images are never touched just by\n * opening a document.\n */\nexport function useAutoGenerate(assetRef: string | undefined, hasAlt: boolean, generate: () => void) {\n const config = useAltTextGeneratorConfig()\n const lastRef = useRef(assetRef)\n const latest = useRef({generate, hasAlt, enabled: false})\n latest.current = {generate, hasAlt, enabled: config.autoGenerate && Boolean(config.apiKey)}\n\n useEffect(() => {\n const previous = lastRef.current\n lastRef.current = assetRef\n if (!assetRef || assetRef === previous) return\n const {generate: run, hasAlt: filled, enabled} = latest.current\n if (!enabled) return\n if (previous || !filled) run()\n }, [assetRef])\n}\n","import {useCallback, useState} from 'react'\nimport {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, useClient, type ImageInputProps} from 'sanity'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {useAltTextGeneratorConfig} from './context'\nimport {generateAltText, type GenerateResult} from './generateAltText'\nimport {useAutoGenerate} from './useAutoGenerate'\n\nconst API_VERSION = '2024-01-01'\n\n/**\n * Drop-in replacement for the default image input: renders the normal image\n * field (upload, crop, hotspot — via `renderDefault`) plus a \"Generate Alt\n * Text\" button that sends the uploaded asset's CDN URL to the Alt Text\n * Generator API and writes the result into the image's `alt` field.\n */\nexport function AltTextImageInput(props: ImageInputProps) {\n const {value, onChange} = props\n const config = useAltTextGeneratorConfig()\n const client = useClient({apiVersion: API_VERSION})\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<Extract<GenerateResult, {ok: false}> | null>(null)\n\n const handleGenerate = useCallback(async () => {\n if (!value?.asset?._ref || !config.apiKey) return\n setLoading(true)\n setError(null)\n\n try {\n const {projectId, dataset} = client.config()\n if (!projectId || !dataset) {\n throw new Error('Could not determine the Sanity project/dataset for this Studio.')\n }\n const imageUrl = imageUrlBuilder({projectId, dataset}).image(value).url()\n\n const result = await generateAltText(imageUrl, config)\n if (!result.ok) {\n setError(result)\n return\n }\n onChange(set(result.altText, ['alt']))\n } catch (e) {\n setError({\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n })\n } finally {\n setLoading(false)\n }\n }, [value, client, config, onChange])\n\n useAutoGenerate(value?.asset?._ref, Boolean((value as {alt?: string} | undefined)?.alt), handleGenerate)\n\n return (\n <Stack gap={2}>\n {props.renderDefault(props)}\n {value?.asset && (\n <Stack gap={2}>\n <Flex>\n <Button\n text={loading ? 'Generating…' : 'Generate Alt Text'}\n tone=\"primary\"\n mode=\"ghost\"\n disabled={loading || !config.apiKey}\n onClick={handleGenerate}\n />\n </Flex>\n {error && (\n <Card tone={error.tone} padding={2} radius={2} border>\n <Text size={1}>{error.message}</Text>\n </Card>\n )}\n {!config.apiKey && (\n <Text size={1} muted>\n No API key configured — open the "Alt Text Generator" tool in the nav to add one.\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n )\n}\n","import {useCallback, useState} from 'react'\nimport {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, useClient, useFormValue, type StringInputProps} from 'sanity'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {useAltTextGeneratorConfig} from './context'\nimport {generateAltText, type GenerateResult} from './generateAltText'\nimport {useAutoGenerate} from './useAutoGenerate'\n\nconst API_VERSION = '2024-01-01'\n\ntype ImageWithAsset = {asset?: {_ref: string}}\n\n/**\n * Attaches to the `alt` STRING field itself (not the whole image type), so\n * the \"Generate Alt Text\" button renders directly under the Alternative Text\n * input instead of below every other field on the image (e.g. Caption).\n *\n * Reads the sibling image value via useFormValue since a field-level input\n * component only gets its own value/path, not the parent object's.\n */\nexport function AltTextField(props: StringInputProps) {\n const {onChange, value = ''} = props\n const config = useAltTextGeneratorConfig()\n const client = useClient({apiVersion: API_VERSION})\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<Extract<GenerateResult, {ok: false}> | null>(null)\n\n // props.path is e.g. ['mainImage', 'alt'] — the image value lives one level up.\n const imagePath = props.path.slice(0, -1)\n const imageValue = useFormValue(imagePath) as ImageWithAsset | undefined\n\n const handleGenerate = useCallback(async () => {\n if (!imageValue?.asset?._ref || !config.apiKey) return\n setLoading(true)\n setError(null)\n\n try {\n const {projectId, dataset} = client.config()\n if (!projectId || !dataset) {\n throw new Error('Could not determine the Sanity project/dataset for this Studio.')\n }\n const imageUrl = imageUrlBuilder({projectId, dataset}).image(imageValue).url()\n\n const result = await generateAltText(imageUrl, config)\n if (!result.ok) {\n setError(result)\n return\n }\n onChange(set(result.altText))\n } catch (e) {\n setError({\n ok: false,\n message: e instanceof Error ? e.message : 'Network error while generating alt text.',\n tone: 'critical',\n })\n } finally {\n setLoading(false)\n }\n }, [imageValue, client, config, onChange])\n\n useAutoGenerate(imageValue?.asset?._ref, Boolean(value), handleGenerate)\n\n return (\n <Stack gap={2}>\n {props.renderDefault(props)}\n {imageValue?.asset && (\n <Stack gap={2}>\n <Flex>\n <Button\n text={loading ? 'Generating…' : 'Generate Alt Text'}\n tone=\"primary\"\n mode=\"ghost\"\n disabled={loading || !config.apiKey}\n onClick={handleGenerate}\n />\n </Flex>\n {error && (\n <Card tone={error.tone} padding={2} radius={2} border>\n <Text size={1}>{error.message}</Text>\n </Card>\n )}\n {!config.apiKey && (\n <Text size={1} muted>\n No API key configured — open the "Alt Text Generator" tool in the nav to add one.\n </Text>\n )}\n </Stack>\n )}\n </Stack>\n )\n}\n","import {definePlugin} from 'sanity'\nimport type {AltTextGeneratorConfig} from './context'\nimport {createAltTextGeneratorProvider} from './AltTextGeneratorProvider'\nimport {AltTextDashboard} from './AltTextDashboard'\n\nexport {AltTextImageInput} from './AltTextImageInput'\nexport {AltTextField} from './AltTextField'\nexport {AltTextDashboard} from './AltTextDashboard'\nexport type {AltTextGeneratorConfig} from './context'\n\n/**\n * Usage in `sanity.config.ts`:\n *\n * ```ts\n * import {defineConfig} from 'sanity'\n * import {altTextGenerator} from 'sanity-plugin-alt-text-generator'\n *\n * export default defineConfig({\n * // ...\n * plugins: [altTextGenerator()],\n * })\n * ```\n *\n * No API key required at setup time — open the \"Alt Text Generator\" tool in\n * the Studio nav and paste your key into its Settings panel (get a free one\n * at alttextgeneratorai.com). You can also pass a default `apiKey` here for\n * local development/CI; the Settings-panel key takes priority once saved.\n *\n * To write alt text automatically when an image is uploaded (if its alt text\n * is empty) or swapped for another image, tick the checkbox in the same Settings panel or pass\n * `altTextGenerator({autoGenerate: true})`. Each image uses one credit.\n *\n * Then wire it into your schema in one of two ways:\n *\n * 1. On the `alt` string field itself (recommended — puts the \"Generate Alt\n * Text\" button right under Alternative Text, above any other fields like\n * Caption):\n *\n * ```ts\n * defineField({\n * name: 'alt',\n * type: 'string',\n * components: {input: AltTextField},\n * })\n * ```\n *\n * 2. On the whole image field (button renders below all of the image's\n * fields at once):\n *\n * ```ts\n * defineField({\n * name: 'mainImage',\n * type: 'image',\n * components: {input: AltTextImageInput},\n * })\n * ```\n *\n * @public\n */\nexport const altTextGenerator = definePlugin<AltTextGeneratorConfig | void>((config) => {\n const staticConfig = config || {}\n\n return {\n name: 'sanity-plugin-alt-text-generator',\n studio: {\n components: {\n layout: createAltTextGeneratorProvider(staticConfig),\n },\n },\n tools: (prev) => [\n ...prev,\n {\n name: 'alt-text-generator',\n title: 'Alt Text Generator',\n component: AltTextDashboard,\n },\n ],\n }\n})\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-alt-text-generator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Generate AI-powered, SEO-friendly alt text for images inside Sanity Studio — per-field, or in bulk across your whole dataset from a built-in dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|