Design der Unterlagen unter Vorlagen einstellbar
Akzentfarbe, Sidebar-Hintergrund, Foto (an/aus, eckig/rund) und Schriftgroesse lagen als Konstanten im Renderer. Sie kommen jetzt aus lib/design.js und sind unter Vorlagen waehlbar, inklusive Vorschau-PDF mit Musterinhalten - die zeigt das Ergebnis, ohne dass die KI laufen muss. Bewusst geschlossen gehalten: die Palette ist eine kurze Liste gedeckter Toene (kein freier Color-Picker), Grauwerte und Grundlayout bleiben fest. Die Schriftgroesse ist nur der Startwert der Seitenskalierung - der Lebenslauf passt weiterhin garantiert auf eine Seite. Das Theme wird durch die Render-Funktionen gereicht statt als Modulzustand gesetzt, damit spaeter mehrere Bewerber parallel generieren koennen, ohne sich gegenseitig das Design umzustellen. Rundes Foto: das Bild fuellt den Kreis formatfuellend und zentriert (Clip statt Skalieren), sonst wuerde ein Hochformat-Foto im Quadrat gestaucht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,9 +27,10 @@ const multer = require('multer');
|
||||
}
|
||||
})();
|
||||
|
||||
const { generateApplicationDocuments, generateEmailReply } = require('./lib/documents');
|
||||
const { generateApplicationDocuments, generateEmailReply, renderDesignVorschau } = require('./lib/documents');
|
||||
const chat = require('./lib/chat');
|
||||
const promptStore = require('./lib/prompts');
|
||||
const designStore = require('./lib/design');
|
||||
const mailer = require('./lib/mailer');
|
||||
const { createExternalApi } = require('./lib/api');
|
||||
const { buildOpenApiSpec } = require('./lib/openapi');
|
||||
@@ -439,6 +440,18 @@ async function loadPrompts() {
|
||||
}
|
||||
}
|
||||
|
||||
// The user's design overrides as { key: value }. Unset keys keep the defaults
|
||||
// from lib/design.js. Read fresh per generation, like the prompts.
|
||||
async function loadDesign() {
|
||||
try {
|
||||
const rows = await dbAll('SELECT key, value FROM design');
|
||||
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
||||
} catch (e) {
|
||||
console.error('Konnte Design nicht laden, nutze Standardwerte:', e.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Match an incoming message to an application: first via In-Reply-To/References
|
||||
// pointing at one of our sent messages, then by sender = a previous recipient.
|
||||
async function matchBewerbung(msg) {
|
||||
@@ -598,6 +611,7 @@ async function runGeneration(bewerbungId, options = {}) {
|
||||
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||
const prompts = await loadPrompts();
|
||||
const design = await loadDesign();
|
||||
|
||||
// Only the explicitly selected extra attachments are enclosed (default: none).
|
||||
const anlagenIds = Array.isArray(options.anlagenIds) ? options.anlagenIds.map(Number) : [];
|
||||
@@ -614,6 +628,7 @@ async function runGeneration(bewerbungId, options = {}) {
|
||||
basisDokumente,
|
||||
settings,
|
||||
prompts,
|
||||
design,
|
||||
// Names of the selected attachments so the cover letter (and the LLM) lists
|
||||
// exactly these under "Anlagen".
|
||||
zusatzAnlagen: selectedAnhaenge.map((a) => a.name || a.dateiname),
|
||||
@@ -967,6 +982,18 @@ function initializeDatabase() {
|
||||
)
|
||||
`, () => {});
|
||||
|
||||
// Design choices for the generated PDFs (accent colour, sidebar tint,
|
||||
// photo shape, font size). Same contract as `prompts`: only deviations
|
||||
// are stored, and lib/design.js validates every value on read, so a
|
||||
// stale row can never produce a broken document.
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS design (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, () => {});
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
@@ -1908,10 +1935,14 @@ initializeDatabase().then(() => {
|
||||
try {
|
||||
const basisDokumente = await dbAll('SELECT * FROM basis_dokumente ORDER BY id ASC');
|
||||
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
|
||||
const design = await loadDesign();
|
||||
res.render('vorlagen', {
|
||||
basisDokumente,
|
||||
basisAnhaenge,
|
||||
prompts: promptStore.list(await loadPrompts()),
|
||||
designFelder: designStore.list(design),
|
||||
designAngepasst: designStore.isAngepasst(design),
|
||||
designFotoAn: designStore.settings(design).foto_anzeigen === '1',
|
||||
hasSignatur: Boolean(currentSignaturFile()),
|
||||
hasFoto: Boolean(currentFotoFile()),
|
||||
basisTypOptions: BASIS_TYP_OPTIONS,
|
||||
@@ -1962,6 +1993,69 @@ initializeDatabase().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Design of the generated PDFs -----
|
||||
|
||||
// Save the design choices. Values are validated by lib/design.js on read, and
|
||||
// a choice equal to the default is stored as a deletion, so the DB only ever
|
||||
// holds real deviations.
|
||||
app.post('/vorlagen/design', async (req, res) => {
|
||||
try {
|
||||
const gewaehlt = designStore.settings(req.body);
|
||||
for (const [key, value] of Object.entries(gewaehlt)) {
|
||||
if (value === designStore.DEFAULTS[key]) {
|
||||
await dbRun('DELETE FROM design WHERE key = ?', [key]);
|
||||
} else {
|
||||
await dbRun(
|
||||
`INSERT INTO design (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`,
|
||||
[key, value]
|
||||
);
|
||||
}
|
||||
}
|
||||
res.redirect('/vorlagen#design');
|
||||
} catch (error) {
|
||||
console.error('Error saving design:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Back to the shipped design.
|
||||
app.post('/vorlagen/design/reset', async (req, res) => {
|
||||
try {
|
||||
await dbRun('DELETE FROM design');
|
||||
res.redirect('/vorlagen#design');
|
||||
} catch (error) {
|
||||
console.error('Error resetting design:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Preview PDF (Anschreiben or Lebenslauf) with sample content — lets the user
|
||||
// see a design choice without spending an LLM run. Query params override the
|
||||
// saved design, so the form can preview a selection before it is saved.
|
||||
app.get('/vorlagen/design/vorschau/:doc.pdf', async (req, res) => {
|
||||
try {
|
||||
const welches = req.params.doc === 'lebenslauf' ? 'lebenslauf' : 'anschreiben';
|
||||
const gespeichert = await loadDesign();
|
||||
// Only known design keys from the query are honoured; design.settings()
|
||||
// drops anything invalid.
|
||||
const design = { ...gespeichert, ...req.query };
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||
const pdfs = renderDesignVorschau({
|
||||
settings,
|
||||
signatur: loadSignatur(),
|
||||
bewerbungsfoto: loadFoto(),
|
||||
design,
|
||||
});
|
||||
res.type('application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="Vorschau_${welches}.pdf"`);
|
||||
res.send(pdfs[welches]);
|
||||
} catch (error) {
|
||||
console.error('Error rendering design preview:', error);
|
||||
res.status(500).send('Vorschau konnte nicht erzeugt werden');
|
||||
}
|
||||
});
|
||||
|
||||
// Add a base document
|
||||
app.post('/vorlagen', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user