streaming, markdown, model selecting, and lots more

This commit is contained in:
Zoe
2026-02-02 23:16:06 -06:00
parent 8c28946703
commit d5a5945c03
114 changed files with 6109 additions and 2938 deletions
+95
View File
@@ -0,0 +1,95 @@
export async function deriveKey(password: string, userId: string) {
const iterations = 1_000_000;
const saltBuffer = new TextEncoder().encode(userId);
const passwordBuffer = new TextEncoder().encode(password);
const baseKey = await window.crypto.subtle.importKey(
"raw",
passwordBuffer,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
const derivedKey = await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: saltBuffer,
iterations: iterations,
hash: "SHA-256",
},
baseKey,
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
const encryptionKey = await crypto.subtle.exportKey(
'jwk',
derivedKey
);
return encryptionKey;
}
function generateIv() {
return window.crypto.getRandomValues(new Uint8Array(12));
}
export function uint8ArrayToBase64(bytes: Uint8Array<ArrayBuffer>) {
let binaryString = '';
for (let i = 0; i < bytes.byteLength; i++) {
binaryString += String.fromCharCode(bytes[i]!);
}
return window.btoa(binaryString);
}
export function base64ToUint8Array(base64: string) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
export async function encryptData(key: CryptoKey, plaintext: string) {
const iv = generateIv();
const data = new TextEncoder().encode(plaintext);
const encryptedContent = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
data
);
// Combine IV and Ciphertext into one Uint8Array for easy storage
const combined = new Uint8Array(iv.length + encryptedContent.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encryptedContent), iv.length);
return combined;
}
export async function decrypt(key: CryptoKey, combinedData: Uint8Array) {
try {
const iv = combinedData.slice(0, 12);
const ciphertext = combinedData.slice(12);
const decryptedBuffer = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
key,
ciphertext
);
return new TextDecoder().decode(decryptedBuffer);
} catch (error) {
throw new Error("Decryption failed. Incorrect password or corrupted data.");
}
}
+20
View File
@@ -0,0 +1,20 @@
export const hash = async (text: string) => {
const msgBuffer = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
export const hashSync = (text: string) => {
let hash = 5381;
let i = 0;
for (const char of text) {
hash = ((hash << 5) - hash) + char.charCodeAt(0);
hash |= 0;
}
return hash.toString(16);
}
+33
View File
@@ -0,0 +1,33 @@
import {
LogoGrok,
LogoGemini
} from '#components';
interface ModelConfig {
icon: any;
keywords: RegExp[];
brandColor: string;
}
const MODEL_MAPPINGS: ModelConfig[] = [
{
icon: markRaw(LogoGrok),
keywords: [/^grok-/, /^x-ai\//],
brandColor: '#000000'
},
{
icon: markRaw(LogoGemini),
keywords: [/gemini-/],
brandColor: '#000000'
}
];
export function getModelConfig(modelId: string) {
const cleanId = modelId.toLowerCase();
const match = MODEL_MAPPINGS.find(cfg =>
cfg.keywords.some(regex => regex.test(cleanId))
);
return match || { icon: null, brandColor: '#64748b' };
}