wc-img-ai 0.2.2 → 0.3.0
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 +123 -25
- package/dist/wc-img-ai.js +189 -129
- 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,124 @@ 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` | ✅ yes | 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`). |
|
|
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` | ✅ yes | Intrinsic height (like `<img height>`). |
|
|
50
|
+
| `alt` | ✅ yes | Alt text, passed to the inner `<img>`. |
|
|
51
|
+
|
|
52
|
+
### Sizing & styling — just like a native `<img>`
|
|
53
|
+
|
|
54
|
+
Set `width`/`height` and style with `class`/CSS; you rarely need inline
|
|
55
|
+
`style`. `width`/`height` become the inner image's content attributes, so the
|
|
56
|
+
browser reserves the box from their aspect-ratio (no layout shift) while CSS
|
|
57
|
+
controls the displayed size:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- full-width 3:1 banner, rounded, no layout shift -->
|
|
61
|
+
<ai-img endpoint="/api/img" prompt="…" width="1536" height="512"
|
|
62
|
+
class="block w-full rounded-xl"></ai-img>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Visual properties bridge the shadow boundary via `inherit`, so utility classes
|
|
66
|
+
like `rounded-xl`, `object-cover`, `shadow-lg` on `<ai-img>` style the image.
|
|
67
|
+
Any other attribute (e.g. `loading`) is passed through to the inner `<img>`.
|
|
28
68
|
|
|
29
|
-
|
|
69
|
+
## How it resolves
|
|
30
70
|
|
|
31
|
-
|
|
71
|
+
```
|
|
72
|
+
src set → render it as a plain <img> (no endpoint call)
|
|
73
|
+
else no prompt, no image-id → fallback → 1×1 transparent PNG (nothing to ask)
|
|
74
|
+
else → POST endpoint once { prompt, imageId, width, height, llm, ratio }
|
|
75
|
+
200 {id,url} → render url, reflect image-id, fire `ai-image` event
|
|
76
|
+
404 / error → fallback → 1×1 transparent PNG
|
|
77
|
+
```
|
|
32
78
|
|
|
33
|
-
|
|
79
|
+
`src` is the cheapest path: if you already have the image (a precomputed or
|
|
80
|
+
returning one), set `src` and the component skips the AI entirely. Only when
|
|
81
|
+
`src` is empty does it fall back to the `prompt`/`image-id` flow.
|
|
34
82
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
83
|
+
> **Performance note.** For an image you _already have the URL for_, a plain
|
|
84
|
+
> `<img src>` will paint faster than `<ai-img src>` — the web component can't
|
|
85
|
+
> render until its own script has loaded and defined the element, whereas a
|
|
86
|
+
> native `<img>` loads with the document. Use `src` for a single unified tag;
|
|
87
|
+
> reach for a native `<img>` (and load `<ai-img>` only when you need to
|
|
88
|
+
> generate) when first paint of a known image is critical.
|
|
38
89
|
|
|
39
|
-
|
|
90
|
+
The component never branches on whether an image exists — the **server** owns
|
|
91
|
+
that decision (see the contract below). If the returned `url` itself fails to
|
|
92
|
+
load in the `<img>` (a transient 404, slow propagation, a stale url), the
|
|
93
|
+
component retries once and then drops to the fallback chain.
|
|
94
|
+
|
|
95
|
+
## The `ai-image` event
|
|
96
|
+
|
|
97
|
+
Fired after a successful resolve. Use it to persist the id (and url) to a
|
|
98
|
+
database so you can render the same image again later without re-generating.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
el.addEventListener("ai-image", (e) => {
|
|
102
|
+
// e.detail = { id, url, prompt }
|
|
103
|
+
db.save(e.detail)
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The minted id is also reflected onto the `image-id` attribute.
|
|
108
|
+
|
|
109
|
+
## Server contract
|
|
110
|
+
|
|
111
|
+
The component talks to a single endpoint with one POST. Your server is the
|
|
112
|
+
"smart" side that decides between fetching a stored image and generating a new
|
|
113
|
+
one.
|
|
114
|
+
|
|
115
|
+
### `POST {endpoint}`
|
|
116
|
+
|
|
117
|
+
Request body:
|
|
40
118
|
|
|
41
119
|
```json
|
|
42
|
-
{
|
|
43
|
-
"prompt": "a sleeping little kitten",
|
|
44
|
-
"width": 256,
|
|
45
|
-
"height": 256
|
|
46
|
-
}
|
|
120
|
+
{ "prompt": "a funny dolphin", "imageId": "V1StGXR8_Z5", "width": 256, "height": 256, "llm": "gemini", "ratio": "16:9" }
|
|
47
121
|
```
|
|
48
122
|
|
|
49
|
-
|
|
123
|
+
`imageId`, `width`, `height`, `llm` and `ratio` are optional. Use `llm`/`ratio`
|
|
124
|
+
to let the server pick a provider and aspect ratio. Expected server behaviour:
|
|
125
|
+
|
|
126
|
+
| Condition | Response |
|
|
127
|
+
| ------------------------------------------- | ----------------- |
|
|
128
|
+
| `imageId` given and stored | `200 {id,url}` — return it, no AI |
|
|
129
|
+
| `imageId` given but missing, `prompt` given | `200 {id,url}` — generate (mint a new id) |
|
|
130
|
+
| no `imageId`, `prompt` given | `200 {id,url}` — generate |
|
|
131
|
+
| nothing to do | `404` |
|
|
50
132
|
|
|
51
|
-
|
|
133
|
+
Response body on success:
|
|
52
134
|
|
|
53
135
|
```json
|
|
54
|
-
"
|
|
136
|
+
{ "id": "V1StGXR8_Z5", "url": "/images/V1StGXR8_Z5.png" }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`url` can be anything the browser can load (a hosted/CDN URL, a route on your
|
|
140
|
+
server, or a data URL).
|
|
141
|
+
|
|
142
|
+
## Reference demo server
|
|
143
|
+
|
|
144
|
+
`demo/server.mjs` is a complete, dependency-light reference implementation:
|
|
145
|
+
OpenAI `gpt-image-2`, nanoid ids, filesystem storage under `images/`, served
|
|
146
|
+
back as stable URLs.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cp .env.example .env # add OPENAI_API_KEY
|
|
150
|
+
pnpm install
|
|
151
|
+
pnpm build
|
|
152
|
+
pnpm demo # http://localhost:3000
|
|
55
153
|
```
|
|
56
154
|
|
|
57
|
-
|
|
155
|
+
## License
|
|
58
156
|
|
|
59
|
-
|
|
157
|
+
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,150 @@ 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 _AiImg = class _AiImg extends s$1 {
|
|
928
951
|
constructor() {
|
|
929
952
|
super(...arguments);
|
|
953
|
+
this.src = "";
|
|
954
|
+
this.endpoint = "";
|
|
955
|
+
this.prompt = "";
|
|
956
|
+
this.imageId = "";
|
|
957
|
+
this.llm = "";
|
|
958
|
+
this.ratio = "";
|
|
930
959
|
this.fallback = "";
|
|
931
960
|
this.width = "";
|
|
932
961
|
this.height = "";
|
|
933
|
-
this.src = "";
|
|
934
962
|
this.alt = "";
|
|
935
|
-
this.imgsrc =
|
|
936
|
-
this.prompt = "";
|
|
963
|
+
this.imgsrc = TRANSPARENT_PIXEL;
|
|
937
964
|
this.imgAttributes = {};
|
|
965
|
+
this.onFallback = false;
|
|
966
|
+
this.retried = false;
|
|
967
|
+
this.resolvedUrl = "";
|
|
968
|
+
this.onImgError = () => {
|
|
969
|
+
if (this.onFallback || !this.resolvedUrl)
|
|
970
|
+
return;
|
|
971
|
+
if (!this.retried) {
|
|
972
|
+
this.retried = true;
|
|
973
|
+
const sep = this.resolvedUrl.includes("?") ? "&" : "?";
|
|
974
|
+
const url = `${this.resolvedUrl}${sep}retry=${Date.now()}`;
|
|
975
|
+
setTimeout(() => {
|
|
976
|
+
this.imgsrc = url;
|
|
977
|
+
}, 800);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this.resolvedUrl = "";
|
|
981
|
+
this.settleFallback();
|
|
982
|
+
};
|
|
938
983
|
}
|
|
939
984
|
connectedCallback() {
|
|
940
985
|
super.connectedCallback();
|
|
941
|
-
this.
|
|
942
|
-
this.fetchImage();
|
|
986
|
+
queueMicrotask(() => this.start());
|
|
943
987
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
988
|
+
start() {
|
|
989
|
+
this.collectPassThroughAttributes();
|
|
990
|
+
if (this.src) {
|
|
991
|
+
this.resolvedUrl = this.src;
|
|
992
|
+
this.settle(this.src);
|
|
947
993
|
return;
|
|
948
994
|
}
|
|
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");
|
|
995
|
+
if (!this.prompt && !this.imageId) {
|
|
996
|
+
this.settleFallback();
|
|
997
|
+
return;
|
|
962
998
|
}
|
|
999
|
+
this.imgsrc = placeholder(this.width, this.height);
|
|
1000
|
+
this.classList.add("spin");
|
|
1001
|
+
void this.resolve();
|
|
963
1002
|
}
|
|
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)) {
|
|
1003
|
+
collectPassThroughAttributes() {
|
|
1004
|
+
for (const attr of Array.from(this.attributes)) {
|
|
1005
|
+
if (!RESERVED_ATTRS.has(attr.name)) {
|
|
976
1006
|
this.imgAttributes[attr.name] = attr.value;
|
|
977
1007
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async resolve() {
|
|
1011
|
+
const result = await resolveImage(this.endpoint, {
|
|
1012
|
+
prompt: this.prompt,
|
|
1013
|
+
imageId: this.imageId,
|
|
1014
|
+
width: Number(this.width) || void 0,
|
|
1015
|
+
height: Number(this.height) || void 0,
|
|
1016
|
+
llm: this.llm,
|
|
1017
|
+
ratio: this.ratio
|
|
984
1018
|
});
|
|
985
|
-
|
|
986
|
-
|
|
1019
|
+
if (!result) {
|
|
1020
|
+
this.settleFallback();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (result.id && result.id !== this.imageId) {
|
|
1024
|
+
this.imageId = result.id;
|
|
1025
|
+
}
|
|
1026
|
+
this.dispatchEvent(
|
|
1027
|
+
new CustomEvent("ai-image", {
|
|
1028
|
+
detail: { id: result.id, url: result.url, prompt: this.prompt },
|
|
1029
|
+
bubbles: true,
|
|
1030
|
+
composed: true
|
|
1031
|
+
})
|
|
987
1032
|
);
|
|
988
|
-
this.
|
|
1033
|
+
this.resolvedUrl = result.url;
|
|
1034
|
+
this.retried = false;
|
|
1035
|
+
this.onFallback = false;
|
|
1036
|
+
this.settle(result.url);
|
|
1037
|
+
}
|
|
1038
|
+
settle(src) {
|
|
1039
|
+
this.imgsrc = src;
|
|
1040
|
+
this.classList.remove("spin");
|
|
1041
|
+
}
|
|
1042
|
+
settleFallback() {
|
|
1043
|
+
this.onFallback = true;
|
|
1044
|
+
this.settle(this.fallback || TRANSPARENT_PIXEL);
|
|
989
1045
|
}
|
|
990
1046
|
render() {
|
|
991
1047
|
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>`}
|
|
1048
|
+
<img
|
|
1049
|
+
src=${this.imgsrc}
|
|
1050
|
+
alt=${this.alt}
|
|
1051
|
+
width=${this.width || T}
|
|
1052
|
+
height=${this.height || T}
|
|
1053
|
+
decoding="async"
|
|
1054
|
+
@error=${this.onImgError}
|
|
1055
|
+
${spread(this.imgAttributes)}
|
|
1056
|
+
/>
|
|
1024
1057
|
`;
|
|
1025
1058
|
}
|
|
1026
1059
|
};
|
|
1027
|
-
|
|
1060
|
+
_AiImg.styles = i$3`
|
|
1028
1061
|
:host {
|
|
1029
1062
|
display: inline-block;
|
|
1063
|
+
position: relative;
|
|
1064
|
+
line-height: 0;
|
|
1030
1065
|
}
|
|
1031
1066
|
|
|
1032
1067
|
img {
|
|
1033
1068
|
display: block;
|
|
1034
|
-
-webkit-user-select: none;
|
|
1035
1069
|
width: 100%;
|
|
1036
|
-
height:
|
|
1070
|
+
height: auto;
|
|
1071
|
+
-webkit-user-select: none;
|
|
1037
1072
|
object-fit: inherit;
|
|
1038
1073
|
object-position: inherit;
|
|
1074
|
+
aspect-ratio: inherit;
|
|
1039
1075
|
filter: inherit;
|
|
1040
1076
|
transform: inherit;
|
|
1041
1077
|
transition: inherit;
|
|
@@ -1043,7 +1079,37 @@ AiImg.styles = i$3`
|
|
|
1043
1079
|
box-shadow: inherit;
|
|
1044
1080
|
clip-path: inherit;
|
|
1045
1081
|
}
|
|
1082
|
+
|
|
1083
|
+
:host(.spin)::before {
|
|
1084
|
+
content: "";
|
|
1085
|
+
position: absolute;
|
|
1086
|
+
inset: 0;
|
|
1087
|
+
margin: auto;
|
|
1088
|
+
background-image: url("data:image/svg+xml;utf8,${SPINNER_BG}");
|
|
1089
|
+
background-repeat: no-repeat;
|
|
1090
|
+
background-position: center;
|
|
1091
|
+
background-size: 25%;
|
|
1092
|
+
}
|
|
1046
1093
|
`;
|
|
1094
|
+
let AiImg = _AiImg;
|
|
1095
|
+
__decorateClass([
|
|
1096
|
+
n$1({ type: String })
|
|
1097
|
+
], AiImg.prototype, "src", 2);
|
|
1098
|
+
__decorateClass([
|
|
1099
|
+
n$1({ type: String })
|
|
1100
|
+
], AiImg.prototype, "endpoint", 2);
|
|
1101
|
+
__decorateClass([
|
|
1102
|
+
n$1({ type: String })
|
|
1103
|
+
], AiImg.prototype, "prompt", 2);
|
|
1104
|
+
__decorateClass([
|
|
1105
|
+
n$1({ type: String, attribute: "image-id", reflect: true })
|
|
1106
|
+
], AiImg.prototype, "imageId", 2);
|
|
1107
|
+
__decorateClass([
|
|
1108
|
+
n$1({ type: String })
|
|
1109
|
+
], AiImg.prototype, "llm", 2);
|
|
1110
|
+
__decorateClass([
|
|
1111
|
+
n$1({ type: String })
|
|
1112
|
+
], AiImg.prototype, "ratio", 2);
|
|
1047
1113
|
__decorateClass([
|
|
1048
1114
|
n$1({ type: String })
|
|
1049
1115
|
], AiImg.prototype, "fallback", 2);
|
|
@@ -1053,21 +1119,15 @@ __decorateClass([
|
|
|
1053
1119
|
__decorateClass([
|
|
1054
1120
|
n$1({ type: String, reflect: true })
|
|
1055
1121
|
], AiImg.prototype, "height", 2);
|
|
1056
|
-
__decorateClass([
|
|
1057
|
-
n$1({ type: String, reflect: true })
|
|
1058
|
-
], AiImg.prototype, "src", 2);
|
|
1059
1122
|
__decorateClass([
|
|
1060
1123
|
n$1({ type: String, reflect: true })
|
|
1061
1124
|
], AiImg.prototype, "alt", 2);
|
|
1062
1125
|
__decorateClass([
|
|
1063
|
-
|
|
1126
|
+
r$1()
|
|
1064
1127
|
], AiImg.prototype, "imgsrc", 2);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
AiImg = __decorateClass([
|
|
1069
|
-
t$1("ai-img")
|
|
1070
|
-
], AiImg);
|
|
1128
|
+
if (typeof customElements !== "undefined" && !customElements.get("ai-img")) {
|
|
1129
|
+
customElements.define("ai-img", AiImg);
|
|
1130
|
+
}
|
|
1071
1131
|
export {
|
|
1072
1132
|
AiImg
|
|
1073
1133
|
};
|
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.0",
|
|
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: set by the server when a new image is minted. */
|
|
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 (e.g. "16:9", "4:1"). */
|
|
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>;
|