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:
+49
-9
@@ -1239,19 +1239,56 @@ const REPLY_SCHEMA = {
|
|||||||
// Draft a reply to a recruiter/company e-mail in the context of an application.
|
// Draft a reply to a recruiter/company e-mail in the context of an application.
|
||||||
// `incoming` = { from, subject, text }; `job` = { firma, stelle };
|
// `incoming` = { from, subject, text }; `job` = { firma, stelle };
|
||||||
// `settings` = { name, ... }. `hinweise` = optional free-text steering.
|
// `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 name = (settings && settings.name) || 'der Bewerber';
|
||||||
const system =
|
const isAbsage = typ === 'absage';
|
||||||
'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' +
|
|
||||||
'deutschsprachige Antwort-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' +
|
const commonRules =
|
||||||
'Du antwortest konkret auf die eingegangene Nachricht. Wichtig: Du erfindest KEINE ' +
|
' Wichtig: Du erfindest KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen). ' +
|
||||||
'Fakten (keine erfundenen Termine, Zahlen, Zusagen). Wenn eine konkrete Angabe nötig ist, ' +
|
'Wenn eine konkrete Angabe nötig ist, die du nicht kennst, setze einen klar erkennbaren ' +
|
||||||
'die du nicht kennst (z. B. ein genauer Terminvorschlag), setze einen klar erkennbaren ' +
|
|
||||||
'Platzhalter in eckigen Klammern, z. B. "[Terminvorschlag einfügen]". Schreibe natürlich, ' +
|
'Platzhalter in eckigen Klammern, z. B. "[Terminvorschlag einfügen]". Schreibe natürlich, ' +
|
||||||
'knapp und verbindlich, wie ein deutscher Muttersprachler - ohne leere Floskeln, ohne ' +
|
'knapp und verbindlich, wie ein deutscher Muttersprachler - ohne leere Floskeln, ohne ' +
|
||||||
'Buzzword-Paare und ohne Selbstetiketten wie "Als Muttersprachler ...". Verwende ausschließlich ' +
|
'Buzzword-Paare und ohne Selbstetiketten wie "Als Muttersprachler ...". Verwende ausschließlich ' +
|
||||||
'deutsche Sprache und das lateinische Alphabet - keine fremdsprachigen Wörter oder Schriftzeichen.';
|
'deutsche Sprache und das lateinische Alphabet - keine fremdsprachigen Wörter oder Schriftzeichen.';
|
||||||
const user =
|
|
||||||
|
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` +
|
`# Kontext der Bewerbung\n` +
|
||||||
`Unternehmen: ${job.firma || '-'}\n` +
|
`Unternehmen: ${job.firma || '-'}\n` +
|
||||||
`Stelle: ${job.stelle || '-'}\n` +
|
`Stelle: ${job.stelle || '-'}\n` +
|
||||||
@@ -1269,9 +1306,12 @@ async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise
|
|||||||
`"betreff": sinnvolle Betreffzeile, i. d. R. "Re: ${incoming.subject || ''}". ` +
|
`"betreff": sinnvolle Betreffzeile, i. d. R. "Re: ${incoming.subject || ''}". ` +
|
||||||
`Verwende ausschließlich den einfachen Bindestrich "-" (niemals – oder —). ` +
|
`Verwende ausschließlich den einfachen Bindestrich "-" (niemals – oder —). ` +
|
||||||
`Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`;
|
`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 });
|
const parsed = await ollamaChatJSON({ system, user, schema: REPLY_SCHEMA, temperature: 0.5 });
|
||||||
return {
|
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'])),
|
text: str(pick(parsed, ['text', 'body', 'inhalt', 'nachricht'])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1579,6 +1579,7 @@ initializeDatabase().then(() => {
|
|||||||
job: { firma: bewerbung.firma, stelle: bewerbung.stelle },
|
job: { firma: bewerbung.firma, stelle: bewerbung.stelle },
|
||||||
settings,
|
settings,
|
||||||
hinweise: String(req.body.hinweise || ''),
|
hinweise: String(req.body.hinweise || ''),
|
||||||
|
typ: String(req.body.typ || 'antwort'),
|
||||||
});
|
});
|
||||||
// Include the quoted original so the reply reads like a mail-client thread.
|
// Include the quoted original so the reply reads like a mail-client thread.
|
||||||
res.json({ ...draft, quote: buildReplyQuote(orig) });
|
res.json({ ...draft, quote: buildReplyQuote(orig) });
|
||||||
|
|||||||
+16
-3
@@ -474,12 +474,18 @@
|
|||||||
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">
|
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">
|
<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>
|
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400">Nachricht</label>
|
||||||
|
<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"
|
<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 %>">
|
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>
|
<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>
|
<span class="ai-reply-label">KI-Antwort generieren</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<textarea name="body" rows="12" required
|
<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>
|
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>
|
||||||
<% if (basisAnhaenge && basisAnhaenge.length) { %>
|
<% if (basisAnhaenge && basisAnhaenge.length) { %>
|
||||||
@@ -849,10 +855,17 @@
|
|||||||
|
|
||||||
// Generate an AI draft reply and drop it into the reply form.
|
// Generate an AI draft reply and drop it into the reply form.
|
||||||
document.querySelectorAll('.ai-reply-btn').forEach((btn) => {
|
document.querySelectorAll('.ai-reply-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
const form = document.getElementById(btn.dataset.target);
|
const form = document.getElementById(btn.dataset.target);
|
||||||
if (!form) return;
|
const typSelect = form ? form.querySelector('.ai-reply-typ') : null;
|
||||||
const label = btn.querySelector('.ai-reply-label');
|
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 () => {
|
||||||
|
if (!form) return;
|
||||||
|
const typ = typSelect ? typSelect.value : 'antwort';
|
||||||
const prev = label ? label.textContent : '';
|
const prev = label ? label.textContent : '';
|
||||||
if (label) label.textContent = 'Generiere …';
|
if (label) label.textContent = 'Generiere …';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -860,7 +873,7 @@
|
|||||||
const res = await fetch('/bewerbung/' + btn.dataset.appId + '/email/ai-reply', {
|
const res = await fetch('/bewerbung/' + btn.dataset.appId + '/email/ai-reply', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || 'Fehler');
|
if (!res.ok) throw new Error(data.error || 'Fehler');
|
||||||
|
|||||||
Reference in New Issue
Block a user