react-a11y-auto-caption 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # react-a11y-auto-caption
2
+
3
+ > A smart React & Next.js component <br/>
4
+ that automatically generates highly accurate `alt` text for images using AI.<br/>
5
+ **Generate captions effortlessly during local development, <br/>
6
+ save them to your database, and serve 100% accessible images in production<br/>
7
+ with zero API costs and zero latency.**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/react-a11y-auto-caption.svg)](https://www.npmjs.com/package/react-a11y-auto-caption)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+ [![Accessibility](https://img.shields.io/badge/Accessibility-100%25-brightgreen.svg)]()
12
+
13
+ ## Why use this library?
14
+
15
+ - **Generate Once, Serve Forever (Best Practice):** Automatically generate captions during local development, easily save them to your database using the `onCaptionGenerated` callback, and completely bypass AI requests in production.
16
+ - **Cost-Free Local AI Server:** Comes with a ready-to-use, lightweight FastAPI Python server. It runs perfectly on your local machine, meaning you don't need to pay for expensive cloud GPUs or third-party API subscriptions (like OpenAI).
17
+ - **Zero-Config Accessibility:** Automatically describes images for screen readers without manual data entry.
18
+ - **First-Class Next.js Support:** Provides a dedicated `<SmartNextImage>` component optimized for Next.js.
19
+ - **Smart Request Caching:** Built-in memory caching prevents duplicate API calls for the same image, saving your server costs.
20
+ - **Concurrent Request Defense:** Safely handles simultaneous renders of the same image across multiple components.
21
+ - **Global Provider Pattern:** Set your API endpoint once at the root level and forget about it.
22
+ - **Developer Experience (DX):** Built-in test mode (`disableAI`) and intelligent console warnings for smooth debugging.
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ npm
29
+ ```bash
30
+ npm install react-a11y-auto-caption
31
+ ```
32
+ yarn berry
33
+ ```bash
34
+ yarn add react-a11y-auto-caption
35
+ ```
36
+ pnpm
37
+ ```bash
38
+ pnpm add react-a11y-auto-caption
39
+ ```
40
+
41
+ ---
42
+ ## Quick Start
43
+
44
+ ### 1. Wrap your app with the Provider (Optional but Recommended) <br/>
45
+ Set your backend API endpoint once globally. <br/>
46
+ Otherwise, you can simply pass apiEndpoint as a prop when using `<SmartImage>` <br/>
47
+
48
+ ```ts
49
+ // App.tsx or layout.tsx
50
+ import { SmartImageProvider } from 'react-a11y-auto-caption';
51
+
52
+ export default function App({ children }) {
53
+ return (
54
+ <SmartImageProvider value={{ apiEndpoint: "https://your-api.com/api/generate-caption" }}>
55
+ {children}
56
+ </SmartImageProvider>
57
+ );
58
+ }
59
+ ```
60
+ ### 2. Use it in your components
61
+ For Vanilla React (Vite, CRA):<br/>
62
+ ```ts
63
+ import { SmartImage } from 'react-a11y-auto-caption';
64
+
65
+ function Gallery() {
66
+ return (
67
+ <SmartImage
68
+ src="[https://example.com/beautiful-landscape.jpg](https://example.com/beautiful-landscape.jpg)"
69
+ announceLive={true}
70
+ />
71
+ );
72
+ }
73
+ ```
74
+
75
+ For Next.js: <br/>
76
+ ```ts
77
+ import { SmartNextImage } from 'react-a11y-auto-caption';
78
+ import dogPic from '../public/dog.jpg';
79
+
80
+ function NextGallery() {
81
+ return (
82
+ <SmartNextImage
83
+ src={dogPic}
84
+ width={500}
85
+ height={300}
86
+ // You can override global settings per component
87
+ disableAI={process.env.NODE_ENV === 'development'}
88
+ />
89
+ );
90
+ }
91
+ ```
92
+ ---
93
+
94
+ ## API Reference
95
+ Both `SmartImage` and `SmartNextImage` inherit all standard `<img>` / `next/image` props, plus the following: <br/>
96
+ ## API Reference (Props)
97
+
98
+ Both `<SmartImage>` and `<SmartNextImage>` inherit all standard HTML `<img>` (or `next/image`) attributes. The following custom props are available:
99
+
100
+ | Prop | Type | Default | Description |
101
+ | :--- | :--- | :--- | :--- |
102
+ | `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider`'s endpoint if provided. |
103
+ | `alt` | `string` | `undefined` | Manual alt text. If provided, the AI generation is completely bypassed. |
104
+ | `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | The text displayed if the AI API request fails or times out. |
105
+ | `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Highly recommended for local development and testing. |
106
+ | `announceLive` | `boolean` | `false` | Enables `aria-live` regions to dynamically announce the generation status to screen readers. |
107
+ | `onCaptionGenerated`| `function` | `undefined` | Callback fired when a caption is successfully generated. `(caption: string) => void` |
108
+
109
+ > **Note:** If you are using `<SmartNextImage>`, you must also provide standard required Next.js image props such as `width` and `height` (unless using `fill`).
110
+ ---
111
+ ## Security & Backend Integration
112
+
113
+ This package requires a backend to process the images through an AI model (like ViT-GPT2). <br/>
114
+ We provide a ready-to-use FastAPI reference server in [this repository.](https://github.com/kong33/SmartImage)<br/>
115
+
116
+ Depending on your architecture, choose one of the following integration methods:<br/>
117
+ ---
118
+ ### First step: Deploying our Standalone AI Microservice
119
+ For security reasons, all cross-origin requests are blocked by default. <br/>
120
+ You MUST set the ALLOWED_ORIGINS environment variable in your server's .env file<br/>
121
+ to allow your frontend to communicate with it. <br/>
122
+
123
+
124
+ ```python
125
+ # For production
126
+
127
+ # .env file on your Python server
128
+ ALLOWED_ORIGINS=https://your-frontend-domain.com,https://your-frontend-domain2.com
129
+ ```
130
+ ```python
131
+ # For local development
132
+
133
+ # .env file on your Python server
134
+ ALLOWED_ORIGINS=http://localhost:3000
135
+ ```
136
+ > **Note:** Change the port if you are using Vite 5173 or another local server. You can allow multiple environments simultaneously by separating them with a comma (no spaces).
137
+
138
+ ---
139
+
140
+ ## Best Practice: Generate Once, Save, Reuse
141
+ Generating captions on-the-fly for every user is slow. <br/>
142
+ The industry standard is to generate the caption once and save it to your database:
143
+
144
+ ```tsx
145
+ <SmartImage
146
+ src={image.url}
147
+ alt={image.savedAlt} // If provided, the AI sleeps!
148
+ apiEndpoint="http://localhost:8000/api/generate-caption"
149
+ onCaptionGenerated={(text) => {
150
+ saveToDatabase(image.id, text); // Save permanently (You create your own database saving function based on your code.)
151
+ }}
152
+ />
@@ -0,0 +1,18 @@
1
+ import React, { ImgHTMLAttributes } from "react";
2
+ export declare const SmartImageProvider: React.FC<{
3
+ value: {
4
+ apiEndpoint?: string;
5
+ disableAI?: boolean;
6
+ };
7
+ children: React.ReactNode;
8
+ }>;
9
+ export interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {
10
+ src?: string;
11
+ apiEndpoint?: string;
12
+ fallbackAlt?: string;
13
+ onCaptionGenerated?: (caption: string) => void;
14
+ disableAI?: boolean;
15
+ announceLive?: boolean;
16
+ onCaptionError?: (error: Error) => void;
17
+ }
18
+ export declare const SmartImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, disableAI: propsDisableAI, announceLive, onCaptionError, ...props }: SmartImageProps) => import("react/jsx-runtime").JSX.Element;
package/dist/index.js ADDED
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SmartImage: () => SmartImage,
24
+ SmartImageProvider: () => SmartImageProvider
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/useAICaption.ts
29
+ var import_react = require("react");
30
+ var SmartImageContext = (0, import_react.createContext)(void 0);
31
+ var MAX_SIZE = 500;
32
+ var LRUCaptionCache = class {
33
+ constructor() {
34
+ this.cache = /* @__PURE__ */ new Map();
35
+ }
36
+ get(key) {
37
+ return this.cache.get(key);
38
+ }
39
+ set(key, value) {
40
+ if (this.cache.size >= MAX_SIZE) {
41
+ this.cache.delete(this.cache.keys().next().value);
42
+ }
43
+ this.cache.set(key, value);
44
+ }
45
+ has(key) {
46
+ return this.cache.has(key);
47
+ }
48
+ clear() {
49
+ this.cache.clear();
50
+ }
51
+ };
52
+ var captionCache = new LRUCaptionCache();
53
+ var pendingRequestCache = /* @__PURE__ */ new Map();
54
+ function isStaticImageData(src) {
55
+ return typeof src === "object" && src !== null && "src" in src;
56
+ }
57
+ function resolveImageUrl(src) {
58
+ if (typeof src === "string") return src;
59
+ if (isStaticImageData(src)) return src.src;
60
+ return src.default.src;
61
+ }
62
+ var log = process.env.NODE_ENV === "development" ? console.log : () => {
63
+ };
64
+ var useAICaptions = ({
65
+ src,
66
+ alt,
67
+ apiEndpoint: propsEndpoint,
68
+ fallbackAlt = "Image loading or caption unavailable",
69
+ onCaptionGenerated,
70
+ disableAI: propsDisableAI,
71
+ onCaptionError
72
+ }) => {
73
+ const context = (0, import_react.useContext)(SmartImageContext);
74
+ const apiEndpoint = propsEndpoint || context?.apiEndpoint;
75
+ const disableAI = propsDisableAI ?? context?.disableAI ?? false;
76
+ const [generatedAlt, setGeneratedAlt] = (0, import_react.useState)("");
77
+ const [isGenerating, setIsGenerating] = (0, import_react.useState)(false);
78
+ const [error, setError] = (0, import_react.useState)(null);
79
+ const onCaptionGeneratedRef = (0, import_react.useRef)(onCaptionGenerated);
80
+ const onCaptionErrorRef = (0, import_react.useRef)(onCaptionError);
81
+ (0, import_react.useEffect)(() => {
82
+ onCaptionGeneratedRef.current = onCaptionGenerated;
83
+ onCaptionErrorRef.current = onCaptionError;
84
+ });
85
+ (0, import_react.useEffect)(() => {
86
+ setError(null);
87
+ if (alt) {
88
+ setGeneratedAlt(alt);
89
+ return;
90
+ }
91
+ if (!src) return;
92
+ if (disableAI) {
93
+ setGeneratedAlt("[Testing mode: AI caption generation disabled]");
94
+ return;
95
+ }
96
+ if (!apiEndpoint) {
97
+ console.warn(
98
+ "[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation."
99
+ );
100
+ setGeneratedAlt(fallbackAlt);
101
+ return;
102
+ }
103
+ const imageUrl = resolveImageUrl(src);
104
+ if (captionCache.has(imageUrl)) {
105
+ log("[SmartImage] Cache hit: Reusing existing caption.");
106
+ const cachedCaption = captionCache.get(imageUrl);
107
+ setGeneratedAlt(cachedCaption);
108
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);
109
+ return;
110
+ }
111
+ let cancelled = false;
112
+ const generateCaption = async () => {
113
+ setIsGenerating(true);
114
+ try {
115
+ if (pendingRequestCache.has(imageUrl)) {
116
+ log("[SmartImage] Pending request detected. Waiting for the existing API call to complete.");
117
+ const caption = await pendingRequestCache.get(imageUrl);
118
+ if (cancelled) return;
119
+ setGeneratedAlt(caption);
120
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);
121
+ return;
122
+ }
123
+ const fetchPromise = (async () => {
124
+ const imageResponse = await fetch(imageUrl);
125
+ const imageBlob = await imageResponse.blob();
126
+ const imageFile = new File([imageBlob], "image.jpg", {
127
+ type: imageBlob.type || "image/jpeg"
128
+ });
129
+ const formData = new FormData();
130
+ formData.append("file", imageFile);
131
+ const response = await fetch(apiEndpoint, {
132
+ method: "POST",
133
+ body: formData
134
+ });
135
+ if (!response.ok) throw new Error("AI API request failed");
136
+ const data = await response.json();
137
+ if (data.caption) return data.caption;
138
+ throw new Error("No caption returned from the API.");
139
+ })();
140
+ pendingRequestCache.set(imageUrl, fetchPromise);
141
+ const newCaption = await fetchPromise;
142
+ pendingRequestCache.delete(imageUrl);
143
+ captionCache.set(imageUrl, newCaption);
144
+ if (cancelled) return;
145
+ setGeneratedAlt(newCaption);
146
+ if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);
147
+ } catch (err) {
148
+ const normalizedError = err instanceof Error ? err : new Error("Unknown error");
149
+ pendingRequestCache.delete(imageUrl);
150
+ if (cancelled) return;
151
+ setError(normalizedError);
152
+ setGeneratedAlt(fallbackAlt);
153
+ if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);
154
+ } finally {
155
+ if (!cancelled) setIsGenerating(false);
156
+ }
157
+ };
158
+ generateCaption();
159
+ return () => {
160
+ cancelled = true;
161
+ };
162
+ }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
163
+ return { generatedAlt, isGenerating, error };
164
+ };
165
+
166
+ // src/index.tsx
167
+ var import_jsx_runtime = require("react/jsx-runtime");
168
+ var SmartImageProvider = ({ value, children }) => {
169
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SmartImageContext.Provider, { value, children });
170
+ };
171
+ var SR_ONLY_STYLE = {
172
+ position: "absolute",
173
+ width: "1px",
174
+ height: "1px",
175
+ padding: 0,
176
+ margin: "-1px",
177
+ overflow: "hidden",
178
+ clip: "rect(0, 0, 0, 0)",
179
+ whiteSpace: "nowrap",
180
+ borderWidth: 0
181
+ };
182
+ var SmartImage = ({
183
+ src,
184
+ alt,
185
+ apiEndpoint: propsEndpoint,
186
+ fallbackAlt = "Image loading or caption unavailable",
187
+ onCaptionGenerated,
188
+ disableAI: propsDisableAI,
189
+ announceLive = false,
190
+ onCaptionError,
191
+ ...props
192
+ }) => {
193
+ const { isGenerating, generatedAlt } = useAICaptions({
194
+ src,
195
+ alt,
196
+ apiEndpoint: propsEndpoint,
197
+ fallbackAlt,
198
+ onCaptionGenerated,
199
+ disableAI: propsDisableAI,
200
+ announceLive,
201
+ onCaptionError
202
+ });
203
+ 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 })
206
+ ] });
207
+ };
208
+ // Annotate the CommonJS export names for ESM import in node:
209
+ 0 && (module.exports = {
210
+ SmartImage,
211
+ SmartImageProvider
212
+ });
213
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
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;