Generate an editable norm-compliant cover email per application

Alongside the tailored PDFs, the LLM now produces a short, DIN-style
Begleit-E-Mail (subject + body) grounded in the same data: same
salutation as the cover letter, the target role, a line or two of core
motivation, a pointer to the attached documents, and a friendly close.

Persisted in new bewerbungen columns (email_betreff, email_anschreiben)
and surfaced in an editable subject field + textarea on the detail page,
so the applicant can tweak it before sending; a copy button puts subject
and body on the clipboard. Values are stored raw and HTML-escaped on
render to avoid double-escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 15:20:56 +02:00
co-authored by Claude Opus 4.8
parent 0bfa5683fb
commit 3c553cadf5
3 changed files with 113 additions and 5 deletions
+33 -2
View File
@@ -82,6 +82,14 @@ const OUTPUT_SCHEMA = {
},
required: ['empfaenger', 'betreff', 'anrede', 'absaetze', 'gruss'],
},
email: {
type: 'object',
properties: {
betreff: { type: 'string' },
text: { type: 'string' },
},
required: ['betreff', 'text'],
},
lebenslauf: {
type: 'object',
properties: {
@@ -116,7 +124,7 @@ const OUTPUT_SCHEMA = {
required: ['profil', 'berufserfahrung', 'studium', 'berufsausbildung', 'weiterbildungen', 'kenntnisse', 'sprachen', 'hobbys'],
},
},
required: ['headline', 'kontakt', 'anschreiben', 'lebenslauf'],
required: ['headline', 'kontakt', 'anschreiben', 'email', 'lebenslauf'],
};
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '' }) {
@@ -174,6 +182,10 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
` "absaetze": ["Absatz 1", "Absatz 2", "Absatz 3"],\n` +
` "gruss": "Mit freundlichen Grüßen"\n` +
` },\n` +
` "email": {\n` +
` "betreff": "Bewerbung als … (ggf. mit Referenz-/Kennnummer)",\n` +
` "text": "Kurzer, sofort versendbarer E-Mail-Text: Anrede, 2-3 knappe Absätze, Grußformel, Name - Absätze durch Leerzeile getrennt"\n` +
` },\n` +
` "lebenslauf": {\n` +
` "profil": "2-3 Sätze Kurzprofil, zugeschnitten auf die Zielstelle",\n` +
` "berufserfahrung": [\n` +
@@ -234,6 +246,17 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
`Nur der ERSTE Absatz beginnt mit einem KLEINbuchstaben (die Anrede endet mit Komma, der Satz wird ` +
`fortgesetzt), z. B. "mit …" oder "als …" — außer das erste Wort ist ein Substantiv, ein Eigenname oder die ` +
`Höflichkeitsform "Ihre/Ihr/Ihnen". Alle WEITEREN Absätze sind neue Sätze und beginnen normal mit GROSSbuchstaben.\n` +
`- email: ein KURZES, normgerechtes Begleit-E-Mail-Anschreiben, mit dem die Bewerbung samt Anhängen ` +
`(Lebenslauf, Anschreiben, ggf. Zeugnisse) per E-Mail versendet wird. Es ist NICHT das vollständige ` +
`Anschreiben - wiederhole dessen Inhalt nicht, fasse dich bewusst knapp (Faustregel: höchstens 6-8 Sätze). ` +
`email.betreff: prägnante Betreffzeile, i. d. R. "Bewerbung als <Stelle>" (eine in Anzeige/Notizen genannte ` +
`Referenz-/Kennnummer anhängen). email.text: vollständiger, sofort versendbarer E-Mail-Text mit exakt dieser ` +
`Struktur - (1) dieselbe Anrede wie im Anschreiben; (2) ein Satz, auf welche Stelle man sich bewirbt (falls ` +
`bekannt: wo die Stelle gefunden wurde); (3) ein bis zwei Sätze zur Kernmotivation/Kernqualifikation; ` +
`(4) ein Satz mit dem Hinweis, dass die vollständigen Bewerbungsunterlagen im Anhang beiliegen; (5) ein ` +
`freundlicher Ausblick auf ein persönliches Gespräch; (6) Grußformel "Mit freundlichen Grüßen"; (7) in der ` +
`letzten Zeile der vollständige Name des Bewerbers. Trenne Anrede, Absätze, Grußformel und Name jeweils durch ` +
`eine Leerzeile (\\n\\n). Höflich, sachlich, in Aktiv-Sätzen, ohne Floskeln und Weichspüler, keine Platzhalter.\n` +
`- profil: 23 Sätze Kurzprofil, konkret auf die Zielstelle zugeschnitten (der individuelle Text zur Stelle).\n` +
`- berufserfahrung: ALLE Stationen aus den Unterlagen, neueste zuerst — lückenloser Verlauf, ` +
`keine Station weglassen, die sonst eine Lücke hinterlässt (Überschneidungen sind ok). ` +
@@ -486,6 +509,14 @@ function normalizeResult(parsed) {
gruss: str(pick(a, ['gruss', 'gruß', 'grussformel', 'closing', 'schluss'])),
};
})(),
email: (() => {
const em = parsed.email || parsed.email_anschreiben || parsed.mail || parsed.begleitmail || {};
if (typeof em === 'string') return { betreff: '', text: str(em) };
return {
betreff: str(pick(em, ['betreff', 'subject', 'titel'])),
text: str(pick(em, ['text', 'body', 'inhalt', 'nachricht', 'mailtext', 'anschreiben'])),
};
})(),
lebenslauf: {
profil: str(pick(l, ['profil', 'kurzprofil', 'zusammenfassung', 'summary', 'ueberblick', 'über_mich'])),
berufserfahrung: pickArray(l, ['berufserfahrung', 'erfahrung', 'work_experience', 'experience', 'stationen'])
@@ -979,7 +1010,7 @@ async function generateApplicationDocuments({ job, basisDokumente, settings, zus
throw new Error('Die KI hat keine verwertbaren Unterlagen erzeugt.');
}
return documents;
return { documents, email: data.email || { betreff: '', text: '' } };
}
module.exports = {