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,163 @@
"use client";
import React, { useState } from "react";
import { useDocumentInfo, toast } from "@payloadcms/ui";
type Action = "upscale" | "recover";
interface ActionState {
loading: boolean;
resultId?: string | number;
}
export const AiMediaButtons: React.FC = () => {
const { id } = useDocumentInfo();
const [upscale, setUpscale] = useState<ActionState>({ loading: false });
const [recover, setRecover] = useState<ActionState>({ loading: false });
if (!id) return null; // Only show on existing documents
const runAction = async (action: Action) => {
const setter = action === "upscale" ? setUpscale : setRecover;
setter({ loading: true });
const label = action === "upscale" ? "AI Upscale" : "AI Recover";
toast.info(
`${label} started this can take 3090 seconds, please wait…`,
);
try {
// The API path is hardcoded here and assuming that's where the host app registers the endpoint.
const response = await fetch(`/api/media/${id}/ai-process`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `${label} failed`);
}
setter({ loading: false, resultId: result.mediaId });
toast.success(
`${label} erfolgreich! Neues Bild (ID: ${result.mediaId}) wurde gespeichert.`,
);
} catch (err: any) {
console.error(`[AiMediaButtons] ${action} error:`, err);
toast.error(
err instanceof Error ? err.message : `${label} fehlgeschlagen`,
);
setter({ loading: false });
}
};
const buttonStyle: React.CSSProperties = {
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
padding: "8px 14px",
borderRadius: "4px",
fontSize: "13px",
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
gap: "6px",
transition: "opacity 0.15s ease",
};
const disabledStyle: React.CSSProperties = {
opacity: 0.55,
cursor: "not-allowed",
};
return (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "10px",
marginBottom: "1.5rem",
marginTop: "0.5rem",
}}
>
{/* AI Upscale */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<button
type="button"
disabled={upscale.loading || recover.loading}
onClick={() => runAction("upscale")}
style={{
...buttonStyle,
...(upscale.loading || recover.loading ? disabledStyle : { cursor: "pointer" }),
}}
>
{upscale.loading ? "⏳ AI Upscale läuft…" : "✨ AI Upscale"}
</button>
{upscale.resultId && (
<a
href={`/admin/collections/media/${upscale.resultId}`}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "12px",
color: "var(--theme-elevation-500)",
textDecoration: "underline",
}}
>
Neues Bild öffnen (ID: {upscale.resultId})
</a>
)}
</div>
{/* AI Recover */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<button
type="button"
disabled={upscale.loading || recover.loading}
onClick={() => runAction("recover")}
style={{
...buttonStyle,
...(upscale.loading || recover.loading ? disabledStyle : { cursor: "pointer" }),
}}
>
{recover.loading ? "⏳ AI Recover läuft…" : "🔄 AI Recover"}
</button>
{recover.resultId && (
<a
href={`/admin/collections/media/${recover.resultId}`}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "12px",
color: "var(--theme-elevation-500)",
textDecoration: "underline",
}}
>
Neues Bild öffnen (ID: {recover.resultId})
</a>
)}
</div>
<p
style={{
width: "100%",
fontSize: "0.8rem",
color: "var(--theme-elevation-500)",
margin: 0,
lineHeight: 1.4,
}}
>
<strong>AI Upscale</strong> verbessert die Auflösung via{" "}
<code>google/upscaler</code>. <strong>AI Recover</strong> restauriert
alte/beschädigte Fotos via{" "}
<code>microsoft/bringing-old-photos-back-to-life</code>. Das
Ergebnis wird als neues Medium gespeichert.
</p>
</div>
);
};