spb/chat-spboucher Public
Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai
TypeScript 78.8%
CSS 15.1%
JavaScript 4.9%
Shell 1.2%
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45// Dependency-free PNG icon generator: rasterizes the ▮▯▮ instrument mark6// (three rounded bars, signal teal on ink) at the sizes the PWA needs.78import { deflateSync } from "node:zlib";9import { writeFileSync, mkdirSync } from "node:fs";1011const INK = [0x0e, 0x11, 0x16, 255];12const TEAL = [0x3f, 0xb6, 0xa8, 255];1314function crc32(buf) {15 let table = crc32.table;16 if (!table) {17 table = crc32.table = new Int32Array(256);18 for (let n = 0; n < 256; n++) {19 let c = n;20 for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;21 table[n] = c;22 }23 }24 let c = 0xffffffff;25 for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);26 return (c ^ 0xffffffff) >>> 0;27}2829function chunk(type, data) {30 const len = Buffer.alloc(4);31 len.writeUInt32BE(data.length);32 const body = Buffer.concat([Buffer.from(type, "ascii"), data]);33 const crc = Buffer.alloc(4);34 crc.writeUInt32BE(crc32(body));35 return Buffer.concat([len, body, crc]);36}3738function encodePng(size, pixels) {39 const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);40 const ihdr = Buffer.alloc(13);41 ihdr.writeUInt32BE(size, 0);42 ihdr.writeUInt32BE(size, 4);43 ihdr[8] = 8; // bit depth44 ihdr[9] = 6; // RGBA45 const raw = Buffer.alloc((size * 4 + 1) * size);46 for (let y = 0; y < size; y++) {47 raw[y * (size * 4 + 1)] = 0; // filter none48 pixels.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4);49 }50 return Buffer.concat([51 sig,52 chunk("IHDR", ihdr),53 chunk("IDAT", deflateSync(raw, { level: 9 })),54 chunk("IEND", Buffer.alloc(0)),55 ]);56}5758function inRoundedRect(x, y, rx, ry, w, h, r) {59 if (x < rx || x >= rx + w || y < ry || y >= ry + h) return false;60 const cx = Math.max(rx + r, Math.min(x, rx + w - r));61 const cy = Math.max(ry + r, Math.min(y, ry + h - r));62 return (x - cx) ** 2 + (y - cy) ** 2 <= r * r || (x >= rx + r && x < rx + w - r) || (y >= ry + r && y < ry + h - r);63}6465function drawIcon(size, { maskable = false } = {}) {66 const px = Buffer.alloc(size * size * 4);67 const s = size / 512;68 const bgRadius = maskable ? 0 : 112 * s;69 const bars = [70 { x: 138, filled: true },71 { x: 228, filled: false },72 { x: 318, filled: true },73 ];74 const stroke = 14 * s;75 for (let y = 0; y < size; y++) {76 for (let x = 0; x < size; x++) {77 let color = [0, 0, 0, 0];78 if (maskable || inRoundedRect(x, y, 0, 0, size, size, bgRadius)) color = INK;79 for (const b of bars) {80 const bx = b.x * s;81 const by = 156 * s;82 const bw = 56 * s;83 const bh = 200 * s;84 const br = 12 * s;85 if (inRoundedRect(x, y, bx, by, bw, bh, br)) {86 if (b.filled || !inRoundedRect(x, y, bx + stroke, by + stroke, bw - 2 * stroke, bh - 2 * stroke, Math.max(br - stroke, 0))) {87 color = TEAL;88 }89 }90 }91 const i = (y * size + x) * 4;92 px[i] = color[0];93 px[i + 1] = color[1];94 px[i + 2] = color[2];95 px[i + 3] = color[3];96 }97 }98 return encodePng(size, px);99}100101mkdirSync("public/icons", { recursive: true });102writeFileSync("public/icons/icon-192.png", drawIcon(192));103writeFileSync("public/icons/icon-512.png", drawIcon(512));104writeFileSync("public/icons/icon-maskable-512.png", drawIcon(512, { maskable: true }));105writeFileSync("public/icons/apple-touch-icon.png", drawIcon(180, { maskable: true }));106console.log("icons written");107