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 +1 -1
- package/src/api/prompts.ts +28 -2
- package/src/components/editor/SettingsTab.tsx +328 -60
- package/src/components/editor/styles.ts +84 -0
- package/src/services/PromptService.ts +14 -0
- package/task.md +81 -4
- package/update.md +17 -0
package/package.json
CHANGED
package/src/api/prompts.ts
CHANGED
|
@@ -22,24 +22,50 @@ export const GET: APIRoute = async () => {
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
// POST /api/prompts -
|
|
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
|
|
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
|
|
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
|
-
|
|
46
|
-
}, [
|
|
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
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</div>
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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`
|
|
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
|