react-a11y-auto-caption 1.0.1 → 1.0.4

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.mjs ADDED
@@ -0,0 +1,185 @@
1
+ // src/useAICaption.ts
2
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
3
+ var SmartImageContext = createContext(void 0);
4
+ var MAX_SIZE = 500;
5
+ var LRUCaptionCache = class {
6
+ constructor() {
7
+ this.cache = /* @__PURE__ */ new Map();
8
+ }
9
+ get(key) {
10
+ return this.cache.get(key);
11
+ }
12
+ set(key, value) {
13
+ if (this.cache.size >= MAX_SIZE) {
14
+ this.cache.delete(this.cache.keys().next().value);
15
+ }
16
+ this.cache.set(key, value);
17
+ }
18
+ has(key) {
19
+ return this.cache.has(key);
20
+ }
21
+ clear() {
22
+ this.cache.clear();
23
+ }
24
+ };
25
+ var captionCache = new LRUCaptionCache();
26
+ var pendingRequestCache = /* @__PURE__ */ new Map();
27
+ function isStaticImageData(src) {
28
+ return typeof src === "object" && src !== null && "src" in src;
29
+ }
30
+ function resolveImageUrl(src) {
31
+ if (typeof src === "string") return src;
32
+ if (isStaticImageData(src)) return src.src;
33
+ return src.default.src;
34
+ }
35
+ var log = process.env.NODE_ENV === "development" ? console.log : () => {
36
+ };
37
+ var useAICaptions = ({
38
+ src,
39
+ alt,
40
+ apiEndpoint: propsEndpoint,
41
+ fallbackAlt = "Image loading or caption unavailable",
42
+ onCaptionGenerated,
43
+ disableAI: propsDisableAI,
44
+ onCaptionError
45
+ }) => {
46
+ const context = useContext(SmartImageContext);
47
+ const apiEndpoint = propsEndpoint || context?.apiEndpoint;
48
+ const disableAI = propsDisableAI ?? context?.disableAI ?? false;
49
+ const [generatedAlt, setGeneratedAlt] = useState("");
50
+ const [isGenerating, setIsGenerating] = useState(false);
51
+ const [error, setError] = useState(null);
52
+ const onCaptionGeneratedRef = useRef(onCaptionGenerated);
53
+ const onCaptionErrorRef = useRef(onCaptionError);
54
+ useEffect(() => {
55
+ onCaptionGeneratedRef.current = onCaptionGenerated;
56
+ onCaptionErrorRef.current = onCaptionError;
57
+ });
58
+ useEffect(() => {
59
+ setError(null);
60
+ if (alt) {
61
+ setGeneratedAlt(alt);
62
+ return;
63
+ }
64
+ if (!src) return;
65
+ if (disableAI) {
66
+ setGeneratedAlt("[Testing mode: AI caption generation disabled]");
67
+ return;
68
+ }
69
+ if (!apiEndpoint) {
70
+ console.warn(
71
+ "[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation."
72
+ );
73
+ setGeneratedAlt(fallbackAlt);
74
+ return;
75
+ }
76
+ const imageUrl = resolveImageUrl(src);
77
+ if (captionCache.has(imageUrl)) {
78
+ log("[SmartImage] Cache hit: Reusing existing caption.");
79
+ const cachedCaption = captionCache.get(imageUrl);
80
+ setGeneratedAlt(cachedCaption);
81
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);
82
+ return;
83
+ }
84
+ let cancelled = false;
85
+ const generateCaption = async () => {
86
+ setIsGenerating(true);
87
+ try {
88
+ if (pendingRequestCache.has(imageUrl)) {
89
+ log("[SmartImage] Pending request detected. Waiting for the existing API call to complete.");
90
+ const caption = await pendingRequestCache.get(imageUrl);
91
+ if (cancelled) return;
92
+ setGeneratedAlt(caption);
93
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);
94
+ return;
95
+ }
96
+ const fetchPromise = (async () => {
97
+ const imageResponse = await fetch(imageUrl);
98
+ const imageBlob = await imageResponse.blob();
99
+ const imageFile = new File([imageBlob], "image.jpg", {
100
+ type: imageBlob.type || "image/jpeg"
101
+ });
102
+ const formData = new FormData();
103
+ formData.append("file", imageFile);
104
+ const response = await fetch(apiEndpoint, {
105
+ method: "POST",
106
+ body: formData
107
+ });
108
+ if (!response.ok) throw new Error("AI API request failed");
109
+ const data = await response.json();
110
+ if (data.caption) return data.caption;
111
+ throw new Error("No caption returned from the API.");
112
+ })();
113
+ pendingRequestCache.set(imageUrl, fetchPromise);
114
+ const newCaption = await fetchPromise;
115
+ pendingRequestCache.delete(imageUrl);
116
+ captionCache.set(imageUrl, newCaption);
117
+ if (cancelled) return;
118
+ setGeneratedAlt(newCaption);
119
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);
120
+ } catch (err) {
121
+ const normalizedError = err instanceof Error ? err : new Error("Unknown error");
122
+ pendingRequestCache.delete(imageUrl);
123
+ if (cancelled) return;
124
+ setError(normalizedError);
125
+ setGeneratedAlt(fallbackAlt);
126
+ if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);
127
+ } finally {
128
+ if (!cancelled) setIsGenerating(false);
129
+ }
130
+ };
131
+ generateCaption();
132
+ return () => {
133
+ cancelled = true;
134
+ };
135
+ }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
136
+ return { generatedAlt, isGenerating, error };
137
+ };
138
+
139
+ // src/index.tsx
140
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
141
+ var SmartImageProvider = ({ value, children }) => {
142
+ return /* @__PURE__ */ jsx(SmartImageContext.Provider, { value, children });
143
+ };
144
+ var SR_ONLY_STYLE = {
145
+ position: "absolute",
146
+ width: "1px",
147
+ height: "1px",
148
+ padding: 0,
149
+ margin: "-1px",
150
+ overflow: "hidden",
151
+ clip: "rect(0, 0, 0, 0)",
152
+ whiteSpace: "nowrap",
153
+ borderWidth: 0
154
+ };
155
+ var SmartImage = ({
156
+ src,
157
+ alt,
158
+ apiEndpoint: propsEndpoint,
159
+ fallbackAlt = "Image loading or caption unavailable",
160
+ onCaptionGenerated,
161
+ disableAI: propsDisableAI,
162
+ announceLive = false,
163
+ onCaptionError,
164
+ ...props
165
+ }) => {
166
+ const { isGenerating, generatedAlt } = useAICaptions({
167
+ src,
168
+ alt,
169
+ apiEndpoint: propsEndpoint,
170
+ fallbackAlt,
171
+ onCaptionGenerated,
172
+ disableAI: propsDisableAI,
173
+ announceLive,
174
+ onCaptionError
175
+ });
176
+ 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 })
179
+ ] });
180
+ };
181
+ export {
182
+ SmartImage,
183
+ SmartImageProvider
184
+ };
185
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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":"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","names":[]}
package/dist/next.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { ImageProps } from "next/image";
3
+ export interface SmartNextImageProps extends Omit<ImageProps, "alt"> {
4
+ alt?: string;
5
+ apiEndpoint?: string;
6
+ fallbackAlt?: string;
7
+ onCaptionGenerated?: (caption: string) => void;
8
+ onCaptionError?: (error: Error) => void;
9
+ disableAI?: boolean;
10
+ announceLive?: boolean;
11
+ }
12
+ 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;
package/dist/next.js ADDED
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/next.tsx
32
+ var next_exports = {};
33
+ __export(next_exports, {
34
+ SR_ONLY_STYLE: () => SR_ONLY_STYLE,
35
+ SmartNextImage: () => SmartNextImage
36
+ });
37
+ module.exports = __toCommonJS(next_exports);
38
+ var import_image = __toESM(require("next/image"));
39
+
40
+ // src/useAICaption.ts
41
+ var import_react = require("react");
42
+ var SmartImageContext = (0, import_react.createContext)(void 0);
43
+ var MAX_SIZE = 500;
44
+ var LRUCaptionCache = class {
45
+ constructor() {
46
+ this.cache = /* @__PURE__ */ new Map();
47
+ }
48
+ get(key) {
49
+ return this.cache.get(key);
50
+ }
51
+ set(key, value) {
52
+ if (this.cache.size >= MAX_SIZE) {
53
+ this.cache.delete(this.cache.keys().next().value);
54
+ }
55
+ this.cache.set(key, value);
56
+ }
57
+ has(key) {
58
+ return this.cache.has(key);
59
+ }
60
+ clear() {
61
+ this.cache.clear();
62
+ }
63
+ };
64
+ var captionCache = new LRUCaptionCache();
65
+ var pendingRequestCache = /* @__PURE__ */ new Map();
66
+ function isStaticImageData(src) {
67
+ return typeof src === "object" && src !== null && "src" in src;
68
+ }
69
+ function resolveImageUrl(src) {
70
+ if (typeof src === "string") return src;
71
+ if (isStaticImageData(src)) return src.src;
72
+ return src.default.src;
73
+ }
74
+ var log = process.env.NODE_ENV === "development" ? console.log : () => {
75
+ };
76
+ var useAICaptions = ({
77
+ src,
78
+ alt,
79
+ apiEndpoint: propsEndpoint,
80
+ fallbackAlt = "Image loading or caption unavailable",
81
+ onCaptionGenerated,
82
+ disableAI: propsDisableAI,
83
+ onCaptionError
84
+ }) => {
85
+ const context = (0, import_react.useContext)(SmartImageContext);
86
+ const apiEndpoint = propsEndpoint || context?.apiEndpoint;
87
+ const disableAI = propsDisableAI ?? context?.disableAI ?? false;
88
+ const [generatedAlt, setGeneratedAlt] = (0, import_react.useState)("");
89
+ const [isGenerating, setIsGenerating] = (0, import_react.useState)(false);
90
+ const [error, setError] = (0, import_react.useState)(null);
91
+ const onCaptionGeneratedRef = (0, import_react.useRef)(onCaptionGenerated);
92
+ const onCaptionErrorRef = (0, import_react.useRef)(onCaptionError);
93
+ (0, import_react.useEffect)(() => {
94
+ onCaptionGeneratedRef.current = onCaptionGenerated;
95
+ onCaptionErrorRef.current = onCaptionError;
96
+ });
97
+ (0, import_react.useEffect)(() => {
98
+ setError(null);
99
+ if (alt) {
100
+ setGeneratedAlt(alt);
101
+ return;
102
+ }
103
+ if (!src) return;
104
+ if (disableAI) {
105
+ setGeneratedAlt("[Testing mode: AI caption generation disabled]");
106
+ return;
107
+ }
108
+ if (!apiEndpoint) {
109
+ console.warn(
110
+ "[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation."
111
+ );
112
+ setGeneratedAlt(fallbackAlt);
113
+ return;
114
+ }
115
+ const imageUrl = resolveImageUrl(src);
116
+ if (captionCache.has(imageUrl)) {
117
+ log("[SmartImage] Cache hit: Reusing existing caption.");
118
+ const cachedCaption = captionCache.get(imageUrl);
119
+ setGeneratedAlt(cachedCaption);
120
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);
121
+ return;
122
+ }
123
+ let cancelled = false;
124
+ const generateCaption = async () => {
125
+ setIsGenerating(true);
126
+ try {
127
+ if (pendingRequestCache.has(imageUrl)) {
128
+ log("[SmartImage] Pending request detected. Waiting for the existing API call to complete.");
129
+ const caption = await pendingRequestCache.get(imageUrl);
130
+ if (cancelled) return;
131
+ setGeneratedAlt(caption);
132
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);
133
+ return;
134
+ }
135
+ const fetchPromise = (async () => {
136
+ const imageResponse = await fetch(imageUrl);
137
+ const imageBlob = await imageResponse.blob();
138
+ const imageFile = new File([imageBlob], "image.jpg", {
139
+ type: imageBlob.type || "image/jpeg"
140
+ });
141
+ const formData = new FormData();
142
+ formData.append("file", imageFile);
143
+ const response = await fetch(apiEndpoint, {
144
+ method: "POST",
145
+ body: formData
146
+ });
147
+ if (!response.ok) throw new Error("AI API request failed");
148
+ const data = await response.json();
149
+ if (data.caption) return data.caption;
150
+ throw new Error("No caption returned from the API.");
151
+ })();
152
+ pendingRequestCache.set(imageUrl, fetchPromise);
153
+ const newCaption = await fetchPromise;
154
+ pendingRequestCache.delete(imageUrl);
155
+ captionCache.set(imageUrl, newCaption);
156
+ if (cancelled) return;
157
+ setGeneratedAlt(newCaption);
158
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);
159
+ } catch (err) {
160
+ const normalizedError = err instanceof Error ? err : new Error("Unknown error");
161
+ pendingRequestCache.delete(imageUrl);
162
+ if (cancelled) return;
163
+ setError(normalizedError);
164
+ setGeneratedAlt(fallbackAlt);
165
+ if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);
166
+ } finally {
167
+ if (!cancelled) setIsGenerating(false);
168
+ }
169
+ };
170
+ generateCaption();
171
+ return () => {
172
+ cancelled = true;
173
+ };
174
+ }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
175
+ return { generatedAlt, isGenerating, error };
176
+ };
177
+
178
+ // src/next.tsx
179
+ var import_jsx_runtime = require("react/jsx-runtime");
180
+ var SR_ONLY_STYLE = {
181
+ position: "absolute",
182
+ width: "1px",
183
+ height: "1px",
184
+ padding: 0,
185
+ margin: "-1px",
186
+ overflow: "hidden",
187
+ clip: "rect(0, 0, 0, 0)",
188
+ whiteSpace: "nowrap",
189
+ borderWidth: 0
190
+ };
191
+ var SmartNextImage = ({
192
+ src,
193
+ alt,
194
+ apiEndpoint: propsEndpoint,
195
+ fallbackAlt = "Image loading or caption unavailable",
196
+ onCaptionGenerated,
197
+ onCaptionError,
198
+ disableAI: propsDisableAI,
199
+ announceLive = false,
200
+ ...props
201
+ }) => {
202
+ const { isGenerating, generatedAlt } = useAICaptions({
203
+ src,
204
+ alt,
205
+ apiEndpoint: propsEndpoint,
206
+ fallbackAlt,
207
+ onCaptionGenerated,
208
+ disableAI: propsDisableAI,
209
+ announceLive
210
+ });
211
+ 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 })
214
+ ] });
215
+ };
216
+ // Annotate the CommonJS export names for ESM import in node:
217
+ 0 && (module.exports = {
218
+ SR_ONLY_STYLE,
219
+ SmartNextImage
220
+ });
221
+ //# sourceMappingURL=next.js.map
@@ -0,0 +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"]}
package/dist/next.mjs ADDED
@@ -0,0 +1,186 @@
1
+ "use client";
2
+
3
+ // src/next.tsx
4
+ import Image from "next/image";
5
+
6
+ // src/useAICaption.ts
7
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
8
+ var SmartImageContext = createContext(void 0);
9
+ var MAX_SIZE = 500;
10
+ var LRUCaptionCache = class {
11
+ constructor() {
12
+ this.cache = /* @__PURE__ */ new Map();
13
+ }
14
+ get(key) {
15
+ return this.cache.get(key);
16
+ }
17
+ set(key, value) {
18
+ if (this.cache.size >= MAX_SIZE) {
19
+ this.cache.delete(this.cache.keys().next().value);
20
+ }
21
+ this.cache.set(key, value);
22
+ }
23
+ has(key) {
24
+ return this.cache.has(key);
25
+ }
26
+ clear() {
27
+ this.cache.clear();
28
+ }
29
+ };
30
+ var captionCache = new LRUCaptionCache();
31
+ var pendingRequestCache = /* @__PURE__ */ new Map();
32
+ function isStaticImageData(src) {
33
+ return typeof src === "object" && src !== null && "src" in src;
34
+ }
35
+ function resolveImageUrl(src) {
36
+ if (typeof src === "string") return src;
37
+ if (isStaticImageData(src)) return src.src;
38
+ return src.default.src;
39
+ }
40
+ var log = process.env.NODE_ENV === "development" ? console.log : () => {
41
+ };
42
+ var useAICaptions = ({
43
+ src,
44
+ alt,
45
+ apiEndpoint: propsEndpoint,
46
+ fallbackAlt = "Image loading or caption unavailable",
47
+ onCaptionGenerated,
48
+ disableAI: propsDisableAI,
49
+ onCaptionError
50
+ }) => {
51
+ const context = useContext(SmartImageContext);
52
+ const apiEndpoint = propsEndpoint || context?.apiEndpoint;
53
+ const disableAI = propsDisableAI ?? context?.disableAI ?? false;
54
+ const [generatedAlt, setGeneratedAlt] = useState("");
55
+ const [isGenerating, setIsGenerating] = useState(false);
56
+ const [error, setError] = useState(null);
57
+ const onCaptionGeneratedRef = useRef(onCaptionGenerated);
58
+ const onCaptionErrorRef = useRef(onCaptionError);
59
+ useEffect(() => {
60
+ onCaptionGeneratedRef.current = onCaptionGenerated;
61
+ onCaptionErrorRef.current = onCaptionError;
62
+ });
63
+ useEffect(() => {
64
+ setError(null);
65
+ if (alt) {
66
+ setGeneratedAlt(alt);
67
+ return;
68
+ }
69
+ if (!src) return;
70
+ if (disableAI) {
71
+ setGeneratedAlt("[Testing mode: AI caption generation disabled]");
72
+ return;
73
+ }
74
+ if (!apiEndpoint) {
75
+ console.warn(
76
+ "[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation."
77
+ );
78
+ setGeneratedAlt(fallbackAlt);
79
+ return;
80
+ }
81
+ const imageUrl = resolveImageUrl(src);
82
+ if (captionCache.has(imageUrl)) {
83
+ log("[SmartImage] Cache hit: Reusing existing caption.");
84
+ const cachedCaption = captionCache.get(imageUrl);
85
+ setGeneratedAlt(cachedCaption);
86
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);
87
+ return;
88
+ }
89
+ let cancelled = false;
90
+ const generateCaption = async () => {
91
+ setIsGenerating(true);
92
+ try {
93
+ if (pendingRequestCache.has(imageUrl)) {
94
+ log("[SmartImage] Pending request detected. Waiting for the existing API call to complete.");
95
+ const caption = await pendingRequestCache.get(imageUrl);
96
+ if (cancelled) return;
97
+ setGeneratedAlt(caption);
98
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);
99
+ return;
100
+ }
101
+ const fetchPromise = (async () => {
102
+ const imageResponse = await fetch(imageUrl);
103
+ const imageBlob = await imageResponse.blob();
104
+ const imageFile = new File([imageBlob], "image.jpg", {
105
+ type: imageBlob.type || "image/jpeg"
106
+ });
107
+ const formData = new FormData();
108
+ formData.append("file", imageFile);
109
+ const response = await fetch(apiEndpoint, {
110
+ method: "POST",
111
+ body: formData
112
+ });
113
+ if (!response.ok) throw new Error("AI API request failed");
114
+ const data = await response.json();
115
+ if (data.caption) return data.caption;
116
+ throw new Error("No caption returned from the API.");
117
+ })();
118
+ pendingRequestCache.set(imageUrl, fetchPromise);
119
+ const newCaption = await fetchPromise;
120
+ pendingRequestCache.delete(imageUrl);
121
+ captionCache.set(imageUrl, newCaption);
122
+ if (cancelled) return;
123
+ setGeneratedAlt(newCaption);
124
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);
125
+ } catch (err) {
126
+ const normalizedError = err instanceof Error ? err : new Error("Unknown error");
127
+ pendingRequestCache.delete(imageUrl);
128
+ if (cancelled) return;
129
+ setError(normalizedError);
130
+ setGeneratedAlt(fallbackAlt);
131
+ if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);
132
+ } finally {
133
+ if (!cancelled) setIsGenerating(false);
134
+ }
135
+ };
136
+ generateCaption();
137
+ return () => {
138
+ cancelled = true;
139
+ };
140
+ }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
141
+ return { generatedAlt, isGenerating, error };
142
+ };
143
+
144
+ // src/next.tsx
145
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
146
+ var SR_ONLY_STYLE = {
147
+ position: "absolute",
148
+ width: "1px",
149
+ height: "1px",
150
+ padding: 0,
151
+ margin: "-1px",
152
+ overflow: "hidden",
153
+ clip: "rect(0, 0, 0, 0)",
154
+ whiteSpace: "nowrap",
155
+ borderWidth: 0
156
+ };
157
+ var SmartNextImage = ({
158
+ src,
159
+ alt,
160
+ apiEndpoint: propsEndpoint,
161
+ fallbackAlt = "Image loading or caption unavailable",
162
+ onCaptionGenerated,
163
+ onCaptionError,
164
+ disableAI: propsDisableAI,
165
+ announceLive = false,
166
+ ...props
167
+ }) => {
168
+ const { isGenerating, generatedAlt } = useAICaptions({
169
+ src,
170
+ alt,
171
+ apiEndpoint: propsEndpoint,
172
+ fallbackAlt,
173
+ onCaptionGenerated,
174
+ disableAI: propsDisableAI,
175
+ announceLive
176
+ });
177
+ 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 })
180
+ ] });
181
+ };
182
+ export {
183
+ SR_ONLY_STYLE,
184
+ SmartNextImage
185
+ };
186
+ //# sourceMappingURL=next.mjs.map