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
This commit is contained in:
2026-03-06 00:55:39 +01:00
parent 560213680c
commit cbed10052b
2 changed files with 81 additions and 13 deletions

View File

@@ -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<any>({ 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',

View File

@@ -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<string>()
const allowedMcpServers = new Set<string>()
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<string, any> = {}
// 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}`
})