New 'erinnerung' topic for manual Lyra community posts that gently remind users they can add optional, anonymous profile details. Wording stays jargon-free (no 'DiGA'/'data'/'study'). Manual-only, not in the auto-cron catalog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
299 lines
10 KiB
Vue
299 lines
10 KiB
Vue
<template>
|
|
<div>
|
|
<div class="mb-8">
|
|
<h1 class="text-xl font-semibold text-white mb-1">Lyra-Posts</h1>
|
|
<p class="text-sm text-gray-500">
|
|
Community-Posts als Lyra oder ReBreak-Account erstellen (KI-generiert oder manuell).
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Author Selection -->
|
|
<div class="mb-6">
|
|
<p class="text-xs text-gray-500 mb-2 uppercase tracking-wide font-medium">Als wer posten?</p>
|
|
<div class="grid grid-cols-2 gap-3 max-w-sm">
|
|
<button
|
|
class="flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors text-left"
|
|
:class="
|
|
author === 'lyra'
|
|
? 'border-purple-700 bg-purple-950/40'
|
|
: 'border-gray-800 bg-gray-900 hover:border-gray-700'
|
|
"
|
|
@click="selectAuthor('lyra')"
|
|
>
|
|
<div class="w-8 h-8 rounded-full bg-purple-950/60 border border-purple-800/50 flex items-center justify-center shrink-0">
|
|
<img v-if="lyraAvatar" :src="lyraAvatar" alt="Lyra" class="w-8 h-8 rounded-full object-cover" />
|
|
<UIcon v-else name="heroicons:cpu-chip" class="h-4 w-4 text-purple-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-semibold text-white leading-tight">{{ lyraNickname }}</p>
|
|
<p class="text-xs text-gray-500">KI-Coach</p>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
class="flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors text-left"
|
|
:class="
|
|
author === 'rebreak'
|
|
? 'border-blue-700 bg-blue-950/40'
|
|
: 'border-gray-800 bg-gray-900 hover:border-gray-700'
|
|
"
|
|
@click="selectAuthor('rebreak')"
|
|
>
|
|
<div class="w-8 h-8 rounded-full bg-blue-950/60 border border-blue-800/50 flex items-center justify-center shrink-0">
|
|
<img v-if="rebreakAvatar" :src="rebreakAvatar" alt="ReBreak" class="w-8 h-8 rounded-full object-cover" />
|
|
<UIcon v-else name="heroicons:shield-check" class="h-4 w-4 text-blue-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-semibold text-white leading-tight">{{ rebreakNickname }}</p>
|
|
<p class="text-xs text-gray-500">Offiziell</p>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Topic Selection -->
|
|
<div class="mb-6">
|
|
<p class="text-xs text-gray-500 mb-2 uppercase tracking-wide font-medium">Thema (fuer KI-Generierung)</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
v-for="t in lyraTopics"
|
|
:key="t.value"
|
|
class="px-3 py-1.5 rounded-full text-xs font-medium transition-colors border"
|
|
:class="
|
|
lyraTopic === t.value
|
|
? 'bg-purple-600 border-purple-600 text-white'
|
|
: 'bg-gray-900 border-gray-700 text-gray-400 hover:border-gray-600 hover:text-gray-300'
|
|
"
|
|
@click="lyraTopic = t.value"
|
|
>
|
|
{{ t.emoji }} {{ t.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context + Generate -->
|
|
<div class="flex gap-3 items-end mb-6">
|
|
<div class="flex-1">
|
|
<label class="text-xs text-gray-500 uppercase tracking-wide font-medium mb-1.5 block">
|
|
Optionaler Kontext
|
|
</label>
|
|
<input
|
|
v-model="lyraContext"
|
|
placeholder="z.B. 'DNS-Blocker Update verfuegbar'"
|
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-gray-600 transition-colors"
|
|
/>
|
|
</div>
|
|
<UButton
|
|
:loading="lyraGenerating"
|
|
:disabled="lyraPosting"
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="heroicons:sparkles"
|
|
@click="generateContent"
|
|
>
|
|
Generieren
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Editable Content -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<label class="text-xs text-gray-500 uppercase tracking-wide font-medium">
|
|
Post-Text
|
|
<span class="text-gray-600 normal-case font-normal ml-1">(direkt editierbar)</span>
|
|
</label>
|
|
<button
|
|
v-if="lyraContent"
|
|
class="text-xs text-gray-600 hover:text-gray-400 transition-colors"
|
|
@click="lyraContent = ''"
|
|
>
|
|
leeren
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
v-model="lyraContent"
|
|
placeholder="Text hier eingeben oder via 'Generieren' erstellen..."
|
|
rows="5"
|
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 resize-none focus:outline-none focus:border-gray-600 transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div v-if="lyraContent.trim()" class="mb-6 rounded-lg border border-gray-800 bg-gray-900 p-5">
|
|
<p class="text-xs text-gray-600 uppercase tracking-wide font-semibold mb-4">Vorschau</p>
|
|
<div class="flex items-start gap-3">
|
|
<div class="w-9 h-9 rounded-full bg-gray-800 border border-gray-700 flex items-center justify-center shrink-0 overflow-hidden">
|
|
<img
|
|
v-if="currentAuthorAvatar"
|
|
:src="currentAuthorAvatar"
|
|
:alt="currentAuthorNickname"
|
|
class="w-9 h-9 object-cover"
|
|
/>
|
|
<UIcon
|
|
v-else
|
|
:name="author === 'rebreak' ? 'heroicons:shield-check' : 'heroicons:cpu-chip'"
|
|
class="h-4 w-4 text-gray-500"
|
|
/>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1.5">
|
|
<span class="text-sm font-semibold text-white">{{ currentAuthorNickname }}</span>
|
|
<span class="text-xs text-purple-400 bg-purple-950/60 px-1.5 py-0.5 rounded-full flex items-center gap-0.5">
|
|
<UIcon name="heroicons:cpu-chip" class="h-3 w-3" />
|
|
KI
|
|
</span>
|
|
</div>
|
|
<p class="text-sm text-gray-200 leading-relaxed whitespace-pre-wrap">{{ lyraContent }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div
|
|
v-if="lyraError"
|
|
class="mb-4 rounded-lg border border-red-900 bg-red-950/40 px-4 py-3 flex items-center gap-2"
|
|
>
|
|
<UIcon name="heroicons:exclamation-triangle" class="h-4 w-4 text-red-400 shrink-0" />
|
|
<p class="text-sm text-red-300">{{ lyraError }}</p>
|
|
</div>
|
|
|
|
<!-- Post Button -->
|
|
<UButton
|
|
block
|
|
:loading="lyraPosting"
|
|
:disabled="!lyraContent.trim() || lyraGenerating"
|
|
icon="heroicons:paper-airplane"
|
|
@click="postAsBot"
|
|
>
|
|
Als {{ currentAuthorNickname }} posten
|
|
</UButton>
|
|
|
|
<!-- Success -->
|
|
<div
|
|
v-if="lyraSuccess"
|
|
class="mt-4 rounded-lg border border-emerald-900 bg-emerald-950/40 p-4 flex items-start gap-3"
|
|
>
|
|
<UIcon name="heroicons:check-circle" class="h-5 w-5 text-emerald-400 shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="text-sm font-semibold text-emerald-300">Post veroeffentlicht</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">
|
|
Erscheint sofort in der Community unter dem Profil von {{ currentAuthorNickname }}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({ middleware: "admin-auth" });
|
|
|
|
const lyraTopics = [
|
|
{ value: "motivation", label: "Motivation", emoji: "💪" },
|
|
{ value: "tipp", label: "CBT-Tipp", emoji: "💡" },
|
|
{ value: "zitat", label: "Zitat", emoji: "📖" },
|
|
{ value: "witzig", label: "Witzig", emoji: "😄" },
|
|
{ value: "news", label: "News/Warnung", emoji: "⚠️" },
|
|
{ value: "feature", label: "Feature", emoji: "✨" },
|
|
{ value: "erinnerung", label: "DiGA-Erinnerung", emoji: "🙋" },
|
|
] as const;
|
|
|
|
const author = ref<"lyra" | "rebreak">("lyra");
|
|
const lyraTopic = ref<string>("motivation");
|
|
const lyraContext = ref("");
|
|
const lyraContent = ref("");
|
|
const lyraGenerating = ref(false);
|
|
const lyraPosting = ref(false);
|
|
const lyraSuccess = ref(false);
|
|
const lyraError = ref("");
|
|
|
|
const lyraNickname = ref("Lyra");
|
|
const lyraAvatar = ref<string | null>(null);
|
|
const rebreakNickname = ref("ReBreak");
|
|
const rebreakAvatar = ref<string | null>(null);
|
|
|
|
const currentAuthorNickname = computed(() =>
|
|
author.value === "rebreak" ? rebreakNickname.value : lyraNickname.value,
|
|
);
|
|
const currentAuthorAvatar = computed(() =>
|
|
author.value === "rebreak" ? rebreakAvatar.value : lyraAvatar.value,
|
|
);
|
|
|
|
async function loadProfile(a: "lyra" | "rebreak") {
|
|
try {
|
|
const profile = await $fetch<{ nickname: string; avatar: string | null }>(
|
|
`/api/admin/lyra-profile`,
|
|
{ query: { author: a } },
|
|
);
|
|
if (a === "lyra") {
|
|
lyraNickname.value = profile.nickname;
|
|
lyraAvatar.value = profile.avatar;
|
|
} else {
|
|
rebreakNickname.value = profile.nickname;
|
|
rebreakAvatar.value = profile.avatar;
|
|
}
|
|
} catch {
|
|
// Fallback-Namen bleiben gesetzt
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadProfile("lyra"), loadProfile("rebreak")]);
|
|
});
|
|
|
|
function selectAuthor(a: "lyra" | "rebreak") {
|
|
author.value = a;
|
|
lyraContent.value = "";
|
|
lyraSuccess.value = false;
|
|
lyraError.value = "";
|
|
}
|
|
|
|
async function generateContent() {
|
|
lyraError.value = "";
|
|
lyraSuccess.value = false;
|
|
lyraGenerating.value = true;
|
|
try {
|
|
const res = await $fetch<{ success: boolean; content: string }>(
|
|
"/api/admin/lyra-generate",
|
|
{
|
|
method: "POST",
|
|
body: {
|
|
author: author.value,
|
|
topic: lyraTopic.value,
|
|
context: lyraContext.value || undefined,
|
|
},
|
|
},
|
|
);
|
|
lyraContent.value = res.content;
|
|
} catch (err: any) {
|
|
lyraError.value =
|
|
err?.data?.statusMessage ?? err?.statusMessage ?? err?.message ?? "Fehler beim Generieren";
|
|
} finally {
|
|
lyraGenerating.value = false;
|
|
}
|
|
}
|
|
|
|
async function postAsBot() {
|
|
if (!lyraContent.value.trim()) return;
|
|
lyraError.value = "";
|
|
lyraSuccess.value = false;
|
|
lyraPosting.value = true;
|
|
try {
|
|
await $fetch("/api/admin/lyra-post", {
|
|
method: "POST",
|
|
body: {
|
|
author: author.value,
|
|
customContent: lyraContent.value.trim(),
|
|
},
|
|
});
|
|
lyraSuccess.value = true;
|
|
lyraContent.value = "";
|
|
lyraContext.value = "";
|
|
} catch (err: any) {
|
|
lyraError.value =
|
|
err?.data?.statusMessage ?? err?.statusMessage ?? err?.message ?? "Fehler beim Posten";
|
|
} finally {
|
|
lyraPosting.value = false;
|
|
}
|
|
}
|
|
</script>
|