@boldvideo/bold-js 0.8.0 → 0.8.2
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/AGENTS.md +28 -2
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +367 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +328 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -22,10 +22,36 @@ pnpm install # Install dependencies
|
|
|
22
22
|
pnpm run build # Build to dist/ (ESM + CJS + type declarations)
|
|
23
23
|
pnpm run lint # Type checking with tsc
|
|
24
24
|
pnpm changeset # Create a changeset for versioning
|
|
25
|
-
pnpm changeset version # Apply changesets and bump versions
|
|
26
|
-
pnpm changeset publish # Publish to npm
|
|
27
25
|
```
|
|
28
26
|
|
|
27
|
+
## Release Workflow
|
|
28
|
+
|
|
29
|
+
This project uses [Changesets](https://github.com/changesets/changesets) with automated GitHub Actions for publishing.
|
|
30
|
+
|
|
31
|
+
### How to Release
|
|
32
|
+
|
|
33
|
+
1. **Create a changeset** when making changes that should be released:
|
|
34
|
+
```bash
|
|
35
|
+
pnpm changeset
|
|
36
|
+
```
|
|
37
|
+
This creates a markdown file in `.changeset/` describing the change and version bump type (patch/minor/major).
|
|
38
|
+
|
|
39
|
+
2. **Commit and push** your changes (including the changeset file) to `main`.
|
|
40
|
+
|
|
41
|
+
3. **Automated release process**:
|
|
42
|
+
- The `changeset-release.yml` workflow runs on push to `main`
|
|
43
|
+
- If changesets exist, it creates a "Release PR" that:
|
|
44
|
+
- Bumps the version in `package.json`
|
|
45
|
+
- Updates `CHANGELOG.md`
|
|
46
|
+
- Removes the changeset files
|
|
47
|
+
- When you merge that Release PR, the workflow automatically publishes to npm
|
|
48
|
+
|
|
49
|
+
### Important Notes
|
|
50
|
+
|
|
51
|
+
- **Never run `pnpm changeset version` or `pnpm changeset publish` locally** - the GitHub Action handles this automatically
|
|
52
|
+
- The CI workflow (`ci.yml`) runs `pnpm run build` which generates the `dist/` folder - this ensures compiled files are always included in the published package
|
|
53
|
+
- The `release.yml` workflow can also publish when a `v*` tag is pushed (alternative release method)
|
|
54
|
+
|
|
29
55
|
## Project Structure
|
|
30
56
|
|
|
31
57
|
```
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @boldvideo/bold-js
|
|
2
2
|
|
|
3
|
+
## 0.8.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e8876a9: Fix release workflow to build dist files before publishing
|
|
8
|
+
|
|
9
|
+
## 0.8.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c643c04: Fix missing compiled dist files in published package
|
|
14
|
+
|
|
3
15
|
## 0.8.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
|
|
34
|
+
DEFAULT_INTERNAL_API_BASE_URL: () => DEFAULT_INTERNAL_API_BASE_URL,
|
|
35
|
+
createClient: () => createClient
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(src_exports);
|
|
38
|
+
|
|
39
|
+
// src/lib/client.ts
|
|
40
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
41
|
+
|
|
42
|
+
// src/lib/fetchers.ts
|
|
43
|
+
async function get(client, url) {
|
|
44
|
+
try {
|
|
45
|
+
const res = await client.get(url);
|
|
46
|
+
if (res.status !== 200) {
|
|
47
|
+
throw new Error(`Unexpected response status: ${res.status}`);
|
|
48
|
+
}
|
|
49
|
+
return res.data;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`Error fetching data from URL: ${url}`, error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function fetchSettings(client) {
|
|
56
|
+
return async (videoLimit = 12) => {
|
|
57
|
+
try {
|
|
58
|
+
return await get(
|
|
59
|
+
client,
|
|
60
|
+
`settings?limit=${videoLimit}`
|
|
61
|
+
);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Error fetching settings with limit: ${videoLimit}`, error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function fetchVideos(client) {
|
|
69
|
+
return async (videoLimit = 12) => {
|
|
70
|
+
try {
|
|
71
|
+
return await get(
|
|
72
|
+
client,
|
|
73
|
+
`videos/latest?limit=${videoLimit}`
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`Error fetching videos with limit: ${videoLimit}`, error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function searchVideos(client) {
|
|
82
|
+
return async (term) => {
|
|
83
|
+
try {
|
|
84
|
+
return await get(client, `videos?query=${term}`);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`Error searching for videos with term: ${term}`, error);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function fetchVideo(client) {
|
|
92
|
+
return async (id) => {
|
|
93
|
+
try {
|
|
94
|
+
return await get(client, `videos/${id}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Error fetching video with ID: ${id}`, error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function fetchPlaylists(client) {
|
|
102
|
+
return async () => {
|
|
103
|
+
try {
|
|
104
|
+
return await get(client, "playlists");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("Error fetching playlists", error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function fetchPlaylist(client) {
|
|
112
|
+
return async (id) => {
|
|
113
|
+
try {
|
|
114
|
+
return await get(client, `playlists/${id}`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`Error fetching playlist with ID: ${id}`, error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/util/throttle.ts
|
|
123
|
+
var throttle = (fn, delay) => {
|
|
124
|
+
let wait = false;
|
|
125
|
+
let timeout;
|
|
126
|
+
let cancelled = false;
|
|
127
|
+
return [
|
|
128
|
+
(...args) => {
|
|
129
|
+
if (cancelled)
|
|
130
|
+
return void 0;
|
|
131
|
+
if (wait)
|
|
132
|
+
return void 0;
|
|
133
|
+
const val = fn(...args);
|
|
134
|
+
wait = true;
|
|
135
|
+
timeout = window.setTimeout(() => {
|
|
136
|
+
wait = false;
|
|
137
|
+
}, delay);
|
|
138
|
+
return val;
|
|
139
|
+
},
|
|
140
|
+
() => {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/lib/tracking.ts
|
|
148
|
+
function sendEvent(client, eventName, data, debug) {
|
|
149
|
+
const payload = {
|
|
150
|
+
n: eventName,
|
|
151
|
+
u: data.url,
|
|
152
|
+
usr: data.userId,
|
|
153
|
+
d: data.domain,
|
|
154
|
+
ua: data.userAgent,
|
|
155
|
+
w: data.deviceWidth,
|
|
156
|
+
vid: data?.videoId || void 0,
|
|
157
|
+
vt: data.title,
|
|
158
|
+
vdur: data?.videoDuration || void 0,
|
|
159
|
+
time: data?.currentTime || void 0
|
|
160
|
+
};
|
|
161
|
+
if (debug)
|
|
162
|
+
console.log(`Bold SDK - Logging event '${eventName}'`, payload);
|
|
163
|
+
client.post("/event", payload);
|
|
164
|
+
}
|
|
165
|
+
var [throttledSendEvent] = throttle(sendEvent, 5e3);
|
|
166
|
+
function trackEvent(client, userId, options) {
|
|
167
|
+
return (video, event) => {
|
|
168
|
+
const eventDetails = {
|
|
169
|
+
...basicInfos(),
|
|
170
|
+
userId,
|
|
171
|
+
videoId: video.id,
|
|
172
|
+
title: video.title,
|
|
173
|
+
videoDuration: video.duration,
|
|
174
|
+
currentTime: event.target?.currentTime || 0
|
|
175
|
+
};
|
|
176
|
+
if (event.type == "timeupdate" || event.type == "time-update") {
|
|
177
|
+
throttledSendEvent(
|
|
178
|
+
client,
|
|
179
|
+
getEventName(event),
|
|
180
|
+
eventDetails,
|
|
181
|
+
options.debug
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
sendEvent(client, getEventName(event), eventDetails, options.debug);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function trackPageView(client, userId, options) {
|
|
189
|
+
return (title) => {
|
|
190
|
+
const eventDetails = {
|
|
191
|
+
...basicInfos(),
|
|
192
|
+
userId,
|
|
193
|
+
title
|
|
194
|
+
};
|
|
195
|
+
sendEvent(client, "page_view", eventDetails, options.debug);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function getEventName(event) {
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case "pause":
|
|
201
|
+
return "video_pause";
|
|
202
|
+
case "play":
|
|
203
|
+
return "video_resume";
|
|
204
|
+
case "loadedmetadata":
|
|
205
|
+
case "loaded-metadata":
|
|
206
|
+
return "video_load";
|
|
207
|
+
case "time-update":
|
|
208
|
+
case "timeupdate":
|
|
209
|
+
return "video_progress";
|
|
210
|
+
default:
|
|
211
|
+
return "unknown_event";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function basicInfos() {
|
|
215
|
+
return {
|
|
216
|
+
url: location.href,
|
|
217
|
+
domain: location.hostname,
|
|
218
|
+
referrer: document.referrer || null,
|
|
219
|
+
deviceWidth: window.innerWidth,
|
|
220
|
+
userAgent: navigator.userAgent
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/lib/ai.ts
|
|
225
|
+
async function* parseSSE(response) {
|
|
226
|
+
const reader = response.body?.getReader();
|
|
227
|
+
if (!reader) {
|
|
228
|
+
throw new Error("Response body is not readable");
|
|
229
|
+
}
|
|
230
|
+
const decoder = new TextDecoder();
|
|
231
|
+
let buffer = "";
|
|
232
|
+
try {
|
|
233
|
+
while (true) {
|
|
234
|
+
const { done, value } = await reader.read();
|
|
235
|
+
if (done)
|
|
236
|
+
break;
|
|
237
|
+
buffer += decoder.decode(value, { stream: true });
|
|
238
|
+
const lines = buffer.split(/\r?\n\r?\n/);
|
|
239
|
+
buffer = lines.pop() || "";
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed || !trimmed.startsWith("data:"))
|
|
243
|
+
continue;
|
|
244
|
+
const json = trimmed.slice(5).trim();
|
|
245
|
+
if (!json)
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
const event = JSON.parse(json);
|
|
249
|
+
yield event;
|
|
250
|
+
if (event.type === "complete") {
|
|
251
|
+
await reader.cancel();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
reader.releaseLock();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function streamRequest(path, body, config) {
|
|
263
|
+
const baseURL = config.baseURL.endsWith("/") ? config.baseURL : `${config.baseURL}/`;
|
|
264
|
+
const url = new URL(path, baseURL);
|
|
265
|
+
const response = await fetch(url, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
"Content-Type": "application/json",
|
|
269
|
+
"Accept": "text/event-stream",
|
|
270
|
+
...config.headers
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify(body)
|
|
273
|
+
});
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
|
|
276
|
+
}
|
|
277
|
+
return parseSSE(response);
|
|
278
|
+
}
|
|
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);
|
|
298
|
+
},
|
|
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);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/lib/constants.ts
|
|
318
|
+
var DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
|
|
319
|
+
var DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
|
|
320
|
+
|
|
321
|
+
// src/lib/client.ts
|
|
322
|
+
function createClient(apiKey, options = {}) {
|
|
323
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
324
|
+
throw new Error("API key is missing or invalid");
|
|
325
|
+
}
|
|
326
|
+
const { debug = false, headers = {} } = options;
|
|
327
|
+
const apiClientOptions = {
|
|
328
|
+
baseURL: options.baseURL ?? DEFAULT_API_BASE_URL,
|
|
329
|
+
headers: {
|
|
330
|
+
Authorization: apiKey,
|
|
331
|
+
...headers
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
let apiClient;
|
|
335
|
+
try {
|
|
336
|
+
apiClient = import_axios.default.create(apiClientOptions);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error("Error creating API client", error);
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
const userId = [...Array(30)].map(() => Math.random().toString(36)[2]).join("");
|
|
342
|
+
const aiConfig = {
|
|
343
|
+
baseURL: apiClientOptions.baseURL,
|
|
344
|
+
headers: apiClientOptions.headers
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
settings: fetchSettings(apiClient),
|
|
348
|
+
videos: {
|
|
349
|
+
list: fetchVideos(apiClient),
|
|
350
|
+
get: fetchVideo(apiClient),
|
|
351
|
+
search: searchVideos(apiClient)
|
|
352
|
+
},
|
|
353
|
+
playlists: {
|
|
354
|
+
list: fetchPlaylists(apiClient),
|
|
355
|
+
get: fetchPlaylist(apiClient)
|
|
356
|
+
},
|
|
357
|
+
ai: createAI(aiConfig),
|
|
358
|
+
trackEvent: trackEvent(apiClient, userId, { debug }),
|
|
359
|
+
trackPageView: trackPageView(apiClient, userId, { debug })
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
363
|
+
0 && (module.exports = {
|
|
364
|
+
DEFAULT_API_BASE_URL,
|
|
365
|
+
DEFAULT_INTERNAL_API_BASE_URL,
|
|
366
|
+
createClient
|
|
367
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
type VideoAttachment = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
file_url: string;
|
|
5
|
+
file_size?: number;
|
|
6
|
+
file_type?: string;
|
|
7
|
+
};
|
|
8
|
+
type VideoDownloadUrls = {
|
|
9
|
+
mp4?: string;
|
|
10
|
+
audio?: string;
|
|
11
|
+
legacy_mp4?: string;
|
|
12
|
+
};
|
|
13
|
+
type VideoSubtitles = {
|
|
14
|
+
label: string;
|
|
15
|
+
url: string;
|
|
16
|
+
engine?: string;
|
|
17
|
+
language: string;
|
|
18
|
+
};
|
|
19
|
+
type VideoTranscript = {
|
|
20
|
+
text: string;
|
|
21
|
+
json: any;
|
|
22
|
+
};
|
|
23
|
+
type VideoMetadata = {
|
|
24
|
+
description: string;
|
|
25
|
+
title: string;
|
|
26
|
+
image: string | null;
|
|
27
|
+
};
|
|
28
|
+
type Video = {
|
|
29
|
+
captions: string;
|
|
30
|
+
captions_label: string;
|
|
31
|
+
captions_lang: string;
|
|
32
|
+
description: string | null;
|
|
33
|
+
duration: number;
|
|
34
|
+
id: string;
|
|
35
|
+
imported_from: string | null;
|
|
36
|
+
legacy_video_url: string | null;
|
|
37
|
+
playback_id: string;
|
|
38
|
+
published_at: string;
|
|
39
|
+
stream_url: string;
|
|
40
|
+
teaser: string | null;
|
|
41
|
+
thumbnail: string;
|
|
42
|
+
title: string;
|
|
43
|
+
type: string;
|
|
44
|
+
meta_data: VideoMetadata;
|
|
45
|
+
chapters?: string;
|
|
46
|
+
attachments?: VideoAttachment[];
|
|
47
|
+
cta?: any | null;
|
|
48
|
+
download_urls?: VideoDownloadUrls;
|
|
49
|
+
internal_id?: string;
|
|
50
|
+
playback_speed?: number;
|
|
51
|
+
subtitles?: VideoSubtitles;
|
|
52
|
+
tags?: string[];
|
|
53
|
+
transcript?: VideoTranscript;
|
|
54
|
+
};
|
|
55
|
+
type Playlist = {
|
|
56
|
+
description?: string;
|
|
57
|
+
id: string;
|
|
58
|
+
is_private: boolean;
|
|
59
|
+
title: string;
|
|
60
|
+
type: string;
|
|
61
|
+
videos: Video[];
|
|
62
|
+
};
|
|
63
|
+
type MenuItem = {
|
|
64
|
+
icon: string;
|
|
65
|
+
is_ext: boolean;
|
|
66
|
+
label: string;
|
|
67
|
+
url: string;
|
|
68
|
+
};
|
|
69
|
+
type PortalDisplay = {
|
|
70
|
+
show_chapters: boolean;
|
|
71
|
+
show_transcripts: boolean;
|
|
72
|
+
};
|
|
73
|
+
type AssistantConfig = {
|
|
74
|
+
headline: string;
|
|
75
|
+
subheadline: string;
|
|
76
|
+
suggestions: string[];
|
|
77
|
+
};
|
|
78
|
+
type PortalLayout = {
|
|
79
|
+
assistant_config: AssistantConfig | null;
|
|
80
|
+
show_playlists: boolean;
|
|
81
|
+
type: string;
|
|
82
|
+
videos_limit: number;
|
|
83
|
+
};
|
|
84
|
+
type PortalNavigation = {
|
|
85
|
+
show_ai_search: boolean;
|
|
86
|
+
show_header: boolean;
|
|
87
|
+
show_search: boolean;
|
|
88
|
+
};
|
|
89
|
+
type PortalTheme = {
|
|
90
|
+
background: string;
|
|
91
|
+
font_body: string;
|
|
92
|
+
font_header: string;
|
|
93
|
+
foreground: string;
|
|
94
|
+
logo_height: number;
|
|
95
|
+
logo_url: string;
|
|
96
|
+
logo_width: number;
|
|
97
|
+
primary: string;
|
|
98
|
+
};
|
|
99
|
+
type Portal = {
|
|
100
|
+
color_scheme?: 'toggle' | 'light' | 'dark';
|
|
101
|
+
display: PortalDisplay;
|
|
102
|
+
layout: PortalLayout;
|
|
103
|
+
navigation: PortalNavigation;
|
|
104
|
+
theme: PortalTheme;
|
|
105
|
+
};
|
|
106
|
+
type ThemeColors = {
|
|
107
|
+
background: string;
|
|
108
|
+
border: string;
|
|
109
|
+
card: string;
|
|
110
|
+
"card-foreground": string;
|
|
111
|
+
destructive: string;
|
|
112
|
+
"destructive-foreground": string;
|
|
113
|
+
foreground: string;
|
|
114
|
+
input: string;
|
|
115
|
+
muted: string;
|
|
116
|
+
"muted-foreground": string;
|
|
117
|
+
popover: string;
|
|
118
|
+
"popover-foreground": string;
|
|
119
|
+
primary: string;
|
|
120
|
+
"primary-foreground": string;
|
|
121
|
+
ring: string;
|
|
122
|
+
secondary: string;
|
|
123
|
+
"secondary-foreground": string;
|
|
124
|
+
};
|
|
125
|
+
type ThemeConfig = {
|
|
126
|
+
dark: ThemeColors;
|
|
127
|
+
light: ThemeColors;
|
|
128
|
+
radius: string;
|
|
129
|
+
};
|
|
130
|
+
type AccountAI = {
|
|
131
|
+
avatar_url: string;
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
greeting: string;
|
|
134
|
+
name: string;
|
|
135
|
+
};
|
|
136
|
+
type Account = {
|
|
137
|
+
ai: AccountAI;
|
|
138
|
+
name: string;
|
|
139
|
+
slug: string;
|
|
140
|
+
};
|
|
141
|
+
type Settings = {
|
|
142
|
+
featured_playlists: Playlist[];
|
|
143
|
+
menu_items: MenuItem[];
|
|
144
|
+
ai_avatar: string;
|
|
145
|
+
ai_name: string;
|
|
146
|
+
ai_greeting?: string;
|
|
147
|
+
has_ai: boolean;
|
|
148
|
+
account: Account;
|
|
149
|
+
favicon_url?: string;
|
|
150
|
+
logo_dark_url?: string;
|
|
151
|
+
logo_url?: string;
|
|
152
|
+
meta_data: {
|
|
153
|
+
channel_name: string;
|
|
154
|
+
description: string;
|
|
155
|
+
image: string | null;
|
|
156
|
+
no_seo: boolean;
|
|
157
|
+
social_graph_image_url?: string;
|
|
158
|
+
title: string;
|
|
159
|
+
title_suffix: string;
|
|
160
|
+
};
|
|
161
|
+
portal: Portal;
|
|
162
|
+
theme_config: ThemeConfig;
|
|
163
|
+
version: string;
|
|
164
|
+
};
|
|
165
|
+
interface Citation {
|
|
166
|
+
video_id: string;
|
|
167
|
+
title: string;
|
|
168
|
+
timestamp_ms: number;
|
|
169
|
+
text: string;
|
|
170
|
+
}
|
|
171
|
+
interface Usage {
|
|
172
|
+
input_tokens: number;
|
|
173
|
+
output_tokens: number;
|
|
174
|
+
}
|
|
175
|
+
type CoachEvent = {
|
|
176
|
+
type: "conversation_created";
|
|
177
|
+
id: string;
|
|
178
|
+
status: "processing";
|
|
179
|
+
} | {
|
|
180
|
+
type: "clarification";
|
|
181
|
+
questions: string[];
|
|
182
|
+
mode: "clarification";
|
|
183
|
+
needs_clarification: true;
|
|
184
|
+
} | {
|
|
185
|
+
type: "token";
|
|
186
|
+
content: string;
|
|
187
|
+
} | {
|
|
188
|
+
type: "answer";
|
|
189
|
+
content: string;
|
|
190
|
+
citations: Citation[];
|
|
191
|
+
usage: Usage;
|
|
192
|
+
} | {
|
|
193
|
+
type: "error";
|
|
194
|
+
message: string;
|
|
195
|
+
step?: string;
|
|
196
|
+
reason?: string;
|
|
197
|
+
timestamp: string;
|
|
198
|
+
} | {
|
|
199
|
+
type: "complete";
|
|
200
|
+
};
|
|
201
|
+
interface CoachOptions {
|
|
202
|
+
message: string;
|
|
203
|
+
conversationId?: string;
|
|
204
|
+
collectionId?: string;
|
|
205
|
+
}
|
|
206
|
+
interface AskOptions {
|
|
207
|
+
message: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type ClientOptions = {
|
|
211
|
+
baseURL?: string;
|
|
212
|
+
debug?: boolean;
|
|
213
|
+
headers?: Record<string, string>;
|
|
214
|
+
};
|
|
215
|
+
declare function createClient(apiKey: string, options?: ClientOptions): {
|
|
216
|
+
settings: (videoLimit?: number) => Promise<{
|
|
217
|
+
data: Settings;
|
|
218
|
+
}>;
|
|
219
|
+
videos: {
|
|
220
|
+
list: (videoLimit?: number) => Promise<{
|
|
221
|
+
data: Video[];
|
|
222
|
+
}>;
|
|
223
|
+
get: (id: string) => Promise<{
|
|
224
|
+
data: Video;
|
|
225
|
+
}>;
|
|
226
|
+
search: (term: string) => Promise<{
|
|
227
|
+
data: Video[];
|
|
228
|
+
}>;
|
|
229
|
+
};
|
|
230
|
+
playlists: {
|
|
231
|
+
list: () => Promise<{
|
|
232
|
+
data: Playlist[];
|
|
233
|
+
}>;
|
|
234
|
+
get: (id: string) => Promise<{
|
|
235
|
+
data: Playlist;
|
|
236
|
+
}>;
|
|
237
|
+
};
|
|
238
|
+
ai: {
|
|
239
|
+
coach(options: CoachOptions): Promise<AsyncIterable<CoachEvent>>;
|
|
240
|
+
ask(videoId: string, options: AskOptions): Promise<AsyncIterable<CoachEvent>>;
|
|
241
|
+
};
|
|
242
|
+
trackEvent: (video: any, event: Event) => void;
|
|
243
|
+
trackPageView: (title: string) => void;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Default API base URL for public API access
|
|
248
|
+
*/
|
|
249
|
+
declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
|
|
250
|
+
/**
|
|
251
|
+
* Default API base URL for internal/hosted API access
|
|
252
|
+
*/
|
|
253
|
+
declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
|
|
254
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// src/lib/client.ts
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
// src/lib/fetchers.ts
|
|
5
|
+
async function get(client, url) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await client.get(url);
|
|
8
|
+
if (res.status !== 200) {
|
|
9
|
+
throw new Error(`Unexpected response status: ${res.status}`);
|
|
10
|
+
}
|
|
11
|
+
return res.data;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`Error fetching data from URL: ${url}`, error);
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function fetchSettings(client) {
|
|
18
|
+
return async (videoLimit = 12) => {
|
|
19
|
+
try {
|
|
20
|
+
return await get(
|
|
21
|
+
client,
|
|
22
|
+
`settings?limit=${videoLimit}`
|
|
23
|
+
);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`Error fetching settings with limit: ${videoLimit}`, error);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function fetchVideos(client) {
|
|
31
|
+
return async (videoLimit = 12) => {
|
|
32
|
+
try {
|
|
33
|
+
return await get(
|
|
34
|
+
client,
|
|
35
|
+
`videos/latest?limit=${videoLimit}`
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Error fetching videos with limit: ${videoLimit}`, error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function searchVideos(client) {
|
|
44
|
+
return async (term) => {
|
|
45
|
+
try {
|
|
46
|
+
return await get(client, `videos?query=${term}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`Error searching for videos with term: ${term}`, error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function fetchVideo(client) {
|
|
54
|
+
return async (id) => {
|
|
55
|
+
try {
|
|
56
|
+
return await get(client, `videos/${id}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Error fetching video with ID: ${id}`, error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function fetchPlaylists(client) {
|
|
64
|
+
return async () => {
|
|
65
|
+
try {
|
|
66
|
+
return await get(client, "playlists");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Error fetching playlists", error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function fetchPlaylist(client) {
|
|
74
|
+
return async (id) => {
|
|
75
|
+
try {
|
|
76
|
+
return await get(client, `playlists/${id}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Error fetching playlist with ID: ${id}`, error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/util/throttle.ts
|
|
85
|
+
var throttle = (fn, delay) => {
|
|
86
|
+
let wait = false;
|
|
87
|
+
let timeout;
|
|
88
|
+
let cancelled = false;
|
|
89
|
+
return [
|
|
90
|
+
(...args) => {
|
|
91
|
+
if (cancelled)
|
|
92
|
+
return void 0;
|
|
93
|
+
if (wait)
|
|
94
|
+
return void 0;
|
|
95
|
+
const val = fn(...args);
|
|
96
|
+
wait = true;
|
|
97
|
+
timeout = window.setTimeout(() => {
|
|
98
|
+
wait = false;
|
|
99
|
+
}, delay);
|
|
100
|
+
return val;
|
|
101
|
+
},
|
|
102
|
+
() => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/lib/tracking.ts
|
|
110
|
+
function sendEvent(client, eventName, data, debug) {
|
|
111
|
+
const payload = {
|
|
112
|
+
n: eventName,
|
|
113
|
+
u: data.url,
|
|
114
|
+
usr: data.userId,
|
|
115
|
+
d: data.domain,
|
|
116
|
+
ua: data.userAgent,
|
|
117
|
+
w: data.deviceWidth,
|
|
118
|
+
vid: data?.videoId || void 0,
|
|
119
|
+
vt: data.title,
|
|
120
|
+
vdur: data?.videoDuration || void 0,
|
|
121
|
+
time: data?.currentTime || void 0
|
|
122
|
+
};
|
|
123
|
+
if (debug)
|
|
124
|
+
console.log(`Bold SDK - Logging event '${eventName}'`, payload);
|
|
125
|
+
client.post("/event", payload);
|
|
126
|
+
}
|
|
127
|
+
var [throttledSendEvent] = throttle(sendEvent, 5e3);
|
|
128
|
+
function trackEvent(client, userId, options) {
|
|
129
|
+
return (video, event) => {
|
|
130
|
+
const eventDetails = {
|
|
131
|
+
...basicInfos(),
|
|
132
|
+
userId,
|
|
133
|
+
videoId: video.id,
|
|
134
|
+
title: video.title,
|
|
135
|
+
videoDuration: video.duration,
|
|
136
|
+
currentTime: event.target?.currentTime || 0
|
|
137
|
+
};
|
|
138
|
+
if (event.type == "timeupdate" || event.type == "time-update") {
|
|
139
|
+
throttledSendEvent(
|
|
140
|
+
client,
|
|
141
|
+
getEventName(event),
|
|
142
|
+
eventDetails,
|
|
143
|
+
options.debug
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
sendEvent(client, getEventName(event), eventDetails, options.debug);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function trackPageView(client, userId, options) {
|
|
151
|
+
return (title) => {
|
|
152
|
+
const eventDetails = {
|
|
153
|
+
...basicInfos(),
|
|
154
|
+
userId,
|
|
155
|
+
title
|
|
156
|
+
};
|
|
157
|
+
sendEvent(client, "page_view", eventDetails, options.debug);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function getEventName(event) {
|
|
161
|
+
switch (event.type) {
|
|
162
|
+
case "pause":
|
|
163
|
+
return "video_pause";
|
|
164
|
+
case "play":
|
|
165
|
+
return "video_resume";
|
|
166
|
+
case "loadedmetadata":
|
|
167
|
+
case "loaded-metadata":
|
|
168
|
+
return "video_load";
|
|
169
|
+
case "time-update":
|
|
170
|
+
case "timeupdate":
|
|
171
|
+
return "video_progress";
|
|
172
|
+
default:
|
|
173
|
+
return "unknown_event";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function basicInfos() {
|
|
177
|
+
return {
|
|
178
|
+
url: location.href,
|
|
179
|
+
domain: location.hostname,
|
|
180
|
+
referrer: document.referrer || null,
|
|
181
|
+
deviceWidth: window.innerWidth,
|
|
182
|
+
userAgent: navigator.userAgent
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/lib/ai.ts
|
|
187
|
+
async function* parseSSE(response) {
|
|
188
|
+
const reader = response.body?.getReader();
|
|
189
|
+
if (!reader) {
|
|
190
|
+
throw new Error("Response body is not readable");
|
|
191
|
+
}
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buffer = "";
|
|
194
|
+
try {
|
|
195
|
+
while (true) {
|
|
196
|
+
const { done, value } = await reader.read();
|
|
197
|
+
if (done)
|
|
198
|
+
break;
|
|
199
|
+
buffer += decoder.decode(value, { stream: true });
|
|
200
|
+
const lines = buffer.split(/\r?\n\r?\n/);
|
|
201
|
+
buffer = lines.pop() || "";
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
if (!trimmed || !trimmed.startsWith("data:"))
|
|
205
|
+
continue;
|
|
206
|
+
const json = trimmed.slice(5).trim();
|
|
207
|
+
if (!json)
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const event = JSON.parse(json);
|
|
211
|
+
yield event;
|
|
212
|
+
if (event.type === "complete") {
|
|
213
|
+
await reader.cancel();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} finally {
|
|
221
|
+
reader.releaseLock();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function streamRequest(path, body, config) {
|
|
225
|
+
const baseURL = config.baseURL.endsWith("/") ? config.baseURL : `${config.baseURL}/`;
|
|
226
|
+
const url = new URL(path, baseURL);
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
"Accept": "text/event-stream",
|
|
232
|
+
...config.headers
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(body)
|
|
235
|
+
});
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
|
|
238
|
+
}
|
|
239
|
+
return parseSSE(response);
|
|
240
|
+
}
|
|
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);
|
|
260
|
+
},
|
|
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);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/lib/constants.ts
|
|
280
|
+
var DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
|
|
281
|
+
var DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
|
|
282
|
+
|
|
283
|
+
// src/lib/client.ts
|
|
284
|
+
function createClient(apiKey, options = {}) {
|
|
285
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
286
|
+
throw new Error("API key is missing or invalid");
|
|
287
|
+
}
|
|
288
|
+
const { debug = false, headers = {} } = options;
|
|
289
|
+
const apiClientOptions = {
|
|
290
|
+
baseURL: options.baseURL ?? DEFAULT_API_BASE_URL,
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: apiKey,
|
|
293
|
+
...headers
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
let apiClient;
|
|
297
|
+
try {
|
|
298
|
+
apiClient = axios.create(apiClientOptions);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error("Error creating API client", error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
const userId = [...Array(30)].map(() => Math.random().toString(36)[2]).join("");
|
|
304
|
+
const aiConfig = {
|
|
305
|
+
baseURL: apiClientOptions.baseURL,
|
|
306
|
+
headers: apiClientOptions.headers
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
settings: fetchSettings(apiClient),
|
|
310
|
+
videos: {
|
|
311
|
+
list: fetchVideos(apiClient),
|
|
312
|
+
get: fetchVideo(apiClient),
|
|
313
|
+
search: searchVideos(apiClient)
|
|
314
|
+
},
|
|
315
|
+
playlists: {
|
|
316
|
+
list: fetchPlaylists(apiClient),
|
|
317
|
+
get: fetchPlaylist(apiClient)
|
|
318
|
+
},
|
|
319
|
+
ai: createAI(aiConfig),
|
|
320
|
+
trackEvent: trackEvent(apiClient, userId, { debug }),
|
|
321
|
+
trackPageView: trackPageView(apiClient, userId, { debug })
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
export {
|
|
325
|
+
DEFAULT_API_BASE_URL,
|
|
326
|
+
DEFAULT_INTERNAL_API_BASE_URL,
|
|
327
|
+
createClient
|
|
328
|
+
};
|