@jhb.software/payload-alt-text-plugin 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -205,9 +205,9 @@ export const customResolver = (): AltTextResolver => ({
205
205
 
206
206
  ## REST API Endpoints
207
207
 
208
- The plugin registers the following REST API endpoints under `/api/alt-text-plugin/`. All endpoints require authentication by default (configurable via the `access` option). Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via the `healthCheck` function).
208
+ The plugin registers the following REST API endpoints under `/api/alt-text/`. All endpoints require authentication by default (configurable via the `access` option). Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via the `healthCheck` function).
209
209
 
210
- ### `POST /api/alt-text-plugin/generate`
210
+ ### `POST /api/alt-text/generate`
211
211
 
212
212
  Generates alt text for a single image. By default, returns the result without saving it (preview mode). Pass `update: true` to also persist the generated alt text and keywords to the document.
213
213
 
@@ -231,7 +231,7 @@ Generates alt text for a single image. By default, returns the result without sa
231
231
  }
232
232
  ```
233
233
 
234
- ### `POST /api/alt-text-plugin/generate/bulk`
234
+ ### `POST /api/alt-text/generate/bulk`
235
235
 
236
236
  Generates and persists alt text for multiple images across all configured locales.
237
237
 
@@ -252,7 +252,7 @@ Generates and persists alt text for multiple images across all configured locale
252
252
  }
253
253
  ```
254
254
 
255
- ### `GET /api/alt-text-plugin/health`
255
+ ### `GET /api/alt-text/health`
256
256
 
257
257
  Returns alt text coverage statistics across all configured collections. Only available when `healthCheck` is enabled.
258
258
 
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui';
4
4
  import { useRouter } from 'next/navigation.js';
5
5
  import { useTransition } from 'react';
6
+ import { PLUGIN_SLUG } from '../constants.js';
6
7
  import { Lightning } from './icons/Lightning.js';
7
8
  import { Spinner } from './icons/Spinner.js';
8
9
  export function BulkGenerateAltTextsButton({ collectionSlug }) {
@@ -20,7 +21,7 @@ export function BulkGenerateAltTextsButton({ collectionSlug }) {
20
21
  throw new Error('Collection slug is required');
21
22
  }
22
23
  try {
23
- const response = await fetch(`${serverURL ?? ''}${apiRoute}/alt-text-plugin/generate/bulk`, {
24
+ const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {
24
25
  body: JSON.stringify({
25
26
  collection: collectionSlug,
26
27
  ids: selectedIds
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { permissions } = useAuth()\n const { selected, setSelection } = useSelection()\n\n const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update)\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(\n `${serverURL ?? ''}${apiRoute}/alt-text-plugin/generate/bulk`,\n {\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n method: 'POST',\n },\n )\n\n if (!response.ok) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as {\n erroredDocs: string[]\n totalDocs: number\n updatedDocs: number\n }\n\n if (data.erroredDocs.length > 0) {\n toast.error(\n t('@jhb.software/payload-alt-text-plugin:failedToGenerateForXImages', {\n count: data.erroredDocs.length,\n }),\n )\n }\n\n // in case not all images were updated, show a warning instead of a success message:\n if (data.updatedDocs === data.totalDocs) {\n toast.success(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n } else {\n toast.warning(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n }\n\n // deselect all previously selected images\n for (const id of selectedIds) {\n setSelection(id)\n }\n\n router.refresh()\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n canUpdateCollection &&\n selectedIds.length > 0 && (\n <div className=\"m-0\" style={{ display: 'flex', justifyContent: 'right' }}>\n <Button\n className=\"m-0\"\n disabled={isPending || selectedIds.length === 0}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltTexts}\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltTextFor', {\n count: selectedIds.length,\n })}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useAuth","useConfig","useSelection","useTranslation","useRouter","useTransition","Lightning","Spinner","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","permissions","selected","setSelection","canUpdateCollection","Boolean","collections","update","config","routes","api","apiRoute","serverURL","selectedIds","Array","from","entries","filter","isSelected","map","id","router","handleGenerateAltTexts","Error","response","fetch","body","JSON","stringify","collection","ids","method","ok","error","data","json","erroredDocs","length","count","updatedDocs","totalDocs","success","total","updated","warning","refresh","console","div","className","style","display","justifyContent","disabled","icon","onClick"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,SAAS,EAAEC,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAChG,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAOrC,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAE5C,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGP;IACd,MAAM,CAACQ,WAAWC,gBAAgB,GAAGP;IACrC,MAAM,EAAEQ,WAAW,EAAE,GAAGb;IACxB,MAAM,EAAEc,QAAQ,EAAEC,YAAY,EAAE,GAAGb;IAEnC,MAAMc,sBAAsBC,QAAQJ,aAAaK,aAAa,CAACT,eAAe,EAAEU;IAChF,MAAM,EACJC,QAAQ,EACNC,QAAQ,EAAEC,KAAKC,QAAQ,EAAE,EACzBC,SAAS,EACV,EACF,GAAGvB;IAEJ,MAAMwB,cAAcC,MAAMC,IAAI,CAACb,SAASc,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAAS7B;IAEf,MAAM8B,yBAAyB;QAC7BtB,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAI0B,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MACrB,GAAGb,aAAa,KAAKD,SAAS,8BAA8B,CAAC,EAC7D;oBACEe,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYhC;wBACZiC,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAGF,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChB7C,MAAM8C,KAAK,CAACnC,EAAE;oBACd;gBACF;gBAEA,MAAMoC,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BlD,MAAM8C,KAAK,CACTnC,EAAE,oEAAoE;wBACpEwC,OAAOJ,KAAKE,WAAW,CAACC,MAAM;oBAChC;gBAEJ;gBAEA,oFAAoF;gBACpF,IAAIH,KAAKK,WAAW,KAAKL,KAAKM,SAAS,EAAE;oBACvCrD,MAAMsD,OAAO,CACX3C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ,OAAO;oBACLpD,MAAMyD,OAAO,CACX9C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BV,aAAaiB;gBACf;gBAEAC,OAAOwB,OAAO;YAChB,EAAE,OAAOZ,OAAO;gBACda,QAAQb,KAAK,CAAC,8BAA8BA;gBAC5C9C,MAAM8C,KAAK,CAACnC,EAAE;YAChB;QACF;IACF;IAEA,OACEM,uBACAS,YAAYwB,MAAM,GAAG,mBACnB,KAACU;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,KAACjE;YACC8D,WAAU;YACVI,UAAUrD,aAAac,YAAYwB,MAAM,KAAK;YAC9CgB,MAAMtD,0BAAY,KAACJ,6BAAa,KAACD;YACjC4D,SAAShC;sBAERxB,EAAE,4DAA4D;gBAC7DwC,OAAOzB,YAAYwB,MAAM;YAC3B;;;AAKV"}
1
+ {"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { permissions } = useAuth()\n const { selected, setSelection } = useSelection()\n\n const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update)\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as {\n erroredDocs: string[]\n totalDocs: number\n updatedDocs: number\n }\n\n if (data.erroredDocs.length > 0) {\n toast.error(\n t('@jhb.software/payload-alt-text-plugin:failedToGenerateForXImages', {\n count: data.erroredDocs.length,\n }),\n )\n }\n\n // in case not all images were updated, show a warning instead of a success message:\n if (data.updatedDocs === data.totalDocs) {\n toast.success(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n } else {\n toast.warning(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n }\n\n // deselect all previously selected images\n for (const id of selectedIds) {\n setSelection(id)\n }\n\n router.refresh()\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n canUpdateCollection &&\n selectedIds.length > 0 && (\n <div className=\"m-0\" style={{ display: 'flex', justifyContent: 'right' }}>\n <Button\n className=\"m-0\"\n disabled={isPending || selectedIds.length === 0}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltTexts}\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltTextFor', {\n count: selectedIds.length,\n })}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useAuth","useConfig","useSelection","useTranslation","useRouter","useTransition","PLUGIN_SLUG","Lightning","Spinner","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","permissions","selected","setSelection","canUpdateCollection","Boolean","collections","update","config","routes","api","apiRoute","serverURL","selectedIds","Array","from","entries","filter","isSelected","map","id","router","handleGenerateAltTexts","Error","response","fetch","body","JSON","stringify","collection","ids","method","ok","error","data","json","erroredDocs","length","count","updatedDocs","totalDocs","success","total","updated","warning","refresh","console","div","className","style","display","justifyContent","disabled","icon","onClick"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,SAAS,EAAEC,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAChG,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAOrC,SAASC,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAE5C,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGR;IACd,MAAM,CAACS,WAAWC,gBAAgB,GAAGR;IACrC,MAAM,EAAES,WAAW,EAAE,GAAGd;IACxB,MAAM,EAAEe,QAAQ,EAAEC,YAAY,EAAE,GAAGd;IAEnC,MAAMe,sBAAsBC,QAAQJ,aAAaK,aAAa,CAACT,eAAe,EAAEU;IAChF,MAAM,EACJC,QAAQ,EACNC,QAAQ,EAAEC,KAAKC,QAAQ,EAAE,EACzBC,SAAS,EACV,EACF,GAAGxB;IAEJ,MAAMyB,cAAcC,MAAMC,IAAI,CAACb,SAASc,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAAS9B;IAEf,MAAM+B,yBAAyB;QAC7BtB,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAI0B,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,GAAGb,aAAa,KAAKD,SAAS,CAAC,EAAElB,YAAY,cAAc,CAAC,EAAE;oBACzFiC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYhC;wBACZiC,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChB9C,MAAM+C,KAAK,CAACnC,EAAE;oBACd;gBACF;gBAEA,MAAMoC,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BnD,MAAM+C,KAAK,CACTnC,EAAE,oEAAoE;wBACpEwC,OAAOJ,KAAKE,WAAW,CAACC,MAAM;oBAChC;gBAEJ;gBAEA,oFAAoF;gBACpF,IAAIH,KAAKK,WAAW,KAAKL,KAAKM,SAAS,EAAE;oBACvCtD,MAAMuD,OAAO,CACX3C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ,OAAO;oBACLrD,MAAM0D,OAAO,CACX9C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BV,aAAaiB;gBACf;gBAEAC,OAAOwB,OAAO;YAChB,EAAE,OAAOZ,OAAO;gBACda,QAAQb,KAAK,CAAC,8BAA8BA;gBAC5C/C,MAAM+C,KAAK,CAACnC,EAAE;YAChB;QACF;IACF;IAEA,OACEM,uBACAS,YAAYwB,MAAM,GAAG,mBACnB,KAACU;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,KAAClE;YACC+D,WAAU;YACVI,UAAUrD,aAAac,YAAYwB,MAAM,KAAK;YAC9CgB,MAAMtD,0BAAY,KAACJ,6BAAa,KAACD;YACjC4D,SAAShC;sBAERxB,EAAE,4DAA4D;gBAC7DwC,OAAOzB,YAAYwB,MAAM;YAC3B;;;AAKV"}
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Button, toast, useConfig, useDocumentInfo, useField, useLocale, useTranslation } from '@payloadcms/ui';
4
4
  import { useTransition } from 'react';
5
+ import { PLUGIN_SLUG } from '../constants.js';
5
6
  import { Lightning } from './icons/Lightning.js';
6
7
  import { Spinner } from './icons/Spinner.js';
7
8
  export function GenerateAltTextButton({ supportedMimeTypes }) {
@@ -32,7 +33,7 @@ export function GenerateAltTextButton({ supportedMimeTypes }) {
32
33
  }
33
34
  startTransition(async ()=>{
34
35
  try {
35
- const response = await fetch(`${serverURL ?? ''}${apiRoute}/alt-text-plugin/generate`, {
36
+ const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate`, {
36
37
  body: JSON.stringify({
37
38
  id: id,
38
39
  collection: collectionSlug,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Button,\n toast,\n useConfig,\n useDocumentInfo,\n useField,\n useLocale,\n useTranslation,\n} from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function GenerateAltTextButton({ supportedMimeTypes }: { supportedMimeTypes?: string[] }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const { id, collectionSlug, docPermissions } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isUnsupportedMimeType =\n !!mimeType && !!supportedMimeTypes && !supportedMimeTypes.includes(mimeType)\n\n // Hide the generate button from users who cannot update the document — the\n // generated alt text would not be persistable by them anyway.\n if (!docPermissions?.update) {\n return null\n }\n\n const handleGenerateAltText = () => {\n if (!collectionSlug || !id) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:cannotGenerateMissingFields'))\n throw new Error('Missing required fields')\n }\n\n startTransition(async () => {\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/alt-text-plugin/generate`, {\n body: JSON.stringify({\n id: id as string,\n collection: collectionSlug,\n locale: locale?.code ?? null, // sent null when localization is disabled\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n let errorMessage = t('@jhb.software/payload-alt-text-plugin:failedToGenerate')\n try {\n const errorData = (await response.json()) as { error: string }\n errorMessage = errorData.error\n } catch (error) {\n console.error('Error generating alt text:', error)\n }\n\n toast.error(errorMessage)\n return\n }\n\n const data = (await response.json()) as {\n altText: string\n keywords: string[]\n }\n\n if (data.altText && data.keywords) {\n setAltText(data.altText)\n setKeywords(data.keywords)\n toast.success(t('@jhb.software/payload-alt-text-plugin:altTextGeneratedSuccess'))\n } else {\n toast.error(t('@jhb.software/payload-alt-text-plugin:noAltTextGenerated'))\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n <div style={{ display: 'flex', gap: '20px', marginTop: '10px' }}>\n <div style={{ color: 'var(--theme-elevation-400)', flex: '1' }}>\n <p>{t('@jhb.software/payload-alt-text-plugin:altTextDescription')}</p>\n <ol style={{ margin: '10px 0', paddingLeft: '20px' }}>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement1')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement2')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement3')}</li>\n </ol>\n </div>\n <div style={{ alignItems: 'center', display: 'flex' }}>\n <Button\n disabled={isPending || !id || isUnsupportedMimeType}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltText}\n tooltip={\n isUnsupportedMimeType\n ? t('@jhb.software/payload-alt-text-plugin:unsupportedMimeType', { mimeType })\n : !id\n ? t('@jhb.software/payload-alt-text-plugin:pleaseSaveDocumentFirst')\n : undefined\n }\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltText')}\n </Button>\n </div>\n </div>\n )\n}\n"],"names":["Button","toast","useConfig","useDocumentInfo","useField","useLocale","useTranslation","useTransition","Lightning","Spinner","GenerateAltTextButton","supportedMimeTypes","t","id","collectionSlug","docPermissions","locale","isPending","startTransition","config","routes","api","apiRoute","serverURL","setValue","setKeywords","path","setAltText","value","mimeType","isUnsupportedMimeType","includes","update","handleGenerateAltText","error","Error","response","fetch","body","JSON","stringify","collection","code","method","ok","errorMessage","errorData","json","console","data","altText","keywords","success","div","style","display","gap","marginTop","color","flex","p","ol","margin","paddingLeft","li","alignItems","disabled","icon","onClick","tooltip","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Button,\n toast,\n useConfig,\n useDocumentInfo,\n useField,\n useLocale,\n useTranslation,\n} from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function GenerateAltTextButton({ supportedMimeTypes }: { supportedMimeTypes?: string[] }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const { id, collectionSlug, docPermissions } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isUnsupportedMimeType =\n !!mimeType && !!supportedMimeTypes && !supportedMimeTypes.includes(mimeType)\n\n // Hide the generate button from users who cannot update the document — the\n // generated alt text would not be persistable by them anyway.\n if (!docPermissions?.update) {\n return null\n }\n\n const handleGenerateAltText = () => {\n if (!collectionSlug || !id) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:cannotGenerateMissingFields'))\n throw new Error('Missing required fields')\n }\n\n startTransition(async () => {\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate`, {\n body: JSON.stringify({\n id: id as string,\n collection: collectionSlug,\n locale: locale?.code ?? null, // sent null when localization is disabled\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n let errorMessage = t('@jhb.software/payload-alt-text-plugin:failedToGenerate')\n try {\n const errorData = (await response.json()) as { error: string }\n errorMessage = errorData.error\n } catch (error) {\n console.error('Error generating alt text:', error)\n }\n\n toast.error(errorMessage)\n return\n }\n\n const data = (await response.json()) as {\n altText: string\n keywords: string[]\n }\n\n if (data.altText && data.keywords) {\n setAltText(data.altText)\n setKeywords(data.keywords)\n toast.success(t('@jhb.software/payload-alt-text-plugin:altTextGeneratedSuccess'))\n } else {\n toast.error(t('@jhb.software/payload-alt-text-plugin:noAltTextGenerated'))\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n <div style={{ display: 'flex', gap: '20px', marginTop: '10px' }}>\n <div style={{ color: 'var(--theme-elevation-400)', flex: '1' }}>\n <p>{t('@jhb.software/payload-alt-text-plugin:altTextDescription')}</p>\n <ol style={{ margin: '10px 0', paddingLeft: '20px' }}>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement1')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement2')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement3')}</li>\n </ol>\n </div>\n <div style={{ alignItems: 'center', display: 'flex' }}>\n <Button\n disabled={isPending || !id || isUnsupportedMimeType}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltText}\n tooltip={\n isUnsupportedMimeType\n ? t('@jhb.software/payload-alt-text-plugin:unsupportedMimeType', { mimeType })\n : !id\n ? t('@jhb.software/payload-alt-text-plugin:pleaseSaveDocumentFirst')\n : undefined\n }\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltText')}\n </Button>\n </div>\n </div>\n )\n}\n"],"names":["Button","toast","useConfig","useDocumentInfo","useField","useLocale","useTranslation","useTransition","PLUGIN_SLUG","Lightning","Spinner","GenerateAltTextButton","supportedMimeTypes","t","id","collectionSlug","docPermissions","locale","isPending","startTransition","config","routes","api","apiRoute","serverURL","setValue","setKeywords","path","setAltText","value","mimeType","isUnsupportedMimeType","includes","update","handleGenerateAltText","error","Error","response","fetch","body","JSON","stringify","collection","code","method","ok","errorMessage","errorData","json","console","data","altText","keywords","success","div","style","display","gap","marginTop","color","flex","p","ol","margin","paddingLeft","li","alignItems","disabled","icon","onClick","tooltip","undefined"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Short plugin slug used as the prefix for the plugin's REST endpoints.
3
+ *
4
+ * Endpoints are served under `<routes.api>/<PLUGIN_SLUG>/...`, e.g.
5
+ * `/api/alt-text/generate` with the default API route.
6
+ */
7
+ export declare const PLUGIN_SLUG = "alt-text";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Short plugin slug used as the prefix for the plugin's REST endpoints.
3
+ *
4
+ * Endpoints are served under `<routes.api>/<PLUGIN_SLUG>/...`, e.g.
5
+ * `/api/alt-text/generate` with the default API route.
6
+ */ export const PLUGIN_SLUG = 'alt-text';
7
+
8
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Short plugin slug used as the prefix for the plugin's REST endpoints.\n *\n * Endpoints are served under `<routes.api>/<PLUGIN_SLUG>/...`, e.g.\n * `/api/alt-text/generate` with the default API route.\n */\nexport const PLUGIN_SLUG = 'alt-text'\n"],"names":["PLUGIN_SLUG"],"mappings":"AAAA;;;;;CAKC,GACD,OAAO,MAAMA,cAAc,WAAU"}
@@ -1,3 +1,4 @@
1
+ import { PLUGIN_SLUG } from '../constants.js';
1
2
  import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js';
2
3
  export const altTextHealthEndpoint = (access)=>async (req)=>{
3
4
  if (!await access({
@@ -16,7 +17,7 @@ export const altTextHealthEndpoint = (access)=>async (req)=>{
16
17
  req.payload.logger.error({
17
18
  err: error,
18
19
  msg: 'Failed to build alt text health response.',
19
- path: '/alt-text-plugin/health',
20
+ path: `/${PLUGIN_SLUG}/health`,
20
21
  plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG
21
22
  });
22
23
  return Response.json({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/altTextHealth.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js'\n\nexport const altTextHealthEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await getAltTextHealth(req)\n\n return Response.json(health)\n } catch (error) {\n req.payload.logger.error({\n err: error,\n msg: 'Failed to build alt text health response.',\n path: '/alt-text-plugin/health',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return Response.json({ error: 'Failed to compute alt text health' }, { status: 500 })\n }\n }\n"],"names":["ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealth","altTextHealthEndpoint","access","req","Response","json","error","status","health","payload","logger","err","msg","path","plugin"],"mappings":"AAIA,SAASA,2BAA2B,EAAEC,gBAAgB,QAAQ,gCAA+B;AAE7F,OAAO,MAAMC,wBACX,CAACC,SACD,OAAOC;QACL,IAAI,CAAE,MAAMD,OAAO;YAAEC;QAAI,IAAK;YAC5B,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,IAAI;YACF,MAAMC,SAAS,MAAMR,iBAAiBG;YAEtC,OAAOC,SAASC,IAAI,CAACG;QACvB,EAAE,OAAOF,OAAO;YACdH,IAAIM,OAAO,CAACC,MAAM,CAACJ,KAAK,CAAC;gBACvBK,KAAKL;gBACLM,KAAK;gBACLC,MAAM;gBACNC,QAAQf;YACV;YAEA,OAAOK,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAoC,GAAG;gBAAEC,QAAQ;YAAI;QACrF;IACF,EAAC"}
1
+ {"version":3,"sources":["../../src/endpoints/altTextHealth.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js'\n\nexport const altTextHealthEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await getAltTextHealth(req)\n\n return Response.json(health)\n } catch (error) {\n req.payload.logger.error({\n err: error,\n msg: 'Failed to build alt text health response.',\n path: `/${PLUGIN_SLUG}/health`,\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return Response.json({ error: 'Failed to compute alt text health' }, { status: 500 })\n }\n }\n"],"names":["PLUGIN_SLUG","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealth","altTextHealthEndpoint","access","req","Response","json","error","status","health","payload","logger","err","msg","path","plugin"],"mappings":"AAIA,SAASA,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,2BAA2B,EAAEC,gBAAgB,QAAQ,gCAA+B;AAE7F,OAAO,MAAMC,wBACX,CAACC,SACD,OAAOC;QACL,IAAI,CAAE,MAAMD,OAAO;YAAEC;QAAI,IAAK;YAC5B,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,IAAI;YACF,MAAMC,SAAS,MAAMR,iBAAiBG;YAEtC,OAAOC,SAASC,IAAI,CAACG;QACvB,EAAE,OAAOF,OAAO;YACdH,IAAIM,OAAO,CAACC,MAAM,CAACJ,KAAK,CAAC;gBACvBK,KAAKL;gBACLM,KAAK;gBACLC,MAAM,CAAC,CAAC,EAAEf,YAAY,OAAO,CAAC;gBAC9BgB,QAAQf;YACV;YAEA,OAAOK,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAoC,GAAG;gBAAEC,QAAQ;YAAI;QACrF;IACF,EAAC"}
package/dist/plugin.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { PLUGIN_SLUG } from './constants.js';
1
2
  import { altTextHealthEndpoint } from './endpoints/altTextHealth.js';
2
3
  import { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js';
3
4
  import { generateAltTextEndpoint } from './endpoints/generateAltText.js';
@@ -149,18 +150,18 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
149
150
  {
150
151
  handler: generateAltTextEndpoint(pluginConfig.access),
151
152
  method: 'post',
152
- path: '/alt-text-plugin/generate'
153
+ path: `/${PLUGIN_SLUG}/generate`
153
154
  },
154
155
  {
155
156
  handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),
156
157
  method: 'post',
157
- path: '/alt-text-plugin/generate/bulk'
158
+ path: `/${PLUGIN_SLUG}/generate/bulk`
158
159
  },
159
160
  ...enableHealthCheck ? [
160
161
  {
161
162
  handler: altTextHealthEndpoint(pluginConfig.healthCheckAccess),
162
163
  method: 'get',
163
- path: '/alt-text-plugin/health'
164
+ path: `/${PLUGIN_SLUG}/health`
164
165
  }
165
166
  ] : []
166
167
  ],
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Widget } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { normalizeCollectionsConfig } from './utilities/mimeTypes.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections)\n\n const access = incomingPluginConfig.access ?? (({ req }) => !!req.user)\n\n // A function form of `healthCheck` doubles as the health report's access\n // gate; otherwise it falls back to the shared `access`.\n const healthCheckAccess =\n typeof incomingPluginConfig.healthCheck === 'function'\n ? incomingPluginConfig.healthCheck\n : access\n\n const pluginConfig: AltTextPluginConfig = {\n access,\n collections: normalizedCollections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n healthCheckAccess,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n maxBulkGenerateIds: incomingPluginConfig.maxBulkGenerateIds ?? 100,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const collectionConfigBySlug = new Map<string, (typeof normalizedCollections)[number]>(\n normalizedCollections.map((entry) => [entry.slug, entry]),\n )\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug)\n\n if (altTextCollectionConfig) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n trackedMimeTypes: altTextCollectionConfig.mimeTypes,\n validate: altTextCollectionConfig.validate,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate',\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate/bulk',\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.healthCheckAccess),\n method: 'get' as const,\n path: '/alt-text-plugin/health',\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n }\n }\n"],"names":["altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","normalizeCollectionsConfig","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","normalizedCollections","collections","access","req","user","healthCheckAccess","pluginConfig","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","maxBulkGenerateIds","resolver","length","Error","collectionConfigBySlug","Map","entry","collectionConfig","altTextCollectionConfig","get","upload","console","warn","defaultFields","localized","Boolean","supportedMimeTypes","trackedMimeTypes","mimeTypes","validate","fields","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","some","widget","custom","altTextPluginConfig","endpoints","handler","method","i18n"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Widget } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { PLUGIN_SLUG } from './constants.js'\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { normalizeCollectionsConfig } from './utilities/mimeTypes.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections)\n\n const access = incomingPluginConfig.access ?? (({ req }) => !!req.user)\n\n // A function form of `healthCheck` doubles as the health report's access\n // gate; otherwise it falls back to the shared `access`.\n const healthCheckAccess =\n typeof incomingPluginConfig.healthCheck === 'function'\n ? incomingPluginConfig.healthCheck\n : access\n\n const pluginConfig: AltTextPluginConfig = {\n access,\n collections: normalizedCollections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n healthCheckAccess,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n maxBulkGenerateIds: incomingPluginConfig.maxBulkGenerateIds ?? 100,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const collectionConfigBySlug = new Map<string, (typeof normalizedCollections)[number]>(\n normalizedCollections.map((entry) => [entry.slug, entry]),\n )\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug)\n\n if (altTextCollectionConfig) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n trackedMimeTypes: altTextCollectionConfig.mimeTypes,\n validate: altTextCollectionConfig.validate,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate`,\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate/bulk`,\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.healthCheckAccess),\n method: 'get' as const,\n path: `/${PLUGIN_SLUG}/health`,\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n }\n }\n"],"names":["PLUGIN_SLUG","altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","normalizeCollectionsConfig","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","normalizedCollections","collections","access","req","user","healthCheckAccess","pluginConfig","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","maxBulkGenerateIds","resolver","length","Error","collectionConfigBySlug","Map","entry","collectionConfig","altTextCollectionConfig","get","upload","console","warn","defaultFields","localized","Boolean","supportedMimeTypes","trackedMimeTypes","mimeTypes","validate","fields","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","some","widget","custom","altTextPluginConfig","endpoints","handler","method","i18n"],"mappings":"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"}
@@ -42,10 +42,14 @@ import { z } from 'zod';
42
42
  * ```
43
43
  */ export const openAIResolver = (config)=>{
44
44
  const { apiKey, baseUrl, model = 'gpt-4.1-nano' } = config;
45
- const openai = new OpenAI({
46
- apiKey,
47
- baseURL: baseUrl
48
- });
45
+ // Build the client lazily (once, on first use): the `resolver` argument is
46
+ // evaluated even when the plugin is disabled, so eager construction would
47
+ // throw on a keyless `enabled: !!process.env.OPENAI_API_KEY` setup.
48
+ let openai;
49
+ const getClient = ()=>openai ??= new OpenAI({
50
+ apiKey,
51
+ baseURL: baseUrl
52
+ });
49
53
  return {
50
54
  key: 'openai',
51
55
  resolve: async ({ filename, imageThumbnailUrl, locale })=>{
@@ -54,7 +58,7 @@ import { z } from 'zod';
54
58
  altText: z.string().describe('A concise, descriptive alt text for the image'),
55
59
  keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
56
60
  });
57
- const response = await openai.chat.completions.parse({
61
+ const response = await getClient().chat.completions.parse({
58
62
  max_completion_tokens: 150,
59
63
  messages: [
60
64
  {
@@ -120,7 +124,7 @@ import { z } from 'zod';
120
124
  keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
121
125
  })
122
126
  ])));
123
- const response = await openai.chat.completions.parse({
127
+ const response = await getClient().chat.completions.parse({
124
128
  max_completion_tokens: 300,
125
129
  messages: [
126
130
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/resolvers/openAI.ts"],"sourcesContent":["import type { AutoParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport type { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport type { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs'\n\nimport OpenAI from 'openai'\nimport { makeParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport { z } from 'zod'\n\nimport type {\n AltTextBulkResolverArgs,\n AltTextBulkResolverResponse,\n AltTextResolver,\n AltTextResolverArgs,\n AltTextResolverResponse,\n} from './types.js'\n\nexport type OpenAIResolverConfig = {\n /** OpenAI API key for authentication */\n apiKey: string\n /**\n * Base URL for the OpenAI-compatible API.\n * Use this to point at alternative providers (e.g. Azure, Nebius, local inference).\n * @default undefined — the OpenAI SDK defaults to 'https://api.openai.com/v1'\n */\n baseUrl?: string\n /**\n * The OpenAI LLM model to use for alt text generation.\n * @default 'gpt-4.1-nano'\n */\n model?: string\n}\n\n/**\n * Creates a chat completion `JSONSchema` response format object from\n * the given Zod schema.\n *\n * This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts\n * because of issue https://github.com/openai/openai-node/issues/1576\n */\nfunction zodResponseFormat<ZodInput extends z.ZodType>(\n zodObject: ZodInput,\n name: string,\n props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'name' | 'schema' | 'strict'>,\n): AutoParseableResponseFormat<z.infer<ZodInput>> {\n return makeParseableResponseFormat(\n {\n type: 'json_schema',\n json_schema: {\n ...props,\n name,\n schema: z.toJSONSchema(zodObject, { target: 'draft-7' }),\n strict: true,\n },\n },\n (content) => zodObject.parse(JSON.parse(content)),\n )\n}\n\n/**\n * Creates an OpenAI-based resolver for alt text generation.\n *\n * @example\n * ```typescript\n * import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * // OpenAI\n * openAIResolver({\n * apiKey: process.env.OPENAI_API_KEY,\n * model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'\n * })\n *\n * // OpenAI-compatible provider (e.g. Nebius)\n * openAIResolver({\n * apiKey: process.env.NEBIUS_API_KEY,\n * baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',\n * model: 'Qwen/Qwen2.5-VL-72B-Instruct',\n * })\n * ```\n */\nexport const openAIResolver = (config: OpenAIResolverConfig): AltTextResolver => {\n const { apiKey, baseUrl, model = 'gpt-4.1-nano' } = config\n const openai = new OpenAI({ apiKey, baseURL: baseUrl })\n\n return {\n key: 'openai',\n resolve: async ({\n filename,\n imageThumbnailUrl,\n locale,\n }: AltTextResolverArgs): Promise<AltTextResolverResponse> => {\n try {\n const modelResponseSchema = z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z.array(z.string()).describe('Keywords that describe the content of the image'),\n })\n\n const response = await openai.chat.completions.parse({\n max_completion_tokens: 150,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following:\n - A concise, descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object. You must respond in the ${locale} language.\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n resolveBulk: async ({\n filename,\n imageThumbnailUrl,\n locales,\n }: AltTextBulkResolverArgs): Promise<AltTextBulkResolverResponse> => {\n try {\n const modelResponseSchema = z.object(\n Object.fromEntries(\n locales.map((locale) => [\n locale,\n z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z\n .array(z.string())\n .describe('Keywords that describe the content of the image'),\n }),\n ]),\n ),\n )\n\n const response = await openai.chat.completions.parse({\n max_completion_tokens: 300,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following in ${locales.join(', ')}:\n - A concise, localized descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A localized list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object with ${locales.join(', ')} keys, each containing \"altText\" and \"keywords\".\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n results: result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating bulk alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n // https://platform.openai.com/docs/guides/images-vision\n supportedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],\n }\n}\n"],"names":["OpenAI","makeParseableResponseFormat","z","zodResponseFormat","zodObject","name","props","type","json_schema","schema","toJSONSchema","target","strict","content","parse","JSON","openAIResolver","config","apiKey","baseUrl","model","openai","baseURL","key","resolve","filename","imageThumbnailUrl","locale","modelResponseSchema","object","altText","string","describe","keywords","array","response","chat","completions","max_completion_tokens","messages","role","image_url","url","text","response_format","result","choices","message","parsed","error","success","console","Error","resolveBulk","locales","Object","fromEntries","map","join","results","supportedMimeTypes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/resolvers/openAI.ts"],"sourcesContent":["import type { AutoParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport type { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport type { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs'\n\nimport OpenAI from 'openai'\nimport { makeParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport { z } from 'zod'\n\nimport type {\n AltTextBulkResolverArgs,\n AltTextBulkResolverResponse,\n AltTextResolver,\n AltTextResolverArgs,\n AltTextResolverResponse,\n} from './types.js'\n\nexport type OpenAIResolverConfig = {\n /** OpenAI API key for authentication */\n apiKey: string\n /**\n * Base URL for the OpenAI-compatible API.\n * Use this to point at alternative providers (e.g. Azure, Nebius, local inference).\n * @default undefined — the OpenAI SDK defaults to 'https://api.openai.com/v1'\n */\n baseUrl?: string\n /**\n * The OpenAI LLM model to use for alt text generation.\n * @default 'gpt-4.1-nano'\n */\n model?: string\n}\n\n/**\n * Creates a chat completion `JSONSchema` response format object from\n * the given Zod schema.\n *\n * This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts\n * because of issue https://github.com/openai/openai-node/issues/1576\n */\nfunction zodResponseFormat<ZodInput extends z.ZodType>(\n zodObject: ZodInput,\n name: string,\n props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'name' | 'schema' | 'strict'>,\n): AutoParseableResponseFormat<z.infer<ZodInput>> {\n return makeParseableResponseFormat(\n {\n type: 'json_schema',\n json_schema: {\n ...props,\n name,\n schema: z.toJSONSchema(zodObject, { target: 'draft-7' }),\n strict: true,\n },\n },\n (content) => zodObject.parse(JSON.parse(content)),\n )\n}\n\n/**\n * Creates an OpenAI-based resolver for alt text generation.\n *\n * @example\n * ```typescript\n * import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * // OpenAI\n * openAIResolver({\n * apiKey: process.env.OPENAI_API_KEY,\n * model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'\n * })\n *\n * // OpenAI-compatible provider (e.g. Nebius)\n * openAIResolver({\n * apiKey: process.env.NEBIUS_API_KEY,\n * baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',\n * model: 'Qwen/Qwen2.5-VL-72B-Instruct',\n * })\n * ```\n */\nexport const openAIResolver = (config: OpenAIResolverConfig): AltTextResolver => {\n const { apiKey, baseUrl, model = 'gpt-4.1-nano' } = config\n\n // Build the client lazily (once, on first use): the `resolver` argument is\n // evaluated even when the plugin is disabled, so eager construction would\n // throw on a keyless `enabled: !!process.env.OPENAI_API_KEY` setup.\n let openai: OpenAI | undefined\n const getClient = (): OpenAI => (openai ??= new OpenAI({ apiKey, baseURL: baseUrl }))\n\n return {\n key: 'openai',\n resolve: async ({\n filename,\n imageThumbnailUrl,\n locale,\n }: AltTextResolverArgs): Promise<AltTextResolverResponse> => {\n try {\n const modelResponseSchema = z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z.array(z.string()).describe('Keywords that describe the content of the image'),\n })\n\n const response = await getClient().chat.completions.parse({\n max_completion_tokens: 150,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following:\n - A concise, descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object. You must respond in the ${locale} language.\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n resolveBulk: async ({\n filename,\n imageThumbnailUrl,\n locales,\n }: AltTextBulkResolverArgs): Promise<AltTextBulkResolverResponse> => {\n try {\n const modelResponseSchema = z.object(\n Object.fromEntries(\n locales.map((locale) => [\n locale,\n z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z\n .array(z.string())\n .describe('Keywords that describe the content of the image'),\n }),\n ]),\n ),\n )\n\n const response = await getClient().chat.completions.parse({\n max_completion_tokens: 300,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following in ${locales.join(', ')}:\n - A concise, localized descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A localized list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object with ${locales.join(', ')} keys, each containing \"altText\" and \"keywords\".\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n results: result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating bulk alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n // https://platform.openai.com/docs/guides/images-vision\n supportedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],\n }\n}\n"],"names":["OpenAI","makeParseableResponseFormat","z","zodResponseFormat","zodObject","name","props","type","json_schema","schema","toJSONSchema","target","strict","content","parse","JSON","openAIResolver","config","apiKey","baseUrl","model","openai","getClient","baseURL","key","resolve","filename","imageThumbnailUrl","locale","modelResponseSchema","object","altText","string","describe","keywords","array","response","chat","completions","max_completion_tokens","messages","role","image_url","url","text","response_format","result","choices","message","parsed","error","success","console","Error","resolveBulk","locales","Object","fromEntries","map","join","results","supportedMimeTypes"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhb.software/payload-alt-text-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A Payload CMS plugin that adds AI-powered alt text generation for images.",
5
5
  "bugs": "https://github.com/jhb-software/payload-plugins/issues",
6
6
  "repository": {
@@ -20,23 +20,23 @@
20
20
  "main": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "dependencies": {
23
- "openai": "^6.44.0",
24
- "p-map": "^7.0.4",
23
+ "openai": "^6.46.0",
24
+ "p-map": "^7.0.5",
25
25
  "zod": "^4.4.3"
26
26
  },
27
27
  "peerDependencies": {
28
- "@payloadcms/translations": "^3.85.1",
29
- "@payloadcms/ui": "^3.85.1",
28
+ "@payloadcms/translations": "^3.86.0",
29
+ "@payloadcms/ui": "^3.86.0",
30
30
  "next": "^15.0.0 || ^16.0.0",
31
- "payload": "^3.85.1",
31
+ "payload": "^3.86.0",
32
32
  "react": "19.2.7",
33
33
  "react-dom": "19.2.7"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@payloadcms/eslint-config": "^3.28.0",
37
- "@payloadcms/translations": "^3.85.1",
37
+ "@payloadcms/translations": "^3.86.0",
38
38
  "@swc/cli": "^0.8.1",
39
- "@swc/core": "^1.15.41",
39
+ "@swc/core": "^1.15.43",
40
40
  "@types/react": "19.2.17",
41
41
  "@types/react-dom": "19.2.3",
42
42
  "copyfiles": "2.4.1",
@@ -44,7 +44,7 @@
44
44
  "prettier": "^3.8.4",
45
45
  "rimraf": "6.1.3",
46
46
  "typescript": "^6.0.3",
47
- "vitest": "^4.1.9"
47
+ "vitest": "^4.1.10"
48
48
  },
49
49
  "files": [
50
50
  "dist"