react-a11y-auto-caption 1.1.2 → 1.1.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/dist/index.d.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  import React, { ImgHTMLAttributes } from "react";
2
- export declare const SmartImageProvider: React.FC<{
2
+ type smartImageProviderProps = {
3
3
  value: {
4
4
  apiEndpoint?: string;
5
5
  disableAI?: boolean;
6
6
  };
7
7
  children: React.ReactNode;
8
- }>;
8
+ };
9
+ export declare const SmartImageProvider: ({ value, children }: smartImageProviderProps) => import("react/jsx-runtime").JSX.Element;
10
+ export * from './useAICaption';
9
11
  export interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {
10
12
  src?: string;
11
13
  apiEndpoint?: string;
12
14
  fallbackAlt?: string;
13
15
  onCaptionGenerated?: (caption: string) => void;
16
+ onCaptionError?: (error: Error) => void;
14
17
  disableAI?: boolean;
15
18
  announceLive?: boolean;
16
- onCaptionError?: (error: Error) => void;
19
+ lazyGenerate?: boolean;
17
20
  }
18
- export declare const SmartImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, disableAI: propsDisableAI, announceLive, onCaptionError, ...props }: SmartImageProps) => import("react/jsx-runtime").JSX.Element;
21
+ export declare const SmartImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, onCaptionError, disableAI: propsDisableAI, announceLive, lazyGenerate, ...props }: SmartImageProps) => import("react/jsx-runtime").JSX.Element;
package/dist/index.js CHANGED
@@ -21,7 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  SmartImage: () => SmartImage,
24
- SmartImageProvider: () => SmartImageProvider
24
+ SmartImageContext: () => SmartImageContext,
25
+ SmartImageProvider: () => SmartImageProvider,
26
+ useAICaptions: () => useAICaptions
25
27
  });
26
28
  module.exports = __toCommonJS(index_exports);
27
29
 
@@ -34,9 +36,16 @@ var LRUCaptionCache = class {
34
36
  this.cache = /* @__PURE__ */ new Map();
35
37
  }
36
38
  get(key) {
37
- return this.cache.get(key);
39
+ if (!this.cache.has(key)) return void 0;
40
+ const value = this.cache.get(key);
41
+ this.cache.delete(key);
42
+ this.cache.set(key, value);
43
+ return value;
38
44
  }
39
45
  set(key, value) {
46
+ if (this.cache.has(key)) {
47
+ this.cache.delete(key);
48
+ }
40
49
  if (this.cache.size >= MAX_SIZE) {
41
50
  this.cache.delete(this.cache.keys().next().value);
42
51
  }
@@ -68,7 +77,9 @@ var useAICaptions = ({
68
77
  fallbackAlt = "Image loading or caption unavailable",
69
78
  onCaptionGenerated,
70
79
  disableAI: propsDisableAI,
71
- onCaptionError
80
+ onCaptionError,
81
+ announceLive = false,
82
+ lazyGenerate = true
72
83
  }) => {
73
84
  const context = (0, import_react.useContext)(SmartImageContext);
74
85
  const apiEndpoint = propsEndpoint || context?.apiEndpoint;
@@ -76,19 +87,41 @@ var useAICaptions = ({
76
87
  const [generatedAlt, setGeneratedAlt] = (0, import_react.useState)("");
77
88
  const [isGenerating, setIsGenerating] = (0, import_react.useState)(false);
78
89
  const [error, setError] = (0, import_react.useState)(null);
90
+ const [announcement, setAnnouncemet] = (0, import_react.useState)("");
91
+ const [shouldGenerate, setShouldGenerate] = (0, import_react.useState)(!lazyGenerate);
79
92
  const onCaptionGeneratedRef = (0, import_react.useRef)(onCaptionGenerated);
80
93
  const onCaptionErrorRef = (0, import_react.useRef)(onCaptionError);
94
+ const imgRef = (0, import_react.useRef)(null);
81
95
  (0, import_react.useEffect)(() => {
82
96
  onCaptionGeneratedRef.current = onCaptionGenerated;
83
97
  onCaptionErrorRef.current = onCaptionError;
84
98
  });
99
+ (0, import_react.useEffect)(() => {
100
+ if (!announcement) return;
101
+ const timer = setTimeout(() => setAnnouncemet(""), 5e3);
102
+ return () => clearTimeout(timer);
103
+ }, [announcement]);
104
+ (0, import_react.useEffect)(() => {
105
+ if (!lazyGenerate || !imgRef.current) return;
106
+ const observer = new IntersectionObserver(
107
+ ([entry]) => {
108
+ if (entry.isIntersecting) {
109
+ setShouldGenerate(true);
110
+ observer.disconnect();
111
+ }
112
+ },
113
+ { rootMargin: "200px" }
114
+ );
115
+ observer.observe(imgRef.current);
116
+ return () => observer.disconnect();
117
+ }, [lazyGenerate]);
85
118
  (0, import_react.useEffect)(() => {
86
119
  setError(null);
87
120
  if (alt) {
88
121
  setGeneratedAlt(alt);
89
122
  return;
90
123
  }
91
- if (!src) return;
124
+ if (!src || !shouldGenerate) return;
92
125
  if (disableAI) {
93
126
  setGeneratedAlt("[Testing mode: AI caption generation disabled]");
94
127
  return;
@@ -160,7 +193,7 @@ var useAICaptions = ({
160
193
  cancelled = true;
161
194
  };
162
195
  }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
163
- return { generatedAlt, isGenerating, error };
196
+ return { generatedAlt, isGenerating, error, imgRef, announcement };
164
197
  };
165
198
 
166
199
  // src/index.tsx
@@ -185,12 +218,13 @@ var SmartImage = ({
185
218
  apiEndpoint: propsEndpoint,
186
219
  fallbackAlt = "Image loading or caption unavailable",
187
220
  onCaptionGenerated,
221
+ onCaptionError,
188
222
  disableAI: propsDisableAI,
189
223
  announceLive = false,
190
- onCaptionError,
224
+ lazyGenerate = true,
191
225
  ...props
192
226
  }) => {
193
- const { isGenerating, generatedAlt } = useAICaptions({
227
+ const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({
194
228
  src,
195
229
  alt,
196
230
  apiEndpoint: propsEndpoint,
@@ -198,16 +232,18 @@ var SmartImage = ({
198
232
  onCaptionGenerated,
199
233
  disableAI: propsDisableAI,
200
234
  announceLive,
201
- onCaptionError
235
+ lazyGenerate
202
236
  });
203
237
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
204
- announceLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: isGenerating ? "Generating image description. Please wait..." : generatedAlt ? `Image description generated: ${generatedAlt}` : "" }),
205
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, alt: generatedAlt, "aria-busy": isGenerating, ...props })
238
+ announceLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: announcement }),
239
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, ref: imgRef, alt: generatedAlt, "aria-busy": isGenerating, ...props })
206
240
  ] });
207
241
  };
208
242
  // Annotate the CommonJS export names for ESM import in node:
209
243
  0 && (module.exports = {
210
244
  SmartImage,
211
- SmartImageProvider
245
+ SmartImageContext,
246
+ SmartImageProvider,
247
+ useAICaptions
212
248
  });
213
249
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx","../src/useAICaption.ts"],"sourcesContent":["import React, { ImgHTMLAttributes } from \"react\";\r\nimport { SmartImageContext, useAICaptions } from \"./useAICaption\";\r\n\r\nexport const SmartImageProvider: React.FC<{\r\n value: { apiEndpoint?: string; disableAI?: boolean };\r\n children: React.ReactNode;\r\n}> = ({ value, children }) => {\r\n return <SmartImageContext.Provider value={value}>{children}</SmartImageContext.Provider>;\r\n};\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n src?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\nconst SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\n\r\nexport const SmartImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n onCaptionError,\r\n ...props\r\n}: SmartImageProps) => {\r\n const { isGenerating, generatedAlt } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n onCaptionError,\r\n });\r\n\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {isGenerating\r\n ? \"Generating image description. Please wait...\"\r\n : generatedAlt\r\n ? `Image description generated: ${generatedAlt}`\r\n : \"\"}\r\n </span>\r\n )}\r\n <img src={src} alt={generatedAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, ImgHTMLAttributes, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n return this.cache.get(key);\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error };\r\n};\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.tsx","../src/useAICaption.ts"],"sourcesContent":["import React, { ImgHTMLAttributes } from \"react\";\r\nimport { SmartImageContext, useAICaptions } from \"./useAICaption\";\r\n\r\ntype smartImageProviderProps = {\r\n value: { apiEndpoint?: string; disableAI?: boolean };\r\n children: React.ReactNode;\r\n};\r\nexport const SmartImageProvider = ({ value, children }: smartImageProviderProps) => {\r\n return <SmartImageContext.Provider value={value}>{children}</SmartImageContext.Provider>;\r\n};\r\nexport * from './useAICaption';\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n src?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\nconst SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\n\r\nexport const SmartImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n ...props\r\n}: SmartImageProps) => {\r\n const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n lazyGenerate,\r\n });\r\n\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {announcement}\r\n </span>\r\n )}\r\n <img src={src} ref={imgRef} alt={generatedAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n if (!this.cache.has(key)) return undefined;\r\n\r\n const value = this.cache.get(key)!;\r\n this.cache.delete(key);\r\n this.cache.set(key, value);\r\n return value;\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.has(key)) {\r\n this.cache.delete(key);\r\n }\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n const [announcement, setAnnouncemet] = useState(\"\");\r\n const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n const imgRef = useRef < HTMLImageElement | null>(null);\r\n\r\n \r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n if (!announcement) return;\r\n const timer = setTimeout(() => setAnnouncemet(\"\"), 5000);\r\n return () => clearTimeout(timer)\r\n }, [announcement])\r\n \r\n useEffect(() => {\r\n if (!lazyGenerate || !imgRef.current) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setShouldGenerate(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { rootMargin: \"200px\" }\r\n );\r\n observer.observe(imgRef.current);\r\n return () => observer.disconnect();\r\n }, [lazyGenerate])\r\n\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src || !shouldGenerate) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -7,9 +7,16 @@ var LRUCaptionCache = class {
7
7
  this.cache = /* @__PURE__ */ new Map();
8
8
  }
9
9
  get(key) {
10
- return this.cache.get(key);
10
+ if (!this.cache.has(key)) return void 0;
11
+ const value = this.cache.get(key);
12
+ this.cache.delete(key);
13
+ this.cache.set(key, value);
14
+ return value;
11
15
  }
12
16
  set(key, value) {
17
+ if (this.cache.has(key)) {
18
+ this.cache.delete(key);
19
+ }
13
20
  if (this.cache.size >= MAX_SIZE) {
14
21
  this.cache.delete(this.cache.keys().next().value);
15
22
  }
@@ -41,7 +48,9 @@ var useAICaptions = ({
41
48
  fallbackAlt = "Image loading or caption unavailable",
42
49
  onCaptionGenerated,
43
50
  disableAI: propsDisableAI,
44
- onCaptionError
51
+ onCaptionError,
52
+ announceLive = false,
53
+ lazyGenerate = true
45
54
  }) => {
46
55
  const context = useContext(SmartImageContext);
47
56
  const apiEndpoint = propsEndpoint || context?.apiEndpoint;
@@ -49,19 +58,41 @@ var useAICaptions = ({
49
58
  const [generatedAlt, setGeneratedAlt] = useState("");
50
59
  const [isGenerating, setIsGenerating] = useState(false);
51
60
  const [error, setError] = useState(null);
61
+ const [announcement, setAnnouncemet] = useState("");
62
+ const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);
52
63
  const onCaptionGeneratedRef = useRef(onCaptionGenerated);
53
64
  const onCaptionErrorRef = useRef(onCaptionError);
65
+ const imgRef = useRef(null);
54
66
  useEffect(() => {
55
67
  onCaptionGeneratedRef.current = onCaptionGenerated;
56
68
  onCaptionErrorRef.current = onCaptionError;
57
69
  });
70
+ useEffect(() => {
71
+ if (!announcement) return;
72
+ const timer = setTimeout(() => setAnnouncemet(""), 5e3);
73
+ return () => clearTimeout(timer);
74
+ }, [announcement]);
75
+ useEffect(() => {
76
+ if (!lazyGenerate || !imgRef.current) return;
77
+ const observer = new IntersectionObserver(
78
+ ([entry]) => {
79
+ if (entry.isIntersecting) {
80
+ setShouldGenerate(true);
81
+ observer.disconnect();
82
+ }
83
+ },
84
+ { rootMargin: "200px" }
85
+ );
86
+ observer.observe(imgRef.current);
87
+ return () => observer.disconnect();
88
+ }, [lazyGenerate]);
58
89
  useEffect(() => {
59
90
  setError(null);
60
91
  if (alt) {
61
92
  setGeneratedAlt(alt);
62
93
  return;
63
94
  }
64
- if (!src) return;
95
+ if (!src || !shouldGenerate) return;
65
96
  if (disableAI) {
66
97
  setGeneratedAlt("[Testing mode: AI caption generation disabled]");
67
98
  return;
@@ -133,7 +164,7 @@ var useAICaptions = ({
133
164
  cancelled = true;
134
165
  };
135
166
  }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
136
- return { generatedAlt, isGenerating, error };
167
+ return { generatedAlt, isGenerating, error, imgRef, announcement };
137
168
  };
138
169
 
139
170
  // src/index.tsx
@@ -158,12 +189,13 @@ var SmartImage = ({
158
189
  apiEndpoint: propsEndpoint,
159
190
  fallbackAlt = "Image loading or caption unavailable",
160
191
  onCaptionGenerated,
192
+ onCaptionError,
161
193
  disableAI: propsDisableAI,
162
194
  announceLive = false,
163
- onCaptionError,
195
+ lazyGenerate = true,
164
196
  ...props
165
197
  }) => {
166
- const { isGenerating, generatedAlt } = useAICaptions({
198
+ const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({
167
199
  src,
168
200
  alt,
169
201
  apiEndpoint: propsEndpoint,
@@ -171,15 +203,17 @@ var SmartImage = ({
171
203
  onCaptionGenerated,
172
204
  disableAI: propsDisableAI,
173
205
  announceLive,
174
- onCaptionError
206
+ lazyGenerate
175
207
  });
176
208
  return /* @__PURE__ */ jsxs(Fragment, { children: [
177
- announceLive && /* @__PURE__ */ jsx("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: isGenerating ? "Generating image description. Please wait..." : generatedAlt ? `Image description generated: ${generatedAlt}` : "" }),
178
- /* @__PURE__ */ jsx("img", { src, alt: generatedAlt, "aria-busy": isGenerating, ...props })
209
+ announceLive && /* @__PURE__ */ jsx("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: announcement }),
210
+ /* @__PURE__ */ jsx("img", { src, ref: imgRef, alt: generatedAlt, "aria-busy": isGenerating, ...props })
179
211
  ] });
180
212
  };
181
213
  export {
182
214
  SmartImage,
183
- SmartImageProvider
215
+ SmartImageContext,
216
+ SmartImageProvider,
217
+ useAICaptions
184
218
  };
185
219
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useAICaption.ts","../src/index.tsx"],"sourcesContent":["import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, ImgHTMLAttributes, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n return this.cache.get(key);\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error };\r\n};\r\n","import React, { ImgHTMLAttributes } from \"react\";\r\nimport { SmartImageContext, useAICaptions } from \"./useAICaption\";\r\n\r\nexport const SmartImageProvider: React.FC<{\r\n value: { apiEndpoint?: string; disableAI?: boolean };\r\n children: React.ReactNode;\r\n}> = ({ value, children }) => {\r\n return <SmartImageContext.Provider value={value}>{children}</SmartImageContext.Provider>;\r\n};\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n src?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\nconst SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\n\r\nexport const SmartImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n onCaptionError,\r\n ...props\r\n}: SmartImageProps) => {\r\n const { isGenerating, generatedAlt } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n onCaptionError,\r\n });\r\n\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {isGenerating\r\n ? \"Generating image description. Please wait...\"\r\n : generatedAlt\r\n ? `Image description generated: ${generatedAlt}`\r\n : \"\"}\r\n </span>\r\n )}\r\n <img src={src} alt={generatedAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n"],"mappings":";AAEA,SAAS,eAAkC,YAAY,WAAW,QAAQ,gBAAgB;AAkBnF,IAAM,oBAAoB,cAAkD,MAAS;AAE5F,IAAM,WAAW;AAEjB,IAAM,kBAAN,MAAsB;AAAA,EAAtB;AACE,SAAQ,QAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,IAAI,KAAiC;AACnC,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EACA,IAAI,KAAa,OAAe;AAC9B,QAAI,KAAK,MAAM,QAAQ,UAAU;AAC/B,WAAK,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IACnD;AACA,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AAAA,EACA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EACA,QAAQ;AACN,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;AAEA,IAAM,eAAe,IAAI,gBAAgB;AACzC,IAAM,sBAAsB,oBAAI,IAA6B;AAU7D,SAAS,kBAAkB,KAAoD;AAC7E,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,SAAS;AAC7D;AAEA,SAAS,gBAAgB,KAAoC;AAC3D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,kBAAkB,GAAG,EAAG,QAAO,IAAI;AACvC,SAAO,IAAI,QAAQ;AACrB;AAEA,IAAM,MAAM,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,MAAM,MAAM;AAAC;AAEnE,IAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AAAA,EACX;AACF,MAA2B;AACzB,QAAM,UAAU,WAAW,iBAAiB;AAE5C,QAAM,cAAc,iBAAiB,SAAS;AAC9C,QAAM,YAAY,kBAAkB,SAAS,aAAa;AAE1D,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,EAAE;AACnD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAErD,QAAM,wBAAwB,OAAO,kBAAkB;AACvD,QAAM,oBAAoB,OAAO,cAAc;AAE/C,YAAU,MAAM;AACd,0BAAsB,UAAU;AAChC,sBAAkB,UAAU;AAAA,EAC9B,CAAC;AAED,YAAU,MAAM;AACd,aAAS,IAAI;AAEb,QAAI,KAAK;AACP,sBAAgB,GAAG;AACnB;AAAA,IACF;AAEA,QAAI,CAAC,IAAK;AAEV,QAAI,WAAW;AACb,sBAAgB,gDAAgD;AAChE;AAAA,IACF;AAEA,QAAI,CAAC,aAAa;AAChB,cAAQ;AAAA,QACN;AAAA,MACF;AACA,sBAAgB,WAAW;AAC3B;AAAA,IACF;AAEA,UAAM,WAAW,gBAAgB,GAAG;AAEpC,QAAI,aAAa,IAAI,QAAQ,GAAG;AAC9B,UAAI,mDAAmD;AACvD,YAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,sBAAgB,aAAa;AAC7B,UAAI,sBAAsB,QAAS,uBAAsB,QAAQ,aAAa;AAC9E;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,UAAM,kBAAkB,YAAY;AAClC,sBAAgB,IAAI;AACpB,UAAI;AACF,YAAI,oBAAoB,IAAI,QAAQ,GAAG;AACrC,cAAI,uFAAuF;AAC3F,gBAAM,UAAU,MAAM,oBAAoB,IAAI,QAAQ;AACtD,cAAI,UAAW;AACf,0BAAgB,OAAO;AACvB,cAAI,sBAAsB,QAAS,uBAAsB,QAAQ,OAAO;AACxE;AAAA,QACF;AAEA,cAAM,gBAAgB,YAAY;AAChC,gBAAM,gBAAgB,MAAM,MAAM,QAAQ;AAC1C,gBAAM,YAAY,MAAM,cAAc,KAAK;AAC3C,gBAAM,YAAY,IAAI,KAAK,CAAC,SAAS,GAAG,aAAa;AAAA,YACnD,MAAM,UAAU,QAAQ;AAAA,UAC1B,CAAC;AACD,gBAAM,WAAW,IAAI,SAAS;AAC9B,mBAAS,OAAO,QAAQ,SAAS;AAEjC,gBAAM,WAAW,MAAM,MAAM,aAAa;AAAA,YACxC,QAAQ;AAAA,YACR,MAAM;AAAA,UACR,CAAC;AAED,cAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,uBAAuB;AACzD,gBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,gBAAM,IAAI,MAAM,mCAAmC;AAAA,QACrD,GAAG;AAEH,4BAAoB,IAAI,UAAU,YAAY;AAE9C,cAAM,aAAa,MAAM;AACzB,4BAAoB,OAAO,QAAQ;AACnC,qBAAa,IAAI,UAAU,UAAU;AAErC,YAAI,UAAW;AACf,wBAAgB,UAAU;AAC1B,YAAI,sBAAsB,QAAS,uBAAsB,QAAQ,UAAU;AAAA,MAC7E,SAAS,KAAK;AACZ,cAAM,kBAAkB,eAAe,QAAQ,MAAM,IAAI,MAAM,eAAe;AAC9E,4BAAoB,OAAO,QAAQ;AACnC,YAAI,UAAW;AACf,iBAAS,eAAe;AACxB,wBAAgB,WAAW;AAC3B,YAAI,kBAAkB,QAAS,mBAAkB,QAAQ,eAAe;AAAA,MAC1E,UAAE;AACA,YAAI,CAAC,UAAW,iBAAgB,KAAK;AAAA,MACvC;AAAA,IACF;AAEA,oBAAgB;AAEhB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,KAAK,KAAK,aAAa,aAAa,SAAS,CAAC;AAElD,SAAO,EAAE,cAAc,cAAc,MAAM;AAC7C;;;ACtLS,SAgDL,UAhDK,KAgDL,YAhDK;AAJF,IAAM,qBAGR,CAAC,EAAE,OAAO,SAAS,MAAM;AAC5B,SAAO,oBAAC,kBAAkB,UAAlB,EAA2B,OAAe,UAAS;AAC7D;AAYA,IAAM,gBAAqC;AAAA,EACzC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,aAAa;AACf;AAEO,IAAM,aAAa,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf;AAAA,EACA,GAAG;AACL,MAAuB;AACrB,QAAM,EAAE,cAAc,aAAa,IAAI,cAAc;AAAA,IACnD;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE,iCACG;AAAA,oBACC,oBAAC,UAAK,OAAO,eAAe,aAAU,UAAS,eAAY,QACxD,yBACG,iDACA,eACE,gCAAgC,YAAY,KAC5C,IACR;AAAA,IAEF,oBAAC,SAAI,KAAU,KAAK,cAAc,aAAW,cAAe,GAAG,OAAO;AAAA,KACxE;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/useAICaption.ts","../src/index.tsx"],"sourcesContent":["import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n if (!this.cache.has(key)) return undefined;\r\n\r\n const value = this.cache.get(key)!;\r\n this.cache.delete(key);\r\n this.cache.set(key, value);\r\n return value;\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.has(key)) {\r\n this.cache.delete(key);\r\n }\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n const [announcement, setAnnouncemet] = useState(\"\");\r\n const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n const imgRef = useRef < HTMLImageElement | null>(null);\r\n\r\n \r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n if (!announcement) return;\r\n const timer = setTimeout(() => setAnnouncemet(\"\"), 5000);\r\n return () => clearTimeout(timer)\r\n }, [announcement])\r\n \r\n useEffect(() => {\r\n if (!lazyGenerate || !imgRef.current) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setShouldGenerate(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { rootMargin: \"200px\" }\r\n );\r\n observer.observe(imgRef.current);\r\n return () => observer.disconnect();\r\n }, [lazyGenerate])\r\n\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src || !shouldGenerate) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n","import React, { ImgHTMLAttributes } from \"react\";\r\nimport { SmartImageContext, useAICaptions } from \"./useAICaption\";\r\n\r\ntype smartImageProviderProps = {\r\n value: { apiEndpoint?: string; disableAI?: boolean };\r\n children: React.ReactNode;\r\n};\r\nexport const SmartImageProvider = ({ value, children }: smartImageProviderProps) => {\r\n return <SmartImageContext.Provider value={value}>{children}</SmartImageContext.Provider>;\r\n};\r\nexport * from './useAICaption';\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n src?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\nconst SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\n\r\nexport const SmartImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n ...props\r\n}: SmartImageProps) => {\r\n const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n lazyGenerate,\r\n });\r\n\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {announcement}\r\n </span>\r\n )}\r\n <img src={src} ref={imgRef} alt={generatedAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n"],"mappings":";AAEA,SAAS,eAAe,YAAY,WAAW,QAAQ,gBAAgB;AAmBhE,IAAM,oBAAoB,cAAkD,MAAS;AAE5F,IAAM,WAAW;AAEjB,IAAM,kBAAN,MAAsB;AAAA,EAAtB;AACE,SAAQ,QAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,IAAI,KAAiC;AACnC,QAAI,CAAC,KAAK,MAAM,IAAI,GAAG,EAAG,QAAO;AAEjC,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,MAAM,IAAI,KAAK,KAAK;AACzB,WAAO;AAAA,EACT;AAAA,EACA,IAAI,KAAa,OAAe;AAC9B,QAAI,KAAK,MAAM,IAAI,GAAG,GAAG;AACvB,WAAK,MAAM,OAAO,GAAG;AAAA,IACvB;AACA,QAAI,KAAK,MAAM,QAAQ,UAAU;AAC/B,WAAK,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IACnD;AACA,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AAAA,EACA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EACA,QAAQ;AACN,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;AAEA,IAAM,eAAe,IAAI,gBAAgB;AACzC,IAAM,sBAAsB,oBAAI,IAA6B;AAE7D,SAAS,kBAAkB,KAAoD;AAC7E,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,SAAS;AAC7D;AAEA,SAAS,gBAAgB,KAAoC;AAC3D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,kBAAkB,GAAG,EAAG,QAAO,IAAI;AACvC,SAAO,IAAI,QAAQ;AACrB;AAEA,IAAM,MAAM,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,MAAM,MAAM;AAAC;AAEnE,IAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,eAAe;AAAA,EACf,eAAe;AACjB,MAA2B;AACzB,QAAM,UAAU,WAAW,iBAAiB;AAE5C,QAAM,cAAc,iBAAiB,SAAS;AAC9C,QAAM,YAAY,kBAAkB,SAAS,aAAa;AAE1D,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,EAAE;AACnD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,cAAc,cAAc,IAAI,SAAS,EAAE;AAClD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,CAAC,YAAY;AAClE,QAAM,wBAAwB,OAAO,kBAAkB;AACvD,QAAM,oBAAoB,OAAO,cAAc;AAE/C,QAAM,SAAS,OAAkC,IAAI;AAGrD,YAAU,MAAM;AACd,0BAAsB,UAAU;AAChC,sBAAkB,UAAU;AAAA,EAC9B,CAAC;AAED,YAAU,MAAM;AACd,QAAI,CAAC,aAAc;AACnB,UAAM,QAAQ,WAAW,MAAM,eAAe,EAAE,GAAG,GAAI;AACvD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,YAAY,CAAC;AAEjB,YAAU,MAAM;AACd,QAAI,CAAC,gBAAgB,CAAC,OAAO,QAAS;AAEtC,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,CAAC,KAAK,MAAM;AACX,YAAI,MAAM,gBAAgB;AACxB,4BAAkB,IAAI;AACtB,mBAAS,WAAW;AAAA,QACtB;AAAA,MACF;AAAA,MACA,EAAE,YAAY,QAAQ;AAAA,IACxB;AACA,aAAS,QAAQ,OAAO,OAAO;AAC/B,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,YAAY,CAAC;AAGjB,YAAU,MAAM;AACd,aAAS,IAAI;AAEb,QAAI,KAAK;AACP,sBAAgB,GAAG;AACnB;AAAA,IACF;AAEA,QAAI,CAAC,OAAO,CAAC,eAAgB;AAE7B,QAAI,WAAW;AACb,sBAAgB,gDAAgD;AAChE;AAAA,IACF;AAEA,QAAI,CAAC,aAAa;AAChB,cAAQ;AAAA,QACN;AAAA,MACF;AACA,sBAAgB,WAAW;AAC3B;AAAA,IACF;AAEA,UAAM,WAAW,gBAAgB,GAAG;AAEpC,QAAI,aAAa,IAAI,QAAQ,GAAG;AAC9B,UAAI,mDAAmD;AACvD,YAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,sBAAgB,aAAa;AAC7B,UAAI,sBAAsB,QAAS,uBAAsB,QAAQ,aAAa;AAC9E;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,UAAM,kBAAkB,YAAY;AAClC,sBAAgB,IAAI;AACpB,UAAI;AACF,YAAI,oBAAoB,IAAI,QAAQ,GAAG;AACrC,cAAI,uFAAuF;AAC3F,gBAAM,UAAU,MAAM,oBAAoB,IAAI,QAAQ;AACtD,cAAI,UAAW;AACf,0BAAgB,OAAO;AACvB,cAAI,sBAAsB,QAAS,uBAAsB,QAAQ,OAAO;AACxE;AAAA,QACF;AAEA,cAAM,gBAAgB,YAAY;AAChC,gBAAM,gBAAgB,MAAM,MAAM,QAAQ;AAC1C,gBAAM,YAAY,MAAM,cAAc,KAAK;AAC3C,gBAAM,YAAY,IAAI,KAAK,CAAC,SAAS,GAAG,aAAa;AAAA,YACnD,MAAM,UAAU,QAAQ;AAAA,UAC1B,CAAC;AACD,gBAAM,WAAW,IAAI,SAAS;AAC9B,mBAAS,OAAO,QAAQ,SAAS;AAEjC,gBAAM,WAAW,MAAM,MAAM,aAAa;AAAA,YACxC,QAAQ;AAAA,YACR,MAAM;AAAA,UACR,CAAC;AAED,cAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,uBAAuB;AACzD,gBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,gBAAM,IAAI,MAAM,mCAAmC;AAAA,QACrD,GAAG;AAEH,4BAAoB,IAAI,UAAU,YAAY;AAE9C,cAAM,aAAa,MAAM;AACzB,4BAAoB,OAAO,QAAQ;AACnC,qBAAa,IAAI,UAAU,UAAU;AAErC,YAAI,UAAW;AACf,wBAAgB,UAAU;AAC1B,YAAI,sBAAsB,QAAS,uBAAsB,QAAQ,UAAU;AAAA,MAC7E,SAAS,KAAK;AACZ,cAAM,kBAAkB,eAAe,QAAQ,MAAM,IAAI,MAAM,eAAe;AAC9E,4BAAoB,OAAO,QAAQ;AACnC,YAAI,UAAW;AACf,iBAAS,eAAe;AACxB,wBAAgB,WAAW;AAC3B,YAAI,kBAAkB,QAAS,mBAAkB,QAAQ,eAAe;AAAA,MAC1E,UAAE;AACA,YAAI,CAAC,UAAW,iBAAgB,KAAK;AAAA,MACvC;AAAA,IACF;AAEA,oBAAgB;AAEhB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,KAAK,KAAK,aAAa,aAAa,SAAS,CAAC;AAElD,SAAO,EAAE,cAAc,cAAc,OAAO,QAAQ,aAAa;AACnE;;;ACnNS,SAmDL,UAnDK,KAmDL,YAnDK;AADF,IAAM,qBAAqB,CAAC,EAAE,OAAO,SAAS,MAA+B;AAClF,SAAO,oBAAC,kBAAkB,UAAlB,EAA2B,OAAe,UAAS;AAC7D;AAcA,IAAM,gBAAqC;AAAA,EACzC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,aAAa;AACf;AAEO,IAAM,aAAa,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf,eAAe;AAAA,EACf,GAAG;AACL,MAAuB;AACrB,QAAM,EAAE,cAAc,cAAc,cAAc,OAAO,IAAI,cAAc;AAAA,IACzE;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE,iCACG;AAAA,oBACC,oBAAC,UAAK,OAAO,eAAe,aAAU,UAAS,eAAY,QACxD,wBACH;AAAA,IAEF,oBAAC,SAAI,KAAU,KAAK,QAAQ,KAAK,cAAc,aAAW,cAAe,GAAG,OAAO;AAAA,KACrF;AAEJ;","names":[]}
package/dist/next.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface SmartNextImageProps extends Omit<ImageProps, "alt"> {
8
8
  onCaptionError?: (error: Error) => void;
9
9
  disableAI?: boolean;
10
10
  announceLive?: boolean;
11
+ lazyGenerate?: boolean;
11
12
  }
12
13
  export declare const SR_ONLY_STYLE: React.CSSProperties;
13
- export declare const SmartNextImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, onCaptionError, disableAI: propsDisableAI, announceLive, ...props }: SmartNextImageProps) => import("react/jsx-runtime").JSX.Element;
14
+ export declare const SmartNextImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, onCaptionError, disableAI: propsDisableAI, announceLive, lazyGenerate, ...props }: SmartNextImageProps) => import("react/jsx-runtime").JSX.Element;
package/dist/next.js CHANGED
@@ -46,9 +46,16 @@ var LRUCaptionCache = class {
46
46
  this.cache = /* @__PURE__ */ new Map();
47
47
  }
48
48
  get(key) {
49
- return this.cache.get(key);
49
+ if (!this.cache.has(key)) return void 0;
50
+ const value = this.cache.get(key);
51
+ this.cache.delete(key);
52
+ this.cache.set(key, value);
53
+ return value;
50
54
  }
51
55
  set(key, value) {
56
+ if (this.cache.has(key)) {
57
+ this.cache.delete(key);
58
+ }
52
59
  if (this.cache.size >= MAX_SIZE) {
53
60
  this.cache.delete(this.cache.keys().next().value);
54
61
  }
@@ -80,7 +87,9 @@ var useAICaptions = ({
80
87
  fallbackAlt = "Image loading or caption unavailable",
81
88
  onCaptionGenerated,
82
89
  disableAI: propsDisableAI,
83
- onCaptionError
90
+ onCaptionError,
91
+ announceLive = false,
92
+ lazyGenerate = true
84
93
  }) => {
85
94
  const context = (0, import_react.useContext)(SmartImageContext);
86
95
  const apiEndpoint = propsEndpoint || context?.apiEndpoint;
@@ -88,19 +97,41 @@ var useAICaptions = ({
88
97
  const [generatedAlt, setGeneratedAlt] = (0, import_react.useState)("");
89
98
  const [isGenerating, setIsGenerating] = (0, import_react.useState)(false);
90
99
  const [error, setError] = (0, import_react.useState)(null);
100
+ const [announcement, setAnnouncemet] = (0, import_react.useState)("");
101
+ const [shouldGenerate, setShouldGenerate] = (0, import_react.useState)(!lazyGenerate);
91
102
  const onCaptionGeneratedRef = (0, import_react.useRef)(onCaptionGenerated);
92
103
  const onCaptionErrorRef = (0, import_react.useRef)(onCaptionError);
104
+ const imgRef = (0, import_react.useRef)(null);
93
105
  (0, import_react.useEffect)(() => {
94
106
  onCaptionGeneratedRef.current = onCaptionGenerated;
95
107
  onCaptionErrorRef.current = onCaptionError;
96
108
  });
109
+ (0, import_react.useEffect)(() => {
110
+ if (!announcement) return;
111
+ const timer = setTimeout(() => setAnnouncemet(""), 5e3);
112
+ return () => clearTimeout(timer);
113
+ }, [announcement]);
114
+ (0, import_react.useEffect)(() => {
115
+ if (!lazyGenerate || !imgRef.current) return;
116
+ const observer = new IntersectionObserver(
117
+ ([entry]) => {
118
+ if (entry.isIntersecting) {
119
+ setShouldGenerate(true);
120
+ observer.disconnect();
121
+ }
122
+ },
123
+ { rootMargin: "200px" }
124
+ );
125
+ observer.observe(imgRef.current);
126
+ return () => observer.disconnect();
127
+ }, [lazyGenerate]);
97
128
  (0, import_react.useEffect)(() => {
98
129
  setError(null);
99
130
  if (alt) {
100
131
  setGeneratedAlt(alt);
101
132
  return;
102
133
  }
103
- if (!src) return;
134
+ if (!src || !shouldGenerate) return;
104
135
  if (disableAI) {
105
136
  setGeneratedAlt("[Testing mode: AI caption generation disabled]");
106
137
  return;
@@ -172,7 +203,7 @@ var useAICaptions = ({
172
203
  cancelled = true;
173
204
  };
174
205
  }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
175
- return { generatedAlt, isGenerating, error };
206
+ return { generatedAlt, isGenerating, error, imgRef, announcement };
176
207
  };
177
208
 
178
209
  // src/next.tsx
@@ -197,20 +228,22 @@ var SmartNextImage = ({
197
228
  onCaptionError,
198
229
  disableAI: propsDisableAI,
199
230
  announceLive = false,
231
+ lazyGenerate = true,
200
232
  ...props
201
233
  }) => {
202
- const { isGenerating, generatedAlt } = useAICaptions({
234
+ const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({
203
235
  src,
204
236
  alt,
205
237
  apiEndpoint: propsEndpoint,
206
238
  fallbackAlt,
207
239
  onCaptionGenerated,
208
240
  disableAI: propsDisableAI,
209
- announceLive
241
+ announceLive,
242
+ lazyGenerate
210
243
  });
211
244
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
212
- announceLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: isGenerating ? "Generating image description. Please wait..." : generatedAlt ? `Image description generated: ${generatedAlt}` : "" }),
213
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src, alt: generatedAlt || fallbackAlt, "aria-busy": isGenerating, ...props })
245
+ announceLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: announcement }),
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src, ref: imgRef, alt: generatedAlt || fallbackAlt, "aria-busy": isGenerating, ...props })
214
247
  ] });
215
248
  };
216
249
  // Annotate the CommonJS export names for ESM import in node:
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.tsx","../src/useAICaption.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport React from \"react\";\r\nimport Image, { ImageProps } from \"next/image\";\r\n\r\nimport { useAICaptions } from \"./useAICaption\";\r\n\r\nexport interface SmartNextImageProps extends Omit<ImageProps, \"alt\"> {\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\nexport const SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\nexport const SmartNextImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n ...props\r\n}: SmartNextImageProps) => {\r\n const { isGenerating, generatedAlt } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n });\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {isGenerating\r\n ? \"Generating image description. Please wait...\"\r\n : generatedAlt\r\n ? `Image description generated: ${generatedAlt}`\r\n : \"\"}\r\n </span>\r\n )}\r\n\r\n <Image src={src} alt={generatedAlt || fallbackAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, ImgHTMLAttributes, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n return this.cache.get(key);\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error };\r\n};\r\n"],"mappings":"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","names":["Image"]}
1
+ {"version":3,"sources":["../src/next.tsx","../src/useAICaption.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport React from \"react\";\r\nimport Image, { ImageProps } from \"next/image\";\r\n\r\nimport { useAICaptions } from \"./useAICaption\";\r\n\r\nexport interface SmartNextImageProps extends Omit<ImageProps, \"alt\"> {\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\nexport const SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\nexport const SmartNextImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n ...props\r\n}: SmartNextImageProps) => {\r\n const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n lazyGenerate,\r\n });\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {announcement}\r\n </span>\r\n )}\r\n\r\n <Image src={src} ref={imgRef} alt={generatedAlt || fallbackAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n if (!this.cache.has(key)) return undefined;\r\n\r\n const value = this.cache.get(key)!;\r\n this.cache.delete(key);\r\n this.cache.set(key, value);\r\n return value;\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.has(key)) {\r\n this.cache.delete(key);\r\n }\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n const [announcement, setAnnouncemet] = useState(\"\");\r\n const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n const imgRef = useRef < HTMLImageElement | null>(null);\r\n\r\n \r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n if (!announcement) return;\r\n const timer = setTimeout(() => setAnnouncemet(\"\"), 5000);\r\n return () => clearTimeout(timer)\r\n }, [announcement])\r\n \r\n useEffect(() => {\r\n if (!lazyGenerate || !imgRef.current) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setShouldGenerate(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { rootMargin: \"200px\" }\r\n );\r\n observer.observe(imgRef.current);\r\n return () => observer.disconnect();\r\n }, [lazyGenerate])\r\n\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src || !shouldGenerate) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n"],"mappings":"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","names":["Image"]}
package/dist/next.mjs CHANGED
@@ -12,9 +12,16 @@ var LRUCaptionCache = class {
12
12
  this.cache = /* @__PURE__ */ new Map();
13
13
  }
14
14
  get(key) {
15
- return this.cache.get(key);
15
+ if (!this.cache.has(key)) return void 0;
16
+ const value = this.cache.get(key);
17
+ this.cache.delete(key);
18
+ this.cache.set(key, value);
19
+ return value;
16
20
  }
17
21
  set(key, value) {
22
+ if (this.cache.has(key)) {
23
+ this.cache.delete(key);
24
+ }
18
25
  if (this.cache.size >= MAX_SIZE) {
19
26
  this.cache.delete(this.cache.keys().next().value);
20
27
  }
@@ -46,7 +53,9 @@ var useAICaptions = ({
46
53
  fallbackAlt = "Image loading or caption unavailable",
47
54
  onCaptionGenerated,
48
55
  disableAI: propsDisableAI,
49
- onCaptionError
56
+ onCaptionError,
57
+ announceLive = false,
58
+ lazyGenerate = true
50
59
  }) => {
51
60
  const context = useContext(SmartImageContext);
52
61
  const apiEndpoint = propsEndpoint || context?.apiEndpoint;
@@ -54,19 +63,41 @@ var useAICaptions = ({
54
63
  const [generatedAlt, setGeneratedAlt] = useState("");
55
64
  const [isGenerating, setIsGenerating] = useState(false);
56
65
  const [error, setError] = useState(null);
66
+ const [announcement, setAnnouncemet] = useState("");
67
+ const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);
57
68
  const onCaptionGeneratedRef = useRef(onCaptionGenerated);
58
69
  const onCaptionErrorRef = useRef(onCaptionError);
70
+ const imgRef = useRef(null);
59
71
  useEffect(() => {
60
72
  onCaptionGeneratedRef.current = onCaptionGenerated;
61
73
  onCaptionErrorRef.current = onCaptionError;
62
74
  });
75
+ useEffect(() => {
76
+ if (!announcement) return;
77
+ const timer = setTimeout(() => setAnnouncemet(""), 5e3);
78
+ return () => clearTimeout(timer);
79
+ }, [announcement]);
80
+ useEffect(() => {
81
+ if (!lazyGenerate || !imgRef.current) return;
82
+ const observer = new IntersectionObserver(
83
+ ([entry]) => {
84
+ if (entry.isIntersecting) {
85
+ setShouldGenerate(true);
86
+ observer.disconnect();
87
+ }
88
+ },
89
+ { rootMargin: "200px" }
90
+ );
91
+ observer.observe(imgRef.current);
92
+ return () => observer.disconnect();
93
+ }, [lazyGenerate]);
63
94
  useEffect(() => {
64
95
  setError(null);
65
96
  if (alt) {
66
97
  setGeneratedAlt(alt);
67
98
  return;
68
99
  }
69
- if (!src) return;
100
+ if (!src || !shouldGenerate) return;
70
101
  if (disableAI) {
71
102
  setGeneratedAlt("[Testing mode: AI caption generation disabled]");
72
103
  return;
@@ -138,7 +169,7 @@ var useAICaptions = ({
138
169
  cancelled = true;
139
170
  };
140
171
  }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
141
- return { generatedAlt, isGenerating, error };
172
+ return { generatedAlt, isGenerating, error, imgRef, announcement };
142
173
  };
143
174
 
144
175
  // src/next.tsx
@@ -163,20 +194,22 @@ var SmartNextImage = ({
163
194
  onCaptionError,
164
195
  disableAI: propsDisableAI,
165
196
  announceLive = false,
197
+ lazyGenerate = true,
166
198
  ...props
167
199
  }) => {
168
- const { isGenerating, generatedAlt } = useAICaptions({
200
+ const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({
169
201
  src,
170
202
  alt,
171
203
  apiEndpoint: propsEndpoint,
172
204
  fallbackAlt,
173
205
  onCaptionGenerated,
174
206
  disableAI: propsDisableAI,
175
- announceLive
207
+ announceLive,
208
+ lazyGenerate
176
209
  });
177
210
  return /* @__PURE__ */ jsxs(Fragment, { children: [
178
- announceLive && /* @__PURE__ */ jsx("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: isGenerating ? "Generating image description. Please wait..." : generatedAlt ? `Image description generated: ${generatedAlt}` : "" }),
179
- /* @__PURE__ */ jsx(Image, { src, alt: generatedAlt || fallbackAlt, "aria-busy": isGenerating, ...props })
211
+ announceLive && /* @__PURE__ */ jsx("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: announcement }),
212
+ /* @__PURE__ */ jsx(Image, { src, ref: imgRef, alt: generatedAlt || fallbackAlt, "aria-busy": isGenerating, ...props })
180
213
  ] });
181
214
  };
182
215
  export {
package/dist/next.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.tsx","../src/useAICaption.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport React from \"react\";\r\nimport Image, { ImageProps } from \"next/image\";\r\n\r\nimport { useAICaptions } from \"./useAICaption\";\r\n\r\nexport interface SmartNextImageProps extends Omit<ImageProps, \"alt\"> {\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\nexport const SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\nexport const SmartNextImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n ...props\r\n}: SmartNextImageProps) => {\r\n const { isGenerating, generatedAlt } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n });\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {isGenerating\r\n ? \"Generating image description. Please wait...\"\r\n : generatedAlt\r\n ? `Image description generated: ${generatedAlt}`\r\n : \"\"}\r\n </span>\r\n )}\r\n\r\n <Image src={src} alt={generatedAlt || fallbackAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, ImgHTMLAttributes, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n return this.cache.get(key);\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error };\r\n};\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/next.tsx","../src/useAICaption.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport React from \"react\";\r\nimport Image, { ImageProps } from \"next/image\";\r\n\r\nimport { useAICaptions } from \"./useAICaption\";\r\n\r\nexport interface SmartNextImageProps extends Omit<ImageProps, \"alt\"> {\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\nexport const SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\nexport const SmartNextImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n onCaptionError,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n ...props\r\n}: SmartNextImageProps) => {\r\n const { isGenerating, generatedAlt, announcement, imgRef } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n lazyGenerate,\r\n });\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {announcement}\r\n </span>\r\n )}\r\n\r\n <Image src={src} ref={imgRef} alt={generatedAlt || fallbackAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n onCaptionError?: (error: Error) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n lazyGenerate?: boolean;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n if (!this.cache.has(key)) return undefined;\r\n\r\n const value = this.cache.get(key)!;\r\n this.cache.delete(key);\r\n this.cache.set(key, value);\r\n return value;\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.has(key)) {\r\n this.cache.delete(key);\r\n }\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n announceLive = false,\r\n lazyGenerate = true,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n const [announcement, setAnnouncemet] = useState(\"\");\r\n const [shouldGenerate, setShouldGenerate] = useState(!lazyGenerate);\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n const imgRef = useRef < HTMLImageElement | null>(null);\r\n\r\n \r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n if (!announcement) return;\r\n const timer = setTimeout(() => setAnnouncemet(\"\"), 5000);\r\n return () => clearTimeout(timer)\r\n }, [announcement])\r\n \r\n useEffect(() => {\r\n if (!lazyGenerate || !imgRef.current) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setShouldGenerate(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { rootMargin: \"200px\" }\r\n );\r\n observer.observe(imgRef.current);\r\n return () => observer.disconnect();\r\n }, [lazyGenerate])\r\n\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src || !shouldGenerate) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n"],"mappings":"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","names":[]}
@@ -1,30 +1,25 @@
1
1
  import { StaticImport } from "next/dist/shared/lib/get-img-props";
2
- import { ImgHTMLAttributes } from "react";
3
2
  export interface UseAICaptionOptions {
4
3
  src?: string | StaticImport;
5
4
  alt?: string;
6
5
  apiEndpoint?: string;
7
6
  fallbackAlt?: string;
8
7
  onCaptionGenerated?: (caption: string) => void;
8
+ onCaptionError?: (error: Error) => void;
9
9
  disableAI?: boolean;
10
10
  announceLive?: boolean;
11
- onCaptionError?: (error: Error) => void;
11
+ lazyGenerate?: boolean;
12
12
  }
13
13
  interface SmartImageContextProps {
14
14
  apiEndpoint?: string;
15
15
  disableAI?: boolean;
16
16
  }
17
17
  export declare const SmartImageContext: import("react").Context<SmartImageContextProps | undefined>;
18
- export interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {
19
- apiEndpoint?: string;
20
- fallbackAlt?: string;
21
- onCaptionGenerated?: (caption: string) => void;
22
- disableAI?: boolean;
23
- announceLive?: boolean;
24
- }
25
- export declare const useAICaptions: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, disableAI: propsDisableAI, onCaptionError, }: UseAICaptionOptions) => {
18
+ export declare const useAICaptions: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, disableAI: propsDisableAI, onCaptionError, announceLive, lazyGenerate, }: UseAICaptionOptions) => {
26
19
  generatedAlt: string;
27
20
  isGenerating: boolean;
28
21
  error: Error | null;
22
+ imgRef: import("react").RefObject<HTMLImageElement | null>;
23
+ announcement: string;
29
24
  };
30
25
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-a11y-auto-caption",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Smart React component that automatically generates alt text for images using AI.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",