wc-img-ai 0.2.2 → 0.3.1
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 +133 -25
- package/dist/wc-img-ai.js +208 -131
- package/package.json +15 -9
- package/types/ai-img.d.ts +28 -8
- package/types/get-generated-image.d.ts +25 -1
package/README.md
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wc-img-ai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI-generated images as a web component. Drop an `<ai-img>` anywhere, give it a
|
|
4
|
+
`prompt` and the URL of your own server endpoint, and it renders an
|
|
5
|
+
AI-generated image — while the API key, image generation and storage all stay
|
|
6
|
+
on the server.
|
|
4
7
|
|
|
5
8
|
```html
|
|
6
9
|
<ai-img
|
|
7
|
-
|
|
10
|
+
endpoint="/api/img"
|
|
8
11
|
width="256"
|
|
9
12
|
height="256"
|
|
10
|
-
prompt="funny dolphin up to no good"
|
|
11
|
-
fallback="https://
|
|
13
|
+
prompt="a funny dolphin up to no good"
|
|
14
|
+
fallback="https://placehold.co/256x256"
|
|
12
15
|
></ai-img>
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
##
|
|
18
|
+
## Why a server endpoint?
|
|
19
|
+
|
|
20
|
+
The component is deliberately dumb: it never holds an API key, never picks a
|
|
21
|
+
provider, and never talks to OpenAI/Gemini directly. It sends a single POST to
|
|
22
|
+
**your** endpoint, and your server decides what to do. That keeps tokens
|
|
23
|
+
server-side and lets you store, cache and bill generation however you like.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
16
26
|
|
|
17
27
|
```bash
|
|
18
|
-
pnpm
|
|
28
|
+
pnpm add wc-img-ai
|
|
19
29
|
```
|
|
20
30
|
|
|
21
31
|
```html
|
|
@@ -24,36 +34,134 @@ pnpm i wc-img-ai
|
|
|
24
34
|
</script>
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
## Attributes
|
|
38
|
+
|
|
39
|
+
| Attribute | Reflected | Description |
|
|
40
|
+
| ----------- | --------- | --------------------------------------------------------------------------- |
|
|
41
|
+
| `src` | — | A ready image URL (or data URL). When set, the component acts as a plain `<img>` and never calls the endpoint. Use it when you already have the image. Highest priority. |
|
|
42
|
+
| `endpoint` | — | Your server route. Receives the POST below. |
|
|
43
|
+
| `prompt` | — | Description used to generate the image. |
|
|
44
|
+
| `image-id` | on mint | Storage handle. Provide a known id to fetch a stored image; the server sets it on the element when a new image is minted. |
|
|
45
|
+
| `llm` | — | Provider/model hint forwarded to the endpoint (e.g. `gemini`, `openai`). |
|
|
46
|
+
| `ratio` | — | Aspect ratio forwarded to the endpoint (e.g. `16:9`, `4:1`). With `width`, it derives the effective height. |
|
|
47
|
+
| `fallback` | — | Image URL shown if nothing resolves. If omitted, a 1×1 transparent PNG is used. |
|
|
48
|
+
| `width` | ✅ yes | Intrinsic width (like `<img width>`) — used for the box aspect-ratio and sent to the endpoint. |
|
|
49
|
+
| `height` | — | Optional intrinsic height (like `<img height>`); derived when omitted and `width`/`ratio` are set. |
|
|
50
|
+
| `alt` | ✅ yes | Alt text, passed to the inner `<img>`. |
|
|
51
|
+
|
|
52
|
+
### Sizing & styling — just like a native `<img>`
|
|
53
|
+
|
|
54
|
+
Set `width` plus `ratio`, or the native-image-compatible `width`/`height` pair,
|
|
55
|
+
and style with `class`/CSS; you rarely need inline `style`. The effective
|
|
56
|
+
dimensions become the inner image's content attributes, so the browser reserves
|
|
57
|
+
the box from their aspect-ratio (no layout shift) while CSS controls the
|
|
58
|
+
displayed size:
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<!-- full-width 3:1 banner, rounded, no layout shift -->
|
|
62
|
+
<ai-img endpoint="/api/img" prompt="…" width="1536" height="512"
|
|
63
|
+
class="block w-full rounded-xl"></ai-img>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
When `ratio` and `width` are set, `height` is optional and the component derives
|
|
67
|
+
it. An explicit `height` remains authoritative for callers that need an exact
|
|
68
|
+
output box:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<ai-img endpoint="/api/img" prompt="…" width="1536" ratio="3:1"
|
|
72
|
+
class="block w-full rounded-xl"></ai-img>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Visual properties bridge the shadow boundary via `inherit`, so utility classes
|
|
76
|
+
like `rounded-xl`, `object-cover`, `shadow-lg` on `<ai-img>` style the image.
|
|
77
|
+
Any other attribute (e.g. `loading`) is passed through to the inner `<img>`.
|
|
78
|
+
|
|
79
|
+
## How it resolves
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src set → render it as a plain <img> (no endpoint call)
|
|
83
|
+
else no prompt, no image-id → fallback → 1×1 transparent PNG (nothing to ask)
|
|
84
|
+
else → POST endpoint once { prompt, imageId, width, height, llm, ratio }
|
|
85
|
+
200 {id,url} → render url, reflect image-id, fire `ai-image` event
|
|
86
|
+
404 / error → fallback → 1×1 transparent PNG
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`src` is the cheapest path: if you already have the image (a precomputed or
|
|
90
|
+
returning one), set `src` and the component skips the AI entirely. Only when
|
|
91
|
+
`src` is empty does it fall back to the `prompt`/`image-id` flow.
|
|
92
|
+
|
|
93
|
+
> **Performance note.** For an image you _already have the URL for_, a plain
|
|
94
|
+
> `<img src>` will paint faster than `<ai-img src>` — the web component can't
|
|
95
|
+
> render until its own script has loaded and defined the element, whereas a
|
|
96
|
+
> native `<img>` loads with the document. Use `src` for a single unified tag;
|
|
97
|
+
> reach for a native `<img>` (and load `<ai-img>` only when you need to
|
|
98
|
+
> generate) when first paint of a known image is critical.
|
|
99
|
+
|
|
100
|
+
The component never branches on whether an image exists — the **server** owns
|
|
101
|
+
that decision (see the contract below). If the returned `url` itself fails to
|
|
102
|
+
load in the `<img>` (a transient 404, slow propagation, a stale url), the
|
|
103
|
+
component retries once and then drops to the fallback chain.
|
|
104
|
+
|
|
105
|
+
## The `ai-image` event
|
|
106
|
+
|
|
107
|
+
Fired after a successful resolve. Use it to persist the id (and url) to a
|
|
108
|
+
database so you can render the same image again later without re-generating.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
el.addEventListener("ai-image", (e) => {
|
|
112
|
+
// e.detail = { id, url, prompt }
|
|
113
|
+
db.save(e.detail)
|
|
114
|
+
})
|
|
115
|
+
```
|
|
28
116
|
|
|
29
|
-
|
|
117
|
+
The minted id is also reflected onto the `image-id` attribute.
|
|
30
118
|
|
|
31
|
-
|
|
119
|
+
## Server contract
|
|
32
120
|
|
|
33
|
-
|
|
121
|
+
The component talks to a single endpoint with one POST. Your server is the
|
|
122
|
+
"smart" side that decides between fetching a stored image and generating a new
|
|
123
|
+
one.
|
|
34
124
|
|
|
35
|
-
|
|
36
|
-
- **width** (number): The width of the desired image in pixels.
|
|
37
|
-
- **height** (number): The height of the desired image in pixels.
|
|
125
|
+
### `POST {endpoint}`
|
|
38
126
|
|
|
39
|
-
|
|
127
|
+
Request body:
|
|
40
128
|
|
|
41
129
|
```json
|
|
42
|
-
{
|
|
43
|
-
"prompt": "a sleeping little kitten",
|
|
44
|
-
"width": 256,
|
|
45
|
-
"height": 256
|
|
46
|
-
}
|
|
130
|
+
{ "prompt": "a funny dolphin", "imageId": "V1StGXR8_Z5", "width": 256, "height": 256, "llm": "gemini", "ratio": "16:9" }
|
|
47
131
|
```
|
|
48
132
|
|
|
49
|
-
|
|
133
|
+
`imageId`, `width`, `height`, `llm` and `ratio` are optional. Use `llm`/`ratio`
|
|
134
|
+
to let the server pick a provider and aspect ratio. Expected server behaviour:
|
|
50
135
|
|
|
51
|
-
|
|
136
|
+
| Condition | Response |
|
|
137
|
+
| ------------------------------------------- | ----------------- |
|
|
138
|
+
| `imageId` given and stored | `200 {id,url}` — return it, no AI |
|
|
139
|
+
| `imageId` given but missing, `prompt` given | `200 {id,url}` — generate (mint a new id) |
|
|
140
|
+
| no `imageId`, `prompt` given | `200 {id,url}` — generate |
|
|
141
|
+
| nothing to do | `404` |
|
|
142
|
+
|
|
143
|
+
Response body on success:
|
|
52
144
|
|
|
53
145
|
```json
|
|
54
|
-
"
|
|
146
|
+
{ "id": "V1StGXR8_Z5", "url": "/images/V1StGXR8_Z5.png" }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`url` can be anything the browser can load (a hosted/CDN URL, a route on your
|
|
150
|
+
server, or a data URL).
|
|
151
|
+
|
|
152
|
+
## Reference demo server
|
|
153
|
+
|
|
154
|
+
`demo/server.mjs` is a complete, dependency-light reference implementation:
|
|
155
|
+
OpenAI `gpt-image-2`, nanoid ids, filesystem storage under `images/`, served
|
|
156
|
+
back as stable URLs.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cp .env.example .env # add OPENAI_API_KEY
|
|
160
|
+
pnpm install
|
|
161
|
+
pnpm build
|
|
162
|
+
pnpm demo # http://localhost:3000
|
|
55
163
|
```
|
|
56
164
|
|
|
57
|
-
|
|
165
|
+
## License
|
|
58
166
|
|
|
59
|
-
|
|
167
|
+
MIT
|
package/dist/wc-img-ai.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
5
|
*/
|
|
6
6
|
var _a;
|
|
7
|
-
const t$
|
|
7
|
+
const t$2 = globalThis, e$3 = t$2.ShadowRoot && (void 0 === t$2.ShadyCSS || t$2.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$3 = Symbol(), o$4 = /* @__PURE__ */ new WeakMap();
|
|
8
8
|
let n$4 = class n {
|
|
9
9
|
constructor(t2, e2, o2) {
|
|
10
10
|
if (this._$cssResult$ = true, o2 !== s$3)
|
|
@@ -24,7 +24,7 @@ let n$4 = class n {
|
|
|
24
24
|
return this.cssText;
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
const r$
|
|
27
|
+
const r$6 = (t2) => new n$4("string" == typeof t2 ? t2 : t2 + "", void 0, s$3), i$3 = (t2, ...e2) => {
|
|
28
28
|
const o2 = 1 === t2.length ? t2[0] : e2.reduce((e3, s3, o3) => e3 + ((t3) => {
|
|
29
29
|
if (true === t3._$cssResult$)
|
|
30
30
|
return t3.cssText;
|
|
@@ -38,21 +38,21 @@ const r$5 = (t2) => new n$4("string" == typeof t2 ? t2 : t2 + "", void 0, s$3),
|
|
|
38
38
|
s3.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
|
|
39
39
|
else
|
|
40
40
|
for (const e2 of o2) {
|
|
41
|
-
const o3 = document.createElement("style"), n3 = t$
|
|
41
|
+
const o3 = document.createElement("style"), n3 = t$2.litNonce;
|
|
42
42
|
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s3.appendChild(o3);
|
|
43
43
|
}
|
|
44
44
|
}, c$3 = e$3 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
|
|
45
45
|
let e2 = "";
|
|
46
46
|
for (const s3 of t3.cssRules)
|
|
47
47
|
e2 += s3.cssText;
|
|
48
|
-
return r$
|
|
48
|
+
return r$6(e2);
|
|
49
49
|
})(t2) : t2;
|
|
50
50
|
/**
|
|
51
51
|
* @license
|
|
52
52
|
* Copyright 2017 Google LLC
|
|
53
53
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
54
54
|
*/
|
|
55
|
-
const { is: i$2, defineProperty: e$2, getOwnPropertyDescriptor: r$
|
|
55
|
+
const { is: i$2, defineProperty: e$2, getOwnPropertyDescriptor: r$5, getOwnPropertyNames: h$2, getOwnPropertySymbols: o$3, getPrototypeOf: n$3 } = Object, a$1 = globalThis, c$2 = a$1.trustedTypes, l$1 = c$2 ? c$2.emptyScript : "", p$1 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s3) => t2, u$1 = { toAttribute(t2, s3) {
|
|
56
56
|
switch (s3) {
|
|
57
57
|
case Boolean:
|
|
58
58
|
t2 = t2 ? l$1 : null;
|
|
@@ -96,7 +96,7 @@ class b extends HTMLElement {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
static getPropertyDescriptor(t2, s3, i2) {
|
|
99
|
-
const { get: e2, set: h2 } = r$
|
|
99
|
+
const { get: e2, set: h2 } = r$5(this.prototype, t2) ?? { get() {
|
|
100
100
|
return this[s3];
|
|
101
101
|
}, set(t3) {
|
|
102
102
|
this[s3] = t3;
|
|
@@ -299,8 +299,8 @@ b.elementStyles = [], b.shadowRootOptions = { mode: "open" }, b[d$1("elementProp
|
|
|
299
299
|
* Copyright 2017 Google LLC
|
|
300
300
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
301
301
|
*/
|
|
302
|
-
const t$
|
|
303
|
-
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y = (t2) => (i2, ...s3) => ({ _$litType$: t2, strings: i2, values: s3 }), x = y(1), w = Symbol.for("lit-noChange"), T = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), E = r$
|
|
302
|
+
const t$1 = globalThis, i$1 = t$1.trustedTypes, s$2 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e$1 = "$lit$", h$1 = `lit$${(Math.random() + "").slice(9)}$`, o$2 = "?" + h$1, n$2 = `<${o$2}>`, r$4 = document, l = () => r$4.createComment(""), c$1 = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof (t2 == null ? void 0 : t2[Symbol.iterator]), d = "[ \n\f\r]", f$2 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
|
303
|
+
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y = (t2) => (i2, ...s3) => ({ _$litType$: t2, strings: i2, values: s3 }), x = y(1), w = Symbol.for("lit-noChange"), T = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), E = r$4.createTreeWalker(r$4, 129);
|
|
304
304
|
function C(t2, i2) {
|
|
305
305
|
if (!Array.isArray(t2) || !t2.hasOwnProperty("raw"))
|
|
306
306
|
throw Error("invalid template strings array");
|
|
@@ -359,7 +359,7 @@ class V {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
static createElement(t2, i2) {
|
|
362
|
-
const s3 = r$
|
|
362
|
+
const s3 = r$4.createElement("template");
|
|
363
363
|
return s3.innerHTML = t2, s3;
|
|
364
364
|
}
|
|
365
365
|
}
|
|
@@ -382,7 +382,7 @@ class S {
|
|
|
382
382
|
return this._$AM._$AU;
|
|
383
383
|
}
|
|
384
384
|
u(t2) {
|
|
385
|
-
const { el: { content: i2 }, parts: s3 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r$
|
|
385
|
+
const { el: { content: i2 }, parts: s3 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r$4).importNode(i2, true);
|
|
386
386
|
E.currentNode = e2;
|
|
387
387
|
let h2 = E.nextNode(), o2 = 0, n3 = 0, l2 = s3[0];
|
|
388
388
|
for (; void 0 !== l2; ) {
|
|
@@ -392,7 +392,7 @@ class S {
|
|
|
392
392
|
}
|
|
393
393
|
o2 !== (l2 == null ? void 0 : l2.index) && (h2 = E.nextNode(), o2++);
|
|
394
394
|
}
|
|
395
|
-
return E.currentNode = r$
|
|
395
|
+
return E.currentNode = r$4, e2;
|
|
396
396
|
}
|
|
397
397
|
p(t2) {
|
|
398
398
|
let i2 = 0;
|
|
@@ -429,7 +429,7 @@ class M {
|
|
|
429
429
|
this._$AH !== t2 && (this._$AR(), this._$AH = this.k(t2));
|
|
430
430
|
}
|
|
431
431
|
_(t2) {
|
|
432
|
-
this._$AH !== T && c$1(this._$AH) ? this._$AA.nextSibling.data = t2 : this.$(r$
|
|
432
|
+
this._$AH !== T && c$1(this._$AH) ? this._$AA.nextSibling.data = t2 : this.$(r$4.createTextNode(t2)), this._$AH = t2;
|
|
433
433
|
}
|
|
434
434
|
g(t2) {
|
|
435
435
|
var _a2;
|
|
@@ -534,8 +534,8 @@ class L {
|
|
|
534
534
|
N(this, t2);
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
|
-
const Z = t$
|
|
538
|
-
Z == null ? void 0 : Z(V, M), (t$
|
|
537
|
+
const Z = t$1.litHtmlPolyfillSupport;
|
|
538
|
+
Z == null ? void 0 : Z(V, M), (t$1.litHtmlVersions ?? (t$1.litHtmlVersions = [])).push("3.1.0");
|
|
539
539
|
const j = (t2, i2, s3) => {
|
|
540
540
|
const e2 = (s3 == null ? void 0 : s3.renderBefore) ?? i2;
|
|
541
541
|
let h2 = e2._$litPart$;
|
|
@@ -576,25 +576,15 @@ let s$1 = class s extends b {
|
|
|
576
576
|
}
|
|
577
577
|
};
|
|
578
578
|
s$1._$litElement$ = true, s$1["finalized"] = true, (_a = globalThis.litElementHydrateSupport) == null ? void 0 : _a.call(globalThis, { LitElement: s$1 });
|
|
579
|
-
const r$
|
|
580
|
-
r$
|
|
579
|
+
const r$3 = globalThis.litElementPolyfillSupport;
|
|
580
|
+
r$3 == null ? void 0 : r$3({ LitElement: s$1 });
|
|
581
581
|
(globalThis.litElementVersions ?? (globalThis.litElementVersions = [])).push("4.0.2");
|
|
582
582
|
/**
|
|
583
583
|
* @license
|
|
584
584
|
* Copyright 2017 Google LLC
|
|
585
585
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
586
586
|
*/
|
|
587
|
-
const
|
|
588
|
-
void 0 !== o2 ? o2.addInitializer(() => {
|
|
589
|
-
customElements.define(t2, e2);
|
|
590
|
-
}) : customElements.define(t2, e2);
|
|
591
|
-
};
|
|
592
|
-
/**
|
|
593
|
-
* @license
|
|
594
|
-
* Copyright 2017 Google LLC
|
|
595
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
596
|
-
*/
|
|
597
|
-
const o$1 = { attribute: true, type: String, converter: u$1, reflect: false, hasChanged: f$3 }, r$1 = (t2 = o$1, e2, r2) => {
|
|
587
|
+
const o$1 = { attribute: true, type: String, converter: u$1, reflect: false, hasChanged: f$3 }, r$2 = (t2 = o$1, e2, r2) => {
|
|
598
588
|
const { kind: n3, metadata: i2 } = r2;
|
|
599
589
|
let s3 = globalThis.litPropertyMetadata.get(i2);
|
|
600
590
|
if (void 0 === s3 && globalThis.litPropertyMetadata.set(i2, s3 = /* @__PURE__ */ new Map()), s3.set(r2.name, t2), "accessor" === n3) {
|
|
@@ -616,11 +606,19 @@ const o$1 = { attribute: true, type: String, converter: u$1, reflect: false, has
|
|
|
616
606
|
throw Error("Unsupported decorator location: " + n3);
|
|
617
607
|
};
|
|
618
608
|
function n$1(t2) {
|
|
619
|
-
return (e2, o2) => "object" == typeof o2 ? r$
|
|
609
|
+
return (e2, o2) => "object" == typeof o2 ? r$2(t2, e2, o2) : ((t3, e3, o3) => {
|
|
620
610
|
const r2 = e3.hasOwnProperty(o3);
|
|
621
611
|
return e3.constructor.createProperty(o3, r2 ? { ...t3, wrapped: true } : t3), r2 ? Object.getOwnPropertyDescriptor(e3, o3) : void 0;
|
|
622
612
|
})(t2, e2, o2);
|
|
623
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* @license
|
|
616
|
+
* Copyright 2017 Google LLC
|
|
617
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
618
|
+
*/
|
|
619
|
+
function r$1(r2) {
|
|
620
|
+
return n$1({ ...r2, state: true, attribute: false });
|
|
621
|
+
}
|
|
624
622
|
/**
|
|
625
623
|
* @license
|
|
626
624
|
* Copyright 2017 Google LLC
|
|
@@ -890,28 +888,34 @@ class SpreadDirective extends SpreadEventsDirective {
|
|
|
890
888
|
}
|
|
891
889
|
const spread = e(SpreadDirective);
|
|
892
890
|
const spinner = `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.a{animation:b .8s linear infinite;fill:#888}.c{animation-delay:-.65s}.d{animation-delay:-.5s}@keyframes b{93.75%,100%{r:3px}46.875%{r:.2px}}</style><circle class="a" cx="4" cy="12" r="3"/><circle class="a c" cx="12" cy="12" r="3"/><circle class="a d" cx="20" cy="12" r="3"/></svg>`;
|
|
893
|
-
const
|
|
894
|
-
|
|
891
|
+
const TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
|
892
|
+
const resolveImage = async (endpoint, req) => {
|
|
893
|
+
if (!endpoint)
|
|
894
|
+
return null;
|
|
895
895
|
try {
|
|
896
896
|
const r2 = await fetch(endpoint, {
|
|
897
897
|
method: "POST",
|
|
898
|
-
headers: {
|
|
899
|
-
"Content-Type": "application/json"
|
|
900
|
-
},
|
|
898
|
+
headers: { "Content-Type": "application/json" },
|
|
901
899
|
body: JSON.stringify({
|
|
902
|
-
prompt,
|
|
903
|
-
|
|
904
|
-
|
|
900
|
+
prompt: req.prompt || void 0,
|
|
901
|
+
imageId: req.imageId || void 0,
|
|
902
|
+
width: req.width || void 0,
|
|
903
|
+
height: req.height || void 0,
|
|
904
|
+
llm: req.llm || void 0,
|
|
905
|
+
ratio: req.ratio || void 0
|
|
905
906
|
})
|
|
906
907
|
});
|
|
907
|
-
if (!r2.ok)
|
|
908
|
-
|
|
908
|
+
if (!r2.ok)
|
|
909
|
+
return null;
|
|
910
|
+
const data = await r2.json();
|
|
911
|
+
if (!data || typeof data.url !== "string" || data.url.length === 0) {
|
|
912
|
+
return null;
|
|
909
913
|
}
|
|
910
|
-
|
|
914
|
+
return { id: typeof data.id === "string" ? data.id : "", url: data.url };
|
|
911
915
|
} catch (error) {
|
|
912
|
-
console.error("
|
|
916
|
+
console.error("ai-img: image request failed:", error);
|
|
917
|
+
return null;
|
|
913
918
|
}
|
|
914
|
-
return response;
|
|
915
919
|
};
|
|
916
920
|
var __defProp = Object.defineProperty;
|
|
917
921
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -924,118 +928,167 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
924
928
|
__defProp(target, key, result);
|
|
925
929
|
return result;
|
|
926
930
|
};
|
|
927
|
-
|
|
931
|
+
const SPINNER_BG = r$6(encodeURIComponent(spinner));
|
|
932
|
+
const RESERVED_ATTRS = /* @__PURE__ */ new Set([
|
|
933
|
+
"endpoint",
|
|
934
|
+
"prompt",
|
|
935
|
+
"image-id",
|
|
936
|
+
"fallback",
|
|
937
|
+
"width",
|
|
938
|
+
"height",
|
|
939
|
+
"llm",
|
|
940
|
+
"ratio",
|
|
941
|
+
"class",
|
|
942
|
+
"style",
|
|
943
|
+
"loading",
|
|
944
|
+
"decoding",
|
|
945
|
+
"src"
|
|
946
|
+
]);
|
|
947
|
+
const placeholder = (width, height) => "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
948
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="#ddd"/></svg>`
|
|
949
|
+
);
|
|
950
|
+
const dimensionsFor = (width, height, ratio) => {
|
|
951
|
+
const ratioMatch = ratio.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
|
952
|
+
const ratioWidth = Number(ratioMatch == null ? void 0 : ratioMatch[1]);
|
|
953
|
+
const ratioHeight = Number(ratioMatch == null ? void 0 : ratioMatch[2]);
|
|
954
|
+
const numericWidth = Number(width);
|
|
955
|
+
if (!height && Number.isFinite(numericWidth) && numericWidth > 0 && Number.isFinite(ratioWidth) && ratioWidth > 0 && Number.isFinite(ratioHeight) && ratioHeight > 0) {
|
|
956
|
+
return {
|
|
957
|
+
width,
|
|
958
|
+
height: String(Math.round(numericWidth * (ratioHeight / ratioWidth)))
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return { width, height };
|
|
962
|
+
};
|
|
963
|
+
const _AiImg = class _AiImg extends s$1 {
|
|
928
964
|
constructor() {
|
|
929
965
|
super(...arguments);
|
|
966
|
+
this.src = "";
|
|
967
|
+
this.endpoint = "";
|
|
968
|
+
this.prompt = "";
|
|
969
|
+
this.imageId = "";
|
|
970
|
+
this.llm = "";
|
|
971
|
+
this.ratio = "";
|
|
930
972
|
this.fallback = "";
|
|
931
973
|
this.width = "";
|
|
932
974
|
this.height = "";
|
|
933
|
-
this.src = "";
|
|
934
975
|
this.alt = "";
|
|
935
|
-
this.imgsrc =
|
|
936
|
-
this.prompt = "";
|
|
976
|
+
this.imgsrc = TRANSPARENT_PIXEL;
|
|
937
977
|
this.imgAttributes = {};
|
|
978
|
+
this.onFallback = false;
|
|
979
|
+
this.retried = false;
|
|
980
|
+
this.resolvedUrl = "";
|
|
981
|
+
this.onImgError = () => {
|
|
982
|
+
if (this.onFallback || !this.resolvedUrl)
|
|
983
|
+
return;
|
|
984
|
+
if (!this.retried) {
|
|
985
|
+
this.retried = true;
|
|
986
|
+
const sep = this.resolvedUrl.includes("?") ? "&" : "?";
|
|
987
|
+
const url = `${this.resolvedUrl}${sep}retry=${Date.now()}`;
|
|
988
|
+
setTimeout(() => {
|
|
989
|
+
this.imgsrc = url;
|
|
990
|
+
}, 800);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
this.resolvedUrl = "";
|
|
994
|
+
this.settleFallback();
|
|
995
|
+
};
|
|
938
996
|
}
|
|
939
997
|
connectedCallback() {
|
|
940
998
|
super.connectedCallback();
|
|
941
|
-
this.
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
999
|
+
queueMicrotask(() => this.start());
|
|
1000
|
+
}
|
|
1001
|
+
start() {
|
|
1002
|
+
this.collectPassThroughAttributes();
|
|
1003
|
+
const dimensions = dimensionsFor(this.width, this.height, this.ratio);
|
|
1004
|
+
if (this.src) {
|
|
1005
|
+
this.resolvedUrl = this.src;
|
|
1006
|
+
this.settle(this.src);
|
|
947
1007
|
return;
|
|
948
1008
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
this.prompt,
|
|
953
|
-
Number(this.width),
|
|
954
|
-
Number(this.height)
|
|
955
|
-
);
|
|
956
|
-
this.imgsrc = openAiResponse || this.fallback;
|
|
957
|
-
this.classList.remove("spin");
|
|
958
|
-
} catch (error) {
|
|
959
|
-
console.error("Error fetching AI image:", error);
|
|
960
|
-
this.imgsrc = this.fallback;
|
|
961
|
-
this.classList.remove("spin");
|
|
1009
|
+
if (!this.prompt && !this.imageId) {
|
|
1010
|
+
this.settleFallback();
|
|
1011
|
+
return;
|
|
962
1012
|
}
|
|
1013
|
+
this.imgsrc = placeholder(dimensions.width || "1", dimensions.height || "1");
|
|
1014
|
+
this.classList.add("spin");
|
|
1015
|
+
void this.resolve();
|
|
963
1016
|
}
|
|
964
|
-
|
|
965
|
-
Array.from(this.attributes)
|
|
966
|
-
if (!
|
|
967
|
-
"loading",
|
|
968
|
-
"decoding",
|
|
969
|
-
"src",
|
|
970
|
-
"fallback",
|
|
971
|
-
"prompt",
|
|
972
|
-
"style",
|
|
973
|
-
"width",
|
|
974
|
-
"height"
|
|
975
|
-
].includes(attr.name)) {
|
|
1017
|
+
collectPassThroughAttributes() {
|
|
1018
|
+
for (const attr of Array.from(this.attributes)) {
|
|
1019
|
+
if (!RESERVED_ATTRS.has(attr.name)) {
|
|
976
1020
|
this.imgAttributes[attr.name] = attr.value;
|
|
977
1021
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async resolve() {
|
|
1025
|
+
const dimensions = dimensionsFor(this.width, this.height, this.ratio);
|
|
1026
|
+
const result = await resolveImage(this.endpoint, {
|
|
1027
|
+
prompt: this.prompt,
|
|
1028
|
+
imageId: this.imageId,
|
|
1029
|
+
width: Number(dimensions.width) || void 0,
|
|
1030
|
+
height: Number(dimensions.height) || void 0,
|
|
1031
|
+
llm: this.llm,
|
|
1032
|
+
ratio: this.ratio
|
|
984
1033
|
});
|
|
985
|
-
|
|
986
|
-
|
|
1034
|
+
if (!result) {
|
|
1035
|
+
this.settleFallback();
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (result.id && result.id !== this.imageId) {
|
|
1039
|
+
this.imageId = result.id;
|
|
1040
|
+
this.setAttribute("image-id", result.id);
|
|
1041
|
+
}
|
|
1042
|
+
this.dispatchEvent(
|
|
1043
|
+
new CustomEvent("ai-image", {
|
|
1044
|
+
detail: { id: result.id, url: result.url, prompt: this.prompt },
|
|
1045
|
+
bubbles: true,
|
|
1046
|
+
composed: true
|
|
1047
|
+
})
|
|
987
1048
|
);
|
|
988
|
-
this.
|
|
1049
|
+
this.resolvedUrl = result.url;
|
|
1050
|
+
this.retried = false;
|
|
1051
|
+
this.onFallback = false;
|
|
1052
|
+
this.settle(result.url);
|
|
1053
|
+
}
|
|
1054
|
+
settle(src) {
|
|
1055
|
+
this.imgsrc = src;
|
|
1056
|
+
this.classList.remove("spin");
|
|
1057
|
+
}
|
|
1058
|
+
settleFallback() {
|
|
1059
|
+
this.onFallback = true;
|
|
1060
|
+
this.settle(this.fallback || TRANSPARENT_PIXEL);
|
|
989
1061
|
}
|
|
990
1062
|
render() {
|
|
1063
|
+
const dimensions = dimensionsFor(this.width, this.height, this.ratio);
|
|
991
1064
|
return x`
|
|
992
|
-
<
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
width: 100%;
|
|
1002
|
-
height: 100%;
|
|
1003
|
-
display: inline-flex;
|
|
1004
|
-
justify-content: center;
|
|
1005
|
-
align-items: center;
|
|
1006
|
-
color: red;
|
|
1007
|
-
background-image: url("data:image/svg+xml;utf8,${encodeURIComponent(
|
|
1008
|
-
spinner
|
|
1009
|
-
)}");
|
|
1010
|
-
background-repeat: no-repeat;
|
|
1011
|
-
background-position: center;
|
|
1012
|
-
background-size: 25%;
|
|
1013
|
-
}
|
|
1014
|
-
</style>
|
|
1015
|
-
|
|
1016
|
-
${this.imgsrc.length > 0 ? x`
|
|
1017
|
-
<img
|
|
1018
|
-
src=${this.imgsrc}
|
|
1019
|
-
decoding="async"
|
|
1020
|
-
loading="eager"
|
|
1021
|
-
${spread(this.imgAttributes)}
|
|
1022
|
-
/>
|
|
1023
|
-
` : x`<div>fail</div>`}
|
|
1065
|
+
<img
|
|
1066
|
+
src=${this.imgsrc}
|
|
1067
|
+
alt=${this.alt}
|
|
1068
|
+
width=${dimensions.width || T}
|
|
1069
|
+
height=${dimensions.height || T}
|
|
1070
|
+
decoding="async"
|
|
1071
|
+
@error=${this.onImgError}
|
|
1072
|
+
${spread(this.imgAttributes)}
|
|
1073
|
+
/>
|
|
1024
1074
|
`;
|
|
1025
1075
|
}
|
|
1026
1076
|
};
|
|
1027
|
-
|
|
1077
|
+
_AiImg.styles = i$3`
|
|
1028
1078
|
:host {
|
|
1029
1079
|
display: inline-block;
|
|
1080
|
+
position: relative;
|
|
1081
|
+
line-height: 0;
|
|
1030
1082
|
}
|
|
1031
1083
|
|
|
1032
1084
|
img {
|
|
1033
1085
|
display: block;
|
|
1034
|
-
-webkit-user-select: none;
|
|
1035
1086
|
width: 100%;
|
|
1036
|
-
height:
|
|
1087
|
+
height: auto;
|
|
1088
|
+
-webkit-user-select: none;
|
|
1037
1089
|
object-fit: inherit;
|
|
1038
1090
|
object-position: inherit;
|
|
1091
|
+
aspect-ratio: inherit;
|
|
1039
1092
|
filter: inherit;
|
|
1040
1093
|
transform: inherit;
|
|
1041
1094
|
transition: inherit;
|
|
@@ -1043,7 +1096,37 @@ AiImg.styles = i$3`
|
|
|
1043
1096
|
box-shadow: inherit;
|
|
1044
1097
|
clip-path: inherit;
|
|
1045
1098
|
}
|
|
1099
|
+
|
|
1100
|
+
:host(.spin)::before {
|
|
1101
|
+
content: "";
|
|
1102
|
+
position: absolute;
|
|
1103
|
+
inset: 0;
|
|
1104
|
+
margin: auto;
|
|
1105
|
+
background-image: url("data:image/svg+xml;utf8,${SPINNER_BG}");
|
|
1106
|
+
background-repeat: no-repeat;
|
|
1107
|
+
background-position: center;
|
|
1108
|
+
background-size: 25%;
|
|
1109
|
+
}
|
|
1046
1110
|
`;
|
|
1111
|
+
let AiImg = _AiImg;
|
|
1112
|
+
__decorateClass([
|
|
1113
|
+
n$1({ type: String })
|
|
1114
|
+
], AiImg.prototype, "src", 2);
|
|
1115
|
+
__decorateClass([
|
|
1116
|
+
n$1({ type: String })
|
|
1117
|
+
], AiImg.prototype, "endpoint", 2);
|
|
1118
|
+
__decorateClass([
|
|
1119
|
+
n$1({ type: String })
|
|
1120
|
+
], AiImg.prototype, "prompt", 2);
|
|
1121
|
+
__decorateClass([
|
|
1122
|
+
n$1({ type: String, attribute: "image-id" })
|
|
1123
|
+
], AiImg.prototype, "imageId", 2);
|
|
1124
|
+
__decorateClass([
|
|
1125
|
+
n$1({ type: String })
|
|
1126
|
+
], AiImg.prototype, "llm", 2);
|
|
1127
|
+
__decorateClass([
|
|
1128
|
+
n$1({ type: String })
|
|
1129
|
+
], AiImg.prototype, "ratio", 2);
|
|
1047
1130
|
__decorateClass([
|
|
1048
1131
|
n$1({ type: String })
|
|
1049
1132
|
], AiImg.prototype, "fallback", 2);
|
|
@@ -1051,23 +1134,17 @@ __decorateClass([
|
|
|
1051
1134
|
n$1({ type: String, reflect: true })
|
|
1052
1135
|
], AiImg.prototype, "width", 2);
|
|
1053
1136
|
__decorateClass([
|
|
1054
|
-
n$1({ type: String
|
|
1137
|
+
n$1({ type: String })
|
|
1055
1138
|
], AiImg.prototype, "height", 2);
|
|
1056
|
-
__decorateClass([
|
|
1057
|
-
n$1({ type: String, reflect: true })
|
|
1058
|
-
], AiImg.prototype, "src", 2);
|
|
1059
1139
|
__decorateClass([
|
|
1060
1140
|
n$1({ type: String, reflect: true })
|
|
1061
1141
|
], AiImg.prototype, "alt", 2);
|
|
1062
1142
|
__decorateClass([
|
|
1063
|
-
|
|
1143
|
+
r$1()
|
|
1064
1144
|
], AiImg.prototype, "imgsrc", 2);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
AiImg = __decorateClass([
|
|
1069
|
-
t$1("ai-img")
|
|
1070
|
-
], AiImg);
|
|
1145
|
+
if (typeof customElements !== "undefined" && !customElements.get("ai-img")) {
|
|
1146
|
+
customElements.define("ai-img", AiImg);
|
|
1147
|
+
}
|
|
1071
1148
|
export {
|
|
1072
1149
|
AiImg
|
|
1073
1150
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wc-img-ai",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "Use AI to generate images for your img tags.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "John Romani",
|
|
@@ -22,15 +22,26 @@
|
|
|
22
22
|
"types"
|
|
23
23
|
],
|
|
24
24
|
"main": "./dist/wc-img-ai.js",
|
|
25
|
+
"types": "./types/ai-img.d.ts",
|
|
25
26
|
"exports": {
|
|
26
|
-
".":
|
|
27
|
-
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./types/ai-img.d.ts",
|
|
29
|
+
"default": "./dist/wc-img-ai.js"
|
|
30
|
+
},
|
|
31
|
+
"./types": "./types/ai-img.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite",
|
|
35
|
+
"build": "tsc && vite build",
|
|
36
|
+
"preview": "vite preview",
|
|
37
|
+
"demo": "node demo/server.mjs"
|
|
28
38
|
},
|
|
29
39
|
"devDependencies": {
|
|
30
40
|
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
|
31
41
|
"@typescript-eslint/parser": "^6.17.0",
|
|
32
42
|
"eslint": "^8.56.0",
|
|
33
43
|
"eslint-plugin-wc": "^2.0.4",
|
|
44
|
+
"nanoid": "^5.0.9",
|
|
34
45
|
"typescript": "^5.2.2",
|
|
35
46
|
"vite": "^5.0.8"
|
|
36
47
|
},
|
|
@@ -42,10 +53,5 @@
|
|
|
42
53
|
},
|
|
43
54
|
"peerDependencies": {
|
|
44
55
|
"lit": "^3.0.0"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"dev": "vite",
|
|
48
|
-
"build": "tsc && vite build",
|
|
49
|
-
"preview": "vite preview"
|
|
50
56
|
}
|
|
51
|
-
}
|
|
57
|
+
}
|
package/types/ai-img.d.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import { LitElement } from "lit";
|
|
2
2
|
export declare class AiImg extends LitElement {
|
|
3
|
+
/**
|
|
4
|
+
* A ready image URL (or data URL). When set, the component acts as a plain
|
|
5
|
+
* <img> and never calls the AI endpoint — use it when you already have the
|
|
6
|
+
* image (e.g. a precomputed/returning one). Highest priority.
|
|
7
|
+
*/
|
|
8
|
+
src: string;
|
|
9
|
+
/** Server route that owns the API key, generation, storage and lookup. */
|
|
10
|
+
endpoint: string;
|
|
11
|
+
/** Description used to generate the image (omit when fetching a known id). */
|
|
12
|
+
prompt: string;
|
|
13
|
+
/** Storage handle. Reflected after the server mints a new image. */
|
|
14
|
+
imageId: string;
|
|
15
|
+
/** Provider/model hint forwarded to the endpoint (e.g. "gemini", "openai"). */
|
|
16
|
+
llm: string;
|
|
17
|
+
/** Aspect ratio forwarded to the endpoint and used to derive an omitted height. */
|
|
18
|
+
ratio: string;
|
|
19
|
+
/** Shown when the image cannot be resolved (otherwise a 1x1 transparent PNG). */
|
|
3
20
|
fallback: string;
|
|
4
21
|
width: string;
|
|
5
22
|
height: string;
|
|
6
|
-
src: string;
|
|
7
23
|
alt: string;
|
|
8
|
-
imgsrc
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
private imgsrc;
|
|
25
|
+
private imgAttributes;
|
|
26
|
+
private onFallback;
|
|
27
|
+
private retried;
|
|
28
|
+
private resolvedUrl;
|
|
13
29
|
connectedCallback(): void;
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
private start;
|
|
31
|
+
private collectPassThroughAttributes;
|
|
32
|
+
private resolve;
|
|
33
|
+
private settle;
|
|
34
|
+
private settleFallback;
|
|
35
|
+
private onImgError;
|
|
16
36
|
protected render(): import("lit").TemplateResult<1>;
|
|
17
37
|
static styles: import("lit").CSSResult;
|
|
18
38
|
}
|
|
@@ -1,2 +1,26 @@
|
|
|
1
1
|
export declare const spinner = "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><style>.a{animation:b .8s linear infinite;fill:#888}.c{animation-delay:-.65s}.d{animation-delay:-.5s}@keyframes b{93.75%,100%{r:3px}46.875%{r:.2px}}</style><circle class=\"a\" cx=\"4\" cy=\"12\" r=\"3\"/><circle class=\"a c\" cx=\"12\" cy=\"12\" r=\"3\"/><circle class=\"a d\" cx=\"20\" cy=\"12\" r=\"3\"/></svg>";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
|
3
|
+
export interface ResolveImageRequest {
|
|
4
|
+
prompt?: string;
|
|
5
|
+
imageId?: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
/** Provider/model hint forwarded to the endpoint (e.g. "gemini", "openai"). */
|
|
9
|
+
llm?: string;
|
|
10
|
+
/** Aspect ratio forwarded to the endpoint (e.g. "16:9", "4:1"). */
|
|
11
|
+
ratio?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolvedImage {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sends a single POST to the endpoint and lets the server decide whether to
|
|
19
|
+
* return an already-stored image (looked up by `imageId`) or generate a new
|
|
20
|
+
* one. The component never branches on existence — it just trusts the result.
|
|
21
|
+
*
|
|
22
|
+
* Resolves to `{ id, url }` on success, or `null` when the endpoint is missing,
|
|
23
|
+
* the request fails, or the server reports the image could not be resolved
|
|
24
|
+
* (e.g. 404). A `null` tells the component to fall back.
|
|
25
|
+
*/
|
|
26
|
+
export declare const resolveImage: (endpoint: string, req: ResolveImageRequest) => Promise<ResolvedImage | null>;
|