diff --git a/src/ai/models/openai/index.ts b/src/ai/models/openai/index.ts
index 946d74d..11c4a40 100644
--- a/src/ai/models/openai/index.ts
+++ b/src/ai/models/openai/index.ts
@@ -23,12 +23,6 @@ export const OpenAIConfig: GenerationConfig = {
prompt: string,
options: { locale: string; model: string; system: string },
) => {
- // const finalPrompt = `Output language code: ${options.locale}
- // Prompt: ${prompt}
- // Output:
- // `
-
- // console.log('finalPrompt: ', finalPrompt)
const streamTextResult = await streamText({
model: openai(options.model),
prompt,
@@ -192,7 +186,6 @@ export const OpenAIConfig: GenerationConfig = {
name: 'OpenAI GPT',
fields: ['richText'],
handler: (text: string, options) => {
- //TODO: change it to open ai text to speech api
return generateRichText(text, options)
},
output: 'text',
@@ -216,10 +209,10 @@ export const OpenAIConfig: GenerationConfig = {
name: 'system',
type: 'textarea',
defaultValue: `INSTRUCTIONS:
- You are a highly skilled and professional blog writer,
- renowned for crafting engaging and well-organized articles.
- When given a title, you meticulously create blogs that are not only
- informative and accurate but also captivating and beautifully structured.`,
+You are a highly skilled and professional blog writer,
+renowned for crafting engaging and well-organized articles.
+When given a title, you meticulously create blogs that are not only
+informative and accurate but also captivating and beautifully structured.`,
label: 'System prompt',
},
{
@@ -231,11 +224,11 @@ export const OpenAIConfig: GenerationConfig = {
name: 'layout',
type: 'textarea',
defaultValue: `[paragraph] - A short introduction to the topic.
- [horizontalrule]
- [list] - A section with headings and a paragraph.
- [horizontalrule]
- [paragraph] - A short conclusion.
- [quote] - A quote from a famous person based on the topic.
+[horizontalrule]
+[list] - A section with headings and a paragraph.
+[horizontalrule]
+[paragraph] - A short conclusion.
+[quote] - A quote from a famous person based on the topic.
`,
label: 'Layout',
},
diff --git a/src/collections/Instructions.ts b/src/collections/Instructions.ts
index 045bf10..1589209 100644
--- a/src/collections/Instructions.ts
+++ b/src/collections/Instructions.ts
@@ -1,6 +1,7 @@
import type { CollectionConfig, GroupField } from 'payload'
import { GenerationModels } from '../ai/models/index.js'
+import { PLUGIN_INSTRUCTIONS_TABLE } from '../defaults.js'
import { PromptEditorField } from '../fields/PromptEditorField/PromptEditorField.js'
import { SelectField } from '../fields/SelectField/SelectField.js'
@@ -20,12 +21,17 @@ const modelOptions = GenerationModels.map((model) => {
})
export const Instructions: CollectionConfig = {
- slug: 'instructions',
+ slug: PLUGIN_INSTRUCTIONS_TABLE,
+
+ // TODO: Revisit permissions, better if end user can provide this
access: {
create: () => true,
read: () => true,
update: () => true,
},
+ admin: {
+ hidden: true,
+ },
fields: [
{
name: 'schema-path',
diff --git a/src/defaults.ts b/src/defaults.ts
index 4188ae5..a089e2b 100644
--- a/src/defaults.ts
+++ b/src/defaults.ts
@@ -1,6 +1,7 @@
export const PLUGIN_NAME = 'plugin-ai'
-export const PLUGIN_INSTRUCTIONS_TABLE = 'instructions'
-export const PLUGIN_INSTRUCTIONS_MAP_GLOBAL = `${PLUGIN_NAME}__${PLUGIN_INSTRUCTIONS_TABLE}_map`
+export const PLUGIN_INSTRUCTIONS_TABLE = `${PLUGIN_NAME}-instructions`
+export const PLUGIN_INSTRUCTIONS_MAP_GLOBAL = `${PLUGIN_NAME}-${PLUGIN_INSTRUCTIONS_TABLE}-map`
+export const PLUGIN_LEXICAL_EDITOR_FEATURE = `${PLUGIN_NAME}-actions-feature`
// Endpoint defaults
export const PLUGIN_API_ENDPOINT_BASE = '/plugin-ai'
diff --git a/src/endpoints/index.ts b/src/endpoints/index.ts
index 172545e..0eb9610 100644
--- a/src/endpoints/index.ts
+++ b/src/endpoints/index.ts
@@ -1,20 +1,24 @@
import type { SerializedEditorState } from 'lexical'
-import type { PayloadRequest } from 'payload'
+import type { BasePayload, PayloadRequest } from 'payload'
import Handlebars from 'handlebars'
import asyncHelpers from 'handlebars-async-helpers'
-import { flattenTopLevelFields } from 'payload'
import type { Endpoints, MenuItems } from '../types.js'
import { GenerationModels } from '../ai/models/index.js'
-import { PLUGIN_API_ENDPOINT_GENERATE, PLUGIN_API_ENDPOINT_GENERATE_UPLOAD } from '../defaults.js'
+import {
+ PLUGIN_API_ENDPOINT_GENERATE,
+ PLUGIN_API_ENDPOINT_GENERATE_UPLOAD,
+ PLUGIN_INSTRUCTIONS_TABLE,
+} from '../defaults.js'
+import { getFieldBySchemaPath } from '../utilities/getFieldBySchemaPath.js'
import { lexicalToHTML } from '../utilities/lexicalToHTML.js'
const asyncHandlebars = asyncHelpers(Handlebars)
const replacePlaceholders = (prompt: string, values: object) => {
- return asyncHandlebars.compile(prompt)(values)
+ return asyncHandlebars.compile(prompt, { trackIds: true })(values)
}
const assignPrompt = async (
@@ -123,14 +127,46 @@ const assignPrompt = async (
}
}
+const registerEditorHelper = (payload, schemaPath) => {
+ //TODO: add autocomplete ability using handlebars template on PromptEditorField and include custom helpers in dropdown
+
+ let fieldInfo = getFieldInfo(payload.collections, schemaPath)
+ const schemaPathChunks = schemaPath.split('.')
+
+ asyncHandlebars.registerHelper(
+ 'toLexicalHTML',
+ async function (content: SerializedEditorState, options) {
+ const collectionSlug = schemaPathChunks[0]
+ const { ids } = options
+ for (const id of ids) {
+ //TODO: Find a better to get schemaPath of defined field in prompt editor
+ const path = `${collectionSlug}.${id}`
+ fieldInfo = getFieldInfo(payload.collections, path)
+ }
+
+ const html = await lexicalToHTML(content, fieldInfo.editor?.editorConfig)
+ return new asyncHandlebars.SafeString(html)
+ },
+ )
+}
+
+const getFieldInfo = (collections: BasePayload['collections'], schemaPath: string) => {
+ let fieldInfo = null
+ //TODO: Only run below for enabled collections
+ for (const collectionsKey in collections) {
+ const collection = collections[collectionsKey]
+ fieldInfo = getFieldBySchemaPath(collection.config, schemaPath)
+ if (fieldInfo) {
+ return fieldInfo
+ }
+ }
+}
+
export const endpoints: Endpoints = {
textarea: {
handler: async (req: PayloadRequest) => {
const data = await req.json?.()
- // console.log('req.payload.config.editor : ', req.payload.config.editor.editorConfig)
-
- console.log('incoming data -----> ', JSON.stringify(data, null, 2))
const { locale = 'en', options } = data
const { action, instructionId } = options
const contextData = data.doc
@@ -141,13 +177,16 @@ export const endpoints: Endpoints = {
// @ts-expect-error
instructions = await req.payload.findByID({
id: instructionId,
- collection: 'instructions',
+ collection: PLUGIN_INSTRUCTIONS_TABLE,
})
}
const { prompt: promptTemplate = '' } = instructions
- const fieldName = instructions['schema-path']?.split('.').pop()
+ const schemaPath = instructions['schema-path']
+ const fieldName = schemaPath?.split('.').pop()
+
+ registerEditorHelper(req.payload, schemaPath)
const prompts = await assignPrompt(action, {
context: contextData,
@@ -191,19 +230,6 @@ export const endpoints: Endpoints = {
handler: async (req: PayloadRequest) => {
const data = await req.json?.()
- const postsCollection = req.payload.collections['posts']
- const flattenFields = flattenTopLevelFields(postsCollection.config.fields)
-
- //TODO: Important find a way to use lexcial editor in more generic way
- const fieldConfig = flattenFields.find((f) => {
- return f.name === 'content'
- })
-
- //TODO: Important
- // @ts-expect-error
- const { editor } = fieldConfig || { editor: {} }
-
- // console.log('fieldConfig : ', fieldConfig)
const { options } = data
const { instructionId, uploadCollectionSlug } = options
const contextData = data.doc
@@ -214,20 +240,14 @@ export const endpoints: Endpoints = {
// @ts-expect-error
instructions = await req.payload.findByID({
id: instructionId,
- collection: 'instructions',
+ collection: PLUGIN_INSTRUCTIONS_TABLE,
})
}
const { prompt: promptTemplate = '' } = instructions
+ const schemaPath = instructions['schema-path']
- //TODO: add autocomplete ability using handlebars template on PromptEditorField and include custom helpers in dropdown
- asyncHandlebars.registerHelper(
- 'toLexicalHTML',
- async function (content: SerializedEditorState) {
- const html = await lexicalToHTML(content, editor.editorConfig)
- return new asyncHandlebars.SafeString(html)
- },
- )
+ registerEditorHelper(req.payload, schemaPath)
const text = await replacePlaceholders(promptTemplate, contextData)
const modelId = instructions['model-id']
diff --git a/src/fields/LexicalEditor/ActionsFeatureComponent.tsx b/src/fields/LexicalEditor/ActionsFeatureComponent.tsx
new file mode 100644
index 0000000..5a307db
--- /dev/null
+++ b/src/fields/LexicalEditor/ActionsFeatureComponent.tsx
@@ -0,0 +1,14 @@
+import { useFieldProps } from '@payloadcms/ui'
+
+import { useInstructions } from '../../providers/InstructionsProvider/hook.js'
+import { Actions } from '../../ui/Actions/Actions.js'
+
+export const ActionsFeatureComponent = () => {
+ const { schemaPath } = useFieldProps()
+
+ const { id: instructionId } = useInstructions({
+ path: schemaPath,
+ })
+
+ return
+}
diff --git a/src/fields/LexicalEditor/feature.client.tsx b/src/fields/LexicalEditor/feature.client.tsx
new file mode 100644
index 0000000..1d5fdcc
--- /dev/null
+++ b/src/fields/LexicalEditor/feature.client.tsx
@@ -0,0 +1,14 @@
+'use client'
+
+import { createClientFeature } from '@payloadcms/richtext-lexical/client'
+
+import { ActionsFeatureComponent } from './ActionsFeatureComponent.js'
+
+export const LexicalEditorFeatureClient = createClientFeature({
+ plugins: [
+ {
+ Component: ActionsFeatureComponent,
+ position: 'belowContainer',
+ },
+ ],
+})
diff --git a/src/fields/LexicalEditor/feature.server.ts b/src/fields/LexicalEditor/feature.server.ts
new file mode 100644
index 0000000..498e25f
--- /dev/null
+++ b/src/fields/LexicalEditor/feature.server.ts
@@ -0,0 +1,23 @@
+import { createServerFeature } from '@payloadcms/richtext-lexical'
+
+import { PLUGIN_LEXICAL_EDITOR_FEATURE } from '../../defaults.js'
+import { LexicalEditorFeatureClient } from './feature.client.js'
+
+export const PayloadAiPluginLexicalEditorFeature = createServerFeature({
+ feature: ({ props }: Record) => {
+ const sanitizedProps = {
+ applyToFocusedEditor:
+ props?.applyToFocusedEditor === undefined ? false : props.applyToFocusedEditor,
+ disableIfParentHasFixedToolbar:
+ props?.disableIfParentHasFixedToolbar === undefined
+ ? false
+ : props.disableIfParentHasFixedToolbar,
+ }
+ return {
+ ClientFeature: LexicalEditorFeatureClient,
+ clientFeatureProps: sanitizedProps,
+ sanitizedServerFeatureProps: sanitizedProps,
+ }
+ },
+ key: PLUGIN_LEXICAL_EDITOR_FEATURE,
+})
diff --git a/src/index.ts b/src/index.ts
index 4940a51..f88654f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,4 +5,5 @@
* without appropriate licensing is prohibited.
*/
+export { PayloadAiPluginLexicalEditorFeature } from './fields/LexicalEditor/feature.server.js'
export { payloadAiPlugin } from './plugin.js'
diff --git a/src/init.ts b/src/init.ts
index 219c404..58bdb3e 100644
--- a/src/init.ts
+++ b/src/init.ts
@@ -1,4 +1,8 @@
-export const init = async (payload, fieldSchemaPaths) => {
+import type { Payload } from 'payload'
+
+import { PLUGIN_INSTRUCTIONS_MAP_GLOBAL, PLUGIN_INSTRUCTIONS_TABLE } from './defaults.js'
+
+export const init = async (payload: Payload, fieldSchemaPaths) => {
payload.logger.info(`— AI Plugin: Initializing...`)
const paths = Object.keys(fieldSchemaPaths)
@@ -8,7 +12,7 @@ export const init = async (payload, fieldSchemaPaths) => {
const path = paths[i]
const fieldType = fieldSchemaPaths[path]
const entry = await payload.find({
- collection: 'instructions',
+ collection: PLUGIN_INSTRUCTIONS_TABLE,
where: {
'field-type': {
equals: fieldType,
@@ -21,7 +25,7 @@ export const init = async (payload, fieldSchemaPaths) => {
if (!entry?.docs?.length) {
const instructions = await payload.create({
- collection: 'instructions',
+ collection: PLUGIN_INSTRUCTIONS_TABLE,
data: {
'field-type': fieldType,
'schema-path': path,
@@ -34,8 +38,11 @@ export const init = async (payload, fieldSchemaPaths) => {
}
}
+ payload.logger.info(
+ `— AI Plugin: Enabled fieldMap: ${JSON.stringify(fieldInstructionsMap, null, 2)}`,
+ )
await payload.updateGlobal({
- slug: 'ai-plugin__instructions_map', // required
+ slug: PLUGIN_INSTRUCTIONS_MAP_GLOBAL, // required
data: {
map: fieldInstructionsMap,
},
diff --git a/src/plugin.ts b/src/plugin.ts
index 60915de..213cfcb 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -5,6 +5,7 @@ import { deepMerge } from 'payload/shared'
import type { PluginConfig } from './types.js'
import { Instructions } from './collections/Instructions.js'
+import { PLUGIN_INSTRUCTIONS_MAP_GLOBAL } from './defaults.js'
import { endpoints } from './endpoints/index.js'
import { init } from './init.js'
import { InstructionsProvider } from './providers/InstructionsProvider/index.js'
@@ -42,12 +43,12 @@ const payloadAiPlugin =
globals: [
...incomingConfig.globals,
{
- slug: 'ai-plugin__instructions_map',
+ slug: PLUGIN_INSTRUCTIONS_MAP_GLOBAL,
access: {
read: () => true,
},
admin: {
- hidden: true,
+ hidden: false,
},
fields: [
{
diff --git a/src/providers/InstructionsProvider/InstructionsProvider.tsx b/src/providers/InstructionsProvider/InstructionsProvider.tsx
index 1296360..4101bed 100644
--- a/src/providers/InstructionsProvider/InstructionsProvider.tsx
+++ b/src/providers/InstructionsProvider/InstructionsProvider.tsx
@@ -2,6 +2,8 @@
import React, { createContext, useEffect, useState } from 'react'
+import { PLUGIN_INSTRUCTIONS_MAP_GLOBAL } from '../../defaults.js'
+
const initialContext = {
instructions: undefined,
}
@@ -15,7 +17,7 @@ export const InstructionsProvider = ({ children }: { children: React.ReactNode }
// their ID is needed to edit them for Drawer, so instead of fetching it
// one by one its map is saved in globals during build
useEffect(() => {
- fetch('/api/globals/ai-plugin__instructions_map')
+ fetch(`/api/globals/${PLUGIN_INSTRUCTIONS_MAP_GLOBAL}`)
.then((res) => {
res.json().then((data) => {
setInstructionsState(data.map)
diff --git a/src/ui/Actions/Actions.tsx b/src/ui/Actions/Actions.tsx
index b5ae452..f13ab88 100644
--- a/src/ui/Actions/Actions.tsx
+++ b/src/ui/Actions/Actions.tsx
@@ -3,9 +3,9 @@
import type { LexicalEditor } from 'lexical'
import { FieldDescription, Popup, useDocumentDrawer, useField, useFieldProps } from '@payloadcms/ui'
-import { $getRoot } from 'lexical'
import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { PLUGIN_INSTRUCTIONS_TABLE } from '../../defaults.js'
import { PluginIcon } from '../Icons/Icons.js'
import styles from './actions.module.css'
import { useGenerate } from './hooks/useGenerate.js'
@@ -28,10 +28,10 @@ function findParentWithClass(element, className) {
}
//TODO: Add undo/redo to the actions toolbar
-export const Actions = ({ descriptionProps, instructionId }) => {
+export const Actions = ({ descriptionProps = {}, instructionId }) => {
const [DocumentDrawer, _, { closeDrawer, openDrawer }] = useDocumentDrawer({
id: instructionId,
- collectionSlug: 'instructions',
+ collectionSlug: PLUGIN_INSTRUCTIONS_TABLE,
})
const fieldProps = useFieldProps()
@@ -82,46 +82,43 @@ export const Actions = ({ descriptionProps, instructionId }) => {
const [isProcessing, setIsProcessing] = useState(false)
- const { generate, isLoading } = useGenerate({ lexicalEditor })
- const { ActiveComponent, Menu } = useMenu(
- { lexicalEditor },
- {
- onCompose: async () => {
- console.log('Composing...')
- setIsProcessing(true)
- await generate({
- action: 'Compose',
- }).finally(() => {
- setIsProcessing(false)
- })
- },
- onExpand: async () => {
- console.log('Expanding...')
- await generate({
- action: 'Expand',
- })
- },
- onProofread: async () => {
- console.log('Proofreading...')
- await generate({
- action: 'Proofread',
- })
- },
- onRephrase: async () => {
- console.log('Rephrasing...')
- await generate({
- action: 'Rephrase',
- })
- },
- onSettings: openDrawer,
- onSimplify: async () => {
- console.log('Simplifying...')
- await generate({
- action: 'Simplify',
- })
- },
+ const { generate, isLoading } = useGenerate()
+ const { ActiveComponent, Menu } = useMenu({
+ onCompose: async () => {
+ console.log('Composing...')
+ setIsProcessing(true)
+ await generate({
+ action: 'Compose',
+ }).finally(() => {
+ setIsProcessing(false)
+ })
},
- )
+ onExpand: async () => {
+ console.log('Expanding...')
+ await generate({
+ action: 'Expand',
+ })
+ },
+ onProofread: async () => {
+ console.log('Proofreading...')
+ await generate({
+ action: 'Proofread',
+ })
+ },
+ onRephrase: async () => {
+ console.log('Rephrasing...')
+ await generate({
+ action: 'Rephrase',
+ })
+ },
+ onSettings: openDrawer,
+ onSimplify: async () => {
+ console.log('Simplifying...')
+ await generate({
+ action: 'Simplify',
+ })
+ },
+ })
const { setValue } = useField({
path: pathFromContext,
diff --git a/src/ui/Actions/hooks/useGenerate.ts b/src/ui/Actions/hooks/useGenerate.ts
index d608e8e..69024fa 100644
--- a/src/ui/Actions/hooks/useGenerate.ts
+++ b/src/ui/Actions/hooks/useGenerate.ts
@@ -1,6 +1,7 @@
-import type { LexicalEditor } from 'lexical'
+import type { ClientCollectionConfig, UploadField } from 'payload'
-import { useField, useFieldProps, useForm, useLocale } from '@payloadcms/ui'
+import { useEditorConfigContext } from '@payloadcms/richtext-lexical/client'
+import { useDocumentInfo, useField, useFieldProps, useForm, useLocale } from '@payloadcms/ui'
import { useCompletion, experimental_useObject as useObject } from 'ai/react'
import { useCallback, useEffect } from 'react'
@@ -12,19 +13,18 @@ import {
PLUGIN_API_ENDPOINT_GENERATE_UPLOAD,
} from '../../../defaults.js'
import { useInstructions } from '../../../providers/InstructionsProvider/hook.js'
+import { getFieldBySchemaPath } from '../../../utilities/getFieldBySchemaPath.js'
import { setSafeLexicalState } from '../../../utilities/setSafeLexicalState.js'
import { useHistory } from './useHistory.js'
-type UseGenerate = {
- lexicalEditor: LexicalEditor
-}
-
//TODO: DONATION IDEA - Add a url to donate in cli when user installs the plugin and uses it for couple of times.
-export const useGenerate = ({ lexicalEditor }: UseGenerate) => {
+export const useGenerate = () => {
const { type, path: pathFromContext, schemaPath } = useFieldProps()
- //TODO: This should be dynamic, i think it was the part of component props but its not inside useFieldProps
- const relationTo = 'media'
+ const editorConfigContext = useEditorConfigContext()
+ const { editor } = editorConfigContext
+
+ const { docConfig } = useDocumentInfo()
const { setValue } = useField({
path: pathFromContext,
@@ -58,13 +58,13 @@ export const useGenerate = ({ lexicalEditor }: UseGenerate) => {
if (!object) return
requestAnimationFrame(() => {
- if (!lexicalEditor) {
+ if (!editor) {
setValue(object)
return
}
// Currently this is being used as setValue for RichText component does not render new changes right away.
- setSafeLexicalState(object, lexicalEditor)
+ setSafeLexicalState(object, editor)
})
}, [object])
@@ -130,13 +130,19 @@ export const useGenerate = ({ lexicalEditor }: UseGenerate) => {
const generateUpload = useCallback(async () => {
const doc = getData()
+
+ const fieldInfo = getFieldBySchemaPath(
+ docConfig as ClientCollectionConfig,
+ schemaPath,
+ ) as UploadField
+
return fetch(`/api${PLUGIN_API_ENDPOINT_GENERATE_UPLOAD}`, {
body: JSON.stringify({
doc,
locale: localFromContext?.code,
options: {
instructionId,
- uploadCollectionSlug: relationTo,
+ uploadCollectionSlug: fieldInfo.relationTo || 'media',
},
} satisfies Parameters[0]),
credentials: 'include',
@@ -159,7 +165,7 @@ export const useGenerate = ({ lexicalEditor }: UseGenerate) => {
.catch((error) => {
console.error('Error generating image', error)
})
- }, [getData, localFromContext?.code, instructionId, relationTo, setValue])
+ }, [getData, localFromContext?.code, instructionId, setValue])
const generate = useCallback(
async (options?: { action: MenuItems }) => {
@@ -170,6 +176,7 @@ export const useGenerate = ({ lexicalEditor }: UseGenerate) => {
if (['text', 'textarea'].includes(type)) {
return streamText(options)
}
+
if (type === 'upload') {
return generateUpload()
}
diff --git a/src/ui/Actions/hooks/useHistory.ts b/src/ui/Actions/hooks/useHistory.ts
index 195c585..9483991 100644
--- a/src/ui/Actions/hooks/useHistory.ts
+++ b/src/ui/Actions/hooks/useHistory.ts
@@ -1,7 +1,7 @@
'use client'
-import { useDocumentInfo, useField, useFieldProps, useForm } from '@payloadcms/ui'
-import { useCallback, useEffect, useState } from 'react'
+import { useDocumentInfo, useField, useFieldProps } from '@payloadcms/ui'
+import { useCallback, useEffect } from 'react'
import { PLUGIN_NAME } from '../../../defaults.js'
@@ -25,7 +25,11 @@ export const useHistory = () => {
const getLatestHistory = useCallback((): HistoryState => {
try {
- return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
+ // This condition is applied, as it was somehow triggering on server side
+ if (typeof localStorage !== 'undefined') {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
+ }
+ return {}
} catch (e) {
console.error('Error parsing history:', e)
return {}
diff --git a/src/ui/Actions/hooks/useMenu.tsx b/src/ui/Actions/hooks/useMenu.tsx
index b64b509..43f6a15 100644
--- a/src/ui/Actions/hooks/useMenu.tsx
+++ b/src/ui/Actions/hooks/useMenu.tsx
@@ -1,7 +1,5 @@
'use client'
-import type { LexicalEditor } from 'lexical'
-
import { useField, useFieldProps } from '@payloadcms/ui'
import React, { memo, useEffect, useMemo, useState } from 'react'
@@ -84,11 +82,7 @@ const getActiveComponent = (ac) => {
}
}
-type UseMenuProps = {
- lexicalEditor: LexicalEditor
-}
-
-export const useMenu = ({ lexicalEditor }: UseMenuProps, menuEvents: UseMenuEvents) => {
+export const useMenu = (menuEvents: UseMenuEvents) => {
const { type: fieldType, path: pathFromContext } = useFieldProps()
const field = useField({ path: pathFromContext })
const [activeComponent, setActiveComponent] = useState('Rephrase')
@@ -111,7 +105,7 @@ export const useMenu = ({ lexicalEditor }: UseMenuProps, menuEvents: UseMenuEven
} else {
setActiveComponent('Rephrase')
}
- }, [initialValue, value, fieldType, lexicalEditor])
+ }, [initialValue, value, fieldType])
const MemoizedActiveComponent = useMemo(() => {
return ({ isLoading }) => {
diff --git a/src/utilities/getFieldBySchemaPath.ts b/src/utilities/getFieldBySchemaPath.ts
new file mode 100644
index 0000000..5ef3eb2
--- /dev/null
+++ b/src/utilities/getFieldBySchemaPath.ts
@@ -0,0 +1,47 @@
+import type { ClientCollectionConfig, ClientFieldConfig, CollectionConfig, Field } from 'payload'
+
+export const getFieldBySchemaPath = (
+ collectionConfig: ClientCollectionConfig | CollectionConfig,
+ schemaPath: string, // e.g., "posts.content"
+): ClientFieldConfig | Field | null => {
+ const pathParts = schemaPath.split('.')
+ const targetFieldName = pathParts[pathParts.length - 1]
+
+ const findField = (fields, remainingPath: string[]): Field | null => {
+ for (const field of fields) {
+ if (remainingPath.length === 1 && field.name === targetFieldName) {
+ return field
+ }
+
+ if (field.type === 'group' && field.fields) {
+ const result = findField(field.fields, remainingPath.slice(1))
+ if (result) return result
+ }
+
+ if (field.type === 'array' && field.fields) {
+ const result = findField(field.fields, remainingPath.slice(1))
+ if (result) return result
+ }
+
+ if (field.type === 'tabs') {
+ for (const tab of field.tabs) {
+ const result = findField(tab.fields, remainingPath)
+ if (result) return result
+ }
+ }
+
+ if (field.type === 'blocks') {
+ for (const block of field.blocks) {
+ if (block.slug === remainingPath[0]) {
+ const result = findField(block.fields, remainingPath.slice(1))
+ if (result) return result
+ }
+ }
+ }
+ }
+
+ return null
+ }
+
+ return findField(collectionConfig.fields, pathParts.slice(1))
+}
diff --git a/src/utilities/updateFieldsConfig.ts b/src/utilities/updateFieldsConfig.ts
index b123495..e7701cd 100644
--- a/src/utilities/updateFieldsConfig.ts
+++ b/src/utilities/updateFieldsConfig.ts
@@ -17,12 +17,18 @@ export const updateFieldsConfig = (collectionConfig: CollectionConfig): UpdateFi
return field
}
- if (field.type && ['richText', 'text', 'textarea', 'upload'].includes(field.type)) {
+ // Map field path for global fieldInstructionsMap to load related instructions
+ // This is done due to save extra API call to get instructions when Field components are loaded in admin
+ // Doing is will only call instructions data when user clicks on settings
+ if (['richText', 'text', 'textarea', 'upload'].includes(field.type)) {
schemaPathMap = {
...schemaPathMap,
[currentSchemaPath]: field.type,
}
+ }
+ // Inject AI actions, richText is not included here as it has to be explicitly defined by user
+ if (['text', 'textarea', 'upload'].includes(field.type)) {
return {
...field,
admin: {