index.ts 14.2 KB
Newer Older
1
2
import { v4 as uuidv4 } from 'uuid';
import sha256 from 'js-sha256';
3
4

import { getModels } from '$lib/apis';
5

Timothy J. Baek's avatar
Timothy J. Baek committed
6
export const getAllModels = async (token: string) => {
7
8
9
10
	let models = await getModels(token).catch((error) => {
		console.log(error);
		return null;
	});
11

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
12
	models = models.filter((models) => models).reduce((a, e, i, arr) => a.concat(e), []);
13

14
	console.log(models);
15
16
	return models;
};
17
18
19
20
21

//////////////////////////
// Helper functions
//////////////////////////

22
23
24
25
26
27
28
29
30
31
32
export const sanitizeResponseContent = (content: string) => {
	return content
		.replace(/<\|[a-z]*$/, '')
		.replace(/<\|[a-z]+\|$/, '')
		.replace(/<$/, '')
		.replaceAll(/<\|[a-z]+\|>/g, ' ')
		.replaceAll('<', '&lt;')
		.trim();
};

export const revertSanitizedResponseContent = (content: string) => {
Danny Liu's avatar
Danny Liu committed
33
	return content.replaceAll('&lt;', '<');
34
35
};

Timothy J. Baek's avatar
Timothy J. Baek committed
36
37
38
39
export const capitalizeFirstLetter = (string) => {
	return string.charAt(0).toUpperCase() + string.slice(1);
};

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export const splitStream = (splitOn) => {
	let buffer = '';
	return new TransformStream({
		transform(chunk, controller) {
			buffer += chunk;
			const parts = buffer.split(splitOn);
			parts.slice(0, -1).forEach((part) => controller.enqueue(part));
			buffer = parts[parts.length - 1];
		},
		flush(controller) {
			if (buffer) controller.enqueue(buffer);
		}
	});
};

export const convertMessagesToHistory = (messages) => {
56
	const history = {
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
		messages: {},
		currentId: null
	};

	let parentMessageId = null;
	let messageId = null;

	for (const message of messages) {
		messageId = uuidv4();

		if (parentMessageId !== null) {
			history.messages[parentMessageId].childrenIds = [
				...history.messages[parentMessageId].childrenIds,
				messageId
			];
		}

		history.messages[messageId] = {
			...message,
			id: messageId,
			parentId: parentMessageId,
			childrenIds: []
		};

		parentMessageId = messageId;
	}

	history.currentId = messageId;
	return history;
};

export const getGravatarURL = (email) => {
	// Trim leading and trailing whitespace from
	// an email address and force all characters
	// to lower case
	const address = String(email).trim().toLowerCase();

	// Create a SHA256 hash of the final string
	const hash = sha256(address);

	// Grab the actual image URL
	return `https://www.gravatar.com/avatar/${hash}`;
};
Timothy J. Baek's avatar
Timothy J. Baek committed
100

101
102
103
export const canvasPixelTest = () => {
	// Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
	// Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
Danny Liu's avatar
Danny Liu committed
104
	const canvas = document.createElement('canvas');
105
106
107
108
109
110
111
	const ctx = canvas.getContext('2d');
	canvas.height = 1;
	canvas.width = 1;
	const imageData = new ImageData(canvas.width, canvas.height);
	const pixelValues = imageData.data;

	// Generate RGB test data
Danny Liu's avatar
Danny Liu committed
112
113
	for (let i = 0; i < imageData.data.length; i += 1) {
		if (i % 4 !== 3) {
114
			pixelValues[i] = Math.floor(256 * Math.random());
Danny Liu's avatar
Danny Liu committed
115
		} else {
116
117
118
119
120
121
122
123
			pixelValues[i] = 255;
		}
	}

	ctx.putImageData(imageData, 0, 0);
	const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;

	// Read RGB data and fail if unmatched
Danny Liu's avatar
Danny Liu committed
124
125
126
127
128
129
130
131
132
133
134
	for (let i = 0; i < p.length; i += 1) {
		if (p[i] !== pixelValues[i]) {
			console.log(
				'canvasPixelTest: Wrong canvas pixel RGB value detected:',
				p[i],
				'at:',
				i,
				'expected:',
				pixelValues[i]
			);
			console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
135
136
137
138
139
			return false;
		}
	}

	return true;
Danny Liu's avatar
Danny Liu committed
140
};
141

142
export const generateInitialsImage = (name) => {
Danny Liu's avatar
Danny Liu committed
143
144
145
146
	const canvas = document.createElement('canvas');
	const ctx = canvas.getContext('2d');
	canvas.width = 100;
	canvas.height = 100;
147

148
	if (!canvasPixelTest()) {
Danny Liu's avatar
Danny Liu committed
149
150
151
		console.log(
			'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
		);
152
153
154
		return '/user.png';
	}

Danny Liu's avatar
Danny Liu committed
155
156
	ctx.fillStyle = '#F39C12';
	ctx.fillRect(0, 0, canvas.width, canvas.height);
157

Danny Liu's avatar
Danny Liu committed
158
159
160
161
	ctx.fillStyle = '#FFFFFF';
	ctx.font = '40px Helvetica';
	ctx.textAlign = 'center';
	ctx.textBaseline = 'middle';
162

Danny Liu's avatar
Danny Liu committed
163
	const sanitizedName = name.trim();
Danny Liu's avatar
Danny Liu committed
164
165
166
167
168
169
170
	const initials =
		sanitizedName.length > 0
			? sanitizedName[0] +
			  (sanitizedName.split(' ').length > 1
					? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
					: '')
			: '';
Danny Liu's avatar
Danny Liu committed
171

Danny Liu's avatar
Danny Liu committed
172
	ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
173

Danny Liu's avatar
Danny Liu committed
174
	return canvas.toDataURL();
175
176
};

Timothy J. Baek's avatar
Timothy J. Baek committed
177
178
export const copyToClipboard = async (text) => {
	let result = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
179
	if (!navigator.clipboard) {
Timothy J. Baek's avatar
Timothy J. Baek committed
180
		const textArea = document.createElement('textarea');
Timothy J. Baek's avatar
Timothy J. Baek committed
181
182
183
184
185
186
187
188
189
190
191
192
		textArea.value = text;

		// Avoid scrolling to bottom
		textArea.style.top = '0';
		textArea.style.left = '0';
		textArea.style.position = 'fixed';

		document.body.appendChild(textArea);
		textArea.focus();
		textArea.select();

		try {
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
			const successful = document.execCommand('copy');
			const msg = successful ? 'successful' : 'unsuccessful';
Timothy J. Baek's avatar
Timothy J. Baek committed
195
			console.log('Fallback: Copying text command was ' + msg);
Timothy J. Baek's avatar
Timothy J. Baek committed
196
			result = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
197
198
199
200
201
		} catch (err) {
			console.error('Fallback: Oops, unable to copy', err);
		}

		document.body.removeChild(textArea);
Timothy J. Baek's avatar
Timothy J. Baek committed
202
		return result;
Timothy J. Baek's avatar
Timothy J. Baek committed
203
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
204
205
206
207

	result = await navigator.clipboard
		.writeText(text)
		.then(() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
208
			console.log('Async: Copying to clipboard was successful!');
Timothy J. Baek's avatar
Timothy J. Baek committed
209
210
211
212
213
214
215
216
			return true;
		})
		.catch((error) => {
			console.error('Async: Could not copy text: ', error);
			return false;
		});

	return result;
Timothy J. Baek's avatar
Timothy J. Baek committed
217
};
Timothy J. Baek's avatar
Timothy J. Baek committed
218

Timothy J. Baek's avatar
Timothy J. Baek committed
219
export const compareVersion = (latest, current) => {
220
	return current === '0.0.0'
Timothy J. Baek's avatar
Timothy J. Baek committed
221
		? false
Timothy J. Baek's avatar
Timothy J. Baek committed
222
		: current.localeCompare(latest, undefined, {
223
224
225
226
				numeric: true,
				sensitivity: 'case',
				caseFirst: 'upper'
		  }) < 0;
Timothy J. Baek's avatar
Timothy J. Baek committed
227
};
Timothy J. Baek's avatar
Timothy J. Baek committed
228
229
230

export const findWordIndices = (text) => {
	const regex = /\[([^\]]+)\]/g;
231
	const matches = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
232
233
234
235
236
237
238
239
240
241
242
243
	let match;

	while ((match = regex.exec(text)) !== null) {
		matches.push({
			word: match[1],
			startIndex: match.index,
			endIndex: regex.lastIndex - 1
		});
	}

	return matches;
};
244

Timothy J. Baek's avatar
Timothy J. Baek committed
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
export const removeFirstHashWord = (inputString) => {
	// Split the string into an array of words
	const words = inputString.split(' ');

	// Find the index of the first word that starts with #
	const index = words.findIndex((word) => word.startsWith('#'));

	// Remove the first word with #
	if (index !== -1) {
		words.splice(index, 1);
	}

	// Join the remaining words back into a string
	const resultString = words.join(' ');

	return resultString;
};

263
264
265
266
267
268
269
270
271
272
273
274
275
export const transformFileName = (fileName) => {
	// Convert to lowercase
	const lowerCaseFileName = fileName.toLowerCase();

	// Remove special characters using regular expression
	const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, '');

	// Replace spaces with dashes
	const finalFileName = sanitizedFileName.replace(/\s+/g, '-');

	return finalFileName;
};

276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
export const calculateSHA256 = async (file) => {
	// Create a FileReader to read the file asynchronously
	const reader = new FileReader();

	// Define a promise to handle the file reading
	const readFile = new Promise((resolve, reject) => {
		reader.onload = () => resolve(reader.result);
		reader.onerror = reject;
	});

	// Read the file as an ArrayBuffer
	reader.readAsArrayBuffer(file);

	try {
		// Wait for the FileReader to finish reading the file
		const buffer = await readFile;

		// Convert the ArrayBuffer to a Uint8Array
		const uint8Array = new Uint8Array(buffer);

		// Calculate the SHA-256 hash using Web Crypto API
		const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array);

		// Convert the hash to a hexadecimal string
		const hashArray = Array.from(new Uint8Array(hashBuffer));
		const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');

Timothy J. Baek's avatar
Timothy J. Baek committed
303
		return `${hashHex}`;
304
305
306
307
308
	} catch (error) {
		console.error('Error calculating SHA-256 hash:', error);
		throw error;
	}
};
309
310
311

export const getImportOrigin = (_chats) => {
	// Check what external service chat imports are from
Timothy J. Baek's avatar
Timothy J. Baek committed
312
313
314
315
316
	if ('mapping' in _chats[0]) {
		return 'openai';
	}
	return 'webui';
};
317

Timothy J. Baek's avatar
Timothy J. Baek committed
318
const convertOpenAIMessages = (convo) => {
319
	// Parse OpenAI chat messages and create chat dictionary for creating new chats
Timothy J. Baek's avatar
Timothy J. Baek committed
320
	const mapping = convo['mapping'];
321
	const messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
322
	let currentId = '';
323
	let lastId = null;
324

Timothy J. Baek's avatar
Timothy J. Baek committed
325
326
	for (let message_id in mapping) {
		const message = mapping[message_id];
327
		currentId = message_id;
328
		try {
Timothy J. Baek's avatar
Timothy J. Baek committed
329
330
331
332
333
334
			if (
				messages.length == 0 &&
				(message['message'] == null ||
					(message['message']['content']['parts']?.[0] == '' &&
						message['message']['content']['text'] == null))
			) {
335
336
337
338
339
340
341
342
				// Skip chat messages with no content
				continue;
			} else {
				const new_chat = {
					id: message_id,
					parentId: lastId,
					childrenIds: message['children'] || [],
					role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user',
Timothy J. Baek's avatar
Timothy J. Baek committed
343
344
345
346
					content:
						message['message']?.['content']?.['parts']?.[0] ||
						message['message']?.['content']?.['text'] ||
						'',
347
348
349
350
351
352
353
354
					model: 'gpt-3.5-turbo',
					done: true,
					context: null
				};
				messages.push(new_chat);
				lastId = currentId;
			}
		} catch (error) {
Timothy J. Baek's avatar
Timothy J. Baek committed
355
			console.log('Error with', message, '\nError:', error);
356
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
357
	}
358
359

	let history = {};
Timothy J. Baek's avatar
Timothy J. Baek committed
360
	messages.forEach((obj) => (history[obj.id] = obj));
361
362

	const chat = {
Timothy J. Baek's avatar
Timothy J. Baek committed
363
364
365
		history: {
			currentId: currentId,
			messages: history // Need to convert this to not a list and instead a json object
366
		},
Timothy J. Baek's avatar
Timothy J. Baek committed
367
		models: ['gpt-3.5-turbo'],
Timothy J. Baek's avatar
Timothy J. Baek committed
368
369
370
		messages: messages,
		options: {},
		timestamp: convo['create_time'],
Timothy J. Baek's avatar
Timothy J. Baek committed
371
		title: convo['title'] ?? 'New Chat'
Timothy J. Baek's avatar
Timothy J. Baek committed
372
373
374
	};
	return chat;
};
375

376
377
378
379
const validateChat = (chat) => {
	// Because ChatGPT sometimes has features we can't use like DALL-E or migh have corrupted messages, need to validate
	const messages = chat.messages;

Timothy J. Baek's avatar
Timothy J. Baek committed
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
	// Check if messages array is empty
	if (messages.length === 0) {
		return false;
	}

	// Last message's children should be an empty array
	const lastMessage = messages[messages.length - 1];
	if (lastMessage.childrenIds.length !== 0) {
		return false;
	}

	// First message's parent should be null
	const firstMessage = messages[0];
	if (firstMessage.parentId !== null) {
		return false;
	}

	// Every message's content should be a string
	for (let message of messages) {
		if (typeof message.content !== 'string') {
			return false;
		}
	}

	return true;
405
406
};

Timothy J. Baek's avatar
Timothy J. Baek committed
407
export const convertOpenAIChats = (_chats) => {
408
	// Create a list of dictionaries with each conversation from import
Timothy J. Baek's avatar
Timothy J. Baek committed
409
	const chats = [];
410
	let failed = 0;
Timothy J. Baek's avatar
Timothy J. Baek committed
411
	for (let convo of _chats) {
Timothy J. Baek's avatar
Timothy J. Baek committed
412
413
		const chat = convertOpenAIMessages(convo);

414
		if (validateChat(chat)) {
Timothy J. Baek's avatar
Timothy J. Baek committed
415
416
417
418
419
420
421
			chats.push({
				id: convo['id'],
				user_id: '',
				title: convo['title'],
				chat: chat,
				timestamp: convo['timestamp']
			});
Timothy J. Baek's avatar
Timothy J. Baek committed
422
423
424
		} else {
			failed++;
		}
425
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
426
	console.log(failed, 'Conversations could not be imported');
Timothy J. Baek's avatar
Timothy J. Baek committed
427
428
	return chats;
};
Timothy J. Baek's avatar
Timothy J. Baek committed
429
430
431
432
433
434
435
436
437
438
439
440

export const isValidHttpUrl = (string) => {
	let url;

	try {
		url = new URL(string);
	} catch (_) {
		return false;
	}

	return url.protocol === 'http:' || url.protocol === 'https:';
};
Timothy J. Baek's avatar
Timothy J. Baek committed
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457

export const removeEmojis = (str) => {
	// Regular expression to match emojis
	const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;

	// Replace emojis with an empty string
	return str.replace(emojiRegex, '');
};

export const extractSentences = (text) => {
	// Split the paragraph into sentences based on common punctuation marks
	const sentences = text.split(/(?<=[.!?])/);

	return sentences
		.map((sentence) => removeEmojis(sentence.trim()))
		.filter((sentence) => sentence !== '');
};
Timothy J. Baek's avatar
Timothy J. Baek committed
458
459
460
461
462
463

export const blobToFile = (blob, fileName) => {
	// Create a new File object from the Blob
	const file = new File([blob], fileName, { type: blob.type });
	return file;
};
464

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/**
 * This function is used to replace placeholders in a template string with the provided prompt.
 * The placeholders can be in the following formats:
 * - `{{prompt}}`: This will be replaced with the entire prompt.
 * - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt.
 * - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt.
 * - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle.
 *
 * @param {string} template - The template string containing placeholders.
 * @param {string} prompt - The string to replace the placeholders with.
 * @returns {string} The template string with the placeholders replaced by the prompt.
 */
export const promptTemplate = (template: string, prompt: string): string => {
	return template.replace(
		/{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g,
		(match, startLength, endLength, middleLength) => {
			if (match === '{{prompt}}') {
				return prompt;
			} else if (match.startsWith('{{prompt:start:')) {
				return prompt.substring(0, startLength);
			} else if (match.startsWith('{{prompt:end:')) {
				return prompt.slice(-endLength);
			} else if (match.startsWith('{{prompt:middletruncate:')) {
				if (prompt.length <= middleLength) {
					return prompt;
				}
				const start = prompt.slice(0, Math.ceil(middleLength / 2));
				const end = prompt.slice(-Math.floor(middleLength / 2));
				return `${start}...${end}`;
			}
			return '';
		}
497
	);
Timothy J. Baek's avatar
Timothy J. Baek committed
498
499
};

500
export const approximateToHumanReadable = (nanoseconds: number) => {
Self Denial's avatar
Self Denial committed
501
502
503
	const seconds = Math.floor((nanoseconds / 1e9) % 60);
	const minutes = Math.floor((nanoseconds / 6e10) % 60);
	const hours = Math.floor((nanoseconds / 3.6e12) % 24);
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519

	const results: string[] = [];

	if (seconds >= 0) {
		results.push(`${seconds}s`);
	}

	if (minutes > 0) {
		results.push(`${minutes}m`);
	}

	if (hours > 0) {
		results.push(`${hours}h`);
	}

	return results.reverse().join(' ');
520
};
521
522
523
524
525
526
527
528
529

export const getTimeRange = (timestamp) => {
	const now = new Date();
	const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds

	// Calculate the difference in milliseconds
	const diffTime = now.getTime() - date.getTime();
	const diffDays = diffTime / (1000 * 3600 * 24);

Timothy J. Baek's avatar
Timothy J. Baek committed
530
531
532
533
534
535
536
537
538
	const nowDate = now.getDate();
	const nowMonth = now.getMonth();
	const nowYear = now.getFullYear();

	const dateDate = date.getDate();
	const dateMonth = date.getMonth();
	const dateYear = date.getFullYear();

	if (nowYear === dateYear && nowMonth === dateMonth && nowDate === dateDate) {
539
		return 'Today';
Timothy J. Baek's avatar
Timothy J. Baek committed
540
	} else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) {
541
542
543
544
545
		return 'Yesterday';
	} else if (diffDays <= 7) {
		return 'Previous 7 days';
	} else if (diffDays <= 30) {
		return 'Previous 30 days';
Timothy J. Baek's avatar
Timothy J. Baek committed
546
	} else if (nowYear === dateYear) {
547
548
549
550
551
		return date.toLocaleString('default', { month: 'long' });
	} else {
		return date.getFullYear().toString();
	}
};