docs: redakteur STATE.md aktualisiert 03.03.2026
Made-with: Cursor
This commit is contained in:
parent
82e0850df2
commit
de5f533096
5 changed files with 391 additions and 23 deletions
148
redax-wp/PLAN_CHAT_FREIE_PROMPTS.md
Normal file
148
redax-wp/PLAN_CHAT_FREIE_PROMPTS.md
Normal file
|
|
@ -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.*
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -19,10 +19,13 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- WordPress-Targets -->
|
||||
<div class="mb-6">
|
||||
<div class="text-xs text-slate-500 mb-2">📡 Publish-Ziele</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- WordPress-Targets (einklappbar) -->
|
||||
<div class="mb-3">
|
||||
<button type="button" onclick="var b=document.getElementById('wp-targets-box');b.classList.toggle('hidden');document.getElementById('wp-targets-chevron').textContent=b.classList.contains('hidden')?'▸':'▾'"
|
||||
class="text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1.5 py-1">
|
||||
<span id="wp-targets-chevron">▸</span> 📡 Publish-Ziele
|
||||
</button>
|
||||
<div id="wp-targets-box" class="hidden mt-2" style="display:flex;flex-wrap:wrap;gap:0.75rem">
|
||||
{% for t in wp_targets %}
|
||||
<div style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
|
||||
|
|
@ -68,10 +71,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- ═══ STUDIO (links, 2/3) ═══ -->
|
||||
<div class="xl:col-span-2 space-y-4">
|
||||
<!-- ═══ STUDIO (oben, volle Breite) ═══ -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Artikel-Generator -->
|
||||
<div class="card p-5">
|
||||
|
|
@ -107,22 +110,23 @@
|
|||
<input type="text" id="article-title" placeholder="Artikel-Titel" class="w-full">
|
||||
</div>
|
||||
|
||||
<!-- Zwei-Spalten Editor + KI verbessern -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<!-- Zwei-Spalten Editor + Vorschau (große Designfläche) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4" style="min-height: 65vh">
|
||||
<div class="flex flex-col min-h-0">
|
||||
<label class="text-xs text-slate-400 block mb-1">Inhalt (HTML)</label>
|
||||
<textarea id="article-content" rows="16" placeholder="Artikel-Inhalt..."
|
||||
oninput="updatePreview()"></textarea>
|
||||
<textarea id="article-content" placeholder="Artikel-Inhalt..."
|
||||
oninput="updatePreview()" class="flex-1 min-h-[320px]"
|
||||
style="min-height: 50vh; resize: vertical"></textarea>
|
||||
<div class="mt-2 flex gap-2 items-center flex-wrap">
|
||||
<input type="text" id="inline-image-url" placeholder="Bild-URL für Inhalt..." class="flex-1 min-w-[120px] text-xs">
|
||||
<button type="button" onclick="insertImageAtCursor()" class="text-xs px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-200">🖼️ Hier einfügen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-[420px]">
|
||||
<div class="flex flex-col min-h-0">
|
||||
<label class="text-xs text-slate-400 block mb-1">Vorschau</label>
|
||||
<div id="wp-preview"
|
||||
class="rounded-lg p-5 overflow-y-auto border border-slate-600"
|
||||
style="min-height: 380px; max-height: 60vh; font-family: Georgia, serif; line-height: 1.7; font-size: 1rem; background: #f8fafc; color: #0f172a;">
|
||||
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;">
|
||||
<span class="text-slate-500 italic">Vorschau erscheint beim Tippen...</span>
|
||||
</div>
|
||||
<div id="wp-draft-link" class="hidden"></div>
|
||||
|
|
@ -130,6 +134,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KI-Chat (freie Eingabe) -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="text-sm font-semibold text-white mb-2 flex items-center gap-2">
|
||||
<span>💬 KI-Chat</span>
|
||||
<button type="button" id="chat-toggle" onclick="var b=document.getElementById('chat-box');b.classList.toggle('hidden');this.textContent=b.classList.contains('hidden')?'▸ aufklappen':'▾ einklappen'"
|
||||
class="text-xs text-slate-500 hover:text-slate-300">▾ einklappen</button>
|
||||
</div>
|
||||
<div id="chat-box">
|
||||
<div id="chat-messages" class="space-y-3 mb-3 max-h-[220px] overflow-y-auto rounded-lg border border-slate-600 p-3 bg-slate-900/50 text-sm"
|
||||
style="min-height: 80px">
|
||||
<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht – die KI kennt deinen Artikel.</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="chat-input" placeholder="z.B. Kürze den ersten Absatz, schreib einen Teaser..."
|
||||
class="flex-1 text-sm" onkeydown="if(event.key==='Enter')sendChat()">
|
||||
<button type="button" id="btn-chat" onclick="sendChat()" class="btn btn-primary">Senden</button>
|
||||
</div>
|
||||
<div id="chat-apply-row" class="hidden mt-2">
|
||||
<button type="button" onclick="applyChatSuggestion()" class="btn btn-success text-xs">✓ In Editor übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO-Panel -->
|
||||
<div class="bg-slate-900/50 border border-slate-700 rounded-lg p-3 mb-4">
|
||||
<div class="text-xs text-slate-400 font-semibold mb-2">🔍 SEO</div>
|
||||
|
|
@ -203,7 +230,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- ═══ REDAKTIONSPLAN (rechts, 1/3) ═══ -->
|
||||
<!-- ═══ REDAKTIONSPLAN (unten, volle Breite, scrollen) ═══ -->
|
||||
<div class="space-y-4">
|
||||
|
||||
{% 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 = '<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht – die KI kennt deinen Artikel.</span>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = chatHistory.map(m => {
|
||||
const isUser = m.role === 'user';
|
||||
return `<div class="flex ${isUser ? 'justify-end' : 'justify-start'}">
|
||||
<div class="max-w-[85%] rounded-lg px-3 py-2 text-xs ${isUser ? 'bg-blue-900/50 border border-blue-700' : 'bg-slate-800 border border-slate-600'}">
|
||||
${(m.content || '').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
</div>`;
|
||||
}).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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue