817 lines
39 KiB
HTML
817 lines
39 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Redax-WP — Studio{% endblock %}
|
||
{% block extra_head %}
|
||
<style>
|
||
#wp-preview, #wp-preview p, #wp-preview h1, #wp-preview h2, #wp-preview h3, #wp-preview li, #wp-preview span { color: #0f172a !important; }
|
||
#wp-preview a { color: #2563eb !important; }
|
||
</style>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div class="max-w-7xl mx-auto px-6 py-6" data-wp-admin="{{ wp_admin_direct }}" data-wp-url="{{ wp_url }}">
|
||
|
||
<!-- Status-Bar -->
|
||
<div class="flex items-center gap-4 mb-4 text-xs text-slate-500 flex-wrap">
|
||
{% if last_published %}
|
||
<span>Letzter Post: <span class="text-slate-300">{{ last_published.wp_url[:50] if last_published.wp_url else last_published.title[:40] }}</span> — {{ last_published.published_at[:16] if last_published.published_at else '' }}</span>
|
||
{% endif %}
|
||
{% if queue_count > 0 %}
|
||
<a href="/feeds" class="text-yellow-400 hover:text-yellow-300">📥 {{ queue_count }} RSS-Artikel in Queue</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 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">
|
||
|
||
<!-- Checkbox (Primary gesperrt) -->
|
||
{% if t.primary %}
|
||
<label style="cursor:default;color:#93c5fd;font-size:0.75rem;white-space:nowrap">
|
||
<input type="checkbox" checked disabled style="accent-color:#3b82f6"> {{ t.label }}
|
||
<span style="opacity:0.5;font-size:0.65rem">(Primary)</span>
|
||
</label>
|
||
{% else %}
|
||
<form method="POST" action="/targets/toggle" style="display:inline;margin:0">
|
||
<input type="hidden" name="name" value="{{ t.name }}">
|
||
<label style="cursor:pointer;color:{% if t.enabled %}#4ade80{% else %}#64748b{% endif %};font-size:0.75rem;white-space:nowrap">
|
||
<input type="checkbox" {% if t.enabled %}checked{% endif %}
|
||
onchange="this.form.submit()"
|
||
style="accent-color:#22c55e;cursor:pointer"> {{ t.label }}
|
||
</label>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<!-- Trennlinie -->
|
||
<span style="color:#334155;font-size:0.8rem">|</span>
|
||
|
||
<!-- Website-Button -->
|
||
<a href="{{ t.url }}" target="_blank"
|
||
style="font-size:0.72rem;padding:3px 10px;border-radius:5px;background:#0f172a;
|
||
border:1px solid #334155;color:#94a3b8;text-decoration:none;white-space:nowrap"
|
||
title="{{ t.url }}">🌐 Website</a>
|
||
|
||
<!-- Admin-Button: direkte URL (bypass Cloudflare WAF) -->
|
||
<a href="{{ t.admin_url }}" target="_blank"
|
||
style="font-size:0.72rem;padding:3px 10px;border-radius:5px;background:#0f172a;
|
||
border:1px solid #334155;color:#94a3b8;text-decoration:none;white-space:nowrap"
|
||
title="Login: {{ t.username }}">⚙️ WP-Admin</a>
|
||
|
||
<!-- Login-Info -->
|
||
<span style="font-size:0.65rem;color:#475569;white-space:nowrap">
|
||
{{ t.username }} / {{ t.admin_pw }}
|
||
</span>
|
||
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-6">
|
||
|
||
<!-- ═══ STUDIO (oben, volle Breite) ═══ -->
|
||
<div class="space-y-4">
|
||
|
||
<!-- Artikel-Generator -->
|
||
<div class="card p-5">
|
||
<h2 class="text-base font-semibold text-white mb-4">✍️ Artikel-Studio</h2>
|
||
|
||
<!-- Quelle + Ton + Prompt -->
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
|
||
<div class="md:col-span-2">
|
||
<label class="text-xs text-slate-400 block mb-1">Quelle (URL oder Text)</label>
|
||
<input type="text" id="source-input" placeholder="https://... oder Text einfügen" class="w-full">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Ton</label>
|
||
<select id="tone-select" class="w-full">
|
||
<option value="informativ">Informativ</option>
|
||
<option value="meinungsstark">Meinungsstark</option>
|
||
<option value="reportage">Reportage</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Prompt</label>
|
||
<select id="prompt-select" class="w-full">
|
||
{% for p in prompts %}
|
||
<option value="{{ p.id }}" {% if p.is_default %}selected{% endif %}>{{ p.name }}{% if p.is_default %} ✓{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Titel -->
|
||
<div class="mb-3">
|
||
<label class="text-xs text-slate-400 block mb-1">Titel</label>
|
||
<input type="text" id="article-title" placeholder="Artikel-Titel" class="w-full">
|
||
</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" 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="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 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>
|
||
<div id="mirror-status-box" class="hidden mt-2 text-xs space-y-1"></div>
|
||
</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>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Meta-Title <span id="seo-title-count" class="text-slate-600">0/60</span></label>
|
||
<input type="text" id="seo-title" placeholder="SEO Titel" class="w-full text-xs" oninput="document.getElementById('seo-title-count').textContent=this.value.length+'/60'">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Meta-Description <span id="seo-desc-count" class="text-slate-600">0/155</span></label>
|
||
<input type="text" id="seo-description" placeholder="Kurzbeschreibung" class="w-full text-xs" oninput="document.getElementById('seo-desc-count').textContent=this.value.length+'/155'">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Fokus-Keyword</label>
|
||
<input type="text" id="focus-keyword" placeholder="Hauptkeyword" class="w-full text-xs">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Medien + Kategorie -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Featured Image URL</label>
|
||
<input type="url" id="featured-image" placeholder="https://..." class="w-full text-xs">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Kategorie</label>
|
||
<select id="category-select" class="w-full text-xs">
|
||
<option value="">— keine —</option>
|
||
{% for cat in wp_categories %}
|
||
<option value="{{ cat.id }}">{{ cat.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aktions-Buttons -->
|
||
<div class="flex flex-col gap-3">
|
||
<div class="flex gap-2 flex-wrap">
|
||
<button id="btn-generate" onclick="generateArticle()" class="btn btn-primary">🤖 KI generieren</button>
|
||
<button onclick="saveDraft()" class="btn btn-ghost">💾 Entwurf</button>
|
||
<button onclick="publishNow()" class="btn btn-success">🚀 Sofort veröffentlichen</button>
|
||
<button onclick="toggleSchedulePanel()" class="btn" style="background:#4c1d95;color:#fff">📅 Einplanen</button>
|
||
</div>
|
||
<div class="flex gap-2 flex-wrap items-center p-3 rounded-lg border border-slate-600 bg-slate-800/60">
|
||
<span class="text-sm font-medium text-slate-300">✨ KI verbessern:</span>
|
||
<input type="text" id="polish-instruction" placeholder="z.B. kürzer, lockerer — oder: Bild https://... nach Absatz 2 einfügen" class="flex-1 min-w-[200px] text-sm">
|
||
<button id="btn-polish" type="button" onclick="polishArticle()" class="btn btn-primary">Verbessern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Einplan-Panel -->
|
||
<div id="schedule-panel" class="hidden mt-4 bg-slate-900 border border-slate-700 rounded-lg p-4">
|
||
<div class="text-sm text-purple-400 font-semibold mb-3">📅 Artikel einplanen</div>
|
||
<div class="flex gap-3 items-end flex-wrap">
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Datum</label>
|
||
<input type="date" id="schedule-date" class="text-xs" onchange="checkSlot()">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Uhrzeit (15-Min-Slots)</label>
|
||
<input type="time" id="schedule-time" value="19:00" step="900" class="text-xs" onchange="checkSlot()">
|
||
</div>
|
||
<button onclick="confirmSchedule()" id="schedule-confirm-btn"
|
||
class="btn btn-primary text-xs">✓ Einplanen</button>
|
||
<button onclick="toggleSchedulePanel()" class="text-xs text-slate-500 hover:text-white px-2 py-1.5">Abbrechen</button>
|
||
</div>
|
||
<div id="slot-status" class="text-xs mt-2 hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ═══ REDAKTIONSPLAN (unten, volle Breite, scrollen) ═══ -->
|
||
<div class="space-y-4">
|
||
|
||
{% set draft_arts = [] %}
|
||
{% for d in plan_days %}
|
||
{% for a in plan_articles.get(d, []) %}
|
||
{% if a.status == 'draft' %}{% set _ = draft_arts.append(a) %}{% endif %}
|
||
{% endfor %}
|
||
{% endfor %}
|
||
{% set undated_drafts = undated_drafts if undated_drafts is defined else [] %}
|
||
|
||
{% if undated_drafts %}
|
||
<div class="card p-4">
|
||
<h2 class="text-sm font-semibold text-slate-300 mb-3">📝 Entwürfe ohne Datum</h2>
|
||
<div class="space-y-1">
|
||
{% for art in undated_drafts %}
|
||
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-700/50 bg-slate-800/30 cursor-pointer hover:border-slate-500 transition"
|
||
onclick="loadArticle({{ art.id }})">
|
||
<span class="text-xs">🤖</span>
|
||
<span class="text-xs text-slate-300 flex-1 truncate">{{ (art.title or 'Kein Titel')[:55] }}</span>
|
||
<span class="text-xs text-slate-500">Entwurf</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card p-4">
|
||
<h2 class="text-base font-semibold text-white mb-3">📅 Redaktionsplan — 7 Tage</h2>
|
||
<p class="text-xs text-slate-600 mb-3">Artikel per Drag & Drop auf einen anderen Tag ziehen zum Umplanen.</p>
|
||
|
||
{% set type_icons = {'ki':'🤖','rss':'📡'} %}
|
||
{% for d in plan_days %}
|
||
{% set arts = plan_articles.get(d, []) %}
|
||
{% set is_today = (d == today) %}
|
||
|
||
<!-- Drop-Zone für jeden Tag -->
|
||
<div class="drop-zone mb-2" data-date="{{ d }}"
|
||
ondragover="event.preventDefault(); this.style.background='rgba(59,130,246,0.08)'"
|
||
ondragleave="this.style.background=''"
|
||
ondrop="onDrop(event, '{{ d }}')">
|
||
|
||
<div style="display:flex;align-items:center;gap:6px;padding:4px 4px 2px 4px">
|
||
<span style="font-size:0.72rem;font-weight:700;color:{% if is_today %}#60a5fa{% else %}#475569{% endif %}">
|
||
{{ d[8:] }}.{{ d[5:7] }}.
|
||
</span>
|
||
{% if is_today %}
|
||
<span style="font-size:0.65rem;background:#1e3a5f;border:1px solid #3b82f6;color:#60a5fa;padding:1px 7px;border-radius:999px">Heute</span>
|
||
{% endif %}
|
||
{% if not arts %}
|
||
<span style="font-size:0.68rem;color:#334155;font-style:italic">— leer</span>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% for art in arts %}
|
||
<!-- Artikel-Karte: draggable -->
|
||
<div id="plan-row-{{ art.id }}"
|
||
draggable="true"
|
||
ondragstart="onDragStart(event, {{ art.id }}, '{{ art.post_time }}')"
|
||
ondragend="onDragEnd(event)"
|
||
style="border-radius:7px;border:1px solid #334155;background:#1e293b;
|
||
margin-bottom:5px;cursor:grab;transition:opacity 0.15s">
|
||
|
||
<!-- Haupt-Zeile -->
|
||
<div style="display:flex;align-items:flex-start;gap:8px;padding:8px 10px"
|
||
onclick="loadArticle({{ art.id }})" style="cursor:pointer">
|
||
<span style="font-size:1rem;flex-shrink:0;margin-top:1px">{{ type_icons.get(art.article_type, '📝') }}</span>
|
||
<div style="flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px;flex-wrap:wrap">
|
||
<span style="font-size:0.7rem;color:#64748b;font-family:monospace">{{ art.post_time }}</span>
|
||
<span style="font-size:0.72rem;padding:1px 7px;border-radius:999px;
|
||
{% if art.status == 'published' %}background:#14532d;color:#4ade80;border:1px solid #166534
|
||
{% elif art.status == 'scheduled' %}background:#1e3a5f;color:#60a5fa;border:1px solid #1d4ed8
|
||
{% else %}background:#292524;color:#a8a29e;border:1px solid #44403c{% endif %}">
|
||
{{ {'draft':'Entwurf','scheduled':'Geplant','published':'✓ Live'}.get(art.status, art.status) }}
|
||
</span>
|
||
</div>
|
||
<!-- Titel -->
|
||
<div style="font-size:0.8rem;color:#e2e8f0;font-weight:500;margin-bottom:4px;cursor:pointer"
|
||
onclick="loadArticle({{ art.id }})">
|
||
{{ (art.title or 'Kein Titel')[:70] }}
|
||
</div>
|
||
<!-- Snippet -->
|
||
{% if art.seo_description %}
|
||
<div style="font-size:0.7rem;color:#64748b;line-height:1.4">
|
||
{{ art.seo_description[:100] }}…
|
||
</div>
|
||
{% endif %}
|
||
<!-- Aktions-Buttons -->
|
||
<div style="display:flex;gap:5px;margin-top:6px;flex-wrap:wrap" onclick="event.stopPropagation()">
|
||
<button onclick="loadArticle({{ art.id }})"
|
||
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
|
||
border:1px solid #475569;color:#94a3b8;cursor:pointer">
|
||
✏️ Bearbeiten
|
||
</button>
|
||
{% if art.wp_post_id %}
|
||
<a href="{{ wp_admin_direct }}/wp-admin/post.php?post={{ art.wp_post_id }}&action=edit" target="_blank"
|
||
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e3a5f;
|
||
border:1px solid #3b82f6;color:#93c5fd;text-decoration:none">
|
||
🌐 WP-Editor
|
||
</a>
|
||
<a href="{{ wp_url }}/?p={{ art.wp_post_id }}&preview=true" target="_blank"
|
||
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
|
||
border:1px solid #475569;color:#94a3b8;text-decoration:none">
|
||
👁 Vorschau
|
||
</a>
|
||
{% endif %}
|
||
{% if art.status != 'published' %}
|
||
<button onclick="openReschedule({{ art.id }}, '{{ d }}', '{{ art.post_time }}')"
|
||
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
|
||
border:1px solid #475569;color:#94a3b8;cursor:pointer" title="Umplanen">
|
||
🗓 Umplanen
|
||
</button>
|
||
<button onclick="deleteArticle({{ art.id }})"
|
||
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
|
||
border:1px solid #475569;color:#94a3b8;cursor:pointer" title="Löschen">
|
||
🗑
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Umplan-Panel -->
|
||
<div id="rs-panel-{{ art.id }}" class="hidden"
|
||
style="border-top:1px solid #334155;padding:8px 10px;background:#0f172a;border-radius:0 0 7px 7px">
|
||
<div style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
||
<input type="date" id="rs-date-{{ art.id }}" value="{{ d }}" class="text-xs py-1 px-2">
|
||
<input type="time" id="rs-time-{{ art.id }}" value="{{ art.post_time }}" step="900" class="text-xs py-1 px-2">
|
||
<button onclick="confirmReschedule({{ art.id }})"
|
||
style="font-size:0.72rem;background:#92400e;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer">✓ OK</button>
|
||
<button onclick="closeReschedule({{ art.id }})"
|
||
style="font-size:0.72rem;background:none;color:#64748b;border:none;cursor:pointer">✗</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- hidden article id -->
|
||
<input type="hidden" id="current-article-id" value="">
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
let currentArticleId = null;
|
||
let currentWpPostId = null;
|
||
|
||
function updatePreview() {
|
||
const content = document.getElementById('article-content').value;
|
||
const title = document.getElementById('article-title').value;
|
||
const preview = document.getElementById('wp-preview');
|
||
preview.innerHTML = (title ? `<h1 style="font-size:1.4rem;font-weight:700;margin-bottom:1rem">${title}</h1>` : '') + content;
|
||
}
|
||
|
||
function insertImageAtCursor() {
|
||
const url = document.getElementById('inline-image-url').value.trim();
|
||
const ta = document.getElementById('article-content');
|
||
if (!url) { showToast('⚠️ Bild-URL eingeben'); return; }
|
||
const img = `<p><img src="${url.replace(/"/g, '"')}" alt="Bild" style="max-width:100%;height:auto;"></p>`;
|
||
const start = ta.selectionStart, end = ta.selectionEnd;
|
||
const before = ta.value.substring(0, start), after = ta.value.substring(end);
|
||
ta.value = before + img + after;
|
||
ta.selectionStart = ta.selectionEnd = start + img.length;
|
||
ta.focus();
|
||
updatePreview();
|
||
showToast('🖼️ Bild eingefügt');
|
||
}
|
||
|
||
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 || 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;
|
||
const content = document.getElementById('article-content').value;
|
||
if (!instruction) { showToast('⚠️ Anweisung eingeben'); return; }
|
||
if (!content && !title) { showToast('⚠️ Kein Inhalt zum Verbessern'); return; }
|
||
|
||
setButtonLoading('btn-polish', true);
|
||
const backup = setTimeout(() => setButtonLoading('btn-polish', false), 130000);
|
||
try {
|
||
const ctrl = new AbortController();
|
||
const t = setTimeout(() => ctrl.abort(), 120000);
|
||
const r = await fetch('/api/polish', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ title, content, instruction, seo_title: document.getElementById('seo-title').value, seo_description: document.getElementById('seo-description').value, focus_keyword: document.getElementById('focus-keyword').value }),
|
||
signal: ctrl.signal
|
||
});
|
||
clearTimeout(t);
|
||
const d = await r.json().catch(() => ({}));
|
||
if (d.error) { showToast('❌ ' + d.error); return; }
|
||
document.getElementById('article-title').value = d.title || '';
|
||
document.getElementById('article-content').value = d.content || '';
|
||
if (d.seo_title) document.getElementById('seo-title').value = d.seo_title;
|
||
if (d.seo_description) document.getElementById('seo-description').value = d.seo_description;
|
||
if (d.focus_keyword) document.getElementById('focus-keyword').value = d.focus_keyword;
|
||
const c1 = document.getElementById('seo-title-count');
|
||
if (c1) c1.textContent = (d.seo_title || '').length + '/60';
|
||
const c2 = document.getElementById('seo-desc-count');
|
||
if (c2) c2.textContent = (d.seo_description || '').length + '/155';
|
||
updatePreview();
|
||
document.getElementById('polish-instruction').value = '';
|
||
showToast('✨ Text verbessert');
|
||
} catch (e) {
|
||
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout' : (e.message || 'Fehler')));
|
||
} finally {
|
||
clearTimeout(backup);
|
||
setButtonLoading('btn-polish', false);
|
||
}
|
||
}
|
||
|
||
async function generateArticle() {
|
||
const source = document.getElementById('source-input').value.trim();
|
||
const tone = document.getElementById('tone-select').value;
|
||
const promptId = document.getElementById('prompt-select')?.value || null;
|
||
if (!source) { showToast('⚠️ Bitte Quelle eingeben'); return; }
|
||
|
||
setButtonLoading('btn-generate', true);
|
||
const backup = setTimeout(() => setButtonLoading('btn-generate', false), 130000);
|
||
try {
|
||
const ctrl = new AbortController();
|
||
const t = setTimeout(() => ctrl.abort(), 120000);
|
||
const r = await fetch('/api/generate', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({source, tone, prompt_id: promptId ? parseInt(promptId, 10) : null}),
|
||
signal: ctrl.signal
|
||
});
|
||
clearTimeout(t);
|
||
const d = await r.json().catch(() => ({}));
|
||
if (d.error) { showToast('❌ ' + d.error); return; }
|
||
|
||
document.getElementById('article-title').value = d.title || '';
|
||
document.getElementById('article-content').value = d.content || '';
|
||
document.getElementById('seo-title').value = d.seo_title || '';
|
||
document.getElementById('seo-description').value = d.seo_description || '';
|
||
document.getElementById('focus-keyword').value = d.focus_keyword || '';
|
||
updatePreview();
|
||
|
||
if (source.startsWith('http')) fetchOgImage(source);
|
||
showToast('✅ Artikel generiert');
|
||
} catch (e) {
|
||
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout (>2 Min)' : (e.message || 'Fehler')));
|
||
} finally {
|
||
clearTimeout(backup);
|
||
setButtonLoading('btn-generate', false);
|
||
}
|
||
}
|
||
|
||
async function fetchOgImage(url) {
|
||
try {
|
||
const r = await fetch('/api/og-image', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({url})
|
||
});
|
||
const d = await r.json();
|
||
if (d.image) document.getElementById('featured-image').value = d.image;
|
||
} catch(e) {}
|
||
}
|
||
|
||
function getArticleData() {
|
||
return {
|
||
id: currentArticleId,
|
||
title: document.getElementById('article-title').value,
|
||
content: document.getElementById('article-content').value,
|
||
source_url: document.getElementById('source-input').value,
|
||
tone: document.getElementById('tone-select').value,
|
||
seo_title: document.getElementById('seo-title').value,
|
||
seo_description: document.getElementById('seo-description').value,
|
||
focus_keyword: document.getElementById('focus-keyword').value,
|
||
featured_image_url: document.getElementById('featured-image').value,
|
||
category_id: document.getElementById('category-select').value || null,
|
||
article_type: 'ki',
|
||
};
|
||
}
|
||
|
||
async function saveDraft() {
|
||
const r = await fetch('/api/article/save', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({...getArticleData(), id: currentArticleId, wp_post_id: currentWpPostId, status: 'draft'})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
currentArticleId = d.id;
|
||
if (d.wp_post_id) currentWpPostId = d.wp_post_id;
|
||
|
||
// Vorschau- und Editor-Links anzeigen
|
||
const linkBox = document.getElementById('wp-draft-link');
|
||
if ((d.wp_preview_url || d.wp_edit_url) && linkBox) {
|
||
const links = [];
|
||
if (d.wp_edit_url) links.push(`<a href="${d.wp_edit_url}" target="_blank"
|
||
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
|
||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
|
||
✎ Im WP-Editor bearbeiten →</a>`);
|
||
if (d.wp_preview_url) links.push(`<a href="${d.wp_preview_url}" target="_blank"
|
||
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
|
||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||
👁 Vorschau ansehen</a>`);
|
||
linkBox.innerHTML = links.join('');
|
||
linkBox.classList.remove('hidden');
|
||
}
|
||
showToast('💾 Entwurf gespeichert & nach WordPress gepusht');
|
||
}
|
||
}
|
||
|
||
async function publishNow() {
|
||
if (!document.getElementById('article-title').value) { showToast('⚠️ Titel fehlt'); return; }
|
||
const r = await fetch('/api/article/schedule', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({
|
||
...getArticleData(),
|
||
status: 'scheduled',
|
||
post_date: new Date().toISOString().split('T')[0],
|
||
post_time: new Date().toTimeString().slice(0,5),
|
||
})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) { currentArticleId = d.id; showToast('🚀 Wird sofort veröffentlicht!'); setTimeout(() => location.reload(), 2000); }
|
||
}
|
||
|
||
function toggleSchedulePanel() {
|
||
const p = document.getElementById('schedule-panel');
|
||
p.classList.toggle('hidden');
|
||
if (!p.classList.contains('hidden')) {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('schedule-date').value = today;
|
||
}
|
||
}
|
||
|
||
async function checkSlot() {
|
||
const date = document.getElementById('schedule-date').value;
|
||
const time = document.getElementById('schedule-time').value;
|
||
if (!date || !time) return;
|
||
const r = await fetch(`/api/slots/${date}`);
|
||
const d = await r.json();
|
||
const statusEl = document.getElementById('slot-status');
|
||
statusEl.classList.remove('hidden');
|
||
if (d.taken && d.taken.includes(time)) {
|
||
statusEl.className = 'text-xs mt-2 text-red-400';
|
||
statusEl.textContent = `❌ Slot ${date} ${time} bereits belegt`;
|
||
document.getElementById('schedule-confirm-btn').disabled = true;
|
||
} else {
|
||
statusEl.className = 'text-xs mt-2 text-green-400';
|
||
statusEl.textContent = `✅ Slot ${date} ${time} ist frei`;
|
||
document.getElementById('schedule-confirm-btn').disabled = false;
|
||
}
|
||
}
|
||
|
||
async function confirmSchedule() {
|
||
const post_date = document.getElementById('schedule-date').value;
|
||
const post_time = document.getElementById('schedule-time').value;
|
||
if (!post_date || !post_time) { showToast('⚠️ Datum und Uhrzeit wählen'); return; }
|
||
if (!document.getElementById('article-title').value) { showToast('⚠️ Titel fehlt'); return; }
|
||
|
||
const r = await fetch('/api/article/schedule', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({...getArticleData(), post_date, post_time})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
showToast(`📅 Eingeplant: ${post_date} ${post_time}`);
|
||
setTimeout(() => location.reload(), 1500);
|
||
} else {
|
||
showToast('❌ Fehler beim Einplanen');
|
||
}
|
||
}
|
||
|
||
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;
|
||
currentWpPostId = d.wp_post_id || null;
|
||
document.getElementById('article-title').value = d.title || '';
|
||
document.getElementById('article-content').value = d.content || '';
|
||
document.getElementById('source-input').value = d.source_url || '';
|
||
document.getElementById('seo-title').value = d.seo_title || '';
|
||
document.getElementById('seo-description').value = d.seo_description || '';
|
||
document.getElementById('focus-keyword').value = d.focus_keyword || '';
|
||
document.getElementById('featured-image').value = d.featured_image_url || '';
|
||
if (d.category_id) document.getElementById('category-select').value = d.category_id;
|
||
|
||
// WP-Links wiederherstellen wenn vorhanden
|
||
const linkBox = document.getElementById('wp-draft-link');
|
||
if (d.wp_post_id && linkBox) {
|
||
const wrap = document.querySelector('[data-wp-admin]');
|
||
const wpAdmin = wrap ? wrap.dataset.wpAdmin : '';
|
||
const wpBase = wrap ? wrap.dataset.wpUrl : '';
|
||
const links = [];
|
||
if (wpAdmin) links.push(`<a href="${wpAdmin}/wp-admin/post.php?post=${d.wp_post_id}&action=edit" target="_blank"
|
||
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
|
||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
|
||
✎ Im WP-Editor bearbeiten →</a>`);
|
||
if (wpBase) links.push(`<a href="${wpBase}/?p=${d.wp_post_id}&preview=true" target="_blank"
|
||
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
|
||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||
👁 Vorschau ansehen</a>`);
|
||
linkBox.innerHTML = links.join('');
|
||
linkBox.classList.remove('hidden');
|
||
} else if (linkBox) {
|
||
linkBox.classList.add('hidden');
|
||
}
|
||
|
||
updatePreview();
|
||
loadMirrorStatus(id);
|
||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||
}
|
||
|
||
// ── Drag & Drop ──
|
||
let dragId = null, dragTime = null;
|
||
|
||
function onDragStart(event, id, time) {
|
||
dragId = id;
|
||
dragTime = time;
|
||
event.dataTransfer.effectAllowed = 'move';
|
||
const el = document.getElementById(`plan-row-${id}`);
|
||
setTimeout(() => { if(el) el.style.opacity = '0.4'; }, 0);
|
||
}
|
||
function onDragEnd(event) {
|
||
if (dragId) {
|
||
const el = document.getElementById(`plan-row-${dragId}`);
|
||
if (el) el.style.opacity = '1';
|
||
}
|
||
document.querySelectorAll('.drop-zone').forEach(z => z.style.background = '');
|
||
}
|
||
async function onDrop(event, newDate) {
|
||
event.preventDefault();
|
||
document.querySelectorAll('.drop-zone').forEach(z => z.style.background = '');
|
||
if (!dragId) return;
|
||
const r = await fetch(`/api/article/${dragId}/reschedule`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({post_date: newDate, post_time: dragTime})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
showToast(`📅 Verschoben auf ${newDate}`);
|
||
setTimeout(() => location.reload(), 800);
|
||
} else {
|
||
showToast('❌ ' + (d.error || 'Fehler'));
|
||
}
|
||
dragId = null;
|
||
}
|
||
|
||
// ── Board: Umplanen ──
|
||
function openReschedule(id, date, time) {
|
||
document.querySelectorAll('[id^="rs-panel-"]').forEach(el => el.classList.add('hidden'));
|
||
document.getElementById(`rs-panel-${id}`).classList.remove('hidden');
|
||
}
|
||
function closeReschedule(id) {
|
||
document.getElementById(`rs-panel-${id}`).classList.add('hidden');
|
||
}
|
||
async function confirmReschedule(id) {
|
||
const date = document.getElementById(`rs-date-${id}`).value;
|
||
const time = document.getElementById(`rs-time-${id}`).value;
|
||
const r = await fetch(`/api/article/${id}/reschedule`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({post_date: date, post_time: time})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) { showToast(`✅ Umgeplant: ${date} ${time}`); setTimeout(() => location.reload(), 1200); }
|
||
else showToast('❌ ' + (d.error || 'Fehler'));
|
||
}
|
||
async function deleteArticle(id) {
|
||
if (!confirm('Artikel wirklich löschen?')) return;
|
||
const r = await fetch(`/api/article/${id}/delete`, {method: 'POST'});
|
||
const d = await r.json();
|
||
if (d.success) { showToast('🗑️ Gelöscht'); setTimeout(() => location.reload(), 1000); }
|
||
}
|
||
|
||
|
||
// ── Mirror-Status nach Publish anzeigen ──
|
||
async function loadMirrorStatus(articleId) {
|
||
try {
|
||
const r = await fetch(`/api/article/${articleId}/mirrors`);
|
||
const mirrors = await r.json();
|
||
const box = document.getElementById('mirror-status-box');
|
||
if (!mirrors.length) { box.classList.add('hidden'); return; }
|
||
box.innerHTML = '<div class="text-slate-500 mb-1">📡 Mirror-Status:</div>';
|
||
for (const m of mirrors) {
|
||
const ok = m.status === 'ok';
|
||
box.innerHTML += `<div class="flex items-center gap-2 ${ok ? 'text-green-400' : 'text-red-400'}">
|
||
${ok ? '✅' : '❌'} ${m.mirror_label}
|
||
${ok && m.mirror_url ? `<a href="${m.mirror_url}" target="_blank" class="underline text-blue-400">→ ansehen</a>` : ''}
|
||
${!ok && m.error ? `<span class="text-slate-500">(${m.error})</span>` : ''}
|
||
</div>`;
|
||
}
|
||
box.classList.remove('hidden');
|
||
} catch(e) {}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('schedule-date').value = today;
|
||
});
|
||
{% endblock %}
|