diff --git a/lib/documents.js b/lib/documents.js index 4fac96d..d69d357 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -1239,39 +1239,79 @@ const REPLY_SCHEMA = { // Draft a reply to a recruiter/company e-mail in the context of an application. // `incoming` = { from, subject, text }; `job` = { firma, stelle }; // `settings` = { name, ... }. `hinweise` = optional free-text steering. -async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise = '' }) { +// `typ` = 'antwort' (default) for a normal reply, or 'absage' for a polite, short +// rejection because the applicant accepted another position. +async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise = '', typ = 'antwort' }) { const name = (settings && settings.name) || 'der Bewerber'; - const system = - 'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' + - 'deutschsprachige Antwort-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' + - 'Du antwortest konkret auf die eingegangene Nachricht. Wichtig: Du erfindest KEINE ' + - 'Fakten (keine erfundenen Termine, Zahlen, Zusagen). Wenn eine konkrete Angabe nötig ist, ' + - 'die du nicht kennst (z. B. ein genauer Terminvorschlag), setze einen klar erkennbaren ' + + const isAbsage = typ === 'absage'; + + const commonRules = + ' Wichtig: Du erfindest KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen). ' + + 'Wenn eine konkrete Angabe nötig ist, die du nicht kennst, setze einen klar erkennbaren ' + 'Platzhalter in eckigen Klammern, z. B. "[Terminvorschlag einfügen]". Schreibe natürlich, ' + 'knapp und verbindlich, wie ein deutscher Muttersprachler - ohne leere Floskeln, ohne ' + 'Buzzword-Paare und ohne Selbstetiketten wie "Als Muttersprachler ...". Verwende ausschließlich ' + 'deutsche Sprache und das lateinische Alphabet - keine fremdsprachigen Wörter oder Schriftzeichen.'; - const user = - `# Kontext der Bewerbung\n` + - `Unternehmen: ${job.firma || '-'}\n` + - `Stelle: ${job.stelle || '-'}\n` + - `Bewerber: ${name}\n\n` + - `# Eingegangene E-Mail\n` + - `Von: ${incoming.from || '-'}\n` + - `Betreff: ${incoming.subject || '-'}\n\n` + - `${incoming.text || ''}\n\n` + - (hinweise && hinweise.trim() ? `# Hinweise für die Antwort (aktiv berücksichtigen)\n${hinweise.trim()}\n\n` : '') + - `# Aufgabe\n` + - `Formuliere eine passende Antwort-E-Mail. Struktur des Feldes "text": Anrede (an den ` + - `konkreten Absender, falls Name erkennbar, sonst "Sehr geehrte Damen und Herren,"), ` + - `2-4 kurze Absätze, Grußformel "Mit freundlichen Grüßen" und in der letzten Zeile der ` + - `Name "${name}". Trenne Anrede, Absätze, Gruß und Name durch je eine Leerzeile (\\n\\n). ` + - `"betreff": sinnvolle Betreffzeile, i. d. R. "Re: ${incoming.subject || ''}". ` + - `Verwende ausschließlich den einfachen Bindestrich "-" (niemals – oder —). ` + - `Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`; + + let system, user, betreffDefault; + if (isAbsage) { + system = + 'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' + + 'deutschsprachige Absage-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' + + 'Du teilst mit, dass du dich entschieden hast, eine andere Stelle anzunehmen, und ' + + 'bedankst dich für die Zeit, das Gespräch und das Vertrauen. Halte die E-Mail bewusst ' + + 'kurz: ein bis zwei kurze Absätze genügen - keine langen Erklärungen, keine Rechtfertigung. ' + + 'Bleibe wertschätzend und bestimmt zugleich; nenne keine Gründe im Detail und erfinde keine ' + + 'konkreten Namen anderer Unternehmen.' + commonRules; + user = + `# Kontext der Bewerbung\n` + + `Unternehmen: ${job.firma || '-'}\n` + + `Stelle: ${job.stelle || '-'}\n` + + `Bewerber: ${name}\n\n` + + `# Eingegangene E-Mail (Bezug)\n` + + `Von: ${incoming.from || '-'}\n` + + `Betreff: ${incoming.subject || '-'}\n\n` + + `${incoming.text || ''}\n\n` + + (hinweise && hinweise.trim() ? `# Hinweise für die Absage (aktiv berücksichtigen)\n${hinweise.trim()}\n\n` : '') + + `# Aufgabe\n` + + `Formuliere eine kurze, höfliche Absage-E-Mail: Du hast eine andere Stelle angenommen ` + + `und ziehst dich aus diesem Prozess zurück. Struktur des Feldes "text": Anrede (an den ` + + `konkreten Absender, falls Name erkennbar, sonst "Sehr geehrte Damen und Herren,"), ein ` + + `bis zwei kurze Absätze, Grußformel "Mit freundlichen Grüßen" und in der letzten Zeile der ` + + `Name "${name}". Trenne Anrede, Absätze, Gruß und Name durch je eine Leerzeile (\\n\\n). ` + + `"betreff": knappe Betreffzeile, i. d. R. "Re: ${incoming.subject || ''}". ` + + `Verwende ausschließlich den einfachen Bindestrich "-" (niemals – oder —). ` + + `Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`; + betreffDefault = 'Re: ' + (incoming.subject || ('Absage zur Bewerbung als ' + (job.stelle || ''))); + } else { + system = + 'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' + + 'deutschsprachige Antwort-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' + + 'Du antwortest konkret auf die eingegangene Nachricht.' + commonRules; + user = + `# Kontext der Bewerbung\n` + + `Unternehmen: ${job.firma || '-'}\n` + + `Stelle: ${job.stelle || '-'}\n` + + `Bewerber: ${name}\n\n` + + `# Eingegangene E-Mail\n` + + `Von: ${incoming.from || '-'}\n` + + `Betreff: ${incoming.subject || '-'}\n\n` + + `${incoming.text || ''}\n\n` + + (hinweise && hinweise.trim() ? `# Hinweise für die Antwort (aktiv berücksichtigen)\n${hinweise.trim()}\n\n` : '') + + `# Aufgabe\n` + + `Formuliere eine passende Antwort-E-Mail. Struktur des Feldes "text": Anrede (an den ` + + `konkreten Absender, falls Name erkennbar, sonst "Sehr geehrte Damen und Herren,"), ` + + `2-4 kurze Absätze, Grußformel "Mit freundlichen Grüßen" und in der letzten Zeile der ` + + `Name "${name}". Trenne Anrede, Absätze, Gruß und Name durch je eine Leerzeile (\\n\\n). ` + + `"betreff": sinnvolle Betreffzeile, i. d. R. "Re: ${incoming.subject || ''}". ` + + `Verwende ausschließlich den einfachen Bindestrich "-" (niemals – oder —). ` + + `Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`; + betreffDefault = 'Re: ' + (incoming.subject || ''); + } + const parsed = await ollamaChatJSON({ system, user, schema: REPLY_SCHEMA, temperature: 0.5 }); return { - betreff: str(pick(parsed, ['betreff', 'subject', 'titel'])) || ('Re: ' + (incoming.subject || '')), + betreff: str(pick(parsed, ['betreff', 'subject', 'titel'])) || betreffDefault, text: str(pick(parsed, ['text', 'body', 'inhalt', 'nachricht'])), }; } diff --git a/server.js b/server.js index 83584bf..1be2f40 100644 --- a/server.js +++ b/server.js @@ -1579,6 +1579,7 @@ initializeDatabase().then(() => { job: { firma: bewerbung.firma, stelle: bewerbung.stelle }, settings, hinweise: String(req.body.hinweise || ''), + typ: String(req.body.typ || 'antwort'), }); // Include the quoted original so the reply reads like a mail-client thread. res.json({ ...draft, quote: buildReplyQuote(orig) }); diff --git a/views/bewerbung.ejs b/views/bewerbung.ejs index 7bffa1c..d11ac11 100644 --- a/views/bewerbung.ejs +++ b/views/bewerbung.ejs @@ -474,11 +474,17 @@ class="reply-subject w-full px-3 py-2 mb-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm">
- +
+ + +
@@ -849,10 +855,17 @@ // Generate an AI draft reply and drop it into the reply form. document.querySelectorAll('.ai-reply-btn').forEach((btn) => { + const form = document.getElementById(btn.dataset.target); + const typSelect = form ? form.querySelector('.ai-reply-typ') : null; + const label = btn.querySelector('.ai-reply-label'); + const syncLabel = () => { + if (!label || !typSelect) return; + label.textContent = typSelect.value === 'absage' ? 'KI-Absage generieren' : 'KI-Antwort generieren'; + }; + if (typSelect) { typSelect.addEventListener('change', syncLabel); syncLabel(); } btn.addEventListener('click', async () => { - const form = document.getElementById(btn.dataset.target); if (!form) return; - const label = btn.querySelector('.ai-reply-label'); + const typ = typSelect ? typSelect.value : 'antwort'; const prev = label ? label.textContent : ''; if (label) label.textContent = 'Generiere …'; btn.disabled = true; @@ -860,7 +873,7 @@ const res = await fetch('/bewerbung/' + btn.dataset.appId + '/email/ai-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email_id: btn.dataset.emailId }), + body: JSON.stringify({ email_id: btn.dataset.emailId, typ }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Fehler');