@boldvideo/bold-js 0.9.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 844d6f1: Add persona and ai_search types to Account settings
8
+
9
+ - Added `Persona` type (discriminated union based on `enabled` flag)
10
+ - Added `AccountAISearch` type with `enabled` boolean
11
+ - Updated `Account` type to include `ai_search` and `persona` fields
12
+
13
+ ## 1.0.0
14
+
15
+ ### Major Changes
16
+
17
+ - 8afb7b3: Breaking: Unified AI API
18
+
19
+ - New methods: `bold.ai.ask()`, `bold.ai.search()`, `bold.ai.chat(videoId, opts)`
20
+ - `bold.ai.coach()` is now an alias for `bold.ai.ask()`
21
+ - New parameter: `prompt` replaces `message`
22
+ - New parameter: `stream` (boolean, default: true) for non-streaming responses
23
+ - New event types: `message_start`, `text_delta`, `sources`, `message_complete`
24
+ - New types: `AIEvent`, `AIResponse`, `Source`, `AIUsage`, `SearchOptions`, `ChatOptions`
25
+ - Removed: `CoachEvent`, `Citation`, `Usage`, `CoachOptions` (old `AskOptions`)
26
+
3
27
  ## 0.9.0
4
28
 
5
29
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -247,7 +247,7 @@ async function* parseSSE(response) {
247
247
  try {
248
248
  const event = JSON.parse(json);
249
249
  yield event;
250
- if (event.type === "complete") {
250
+ if (event.type === "message_complete" || event.type === "error") {
251
251
  await reader.cancel();
252
252
  return;
253
253
  }
@@ -259,9 +259,12 @@ async function* parseSSE(response) {
259
259
  reader.releaseLock();
260
260
  }
261
261
  }
262
+ function buildURL(baseURL, path) {
263
+ const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
264
+ return new URL(path, base);
265
+ }
262
266
  async function streamRequest(path, body, config) {
263
- const baseURL = config.baseURL.endsWith("/") ? config.baseURL : `${config.baseURL}/`;
264
- const url = new URL(path, baseURL);
267
+ const url = buildURL(config.baseURL, path);
265
268
  const response = await fetch(url, {
266
269
  method: "POST",
267
270
  headers: {
@@ -276,41 +279,66 @@ async function streamRequest(path, body, config) {
276
279
  }
277
280
  return parseSSE(response);
278
281
  }
279
- function createAI(config) {
280
- return {
281
- /**
282
- * Coach - Library-wide RAG assistant
283
- *
284
- * Requires native fetch (Node 18+ or browser)
285
- *
286
- * @example
287
- * const stream = await bold.ai.coach({ message: "How do I price my SaaS?" });
288
- * for await (const event of stream) {
289
- * if (event.type === "token") console.log(event.content);
290
- * }
291
- */
292
- async coach(options) {
293
- const path = options.conversationId ? `coach/${options.conversationId}` : "coach";
294
- const body = { message: options.message };
295
- if (options.collectionId)
296
- body.collection_id = options.collectionId;
297
- return streamRequest(path, body, config);
282
+ async function jsonRequest(path, body, config) {
283
+ const url = buildURL(config.baseURL, path);
284
+ const response = await fetch(url, {
285
+ method: "POST",
286
+ headers: {
287
+ "Content-Type": "application/json",
288
+ "Accept": "application/json",
289
+ ...config.headers
298
290
  },
299
- /**
300
- * Ask - Video-specific Q&A
301
- *
302
- * Requires native fetch (Node 18+ or browser)
303
- *
304
- * @example
305
- * const stream = await bold.ai.ask("video-id", { message: "What is this about?" });
306
- * for await (const event of stream) {
307
- * if (event.type === "token") console.log(event.content);
308
- * }
309
- */
310
- async ask(videoId, options) {
311
- const path = `videos/${videoId}/ask`;
312
- return streamRequest(path, { message: options.message }, config);
291
+ body: JSON.stringify(body)
292
+ });
293
+ if (!response.ok) {
294
+ throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
295
+ }
296
+ return response.json();
297
+ }
298
+ function createAI(config) {
299
+ async function ask(options) {
300
+ const path = options.conversationId ? `ai/ask/${options.conversationId}` : "ai/ask";
301
+ const body = { prompt: options.prompt };
302
+ if (options.collectionId)
303
+ body.collection_id = options.collectionId;
304
+ if (options.stream === false) {
305
+ body.stream = false;
306
+ return jsonRequest(path, body, config);
313
307
  }
308
+ return streamRequest(path, body, config);
309
+ }
310
+ async function coach(options) {
311
+ return ask(options);
312
+ }
313
+ async function search(options) {
314
+ const path = "ai/search";
315
+ const body = { prompt: options.prompt };
316
+ if (options.limit)
317
+ body.limit = options.limit;
318
+ if (options.collectionId)
319
+ body.collection_id = options.collectionId;
320
+ if (options.videoId)
321
+ body.video_id = options.videoId;
322
+ if (options.stream === false) {
323
+ body.stream = false;
324
+ return jsonRequest(path, body, config);
325
+ }
326
+ return streamRequest(path, body, config);
327
+ }
328
+ async function chat(videoId, options) {
329
+ const path = options.conversationId ? `ai/videos/${videoId}/chat/${options.conversationId}` : `ai/videos/${videoId}/chat`;
330
+ const body = { prompt: options.prompt };
331
+ if (options.stream === false) {
332
+ body.stream = false;
333
+ return jsonRequest(path, body, config);
334
+ }
335
+ return streamRequest(path, body, config);
336
+ }
337
+ return {
338
+ ask,
339
+ coach,
340
+ search,
341
+ chat
314
342
  };
315
343
  }
316
344
 
package/dist/index.d.ts CHANGED
@@ -133,9 +133,24 @@ type AccountAI = {
133
133
  greeting: string;
134
134
  name: string;
135
135
  };
136
+ type AccountAISearch = {
137
+ enabled: boolean;
138
+ };
139
+ type PersonaEnabled = {
140
+ enabled: true;
141
+ name: string;
142
+ greeting: string;
143
+ conversation_starters: string[];
144
+ };
145
+ type PersonaDisabled = {
146
+ enabled: false;
147
+ };
148
+ type Persona = PersonaEnabled | PersonaDisabled;
136
149
  type Account = {
137
150
  ai: AccountAI;
151
+ ai_search: AccountAISearch;
138
152
  name: string;
153
+ persona: Persona;
139
154
  slug: string;
140
155
  };
141
156
  type Settings = {
@@ -162,49 +177,164 @@ type Settings = {
162
177
  theme_config: ThemeConfig;
163
178
  version: string;
164
179
  };
165
- interface Citation {
166
- video: Pick<Video, 'internal_id' | 'title' | 'playback_id'>;
167
- start_ms: number;
168
- end_ms: number;
180
+ /**
181
+ * Source citation from AI responses
182
+ */
183
+ interface Source {
184
+ video_id: string;
185
+ title: string;
186
+ timestamp: number;
187
+ timestamp_end?: number;
169
188
  text: string;
189
+ playback_id?: string;
190
+ speaker?: string;
170
191
  }
171
- interface Usage {
172
- input_tokens: number;
173
- output_tokens: number;
192
+ /**
193
+ * Token usage statistics
194
+ */
195
+ interface AIUsage {
196
+ prompt_tokens: number;
197
+ completion_tokens: number;
198
+ total_tokens: number;
174
199
  }
175
- type CoachEvent = {
176
- type: "conversation_created";
200
+ /**
201
+ * SSE event types for AI streaming responses
202
+ */
203
+ type AIEvent = {
204
+ type: "message_start";
177
205
  id: string;
178
- status: "processing";
206
+ model?: string;
207
+ } | {
208
+ type: "sources";
209
+ sources: Source[];
210
+ } | {
211
+ type: "text_delta";
212
+ delta: string;
179
213
  } | {
180
214
  type: "clarification";
181
215
  questions: string[];
182
- mode: "clarification";
183
- needs_clarification: true;
184
- } | {
185
- type: "token";
186
- content: string;
187
216
  } | {
188
- type: "answer";
217
+ type: "message_complete";
189
218
  content: string;
190
- citations: Citation[];
191
- usage: Usage;
219
+ sources: Source[];
220
+ usage: AIUsage;
192
221
  } | {
193
222
  type: "error";
223
+ code: string;
194
224
  message: string;
195
- step?: string;
196
- reason?: string;
197
- timestamp: string;
198
- } | {
199
- type: "complete";
225
+ retryable: boolean;
226
+ details?: Record<string, unknown>;
200
227
  };
201
- interface CoachOptions {
202
- message: string;
228
+ /**
229
+ * Non-streaming AI response
230
+ */
231
+ interface AIResponse {
232
+ id: string;
233
+ content: string;
234
+ sources: Source[];
235
+ usage: AIUsage;
236
+ model?: string;
237
+ }
238
+ /**
239
+ * Options for bold.ai.ask() and bold.ai.coach()
240
+ */
241
+ interface AskOptions {
242
+ prompt: string;
243
+ stream?: boolean;
203
244
  conversationId?: string;
204
245
  collectionId?: string;
205
246
  }
206
- interface AskOptions {
207
- message: string;
247
+ /**
248
+ * Options for bold.ai.search()
249
+ */
250
+ interface SearchOptions {
251
+ prompt: string;
252
+ stream?: boolean;
253
+ limit?: number;
254
+ collectionId?: string;
255
+ videoId?: string;
256
+ }
257
+ /**
258
+ * Options for bold.ai.chat()
259
+ *
260
+ * conversationId: Pass to continue an existing conversation (multi-turn chat).
261
+ * If omitted, a new conversation is created. The id is returned in the
262
+ * message_start event - capture it to pass to subsequent requests.
263
+ */
264
+ interface ChatOptions {
265
+ prompt: string;
266
+ stream?: boolean;
267
+ conversationId?: string;
268
+ }
269
+
270
+ /**
271
+ * AI client interface for type-safe method overloading
272
+ */
273
+ interface AIClient {
274
+ /**
275
+ * Ask - Library-wide RAG assistant
276
+ *
277
+ * @example
278
+ * // Streaming (default)
279
+ * const stream = await bold.ai.ask({ prompt: "How do I price my SaaS?" });
280
+ * for await (const event of stream) {
281
+ * if (event.type === "text_delta") process.stdout.write(event.delta);
282
+ * }
283
+ *
284
+ * @example
285
+ * // Non-streaming
286
+ * const response = await bold.ai.ask({ prompt: "How do I price my SaaS?", stream: false });
287
+ * console.log(response.content);
288
+ */
289
+ ask(options: AskOptions & {
290
+ stream: false;
291
+ }): Promise<AIResponse>;
292
+ ask(options: AskOptions & {
293
+ stream?: true;
294
+ }): Promise<AsyncIterable<AIEvent>>;
295
+ ask(options: AskOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
296
+ /**
297
+ * Coach - Alias for ask() (Library-wide RAG assistant)
298
+ */
299
+ coach(options: AskOptions & {
300
+ stream: false;
301
+ }): Promise<AIResponse>;
302
+ coach(options: AskOptions & {
303
+ stream?: true;
304
+ }): Promise<AsyncIterable<AIEvent>>;
305
+ coach(options: AskOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
306
+ /**
307
+ * Search - Semantic search with light synthesis
308
+ *
309
+ * @example
310
+ * const stream = await bold.ai.search({ prompt: "pricing strategies", limit: 10 });
311
+ * for await (const event of stream) {
312
+ * if (event.type === "sources") console.log("Found:", event.sources.length, "results");
313
+ * }
314
+ */
315
+ search(options: SearchOptions & {
316
+ stream: false;
317
+ }): Promise<AIResponse>;
318
+ search(options: SearchOptions & {
319
+ stream?: true;
320
+ }): Promise<AsyncIterable<AIEvent>>;
321
+ search(options: SearchOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
322
+ /**
323
+ * Chat - Video-scoped conversation
324
+ *
325
+ * @example
326
+ * const stream = await bold.ai.chat("video-id", { prompt: "What is discussed at 5 minutes?" });
327
+ * for await (const event of stream) {
328
+ * if (event.type === "text_delta") process.stdout.write(event.delta);
329
+ * }
330
+ */
331
+ chat(videoId: string, options: ChatOptions & {
332
+ stream: false;
333
+ }): Promise<AIResponse>;
334
+ chat(videoId: string, options: ChatOptions & {
335
+ stream?: true;
336
+ }): Promise<AsyncIterable<AIEvent>>;
337
+ chat(videoId: string, options: ChatOptions): Promise<AsyncIterable<AIEvent> | AIResponse>;
208
338
  }
209
339
 
210
340
  type ClientOptions = {
@@ -235,10 +365,7 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
235
365
  data: Playlist;
236
366
  }>;
237
367
  };
238
- ai: {
239
- coach(options: CoachOptions): Promise<AsyncIterable<CoachEvent>>;
240
- ask(videoId: string, options: AskOptions): Promise<AsyncIterable<CoachEvent>>;
241
- };
368
+ ai: AIClient;
242
369
  trackEvent: (video: any, event: Event) => void;
243
370
  trackPageView: (title: string) => void;
244
371
  };
@@ -252,4 +379,4 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
252
379
  */
253
380
  declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
254
381
 
255
- export { Account, AccountAI, AskOptions, AssistantConfig, Citation, ClientOptions, CoachEvent, CoachOptions, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalLayout, PortalNavigation, PortalTheme, Settings, ThemeColors, ThemeConfig, Usage, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
382
+ export { AIEvent, AIResponse, AIUsage, Account, AccountAI, AskOptions, AssistantConfig, ChatOptions, ClientOptions, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalLayout, PortalNavigation, PortalTheme, SearchOptions, Settings, Source, ThemeColors, ThemeConfig, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
package/dist/index.js CHANGED
@@ -209,7 +209,7 @@ async function* parseSSE(response) {
209
209
  try {
210
210
  const event = JSON.parse(json);
211
211
  yield event;
212
- if (event.type === "complete") {
212
+ if (event.type === "message_complete" || event.type === "error") {
213
213
  await reader.cancel();
214
214
  return;
215
215
  }
@@ -221,9 +221,12 @@ async function* parseSSE(response) {
221
221
  reader.releaseLock();
222
222
  }
223
223
  }
224
+ function buildURL(baseURL, path) {
225
+ const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
226
+ return new URL(path, base);
227
+ }
224
228
  async function streamRequest(path, body, config) {
225
- const baseURL = config.baseURL.endsWith("/") ? config.baseURL : `${config.baseURL}/`;
226
- const url = new URL(path, baseURL);
229
+ const url = buildURL(config.baseURL, path);
227
230
  const response = await fetch(url, {
228
231
  method: "POST",
229
232
  headers: {
@@ -238,41 +241,66 @@ async function streamRequest(path, body, config) {
238
241
  }
239
242
  return parseSSE(response);
240
243
  }
241
- function createAI(config) {
242
- return {
243
- /**
244
- * Coach - Library-wide RAG assistant
245
- *
246
- * Requires native fetch (Node 18+ or browser)
247
- *
248
- * @example
249
- * const stream = await bold.ai.coach({ message: "How do I price my SaaS?" });
250
- * for await (const event of stream) {
251
- * if (event.type === "token") console.log(event.content);
252
- * }
253
- */
254
- async coach(options) {
255
- const path = options.conversationId ? `coach/${options.conversationId}` : "coach";
256
- const body = { message: options.message };
257
- if (options.collectionId)
258
- body.collection_id = options.collectionId;
259
- return streamRequest(path, body, config);
244
+ async function jsonRequest(path, body, config) {
245
+ const url = buildURL(config.baseURL, path);
246
+ const response = await fetch(url, {
247
+ method: "POST",
248
+ headers: {
249
+ "Content-Type": "application/json",
250
+ "Accept": "application/json",
251
+ ...config.headers
260
252
  },
261
- /**
262
- * Ask - Video-specific Q&A
263
- *
264
- * Requires native fetch (Node 18+ or browser)
265
- *
266
- * @example
267
- * const stream = await bold.ai.ask("video-id", { message: "What is this about?" });
268
- * for await (const event of stream) {
269
- * if (event.type === "token") console.log(event.content);
270
- * }
271
- */
272
- async ask(videoId, options) {
273
- const path = `videos/${videoId}/ask`;
274
- return streamRequest(path, { message: options.message }, config);
253
+ body: JSON.stringify(body)
254
+ });
255
+ if (!response.ok) {
256
+ throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
257
+ }
258
+ return response.json();
259
+ }
260
+ function createAI(config) {
261
+ async function ask(options) {
262
+ const path = options.conversationId ? `ai/ask/${options.conversationId}` : "ai/ask";
263
+ const body = { prompt: options.prompt };
264
+ if (options.collectionId)
265
+ body.collection_id = options.collectionId;
266
+ if (options.stream === false) {
267
+ body.stream = false;
268
+ return jsonRequest(path, body, config);
275
269
  }
270
+ return streamRequest(path, body, config);
271
+ }
272
+ async function coach(options) {
273
+ return ask(options);
274
+ }
275
+ async function search(options) {
276
+ const path = "ai/search";
277
+ const body = { prompt: options.prompt };
278
+ if (options.limit)
279
+ body.limit = options.limit;
280
+ if (options.collectionId)
281
+ body.collection_id = options.collectionId;
282
+ if (options.videoId)
283
+ body.video_id = options.videoId;
284
+ if (options.stream === false) {
285
+ body.stream = false;
286
+ return jsonRequest(path, body, config);
287
+ }
288
+ return streamRequest(path, body, config);
289
+ }
290
+ async function chat(videoId, options) {
291
+ const path = options.conversationId ? `ai/videos/${videoId}/chat/${options.conversationId}` : `ai/videos/${videoId}/chat`;
292
+ const body = { prompt: options.prompt };
293
+ if (options.stream === false) {
294
+ body.stream = false;
295
+ return jsonRequest(path, body, config);
296
+ }
297
+ return streamRequest(path, body, config);
298
+ }
299
+ return {
300
+ ask,
301
+ coach,
302
+ search,
303
+ chat
276
304
  };
277
305
  }
278
306
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boldvideo/bold-js",
3
3
  "license": "MIT",
4
- "version": "0.9.0",
4
+ "version": "1.0.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",