diff --git a/redax-wp/PLAN_CHAT_FREIE_PROMPTS.md b/redax-wp/PLAN_CHAT_FREIE_PROMPTS.md new file mode 100644 index 00000000..08869b82 --- /dev/null +++ b/redax-wp/PLAN_CHAT_FREIE_PROMPTS.md @@ -0,0 +1,148 @@ +# Plan: Chat-Fenster mit freier Promptwahl + +**Stand:** 02.03.2026 +**Status:** ✅ **Umgesetzt** +**Ziel:** Schnelle, direkte KI-Interaktion ohne Umweg über feste Prompts + +--- + +## 1. Übersicht + +| Element | Beschreibung | +|--------|--------------| +| **Chatfenster** | Unter dem Editor, freie Texteingabe an die KI | +| **Kontext** | Aktueller Artikel (Titel, Inhalt, SEO) wird automatisch mitgegeben | +| **Prompts** | Feste Prompts bleiben optional (für "KI generieren" aus Quelle) | +| **Priorität** | Chat = Hauptweg, feste Prompts = Nebenweg | + +--- + +## 2. Layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Nav, Status, Publish-Ziele │ +├──────────────────────────────┬──────────────────────────────┤ +│ Inhalt (Editor) │ Vorschau │ +│ │ │ +├──────────────────────────────┴──────────────────────────────┤ +│ 💬 KI-Chat (freie Eingabe) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Nachrichtenverlauf (User/KI) │ │ +│ ├───────────────────────────────────────────────────────┤ │ +│ │ [Eingabefeld] [Senden] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ SEO, Medien, Buttons │ +├─────────────────────────────────────────────────────────────┤ +│ Redaktionsplan (nach Scrollen) │ +└─────────────────────────────────────────────────────────────┘ +``` + +- Chat direkt unter Editor + Vorschau +- **Standard: offen** (schnelle Interaktion), einklappbar mit Chevron „▸ KI-Chat“ +- Eingabefeld groß genug für längere Anweisungen + +--- + +## 3. API + +### Endpoint: `POST /api/chat` + +**Request:** +```json +{ + "message": "Mach den ersten Absatz knackiger", + "history": [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."} + ], + "context": { + "title": "Aktueller Titel", + "content": "Aktueller Inhalt...", + "seo_title": "...", + "seo_description": "...", + "focus_keyword": "..." + } +} +``` + +**Response:** +```json +{ + "reply": "Antwort der KI...", + "suggested_content": "Optional: überarbeiteter Text zum Übernehmen" +} +``` + +- Backend: OpenRouter wie bei `/api/polish`, aber mit Chat-System-Prompt +- System-Prompt: „Du bist ein Redakteurs-Assistent. Der User arbeitet an einem Artikel. Kontext: [Titel, Inhalt, SEO]. Antworte kurz und handlungsorientiert. Wenn der User Änderungen will, gib den überarbeiteten Text als `suggested_content` zurück.“ + +--- + +## 4. Chat-UI (Frontend) + +| Element | Beschreibung | +|---------|--------------| +| Nachrichtenliste | User-Nachrichten rechts/unterschiedlich, KI links/anders | +| Eingabefeld | Textarea oder input, Enter zum Senden | +| Senden-Button | Expliziter Klick | +| Spinner | Während die KI antwortet | +| Übernehmen-Button | Bei Antworten mit `suggested_content` – übernimmt in Editor/SEO | +| History | Nur Session (im Speicher), max. 6 Nachrichtenpaare, kein DB-Persistenz (v1) | + +--- + +## 5. Ablauf + +1. User tippt im Chat: „Kürze den zweiten Absatz“ +2. Frontend sendet: message + context (aktueller Artikel) + history (falls vorhanden) +3. Backend ruft OpenRouter auf, KI antwortet +4. Antwort erscheint im Chat +5. Optional: KI liefert `suggested_content` → Button „In Editor übernehmen“ + +--- + +## 6. Feste Prompts (Anpassung) + +| Änderung | Beschreibung | +|----------|--------------| +| Quelle + Ton + Prompt | Bleiben für „KI generieren“ (Artikel aus URL/Text erzeugen) | +| Position | Können kompakter werden (z.B. eine Zeile), Chat ist prominenter | +| Optional | Wenn User nur chatten will: Quelle/Prompt ignorierbar | + +--- + +## 7. Aufwand (grobe Schätzung) + +| Teil | Zeilen / Aufwand | +|------|------------------| +| `POST /api/chat` in app.py | ~40 Zeilen | +| Chat-UI in index.html | ~80 Zeilen HTML/JS | +| Integration Kontext (getArticleData) | ~5 Zeilen | +| **Gesamt** | ~2–3 Stunden | + +--- + +## 8. Entscheidungen (festgelegt) + +| Punkt | Entscheidung | +|-------|--------------| +| **Chat Standard** | Offen (schnelle Interaktion), einklappbar optional | +| **History persistieren** | Nein – nur Session, bei Artikel-Wechsel/Reload neu | +| **Übernehmen-Button** | Zeigen, wenn `suggested_content` vorhanden | +| **Max. History** | 6 Nachrichtenpaare (12 Nachrichten) | + +--- + +## 9. Reihenfolge (bei Umsetzung) + +1. API-Endpoint `/api/chat` +2. Chat-UI (Eingabe + Senden + Anzeige) +3. Kontext-Anbindung (Artikel mitschicken) +4. Übernehmen-Button (optional) +5. Feinschliff (Spinner, Fehlerbehandlung, Einklappbar) + +--- + +*Plan erstellt für Redax-WP. Umsetzung nur nach explizitem OK.* diff --git a/redax-wp/STATE.md b/redax-wp/STATE.md index e0f2178b..c18089e1 100644 --- a/redax-wp/STATE.md +++ b/redax-wp/STATE.md @@ -1,11 +1,11 @@ # STATE: Redax-WP (Redakteur) -**Stand: 28.02.2026** +**Stand: 02.03.2026** --- ## Status -✅ **Vollständig in Betrieb — Multi-Publish, KI-Serie, animierte Grafiken** +✅ **Vollständig in Betrieb — Multi-Publish, KI-Chat, KI-Serie** --- @@ -38,7 +38,7 @@ │ ├── app.py Flask-App, Scheduler, alle Routes │ ├── wordpress.py WordPressClient + WordPressMirrorClient │ ├── database.py SQLite Schema + Helpers -│ ├── openrouter.py OpenRouter API (sync wrapper) +│ ├── openrouter.py OpenRouter API (generate + generate_chat für KI-Chat) │ ├── rss_fetcher.py RSS Feed Parser │ ├── logger.py JSON Logging │ ├── Dockerfile.web Docker Image Build @@ -63,7 +63,8 @@ redax-db MySQL 8 ### KI-Artikel - Quelle eingeben → Ton wählen → KI generiert Artikel + SEO-Felder automatisch -- Zwei-Spalten-Editor: Markdown links / WordPress-Vorschau rechts +- Zwei-Spalten-Editor: Markdown links / WordPress-Vorschau rechts (große Designfläche 50vh/75vh) +- **KI-Chat** unter Editor: Freie Texteingabe an die KI, Artikelkontext wird automatisch mitgegeben, max. 6 Nachrichtenpaare History, Button „In Editor übernehmen“ bei Änderungsvorschlägen - Featured Image aus og:image der Quelle automatisch - Kategorie + Tags aus WordPress live geladen - Publish / Entwurf / Einplanen (15-Minuten-Slots) @@ -79,7 +80,8 @@ redax-db MySQL 8 - Credentials (User/PW) direkt im Dashboard sichtbar - WP-Admin Direktzugang via socat-Proxy (bypass Cloudflare WAF) -### Redaktionsplan (verbessert) +### Redaktionsplan +- **Layout:** Unten (volle Breite), nach Scrollen sichtbar — Studio nimmt oben gesamte Breite - 7-Tage-Kalender mit KI + RSS gemeinsam - Badge: 🤖 KI / 📡 RSS - **Drag & Drop** zum Umplanen zwischen Tagen @@ -88,6 +90,10 @@ redax-db MySQL 8 - **Entwürfe ohne Datum** in separater Sektion sichtbar - WP-Editor-Link für Arakava News via socat-Proxy: `http://100.88.230.59:8101/wp-admin/` +### Entwurf-Speicher +- Zwei Links nach Push: **Im WP-Editor bearbeiten** (WP_ADMIN_DIRECT_URL) + **Vorschau ansehen** +- Publish-Ziele (WP-Targets) einklappbar, Standard eingeklappt + ### RSS-Feeds - Beliebig viele Feeds konfigurierbar - Modi: Manuell / Auto-Publish (Teaser) / KI-Rewrite + Publish @@ -134,7 +140,7 @@ Direkt-URL: `http://100.88.230.59:8101/wp-admin/` | Was | Pfad | |-----|------| | App | /opt/redax-wp/ | -| Datenbank | /opt/redax-wp/data/redax.db | +| Datenbank | /opt/redax-wp/data/db/redax.db | | WordPress-Dateien | /opt/redax-wp/data/wordpress/ | | MySQL-Daten | /opt/redax-wp/data/mysql/ | | Logs | /opt/redax-wp/logs/ | @@ -184,6 +190,14 @@ TELEGRAM_CHANNEL_ID=... ## Changelog +### 02.03.2026 — KI-Chat + Layout +- **KI-Chat** unter Editor: Freie Texteingabe, Artikelkontext, History (6 Paare), „In Editor übernehmen“ +- **API** `/api/chat` + `openrouter.generate_chat(messages)` +- **Layout:** Redaktionsplan nach unten (volle Breite), Studio oben volle Breite +- **Publish-Ziele** einklappbar +- **Editor + Vorschau** größer (50vh / 75vh) +- **Entwurf:** Zwei Links (WP-Editor bearbeiten + Vorschau) + ### 28.02.2026 — ESP32-Serie Teil 2 + Animiertes Hydraulikschema - **ESP32-Serie Teil 2** als WP-Entwurf erstellt (Post 1340 auf Arakava News) - Titel: "70 Euro gegen Heizungschaos: Die Hardware für mein Smart-Home-Projekt" diff --git a/redax-wp/src/app.py b/redax-wp/src/app.py index 9aed45c9..d6a8a2cb 100644 --- a/redax-wp/src/app.py +++ b/redax-wp/src/app.py @@ -345,6 +345,85 @@ KEYWORD: [1 Wort]""" return jsonify({'error': str(e)}), 500 +@app.route('/api/chat', methods=['POST']) +def api_chat(): + """Freier KI-Chat mit Artikelkontext. History max 6 Paare.""" + data = request.json + message = (data.get('message') or '').strip() + history = data.get('history') or [] + ctx = data.get('context') or {} + + if not message: + return jsonify({'error': 'Bitte Nachricht eingeben'}), 400 + + # History auf 6 Paare begrenzen + history = history[-12:] # max 12 messages (6 user + 6 assistant) + + title = ctx.get('title', '') + content = ctx.get('content', '') + seo_title = ctx.get('seo_title', '') + seo_desc = ctx.get('seo_description', '') + keyword = ctx.get('focus_keyword', '') + + system = """Du bist ein Redakteurs-Assistent. Der User arbeitet an einem WordPress-Artikel. +Kontext des aktuellen Artikels: +- Titel: """ + (title or '(leer)') + """ +- Inhalt: """ + (content[:3000] + '...' if len(content or '') > 3000 else (content or '(leer)')) + """ +- SEO-Titel: """ + (seo_title or '(leer)') + """ +- SEO-Beschreibung: """ + (seo_desc or '(leer)') + """ +- Fokus-Keyword: """ + (keyword or '(leer)') + """ + +Antworte kurz und handlungsorientiert. Wenn der User Änderungen am Artikel wünscht (kürzen, umschreiben, Teaser, etc.): +Gib deine Antwort, und am Ende in diesem Format den überarbeiteten Inhalt: +===APPLY=== +TITEL: [neuer Titel falls geändert] +INHALT: [HTML-Inhalt] +SEO_TITLE: [optional, max 60 Zeichen] +SEO_DESC: [optional, max 155 Zeichen] +KEYWORD: [optional] +===ENDAPPLY=== +Nur die Felder angeben die sich ändern. Ohne ===APPLY=== wenn du keinen Text zum Übernehmen lieferst.""" + + messages = [{"role": "system", "content": system}] + for h in history: + if h.get('role') in ('user', 'assistant') and h.get('content'): + messages.append({"role": h["role"], "content": h["content"]}) + messages.append({"role": "user", "content": message}) + + try: + raw = openrouter.generate_chat(messages) + suggested = None + if '===APPLY===' in raw and '===ENDAPPLY===' in raw: + try: + block = raw.split('===APPLY===')[1].split('===ENDAPPLY===')[0].strip() + parts = {} + for line in block.split('\n'): + if ':' in line: + k, v = line.split(':', 1) + parts[k.strip().upper()] = v.strip() + suggested = { + 'title': parts.get('TITEL') or title, + 'content': parts.get('INHALT') or content, + 'seo_title': parts.get('SEO_TITLE') or seo_title, + 'seo_description': parts.get('SEO_DESC') or seo_desc, + 'focus_keyword': parts.get('KEYWORD') or keyword, + } + reply = raw.split('===APPLY===')[0].strip() + except Exception: + reply = raw + else: + reply = raw + + flog.info('chat_message', msg_len=len(message)) + out = {'reply': reply} + if suggested: + out['suggested_content'] = suggested + return jsonify(out) + except Exception as e: + flog.error('chat_failed', error=str(e)) + return jsonify({'error': str(e)}), 500 + + @app.route('/api/article/save', methods=['POST']) def api_save_article(): data = request.json diff --git a/redax-wp/src/openrouter.py b/redax-wp/src/openrouter.py index f55774cc..102f3669 100644 --- a/redax-wp/src/openrouter.py +++ b/redax-wp/src/openrouter.py @@ -96,3 +96,27 @@ def generate(system_prompt: str, user_message: str) -> str: raise Exception(f"OpenRouter Fehler {resp.status}: {data}") return data["choices"][0]["message"]["content"].strip() return asyncio.run(_gen()) + + +def generate_chat(messages: list) -> str: + """Chat mit History. messages: [{"role":"system/user/assistant","content":"..."}, ...]""" + import asyncio, aiohttp as _aiohttp + async def _gen(): + payload = { + "model": DEFAULT_MODEL, + "messages": messages, + "max_tokens": 2500, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {OPENROUTER_API_KEY}", + "Content-Type": "application/json", + } + timeout = _aiohttp.ClientTimeout(total=90) + async with _aiohttp.ClientSession(timeout=timeout) as session: + async with session.post(f"{OPENROUTER_BASE}/chat/completions", json=payload, headers=headers) as resp: + data = await resp.json() + if resp.status != 200: + raise Exception(f"OpenRouter Fehler {resp.status}: {data}") + return data["choices"][0]["message"]["content"].strip() + return asyncio.run(_gen()) diff --git a/redax-wp/src/templates/index.html b/redax-wp/src/templates/index.html index c3eff513..74875af9 100644 --- a/redax-wp/src/templates/index.html +++ b/redax-wp/src/templates/index.html @@ -19,10 +19,13 @@ {% endif %} - -
-
📡 Publish-Ziele
-
+ +
+ + -
+
- -
+ +
@@ -107,22 +110,23 @@
- -
-
+ +
+
- +
-
+
+ class="rounded-lg p-5 overflow-y-auto border border-slate-600 flex-1" + style="min-height: 50vh; max-height: 75vh; font-family: Georgia, serif; line-height: 1.7; font-size: 1rem; background: #f8fafc; color: #0f172a;"> Vorschau erscheint beim Tippen...
@@ -130,6 +134,29 @@
+ +
+
+ 💬 KI-Chat + +
+
+
+ Schreibe eine Nachricht – die KI kennt deinen Artikel. +
+
+ + +
+ +
+
+
🔍 SEO
@@ -203,7 +230,7 @@
- +
{% set draft_arts = [] %} @@ -380,16 +407,91 @@ function insertImageAtCursor() { function setButtonLoading(btnId, loading) { const btn = document.getElementById(btnId); if (!btn) return; + const labels = { 'btn-generate': '🤖 KI generieren', 'btn-polish': 'Verbessern', 'btn-chat': 'Senden' }; if (loading) { btn.dataset.origText = btn.textContent; btn.disabled = true; btn.textContent = 'Bitte warten...'; } else { btn.disabled = false; - btn.textContent = btn.dataset.origText || (btnId === 'btn-generate' ? '🤖 KI generieren' : 'Verbessern'); + btn.textContent = btn.dataset.origText || labels[btnId] || 'OK'; } } +let chatHistory = []; +let lastSuggested = null; + +function renderChatMessages() { + const el = document.getElementById('chat-messages'); + if (!el) return; + if (chatHistory.length === 0) { + el.innerHTML = 'Schreibe eine Nachricht – die KI kennt deinen Artikel.'; + return; + } + el.innerHTML = chatHistory.map(m => { + const isUser = m.role === 'user'; + return `
+
+ ${(m.content || '').replace(//g, '>').replace(/\n/g, '
')} +
+
`; + }).join(''); + el.scrollTop = el.scrollHeight; +} + +async function sendChat() { + const input = document.getElementById('chat-input'); + const msg = (input?.value || '').trim(); + if (!msg) return; + + const ctx = getArticleData(); + const history = chatHistory.slice(-12).map(m => ({ role: m.role, content: m.content })); + chatHistory.push({ role: 'user', content: msg }); + input.value = ''; + renderChatMessages(); + document.getElementById('chat-apply-row').classList.add('hidden'); + lastSuggested = null; + + setButtonLoading('btn-chat', true); + try { + const r = await fetch('/api/chat', { + method: 'POST', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ + message: msg, + history: history, + context: { title: ctx.title, content: ctx.content, seo_title: ctx.seo_title, seo_description: ctx.seo_description, focus_keyword: ctx.focus_keyword } + }) + }); + const d = await r.json().catch(() => ({})); + if (d.error) { showToast('❌ ' + d.error); chatHistory.pop(); renderChatMessages(); return; } + chatHistory.push({ role: 'assistant', content: d.reply }); + if (chatHistory.length > 12) chatHistory = chatHistory.slice(-12); + if (d.suggested_content) { lastSuggested = d.suggested_content; document.getElementById('chat-apply-row').classList.remove('hidden'); } + renderChatMessages(); + } catch (e) { + showToast('❌ ' + (e.message || 'Fehler')); chatHistory.pop(); renderChatMessages(); + } finally { + setButtonLoading('btn-chat', false); + } +} + +function applyChatSuggestion() { + if (!lastSuggested) return; + if (lastSuggested.title) document.getElementById('article-title').value = lastSuggested.title; + if (lastSuggested.content) document.getElementById('article-content').value = lastSuggested.content; + if (lastSuggested.seo_title) document.getElementById('seo-title').value = lastSuggested.seo_title; + if (lastSuggested.seo_description) document.getElementById('seo-description').value = lastSuggested.seo_description; + if (lastSuggested.focus_keyword) document.getElementById('focus-keyword').value = lastSuggested.focus_keyword; + const c1 = document.getElementById('seo-title-count'); + if (c1) c1.textContent = (lastSuggested.seo_title || '').length + '/60'; + const c2 = document.getElementById('seo-desc-count'); + if (c2) c2.textContent = (lastSuggested.seo_description || '').length + '/155'; + updatePreview(); + document.getElementById('chat-apply-row').classList.add('hidden'); + lastSuggested = null; + showToast('✓ Übernommen'); +} + async function polishArticle() { const instruction = document.getElementById('polish-instruction').value.trim(); const title = document.getElementById('article-title').value; @@ -586,6 +688,7 @@ async function confirmSchedule() { } async function loadArticle(id) { + if (currentArticleId !== id) { chatHistory = []; lastSuggested = null; if (document.getElementById('chat-apply-row')) document.getElementById('chat-apply-row').classList.add('hidden'); renderChatMessages(); } const r = await fetch(`/api/article/${id}`); const d = await r.json(); currentArticleId = id;