- DB-Migration: UNIQUE(date) → UNIQUE(date, post_time) — alte DBs werden automatisch beim Start migriert (database.py init_db) - api_save: gibt article_id zurück für nachgelagerte Operationen - confirmPlan(): speichert auf selectedDate, verschiebt dann ggf. per reschedule auf Zieldatum — fixes "Kein Artikel für diesen Tag vorhanden" - Alle Source-Dateien (app.py, database.py, templates, ...) hinzugefügt - arakava-news: cursor-memory-system Artikel + SVG-Diagramm hinzugefügt Made-with: Cursor
224 lines
9.4 KiB
HTML
224 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Prompts — FünfVorAcht</title>
|
|
<link rel="stylesheet" href="/static/tailwind.min.css">
|
|
<style>
|
|
body { background: #0f172a; color: #e2e8f0; }
|
|
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
|
|
.tg-preview { background: #212d3b; border-left: 3px solid #3b82f6; font-family: system-ui; white-space: pre-wrap; line-height: 1.6; }
|
|
textarea { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; border-radius: 8px; resize: vertical; }
|
|
textarea:focus { outline: none; border-color: #3b82f6; }
|
|
input[type=text] { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; border-radius: 8px; }
|
|
input[type=text]:focus { outline: none; border-color: #3b82f6; }
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen">
|
|
|
|
<nav class="bg-slate-900 border-b border-slate-700 px-6 py-4 flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center gap-1 bg-slate-800 border border-slate-700 rounded-lg p-1">
|
|
<span class="flex items-center gap-1.5 bg-slate-700 text-white text-xs font-semibold px-3 py-1.5 rounded-md">🕗 FünfVorAcht</span>
|
|
<a href="https://redakteur.orbitalo.net" target="_blank"
|
|
class="flex items-center gap-1.5 text-slate-500 hover:text-slate-200 hover:bg-slate-700 text-xs font-medium px-3 py-1.5 rounded-md transition">📝 Redakteur</a>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-4 text-sm">
|
|
<a href="/" class="text-slate-400 hover:text-white">Studio</a>
|
|
<a href="/history" class="text-slate-400 hover:text-white">History</a>
|
|
<a href="/prompts" class="text-blue-400 font-semibold">Prompts</a>
|
|
<a href="/settings" class="text-slate-400 hover:text-white">Einstellungen</a>
|
|
<a href="/hilfe" class="text-slate-400 hover:text-white">❓ Hilfe</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="max-w-6xl mx-auto px-6 py-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold text-white">🧠 Prompt-Bibliothek</h1>
|
|
<button onclick="showNewPrompt()"
|
|
class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm transition">
|
|
+ Neuer Prompt
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
<!-- Prompt Liste -->
|
|
<div class="space-y-4">
|
|
{% for p in prompts %}
|
|
<div class="card p-4 cursor-pointer hover:border-blue-500 transition {% if p.is_default %}border-green-600{% endif %}"
|
|
onclick="loadPrompt({{ p.id }}, {{ p.system_prompt|tojson }}, {{ p.name|tojson }}, {{ p.test_result|tojson if p.test_result else 'null' }}, {{ p.is_default }})">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-semibold text-white">{{ p.name }}</span>
|
|
{% if p.is_default %}
|
|
<span class="bg-green-800 text-green-300 text-xs px-2 py-0.5 rounded-full">Standard</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if not p.is_default %}
|
|
<form action="/prompts/delete/{{ p.id }}" method="POST" onclick="event.stopPropagation()" onsubmit="return confirm('Prompt «{{ p.name }}» wirklich löschen?')">
|
|
<button type="submit" class="text-red-400 hover:text-red-300 text-xs px-2 py-1 rounded hover:bg-red-900/30 transition">🗑</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-xs text-slate-400 truncate">{{ p.system_prompt[:100] }}…</div>
|
|
{% if p.last_tested_at %}
|
|
<div class="text-xs text-slate-500 mt-1">Zuletzt getestet: {{ p.last_tested_at[:16] }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Editor + Preview -->
|
|
<div class="space-y-4">
|
|
<div class="card p-4" id="editor-card">
|
|
<h2 class="text-white font-semibold mb-4" id="editor-title">Prompt auswählen</h2>
|
|
<input type="hidden" id="prompt-id">
|
|
|
|
<div class="mb-3">
|
|
<label class="text-xs text-slate-400 mb-1 block">Name</label>
|
|
<input type="text" id="prompt-name" class="w-full px-3 py-2 text-sm" placeholder="z.B. Standard">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="text-xs text-slate-400 mb-1 block">
|
|
System-Prompt
|
|
<span class="text-slate-600 ml-2">Variablen: {source} {date} {tag}</span>
|
|
</label>
|
|
<textarea id="prompt-text" class="w-full px-3 py-2 text-sm" rows="12" placeholder="Prompt hier eingeben…"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="text-xs text-slate-400 mb-1 block">Test-Quelle (URL oder Text)</label>
|
|
<input type="text" id="test-source" class="w-full px-3 py-2 text-sm"
|
|
value="https://tagesschau.de" placeholder="URL oder Text zum Testen">
|
|
</div>
|
|
|
|
<div class="flex gap-2 flex-wrap">
|
|
<button onclick="testPrompt()"
|
|
class="bg-yellow-700 hover:bg-yellow-600 text-white px-4 py-2 rounded-lg text-sm transition">
|
|
🧪 Testen
|
|
</button>
|
|
<button onclick="savePrompt()"
|
|
class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm transition">
|
|
💾 Speichern
|
|
</button>
|
|
<button onclick="setDefault()"
|
|
class="bg-green-700 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-sm transition">
|
|
⭐ Als Standard
|
|
</button>
|
|
<button onclick="deleteCurrentPrompt()" id="btn-delete"
|
|
class="bg-red-800 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm transition ml-auto"
|
|
style="display:none">
|
|
🗑 Löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test-Preview -->
|
|
<div class="card p-4" id="preview-card" style="display:none">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-white font-semibold">📱 Telegram-Vorschau</h3>
|
|
<span class="text-xs text-slate-500" id="char-count"></span>
|
|
</div>
|
|
<div id="tg-preview" class="tg-preview rounded-lg p-4 text-sm text-slate-200"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentPromptId = null;
|
|
let currentIsDefault = false;
|
|
|
|
function loadPrompt(id, text, name, testResult, isDefault) {
|
|
currentPromptId = id;
|
|
currentIsDefault = !!isDefault;
|
|
document.getElementById('prompt-id').value = id;
|
|
document.getElementById('prompt-name').value = name;
|
|
document.getElementById('prompt-text').value = text;
|
|
document.getElementById('editor-title').textContent = `Bearbeiten: ${name}`;
|
|
document.getElementById('btn-delete').style.display = isDefault ? 'none' : 'block';
|
|
if (testResult) {
|
|
showPreview(testResult);
|
|
}
|
|
}
|
|
|
|
function showNewPrompt() {
|
|
currentPromptId = null;
|
|
currentIsDefault = false;
|
|
document.getElementById('prompt-id').value = '';
|
|
document.getElementById('prompt-name').value = '';
|
|
document.getElementById('prompt-text').value = '';
|
|
document.getElementById('editor-title').textContent = 'Neuer Prompt';
|
|
document.getElementById('btn-delete').style.display = 'none';
|
|
}
|
|
|
|
async function deleteCurrentPrompt() {
|
|
if (!currentPromptId) return;
|
|
if (currentIsDefault) return alert('Standard-Prompt kann nicht gelöscht werden');
|
|
const name = document.getElementById('prompt-name').value;
|
|
if (!confirm(`Prompt «${name}» wirklich löschen?`)) return;
|
|
await fetch(`/prompts/delete/${currentPromptId}`, {method: 'POST'});
|
|
location.reload();
|
|
}
|
|
|
|
async function testPrompt() {
|
|
const system_prompt = document.getElementById('prompt-text').value;
|
|
const source = document.getElementById('test-source').value;
|
|
const btn = event.target;
|
|
btn.textContent = '⏳ Generiere...';
|
|
btn.disabled = true;
|
|
try {
|
|
const r = await fetch('/prompts/test', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({system_prompt, source, tag: 'Politik', prompt_id: currentPromptId})
|
|
});
|
|
const d = await r.json();
|
|
if (d.success) {
|
|
showPreview(d.result);
|
|
} else {
|
|
alert('Fehler: ' + d.error);
|
|
}
|
|
} catch(e) {
|
|
alert('Verbindungsfehler: ' + e);
|
|
} finally {
|
|
btn.textContent = '🧪 Testen';
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function showPreview(text) {
|
|
const preview = document.getElementById('tg-preview');
|
|
const card = document.getElementById('preview-card');
|
|
// Render Telegram HTML tags visually
|
|
let html = text
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/<b>(.*?)<\/b>/gs, '<strong>$1</strong>')
|
|
.replace(/<i>(.*?)<\/i>/gs, '<em>$1</em>')
|
|
.replace(/<a href="(.*?)">(.*?)<\/a>/gs, '<a href="$1" class="text-blue-400 underline">$2</a>');
|
|
preview.innerHTML = html;
|
|
document.getElementById('char-count').textContent = `${text.length} Zeichen ${text.length > 4096 ? '⚠️ ZU LANG' : '✓'}`;
|
|
card.style.display = 'block';
|
|
}
|
|
|
|
async function savePrompt() {
|
|
const form = new FormData();
|
|
form.append('id', document.getElementById('prompt-id').value);
|
|
form.append('name', document.getElementById('prompt-name').value);
|
|
form.append('system_prompt', document.getElementById('prompt-text').value);
|
|
const r = await fetch('/prompts/save', {method: 'POST', body: form});
|
|
location.reload();
|
|
}
|
|
|
|
async function setDefault() {
|
|
if (!currentPromptId) return alert('Zuerst Prompt auswählen');
|
|
const r = await fetch(`/prompts/default/${currentPromptId}`, {method: 'POST'});
|
|
location.reload();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|