react-a11y-auto-caption 1.1.5 → 1.1.7
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 +225 -78
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -2
- package/dist/index.mjs.map +1 -1
- package/dist/next.js +5 -2
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +5 -2
- package/dist/next.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# react-a11y-auto-caption
|
|
2
2
|
|
|
3
|
-
> AI-powered alt text generation component for React and Next.js images.
|
|
3
|
+
> AI-powered alt text generation component for React and Next.js images.
|
|
4
4
|
> Generate captions during development, save them once, and reuse them in production for fast, accessible images.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/react-a11y-auto-caption)
|
|
@@ -8,38 +8,83 @@
|
|
|
8
8
|
[]()
|
|
9
9
|
|
|
10
10
|
---
|
|
11
|
+
|
|
11
12
|
## Why use react-a11y-auto-caption?
|
|
12
13
|
|
|
13
14
|
- **Generate once, reuse forever** — create captions during development, save them, and skip AI calls in production.
|
|
14
15
|
- **Built for accessibility** — automatically provide meaningful alt text for screen readers.
|
|
15
16
|
- **Works with React and Next.js** — includes both `<SmartImage>` and `<SmartNextImage>`.
|
|
16
|
-
- **
|
|
17
|
-
- **Production-friendly** — caching, duplicate-request protection, and error handling are built in.
|
|
17
|
+
- **Local-first by default** — run the official caption server with `npx`.
|
|
18
|
+
- **Production-friendly** — caching, duplicate-request protection, lazy generation, and error handling are built in.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install react-a11y-auto-caption
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn add react-a11y-auto-caption
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add react-a11y-auto-caption
|
|
38
|
+
```
|
|
18
39
|
|
|
19
40
|
---
|
|
20
41
|
|
|
21
42
|
## Quick Start
|
|
22
43
|
|
|
23
|
-
###
|
|
44
|
+
### 1. Start the local caption server
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx react-a11y-auto-caption-server
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Default endpoint:
|
|
51
|
+
|
|
52
|
+
```txt
|
|
53
|
+
http://127.0.0.1:8000/api/generate-caption
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If port `8000` is unavailable, use another port:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx react-a11y-auto-caption-server --port 5000
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then use:
|
|
63
|
+
|
|
64
|
+
```txt
|
|
65
|
+
http://127.0.0.1:5000/api/generate-caption
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Use `SmartImage`
|
|
24
69
|
|
|
25
70
|
```tsx
|
|
26
|
-
import { SmartImage } from
|
|
71
|
+
import { SmartImage } from "react-a11y-auto-caption";
|
|
27
72
|
|
|
28
73
|
export default function Demo() {
|
|
29
74
|
return (
|
|
30
75
|
<SmartImage
|
|
31
|
-
src="
|
|
32
|
-
apiEndpoint="http://
|
|
76
|
+
src="/example.jpg"
|
|
77
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
33
78
|
/>
|
|
34
79
|
);
|
|
35
80
|
}
|
|
36
81
|
```
|
|
37
82
|
|
|
38
|
-
###
|
|
83
|
+
### 3. Use `SmartNextImage`
|
|
39
84
|
|
|
40
85
|
```tsx
|
|
41
|
-
import { SmartNextImage } from
|
|
42
|
-
import sampleImage from
|
|
86
|
+
import { SmartNextImage } from "react-a11y-auto-caption/next";
|
|
87
|
+
import sampleImage from "../public/sample.jpg";
|
|
43
88
|
|
|
44
89
|
export default function Demo() {
|
|
45
90
|
return (
|
|
@@ -47,159 +92,261 @@ export default function Demo() {
|
|
|
47
92
|
src={sampleImage}
|
|
48
93
|
width={500}
|
|
49
94
|
height={300}
|
|
50
|
-
apiEndpoint="http://
|
|
95
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
51
96
|
/>
|
|
52
97
|
);
|
|
53
98
|
}
|
|
54
99
|
```
|
|
55
|
-
> **Optional**: set the API endpoint once with a Provider
|
|
56
100
|
|
|
57
|
-
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Provider Setup
|
|
58
104
|
|
|
59
|
-
|
|
105
|
+
You can set the backend API endpoint once globally instead of passing `apiEndpoint` to every image.
|
|
60
106
|
|
|
61
107
|
```tsx
|
|
62
|
-
// App.tsx or layout.tsx
|
|
63
108
|
import { SmartImageProvider } from "react-a11y-auto-caption";
|
|
64
109
|
|
|
65
110
|
export default function App({ children }) {
|
|
66
111
|
return (
|
|
67
|
-
<SmartImageProvider
|
|
112
|
+
<SmartImageProvider
|
|
113
|
+
value={{
|
|
114
|
+
apiEndpoint: "http://127.0.0.1:8000/api/generate-caption",
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
68
117
|
{children}
|
|
69
118
|
</SmartImageProvider>
|
|
70
119
|
);
|
|
71
120
|
}
|
|
72
121
|
```
|
|
73
122
|
|
|
123
|
+
Then use `SmartImage` without repeating the endpoint:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<SmartImage src="/example.jpg" />
|
|
127
|
+
```
|
|
128
|
+
|
|
74
129
|
---
|
|
75
130
|
|
|
76
|
-
##
|
|
131
|
+
## Recommended Workflow
|
|
77
132
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
1. Run the local caption server with `npx react-a11y-auto-caption-server`
|
|
134
|
+
2. Generate captions during development
|
|
135
|
+
3. Save generated captions to your database with `onCaptionGenerated`
|
|
136
|
+
4. Pass the saved `alt` text in production
|
|
137
|
+
5. Skip AI requests entirely for faster production pages
|
|
81
138
|
|
|
82
|
-
|
|
83
|
-
yarn add react-a11y-auto-caption
|
|
139
|
+
Example:
|
|
84
140
|
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
```tsx
|
|
142
|
+
<SmartImage
|
|
143
|
+
src="/example.jpg"
|
|
144
|
+
alt={savedAlt || undefined}
|
|
145
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
146
|
+
onCaptionGenerated={(caption) => {
|
|
147
|
+
saveAltTextToDatabase(caption);
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
87
150
|
```
|
|
88
151
|
|
|
152
|
+
> If `alt` is provided, AI generation is bypassed. This is intentional so saved captions can be reused in production.
|
|
153
|
+
|
|
89
154
|
---
|
|
90
|
-
## Recommended workflow
|
|
91
155
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
156
|
+
## Public Demo Server
|
|
157
|
+
|
|
158
|
+
You can test the package with the public demo server:
|
|
159
|
+
|
|
160
|
+
```txt
|
|
161
|
+
https://kong3333-react-a11y-auto-caption-server.hf.space/api/generate-caption
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<SmartImage
|
|
168
|
+
src="/example.jpg"
|
|
169
|
+
apiEndpoint="https://kong3333-react-a11y-auto-caption-server.hf.space/api/generate-caption"
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
> The public demo server is for testing only. It may be slow, paused, or unavailable depending on free-tier limits.
|
|
174
|
+
> For production, run your own caption server.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Backend Integration
|
|
179
|
+
|
|
180
|
+
This package needs a caption API endpoint that accepts an image file and returns a caption.
|
|
181
|
+
|
|
182
|
+
The official local server is the easiest option:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npx react-a11y-auto-caption-server
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Default endpoint:
|
|
189
|
+
|
|
190
|
+
```txt
|
|
191
|
+
http://127.0.0.1:8000/api/generate-caption
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Custom port:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npx react-a11y-auto-caption-server --port 5000
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Custom endpoint:
|
|
201
|
+
|
|
202
|
+
```txt
|
|
203
|
+
http://127.0.0.1:5000/api/generate-caption
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The caption server automatically allows local development origins such as:
|
|
207
|
+
|
|
208
|
+
```txt
|
|
209
|
+
http://localhost:<any-port>
|
|
210
|
+
http://127.0.0.1:<any-port>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
For production or internal company servers, configure `ALLOWED_ORIGINS` on the server side.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
|
|
217
|
+
```env
|
|
218
|
+
ALLOWED_ORIGINS=https://your-frontend-domain.com,http://localhost:3000
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
If your frontend runs locally but the caption server runs on another machine, your frontend endpoint should point to that machine:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<SmartImage
|
|
225
|
+
src="/example.jpg"
|
|
226
|
+
apiEndpoint="http://192.168.0.20:8000/api/generate-caption"
|
|
227
|
+
/>
|
|
228
|
+
```
|
|
96
229
|
|
|
97
230
|
---
|
|
98
231
|
|
|
99
232
|
## API Reference
|
|
100
233
|
|
|
101
|
-
Both `<SmartImage>` and `<SmartNextImage>` inherit
|
|
234
|
+
Both `<SmartImage>` and `<SmartNextImage>` inherit standard HTML `<img>` or `next/image` props, plus the following:
|
|
102
235
|
|
|
103
236
|
| Prop | Type | Default | Description |
|
|
104
237
|
| :--- | :--- | :--- | :--- |
|
|
105
238
|
| `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider` endpoint if provided. |
|
|
106
239
|
| `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
|
|
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.
|
|
110
|
-
| `announceLive` | `boolean` | `false` | Enables `aria-live` region to announce generation status to screen readers. |
|
|
240
|
+
| `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | Text used when the AI request fails. |
|
|
241
|
+
| `lazyGenerate` | `boolean` | `true` | Delays AI API calls until the image enters the viewport using `IntersectionObserver`. |
|
|
242
|
+
| `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Useful for tests. |
|
|
243
|
+
| `announceLive` | `boolean` | `false` | Enables an `aria-live` region to announce generation status to screen readers. |
|
|
111
244
|
| `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.
|
|
245
|
+
| `onCaptionError` | `(error: Error) => void` | `undefined` | Callback fired when caption generation fails. Useful for logging or toast notifications. |
|
|
113
246
|
|
|
114
|
-
>
|
|
247
|
+
> `<SmartNextImage>` requires standard Next.js image props such as `width` and `height`, unless using `fill`.
|
|
115
248
|
|
|
116
249
|
---
|
|
117
250
|
|
|
118
251
|
## Error Handling
|
|
119
252
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
**Via callback** — for external handling like logging or toasts:
|
|
253
|
+
Use `onCaptionError` for logging, toast messages, or debugging.
|
|
123
254
|
|
|
124
255
|
```tsx
|
|
125
256
|
<SmartImage
|
|
126
|
-
src=
|
|
127
|
-
|
|
257
|
+
src="/example.jpg"
|
|
258
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
259
|
+
onCaptionError={(error) => {
|
|
260
|
+
console.error("Caption generation failed:", error);
|
|
261
|
+
}}
|
|
128
262
|
/>
|
|
129
263
|
```
|
|
130
264
|
|
|
131
|
-
|
|
265
|
+
You can also use the hook directly:
|
|
132
266
|
|
|
133
267
|
```tsx
|
|
134
|
-
const { generatedAlt, isGenerating, error } = useAICaptions({
|
|
268
|
+
const { generatedAlt, isGenerating, error } = useAICaptions({
|
|
269
|
+
src: "/example.jpg",
|
|
270
|
+
apiEndpoint: "http://127.0.0.1:8000/api/generate-caption",
|
|
271
|
+
});
|
|
135
272
|
|
|
136
273
|
if (error) return <p>Failed to generate caption.</p>;
|
|
137
274
|
```
|
|
138
275
|
|
|
139
276
|
---
|
|
140
277
|
|
|
141
|
-
##
|
|
278
|
+
## Troubleshooting
|
|
142
279
|
|
|
143
|
-
|
|
280
|
+
### API request does not run
|
|
144
281
|
|
|
145
|
-
|
|
282
|
+
Check that:
|
|
146
283
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
284
|
+
- `apiEndpoint` points to `/api/generate-caption`
|
|
285
|
+
- the caption server is running
|
|
286
|
+
- the frontend endpoint uses the same port as the server
|
|
287
|
+
- `alt` is not already provided if you expect AI generation
|
|
288
|
+
- `lazyGenerate={false}` is used while debugging viewport-related issues
|
|
289
|
+
- the latest package version is installed
|
|
150
290
|
|
|
151
|
-
|
|
152
|
-
> For production, please run your own caption server.
|
|
291
|
+
Debug example:
|
|
153
292
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
293
|
+
```tsx
|
|
294
|
+
<SmartImage
|
|
295
|
+
src="/example.jpg"
|
|
296
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
297
|
+
lazyGenerate={false}
|
|
298
|
+
onCaptionGenerated={(caption) => console.log("Generated:", caption)}
|
|
299
|
+
onCaptionError={(error) => console.error("Caption error:", error)}
|
|
300
|
+
/>
|
|
158
301
|
```
|
|
159
302
|
|
|
160
|
-
|
|
303
|
+
### Port 8000 is unavailable
|
|
161
304
|
|
|
162
|
-
|
|
163
|
-
|
|
305
|
+
Run the server on another port:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npx react-a11y-auto-caption-server --port 8001
|
|
164
309
|
```
|
|
165
310
|
|
|
166
|
-
|
|
311
|
+
Then update your frontend:
|
|
167
312
|
|
|
168
313
|
```tsx
|
|
169
314
|
<SmartImage
|
|
170
315
|
src="/example.jpg"
|
|
171
|
-
apiEndpoint="http://127.0.0.1:
|
|
316
|
+
apiEndpoint="http://127.0.0.1:8001/api/generate-caption"
|
|
172
317
|
/>
|
|
173
318
|
```
|
|
174
319
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
npx react-a11y-auto-caption-server --port 5000
|
|
179
|
-
```
|
|
320
|
+
### External image URLs
|
|
180
321
|
|
|
181
|
-
|
|
322
|
+
If an external image URL fails before reaching the caption server, try a local image first:
|
|
182
323
|
|
|
183
324
|
```tsx
|
|
184
325
|
<SmartImage
|
|
185
|
-
src="/
|
|
186
|
-
apiEndpoint="http://127.0.0.1:
|
|
326
|
+
src="/sample.jpg"
|
|
327
|
+
apiEndpoint="http://127.0.0.1:8000/api/generate-caption"
|
|
187
328
|
/>
|
|
188
329
|
```
|
|
189
330
|
|
|
190
|
-
|
|
331
|
+
Some external image hosts block browser-side `fetch()` access even if the image displays correctly in an `<img>` tag.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## What's New
|
|
191
336
|
|
|
192
|
-
|
|
337
|
+
- Fixed lazy generation so captions are triggered correctly after images enter the viewport.
|
|
338
|
+
- Added official `npx react-a11y-auto-caption-server` workflow.
|
|
339
|
+
- Added custom backend server port support.
|
|
340
|
+
- Added public demo server option for quick testing.
|
|
193
341
|
|
|
194
342
|
---
|
|
195
343
|
|
|
196
|
-
##
|
|
344
|
+
## Related
|
|
345
|
+
|
|
346
|
+
- [`react-a11y-auto-caption-server`](https://www.npmjs.com/package/react-a11y-auto-caption-server)
|
|
347
|
+
|
|
348
|
+
---
|
|
197
349
|
|
|
198
|
-
|
|
199
|
-
- **`onCaptionError` callback:** New prop to handle caption generation failures externally (e.g. logging, toast notifications).
|
|
200
|
-
- **`error` state:** `useAICaptions` hook now returns an `error` field so you can branch your UI on failure.
|
|
201
|
-
- **Unmount safety:** Caption generation is now safely cancelled when a component unmounts, preventing stale state updates.
|
|
202
|
-
- **Next.js static import support:** `<SmartNextImage>` now correctly resolves both `StaticImageData` and `StaticRequire` import types.
|
|
203
|
-
- **Subpath exports:** Added `react-a11y-auto-caption/next` entry point for cleaner Next.js-specific imports.
|
|
350
|
+
## License
|
|
204
351
|
|
|
205
|
-
|
|
352
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -101,6 +101,9 @@ var useAICaptions = ({
|
|
|
101
101
|
const timer = setTimeout(() => setAnnouncemet(""), 5e3);
|
|
102
102
|
return () => clearTimeout(timer);
|
|
103
103
|
}, [announcement]);
|
|
104
|
+
(0, import_react.useEffect)(() => {
|
|
105
|
+
setShouldGenerate(!lazyGenerate);
|
|
106
|
+
}, [src, lazyGenerate]);
|
|
104
107
|
(0, import_react.useEffect)(() => {
|
|
105
108
|
if (!lazyGenerate || !imgRef.current) return;
|
|
106
109
|
const observer = new IntersectionObserver(
|
|
@@ -114,7 +117,7 @@ var useAICaptions = ({
|
|
|
114
117
|
);
|
|
115
118
|
observer.observe(imgRef.current);
|
|
116
119
|
return () => observer.disconnect();
|
|
117
|
-
}, [lazyGenerate]);
|
|
120
|
+
}, [lazyGenerate, src]);
|
|
118
121
|
(0, import_react.useEffect)(() => {
|
|
119
122
|
setError(null);
|
|
120
123
|
if (alt) {
|
|
@@ -192,7 +195,7 @@ var useAICaptions = ({
|
|
|
192
195
|
return () => {
|
|
193
196
|
cancelled = true;
|
|
194
197
|
};
|
|
195
|
-
}, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
|
|
198
|
+
}, [src, alt, apiEndpoint, fallbackAlt, disableAI, shouldGenerate]);
|
|
196
199
|
return { generatedAlt, isGenerating, error, imgRef, announcement };
|
|
197
200
|
};
|
|
198
201
|
|
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\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":[]}
|
|
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 setShouldGenerate(!lazyGenerate);\r\n }, [src, lazyGenerate]);\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, src])\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, shouldGenerate]);\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
|
@@ -72,6 +72,9 @@ var useAICaptions = ({
|
|
|
72
72
|
const timer = setTimeout(() => setAnnouncemet(""), 5e3);
|
|
73
73
|
return () => clearTimeout(timer);
|
|
74
74
|
}, [announcement]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setShouldGenerate(!lazyGenerate);
|
|
77
|
+
}, [src, lazyGenerate]);
|
|
75
78
|
useEffect(() => {
|
|
76
79
|
if (!lazyGenerate || !imgRef.current) return;
|
|
77
80
|
const observer = new IntersectionObserver(
|
|
@@ -85,7 +88,7 @@ var useAICaptions = ({
|
|
|
85
88
|
);
|
|
86
89
|
observer.observe(imgRef.current);
|
|
87
90
|
return () => observer.disconnect();
|
|
88
|
-
}, [lazyGenerate]);
|
|
91
|
+
}, [lazyGenerate, src]);
|
|
89
92
|
useEffect(() => {
|
|
90
93
|
setError(null);
|
|
91
94
|
if (alt) {
|
|
@@ -163,7 +166,7 @@ var useAICaptions = ({
|
|
|
163
166
|
return () => {
|
|
164
167
|
cancelled = true;
|
|
165
168
|
};
|
|
166
|
-
}, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
|
|
169
|
+
}, [src, alt, apiEndpoint, fallbackAlt, disableAI, shouldGenerate]);
|
|
167
170
|
return { generatedAlt, isGenerating, error, imgRef, announcement };
|
|
168
171
|
};
|
|
169
172
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -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, 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":[]}
|
|
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 setShouldGenerate(!lazyGenerate);\r\n }, [src, lazyGenerate]);\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, src])\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, shouldGenerate]);\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.js
CHANGED
|
@@ -111,6 +111,9 @@ var useAICaptions = ({
|
|
|
111
111
|
const timer = setTimeout(() => setAnnouncemet(""), 5e3);
|
|
112
112
|
return () => clearTimeout(timer);
|
|
113
113
|
}, [announcement]);
|
|
114
|
+
(0, import_react.useEffect)(() => {
|
|
115
|
+
setShouldGenerate(!lazyGenerate);
|
|
116
|
+
}, [src, lazyGenerate]);
|
|
114
117
|
(0, import_react.useEffect)(() => {
|
|
115
118
|
if (!lazyGenerate || !imgRef.current) return;
|
|
116
119
|
const observer = new IntersectionObserver(
|
|
@@ -124,7 +127,7 @@ var useAICaptions = ({
|
|
|
124
127
|
);
|
|
125
128
|
observer.observe(imgRef.current);
|
|
126
129
|
return () => observer.disconnect();
|
|
127
|
-
}, [lazyGenerate]);
|
|
130
|
+
}, [lazyGenerate, src]);
|
|
128
131
|
(0, import_react.useEffect)(() => {
|
|
129
132
|
setError(null);
|
|
130
133
|
if (alt) {
|
|
@@ -202,7 +205,7 @@ var useAICaptions = ({
|
|
|
202
205
|
return () => {
|
|
203
206
|
cancelled = true;
|
|
204
207
|
};
|
|
205
|
-
}, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
|
|
208
|
+
}, [src, alt, apiEndpoint, fallbackAlt, disableAI, shouldGenerate]);
|
|
206
209
|
return { generatedAlt, isGenerating, error, imgRef, announcement };
|
|
207
210
|
};
|
|
208
211
|
|
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 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"]}
|
|
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 setShouldGenerate(!lazyGenerate);\r\n }, [src, lazyGenerate]);\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, src])\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, shouldGenerate]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAAkC;;;ACDlC,mBAAuE;AAmBhE,IAAM,wBAAoB,4BAAkD,MAAS;AAE5F,IAAM,WAAW;AAEjB,IAAM,kBAAN,MAAsB;AAAA,EAAtB;AACE,SAAQ,QAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,IAAI,KAAiC;AACnC,QAAI,CAAC,KAAK,MAAM,IAAI,GAAG,EAAG,QAAO;AAEjC,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,MAAM,IAAI,KAAK,KAAK;AACzB,WAAO;AAAA,EACT;AAAA,EACA,IAAI,KAAa,OAAe;AAC9B,QAAI,KAAK,MAAM,IAAI,GAAG,GAAG;AACvB,WAAK,MAAM,OAAO,GAAG;AAAA,IACvB;AACA,QAAI,KAAK,MAAM,QAAQ,UAAU;AAC/B,WAAK,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IACnD;AACA,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AAAA,EACA,IAAI,KAAa;AACf,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EACA,QAAQ;AACN,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;AAEA,IAAM,eAAe,IAAI,gBAAgB;AACzC,IAAM,sBAAsB,oBAAI,IAA6B;AAE7D,SAAS,kBAAkB,KAAoD;AAC7E,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,SAAS;AAC7D;AAEA,SAAS,gBAAgB,KAAoC;AAC3D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,kBAAkB,GAAG,EAAG,QAAO,IAAI;AACvC,SAAO,IAAI,QAAQ;AACrB;AAEA,IAAM,MAAM,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,MAAM,MAAM;AAAC;AAEnE,IAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,eAAe;AAAA,EACf,eAAe;AACjB,MAA2B;AACzB,QAAM,cAAU,yBAAW,iBAAiB;AAE5C,QAAM,cAAc,iBAAiB,SAAS;AAC9C,QAAM,YAAY,kBAAkB,SAAS,aAAa;AAE1D,QAAM,CAAC,cAAc,eAAe,QAAI,uBAAS,EAAE;AACnD,QAAM,CAAC,cAAc,eAAe,QAAI,uBAAS,KAAK;AACtD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAuB,IAAI;AACrD,QAAM,CAAC,cAAc,cAAc,QAAI,uBAAS,EAAE;AAClD,QAAM,CAAC,gBAAgB,iBAAiB,QAAI,uBAAS,CAAC,YAAY;AAClE,QAAM,4BAAwB,qBAAO,kBAAkB;AACvD,QAAM,wBAAoB,qBAAO,cAAc;AAE/C,QAAM,aAAS,qBAAkC,IAAI;AAGrD,8BAAU,MAAM;AACd,0BAAsB,UAAU;AAChC,sBAAkB,UAAU;AAAA,EAC9B,CAAC;AAED,8BAAU,MAAM;AACd,QAAI,CAAC,aAAc;AACnB,UAAM,QAAQ,WAAW,MAAM,eAAe,EAAE,GAAG,GAAI;AACvD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,YAAY,CAAC;AAEjB,8BAAU,MAAM;AAChB,sBAAkB,CAAC,YAAY;AAAA,EAC/B,GAAG,CAAC,KAAK,YAAY,CAAC;AAEtB,8BAAU,MAAM;AACd,QAAI,CAAC,gBAAgB,CAAC,OAAO,QAAS;AAEtC,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,CAAC,KAAK,MAAM;AACX,YAAI,MAAM,gBAAgB;AACxB,4BAAkB,IAAI;AACtB,mBAAS,WAAW;AAAA,QACtB;AAAA,MACF;AAAA,MACA,EAAE,YAAY,QAAQ;AAAA,IACxB;AACA,aAAS,QAAQ,OAAO,OAAO;AAC/B,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,cAAc,GAAG,CAAC;AAGtB,8BAAU,MAAM;AACd,aAAS,IAAI;AAEb,QAAI,KAAK;AACP,sBAAgB,GAAG;AACnB;AAAA,IACF;AAEA,QAAI,CAAC,OAAO,CAAC,eAAgB;AAE7B,QAAI,WAAW;AACb,sBAAgB,gDAAgD;AAChE;AAAA,IACF;AAEA,QAAI,CAAC,aAAa;AAChB,cAAQ;AAAA,QACN;AAAA,MACF;AACA,sBAAgB,WAAW;AAC3B;AAAA,IACF;AAEA,UAAM,WAAW,gBAAgB,GAAG;AAEpC,QAAI,aAAa,IAAI,QAAQ,GAAG;AAC9B,UAAI,mDAAmD;AACvD,YAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,sBAAgB,aAAa;AAC7B,UAAI,sBAAsB,QAAS,uBAAsB,QAAQ,aAAa;AAC9E;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,UAAM,kBAAkB,YAAY;AAClC,sBAAgB,IAAI;AACpB,UAAI;AACF,YAAI,oBAAoB,IAAI,QAAQ,GAAG;AACrC,cAAI,uFAAuF;AAC3F,gBAAM,UAAU,MAAM,oBAAoB,IAAI,QAAQ;AACtD,cAAI,UAAW;AACf,0BAAgB,OAAO;AACvB,cAAI,sBAAsB,QAAS,uBAAsB,QAAQ,OAAO;AACxE;AAAA,QACF;AAEA,cAAM,gBAAgB,YAAY;AAChC,gBAAM,gBAAgB,MAAM,MAAM,QAAQ;AAC1C,gBAAM,YAAY,MAAM,cAAc,KAAK;AAC3C,gBAAM,YAAY,IAAI,KAAK,CAAC,SAAS,GAAG,aAAa;AAAA,YACnD,MAAM,UAAU,QAAQ;AAAA,UAC1B,CAAC;AACD,gBAAM,WAAW,IAAI,SAAS;AAC9B,mBAAS,OAAO,QAAQ,SAAS;AAEjC,gBAAM,WAAW,MAAM,MAAM,aAAa;AAAA,YACxC,QAAQ;AAAA,YACR,MAAM;AAAA,UACR,CAAC;AAED,cAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,uBAAuB;AACzD,gBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,gBAAM,IAAI,MAAM,mCAAmC;AAAA,QACrD,GAAG;AAEH,4BAAoB,IAAI,UAAU,YAAY;AAE9C,cAAM,aAAa,MAAM;AACzB,4BAAoB,OAAO,QAAQ;AACnC,qBAAa,IAAI,UAAU,UAAU;AAErC,YAAI,UAAW;AACf,wBAAgB,UAAU;AAC1B,YAAI,sBAAsB,QAAS,uBAAsB,QAAQ,UAAU;AAAA,MAC7E,SAAS,KAAK;AACZ,cAAM,kBAAkB,eAAe,QAAQ,MAAM,IAAI,MAAM,eAAe;AAC9E,4BAAoB,OAAO,QAAQ;AACnC,YAAI,UAAW;AACf,iBAAS,eAAe;AACxB,wBAAgB,WAAW;AAC3B,YAAI,kBAAkB,QAAS,mBAAkB,QAAQ,eAAe;AAAA,MAC1E,UAAE;AACA,YAAI,CAAC,UAAW,iBAAgB,KAAK;AAAA,MACvC;AAAA,IACF;AAEA,oBAAgB;AAEhB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,KAAK,KAAK,aAAa,aAAa,WAAW,cAAc,CAAC;AAElE,SAAO,EAAE,cAAc,cAAc,OAAO,QAAQ,aAAa;AACnE;;;AD5KI;AAlCG,IAAM,gBAAqC;AAAA,EAChD,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,aAAa;AACf;AACO,IAAM,iBAAiB,CAAC;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf,eAAe;AAAA,EACf,GAAG;AACL,MAA2B;AACzB,QAAM,EAAE,cAAc,cAAc,cAAc,OAAO,IAAI,cAAc;AAAA,IACzE;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF,CAAC;AACD,SACE,4EACG;AAAA,oBACC,4CAAC,UAAK,OAAO,eAAe,aAAU,UAAS,eAAY,QACxD,wBACH;AAAA,IAGF,4CAAC,aAAAA,SAAA,EAAM,KAAU,KAAK,QAAQ,KAAK,gBAAgB,aAAa,aAAW,cAAe,GAAG,OAAO;AAAA,KACtG;AAEJ;","names":["Image"]}
|
package/dist/next.mjs
CHANGED
|
@@ -77,6 +77,9 @@ var useAICaptions = ({
|
|
|
77
77
|
const timer = setTimeout(() => setAnnouncemet(""), 5e3);
|
|
78
78
|
return () => clearTimeout(timer);
|
|
79
79
|
}, [announcement]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setShouldGenerate(!lazyGenerate);
|
|
82
|
+
}, [src, lazyGenerate]);
|
|
80
83
|
useEffect(() => {
|
|
81
84
|
if (!lazyGenerate || !imgRef.current) return;
|
|
82
85
|
const observer = new IntersectionObserver(
|
|
@@ -90,7 +93,7 @@ var useAICaptions = ({
|
|
|
90
93
|
);
|
|
91
94
|
observer.observe(imgRef.current);
|
|
92
95
|
return () => observer.disconnect();
|
|
93
|
-
}, [lazyGenerate]);
|
|
96
|
+
}, [lazyGenerate, src]);
|
|
94
97
|
useEffect(() => {
|
|
95
98
|
setError(null);
|
|
96
99
|
if (alt) {
|
|
@@ -168,7 +171,7 @@ var useAICaptions = ({
|
|
|
168
171
|
return () => {
|
|
169
172
|
cancelled = true;
|
|
170
173
|
};
|
|
171
|
-
}, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
|
|
174
|
+
}, [src, alt, apiEndpoint, fallbackAlt, disableAI, shouldGenerate]);
|
|
172
175
|
return { generatedAlt, isGenerating, error, imgRef, announcement };
|
|
173
176
|
};
|
|
174
177
|
|
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 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
|
+
{"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 setShouldGenerate(!lazyGenerate);\r\n }, [src, lazyGenerate]);\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, src])\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, shouldGenerate]);\r\n\r\n return { generatedAlt, isGenerating, error, imgRef, announcement };\r\n};\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED