import { LocalStorage } from "@raycast/api"; import type { PlaceholderMapping } from "./types"; const REPLY_DEFAULTS_KEY = "velum.reply.defaults.v1"; export type ReplyDefaults = { greeting: string; signOff: string; creativity: string; disclosure: boolean; }; export async function loadReplyDefaults(): Promise> { const raw = await LocalStorage.getItem(REPLY_DEFAULTS_KEY); if (!raw) return {}; try { return JSON.parse(raw) as Partial; } catch { return {}; } } export async function saveReplyDefaults(values: ReplyDefaults): Promise { await LocalStorage.setItem(REPLY_DEFAULTS_KEY, JSON.stringify(values)); } export const GREETING_OPTIONS: Array<{ value: string; title: string }> = [ { value: "Lieber", title: "Lieber" }, { value: "Liebe", title: "Liebe" }, { value: "Hallo", title: "Hallo" }, { value: "Hi", title: "Hi" }, { value: "Sehr geehrter", title: "Sehr geehrter" }, { value: "Sehr geehrte", title: "Sehr geehrte" }, ]; export const SIGN_OFF_OPTIONS: Array<{ value: string; title: string }> = [ { value: "Alles Liebe,", title: "Alles Liebe," }, { value: "Liebe Grüße,", title: "Liebe Grüße," }, { value: "Viele Grüße,", title: "Viele Grüße," }, { value: "Beste Grüße,", title: "Beste Grüße," }, { value: "Mit freundlichen Grüßen,", title: "Mit freundlichen Grüßen," }, ]; export function listPersonPlaceholdersInText( pseudonymizedText: string, mapping: PlaceholderMapping, ): string[] { return listPersonPlaceholders(mapping).filter((placeholder) => pseudonymizedText.includes(placeholder), ); } export function guessRecipientPlaceholder( pseudonymizedText: string, mapping: PlaceholderMapping, ): string | null { const persons = listPersonPlaceholdersInText(pseudonymizedText, mapping); if (persons.length === 0) return null; const fromMatch = pseudonymizedText.match( /(?:^|\n)\s*(?:Von|From|Absender)\s*:[^\n]*?()/i, ); if (fromMatch) return fromMatch[1]; const signOffMatch = pseudonymizedText.match( /(?:Liebe Grüße|Liebe Gruesse|Beste Grüße|Beste Gruesse|Mit freundlichen Grüßen|Mit freundlichen Gruessen|Viele Grüße|Viele Gruesse|Alles Liebe|Herzliche Grüße|Herzliche Gruesse|MfG|LG|VG|Cheers|Best regards|Kind regards)[\s,!.\n]+()/i, ); if (signOffMatch) return signOffMatch[1]; const greetingMatch = pseudonymizedText.match( /(?:Lieber|Liebe|Hallo|Hi|Hey|Sehr geehrter|Sehr geehrte|Guten Tag)\s+(?:Herr |Frau )?()/i, ); const greetingPlaceholder = greetingMatch ? greetingMatch[1] : null; for (const placeholder of persons) { if (placeholder === greetingPlaceholder) continue; return placeholder; } return persons[0]; } export function listPersonPlaceholders(mapping: PlaceholderMapping): string[] { return Object.entries(mapping) .filter(([, entry]) => entry.type.toUpperCase() === "PERSON") .map(([placeholder]) => placeholder) .sort((a, b) => { const numA = Number.parseInt(a.replace(/\D/g, ""), 10); const numB = Number.parseInt(b.replace(/\D/g, ""), 10); if (Number.isFinite(numA) && Number.isFinite(numB)) { return numA - numB; } return a.localeCompare(b); }); } export function extractPersonPlaceholdersFromText( pseudonymizedText: string, ): string[] { const matches = pseudonymizedText.match(//g) ?? []; const unique = Array.from(new Set(matches)); return unique.sort((a, b) => { const numA = Number.parseInt(a.replace(/\D/g, ""), 10); const numB = Number.parseInt(b.replace(/\D/g, ""), 10); return numA - numB; }); } export function buildReplyPrompt(options: { pseudonymizedText: string; greeting: string; instructions?: string; }): string { const extra = options.instructions?.trim() ? `\n\nZusätzliche Anweisungen des Nutzers:\n${options.instructions.trim()}` : ""; const availablePersons = extractPersonPlaceholdersFromText( options.pseudonymizedText, ); const allowlistText = availablePersons.length > 0 ? `Erlaubte PERSON-Platzhalter (du darfst NUR genau einen aus dieser Liste verwenden, zeichengetreu inkl. spitzer Klammern): ${availablePersons.join(", ")}.` : "In der Konversation kommt kein PERSON-Platzhalter vor. Verzichte in diesem Fall ausnahmsweise auf den Namen in der Anrede und schreibe nur die Anrede ohne Namen, gefolgt von einem Komma."; return [ "Du erhältst eine pseudonymisierte Email-Konversation (oder eine einzelne Email) und sollst eine Antwort verfassen.", "", "Personenbezogene Daten wurden bereits durch Platzhalter wie , , , , ersetzt.", "STRENGE REGEL: Gib jeden Platzhalter zeichengetreu (inklusive spitzer Klammern, Großbuchstaben und Unterstrich + Nummer) zurück. Erfinde keine neuen Platzhalter.", "Du darfst Platzhalter NIEMALS auflösen, raten, übersetzen oder mit erfundenen Namen ersetzen.", "", "WICHTIG — wer ist der Empfänger der Antwort?", "Die zu beantwortende Email ist die OBERSTE/NEUESTE im Text (Email-Clients zeigen neueste oben).", "In dieser obersten Email gilt:", "- Direkt am Anfang steht meist eine Anrede wie „Lieber ,“ oder „Hallo ,“. Diese Person ist der EMPFÄNGER der EINGEGANGENEN Email — also der NUTZER selbst, für den du die Antwort verfasst. Verwende DIESEN Platzhalter NICHT als Adressaten deiner Antwort.", "- Am Ende der obersten Email (vor möglichen älteren, eingerückten/zitierten Mails) steht meist eine Grußformel („LG “, „Liebe Grüße “, „Mit freundlichen Grüßen “, o. ä.). Diese Person ist der SENDER der eingegangenen Email — und damit der EMPFÄNGER deiner Antwort.", "Beispiel: Eingang lautet „Lieber , … LG “. Dann ist der Nutzer (NICHT verwenden), und du adressierst die Antwort an .", "", "Aufgabe: Verfasse die Antwort auf Deutsch.", "", "Format der Antwort — exakt einhalten:", `1. Erste Zeile: Begrüßung im Format „${options.greeting} ,“ — wähle den Platzhalter des SENDERS der obersten Email (siehe oben). Wähle NICHT den Platzhalter aus der Anrede der eingegangenen Email.`, ` ${allowlistText}`, " ABSOLUT VERBOTEN: Schreibe niemals ``, ``, ``, `` oder ähnliche Schema-Platzhalter mit Buchstaben statt Ziffern. Nur konkrete Werte aus der Allowlist sind erlaubt.", "2. Eine Leerzeile.", "3. Den eigentlichen Antworttext.", "WICHTIG: Schreibe KEINE Signatur, KEINE Grußformel und KEINEN Absendernamen am Ende — die Grußformel und der Absender werden außerhalb der KI angefügt.", extra, "", "--- Email-Konversation (pseudonymisiert) ---", options.pseudonymizedText, "--- Ende der Konversation ---", ].join("\n"); } export function composeReplyEmail(options: { body: string; signOff: string; userFullName: string; disclosureBlock?: string; }): string { const trimmedBody = options.body.trim(); const signature = `${options.signOff}\n${options.userFullName.trim()}`; const tail = options.disclosureBlock?.trim() ? `\n\n${options.disclosureBlock.trim()}` : ""; return `${trimmedBody}\n\n${signature}${tail}`; } export function extractAllPlaceholdersFromText( pseudonymizedText: string, ): string[] { const matches = pseudonymizedText.match(/<[A-Z]+_\d+>/g) ?? []; return Array.from(new Set(matches)); } export type DisclosureContent = { text: string; html: string; }; function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">"); } const TYPE_LABELS: Record = { PERSON: "Personen", ORT: "Orte", LOCATION: "Orte", GPE: "Orte", EMAIL: "E-Mail-Adressen", EMAIL_ADDRESS: "E-Mail-Adressen", PHONE: "Telefonnummern", PHONE_NUMBER: "Telefonnummern", ADDRESS: "Adressen", ORG: "Organisationen", ORGANIZATION: "Organisationen", DATE: "Datumsangaben", TIME: "Zeitangaben", IBAN: "IBANs", IBAN_CODE: "IBANs", URL: "URLs", CREDIT_CARD: "Kreditkartennummern", ID: "IDs", MISC: "Sonstiges", }; function labelForType(type: string): string { const upper = type.toUpperCase(); return TYPE_LABELS[upper] ?? upper; } export function buildDisclosureContent( pseudonymizedText: string, mapping: PlaceholderMapping, ): DisclosureContent | null { const entries = Object.entries(mapping) .filter(([key]) => { const stripped = key.replace(/[<>]/g, ""); return ( pseudonymizedText.includes(key) || pseudonymizedText.includes(`<${stripped}>`) || pseudonymizedText.includes(stripped) ); }) .map(([key, entry]) => ({ original: entry.original, placeholder: key.replace(/[<>]/g, ""), type: entry.type, })) .sort((a, b) => a.placeholder.localeCompare(b.placeholder)); if (entries.length === 0) return null; const groups = new Map(); for (const entry of entries) { const label = labelForType(entry.type); const list = groups.get(label) ?? []; list.push(entry); groups.set(label, list); } const groupedEntries = Array.from(groups.entries()).sort(([a], [b]) => a.localeCompare(b), ); const heading = "Dieses Email wurde KI generiert."; const subheading = "Folgende Texte wurden vor der Übermittlung lokal maskiert:"; const text = [ "---", heading, subheading, ...groupedEntries.map( ([label, list]) => `${label}: ${list.map((e) => `${e.original} (${e.placeholder})`).join(", ")}`, ), ].join("\n"); const html = [ "
", `

${escapeHtml(heading)}
${escapeHtml(subheading)}

`, "

", groupedEntries .map( ([label, list]) => `${escapeHtml(label)}: ${list .map( (e) => `${escapeHtml(e.original)} (${escapeHtml(e.placeholder)})`, ) .join(", ")}`, ) .join("
"), "

", ].join("\n"); return { text, html }; } export function plainTextToHtmlEmail(plainText: string): string { const escaped = plainText .replace(/&/g, "&") .replace(//g, ">"); const paragraphs = escaped.split(/\n\n+/); return paragraphs .map((paragraph) => { if (/^\s*---\s*$/.test(paragraph)) { return "
"; } if (/^---\n/.test(paragraph)) { const rest = paragraph.replace(/^---\n/, ""); return `
\n

${rest.replace(/\n/g, "
")}

`; } return `

${paragraph.replace(/\n/g, "
")}

`; }) .join("\n"); }