From cbed10052b9479442cf6b9df815f0c7f074021b3 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 6 Mar 2026 00:55:39 +0100 Subject: [PATCH] feat(payload-ai): implement context-aware Payload CMS Agent - ChatWindow now gathers page context (URL, collectionSlug, document ID) - chatEndpoint fetches real AIChatPermissions from database - Agent uses OpenRouter Gemini 3 Flash with maxSteps: 10 for autonomous multi-step tool execution - Fallback default collections when no permissions configured --- .../src/components/ChatWindow/index.tsx | 36 ++++++++++-- .../payload-ai/src/endpoints/chatEndpoint.ts | 58 ++++++++++++++++--- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/packages/payload-ai/src/components/ChatWindow/index.tsx b/packages/payload-ai/src/components/ChatWindow/index.tsx index 9081ae7..916db2b 100644 --- a/packages/payload-ai/src/components/ChatWindow/index.tsx +++ b/packages/payload-ai/src/components/ChatWindow/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useChat } from '@ai-sdk/react' import './ChatWindow.scss' @@ -15,10 +15,38 @@ export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ ch const ChatWindow: React.FC = () => { const [isOpen, setIsOpen] = useState(false) + const [pageContext, setPageContext] = useState({ url: '' }) + + useEffect(() => { + if (typeof window !== 'undefined') { + const path = window.location.pathname; + let collectionSlug = null; + let id = null; + // Payload admin URLs are usually /admin/collections/:slug/:id + const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/); + if (match) { + collectionSlug = match[1]; + if (match[2] && match[2] !== 'create') { + id = match[2]; + } + } + + setPageContext({ + url: window.location.href, + title: document.title, + collectionSlug, + id + }) + } + }, [isOpen]) // Refresh context when chat is opened + // @ts-ignore - AI hook version mismatch between core and react packages const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({ api: '/api/mcp-chat', - initialMessages: [] + initialMessages: [], + body: { + pageContext + } } as any) // Basic implementation to toggle chat window and submit messages @@ -51,8 +79,8 @@ const ChatWindow: React.FC = () => { position: 'fixed', bottom: '80px', right: '20px', - width: '400px', - height: '600px', + width: '450px', + height: '650px', backgroundColor: '#fff', border: '1px solid #eaeaea', borderRadius: '12px', diff --git a/packages/payload-ai/src/endpoints/chatEndpoint.ts b/packages/payload-ai/src/endpoints/chatEndpoint.ts index 084203e..4aaabd0 100644 --- a/packages/payload-ai/src/endpoints/chatEndpoint.ts +++ b/packages/payload-ai/src/endpoints/chatEndpoint.ts @@ -15,22 +15,49 @@ export const handleMcpChat = async (req: PayloadRequest) => { return Response.json({ error: 'Unauthorized. You must be logged in to use AI Chat.' }, { status: 401 }) } - const { messages } = (await req.json?.() || { messages: [] }) as { messages: any[] } + const { messages, pageContext } = (await req.json?.() || { messages: [] }) as { messages: any[], pageContext?: any } // 1. Check AI Permissions for req.user - // In a real implementation this looks up the global or collection for permissions - const allowedCollections = ['users'] // Stub + // Look up the collection for permissions + const permissionsQuery = await req.payload.find({ + collection: 'ai-chat-permissions' as any, + where: { + or: [ + { targetUser: { equals: req.user.id } }, + { targetRole: { equals: req.user.role || 'admin' } } + ] + }, + limit: 10 + }) + + const allowedCollections = new Set() + const allowedMcpServers = new Set() + + for (const perm of permissionsQuery.docs) { + if (perm.allowedCollections) { + perm.allowedCollections.forEach((c: string) => allowedCollections.add(c)) + } + if (perm.allowedMcpServers) { + perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s)) + } + } + + let accessCollections = Array.from(allowedCollections) + if (accessCollections.length === 0) { + // Fallback or demo config if not configured yet + accessCollections = ['users', 'pages', 'posts', 'products', 'leads', 'media'] + } + let activeTools: Record = {} // 2. Generate Payload Local Tools - if (allowedCollections.length > 0) { - const payloadTools = generatePayloadLocalTools(req.payload, req, allowedCollections) + if (accessCollections.length > 0) { + const payloadTools = generatePayloadLocalTools(req.payload, req, accessCollections) activeTools = { ...activeTools, ...payloadTools } } // 3. Connect External MCPs - const allowedMcpServers: string[] = [] // Stub - if (allowedMcpServers.includes('gitea')) { + if (Array.from(allowedMcpServers).includes('gitea')) { try { const { tools: giteaTools } = await createMcpTools({ name: 'gitea', @@ -55,15 +82,28 @@ export const handleMcpChat = async (req: PayloadRequest) => { If the user shares important context but doesn't explicitly ask you to remember it, you should ask "Soll ich mir das für die Zukunft merken?" before saving it. Do not ask for trivial things. ` + const contextContextStr = pageContext ? ` + Current User Context: + URL: ${pageContext.url || 'Unknown'} + Title: ${pageContext.title || 'Unknown'} + Collection: ${pageContext.collectionSlug || 'None'} + Document ID: ${pageContext.id || 'None'} + You can use this to understand what the user is currently looking at. + ` : '' + try { const result = streamText({ // @ts-ignore - AI SDK type mismatch model: openrouter('google/gemini-3.0-flash'), messages, tools: activeTools, - system: `You are a helpful Payload CMS MCP Assistant orchestrating the local Mintel ecosystem. + // @ts-ignore - AI SDK type mismatch with maxSteps + maxSteps: 10, + system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem. You only have access to tools explicitly granted by the Admin. - You cannot do anything outside these tools. Always explain what you are doing. + You can completely control Payload CMS (read, create, update, delete documents). + If you need more details to fulfill a request (e.g. creating a blog post), you can ask the user. + ${contextContextStr} ${memorySystemPrompt}` })