E-Mail: KI-Absage generieren (andere Stelle angenommen)
Im Antwort-Formular gibt es neben dem KI-Button jetzt ein Dropdown Antwort/Absage. Für Absage formuliert die KI eine kurze, höfliche E-Mail: Bewerber hat eine andere Stelle angenommen, bedankt sich, zieht sich aus dem Prozess zurück. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+66
-26
@@ -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'])),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) });
|
||||
|
||||
+21
-8
@@ -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">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400">Nachricht</label>
|
||||
<button type="button" class="ai-reply-btn inline-flex items-center gap-1 text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400 hover:underline"
|
||||
data-email-id="<%= e.id %>" data-app-id="<%= application.id %>" data-target="reply-<%= e.id %>">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
<span class="ai-reply-label">KI-Antwort generieren</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<select class="ai-reply-typ text-xs px-1.5 py-0.5 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200">
|
||||
<option value="antwort">Antwort</option>
|
||||
<option value="absage">Absage (andere Stelle)</option>
|
||||
</select>
|
||||
<button type="button" class="ai-reply-btn inline-flex items-center gap-1 text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400 hover:underline"
|
||||
data-email-id="<%= e.id %>" data-app-id="<%= application.id %>" data-target="reply-<%= e.id %>">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
<span class="ai-reply-label">KI-Antwort generieren</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea name="body" rows="12" required
|
||||
class="reply-body w-full px-3 py-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 leading-relaxed"><%= e.reply_quote ? '\n\n' + e.reply_quote : '' %></textarea>
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user