nca-ai-cms-astro-plugin 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -22,24 +22,50 @@ export const GET: APIRoute = async () => {
22
22
  }
23
23
  };
24
24
 
25
- // POST /api/prompts - Update a prompt or setting
25
+ // POST /api/prompts - Create or update a prompt or setting
26
26
  export const POST: APIRoute = async ({ request }) => {
27
27
  try {
28
28
  const data = await request.json();
29
29
 
30
+ // Create a new prompt
31
+ if (data.action === 'create' && data.name && data.category && data.promptText) {
32
+ const id = `${data.category}_${Date.now()}`;
33
+ await service.createPrompt(id, data.name, data.category, data.promptText);
34
+ return jsonResponse({ success: true, type: 'prompt', id });
35
+ }
36
+
37
+ // Update an existing prompt
30
38
  if (data.type === 'prompt' && data.id && data.promptText !== undefined) {
31
39
  await service.updatePrompt(data.id, data.promptText);
32
40
  return jsonResponse({ success: true, type: 'prompt', id: data.id });
33
41
  }
34
42
 
43
+ // Update a setting
35
44
  if (data.type === 'setting' && data.key && data.value !== undefined) {
36
45
  await service.updateSetting(data.key, data.value);
37
46
  return jsonResponse({ success: true, type: 'setting', key: data.key });
38
47
  }
39
48
 
40
- return jsonError('Invalid request: missing type, id/key, or value', 400);
49
+ return jsonError('Invalid request: missing required fields', 400);
41
50
  } catch (error) {
42
51
  console.error('Update prompt error:', error);
43
52
  return jsonError(error);
44
53
  }
45
54
  };
55
+
56
+ // DELETE /api/prompts - Delete a prompt
57
+ export const DELETE: APIRoute = async ({ request }) => {
58
+ try {
59
+ const data = await request.json();
60
+
61
+ if (!data.id) {
62
+ return jsonError('Missing prompt id', 400);
63
+ }
64
+
65
+ await service.deletePrompt(data.id);
66
+ return jsonResponse({ success: true, id: data.id });
67
+ } catch (error) {
68
+ console.error('Delete prompt error:', error);
69
+ return jsonError(error);
70
+ }
71
+ };
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
- import type { Prompt, SettingsSubTab } from './types';
2
+ import type { Prompt, Setting, SettingsSubTab } from './types';
3
3
  import { styles } from './styles';
4
4
 
5
5
  const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
@@ -10,21 +10,83 @@ const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
10
10
  { key: 'website', label: 'Website' },
11
11
  ];
12
12
 
13
+ const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
14
+
15
+ const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
16
+ homepage: [
17
+ { key: 'hero_headline', label: 'Hero Ueberschrift', type: 'input' },
18
+ { key: 'hero_text', label: 'Hero Text', type: 'textarea' },
19
+ { key: 'target_audience', label: 'Zielgruppe', type: 'input' },
20
+ { key: 'tone', label: 'Tonalitaet', type: 'input' },
21
+ { key: 'core_message', label: 'Kernbotschaft', type: 'textarea' },
22
+ ],
23
+ website: [
24
+ { key: 'cta_url', label: 'CTA Link', type: 'input' },
25
+ { key: 'cta_style', label: 'CTA Stil', type: 'input' },
26
+ { key: 'cta_prompt', label: 'CTA Prompt', type: 'textarea' },
27
+ { key: 'core_tags', label: 'Core Tags (kommagetrennt)', type: 'input' },
28
+ { key: 'brand_guidelines', label: 'Markenrichtlinien', type: 'textarea' },
29
+ ],
30
+ };
31
+
32
+ const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: string; example: string }> = {
33
+ 'homepage': {
34
+ title: 'Homepage-Einstellungen',
35
+ description: 'Konfiguriere hier die zentralen Inhalte deiner Startseite: Hero-Text, Zielgruppe, Tonalitaet und Kernbotschaft.',
36
+ example: '',
37
+ },
38
+ 'content-ai': {
39
+ title: 'Content-KI Prompts',
40
+ description: 'Steuere hier, wie die KI Blogartikel und Texte generiert. Je praeziser der Prompt, desto besser das Ergebnis. Definiere Schreibstil, SEO-Keywords und inhaltliche Schwerpunkte.',
41
+ example: 'Beispiel: "Schreibe einen Fachartikel mit 800-1200 Woertern. Verwende die Keywords [KEYWORD] natuerlich im Text. Struktur: Einleitung mit Hook, 3-4 Abschnitte mit H2, Fazit mit CTA. Ton: fachlich aber verstaendlich."',
42
+ },
43
+ 'analysis-ai': {
44
+ title: 'Analyse-KI Prompts',
45
+ description: 'Konfiguriere Prompts fuer die inhaltliche Analyse bestehender Texte. Die KI kann Texte auf SEO, Lesbarkeit, Barrierefreiheit und Content-Qualitaet pruefen.',
46
+ example: 'Beispiel: "Analysiere den Text auf SEO-Optimierung. Pruefe: Keyword-Dichte, Meta-Beschreibung, Ueberschriften-Hierarchie, interne Verlinkung. Gib konkrete Verbesserungsvorschlaege."',
47
+ },
48
+ 'image-ai': {
49
+ title: 'Bild-KI Prompts',
50
+ description: 'Definiere Prompts fuer die KI-Bildgenerierung. Beschreibe Stil, Farbpalette und Bildkomposition fuer konsistente visuelle Inhalte.',
51
+ example: 'Beispiel: "Erstelle ein Blog-Header-Bild im minimalistischen Flat-Design. Farbpalette: Dunkelblau (#1a365d), Weiss, Akzent-Rot (#e63946). Kein Text im Bild. Seitenverhaeltnis 16:9."',
52
+ },
53
+ 'website': {
54
+ title: 'Website-Einstellungen',
55
+ description: 'Allgemeine Einstellungen fuer deine Website: CTA-Texte, Standard-Tags und Markenrichtlinien.',
56
+ example: '',
57
+ },
58
+ };
59
+
60
+ function isSettingsTab(tab: SettingsSubTab): boolean {
61
+ return SETTINGS_TABS.includes(tab);
62
+ }
63
+
13
64
  interface EditState {
14
65
  id: string;
15
66
  value: string;
16
67
  }
17
68
 
69
+ interface AddFormState {
70
+ name: string;
71
+ promptText: string;
72
+ }
73
+
18
74
  export function SettingsTab() {
19
75
  const [activeSubTab, setActiveSubTab] =
20
76
  useState<SettingsSubTab>('homepage');
21
77
  const [prompts, setPrompts] = useState<Prompt[]>([]);
78
+ const [_settings, setSettings] = useState<Setting[]>([]);
79
+ const [settingsForm, setSettingsForm] = useState<Record<string, string>>({});
80
+ const [settingsSaved, setSettingsSaved] = useState(false);
22
81
  const [loading, setLoading] = useState(true);
23
82
  const [error, setError] = useState<string | null>(null);
24
83
  const [editing, setEditing] = useState<EditState | null>(null);
25
84
  const [saving, setSaving] = useState(false);
85
+ const [showAddForm, setShowAddForm] = useState(false);
86
+ const [addForm, setAddForm] = useState<AddFormState>({ name: '', promptText: '' });
87
+ const [deleting, setDeleting] = useState<string | null>(null);
26
88
 
27
- const loadPrompts = useCallback(async () => {
89
+ const loadData = useCallback(async () => {
28
90
  setLoading(true);
29
91
  setError(null);
30
92
  try {
@@ -32,6 +94,13 @@ export function SettingsTab() {
32
94
  if (!res.ok) throw new Error('Fehler beim Laden der Einstellungen');
33
95
  const data = await res.json();
34
96
  setPrompts(Array.isArray(data.prompts) ? data.prompts : []);
97
+ const loadedSettings: Setting[] = Array.isArray(data.settings) ? data.settings : [];
98
+ setSettings(loadedSettings);
99
+ const formValues: Record<string, string> = {};
100
+ for (const s of loadedSettings) {
101
+ formValues[s.key] = s.value;
102
+ }
103
+ setSettingsForm(formValues);
35
104
  } catch (err) {
36
105
  setError(
37
106
  err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten',
@@ -42,8 +111,8 @@ export function SettingsTab() {
42
111
  }, []);
43
112
 
44
113
  useEffect(() => {
45
- loadPrompts();
46
- }, [loadPrompts]);
114
+ loadData();
115
+ }, [loadData]);
47
116
 
48
117
  const filteredPrompts = prompts.filter(
49
118
  (p) => p.category === activeSubTab,
@@ -86,6 +155,83 @@ export function SettingsTab() {
86
155
  }
87
156
  };
88
157
 
158
+ const handleSettingsSave = async () => {
159
+ setSaving(true);
160
+ setError(null);
161
+ setSettingsSaved(false);
162
+ try {
163
+ const fields = SETTINGS_FIELDS[activeSubTab] ?? [];
164
+ for (const field of fields) {
165
+ const value = settingsForm[field.key] ?? '';
166
+ const res = await fetch('/api/prompts', {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ type: 'setting', key: field.key, value }),
170
+ });
171
+ if (!res.ok) throw new Error(`Fehler beim Speichern von "${field.label}"`);
172
+ }
173
+ setSettingsSaved(true);
174
+ setTimeout(() => setSettingsSaved(false), 3000);
175
+ } catch (err) {
176
+ setError(
177
+ err instanceof Error ? err.message : 'Fehler beim Speichern',
178
+ );
179
+ } finally {
180
+ setSaving(false);
181
+ }
182
+ };
183
+
184
+ const handleAdd = async () => {
185
+ if (!addForm.name.trim() || !addForm.promptText.trim()) return;
186
+ setSaving(true);
187
+ setError(null);
188
+ try {
189
+ const res = await fetch('/api/prompts', {
190
+ method: 'POST',
191
+ headers: { 'Content-Type': 'application/json' },
192
+ body: JSON.stringify({
193
+ action: 'create',
194
+ name: addForm.name,
195
+ category: activeSubTab,
196
+ promptText: addForm.promptText,
197
+ }),
198
+ });
199
+ if (!res.ok) throw new Error('Fehler beim Erstellen');
200
+ setAddForm({ name: '', promptText: '' });
201
+ setShowAddForm(false);
202
+ await loadData();
203
+ } catch (err) {
204
+ setError(
205
+ err instanceof Error ? err.message : 'Fehler beim Erstellen',
206
+ );
207
+ } finally {
208
+ setSaving(false);
209
+ }
210
+ };
211
+
212
+ const handleDelete = async (id: string) => {
213
+ setDeleting(id);
214
+ setError(null);
215
+ try {
216
+ const res = await fetch('/api/prompts', {
217
+ method: 'DELETE',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({ id }),
220
+ });
221
+ if (!res.ok) throw new Error('Fehler beim Loeschen');
222
+ setPrompts((prev) => prev.filter((p) => p.id !== id));
223
+ if (editing?.id === id) setEditing(null);
224
+ } catch (err) {
225
+ setError(
226
+ err instanceof Error ? err.message : 'Fehler beim Loeschen',
227
+ );
228
+ } finally {
229
+ setDeleting(null);
230
+ }
231
+ };
232
+
233
+ const guide = CATEGORY_GUIDES[activeSubTab];
234
+
89
235
  return (
90
236
  <div style={styles.settingsContent}>
91
237
  <div style={styles.subTabNav} role="tablist">
@@ -103,6 +249,8 @@ export function SettingsTab() {
103
249
  onClick={() => {
104
250
  setActiveSubTab(tab.key);
105
251
  setEditing(null);
252
+ setShowAddForm(false);
253
+ setSettingsSaved(false);
106
254
  }}
107
255
  >
108
256
  {tab.label}
@@ -116,69 +264,189 @@ export function SettingsTab() {
116
264
 
117
265
  {error && <div style={styles.error}>{error}</div>}
118
266
 
119
- {!loading && filteredPrompts.length === 0 && (
120
- <div style={styles.emptyState}>
121
- Keine Einstellungen in dieser Kategorie.
267
+ {/* Settings tabs: Homepage, Website key-value form */}
268
+ {!loading && isSettingsTab(activeSubTab) && (
269
+ <div style={styles.addForm}>
270
+ <div style={styles.addFormTitle}>{guide.title}</div>
271
+ <div style={{ ...styles.emptyGuideText, marginBottom: '1rem' }}>{guide.description}</div>
272
+ {(SETTINGS_FIELDS[activeSubTab] ?? []).map((field) => (
273
+ <div key={field.key} style={styles.addFormField}>
274
+ <label style={styles.addFormLabel}>{field.label}</label>
275
+ {field.type === 'textarea' ? (
276
+ <textarea
277
+ style={styles.textarea}
278
+ value={settingsForm[field.key] ?? ''}
279
+ onChange={(e) =>
280
+ setSettingsForm({ ...settingsForm, [field.key]: e.target.value })
281
+ }
282
+ />
283
+ ) : (
284
+ <input
285
+ style={styles.addFormInput}
286
+ type="text"
287
+ value={settingsForm[field.key] ?? ''}
288
+ onChange={(e) =>
289
+ setSettingsForm({ ...settingsForm, [field.key]: e.target.value })
290
+ }
291
+ />
292
+ )}
293
+ </div>
294
+ ))}
295
+ <div style={styles.editButtons}>
296
+ <button
297
+ type="button"
298
+ style={styles.addButton}
299
+ onClick={handleSettingsSave}
300
+ disabled={saving}
301
+ >
302
+ {saving ? 'Speichere...' : 'Einstellungen speichern'}
303
+ </button>
304
+ {settingsSaved && (
305
+ <span style={{ color: '#4ade80', fontSize: '0.85rem', alignSelf: 'center' }}>
306
+ Gespeichert
307
+ </span>
308
+ )}
309
+ </div>
122
310
  </div>
123
311
  )}
124
312
 
125
- {!loading && filteredPrompts.length > 0 && (
126
- <div style={styles.cardGrid}>
127
- {filteredPrompts.map((prompt) => (
128
- <div key={prompt.id} style={styles.settingsCard}>
129
- <div style={styles.settingsCardHeader}>
130
- <div style={styles.settingsItemLabel}>{prompt.name}</div>
131
- <div style={styles.settingsItemCategory}>
132
- {prompt.category}
133
- </div>
134
- </div>
313
+ {/* Prompt tabs: Content-KI, Analyse-KI, Bild-KI — prompt cards */}
314
+ {!loading && !isSettingsTab(activeSubTab) && (
315
+ <>
316
+ {filteredPrompts.length === 0 && !showAddForm && (
317
+ <div style={styles.emptyGuide}>
318
+ <div style={styles.emptyGuideTitle}>{guide.title}</div>
319
+ <div style={styles.emptyGuideText}>{guide.description}</div>
320
+ {guide.example && (
321
+ <div style={styles.emptyGuideExample}>{guide.example}</div>
322
+ )}
323
+ </div>
324
+ )}
135
325
 
136
- {editing?.id === prompt.id ? (
137
- <div style={styles.editArea}>
138
- <textarea
139
- style={styles.textarea}
140
- value={editing.value}
141
- onChange={(e) =>
142
- setEditing({ ...editing, value: e.target.value })
143
- }
144
- />
145
- <div style={styles.editButtons}>
146
- <button
147
- type="button"
148
- style={styles.saveButton}
149
- onClick={handleSave}
150
- disabled={saving}
151
- >
152
- {saving ? 'Speichere...' : 'Speichern'}
153
- </button>
154
- <button
155
- type="button"
156
- style={styles.cancelButton}
157
- onClick={handleCancel}
158
- >
159
- Abbrechen
160
- </button>
326
+ {filteredPrompts.length > 0 && (
327
+ <div style={styles.cardGrid}>
328
+ {filteredPrompts.map((prompt) => (
329
+ <div key={prompt.id} style={styles.settingsCard}>
330
+ <div style={styles.settingsCardHeader}>
331
+ <div style={styles.settingsItemLabel}>{prompt.name}</div>
332
+ <div style={styles.settingsItemCategory}>
333
+ {prompt.category}
334
+ </div>
161
335
  </div>
336
+
337
+ {editing?.id === prompt.id ? (
338
+ <div style={styles.editArea}>
339
+ <textarea
340
+ style={styles.textarea}
341
+ value={editing.value}
342
+ onChange={(e) =>
343
+ setEditing({ ...editing, value: e.target.value })
344
+ }
345
+ />
346
+ <div style={styles.editButtons}>
347
+ <button
348
+ type="button"
349
+ style={styles.saveButton}
350
+ onClick={handleSave}
351
+ disabled={saving}
352
+ >
353
+ {saving ? 'Speichere...' : 'Speichern'}
354
+ </button>
355
+ <button
356
+ type="button"
357
+ style={styles.cancelButton}
358
+ onClick={handleCancel}
359
+ >
360
+ Abbrechen
361
+ </button>
362
+ </div>
363
+ </div>
364
+ ) : (
365
+ <>
366
+ <div style={styles.settingsItemValue}>
367
+ {prompt.promptText}
368
+ </div>
369
+ <div style={{ padding: '0 1rem 1rem', display: 'flex', gap: '0.5rem' }}>
370
+ <button
371
+ type="button"
372
+ style={styles.editButton}
373
+ onClick={() => handleEdit(prompt)}
374
+ >
375
+ Bearbeiten
376
+ </button>
377
+ <button
378
+ type="button"
379
+ style={styles.deleteButton}
380
+ onClick={() => handleDelete(prompt.id)}
381
+ disabled={deleting === prompt.id}
382
+ >
383
+ {deleting === prompt.id ? 'Loesche...' : 'Loeschen'}
384
+ </button>
385
+ </div>
386
+ </>
387
+ )}
162
388
  </div>
163
- ) : (
164
- <>
165
- <div style={styles.settingsItemValue}>
166
- {prompt.promptText}
167
- </div>
168
- <div style={{ padding: '0 1rem 1rem' }}>
169
- <button
170
- type="button"
171
- style={styles.editButton}
172
- onClick={() => handleEdit(prompt)}
173
- >
174
- Bearbeiten
175
- </button>
176
- </div>
177
- </>
178
- )}
389
+ ))}
179
390
  </div>
180
- ))}
181
- </div>
391
+ )}
392
+
393
+ {showAddForm && (
394
+ <div style={styles.addForm}>
395
+ <div style={styles.addFormTitle}>Neuen Prompt hinzufuegen ({guide.title})</div>
396
+ <div style={styles.addFormField}>
397
+ <label style={styles.addFormLabel}>Name</label>
398
+ <input
399
+ style={styles.addFormInput}
400
+ type="text"
401
+ placeholder="z.B. Blog-Artikel Prompt, SEO-Analyse, Hero-Text"
402
+ value={addForm.name}
403
+ onChange={(e) => setAddForm({ ...addForm, name: e.target.value })}
404
+ />
405
+ </div>
406
+ <div style={styles.addFormField}>
407
+ <label style={styles.addFormLabel}>Prompt-Text</label>
408
+ <textarea
409
+ style={styles.textarea}
410
+ placeholder={guide.example}
411
+ value={addForm.promptText}
412
+ onChange={(e) => setAddForm({ ...addForm, promptText: e.target.value })}
413
+ />
414
+ </div>
415
+ <div style={styles.editButtons}>
416
+ <button
417
+ type="button"
418
+ style={styles.addButton}
419
+ onClick={handleAdd}
420
+ disabled={saving || !addForm.name.trim() || !addForm.promptText.trim()}
421
+ >
422
+ {saving ? 'Erstelle...' : 'Erstellen'}
423
+ </button>
424
+ <button
425
+ type="button"
426
+ style={styles.cancelButton}
427
+ onClick={() => {
428
+ setShowAddForm(false);
429
+ setAddForm({ name: '', promptText: '' });
430
+ }}
431
+ >
432
+ Abbrechen
433
+ </button>
434
+ </div>
435
+ </div>
436
+ )}
437
+
438
+ {!showAddForm && (
439
+ <div style={{ textAlign: 'center' as const, padding: '1rem 0' }}>
440
+ <button
441
+ type="button"
442
+ style={styles.addButton}
443
+ onClick={() => setShowAddForm(true)}
444
+ >
445
+ + Neuen Prompt hinzufuegen
446
+ </button>
447
+ </div>
448
+ )}
449
+ </>
182
450
  )}
183
451
  </div>
184
452
  );
@@ -594,4 +594,88 @@ export const styles: Record<string, React.CSSProperties> = {
594
594
  color: textMuted,
595
595
  fontSize: '0.95rem',
596
596
  },
597
+ emptyGuide: {
598
+ padding: '2rem',
599
+ background: surfaceElevated,
600
+ border: `1px solid ${border}`,
601
+ borderRadius: '12px',
602
+ marginBottom: '1.5rem',
603
+ },
604
+ emptyGuideTitle: {
605
+ fontSize: '1.1rem',
606
+ fontWeight: 600,
607
+ color: text,
608
+ marginBottom: '0.75rem',
609
+ fontFamily: fontDisplay,
610
+ },
611
+ emptyGuideText: {
612
+ fontSize: '0.9rem',
613
+ color: textMuted,
614
+ lineHeight: 1.6,
615
+ marginBottom: '0.5rem',
616
+ },
617
+ emptyGuideExample: {
618
+ background: bg,
619
+ border: `1px solid ${border}`,
620
+ borderRadius: '8px',
621
+ padding: '0.75rem 1rem',
622
+ marginTop: '0.75rem',
623
+ fontSize: '0.85rem',
624
+ fontFamily: fontMono,
625
+ color: textAccent,
626
+ lineHeight: 1.5,
627
+ },
628
+ addForm: {
629
+ padding: '1.5rem',
630
+ background: surface,
631
+ border: `1px solid ${border}`,
632
+ borderRadius: '12px',
633
+ },
634
+ addFormTitle: {
635
+ fontSize: '1rem',
636
+ fontWeight: 600,
637
+ color: text,
638
+ marginBottom: '1rem',
639
+ },
640
+ addFormField: {
641
+ marginBottom: '0.75rem',
642
+ },
643
+ addFormLabel: {
644
+ display: 'block',
645
+ fontSize: '0.8rem',
646
+ color: textMuted,
647
+ marginBottom: '0.25rem',
648
+ fontWeight: 500,
649
+ },
650
+ addFormInput: {
651
+ width: '100%',
652
+ background: bg,
653
+ border: `1px solid ${border}`,
654
+ borderRadius: '8px',
655
+ padding: '0.5rem 0.75rem',
656
+ color: text,
657
+ fontSize: '0.85rem',
658
+ outline: 'none',
659
+ boxSizing: 'border-box' as const,
660
+ },
661
+ deleteButton: {
662
+ background: 'transparent',
663
+ border: `1px solid ${error}`,
664
+ color: error,
665
+ padding: '0.4rem 0.8rem',
666
+ borderRadius: '6px',
667
+ fontSize: '0.8rem',
668
+ fontWeight: 500,
669
+ cursor: 'pointer',
670
+ },
671
+ addButton: {
672
+ background: primary,
673
+ border: 'none',
674
+ color: '#fff',
675
+ padding: '0.5rem 1rem',
676
+ borderRadius: '8px',
677
+ fontSize: '0.85rem',
678
+ fontWeight: 600,
679
+ cursor: 'pointer',
680
+ },
597
681
  };
@@ -16,6 +16,20 @@ export class PromptService {
16
16
  return result?.promptText ?? null;
17
17
  }
18
18
 
19
+ async createPrompt(id: string, name: string, category: string, promptText: string): Promise<void> {
20
+ await db.insert(Prompts).values({
21
+ id,
22
+ name,
23
+ category,
24
+ promptText,
25
+ updatedAt: new Date(),
26
+ });
27
+ }
28
+
29
+ async deletePrompt(id: string): Promise<void> {
30
+ await db.delete(Prompts).where(eq(Prompts.id, id));
31
+ }
32
+
19
33
  async updatePrompt(id: string, text: string): Promise<void> {
20
34
  await db
21
35
  .update(Prompts)
package/task.md CHANGED
@@ -13,7 +13,80 @@ Line 33-34: API `/api/prompts` returns `{ prompts, settings }` but the component
13
13
  + setPrompts(Array.isArray(data.prompts) ? data.prompts : []);
14
14
  ```
15
15
 
16
- ## 2. Auto-register dependencies in index.ts
16
+ ## 2. Separate Settings from Prompts in SettingsTab
17
+
18
+ **File:** `src/components/editor/SettingsTab.tsx`
19
+
20
+ The SettingsTab currently treats all 5 sub-tabs identically, showing the "Neuen Prompt hinzufuegen" (prompt CRUD) UI for every tab. But **Homepage** and **Website** are **site settings** (key-value pairs in `SiteSettings` table), not AI prompts. Only **Content-KI**, **Analyse-KI**, and **Bild-KI** should show the prompt CRUD interface.
21
+
22
+ ### Changes required:
23
+
24
+ 1. **Import `Setting` type** from `./types` (already defined there)
25
+
26
+ 2. **Add constants** at the top of the file:
27
+
28
+ ```ts
29
+ const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
30
+
31
+ const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
32
+ homepage: [
33
+ { key: 'hero_headline', label: 'Hero Ueberschrift', type: 'input' },
34
+ { key: 'hero_text', label: 'Hero Text', type: 'textarea' },
35
+ { key: 'target_audience', label: 'Zielgruppe', type: 'input' },
36
+ { key: 'tone', label: 'Tonalitaet', type: 'input' },
37
+ { key: 'core_message', label: 'Kernbotschaft', type: 'textarea' },
38
+ ],
39
+ website: [
40
+ { key: 'cta_url', label: 'CTA Link', type: 'input' },
41
+ { key: 'cta_style', label: 'CTA Stil', type: 'input' },
42
+ { key: 'cta_prompt', label: 'CTA Prompt', type: 'textarea' },
43
+ { key: 'core_tags', label: 'Core Tags (kommagetrennt)', type: 'input' },
44
+ { key: 'brand_guidelines', label: 'Markenrichtlinien', type: 'textarea' },
45
+ ],
46
+ };
47
+ ```
48
+
49
+ 3. **Add helper function:**
50
+
51
+ ```ts
52
+ function isSettingsTab(tab: SettingsSubTab): boolean {
53
+ return SETTINGS_TABS.includes(tab);
54
+ }
55
+ ```
56
+
57
+ 4. **Add new state** in the component:
58
+
59
+ ```ts
60
+ const [settings, setSettings] = useState<Setting[]>([]);
61
+ const [settingsForm, setSettingsForm] = useState<Record<string, string>>({});
62
+ const [settingsSaved, setSettingsSaved] = useState(false);
63
+ ```
64
+
65
+ 5. **Update `loadPrompts`** to also load settings:
66
+
67
+ ```ts
68
+ const loadedSettings: Setting[] = Array.isArray(data.settings) ? data.settings : [];
69
+ setSettings(loadedSettings);
70
+ const formValues: Record<string, string> = {};
71
+ for (const s of loadedSettings) {
72
+ formValues[s.key] = s.value;
73
+ }
74
+ setSettingsForm(formValues);
75
+ ```
76
+
77
+ 6. **Add `handleSettingsSave`** function that POSTs each field with `{ type: 'setting', key, value }` to `/api/prompts`
78
+
79
+ 7. **Update the render/return** to branch on `isSettingsTab(activeSubTab)`:
80
+ - **Settings tabs** (homepage, website): render a key-value form with text inputs/textareas for each field defined in `SETTINGS_FIELDS`, plus a "Einstellungen speichern" button
81
+ - **Prompt tabs** (content-ai, analysis-ai, image-ai): keep existing prompt card grid + "Neuen Prompt hinzufuegen" button (unchanged)
82
+
83
+ 8. **Update `CATEGORY_GUIDES`** for homepage and website — change descriptions to reflect settings (not prompts), remove example text
84
+
85
+ ### API usage for settings
86
+ - Read: from `data.settings` array returned by `GET /api/prompts`, find by `key`
87
+ - Write: `POST /api/prompts` with `{ type: 'setting', key, value }`
88
+
89
+ ## 3. Auto-register dependencies in index.ts
17
90
 
18
91
  **File:** `src/index.ts`
19
92
 
@@ -45,13 +118,13 @@ The consumer currently must manually add `react()`, `db()`, `output: 'server'`,
45
118
  }
46
119
  ```
47
120
 
48
- ## 3. Bump version to 1.0.5
121
+ ## 4. Bump version to 1.0.5
49
122
 
50
123
  **File:** `package.json`
51
124
 
52
125
  Change `"version": "1.0.4"` to `"version": "1.0.5"`.
53
126
 
54
- ## 4. Update README with simplified setup
127
+ ## 5. Update README with simplified setup
55
128
 
56
129
  **File:** `README.md`
57
130
 
@@ -74,4 +147,8 @@ Note that `prerender: false` is already present on all 18 `injectRoute()` calls
74
147
  1. In a test project, use only `integrations: [ncaAiCms()]` in astro config
75
148
  2. Run `npx astro dev`
76
149
  3. Open `/login` — should render without errors
77
- 4. Log in, open `/editor` settings tab should load without crash
150
+ 4. Log in, open `/editor` Settings tab → Homepage: should show key-value form fields, no "Neuen Prompt hinzufuegen"
151
+ 5. Homepage tab → edit hero text → save → reload → value persists
152
+ 6. Website tab → edit CTA URL, core tags → save → reload → values persist
153
+ 7. Content-KI tab → still shows prompt cards with create/edit/delete
154
+ 8. Analyse-KI and Bild-KI → same prompt UI as before
package/update.md CHANGED
@@ -1,3 +1,20 @@
1
+ # v1.0.6
2
+
3
+ ## Separate settings from prompts in SettingsTab
4
+ - Homepage and Website tabs now show key-value settings forms (hero text, zielgruppe, CTA, core tags, etc.)
5
+ - Content-KI, Analyse-KI, and Bild-KI tabs show prompt card UI with create/edit/delete
6
+ - Settings are saved via `POST /api/prompts` with `type: setting`
7
+ - Each settings tab has defined fields: homepage (hero, zielgruppe, ton, kernbotschaft), website (CTA, tags, markenrichtlinien)
8
+
9
+ ## Category guides and custom prompts
10
+ - Empty prompt categories show content marketing guidance with concrete examples
11
+ - New "+ Neuen Prompt hinzufuegen" button to create custom prompts
12
+ - Each prompt card now has a delete button
13
+ - API: added POST with `action: create` and DELETE endpoint for prompts
14
+ - PromptService: added `createPrompt()` and `deletePrompt()` methods
15
+
16
+ ---
17
+
1
18
  # v1.0.5
2
19
 
3
20
  ## Fix: SettingsTab crash on prompts response