SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
10.8 KB · 193 lines typescript
Raw Blame History
1/**2 * Hand-written HTML emails (no React Email dependency): small, deterministic, and3 * renderable everywhere. Each template returns { subject, html, text }.4 */56export interface EmailContent {7  subject: string;8  html: string;9  text: string;10}1112const BRAND = {13  name: "PolyLLM",14  tagline: "Your models. Your keys. One workspace.",15  bg: "#0b0c10",16  card: "#12141a",17  fg: "#f4f5f7",18  muted: "#9aa3b2",19  accent: "#7c8cff",20  accentDark: "#5b6cf0",21  border: "#232734",22};2324function escapeHtml(s: string): string {25  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");26}2728function layout(opts: { siteUrl: string; preheader: string; title: string; body: string; cta?: { label: string; url: string }; footnote?: string }): string {29  const cta = opts.cta30    ? `<table role="presentation" cellspacing="0" cellpadding="0" style="margin:28px 0 8px"><tr><td style="border-radius:10px;background:${BRAND.accent}">31         <a href="${opts.cta.url}" style="display:inline-block;padding:13px 22px;font:600 15px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:#0b0c10;text-decoration:none;border-radius:10px">${escapeHtml(opts.cta.label)}</a>32       </td></tr></table>33       <p style="margin:12px 0 0;font:13px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.muted}">Or paste this link in your browser:<br><a href="${opts.cta.url}" style="color:${BRAND.accent};word-break:break-all">${opts.cta.url}</a></p>`34    : "";35  return `<!doctype html>36<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(opts.title)}</title>37<meta name="color-scheme" content="dark light"><meta name="supported-color-schemes" content="dark light"></head>38<body style="margin:0;padding:0;background:${BRAND.bg}">39<span style="display:none!important;visibility:hidden;opacity:0;color:transparent;height:0;width:0;overflow:hidden">${escapeHtml(opts.preheader)}</span>40<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:${BRAND.bg};padding:40px 16px">41<tr><td align="center">42<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:560px">43  <tr><td style="padding:0 8px 20px">44    <a href="${opts.siteUrl}" style="text-decoration:none;display:inline-flex;align-items:center;gap:10px">45      <span style="display:inline-block;width:28px;height:28px;border-radius:8px;background:linear-gradient(135deg,${BRAND.accent},#b58cff);vertical-align:middle"></span>46      <span style="font:700 18px/28px -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.fg};vertical-align:middle;margin-left:10px">PolyLLM</span>47    </a>48  </td></tr>49  <tr><td style="background:${BRAND.card};border:1px solid ${BRAND.border};border-radius:16px;padding:36px 32px">50    <h1 style="margin:0 0 16px;font:700 22px/1.3 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.fg};letter-spacing:-0.01em">${escapeHtml(opts.title)}</h1>51    <div style="font:15px/1.65 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.fg}">${opts.body}</div>52    ${cta}53    ${opts.footnote ? `<p style="margin:24px 0 0;padding-top:20px;border-top:1px solid ${BRAND.border};font:13px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.muted}">${opts.footnote}</p>` : ""}54  </td></tr>55  <tr><td style="padding:20px 8px 0;font:12px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;color:${BRAND.muted}">56    ${BRAND.name} — ${BRAND.tagline}<br>57    <a href="${opts.siteUrl}" style="color:${BRAND.muted}">${opts.siteUrl.replace(/^https?:\/\//, "")}</a>58  </td></tr>59</table></td></tr></table></body></html>`;60}6162function p(text: string) {63  return `<p style="margin:0 0 14px">${text}</p>`;64}6566const textFooter = (siteUrl: string) => `\n\n—\n${BRAND.name} — ${BRAND.tagline}\n${siteUrl}\n`;6768export function verificationEmail(siteUrl: string, url: string): EmailContent {69  return {70    subject: "Verify your email — PolyLLM",71    html: layout({72      siteUrl,73      preheader: "Confirm your email address to activate your PolyLLM account.",74      title: "Confirm your email",75      body: p("Welcome to PolyLLM. Click the button below to verify your email address and activate your account.") + p("This link expires in 24 hours."),76      cta: { label: "Verify email", url },77      footnote: "If you didn't create a PolyLLM account, you can safely ignore this email.",78    }),79    text: `Confirm your email\n\nWelcome to PolyLLM. Open the link below to verify your email address and activate your account (expires in 24 hours):\n\n${url}\n\nIf you didn't create a PolyLLM account, you can ignore this email.${textFooter(siteUrl)}`,80  };81}8283export function welcomeEmail(siteUrl: string, name: string): EmailContent {84  const hi = name ? `Hi ${escapeHtml(name)},` : "Hi,";85  return {86    subject: "Welcome to PolyLLM",87    html: layout({88      siteUrl,89      preheader: "Your account is verified. Connect a provider to start chatting.",90      title: "Welcome to PolyLLM",91      body:92        p(hi) +93        p("Your email is verified. PolyLLM is a universal control center for AI models: connect your own API keys, discover the latest models, configure them precisely, and chat with every provider through one interface.") +94        p("<strong>Next step:</strong> connect at least one provider (OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, Kimi, OpenRouter or Cerebras). Your keys are encrypted at rest and only decrypted on the server immediately before a request."),95      cta: { label: "Connect a provider", url: `${siteUrl}/app/settings/providers` },96    }),97    text: `${name ? `Hi ${name},` : "Hi,"}\n\nYour email is verified. Connect at least one provider to start chatting:\n${siteUrl}/app/settings/providers${textFooter(siteUrl)}`,98  };99}100101export function passwordResetEmail(siteUrl: string, url: string): EmailContent {102  return {103    subject: "Reset your PolyLLM password",104    html: layout({105      siteUrl,106      preheader: "Use this link to choose a new password.",107      title: "Reset your password",108      body: p("We received a request to reset the password of your PolyLLM account. Click below to choose a new one.") + p("This link expires in 1 hour and can be used once."),109      cta: { label: "Choose a new password", url },110      footnote: "If you didn't request a password reset, no action is needed — your password stays unchanged.",111    }),112    text: `Reset your password\n\nOpen this link to choose a new password (expires in 1 hour):\n\n${url}\n\nIf you didn't request this, ignore this email.${textFooter(siteUrl)}`,113  };114}115116export function passwordChangedEmail(siteUrl: string, when: string, ip?: string | null): EmailContent {117  const where = ip ? ` from IP ${escapeHtml(ip)}` : "";118  return {119    subject: "Your PolyLLM password was changed",120    html: layout({121      siteUrl,122      preheader: "Security notice: your password was changed.",123      title: "Password changed",124      body: p(`The password of your PolyLLM account was changed on ${escapeHtml(when)}${where}.`) + p("If this was you, no action is needed. Other sessions have been signed out."),125      cta: { label: "Review security settings", url: `${siteUrl}/app/settings/security` },126      footnote: "If you did not make this change, reset your password immediately and review your active sessions.",127    }),128    text: `Password changed\n\nThe password of your PolyLLM account was changed on ${when}${ip ? ` from IP ${ip}` : ""}. If this wasn't you, reset your password immediately: ${siteUrl}/forgot-password${textFooter(siteUrl)}`,129  };130}131132export function emailChangeApprovalEmail(siteUrl: string, url: string, newEmail: string): EmailContent {133  return {134    subject: "Approve your email change — PolyLLM",135    html: layout({136      siteUrl,137      preheader: "Confirm that you want to change the email on your account.",138      title: "Approve email change",139      body: p(`You asked to change the email of your PolyLLM account to <strong>${escapeHtml(newEmail)}</strong>.`) + p("Confirm this change from your current address by clicking below."),140      cta: { label: "Approve change", url },141      footnote: "If you didn't request this, ignore this email — nothing changes without your approval.",142    }),143    text: `Approve email change\n\nYou asked to change your PolyLLM email to ${newEmail}. Confirm here:\n\n${url}${textFooter(siteUrl)}`,144  };145}146147export function emailChangedEmail(siteUrl: string, newEmail: string): EmailContent {148  return {149    subject: "Your PolyLLM email was changed",150    html: layout({151      siteUrl,152      preheader: "Security notice: the email on your account changed.",153      title: "Email changed",154      body: p(`The email address of your PolyLLM account is now <strong>${escapeHtml(newEmail)}</strong>.`),155      footnote: "If you did not make this change, contact us immediately by replying to this email.",156    }),157    text: `Email changed\n\nThe email of your PolyLLM account is now ${newEmail}. If this wasn't you, reply to this email immediately.${textFooter(siteUrl)}`,158  };159}160161export function accountDeletionEmail(siteUrl: string, url: string): EmailContent {162  return {163    subject: "Confirm account deletion — PolyLLM",164    html: layout({165      siteUrl,166      preheader: "Confirm that you want to permanently delete your account.",167      title: "Delete your account?",168      body: p("You asked to permanently delete your PolyLLM account, including your conversations, presets, usage history and encrypted provider keys.") + p("<strong>This cannot be undone.</strong> Confirm by clicking below."),169      cta: { label: "Permanently delete my account", url },170      footnote: "If you didn't request this, ignore this email — your account stays untouched.",171    }),172    text: `Delete your account?\n\nConfirm permanent deletion of your PolyLLM account (irreversible):\n\n${url}${textFooter(siteUrl)}`,173  };174}175176export function newLoginEmail(siteUrl: string, when: string, ip?: string | null, userAgent?: string | null): EmailContent {177  return {178    subject: "New sign-in to PolyLLM",179    html: layout({180      siteUrl,181      preheader: "A new sign-in to your account was detected.",182      title: "New sign-in detected",183      body:184        p(`Your PolyLLM account was signed in on ${escapeHtml(when)}.`) +185        `<ul style="margin:0 0 14px;padding-left:18px">${ip ? `<li>IP address: ${escapeHtml(ip)}</li>` : ""}${userAgent ? `<li>Device: ${escapeHtml(userAgent.slice(0, 120))}</li>` : ""}</ul>` +186        p("If this was you, no action is needed."),187      cta: { label: "Review sessions", url: `${siteUrl}/app/settings/security` },188      footnote: "If you don't recognize this sign-in, change your password and sign out of all devices.",189    }),190    text: `New sign-in detected on ${when}${ip ? ` from ${ip}` : ""}. Review your sessions: ${siteUrl}/app/settings/security${textFooter(siteUrl)}`,191  };192}193