diff --git a/packages/payload-ai/package.json b/packages/payload-ai/package.json index 4c33e36..6b7a8ca 100644 --- a/packages/payload-ai/package.json +++ b/packages/payload-ai/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/payload-ai", - "version": "1.9.10", + "version": "1.9.13", "description": "Reusable Payload CMS AI Extensions", "type": "module", "scripts": { diff --git a/packages/payload-ai/src/chatPlugin.ts b/packages/payload-ai/src/chatPlugin.ts index 6165dc9..c9ccab7 100644 --- a/packages/payload-ai/src/chatPlugin.ts +++ b/packages/payload-ai/src/chatPlugin.ts @@ -1,6 +1,8 @@ import type { Config, Plugin } from 'payload' import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js' import type { PayloadChatPluginConfig } from './types.js' +import { optimizePostEndpoint } from './endpoints/optimizeEndpoint.js' +import { generateSlugEndpoint, generateThumbnailEndpoint, generateSingleFieldEndpoint } from './endpoints/generateEndpoints.js' export const payloadChatPlugin = (pluginOptions: PayloadChatPluginConfig): Plugin => @@ -48,6 +50,26 @@ export const payloadChatPlugin = return Response.json({ message: "Chat endpoint active" }) }, }, + { + path: '/api/mintel-ai/optimize', + method: 'post', + handler: optimizePostEndpoint, + }, + { + path: '/api/mintel-ai/generate-slug', + method: 'post', + handler: generateSlugEndpoint, + }, + { + path: '/api/mintel-ai/generate-thumbnail', + method: 'post', + handler: generateThumbnailEndpoint, + }, + { + path: '/api/mintel-ai/generate-single-field', + method: 'post', + handler: generateSingleFieldEndpoint, + }, ] // 3. Inject Chat React Component into Admin UI diff --git a/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx b/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx index 9b45660..0ab9dca 100644 --- a/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx +++ b/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx @@ -2,8 +2,6 @@ import React, { useState } from "react"; import { useField, useDocumentInfo, useForm } from "@payloadcms/ui"; -import { generateSingleFieldAction } from "../../actions/generateField.js"; - export function AiFieldButton({ path, field }: { path: string; field: any }) { const [isGenerating, setIsGenerating] = useState(false); const [instructions, setInstructions] = useState(""); @@ -44,19 +42,26 @@ export function AiFieldButton({ path, field }: { path: string; field: any }) { ? field.admin.description : ""; - const res = await generateSingleFieldAction( - (title as string) || "", - draftContent, - fieldName, - fieldDescription, - instructions, - ); + const resData = await fetch("/api/api/mintel-ai/generate-single-field", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + documentTitle: (title as string) || "", + documentContent: draftContent, + fieldName, + fieldDescription, + instructions, + }), + }); + const res = await resData.json(); + if (res.success && res.text) { setValue(res.text); } else { alert("Fehler: " + res.error); } - } catch (e) { + } catch (e: any) { + console.error(e) alert("Fehler bei der Generierung."); } finally { setIsGenerating(false); diff --git a/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx b/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx index ee06472..36c7b3c 100644 --- a/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx +++ b/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx @@ -2,8 +2,6 @@ import React, { useState, useEffect } from "react"; import { useForm, useField } from "@payloadcms/ui"; -import { generateSlugAction } from "../../actions/generateField.js"; - export function GenerateSlugButton({ path }: { path: string }) { const [isGenerating, setIsGenerating] = useState(false); const [instructions, setInstructions] = useState(""); @@ -45,18 +43,24 @@ export function GenerateSlugButton({ path }: { path: string }) { setIsGenerating(true); try { - const res = await generateSlugAction( - title, - draftContent, - initialValue as string, - instructions, - ); + const resData = await fetch("/api/api/mintel-ai/generate-slug", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title, + draftContent, + oldSlug: initialValue as string, + instructions, + }), + }); + const res = await resData.json(); + if (res.success && res.slug) { setValue(res.slug); } else { alert("Fehler: " + res.error); } - } catch (e) { + } catch (e: any) { console.error(e); alert("Unerwarteter Fehler."); } finally { diff --git a/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx b/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx index bf2bcd5..63a7c67 100644 --- a/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx +++ b/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx @@ -2,8 +2,6 @@ import React, { useState, useEffect } from "react"; import { useForm, useField } from "@payloadcms/ui"; -import { generateThumbnailAction } from "../../actions/generateField.js"; - export function GenerateThumbnailButton({ path }: { path: string }) { const [isGenerating, setIsGenerating] = useState(false); const [instructions, setInstructions] = useState(""); @@ -45,17 +43,23 @@ export function GenerateThumbnailButton({ path }: { path: string }) { setIsGenerating(true); try { - const res = await generateThumbnailAction( - draftContent, - title, - instructions, - ); + const resData = await fetch("/api/api/mintel-ai/generate-thumbnail", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + draftContent, + title, + instructions, + }), + }); + const res = await resData.json(); + if (res.success && res.mediaId) { setValue(res.mediaId); } else { alert("Fehler: " + res.error); } - } catch (e) { + } catch (e: any) { console.error(e); alert("Unerwarteter Fehler."); } finally { diff --git a/packages/payload-ai/src/components/OptimizeButton.tsx b/packages/payload-ai/src/components/OptimizeButton.tsx index 849926d..6eec856 100644 --- a/packages/payload-ai/src/components/OptimizeButton.tsx +++ b/packages/payload-ai/src/components/OptimizeButton.tsx @@ -2,7 +2,6 @@ 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() { @@ -57,7 +56,12 @@ export function OptimizeButton() { // 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); + const res = await fetch("/api/api/mintel-ai/optimize", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ draftContent: payloadText, instructions }), + }); + const response = await res.json(); if (response.success && response.lexicalAST) { // 3. Inject the new Lexical AST directly into the field form state diff --git a/packages/payload-ai/src/actions/generateField.ts b/packages/payload-ai/src/endpoints/generateEndpoints.ts similarity index 66% rename from packages/payload-ai/src/actions/generateField.ts rename to packages/payload-ai/src/endpoints/generateEndpoints.ts index a7b9c6b..bd73f12 100644 --- a/packages/payload-ai/src/actions/generateField.ts +++ b/packages/payload-ai/src/endpoints/generateEndpoints.ts @@ -1,8 +1,4 @@ -"use server"; - -import { getPayloadHMR } from "@payloadcms/next/utilities"; -// @ts-ignore - dynamic config resolution from next.js payload plugin -import configPromise from "@payload-config"; +import { PayloadRequest } from "payload"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as os from "node:os"; @@ -30,13 +26,9 @@ async function getOrchestrator() { }); } -export async function generateSlugAction( - title: string, - draftContent: string, - oldSlug?: string, - instructions?: string, -) { +export const generateSlugEndpoint = async (req: PayloadRequest) => { try { + const { title, draftContent, oldSlug, instructions } = (await req.json?.() || {}) as any; const orchestrator = await getOrchestrator(); const newSlug = await orchestrator.generateSlug( draftContent, @@ -45,9 +37,8 @@ export async function generateSlugAction( ); if (oldSlug && oldSlug !== newSlug) { - const payload = await getPayloadHMR({ config: configPromise as any }); - await payload.create({ - collection: "redirects", + await req.payload.create({ + collection: "redirects" as any, data: { from: oldSlug, to: newSlug, @@ -55,42 +46,25 @@ export async function generateSlugAction( }); } - return { success: true, slug: newSlug }; + return Response.json({ success: true, slug: newSlug }); } catch (e: any) { - return { success: false, error: e.message }; + return Response.json({ success: false, error: e.message }, { status: 500 }); } } -export async function generateThumbnailAction( - draftContent: string, - title?: string, - instructions?: string, -) { +export const generateThumbnailEndpoint = async (req: PayloadRequest) => { try { - const payload = await getPayloadHMR({ config: configPromise as any }); + const { draftContent, title, instructions } = (await req.json?.() || {}) as any; const OPENROUTER_KEY = process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; const REPLICATE_KEY = process.env.REPLICATE_API_KEY; - if (!OPENROUTER_KEY) { - throw new Error("Missing OPENROUTER_API_KEY in .env"); - } - if (!REPLICATE_KEY) { - throw new Error( - "Missing REPLICATE_API_KEY in .env (Required for Thumbnails)", - ); - } + if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env"); + if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env"); - const importDynamic = new Function( - "modulePath", - "return import(modulePath)", - ); - const { AiBlogPostOrchestrator } = await importDynamic( - "@mintel/content-engine", - ); - const { ThumbnailGenerator } = await importDynamic( - "@mintel/thumbnail-generator", - ); + const importDynamic = new Function("modulePath", "return import(modulePath)"); + const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine"); + const { ThumbnailGenerator } = await importDynamic("@mintel/thumbnail-generator"); const orchestrator = new AiBlogPostOrchestrator({ apiKey: OPENROUTER_KEY, @@ -112,8 +86,8 @@ export async function generateThumbnailAction( const stat = await fs.stat(tmpPath); const fileName = path.basename(tmpPath); - const newMedia = await payload.create({ - collection: "media", + const newMedia = await req.payload.create({ + collection: "media" as any, data: { alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail", }, @@ -125,31 +99,24 @@ export async function generateThumbnailAction( }, }); - // Cleanup temp file await fs.unlink(tmpPath).catch(() => { }); - return { success: true, mediaId: newMedia.id }; + return Response.json({ success: true, mediaId: newMedia.id }); } catch (e: any) { - return { success: false, error: e.message }; + return Response.json({ success: false, error: e.message }, { status: 500 }); } } -export async function generateSingleFieldAction( - documentTitle: string, - documentContent: string, - fieldName: string, - fieldDescription: string, - instructions?: string, -) { + +export const generateSingleFieldEndpoint = async (req: PayloadRequest) => { try { + const { documentTitle, documentContent, fieldName, fieldDescription, instructions } = (await req.json?.() || {}) as any; + const OPENROUTER_KEY = process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY"); - const payload = await getPayloadHMR({ config: configPromise as any }); - - // Fetch context documents from DB - const contextDocsData = await payload.find({ - collection: "context-files", + const contextDocsData = await req.payload.find({ + collection: "context-files" as any, limit: 100, }); const projectContext = contextDocsData.docs @@ -184,8 +151,8 @@ CRITICAL RULES: }); const data = await res.json(); const text = data.choices?.[0]?.message?.content?.trim() || ""; - return { success: true, text }; + return Response.json({ success: true, text }); } catch (e: any) { - return { success: false, error: e.message }; + return Response.json({ success: false, error: e.message }, { status: 500 }); } } diff --git a/packages/payload-ai/src/actions/optimizePost.ts b/packages/payload-ai/src/endpoints/optimizeEndpoint.ts similarity index 53% rename from packages/payload-ai/src/actions/optimizePost.ts rename to packages/payload-ai/src/endpoints/optimizeEndpoint.ts index 2791d0d..71e5646 100644 --- a/packages/payload-ai/src/actions/optimizePost.ts +++ b/packages/payload-ai/src/endpoints/optimizeEndpoint.ts @@ -1,17 +1,15 @@ -"use server"; +import { PayloadRequest } from 'payload' +import { parseMarkdownToLexical } from "../utils/lexicalParser.js"; -import { parseMarkdownToLexical } from "../utils/lexicalParser"; -import { getPayloadHMR } from "@payloadcms/next/utilities"; -// @ts-ignore - dynamic config resolution from next.js payload plugin -import configPromise from "@payload-config"; - -export async function optimizePostText( - draftContent: string, - instructions?: string, -) { +export const optimizePostEndpoint = async (req: PayloadRequest) => { try { - const payload = await getPayloadHMR({ config: configPromise as any }); - const globalAiSettings = (await payload.findGlobal({ slug: "ai-settings" })) as any; + const { draftContent, instructions } = (await req.json?.() || {}) as { draftContent: string; instructions?: string }; + + if (!draftContent) { + return Response.json({ error: 'Missing draftContent' }, { status: 400 }) + } + + const globalAiSettings = (await req.payload.findGlobal({ slug: "ai-settings" })) as any; const customSources = globalAiSettings?.customSources?.map((s: any) => s.sourceName) || []; @@ -20,18 +18,12 @@ export async function optimizePostText( const REPLICATE_KEY = process.env.REPLICATE_API_KEY; if (!OPENROUTER_KEY) { - throw new Error( - "OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.", - ); + return Response.json({ error: "OPENROUTER_KEY not found in environment." }, { status: 500 }) } - const importDynamic = new Function( - "modulePath", - "return import(modulePath)", - ); - const { AiBlogPostOrchestrator } = await importDynamic( - "@mintel/content-engine", - ); + // Dynamically import to avoid bundling it into client components that might accidentally import this file + const importDynamic = new Function("modulePath", "return import(modulePath)"); + const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine"); const orchestrator = new AiBlogPostOrchestrator({ apiKey: OPENROUTER_KEY, @@ -39,9 +31,8 @@ export async function optimizePostText( model: "google/gemini-3-flash-preview", }); - // Fetch context documents purely from DB - const contextDocsData = await payload.find({ - collection: "context-files", + const contextDocsData = await req.payload.find({ + collection: "context-files" as any, limit: 100, }); const projectContext = contextDocsData.docs.map((doc: any) => doc.content); @@ -49,19 +40,19 @@ export async function optimizePostText( const optimizedMarkdown = await orchestrator.optimizeDocument({ content: draftContent, projectContext, - availableComponents: [], // Removed hardcoded config.components dependency + availableComponents: [], instructions, internalLinks: [], customSources, }); if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") { - throw new Error("AI returned invalid markup."); + return Response.json({ error: "AI returned invalid markup." }, { status: 500 }) } const blocks = parseMarkdownToLexical(optimizedMarkdown); - return { + return Response.json({ success: true, lexicalAST: { root: { @@ -73,12 +64,12 @@ export async function optimizePostText( direction: "ltr", }, }, - }; + }) } catch (error: any) { - console.error("Failed to optimize post:", error); - return { + console.error("Failed to optimize post in endpoint:", error); + return Response.json({ success: false, error: error.message || "An unknown error occurred during optimization.", - }; + }, { status: 500 }) } } diff --git a/packages/payload-ai/src/index.ts b/packages/payload-ai/src/index.ts index 75a0754..a6750a6 100644 --- a/packages/payload-ai/src/index.ts +++ b/packages/payload-ai/src/index.ts @@ -4,8 +4,6 @@ */ export * from './globals/AiSettings'; -export * from './actions/generateField'; -export * from './actions/optimizePost'; export * from './components/FieldGenerators/AiFieldButton'; export * from './components/AiMediaButtons'; export * from './components/OptimizeButton'; diff --git a/packages/payload-ai/src/types.d.ts b/packages/payload-ai/src/types.d.ts index dc58ac9..ad7f8ed 100644 --- a/packages/payload-ai/src/types.d.ts +++ b/packages/payload-ai/src/types.d.ts @@ -1,5 +1,8 @@ -declare module "@payload-config" { - import { Config } from "payload"; - const configPromise: Promise; - export default configPromise; +export type PayloadChatPluginConfig = { + enabled?: boolean + /** Render the chat bubble on the bottom right? Defaults to true */ + renderChatBubble?: boolean + allowedCollections?: string[] + mcpServers?: any[] } +