andy-stack_vaultkeeper-ai/Helpers/StringTools.ts
Andrew Beal a4800bd6da refactor: convert attachments to file-based storage with lazy loading.
Migrate attachment storage from base64-in-JSON to separate binary files with SHA-256 naming, add automatic garbage collection, implement lazy loading via getBase64() and getMimeType() methods, normalize text MIME types, add image resizing, and update all AI provider integrations. Fix small issue that caused the chat area to scroll down on message streaming end even when scrolled up.
2026-02-15 22:46:25 +00:00

126 lines
No EOL
4.2 KiB
TypeScript

import { Exception } from "./Exception";
export abstract class StringTools {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment -- regex-parser is a CommonJS module without ESM support
private static RegexParser: (input: string) => RegExp = require("regex-parser");
public static isValidJson(str: string): boolean {
try {
JSON.parse(str);
} catch {
return false;
}
return true;
}
public static dateToString(date: Date, includeTime: boolean = true): string {
if (includeTime) {
return date.toLocaleString('sv-SE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/[:\s]/g, '-');
} else {
return date.toLocaleDateString('sv-SE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/[:\s]/g, '-');
}
}
public static escapeRegex(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
public static asRegex(input: string, requiredFlags: string[]): RegExp | null {
let regex: RegExp;
try {
regex = this.RegexParser(input);
let flags = regex.flags;
for (const requiredFlag of requiredFlags) {
if (!flags.includes(requiredFlag)) {
flags = flags + requiredFlag;
}
}
regex = new RegExp(regex.source, flags);
} catch {
try { // If parsing fails, escape the input and use required flags
regex = new RegExp(StringTools.escapeRegex(input), requiredFlags.join(""));
} catch {
return null;
}
}
return regex;
}
public static toBytes(input: string): Uint8Array<ArrayBuffer> {
const binaryString = atob(input);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
public static async computeSHA256Hash(base64: string): Promise<string> {
const bytes = this.toBytes(base64);
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
public static async resizeB64Image(base64: string, mimeType: string, maxWidth: number = 1000, maxHeight: number = 1000): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
let width = img.width;
let height = img.height;
if (width > maxWidth || height > maxHeight) {
const aspectRatio = width / height;
if (width > height) {
width = maxWidth;
height = maxWidth / aspectRatio;
} else {
height = maxHeight;
width = maxHeight * aspectRatio;
}
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
reject(Exception.new("Failed to get canvas context"));
return;
}
context.drawImage(img, 0, 0, width, height);
const dataURL = canvas.toDataURL(mimeType, 0.92);
const base64Only = dataURL.split(',')[1];
resolve(base64Only);
};
img.onerror = () => reject(Exception.new("Failed to load image"));
if (!base64.startsWith('data:')) {
base64 = `data:${mimeType};base64,${base64}`;
}
img.src = base64;
});
}
}