@simula/ads 1.1.0 → 1.2.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 CHANGED
@@ -45,8 +45,7 @@ function ChatInterface() {
45
45
  {msg.role === "assistant" && (
46
46
  <InChatAdSlot
47
47
  messages={messages.slice(0, i + 1)}
48
- formats="all"
49
- theme={{ theme: "light", accent: "blue" }}
48
+ theme={{ mode: "light", accent: "blue" }}
50
49
  />
51
50
  )}
52
51
  </div>
@@ -120,8 +119,7 @@ Displays an ad based on conversation context.
120
119
  | -------------- | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
120
  | `messages` | `Message[]` | ✅ | — | Array of `{ role, content }`; pass recent conversation (e.g. last 6 turns). |
122
121
  | `trigger` | `Promise<any>` | ❌ | Fires immediately on viewability | Promise to await before fetching the ad (e.g. LLM call). |
123
- | `formats` | `string \| string[]` | ❌ | `['all']` | Preferred ad formats: `'all'`, `'tips'`, `'interactive'`, `'suggestions'`, `'text'`, `'highlight'`, `'visual_banner'`, `'image_feature'`, or an array like `['text', 'highlight']`. See [Appendix: Ad Formats](#appendix-ad-formats) for visual examples.<br>**A/B Testing:** Pass an array to automatically A/B test different formats and let Simula pick the best over time. |
124
- | `theme` | `SimulaTheme` | ❌ | `{ theme: 'auto', width: 'auto', accent: ['neutral','image'], font: 'sans-serif', cornerRadius: 8 }` | Customize ad appearance (see Theme Options). Arrays trigger A/B testing. |
122
+ | `theme` | `SimulaTheme` | ❌ | `{ mode: 'auto', width: 'auto', accent: ['neutral','image'], font: 'sans-serif', cornerRadius: 8 }` | Customize ad appearance (see Theme Options). Arrays trigger A/B testing. |
125
123
  | `charDesc` | `string` | ❌ | `undefined` | Character description for additional context to improve ad targeting. |
126
124
  | `debounceMs` | `number` | ❌ | `0` | Delay in milliseconds before fetching. |
127
125
  | `onImpression` | `(ad: AdData) => void` | ❌ | `undefined` | Callback when ad is viewable (50% visible for ≥1s). |
@@ -141,7 +139,7 @@ Displays an ad based on conversation context.
141
139
 
142
140
  ```ts
143
141
  interface SimulaTheme {
144
- theme?: "light" | "dark" | "auto"; // default: "auto"
142
+ mode?: "light" | "dark" | "auto"; // default: "auto"
145
143
  accent?: AccentOption | AccentOption[]; // default: ["neutral", "image"] (A/B tested)
146
144
  font?: FontOption | FontOption[]; // default: "sans-serif"
147
145
  width?: number | string; // default: "auto" (min 320px)
@@ -158,26 +156,28 @@ interface SimulaTheme {
158
156
  > **Width:** min **320px**, accepts px/%, or `auto`
159
157
 
160
158
  > **A/B Testing:**
161
- > When you pass an **array** (e.g., `accent: ['blue', 'green', 'purple']`), Simula will **automatically A/B test** across the provided options—colors, fonts, or formats—and **optimize over time for the best-performing variant**.
159
+ > When you pass an **array** (e.g., `accent: ['blue', 'green', 'purple']`), Simula will **automatically A/B test** across the provided options—colors or fonts—and **optimize over time for the best-performing variant**.
160
+ >
161
+ > **💡 We strongly encourage adding the `"image"` option for accent** (e.g., `accent: ['blue', 'image']`)
162
162
 
163
163
  ```tsx
164
- // Default theme (auto)
164
+ // Default mode (auto)
165
165
  <InChatAdSlot messages={messages} />
166
166
 
167
- // Light theme
168
- <InChatAdSlot messages={messages} theme={{ theme: "light", accent: "blue" }} />
167
+ // Light mode
168
+ <InChatAdSlot messages={messages} theme={{ mode: "light", accent: "blue" }} />
169
169
 
170
- // Dark theme with custom width
170
+ // Dark mode with custom width
171
171
  <InChatAdSlot
172
172
  messages={messages}
173
- theme={{ theme: "dark", accent: "purple", width: 600, cornerRadius: 12 }}
173
+ theme={{ mode: "dark", accent: "purple", width: 600, cornerRadius: 12 }}
174
174
  />
175
175
 
176
- // A/B testing
176
+ // A/B testing (recommended: include "image" for best performance)
177
177
  <InChatAdSlot
178
178
  messages={messages}
179
179
  theme={{
180
- accent: ["blue", "green", "purple"], // A/B test colors
180
+ accent: ["blue", "green", "image"], // A/B test colors - include "image" for best CPM
181
181
  font: ["sans-serif", "serif"], // A/B test fonts
182
182
  width: "100%"
183
183
  }}
@@ -247,8 +247,7 @@ function ChatApp() {
247
247
  key={`adslot-${i}`} // ✅ Required if rendering in a list
248
248
  trigger={msg.llmPromise} // default: fires immediately if not provided
249
249
  messages={messages.slice(0, i + 1)}
250
- formats="all"
251
- theme={{ theme: "light", accent: "blue", width: "auto" }}
250
+ theme={{ mode: "light", accent: "blue", width: "auto" }}
252
251
  />
253
252
  )}
254
253
  </div>
@@ -284,7 +283,7 @@ function ChatApp() {
284
283
  * **Session Management** – Automatic
285
284
  * **Robust Error Handling** – Graceful degradation & callbacks
286
285
  * **TypeScript Support** – Built-in type definitions
287
- * **Built-in A/B Testing** – Test multiple colors, formats, or fonts by passing arrays and let Simula optimize performance over time
286
+ * **Built-in A/B Testing** – Test multiple colors or fonts by passing arrays and let Simula optimize performance over time
288
287
 
289
288
  ---
290
289
 
@@ -342,6 +341,8 @@ MIT
342
341
 
343
342
  ## 📑 Appendix: Ad Formats
344
343
 
344
+ Format examples are shown below, as well as more custom formats. If you want any formats excluded, reach out to us and we can exclude it programmatically in the backend. But we strongly encourage allowing all formats so we can A/B test the one your users are most receptive towards to maximize CPM.
345
+
345
346
  Visual examples of all available ad formats across mobile and desktop:
346
347
 
347
348
  | Format | Mobile | Desktop |
@@ -352,55 +353,4 @@ Visual examples of all available ad formats across mobile and desktop:
352
353
  | **text** | <img src="./assets/text_mobile.png" width="200" alt="Text format on mobile" /> | <img src="./assets/text_desktop.png" width="400" alt="Text format on desktop" /> |
353
354
  | **highlight** | <img src="./assets/highlight_mobile.png" width="200" alt="Highlight format on mobile" /> | <img src="./assets/highlight_desktop.png" width="400" alt="Highlight format on desktop" /> |
354
355
  | **visual_banner** | <img src="./assets/visual_banner_mobile.png" width="200" alt="Visual banner format on mobile" /> | <img src="./assets/visual_banner_desktop.png" width="400" alt="Visual banner format on desktop" /> |
355
- | **image_feature** | <img src="./assets/image_feature_mobile.png" width="200" alt="Image feature format on mobile" /> | <img src="./assets/image_feature_desktop.png" width="400" alt="Image feature format on desktop" /> |
356
-
357
- > **Note:** The `'all'` format allows Simula to automatically select the best format based on context and performance.
358
-
359
- ---
360
-
361
- ## 📑 Appendix: Invalid Format & Accent Combinations
362
-
363
- Certain ad formats have restrictions on which accent colors can be used. **These restrictions only apply when you specify an invalid combination with no other valid options.** When A/B testing with arrays, Simula automatically selects valid combinations from the available options.
364
-
365
- | Format | Allowed Accents | Restrictions |
366
- |--------|----------------|--------------|
367
- | **interactive** | All colors, `'image'`, `'neutral'`, `'gray'`, `'tan'` | ❌ Cannot use `'transparent'` |
368
- | **tips** | All colors, `'image'`, `'neutral'`, `'gray'`, `'tan'` | ❌ Cannot use `'transparent'` |
369
- | **text** | **Only** `'transparent'` | ❌ Cannot use colors or `'image'` |
370
- | **highlight** | All colors, `'image'`, `'neutral'`, `'gray'`, `'tan'` | ❌ Cannot use `'transparent'` |
371
- | **visual_banner** | All colors, `'neutral'`, `'gray'`, `'tan'` | ❌ Cannot use `'image'` or `'transparent'` |
372
- | **image_feature** | All colors, `'neutral'`, `'gray'`, `'tan'` | ❌ Cannot use `'image'` or `'transparent'` |
373
- | **suggestions** | All accents allowed | ✅ No restrictions |
374
-
375
- **Color accents:** `'blue'`, `'red'`, `'green'`, `'yellow'`, `'purple'`, `'pink'`, `'orange'`, `'neutral'`, `'gray'`, `'tan'`
376
-
377
- ### Examples
378
-
379
- ```tsx
380
- // ✅ Valid - single format, single accent
381
- <InChatAdSlot formats="interactive" theme={{ accent: "blue" }} />
382
- <InChatAdSlot formats="text" theme={{ accent: "transparent" }} />
383
- <InChatAdSlot formats="visual_banner" theme={{ accent: "purple" }} />
384
-
385
- // ✅ Valid - A/B testing with arrays (Simula auto-selects valid combinations)
386
- <InChatAdSlot
387
- formats={['text', 'highlight']}
388
- theme={{ accent: ['image', 'transparent'] }}
389
- />
390
- // If 'text' is selected → uses 'transparent' (skips 'image')
391
- // If 'highlight' is selected → uses 'image' (skips 'transparent')
392
-
393
- <InChatAdSlot
394
- formats={['interactive', 'visual_banner']}
395
- theme={{ accent: ['blue', 'transparent'] }}
396
- />
397
- // If 'interactive' is selected → uses 'blue' (skips 'transparent')
398
- // If 'visual_banner' is selected → uses 'blue' (skips 'transparent')
399
-
400
- // ❌ Invalid - no valid options available
401
- <InChatAdSlot formats="interactive" theme={{ accent: "transparent" }} /> // interactive cannot use transparent (no fallback)
402
- <InChatAdSlot formats="text" theme={{ accent: "blue" }} /> // text can ONLY use transparent (no fallback)
403
- <InChatAdSlot formats="text" theme={{ accent: ['blue', 'red', 'image'] }} /> // text needs transparent but none provided
404
- ```
405
-
406
- > **Key Point:** Restrictions only matter when there are **no valid alternatives**. When A/B testing with multiple formats or accents, Simula intelligently matches compatible combinations.
356
+ |
@@ -1 +1 @@
1
- {"version":3,"file":"InChatAdSlot.d.ts","sourceRoot":"","sources":["../src/InChatAdSlot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AASjF,OAAO,EAAE,iBAAiB,EAAU,MAAM,SAAS,CAAC;AAepD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAyXpD,CAAC"}
1
+ {"version":3,"file":"InChatAdSlot.d.ts","sourceRoot":"","sources":["../src/InChatAdSlot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AASjF,OAAO,EAAE,iBAAiB,EAAU,MAAM,SAAS,CAAC;AAepD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAqXpD,CAAC"}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import React$1 from 'react';
3
3
  type AccentOption = 'blue' | 'red' | 'green' | 'yellow' | 'purple' | 'pink' | 'orange' | 'neutral' | 'gray' | 'tan' | 'transparent' | 'image';
4
4
  type FontOption = 'san-serif' | 'serif' | 'monospace';
5
5
  interface SimulaTheme {
6
+ mode?: 'light' | 'dark' | 'auto';
6
7
  theme?: 'light' | 'dark' | 'auto';
7
8
  accent?: AccentOption | AccentOption[];
8
9
  font?: FontOption | FontOption[];
package/dist/index.js CHANGED
@@ -52,14 +52,19 @@ const fetchAd = async (request) => {
52
52
  try {
53
53
  const conversationHistory = request.messages;
54
54
  // Normalize theme accent and font to arrays for backend
55
- const normalizedTheme = request.theme ? {
56
- ...request.theme,
57
- accent: request.theme.accent ? (Array.isArray(request.theme.accent) ? request.theme.accent : [request.theme.accent]) : undefined,
58
- font: request.theme.font ? (Array.isArray(request.theme.font) ? request.theme.font : [request.theme.font]) : undefined,
59
- } : undefined;
55
+ // Also handle backward compatibility: prefer 'mode' over 'theme', but support both
56
+ const normalizedTheme = request.theme ? (() => {
57
+ var _a;
58
+ const { theme: themeDeprecated, ...themeRest } = request.theme;
59
+ return {
60
+ ...themeRest,
61
+ mode: (_a = request.theme.mode) !== null && _a !== void 0 ? _a : themeDeprecated, // Prefer 'mode', fallback to 'theme' for backward compatibility
62
+ accent: request.theme.accent ? (Array.isArray(request.theme.accent) ? request.theme.accent : [request.theme.accent]) : undefined,
63
+ font: request.theme.font ? (Array.isArray(request.theme.font) ? request.theme.font : [request.theme.font]) : undefined,
64
+ };
65
+ })() : undefined;
60
66
  const requestBody = {
61
67
  messages: conversationHistory,
62
- types: request.formats,
63
68
  slot_id: request.slotId,
64
69
  theme: normalizedTheme,
65
70
  session_id: request.sessionId,
@@ -212,10 +217,6 @@ const validateInChatAdSlotProps = (props) => {
212
217
  throw new Error(`Invalid message at index ${i}: "content" must be a string`);
213
218
  }
214
219
  });
215
- // Validate formats (optional)
216
- if (props.formats !== undefined) {
217
- validateFormats(props.formats);
218
- }
219
220
  // Validate theme (optional)
220
221
  if (props.theme !== undefined) {
221
222
  validateTheme(props.theme);
@@ -246,50 +247,33 @@ const validateInChatAdSlotProps = (props) => {
246
247
  // Note: trigger validation (Promise check) is skipped because checking instanceof Promise
247
248
  // is unreliable across different Promise implementations and contexts
248
249
  };
249
- /**
250
- * Validates formats - accepts string or string[]
251
- * Throws descriptive errors for invalid formats
252
- */
253
- const validateFormats = (formats) => {
254
- if (!formats)
255
- return;
256
- const validFormatOptions = ['all', 'tips', 'interactive', 'suggestions', 'text', 'highlight', 'visual_banner', 'image_feature'];
257
- // Normalize to array for validation
258
- const formatsArray = Array.isArray(formats) ? formats : [formats];
259
- formatsArray.forEach((format, i) => {
260
- if (typeof format !== 'string') {
261
- throw new Error(`Invalid format type at index ${i}: "${typeof format}". Must be a string. Valid values: ${validFormatOptions.join(', ')}`);
262
- }
263
- if (!validFormatOptions.includes(format)) {
264
- throw new Error(`Invalid format value${Array.isArray(formats) ? ` at index ${i}` : ''}: "${format}". Valid values: ${validFormatOptions.join(', ')}`);
265
- }
266
- });
267
- };
268
250
  /**
269
251
  * Validates theme object
270
252
  * Throws descriptive errors for invalid theme properties
271
253
  */
272
254
  const validateTheme = (theme) => {
255
+ var _a;
273
256
  if (!theme || typeof theme !== 'object') {
274
257
  throw new Error('Invalid "theme": must be an object');
275
258
  }
276
- const validThemeOptions = ['light', 'dark', 'auto'];
259
+ const validModeOptions = ['light', 'dark', 'auto'];
277
260
  const validAccentOptions = ['blue', 'red', 'green', 'yellow', 'purple', 'pink', 'orange', 'neutral', 'gray', 'tan', 'transparent', 'image'];
278
261
  const validFontOptions = ['san-serif', 'serif', 'monospace'];
279
- const validKeys = ['theme', 'accent', 'font', 'width', 'cornerRadius'];
262
+ const validKeys = ['mode', 'theme', 'accent', 'font', 'width', 'cornerRadius']; // 'theme' kept for backward compatibility
280
263
  // Check for invalid top-level keys
281
264
  Object.keys(theme).forEach(key => {
282
265
  if (!validKeys.includes(key)) {
283
266
  throw new Error(`Invalid theme parameter "${key}". Valid parameters: ${validKeys.join(', ')}`);
284
267
  }
285
268
  });
286
- // Validate theme value
287
- if (theme.theme !== undefined) {
288
- if (typeof theme.theme !== 'string') {
289
- throw new Error(`Invalid theme type "${typeof theme.theme}". Must be a string. Valid values: ${validThemeOptions.join(', ')}`);
269
+ // Validate mode value (prefer 'mode' over 'theme' for backward compatibility)
270
+ const modeValue = (_a = theme.mode) !== null && _a !== void 0 ? _a : theme.theme;
271
+ if (modeValue !== undefined) {
272
+ if (typeof modeValue !== 'string') {
273
+ throw new Error(`Invalid mode/theme type "${typeof modeValue}". Must be a string. Valid values: ${validModeOptions.join(', ')}`);
290
274
  }
291
- if (!validThemeOptions.includes(theme.theme)) {
292
- throw new Error(`Invalid theme value "${theme.theme}". Valid values: ${validThemeOptions.join(', ')}`);
275
+ if (!validModeOptions.includes(modeValue)) {
276
+ throw new Error(`Invalid mode/theme value "${modeValue}". Valid values: ${validModeOptions.join(', ')}`);
293
277
  }
294
278
  }
295
279
  // Validate accent value(s)
@@ -1589,7 +1573,10 @@ const getFontStyles = (font = 'san-serif') => {
1589
1573
  };
1590
1574
 
1591
1575
  const createInChatAdSlotCSS = (theme = {}) => {
1592
- const { theme: themeMode = 'light', accent = 'blue', font = 'san-serif', width = 'auto', } = theme;
1576
+ var _a, _b;
1577
+ // Backward compatibility: prefer 'mode' over 'theme', but support both
1578
+ const themeMode = (_b = (_a = theme.mode) !== null && _a !== void 0 ? _a : theme.theme) !== null && _b !== void 0 ? _b : 'light';
1579
+ const { accent = 'blue', font = 'san-serif', width = 'auto', } = theme;
1593
1580
  // If accent or font is an array, use the first value for styling
1594
1581
  Array.isArray(accent) ? accent[0] : accent;
1595
1582
  const effectiveFont = Array.isArray(font) ? font[0] : font;
@@ -1744,9 +1731,8 @@ const hasValidMessages = (messages) => {
1744
1731
  const InChatAdSlot = (props) => {
1745
1732
  // Validate props early
1746
1733
  validateInChatAdSlotProps(props);
1747
- const { messages, trigger, formats: formatsRaw = ['all'], theme = {}, debounceMs = 0, charDesc, onImpression, onClick, onError, } = props;
1748
- // Normalize formats to array (similar to theme.accent and theme.font)
1749
- const formats = Array.isArray(formatsRaw) ? formatsRaw : [formatsRaw];
1734
+ const { messages, trigger, formats, // Deprecated: ignored for backward compatibility
1735
+ theme = {}, debounceMs = 0, charDesc, onImpression, onClick, onError, } = props;
1750
1736
  const { apiKey, sessionId } = useSimula();
1751
1737
  // Generate a stable slotId for this component instance
1752
1738
  const slotId = react.useMemo(() => `slot-${v4()}`, []);
@@ -1878,7 +1864,6 @@ const InChatAdSlot = (props) => {
1878
1864
  }
1879
1865
  const result = await fetchAd({
1880
1866
  messages,
1881
- formats,
1882
1867
  apiKey,
1883
1868
  slotId,
1884
1869
  theme: themeForBackend,
@@ -1909,7 +1894,7 @@ const InChatAdSlot = (props) => {
1909
1894
  finally {
1910
1895
  setLoading(false);
1911
1896
  }
1912
- }, [hasBeenViewed, loading, hasTriggered, error, messages, formats, apiKey, slotId, theme, onError, isBot, reasons, sessionId, measuredWidth]);
1897
+ }, [hasBeenViewed, loading, hasTriggered, error, messages, apiKey, slotId, theme, onError, isBot, reasons, sessionId, measuredWidth]);
1913
1898
  useDebounce(() => {
1914
1899
  if (shouldFetch) {
1915
1900
  fetchAdData();