feat: extract reusable @mintel/payload-ai package

This commit is contained in:
2026-03-02 21:00:09 +01:00
parent 72556af24c
commit 80eefad5ea
15 changed files with 2943 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useDocumentInfo } from "@payloadcms/ui";
import { optimizePostText } from "../actions/optimizePost.js";
import { Button } from "@payloadcms/ui";
export function OptimizeButton() {
const [isOptimizing, setIsOptimizing] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isOptimizing) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Lexical Block-Optimierung läuft noch (dies dauert bis zu 45 Sekunden). Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isOptimizing]);
const { fields, setModified, replaceState } = useForm();
const { title } = useDocumentInfo();
const handleOptimize = async () => {
// ... gathering draftContent logic
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
if (!draftContent || draftContent.trim().length < 50) {
alert(
"Der Entwurf ist zu kurz. Bitte tippe zuerst ein paar Stichpunkte oder einen Rohling ein.",
);
return;
}
setIsOptimizing(true);
try {
// 2. We inject the title so the AI knows what it's writing about
const payloadText = `---\ntitle: "${title}"\n---\n\n${draftContent}`;
const response = await optimizePostText(payloadText, instructions);
if (response.success && response.lexicalAST) {
// 3. Inject the new Lexical AST directly into the field form state
// We use Payload's useForm hook replacing the value of the 'content' field.
replaceState({
...fields,
content: {
...fields.content,
value: response.lexicalAST,
initialValue: response.lexicalAST,
},
});
setModified(true);
alert(
"🎉 Artikel wurde erfolgreich von der AI optimiert und mit Lexical Components angereichert.",
);
} else {
alert("❌ Fehler: " + response.error);
}
} catch (error) {
console.error("Optimization failed:", error);
alert("Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setIsOptimizing(false);
}
};
return (
<div className="mb-8 p-4 bg-slate-50 border border-slate-200 rounded-md">
<h3 className="text-sm font-semibold mb-2">AI Post Optimizer</h3>
<p className="text-xs text-slate-500 mb-4">
Lass Mintel AI deinen Text-Rohentwurf analysieren und automatisch in
einen voll formatierten Lexical Artikel mit passenden B2B Komponenten
(MemeCards, Mermaids) umwandeln.
</p>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale Anweisungen an die AI (z.B. 'schreibe etwas lockerer' oder 'fokussiere dich auf SEO')..."
disabled={isOptimizing}
style={{
width: "100%",
minHeight: "60px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "16px",
}}
/>
<button
type="button"
onClick={handleOptimize}
disabled={isOptimizing}
className="btn btn--icon-style-none btn--size-medium mt-4"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isOptimizing ? 0.7 : 1,
cursor: isOptimizing ? "not-allowed" : "pointer",
}}
>
<span className="btn__content" style={{ fontWeight: 600 }}>
{isOptimizing ? "✨ AI arbeitet (ca 30s)..." : "✨ Jetzt optimieren"}
</span>
</button>
</div>
);
}