@primestyleai/tryon 1.0.1 → 1.1.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.
@@ -0,0 +1,186 @@
1
+ const DEFAULT_API_URL = "https://api.primestyleai.com";
2
+ class ApiClient {
3
+ constructor(apiKey, apiUrl) {
4
+ this.apiKey = apiKey;
5
+ this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
6
+ }
7
+ get headers() {
8
+ return {
9
+ "Content-Type": "application/json",
10
+ Authorization: `Bearer ${this.apiKey}`
11
+ };
12
+ }
13
+ async submitTryOn(modelImage, garmentImage) {
14
+ const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
15
+ method: "POST",
16
+ headers: this.headers,
17
+ body: JSON.stringify({ modelImage, garmentImage })
18
+ });
19
+ if (!res.ok) {
20
+ const data = await res.json().catch(() => ({}));
21
+ if (res.status === 402) {
22
+ throw new PrimeStyleError(
23
+ data.message || "Insufficient tokens",
24
+ "INSUFFICIENT_TOKENS"
25
+ );
26
+ }
27
+ throw new PrimeStyleError(
28
+ data.message || "Failed to submit try-on",
29
+ "API_ERROR"
30
+ );
31
+ }
32
+ return res.json();
33
+ }
34
+ async getStatus(jobId) {
35
+ const res = await fetch(`${this.baseUrl}/api/v1/tryon/status/${jobId}`, {
36
+ headers: this.headers
37
+ });
38
+ if (!res.ok) {
39
+ const data = await res.json().catch(() => ({}));
40
+ throw new PrimeStyleError(
41
+ data.message || "Failed to get status",
42
+ "API_ERROR"
43
+ );
44
+ }
45
+ return res.json();
46
+ }
47
+ getStreamUrl() {
48
+ return `${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`;
49
+ }
50
+ }
51
+ class PrimeStyleError extends Error {
52
+ constructor(message, code) {
53
+ super(message);
54
+ this.name = "PrimeStyleError";
55
+ this.code = code;
56
+ }
57
+ }
58
+ class SseClient {
59
+ constructor(streamUrl) {
60
+ this.eventSource = null;
61
+ this.listeners = /* @__PURE__ */ new Map();
62
+ this.reconnectTimer = null;
63
+ this.reconnectAttempts = 0;
64
+ this.maxReconnectAttempts = 5;
65
+ this.streamUrl = streamUrl;
66
+ }
67
+ connect() {
68
+ if (this.eventSource) return;
69
+ this.eventSource = new EventSource(this.streamUrl);
70
+ this.eventSource.addEventListener("vto-update", (event) => {
71
+ try {
72
+ const data = JSON.parse(event.data);
73
+ this.emit(data.galleryId, data);
74
+ } catch {
75
+ }
76
+ });
77
+ this.eventSource.onopen = () => {
78
+ this.reconnectAttempts = 0;
79
+ };
80
+ this.eventSource.onerror = () => {
81
+ this.eventSource?.close();
82
+ this.eventSource = null;
83
+ this.scheduleReconnect();
84
+ };
85
+ }
86
+ scheduleReconnect() {
87
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
88
+ if (this.listeners.size === 0) return;
89
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
90
+ this.reconnectAttempts++;
91
+ this.reconnectTimer = setTimeout(() => {
92
+ this.connect();
93
+ }, delay);
94
+ }
95
+ onJob(jobId, callback) {
96
+ if (!this.listeners.has(jobId)) {
97
+ this.listeners.set(jobId, /* @__PURE__ */ new Set());
98
+ }
99
+ this.listeners.get(jobId).add(callback);
100
+ if (!this.eventSource) {
101
+ this.connect();
102
+ }
103
+ return () => {
104
+ const jobListeners = this.listeners.get(jobId);
105
+ if (jobListeners) {
106
+ jobListeners.delete(callback);
107
+ if (jobListeners.size === 0) {
108
+ this.listeners.delete(jobId);
109
+ }
110
+ }
111
+ if (this.listeners.size === 0) {
112
+ this.disconnect();
113
+ }
114
+ };
115
+ }
116
+ emit(jobId, update) {
117
+ const callbacks = this.listeners.get(jobId);
118
+ if (callbacks) {
119
+ callbacks.forEach((cb) => cb(update));
120
+ }
121
+ }
122
+ disconnect() {
123
+ if (this.reconnectTimer) {
124
+ clearTimeout(this.reconnectTimer);
125
+ this.reconnectTimer = null;
126
+ }
127
+ if (this.eventSource) {
128
+ this.eventSource.close();
129
+ this.eventSource = null;
130
+ }
131
+ this.listeners.clear();
132
+ this.reconnectAttempts = 0;
133
+ }
134
+ }
135
+ const MAX_DIMENSION = 1024;
136
+ const JPEG_QUALITY = 0.85;
137
+ function compressImage(file) {
138
+ return new Promise((resolve, reject) => {
139
+ const reader = new FileReader();
140
+ reader.onload = () => {
141
+ const img = new Image();
142
+ img.onload = () => {
143
+ try {
144
+ const canvas = document.createElement("canvas");
145
+ let { width, height } = img;
146
+ if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
147
+ if (width > height) {
148
+ height = Math.round(height * MAX_DIMENSION / width);
149
+ width = MAX_DIMENSION;
150
+ } else {
151
+ width = Math.round(width * MAX_DIMENSION / height);
152
+ height = MAX_DIMENSION;
153
+ }
154
+ }
155
+ canvas.width = width;
156
+ canvas.height = height;
157
+ const ctx = canvas.getContext("2d");
158
+ if (!ctx) {
159
+ reject(new Error("Canvas context not available"));
160
+ return;
161
+ }
162
+ ctx.drawImage(img, 0, 0, width, height);
163
+ const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY);
164
+ resolve(dataUrl);
165
+ } catch (err) {
166
+ reject(err);
167
+ }
168
+ };
169
+ img.onerror = () => reject(new Error("Failed to load image"));
170
+ img.src = reader.result;
171
+ };
172
+ reader.onerror = () => reject(new Error("Failed to read file"));
173
+ reader.readAsDataURL(file);
174
+ });
175
+ }
176
+ function isValidImageFile(file) {
177
+ const accepted = ["image/jpeg", "image/png", "image/webp"];
178
+ return accepted.includes(file.type);
179
+ }
180
+ export {
181
+ ApiClient as A,
182
+ PrimeStyleError as P,
183
+ SseClient as S,
184
+ compressImage as c,
185
+ isValidImageFile as i
186
+ };
@@ -1,137 +1,5 @@
1
- const DEFAULT_API_URL = "https://api.primestyleai.com";
2
- class ApiClient {
3
- constructor(apiKey, apiUrl) {
4
- this.apiKey = apiKey;
5
- this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
6
- }
7
- get headers() {
8
- return {
9
- "Content-Type": "application/json",
10
- Authorization: `Bearer ${this.apiKey}`
11
- };
12
- }
13
- async submitTryOn(modelImage, garmentImage) {
14
- const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
15
- method: "POST",
16
- headers: this.headers,
17
- body: JSON.stringify({ modelImage, garmentImage })
18
- });
19
- if (!res.ok) {
20
- const data = await res.json().catch(() => ({}));
21
- if (res.status === 402) {
22
- throw new PrimeStyleError(
23
- data.message || "Insufficient tokens",
24
- "INSUFFICIENT_TOKENS"
25
- );
26
- }
27
- throw new PrimeStyleError(
28
- data.message || "Failed to submit try-on",
29
- "API_ERROR"
30
- );
31
- }
32
- return res.json();
33
- }
34
- async getStatus(jobId) {
35
- const res = await fetch(`${this.baseUrl}/api/v1/tryon/status/${jobId}`, {
36
- headers: this.headers
37
- });
38
- if (!res.ok) {
39
- const data = await res.json().catch(() => ({}));
40
- throw new PrimeStyleError(
41
- data.message || "Failed to get status",
42
- "API_ERROR"
43
- );
44
- }
45
- return res.json();
46
- }
47
- getStreamUrl() {
48
- return `${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`;
49
- }
50
- }
51
- class PrimeStyleError extends Error {
52
- constructor(message, code) {
53
- super(message);
54
- this.name = "PrimeStyleError";
55
- this.code = code;
56
- }
57
- }
58
- class SseClient {
59
- constructor(streamUrl) {
60
- this.eventSource = null;
61
- this.listeners = /* @__PURE__ */ new Map();
62
- this.reconnectTimer = null;
63
- this.reconnectAttempts = 0;
64
- this.maxReconnectAttempts = 5;
65
- this.streamUrl = streamUrl;
66
- }
67
- connect() {
68
- if (this.eventSource) return;
69
- this.eventSource = new EventSource(this.streamUrl);
70
- this.eventSource.addEventListener("vto-update", (event) => {
71
- try {
72
- const data = JSON.parse(event.data);
73
- this.emit(data.galleryId, data);
74
- } catch {
75
- }
76
- });
77
- this.eventSource.onopen = () => {
78
- this.reconnectAttempts = 0;
79
- };
80
- this.eventSource.onerror = () => {
81
- this.eventSource?.close();
82
- this.eventSource = null;
83
- this.scheduleReconnect();
84
- };
85
- }
86
- scheduleReconnect() {
87
- if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
88
- if (this.listeners.size === 0) return;
89
- const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
90
- this.reconnectAttempts++;
91
- this.reconnectTimer = setTimeout(() => {
92
- this.connect();
93
- }, delay);
94
- }
95
- onJob(jobId, callback) {
96
- if (!this.listeners.has(jobId)) {
97
- this.listeners.set(jobId, /* @__PURE__ */ new Set());
98
- }
99
- this.listeners.get(jobId).add(callback);
100
- if (!this.eventSource) {
101
- this.connect();
102
- }
103
- return () => {
104
- const jobListeners = this.listeners.get(jobId);
105
- if (jobListeners) {
106
- jobListeners.delete(callback);
107
- if (jobListeners.size === 0) {
108
- this.listeners.delete(jobId);
109
- }
110
- }
111
- if (this.listeners.size === 0) {
112
- this.disconnect();
113
- }
114
- };
115
- }
116
- emit(jobId, update) {
117
- const callbacks = this.listeners.get(jobId);
118
- if (callbacks) {
119
- callbacks.forEach((cb) => cb(update));
120
- }
121
- }
122
- disconnect() {
123
- if (this.reconnectTimer) {
124
- clearTimeout(this.reconnectTimer);
125
- this.reconnectTimer = null;
126
- }
127
- if (this.eventSource) {
128
- this.eventSource.close();
129
- this.eventSource = null;
130
- }
131
- this.listeners.clear();
132
- this.reconnectAttempts = 0;
133
- }
134
- }
1
+ import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage } from "./image-utils-usff6Qu8.js";
2
+ import { P } from "./image-utils-usff6Qu8.js";
135
3
  function detectProductImage() {
136
4
  const ogImage = document.querySelector(
137
5
  'meta[property="og:image"]'
@@ -193,51 +61,6 @@ function extractSchemaImage(data) {
193
61
  }
194
62
  return null;
195
63
  }
196
- const MAX_DIMENSION = 1024;
197
- const JPEG_QUALITY = 0.85;
198
- function compressImage(file) {
199
- return new Promise((resolve, reject) => {
200
- const reader = new FileReader();
201
- reader.onload = () => {
202
- const img = new Image();
203
- img.onload = () => {
204
- try {
205
- const canvas = document.createElement("canvas");
206
- let { width, height } = img;
207
- if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
208
- if (width > height) {
209
- height = Math.round(height * MAX_DIMENSION / width);
210
- width = MAX_DIMENSION;
211
- } else {
212
- width = Math.round(width * MAX_DIMENSION / height);
213
- height = MAX_DIMENSION;
214
- }
215
- }
216
- canvas.width = width;
217
- canvas.height = height;
218
- const ctx = canvas.getContext("2d");
219
- if (!ctx) {
220
- reject(new Error("Canvas context not available"));
221
- return;
222
- }
223
- ctx.drawImage(img, 0, 0, width, height);
224
- const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY);
225
- resolve(dataUrl);
226
- } catch (err) {
227
- reject(err);
228
- }
229
- };
230
- img.onerror = () => reject(new Error("Failed to load image"));
231
- img.src = reader.result;
232
- };
233
- reader.onerror = () => reject(new Error("Failed to read file"));
234
- reader.readAsDataURL(file);
235
- });
236
- }
237
- function isValidImageFile(file) {
238
- const accepted = ["image/jpeg", "image/png", "image/webp"];
239
- return accepted.includes(file.type);
240
- }
241
64
  function getStyles() {
242
65
  return `
243
66
  :host {
@@ -1160,7 +983,7 @@ if (typeof window !== "undefined" && !customElements.get("primestyle-tryon")) {
1160
983
  }
1161
984
  export {
1162
985
  ApiClient,
1163
- PrimeStyleError,
986
+ P as PrimeStyleError,
1164
987
  PrimeStyleTryon,
1165
988
  SseClient,
1166
989
  compressImage,
@@ -0,0 +1,33 @@
1
+ import { type CSSProperties } from "react";
2
+ import type { ButtonStyles, ModalStyles } from "../types";
3
+ export interface PrimeStyleTryonProps {
4
+ /** Product image URL to try on */
5
+ productImage: string;
6
+ /** Button text (defaults to "Virtual Try-On") */
7
+ buttonText?: string;
8
+ /** API base URL — defaults to NEXT_PUBLIC_PRIMESTYLE_API_URL env or https://api.primestyleai.com */
9
+ apiUrl?: string;
10
+ /** Show "Powered by PrimeStyle" in modal footer (defaults to true) */
11
+ showPoweredBy?: boolean;
12
+ /** Button appearance customization */
13
+ buttonStyles?: ButtonStyles;
14
+ /** Modal appearance customization */
15
+ modalStyles?: ModalStyles;
16
+ /** Additional className on the root wrapper */
17
+ className?: string;
18
+ /** Additional inline styles on the root wrapper */
19
+ style?: CSSProperties;
20
+ onOpen?: () => void;
21
+ onClose?: () => void;
22
+ onUpload?: (file: File) => void;
23
+ onProcessing?: (jobId: string) => void;
24
+ onComplete?: (result: {
25
+ jobId: string;
26
+ imageUrl: string;
27
+ }) => void;
28
+ onError?: (error: {
29
+ message: string;
30
+ code?: string;
31
+ }) => void;
32
+ }
33
+ export declare function PrimeStyleTryon({ productImage, buttonText, apiUrl, showPoweredBy, buttonStyles: btnS, modalStyles: mdlS, className, style, onOpen, onClose, onUpload, onProcessing, onComplete, onError, }: PrimeStyleTryonProps): import("react/jsx-runtime").JSX.Element;