react-a11y-auto-caption 1.1.2 → 1.1.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/README.md CHANGED
@@ -1,170 +1,196 @@
1
- # react-a11y-auto-caption
2
-
3
- > AI-powered alt text generation component for React and Next.js images.
4
- > Generate captions during development, save them once, and reuse them in production for fast, accessible images.
5
-
6
- [![npm version](https://img.shields.io/npm/v/react-a11y-auto-caption.svg)](https://www.npmjs.com/package/react-a11y-auto-caption)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
- [![Accessibility](https://img.shields.io/badge/Accessibility-100%25-brightgreen.svg)]()
9
-
10
- ---
11
- ## Why use react-a11y-auto-caption?
12
-
13
- - **Generate once, reuse forever** — create captions during development, save them, and skip AI calls in production.
14
- - **Built for accessibility** — automatically provide meaningful alt text for screen readers.
15
- - **Works with React and Next.js** — includes both `<SmartImage>` and `<SmartNextImage>`.
16
- - **Bring your own backend** — use your own FastAPI, Flask, or Node caption API.
17
- - **Production-friendly** — caching, duplicate-request protection, and error handling are built in.
18
-
19
- ---
20
-
21
- ## Quick Start
22
-
23
- ### React
24
-
25
- ```tsx
26
- import { SmartImage } from 'react-a11y-auto-caption';
27
-
28
- export default function Demo() {
29
- return (
30
- <SmartImage
31
- src="https://example.com/image.jpg"
32
- apiEndpoint="http://localhost:8000/api/generate-caption"
33
- />
34
- );
35
- }
36
- ```
37
-
38
- ### Next.js
39
-
40
- ```tsx
41
- import { SmartNextImage } from 'react-a11y-auto-caption/next';
42
- import sampleImage from '../public/sample.jpg';
43
-
44
- export default function Demo() {
45
- return (
46
- <SmartNextImage
47
- src={sampleImage}
48
- width={500}
49
- height={300}
50
- apiEndpoint="http://localhost:8000/api/generate-caption"
51
- />
52
- );
53
- }
54
- ```
55
- > **Optional**: set the API endpoint once with a Provider
56
-
57
- ### Wrap your app with the Provider (Optional but Recommended)
58
-
59
- Set your backend API endpoint once globally. Otherwise, pass `apiEndpoint` directly as a prop.
60
-
61
- ```tsx
62
- // App.tsx or layout.tsx
63
- import { SmartImageProvider } from "react-a11y-auto-caption";
64
-
65
- export default function App({ children }) {
66
- return (
67
- <SmartImageProvider value={{ apiEndpoint: "https://your-api.com/api/generate-caption" }}>
68
- {children}
69
- </SmartImageProvider>
70
- );
71
- }
72
- ```
73
-
74
- ---
75
-
76
- ## Installation
77
-
78
- ```bash
79
- # npm
80
- npm install react-a11y-auto-caption
81
-
82
- # yarn
83
- yarn add react-a11y-auto-caption
84
-
85
- # pnpm
86
- pnpm add react-a11y-auto-caption
87
- ```
88
-
89
- ---
90
- ## Recommended workflow
91
-
92
- 1. Generate captions during local development with light [python server](https://github.com/kong33/SmartImage)
93
- 2. Save them to your database with `onCaptionGenerated`
94
- 3. Pass the saved `alt` text in production
95
- 4. Skip AI requests entirely for zero extra latency
96
-
97
- ---
98
-
99
- ## API Reference
100
-
101
- Both `<SmartImage>` and `<SmartNextImage>` inherit all standard HTML `<img>` (or `next/image`) attributes, plus the following:
102
-
103
- | Prop | Type | Default | Description |
104
- | :--- | :--- | :--- | :--- |
105
- | `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider` endpoint if provided. |
106
- | `alt` | `string` | `undefined` | Manual alt text. If provided, AI generation is completely bypassed. |
107
- | `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | Text used when the AI request fails or times out. |
108
- | `lazyGenerate` | `boolean` | `true` | Delays AI API calls until the image enters the viewport using `IntersectionObserver`.|
109
- | `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Recommended for testing. |
110
- | `announceLive` | `boolean` | `false` | Enables `aria-live` region to announce generation status to screen readers. |
111
- | `onCaptionGenerated` | `(caption: string) => void` | `undefined` | Callback fired when a caption is successfully generated. |
112
- | `onCaptionError` | `(error: Error) => void` | `undefined` | Callback fired when caption generation fails. Use for logging or toast notifications. |
113
-
114
- > **Note:** `<SmartNextImage>` requires standard Next.js image props such as `width` and `height` (unless using `fill`).
115
-
116
- ---
117
-
118
- ## Error Handling
119
-
120
- You can handle errors in two ways depending on your use case.
121
-
122
- **Via callback** — for external handling like logging or toasts:
123
-
124
- ```tsx
125
- <SmartImage
126
- src={imageUrl}
127
- onCaptionError={(err) => toast.error(err.message)}
128
- />
129
- ```
130
-
131
- **Via `useAICaptions` hook** — for UI branching:
132
-
133
- ```tsx
134
- const { generatedAlt, isGenerating, error } = useAICaptions({ src });
135
-
136
- if (error) return <p>Failed to generate caption.</p>;
137
- ```
138
-
139
- ---
140
-
141
- ## Security & Backend Integration
142
-
143
- This package requires a backend to process images through an AI model (e.g. ViT-GPT2). <br/>
144
- We provide a ready-to-use FastAPI reference server in [this repository](https://github.com/kong33/SmartImage).
145
-
146
- For security, all cross-origin requests are blocked by default. Set the `ALLOWED_ORIGINS` environment variable in your server's `.env` file:
147
-
148
- ```bash
149
- # Production
150
- ALLOWED_ORIGINS=https://your-frontend-domain.com
151
-
152
- # Local development
153
- ALLOWED_ORIGINS=http://localhost:3000
154
- ```
155
-
156
- > **Note:** Separate multiple origins with a comma and no spaces. Adjust the port if using Vite (`5173`) or another local server.
157
-
158
-
159
- ---
160
-
161
- ## What's New in v1.0.4
162
-
163
- - **LRU Cache:** Replaced unbounded `Map` cache with an LRU implementation to prevent memory leaks in long-running apps.
164
- - **`onCaptionError` callback:** New prop to handle caption generation failures externally (e.g. logging, toast notifications).
165
- - **`error` state:** `useAICaptions` hook now returns an `error` field so you can branch your UI on failure.
166
- - **Unmount safety:** Caption generation is now safely cancelled when a component unmounts, preventing stale state updates.
167
- - **Next.js static import support:** `<SmartNextImage>` now correctly resolves both `StaticImageData` and `StaticRequire` import types.
168
- - **Subpath exports:** Added `react-a11y-auto-caption/next` entry point for cleaner Next.js-specific imports.
169
-
170
-
1
+ # react-a11y-auto-caption
2
+
3
+ > AI-powered alt text generation component for React and Next.js images.
4
+ > Generate captions during development, save them once, and reuse them in production for fast, accessible images.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/react-a11y-auto-caption.svg)](https://www.npmjs.com/package/react-a11y-auto-caption)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Accessibility](https://img.shields.io/badge/Accessibility-100%25-brightgreen.svg)]()
9
+
10
+ ---
11
+ ## Why use react-a11y-auto-caption?
12
+
13
+ - **Generate once, reuse forever** — create captions during development, save them, and skip AI calls in production.
14
+ - **Built for accessibility** — automatically provide meaningful alt text for screen readers.
15
+ - **Works with React and Next.js** — includes both `<SmartImage>` and `<SmartNextImage>`.
16
+ - **Bring your own backend** — use your own FastAPI, Flask, or Node caption API.
17
+ - **Production-friendly** — caching, duplicate-request protection, and error handling are built in.
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ### React
24
+
25
+ ```tsx
26
+ import { SmartImage } from 'react-a11y-auto-caption';
27
+
28
+ export default function Demo() {
29
+ return (
30
+ <SmartImage
31
+ src="https://example.com/image.jpg"
32
+ apiEndpoint="http://localhost:8000/api/generate-caption"
33
+ />
34
+ );
35
+ }
36
+ ```
37
+
38
+ ### Next.js
39
+
40
+ ```tsx
41
+ import { SmartNextImage } from 'react-a11y-auto-caption/next';
42
+ import sampleImage from '../public/sample.jpg';
43
+
44
+ export default function Demo() {
45
+ return (
46
+ <SmartNextImage
47
+ src={sampleImage}
48
+ width={500}
49
+ height={300}
50
+ apiEndpoint="http://localhost:8000/api/generate-caption"
51
+ />
52
+ );
53
+ }
54
+ ```
55
+ > **Optional**: set the API endpoint once with a Provider
56
+
57
+ ### Wrap your app with the Provider (Optional but Recommended)
58
+
59
+ Set your backend API endpoint once globally. Otherwise, pass `apiEndpoint` directly as a prop.
60
+
61
+ ```tsx
62
+ // App.tsx or layout.tsx
63
+ import { SmartImageProvider } from "react-a11y-auto-caption";
64
+
65
+ export default function App({ children }) {
66
+ return (
67
+ <SmartImageProvider value={{ apiEndpoint: "https://your-api.com/api/generate-caption" }}>
68
+ {children}
69
+ </SmartImageProvider>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ # npm
80
+ npm install react-a11y-auto-caption
81
+
82
+ # yarn
83
+ yarn add react-a11y-auto-caption
84
+
85
+ # pnpm
86
+ pnpm add react-a11y-auto-caption
87
+ ```
88
+
89
+ ---
90
+ ## Recommended workflow
91
+
92
+ 1. Generate captions during local development with light [python server](https://github.com/kong33/SmartImage)
93
+ 2. Save them to your database with `onCaptionGenerated`
94
+ 3. Pass the saved `alt` text in production
95
+ 4. Skip AI requests entirely for zero extra latency
96
+
97
+ ---
98
+
99
+ ## API Reference
100
+
101
+ Both `<SmartImage>` and `<SmartNextImage>` inherit all standard HTML `<img>` (or `next/image`) attributes, plus the following:
102
+
103
+ | Prop | Type | Default | Description |
104
+ | :--- | :--- | :--- | :--- |
105
+ | `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider` endpoint if provided. |
106
+ | `alt` | `string` | `undefined` | Manual alt text. If provided, AI generation is completely bypassed. |
107
+ | `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | Text used when the AI request fails or times out. |
108
+ | `lazyGenerate` | `boolean` | `true` | Delays AI API calls until the image enters the viewport using `IntersectionObserver`.|
109
+ | `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Recommended for testing. |
110
+ | `announceLive` | `boolean` | `false` | Enables `aria-live` region to announce generation status to screen readers. |
111
+ | `onCaptionGenerated` | `(caption: string) => void` | `undefined` | Callback fired when a caption is successfully generated. |
112
+ | `onCaptionError` | `(error: Error) => void` | `undefined` | Callback fired when caption generation fails. Use for logging or toast notifications. |
113
+
114
+ > **Note:** `<SmartNextImage>` requires standard Next.js image props such as `width` and `height` (unless using `fill`).
115
+
116
+ ---
117
+
118
+ ## Error Handling
119
+
120
+ You can handle errors in two ways depending on your use case.
121
+
122
+ **Via callback** — for external handling like logging or toasts:
123
+
124
+ ```tsx
125
+ <SmartImage
126
+ src={imageUrl}
127
+ onCaptionError={(err) => toast.error(err.message)}
128
+ />
129
+ ```
130
+
131
+ **Via `useAICaptions` hook** — for UI branching:
132
+
133
+ ```tsx
134
+ const { generatedAlt, isGenerating, error } = useAICaptions({ src });
135
+
136
+ if (error) return <p>Failed to generate caption.</p>;
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Backend Integration
142
+
143
+ This package needs a caption API endpoint to generate alt text.
144
+
145
+ The easiest way is to run the official local server:
146
+
147
+ ```bash
148
+ npx react-a11y-auto-caption-server
149
+ ```
150
+
151
+ By default, the server runs at:
152
+
153
+ ```txt
154
+ http://127.0.0.1:8000/api/generate-caption
155
+ ```
156
+
157
+ Use it with `SmartImage` or `SmartNextImage`:
158
+
159
+ ```tsx
160
+ <SmartImage
161
+ src="/example.jpg"
162
+ apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
163
+ />
164
+ ```
165
+
166
+ You can also change the port:
167
+
168
+ ```bash
169
+ npx react-a11y-auto-caption-server --port 5000
170
+ ```
171
+
172
+ Then update the endpoint:
173
+
174
+ ```tsx
175
+ <SmartImage
176
+ src="/example.jpg"
177
+ apiEndpoint="http://127.0.0.1:5000/api/generate-caption"
178
+ />
179
+ ```
180
+
181
+ > First run may take a few minutes because the server creates a Python environment and installs AI dependencies.
182
+
183
+ For production, we recommend running your own caption server and applying CORS restrictions, file size limits, and rate limiting.
184
+
185
+ ---
186
+
187
+ ## What's New in v1.0.4
188
+
189
+ - **LRU Cache:** Replaced unbounded `Map` cache with an LRU implementation to prevent memory leaks in long-running apps.
190
+ - **`onCaptionError` callback:** New prop to handle caption generation failures externally (e.g. logging, toast notifications).
191
+ - **`error` state:** `useAICaptions` hook now returns an `error` field so you can branch your UI on failure.
192
+ - **Unmount safety:** Caption generation is now safely cancelled when a component unmounts, preventing stale state updates.
193
+ - **Next.js static import support:** `<SmartNextImage>` now correctly resolves both `StaticImageData` and `StaticRequire` import types.
194
+ - **Subpath exports:** Added `react-a11y-auto-caption/next` entry point for cleaner Next.js-specific imports.
195
+
196
+
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":"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","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":"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","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.4",
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",