UNPKG

@promptbook/browser

Version:

Promptbook: Turn your company's scattered knowledge into AI ready books

1,616 lines (1,562 loc) โ€ข 472 kB
import spaceTrim$1, { spaceTrim } from 'spacetrim'; import { randomBytes } from 'crypto'; import { isRunningInBrowser } from 'openai/core'; import { Subject } from 'rxjs'; import { forTime } from 'waitasecond'; import hexEncoder from 'crypto-js/enc-hex'; import sha256 from 'crypto-js/sha256'; import { basename, join, dirname, isAbsolute } from 'path'; import { SHA256 } from 'crypto-js'; import { lookup, extension } from 'mime-types'; import { parse, unparse } from 'papaparse'; import 'moment'; import 'colors'; import { Registration } from 'destroyable'; // โš ๏ธ WARNING: This code has been generated so that any manual changes will be overwritten /** * The version of the Book language * * @generated * @see https://github.com/webgptorg/book */ const BOOK_LANGUAGE_VERSION = '1.0.0'; /** * The version of the Promptbook engine * * @generated * @see https://github.com/webgptorg/promptbook */ const PROMPTBOOK_ENGINE_VERSION = '0.103.0-30'; /** * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Generates random token * * Note: This function is cryptographically secure (it uses crypto.randomBytes internally) * * @private internal helper function * @returns secure random token */ function $randomToken(randomness) { return randomBytes(randomness).toString('hex'); } /** * TODO: Maybe use nanoid instead https://github.com/ai/nanoid */ /** * This error indicates errors during the execution of the pipeline * * @public exported from `@promptbook/core` */ class PipelineExecutionError extends Error { constructor(message) { // Added id parameter super(message); this.name = 'PipelineExecutionError'; // TODO: [๐Ÿ™] DRY - Maybe $randomId this.id = `error-${$randomToken(8 /* <- TODO: To global config + Use Base58 to avoid similar char conflicts */)}`; Object.setPrototypeOf(this, PipelineExecutionError.prototype); } } /** * TODO: [๐Ÿง ][๐ŸŒ‚] Add id to all errors */ /** * Wrapper around `window.prompt` synchronous function that interacts with the user via browser prompt * * Warning: It is used for testing and mocking * **NOT intended to use in the production** due to its synchronous nature. * * @public exported from `@promptbook/browser` */ class SimplePromptInterfaceTools { constructor(options = {}) { this.options = options; } /** * Trigger window.prompt dialog */ async promptDialog(options) { const answer = window.prompt(spaceTrim((block) => ` ${block(options.promptTitle)} ${block(options.promptMessage)} `)); if (this.options.isVerbose) { console.info(spaceTrim((block) => ` ๐Ÿ“– ${block(options.promptTitle)} ๐Ÿ‘ค ${block(answer || '๐Ÿšซ User cancelled prompt')} `)); } if (answer === null) { throw new PipelineExecutionError('User cancelled prompt'); } return answer; } } /** * Note: [๐Ÿ”ต] Code in this file should never be published outside of `@promptbook/browser` */ /** * This error type indicates that you try to use a feature that is not available in the current environment * * @public exported from `@promptbook/core` */ class EnvironmentMismatchError extends Error { constructor(message) { super(message); this.name = 'EnvironmentMismatchError'; Object.setPrototypeOf(this, EnvironmentMismatchError.prototype); } } /** * Detects if the code is running in a browser environment in main thread (Not in a web worker) * * Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment * * @public exported from `@promptbook/utils` */ const $isRunningInBrowser = new Function(` try { return this === window; } catch (e) { return false; } `); /** * TODO: [๐ŸŽบ] */ /** * Detects if the code is running in a web worker * * Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment * * @public exported from `@promptbook/utils` */ const $isRunningInWebWorker = new Function(` try { if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { return true; } else { return false; } } catch (e) { return false; } `); /** * TODO: [๐ŸŽบ] */ /** * Available remote servers for the Promptbook * * @public exported from `@promptbook/core` */ const REMOTE_SERVER_URLS = [ { title: 'Promptbook', description: `Servers of Promptbook.studio`, owner: 'AI Web, LLC <legal@ptbk.io> (https://www.ptbk.io/)', isAnonymousModeAllowed: true, urls: [ 'https://promptbook.s5.ptbk.io/', // Note: Servers 1-4 are not running ], }, /* Note: Working on older version of Promptbook and not supported anymore { title: 'Pavol Promptbook Server', description: `Personal server of Pavol Hejnรฝ with simple testing server, DO NOT USE IT FOR PRODUCTION`, owner: 'Pavol Hejnรฝ <pavol@ptbk.io> (https://www.pavolhejny.com/)', isAnonymousModeAllowed: true, urls: ['https://api.pavolhejny.com/promptbook'], }, */ ]; /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * @private util of `@promptbook/color` * @de */ class TakeChain { constructor(value) { this.value = value; } then(callback) { const newValue = callback(this.value); return take(newValue); } } /** * A function that takes an initial value and returns a proxy object with chainable methods. * * @param {*} initialValue - The initial value. * @returns {Proxy<WithTake<TValue>>} - A proxy object with a `take` method. * * @private util of `@promptbook/color` * @deprecated [๐Ÿคก] Use some better functional library instead of `TakeChain` */ function take(initialValue) { if (initialValue instanceof TakeChain) { return initialValue; } return new Proxy(new TakeChain(initialValue), { get(target, property, receiver) { if (Reflect.has(target, property)) { return Reflect.get(target, property, receiver); } else if (Reflect.has(initialValue, property)) { return Reflect.get(initialValue, property, receiver); } else { return undefined; } }, }); } /** * ๐ŸŽจ List of all 140 color names which are supported by CSS * * @public exported from `@promptbook/color` */ const CSS_COLORS = { transparent: 'rgba(0,0,0,0)', aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4', azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000', blanchedalmond: '#ffebcd', blue: '#0000ff', blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c', cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9', darkgrey: '#a9a9a9', darkgreen: '#006400', darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969', dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22', fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', gray: '#808080', grey: '#808080', green: '#008000', greenyellow: '#adff2f', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c', indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa', lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgrey: '#d3d3d3', lightgreen: '#90ee90', lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32', linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa', mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371', mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5', navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000', olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb', plum: '#dda0dd', powderblue: '#b0e0e6', purple: '#800080', rebeccapurple: '#663399', red: '#ff0000', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee', sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb', slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', teal: '#008080', thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3', white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00', yellowgreen: '#9acd32', }; /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Validates that a channel value is a valid number within the range of 0 to 255. * Throws an error if the value is not valid. * * @param channelName - The name of the channel being validated. * @param value - The value of the channel to validate. * @throws Will throw an error if the value is not a valid channel number. * * @private util of `@promptbook/color` */ function checkChannelValue(channelName, value) { if (typeof value !== 'number') { throw new Error(`${channelName} channel value is not number but ${typeof value}`); } if (isNaN(value)) { throw new Error(`${channelName} channel value is NaN`); } if (Math.round(value) !== value) { throw new Error(`${channelName} channel is not whole number, it is ${value}`); } if (value < 0) { throw new Error(`${channelName} channel is lower than 0, it is ${value}`); } if (value > 255) { throw new Error(`${channelName} channel is greater than 255, it is ${value}`); } } /** * TODO: [๐Ÿง ][๐Ÿš“] Is/which combination it better to use asserts/check, validate or is utility function? */ /** * Color object represents an RGB color with alpha channel * * Note: There is no fromObject/toObject because the most logical way to serialize color is as a hex string (#009edd) * * @public exported from `@promptbook/color` */ class Color { /** * Creates a new Color instance from miscellaneous formats * - It can receive Color instance and just return the same instance * - It can receive color in string format for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)` * * Note: This is not including fromImage because detecting color from an image is heavy task which requires async stuff and we cannot safely determine with overloading if return value will be a promise * * @param color * @returns Color object */ static from(color) { if (color instanceof Color) { return take(color); } else if (Color.isColor(color)) { return take(color); } else if (typeof color === 'string') { return Color.fromString(color); } else { console.error({ color }); throw new Error(`Can not create color from given object`); } } /** * Creates a new Color instance from miscellaneous string formats * * @param color as a string for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`, `red`, `darkgrey`,... * @returns Color object */ static fromString(color) { if (CSS_COLORS[color]) { return Color.fromString(CSS_COLORS[color]); // ----- } else if (Color.isHexColorString(color)) { return Color.fromHex(color); // ----- } else if (/^hsl\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?%)\s*,\s*(\d+(?:\.\d+)?%)\)$/.test(color)) { return Color.fromHsl(color); // ----- } else if (/^rgb\((\s*[0-9-.%]+\s*,?){3}\)$/.test(color)) { // TODO: [0] Should be fromRgbString and fromRgbaString one or two functions return Color.fromRgbString(color); // ----- } else if (/^rgba\((\s*[0-9-.%]+\s*,?){4}\)$/.test(color)) { return Color.fromRgbaString(color); // ----- } else { throw new Error(`Can not create a new Color instance from string "${color}".`); } } /** * Gets common color * * @param key as a css string like `midnightblue` * @returns Color object */ static get(key) { if (!CSS_COLORS[key]) { throw new Error(`"${key}" is not a common css color.`); } return Color.fromString(CSS_COLORS[key]); } /** * Creates a new Color instance from average color of given image * * @param image as a source for example `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYJh39z8ABJgCe/ZvAS4AAAAASUVORK5CYII=` * @returns Color object */ static async fromImage(image) { return Color.fromHex(`#009edd`); } /** * Creates a new Color instance from color in hex format * * @param color in hex for example `#009edd`, `009edd`, `#555`,... * @returns Color object */ static fromHex(hex) { const hexOriginal = hex; if (hex.startsWith('#')) { hex = hex.substring(1); } if (hex.length === 3) { return Color.fromHex3(hex); } if (hex.length === 6) { return Color.fromHex6(hex); } if (hex.length === 8) { return Color.fromHex8(hex); } throw new Error(`Can not parse color from hex string "${hexOriginal}"`); } /** * Creates a new Color instance from color in hex format with 3 color digits (without alpha channel) * * @param color in hex for example `09d` * @returns Color object */ static fromHex3(hex) { const r = parseInt(hex.substr(0, 1), 16) * 16; const g = parseInt(hex.substr(1, 1), 16) * 16; const b = parseInt(hex.substr(2, 1), 16) * 16; return take(new Color(r, g, b)); } /** * Creates a new Color instance from color in hex format with 6 color digits (without alpha channel) * * @param color in hex for example `009edd` * @returns Color object */ static fromHex6(hex) { const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); return take(new Color(r, g, b)); } /** * Creates a new Color instance from color in hex format with 8 color digits (with alpha channel) * * @param color in hex for example `009edd` * @returns Color object */ static fromHex8(hex) { const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); const a = parseInt(hex.substr(6, 2), 16); return take(new Color(r, g, b, a)); } /** * Creates a new Color instance from color in hsl format * * @param color as a hsl for example `hsl(197.1,100%,43.3%)` * @returns Color object */ static fromHsl(hsl) { const match = hsl.match(/^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/); if (!match) { throw new Error(`Invalid hsl string format: "${hsl}"`); } const h = parseFloat(match[1]); const s = parseFloat(match[2]) / 100; const l = parseFloat(match[3]) / 100; // HSL to RGB conversion const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = l - c / 2; let r1 = 0, g1 = 0, b1 = 0; if (h >= 0 && h < 60) { r1 = c; g1 = x; b1 = 0; } else if (h >= 60 && h < 120) { r1 = x; g1 = c; b1 = 0; } else if (h >= 120 && h < 180) { r1 = 0; g1 = c; b1 = x; } else if (h >= 180 && h < 240) { r1 = 0; g1 = x; b1 = c; } else if (h >= 240 && h < 300) { r1 = x; g1 = 0; b1 = c; } else if (h >= 300 && h < 360) { r1 = c; g1 = 0; b1 = x; } const r = Math.round((r1 + m) * 255); const g = Math.round((g1 + m) * 255); const b = Math.round((b1 + m) * 255); return take(new Color(r, g, b)); } /** * Creates a new Color instance from color in rgb format * * @param color as a rgb for example `rgb(0,158,221)`, `rgb(0%,62%,86.7%)` * @returns Color object */ static fromRgbString(rgb) { const match = rgb.match(/^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/); if (!match) { throw new Error(`Invalid rgb string format: "${rgb}"`); } const parseChannel = (value) => { if (value.endsWith('%')) { // Percentage value const percent = parseFloat(value); return Math.round((percent / 100) * 255); } else { // Numeric value return Math.round(parseFloat(value)); } }; const r = parseChannel(match[1]); const g = parseChannel(match[2]); const b = parseChannel(match[3]); return take(new Color(r, g, b)); } /** * Creates a new Color instance from color in rbga format * * @param color as a rgba for example `rgba(0,158,221,0.5)`, `rgb(0%,62%,86.7%,50%)` * @returns Color object */ static fromRgbaString(rgba) { const match = rgba.match(/^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/); if (!match) { throw new Error(`Invalid rgba string format: "${rgba}"`); } const parseChannel = (value) => { if (value.endsWith('%')) { const percent = parseFloat(value); return Math.round((percent / 100) * 255); } else { return Math.round(parseFloat(value)); } }; const parseAlpha = (value) => { if (value.endsWith('%')) { const percent = parseFloat(value); return Math.round((percent / 100) * 255); } else { const alphaFloat = parseFloat(value); // If alpha is between 0 and 1, treat as float if (alphaFloat <= 1) { return Math.round(alphaFloat * 255); } // Otherwise, treat as 0-255 return Math.round(alphaFloat); } }; const r = parseChannel(match[1]); const g = parseChannel(match[2]); const b = parseChannel(match[3]); const a = parseAlpha(match[4]); return take(new Color(r, g, b, a)); } /** * Creates a new Color for color channels values * * @param red number from 0 to 255 * @param green number from 0 to 255 * @param blue number from 0 to 255 * @param alpha number from 0 (transparent) to 255 (opaque = default) * @returns Color object */ static fromValues(red, green, blue, alpha = 255) { return take(new Color(red, green, blue, alpha)); } /** * Checks if the given value is a valid Color object. * * @param {unknown} value - The value to check. * @return {value is WithTake<Color>} Returns true if the value is a valid Color object, false otherwise. */ static isColor(value) { if (typeof value !== 'object') { return false; } if (value === null) { return false; } if (typeof value.red !== 'number' || typeof value.green !== 'number' || typeof value.blue !== 'number' || typeof value.alpha !== 'number') { return false; } if (typeof value.then !== 'function') { return false; } return true; } /** * Checks if the given value is a valid hex color string * * @param value - value to check * @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.) */ static isHexColorString(value) { return typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value); } /** * Creates new Color object * * Note: Consider using one of static methods like `from` or `fromString` * * @param red number from 0 to 255 * @param green number from 0 to 255 * @param blue number from 0 to 255 * @param alpha number from 0 (transparent) to 255 (opaque) */ constructor(red, green, blue, alpha = 255) { this.red = red; this.green = green; this.blue = blue; this.alpha = alpha; checkChannelValue('Red', red); checkChannelValue('Green', green); checkChannelValue('Blue', blue); checkChannelValue('Alpha', alpha); } /** * Shortcut for `red` property * Number from 0 to 255 * @alias red */ get r() { return this.red; } /** * Shortcut for `green` property * Number from 0 to 255 * @alias green */ get g() { return this.green; } /** * Shortcut for `blue` property * Number from 0 to 255 * @alias blue */ get b() { return this.blue; } /** * Shortcut for `alpha` property * Number from 0 (transparent) to 255 (opaque) * @alias alpha */ get a() { return this.alpha; } /** * Shortcut for `alpha` property * Number from 0 (transparent) to 255 (opaque) * @alias alpha */ get opacity() { return this.alpha; } /** * Shortcut for 1-`alpha` property */ get transparency() { return 255 - this.alpha; } clone() { return take(new Color(this.red, this.green, this.blue, this.alpha)); } toString() { return this.toHex(); } toHex() { if (this.alpha === 255) { return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue .toString(16) .padStart(2, '0')}`; } else { return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue .toString(16) .padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`; } } toRgb() { if (this.alpha === 255) { return `rgb(${this.red}, ${this.green}, ${this.blue})`; } else { return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`; } } toHsl() { throw new Error(`Getting HSL is not implemented`); } } /** * TODO: [๐Ÿฅป] Split Color class and color type * TODO: For each method a corresponding static method should be created * Like clone can be done by color.clone() OR Color.clone(color) * TODO: Probably as an independent LIB OR add to LIB xyzt (ask @roseckyj) * TODO: !! Transfer back to Collboard (whole directory) * TODO: Maybe [๐ŸŒ๏ธโ€โ™‚๏ธ] change ACRY toString => (toHex) toRgb when there will be toRgb and toRgba united * TODO: Convert getters to methods - getters only for values * TODO: Write tests * TODO: Getters for alpha, opacity, transparency, r, b, g, h, s, l, a,... * TODO: [0] Should be fromRgbString and fromRgbaString one or two functions + one or two regex * TODO: Use rgb, rgba, hsl for testing and parsing with the same regex * TODO: Regex for rgb, rgba, hsl does not support all options like deg, rad, turn,... * TODO: Convolution matrix * TODO: Maybe connect with textures */ /** * Converts HSL values to RGB values * * @param hue [0-1] * @param saturation [0-1] * @param lightness [0-1] * @returns [red, green, blue] [0-255] * * @private util of `@promptbook/color` */ function hslToRgb(hue, saturation, lightness) { let red; let green; let blue; if (saturation === 0) { // achromatic red = lightness; green = lightness; blue = lightness; } else { // TODO: Extract to separate function const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation; const p = 2 * lightness - q; red = hue2rgb(p, q, hue + 1 / 3); green = hue2rgb(p, q, hue); blue = hue2rgb(p, q, hue - 1 / 3); } return [Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)]; } /** * TODO: Properly name all used internal variables */ /** * Converts RGB values to HSL values * * @param red [0-255] * @param green [0-255] * @param blue [0-255] * @returns [hue, saturation, lightness] [0-1] * * @private util of `@promptbook/color` */ function rgbToHsl(red, green, blue) { red /= 255; green /= 255; blue /= 255; const max = Math.max(red, green, blue); const min = Math.min(red, green, blue); let hue; let saturation; const lightness = (max + min) / 2; if (max === min) { // achromatic hue = 0; saturation = 0; } else { const d = max - min; saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case red: hue = (green - blue) / d + (green < blue ? 6 : 0); break; case green: hue = (blue - red) / d + 2; break; case blue: hue = (red - green) / d + 4; break; default: hue = 0; } hue /= 6; } return [hue, saturation, lightness]; } /** * TODO: Properly name all used internal variables */ /** * Makes color transformer which lighten the given color * * @param amount from 0 to 1 * * @public exported from `@promptbook/color` */ function lighten(amount) { return ({ red, green, blue, alpha }) => { const [h, s, lInitial] = rgbToHsl(red, green, blue); let l = lInitial + amount; l = Math.max(0, Math.min(l, 1)); // Replace lodash clamp with Math.max and Math.min const [r, g, b] = hslToRgb(h, s, l); return Color.fromValues(r, g, b, alpha); }; } /** * TODO: Maybe implement by mix+hsl */ /** * Calculates distance between two colors * * @param color1 first color * @param color2 second color * * Note: This function is inefficient. Use colorDistanceSquared instead if possible. * * @public exported from `@promptbook/color` */ /** * Calculates distance between two colors without square root * * @param color1 first color * @param color2 second color * * @public exported from `@promptbook/color` */ function colorDistanceSquared(color1, color2) { const rmean = (color1.red + color2.red) / 2; const r = color1.red - color2.red; const g = color1.green - color2.green; const b = color1.blue - color2.blue; const weightR = 2 + rmean / 256; const weightG = 4.0; const weightB = 2 + (255 - rmean) / 256; const distance = weightR * r * r + weightG * g * g + weightB * b * b; return distance; } /** * Makes color transformer which finds the nearest color from the given list * * @param colors array of colors to choose from * * @public exported from `@promptbook/color` */ function nearest(...colors) { return (color) => { const distances = colors.map((c) => colorDistanceSquared(c, color)); const minDistance = Math.min(...distances); const minIndex = distances.indexOf(minDistance); const nearestColor = colors[minIndex]; return nearestColor; }; } /** * Color transformer which returns the negative color * * @public exported from `@promptbook/color` */ function negative(color) { const r = 255 - color.red; const g = 255 - color.green; const b = 255 - color.blue; return Color.fromValues(r, g, b, color.alpha); } /** * Makes color transformer which finds the furthest color from the given list * * @param colors array of colors to choose from * * @public exported from `@promptbook/color` */ function furthest(...colors) { return (color) => { const furthestColor = negative(nearest(...colors.map(negative))(color)); return furthestColor; }; } /** * Makes color transformer which finds the best text color (black or white) for the given background color * * @public exported from `@promptbook/color` */ furthest(Color.get('white'), Color.from('black')); /** * Makes color transformer which returns a grayscale version of the color * * @param amount from 0 to 1 * * @public exported from `@promptbook/color` */ function grayscale(amount) { return ({ red, green, blue, alpha }) => { const average = (red + green + blue) / 3; red = Math.round(average * amount + red * (1 - amount)); green = Math.round(average * amount + green * (1 - amount)); blue = Math.round(average * amount + blue * (1 - amount)); return Color.fromValues(red, green, blue, alpha); }; } /** * Makes color transformer which saturate the given color * * @param amount from -1 to 1 * * @public exported from `@promptbook/color` */ function saturate(amount) { return ({ red, green, blue, alpha }) => { const [h, sInitial, l] = rgbToHsl(red, green, blue); let s = sInitial + amount; s = Math.max(0, Math.min(s, 1)); const [r, g, b] = hslToRgb(h, s, l); return Color.fromValues(r, g, b, alpha); }; } /** * TODO: Maybe implement by mix+hsl */ /** * Returns the same value that is passed as argument. * No side effects. * * Note: It can be useful for: * * 1) Leveling indentation * 2) Putting always-true or always-false conditions without getting eslint errors * * @param value any values * @returns the same values * @private within the repository */ function just(value) { if (value === undefined) { return undefined; } return value; } /** * Name for the Promptbook * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ const NAME = `Promptbook`; /** * Email of the responsible person * * @public exported from `@promptbook/core` */ const ADMIN_EMAIL = 'pavol@ptbk.io'; /** * Name of the responsible person for the Promptbook on GitHub * * @public exported from `@promptbook/core` */ const ADMIN_GITHUB_NAME = 'hejny'; // <- TODO: [๐ŸŠ] Pick the best claim /** * Color of the Promptbook * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ const PROMPTBOOK_COLOR = Color.fromHex('#79EAFD'); // <- TODO: [๐Ÿง ][๐Ÿˆต] Using `Color` here increases the package size approx 3kb, maybe remove it /** * Colors for syntax highlighting in the `<BookEditor/>` * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ ({ TITLE: Color.fromHex('#244EA8'), LINE: Color.fromHex('#eeeeee'), COMMITMENT: Color.fromHex('#DA0F78'), PARAMETER: Color.fromHex('#8e44ad'), }); // <- TODO: [๐Ÿง ][๐Ÿˆต] Using `Color` here increases the package size approx 3kb, maybe remove it /** * Chat color of the Promptbook (in chat) * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ PROMPTBOOK_COLOR.then(lighten(0.1)).then(saturate(0.9)).then(grayscale(0.9)); // <- TODO: [๐Ÿง ][๐Ÿˆต] Using `Color` and `lighten`, `saturate`,... here increases the package size approx 3kb, maybe remove it /** * Color of the user (in chat) * * TODO: [๐Ÿ—ฝ] Unite branding and make single place for it * * @public exported from `@promptbook/core` */ Color.fromHex('#1D4ED8'); // <- TODO: [๐Ÿง ][๐Ÿˆต] Using `Color` here increases the package size approx 3kb, maybe remove it /** * When the title is not provided, the default title is used * * @public exported from `@promptbook/core` */ const DEFAULT_BOOK_TITLE = `โœจ Untitled Book`; /** * Maximum file size limit * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB /** * Threshold value that determines when a dataset is considered "big" * and may require special handling or optimizations * * For example, when error occurs in one item of the big dataset, it will not fail the whole pipeline * * @public exported from `@promptbook/core` */ const BIG_DATASET_TRESHOLD = 50; /** * Placeholder text used to represent a placeholder value of failed operation * * @public exported from `@promptbook/core` */ const FAILED_VALUE_PLACEHOLDER = '!?'; // <- TODO: [๐Ÿง ] Better system for generator warnings - not always "code" and "by `@promptbook/cli`" /** * The maximum number of iterations for a loops * * @private within the repository - too low-level in comparison with other `MAX_...` */ const LOOP_LIMIT = 1000; /** * Strings to represent various values in the context of parameter values * * @public exported from `@promptbook/utils` */ const VALUE_STRINGS = { empty: '(nothing; empty string)', null: '(no value; null)', undefined: '(unknown value; undefined)', nan: '(not a number; NaN)', infinity: '(infinity; โˆž)', negativeInfinity: '(negative infinity; -โˆž)', unserializable: '(unserializable value)', circular: '(circular JSON)', }; /** * Small number limit * * @public exported from `@promptbook/utils` */ const SMALL_NUMBER = 0.001; /** * Short time interval to prevent race conditions in milliseconds * * @private within the repository - too low-level in comparison with other `MAX_...` */ const IMMEDIATE_TIME = 10; /** * The maximum length of the (generated) filename * * @public exported from `@promptbook/core` */ const MAX_FILENAME_LENGTH = 30; /** * Strategy for caching the intermediate results for knowledge sources * * @public exported from `@promptbook/core` */ const DEFAULT_INTERMEDIATE_FILES_STRATEGY = 'HIDE_AND_KEEP'; // <- TODO: [๐Ÿ˜ก] Change to 'VISIBLE' /** * The maximum number of (LLM) tasks running in parallel * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] /** * The maximum number of attempts to execute LLM task before giving up * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 7; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] // <- TODO: [๐Ÿ•] Make also `BOOKS_DIRNAME_ALTERNATIVES` // TODO: Just `.promptbook` in config, hardcode subfolders like `download-cache` or `execution-cache` /** * Where to store the temporary downloads * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_DOWNLOAD_CACHE_DIRNAME = './.promptbook/download-cache'; /** * Where to store the scrape cache * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_SCRAPE_CACHE_DIRNAME = './.promptbook/scrape-cache'; /** * Default remote server URL for the Promptbook * * @public exported from `@promptbook/core` */ const DEFAULT_REMOTE_SERVER_URL = REMOTE_SERVER_URLS[0].urls[0]; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * Default settings for parsing and generating CSV files in Promptbook. * * @public exported from `@promptbook/core` */ const DEFAULT_CSV_SETTINGS = Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * Controls whether verbose logging is enabled by default throughout the application. * * @public exported from `@promptbook/core` */ let DEFAULT_IS_VERBOSE = false; /** * Controls whether auto-installation of dependencies is enabled by default. * * @public exported from `@promptbook/core` */ const DEFAULT_IS_AUTO_INSTALLED = false; /** * Default simulated duration for a task in milliseconds (used for progress reporting) * * @public exported from `@promptbook/core` */ const DEFAULT_TASK_SIMULATED_DURATION_MS = 5 * 60 * 1000; // 5 minutes /** * Default rate limits (requests per minute) * * Note: Adjust based on the provider tier you are have * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_REQUESTS_PER_MINUTE = 60; /** * API request timeout in milliseconds * Can be overridden via API_REQUEST_TIMEOUT environment variable * * @public exported from `@promptbook/core` */ parseInt(process.env.API_REQUEST_TIMEOUT || '90000'); /** * Indicates whether pipeline logic validation is enabled. When true, the pipeline logic is checked for consistency. * * @private within the repository */ const IS_PIPELINE_LOGIC_VALIDATED = just( /**/ // Note: In normal situations, we check the pipeline logic: true); /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteServerUrl and path into single value */ /** * This error type indicates that some part of the code is not implemented yet * * @public exported from `@promptbook/core` */ class NotYetImplementedError extends Error { constructor(message) { super(spaceTrim((block) => ` ${block(message)} Note: This feature is not implemented yet but it will be soon. If you want speed up the implementation or just read more, look here: https://github.com/webgptorg/promptbook Or contact us on pavol@ptbk.io `)); this.name = 'NotYetImplementedError'; Object.setPrototypeOf(this, NotYetImplementedError.prototype); } } /** * Make error report URL for the given error * * @private private within the repository */ function getErrorReportUrl(error) { const report = { title: `๐Ÿœ Error report from ${NAME}`, body: spaceTrim$1((block) => ` \`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}. \`\`\` ${block(error.message || '(no error message)')} \`\`\` ## More info: - **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION} - **Book language version:** ${BOOK_LANGUAGE_VERSION} - **Time:** ${new Date().toISOString()} <details> <summary>Stack trace:</summary> ## Stack trace: \`\`\`stacktrace ${block(error.stack || '(empty)')} \`\`\` </details> `), }; const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`); reportUrl.searchParams.set('labels', 'bug'); reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME); reportUrl.searchParams.set('title', report.title); reportUrl.searchParams.set('body', report.body); return reportUrl; } /** * This error type indicates that the error should not happen and its last check before crashing with some other error * * @public exported from `@promptbook/core` */ class UnexpectedError extends Error { constructor(message) { super(spaceTrim((block) => ` ${block(message)} Note: This error should not happen. It's probably a bug in the pipeline collection Please report issue: ${block(getErrorReportUrl(new Error(message)).href)} Or contact us on ${ADMIN_EMAIL} `)); this.name = 'UnexpectedError'; Object.setPrototypeOf(this, UnexpectedError.prototype); } } /** * Safely retrieves the global scope object (window in browser, global in Node.js) * regardless of the JavaScript environment in which the code is running * * Note: `$` is used to indicate that this function is not a pure function - it access global scope * * @private internal function of `$Register` */ function $getGlobalScope() { return Function('return this')(); } /** * Normalizes a text string to SCREAMING_CASE (all uppercase with underscores). * * Note: [๐Ÿ”‚] This function is idempotent. * * @param text The text string to be converted to SCREAMING_CASE format. * @returns The normalized text in SCREAMING_CASE format. * @example 'HELLO_WORLD' * @example 'I_LOVE_PROMPTBOOK' * @public exported from `@promptbook/utils` */ function normalizeTo_SCREAMING_CASE(text) { let charType; let lastCharType = 'OTHER'; let normalizedName = ''; for (const char of text) { let normalizedChar; if (/^[a-z]$/.test(char)) { charType = 'LOWERCASE'; normalizedChar = char.toUpperCase(); } else if (/^[A-Z]$/.test(char)) { charType = 'UPPERCASE'; normalizedChar = char; } else if (/^[0-9]$/.test(char)) { charType = 'NUMBER'; normalizedChar = char; } else { charType = 'OTHER'; normalizedChar = '_'; } if (charType !== lastCharType && !(lastCharType === 'UPPERCASE' && charType === 'LOWERCASE') && !(lastCharType === 'NUMBER') && !(charType === 'NUMBER')) { normalizedName += '_'; } normalizedName += normalizedChar; lastCharType = charType; } normalizedName = normalizedName.replace(/_+/g, '_'); normalizedName = normalizedName.replace(/_?\/_?/g, '/'); normalizedName = normalizedName.replace(/^_/, ''); normalizedName = normalizedName.replace(/_$/, ''); return normalizedName; } /** * TODO: Tests * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'Moje tabule' })).toEqual('/VtG7sR9rRJqwNEdM2/Moje tabule'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: 'ฤ›ลกฤล™ลพลพรฝรกรญรบลฏ' })).toEqual('/VtG7sR9rRJqwNEdM2/escrzyaieuu'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj'); * > expect(encodeRoutePath({ uriId: 'VtG7sR9rRJqwNEdM2', name: ' ahoj_ahojAhoj ahoj ' })).toEqual('/VtG7sR9rRJqwNEdM2/ahoj-ahoj-ahoj-ahoj'); * TODO: [๐ŸŒบ] Use some intermediate util splitWords */ /** * Normalizes a text string to snake_case format. * * Note: [๐Ÿ”‚] This function is idempotent. * * @param text The text string to be converted to snake_case format. * @returns The normalized text in snake_case format. * @example 'hello_world' * @example 'i_love_promptbook' * @public exported from `@promptbook/utils` */ function normalizeTo_snake_case(text) { return normalizeTo_SCREAMING_CASE(text).toLowerCase(); } /** * Global registry for storing and managing registered entities of a given type. * * Note: `$` is used to indicate that this function is not a pure function - it accesses and adds variables in global scope. * * @private internal utility, exported are only singleton instances of this class */ class $Register { constructor(registerName) { this.registerName = registerName; const storageName = `_promptbook_${normalizeTo_snake_case(registerName)}`; const globalScope = $getGlobalScope(); if (globalScope[storageName] === undefined) { globalScope[storageName] = []; } else if (!Array.isArray(globalScope[storageName])) { throw new UnexpectedError(`Expected (global) ${storageName} to be an array, but got ${typeof globalScope[storageName]}`); } this.storage = globalScope[storageName]; } list() { // <- TODO: ReadonlyDeep<ReadonlyArray<TRegistered>> return this.storage; } register(registered) { const { packageName, className } = registered; const existingRegistrationIndex = this.storage.findIndex((item) => item.packageName === packageName && item.className === className); const existingRegistration = this.storage[existingRegistrationIndex]; if (!existingRegistration) { this.storage.push(registered); } else { this.storage[existingRegistrationIndex] = registered; } return { registerName: this.registerName, packageName, className, get isDestroyed() { return false; }, destroy() { throw new NotYetImplementedError(`Registration to ${this.registerName} is permanent in this version of Promptbook`); }, }; } } /** * Registry for all available scrapers in the system. * Central point for registering and accessing different types of content scrapers. * * Note: `$` is used to indicate that this interacts with the global scope * @singleton Only one instance of each register is created per build, but there can be more than one in different build modules * @public exported from `@promptbook/core` */ const $scrapersRegister = new $Register('scraper_constructors'); /** * TODO: [ยฎ] DRY Register logic */ /** * Provides a collection of scrapers optimized for browser environments. * Only includes scrapers that can safely run in a browser context. * * Note: Browser scrapers have limitations compared to Node.js scrapers. * * 1) `provideScrapersForNode` use as default * 2) `provideScrapersForBrowser` use in limited browser environment * * @public exported from `@promptbook/browser` */ async function $provideScrapersForBrowser(tools, options) { if (!$isRunningInBrowser() || $isRunningInWebWorker()) { throw new EnvironmentMismatchError('Function `$provideScrapersForBrowser` works only in browser environment'); } const { isAutoInstalled /* Note: [0] Intentionally not assigning a default value = IS_AUTO_INSTALLED */ } = options || {}; if (isAutoInstalled === true /* <- Note: [0] Ignoring undefined, just checking EXPLICIT requirement for install */) { throw new EnvironmentMismatchError('Auto-installing is not supported in browser environment'); } const scrapers = []; for (const scraperFactory of $scrapersRegister.list()) { const scraper = await scraperFactory(tools, options || {}); scrapers.push(scraper); } return scrapers; } /** * Creates a PromptbookStorage backed by IndexedDB. * Uses a single object store named 'promptbook'. * @private for `getIndexedDbStorage` */ function makePromptbookStorageFromIndexedDb(options) { const { databaseName, storeName } = options; function getDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(databaseName, 1); request.onupgradeneeded = () => { request.result.createObjectStore(storeName); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } return { async getItem(key) { const database = await getDatabase(); return new Promise((resolve, reject) => { const transaction = database.transaction(storeName, 'readonly');