@ai-stack/payloadcms 3.2.20-beta → 3.2.22-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/models/anthropic/index.js +9 -15
- package/dist/ai/models/anthropic/index.js.map +1 -1
- package/dist/ai/models/generateObject.d.ts +11 -0
- package/dist/ai/models/generateObject.js +22 -0
- package/dist/ai/models/generateObject.js.map +1 -0
- package/dist/ai/models/openai/index.js +19 -17
- package/dist/ai/models/openai/index.js.map +1 -1
- package/dist/collections/Instructions.js +2 -0
- package/dist/collections/Instructions.js.map +1 -1
- package/dist/endpoints/index.js +55 -6
- package/dist/endpoints/index.js.map +1 -1
- package/dist/fields/ComposeField/ComposeField.js +5 -3
- package/dist/fields/ComposeField/ComposeField.js.map +1 -1
- package/dist/fields/ComposeField/ComposeField.jsx +32 -0
- package/dist/fields/LexicalEditor/ComposeFeatureComponent.js +1 -1
- package/dist/fields/LexicalEditor/ComposeFeatureComponent.js.map +1 -1
- package/dist/fields/LexicalEditor/ComposeFeatureComponent.jsx +24 -0
- package/dist/fields/LexicalEditor/feature.client.jsx +21 -0
- package/dist/fields/PromptEditorField/PromptEditorField.jsx +42 -0
- package/dist/fields/SelectField/SelectField.jsx +38 -0
- package/dist/providers/FieldProvider/FieldProvider.d.ts +7 -4
- package/dist/providers/FieldProvider/FieldProvider.js +7 -6
- package/dist/providers/FieldProvider/FieldProvider.js.map +1 -1
- package/dist/providers/FieldProvider/FieldProvider.jsx +27 -0
- package/dist/providers/FieldProvider/useFieldProps.d.ts +1 -1
- package/dist/providers/FieldProvider/useFieldProps.js +2 -2
- package/dist/providers/FieldProvider/useFieldProps.js.map +1 -1
- package/dist/providers/InstructionsProvider/InstructionsProvider.jsx +45 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js.map +1 -1
- package/dist/ui/Compose/Compose.js +12 -81
- package/dist/ui/Compose/Compose.js.map +1 -1
- package/dist/ui/Compose/Compose.jsx +141 -0
- package/dist/ui/Compose/UndoRedoActions.jsx +34 -0
- package/dist/ui/Compose/compose.module.css +99 -12
- package/dist/ui/Compose/hooks/menu/Item.jsx +15 -0
- package/dist/ui/Compose/hooks/menu/TranslateMenu.js +1 -2
- package/dist/ui/Compose/hooks/menu/TranslateMenu.js.map +1 -1
- package/dist/ui/Compose/hooks/menu/TranslateMenu.jsx +58 -0
- package/dist/ui/Compose/hooks/menu/items.jsx +10 -0
- package/dist/ui/Compose/hooks/menu/useMenu.js +1 -1
- package/dist/ui/Compose/hooks/menu/useMenu.js.map +1 -1
- package/dist/ui/Compose/hooks/menu/useMenu.jsx +89 -0
- package/dist/ui/Compose/hooks/useActiveFieldTracking.d.ts +5 -0
- package/dist/ui/Compose/hooks/useActiveFieldTracking.js +225 -0
- package/dist/ui/Compose/hooks/useActiveFieldTracking.js.map +1 -0
- package/dist/ui/Compose/hooks/useGenerate.js +46 -79
- package/dist/ui/Compose/hooks/useGenerate.js.map +1 -1
- package/dist/ui/Icons/Icons.jsx +78 -0
- package/dist/ui/Icons/LottieAnimation.jsx +64 -0
- package/dist/utilities/extractPromptAttachments.d.ts +2 -2
- package/dist/utilities/extractPromptAttachments.js.map +1 -1
- package/dist/utilities/fieldToJsonSchema.d.ts +37 -0
- package/dist/utilities/fieldToJsonSchema.js +274 -0
- package/dist/utilities/fieldToJsonSchema.js.map +1 -0
- package/dist/utilities/getFieldBySchemaPath.d.ts +12 -1
- package/dist/utilities/getFieldBySchemaPath.js +63 -29
- package/dist/utilities/getFieldBySchemaPath.js.map +1 -1
- package/package.json +2 -1
- package/dist/ai/models/anthropic/generateRichText.d.ts +0 -1
- package/dist/ai/models/anthropic/generateRichText.js +0 -36
- package/dist/ai/models/anthropic/generateRichText.js.map +0 -1
- package/dist/ai/models/openai/generateRichText.d.ts +0 -1
- package/dist/ai/models/openai/generateRichText.js +0 -37
- package/dist/ai/models/openai/generateRichText.js.map +0 -1
|
@@ -19,7 +19,7 @@ const getActiveComponent = (ac)=>{
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
export const useMenu = (menuEvents, options)=>{
|
|
22
|
-
const { type: fieldType, path: pathFromContext } = useFieldProps();
|
|
22
|
+
const { field: { type: fieldType } = {}, path: pathFromContext } = useFieldProps();
|
|
23
23
|
const field = useField({
|
|
24
24
|
path: pathFromContext ?? ''
|
|
25
25
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/ui/Compose/hooks/menu/useMenu.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type { ActionMenuItems, UseMenuEvents, UseMenuOptions } from '../../../../types.js'\n\nimport { useFieldProps } from '../../../../providers/FieldProvider/useFieldProps.js'\nimport { Compose, Proofread, Rephrase } from './items.js'\nimport { menuItemsMap } from './itemsMap.js'\nimport styles from './menu.module.scss'\n\nconst getActiveComponent = (ac: ActionMenuItems) => {\n switch (ac) {\n case 'Compose':\n return Compose\n case 'Proofread':\n return Proofread\n case 'Rephrase':\n return Rephrase\n default:\n return Rephrase\n }\n}\n\nexport const useMenu = (menuEvents: UseMenuEvents, options: UseMenuOptions) => {\n const { type: fieldType, path: pathFromContext } = useFieldProps()\n const field = useField({ path: pathFromContext ?? '' })\n const [activeComponent, setActiveComponent] = useState<ActionMenuItems>('Rephrase')\n\n const { initialValue, value } = field\n\n useEffect(() => {\n if (!value) {\n setActiveComponent('Compose')\n return\n }\n\n if (menuItemsMap.some((i) => i.excludedFor?.includes(fieldType ?? ''))) {\n setActiveComponent('Compose')\n return\n }\n\n if (typeof value === 'string' && value !== initialValue) {\n setActiveComponent('Proofread')\n } else {\n setActiveComponent('Rephrase')\n }\n }, [initialValue, value, fieldType])\n\n const MemoizedActiveComponent = useMemo(() => {\n return ({ isLoading, stop }: { isLoading: boolean; stop: () => void }) => {\n const ActiveComponent = getActiveComponent(activeComponent)\n const activeItem = menuItemsMap.find((i) => i.name === activeComponent)!\n return (\n <ActiveComponent\n hideIcon\n onClick={(data: unknown) => {\n if (!isLoading) {\n const trigger = menuEvents[`on${activeComponent}`]\n if (typeof trigger === 'function') {\n trigger(data)\n } else {\n console.error('No trigger found for', activeComponent)\n }\n } else {\n stop()\n }\n }}\n title={isLoading ? 'Click to stop' : activeItem.name}\n >\n {isLoading && activeItem.loadingText}\n </ActiveComponent>\n )\n }\n }, [activeComponent, menuEvents])\n\n const filteredMenuItems = useMemo(\n () =>\n menuItemsMap.filter((i) => {\n if (i.name === 'Settings' && !options.isConfigAllowed) {\n return false\n } // Disable settings if a user role is not permitted\n return i.name !== activeComponent && !i.excludedFor?.includes(fieldType ?? '')\n }),\n [activeComponent, fieldType, options.isConfigAllowed],\n )\n\n const MemoizedMenu = useMemo(() => {\n return ({ isLoading, onClose }: { isLoading: boolean; onClose: () => void }) => (\n <div className={styles.menu}>\n {filteredMenuItems.map((i) => {\n const Action = i.component\n return (\n <Action\n disabled={isLoading}\n key={i.name}\n onClick={(data: unknown) => {\n if (i.name !== 'Settings') {\n setActiveComponent(i.name)\n }\n\n menuEvents[`on${i.name}`]?.(data)\n onClose()\n }}\n >\n {isLoading && i.loadingText}\n </Action>\n )\n })}\n </div>\n )\n }, [filteredMenuItems, menuEvents])\n\n return {\n ActiveComponent: MemoizedActiveComponent,\n Menu: MemoizedMenu,\n }\n}\n"],"names":["useField","React","useEffect","useMemo","useState","useFieldProps","Compose","Proofread","Rephrase","menuItemsMap","styles","getActiveComponent","ac","useMenu","menuEvents","options","type","fieldType","path","pathFromContext","
|
|
1
|
+
{"version":3,"sources":["../../../../../src/ui/Compose/hooks/menu/useMenu.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type { ActionMenuItems, UseMenuEvents, UseMenuOptions } from '../../../../types.js'\n\nimport { useFieldProps } from '../../../../providers/FieldProvider/useFieldProps.js'\nimport { Compose, Proofread, Rephrase } from './items.js'\nimport { menuItemsMap } from './itemsMap.js'\nimport styles from './menu.module.scss'\n\nconst getActiveComponent = (ac: ActionMenuItems) => {\n switch (ac) {\n case 'Compose':\n return Compose\n case 'Proofread':\n return Proofread\n case 'Rephrase':\n return Rephrase\n default:\n return Rephrase\n }\n}\n\nexport const useMenu = (menuEvents: UseMenuEvents, options: UseMenuOptions) => {\n const { field:{ type: fieldType } = {}, path: pathFromContext } = useFieldProps()\n const field = useField({ path: pathFromContext ?? '' })\n const [activeComponent, setActiveComponent] = useState<ActionMenuItems>('Rephrase')\n\n const { initialValue, value } = field\n\n useEffect(() => {\n if (!value) {\n setActiveComponent('Compose')\n return\n }\n\n if (menuItemsMap.some((i) => i.excludedFor?.includes(fieldType ?? ''))) {\n setActiveComponent('Compose')\n return\n }\n\n if (typeof value === 'string' && value !== initialValue) {\n setActiveComponent('Proofread')\n } else {\n setActiveComponent('Rephrase')\n }\n }, [initialValue, value, fieldType])\n\n const MemoizedActiveComponent = useMemo(() => {\n return ({ isLoading, stop }: { isLoading: boolean; stop: () => void }) => {\n const ActiveComponent = getActiveComponent(activeComponent)\n const activeItem = menuItemsMap.find((i) => i.name === activeComponent)!\n return (\n <ActiveComponent\n hideIcon\n onClick={(data: unknown) => {\n if (!isLoading) {\n const trigger = menuEvents[`on${activeComponent}`]\n if (typeof trigger === 'function') {\n trigger(data)\n } else {\n console.error('No trigger found for', activeComponent)\n }\n } else {\n stop()\n }\n }}\n title={isLoading ? 'Click to stop' : activeItem.name}\n >\n {isLoading && activeItem.loadingText}\n </ActiveComponent>\n )\n }\n }, [activeComponent, menuEvents])\n\n const filteredMenuItems = useMemo(\n () =>\n menuItemsMap.filter((i) => {\n if (i.name === 'Settings' && !options.isConfigAllowed) {\n return false\n } // Disable settings if a user role is not permitted\n return i.name !== activeComponent && !i.excludedFor?.includes(fieldType ?? '')\n }),\n [activeComponent, fieldType, options.isConfigAllowed],\n )\n\n const MemoizedMenu = useMemo(() => {\n return ({ isLoading, onClose }: { isLoading: boolean; onClose: () => void }) => (\n <div className={styles.menu}>\n {filteredMenuItems.map((i) => {\n const Action = i.component\n return (\n <Action\n disabled={isLoading}\n key={i.name}\n onClick={(data: unknown) => {\n if (i.name !== 'Settings') {\n setActiveComponent(i.name)\n }\n\n menuEvents[`on${i.name}`]?.(data)\n onClose()\n }}\n >\n {isLoading && i.loadingText}\n </Action>\n )\n })}\n </div>\n )\n }, [filteredMenuItems, menuEvents])\n\n return {\n ActiveComponent: MemoizedActiveComponent,\n Menu: MemoizedMenu,\n }\n}\n"],"names":["useField","React","useEffect","useMemo","useState","useFieldProps","Compose","Proofread","Rephrase","menuItemsMap","styles","getActiveComponent","ac","useMenu","menuEvents","options","field","type","fieldType","path","pathFromContext","activeComponent","setActiveComponent","initialValue","value","some","i","excludedFor","includes","MemoizedActiveComponent","isLoading","stop","ActiveComponent","activeItem","find","name","hideIcon","onClick","data","trigger","console","error","title","loadingText","filteredMenuItems","filter","isConfigAllowed","MemoizedMenu","onClose","div","className","menu","map","Action","component","disabled","Menu"],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useField } from '@payloadcms/ui';
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useFieldProps } from '../../../../providers/FieldProvider/useFieldProps.js';
|
|
5
|
+
import { Compose, Proofread, Rephrase } from './items.js';
|
|
6
|
+
import { menuItemsMap } from './itemsMap.js';
|
|
7
|
+
import styles from './menu.module.scss';
|
|
8
|
+
const getActiveComponent = (ac) => {
|
|
9
|
+
switch (ac) {
|
|
10
|
+
case 'Compose':
|
|
11
|
+
return Compose;
|
|
12
|
+
case 'Proofread':
|
|
13
|
+
return Proofread;
|
|
14
|
+
case 'Rephrase':
|
|
15
|
+
return Rephrase;
|
|
16
|
+
default:
|
|
17
|
+
return Rephrase;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const useMenu = (menuEvents, options) => {
|
|
21
|
+
const { field: { type: fieldType } = {}, path: pathFromContext } = useFieldProps();
|
|
22
|
+
const field = useField({ path: pathFromContext ?? '' });
|
|
23
|
+
const [activeComponent, setActiveComponent] = useState('Rephrase');
|
|
24
|
+
const { initialValue, value } = field;
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!value) {
|
|
27
|
+
setActiveComponent('Compose');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (menuItemsMap.some((i) => i.excludedFor?.includes(fieldType ?? ''))) {
|
|
31
|
+
setActiveComponent('Compose');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'string' && value !== initialValue) {
|
|
35
|
+
setActiveComponent('Proofread');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
setActiveComponent('Rephrase');
|
|
39
|
+
}
|
|
40
|
+
}, [initialValue, value, fieldType]);
|
|
41
|
+
const MemoizedActiveComponent = useMemo(() => {
|
|
42
|
+
return ({ isLoading, stop }) => {
|
|
43
|
+
const ActiveComponent = getActiveComponent(activeComponent);
|
|
44
|
+
const activeItem = menuItemsMap.find((i) => i.name === activeComponent);
|
|
45
|
+
return (<ActiveComponent hideIcon onClick={(data) => {
|
|
46
|
+
if (!isLoading) {
|
|
47
|
+
const trigger = menuEvents[`on${activeComponent}`];
|
|
48
|
+
if (typeof trigger === 'function') {
|
|
49
|
+
trigger(data);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.error('No trigger found for', activeComponent);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
stop();
|
|
57
|
+
}
|
|
58
|
+
}} title={isLoading ? 'Click to stop' : activeItem.name}>
|
|
59
|
+
{isLoading && activeItem.loadingText}
|
|
60
|
+
</ActiveComponent>);
|
|
61
|
+
};
|
|
62
|
+
}, [activeComponent, menuEvents]);
|
|
63
|
+
const filteredMenuItems = useMemo(() => menuItemsMap.filter((i) => {
|
|
64
|
+
if (i.name === 'Settings' && !options.isConfigAllowed) {
|
|
65
|
+
return false;
|
|
66
|
+
} // Disable settings if a user role is not permitted
|
|
67
|
+
return i.name !== activeComponent && !i.excludedFor?.includes(fieldType ?? '');
|
|
68
|
+
}), [activeComponent, fieldType, options.isConfigAllowed]);
|
|
69
|
+
const MemoizedMenu = useMemo(() => {
|
|
70
|
+
return ({ isLoading, onClose }) => (<div className={styles.menu}>
|
|
71
|
+
{filteredMenuItems.map((i) => {
|
|
72
|
+
const Action = i.component;
|
|
73
|
+
return (<Action disabled={isLoading} key={i.name} onClick={(data) => {
|
|
74
|
+
if (i.name !== 'Settings') {
|
|
75
|
+
setActiveComponent(i.name);
|
|
76
|
+
}
|
|
77
|
+
menuEvents[`on${i.name}`]?.(data);
|
|
78
|
+
onClose();
|
|
79
|
+
}}>
|
|
80
|
+
{isLoading && i.loadingText}
|
|
81
|
+
</Action>);
|
|
82
|
+
})}
|
|
83
|
+
</div>);
|
|
84
|
+
}, [filteredMenuItems, menuEvents]);
|
|
85
|
+
return {
|
|
86
|
+
ActiveComponent: MemoizedActiveComponent,
|
|
87
|
+
Menu: MemoizedMenu,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Allowed field type classes that should show the active state
|
|
5
|
+
*/ const ALLOWED_FIELD_TYPES = [
|
|
6
|
+
'upload',
|
|
7
|
+
'text',
|
|
8
|
+
'textarea',
|
|
9
|
+
'rich-text-lexical'
|
|
10
|
+
];
|
|
11
|
+
let currentContainer = null;
|
|
12
|
+
let rafId = null // Track RAF to cancel if needed
|
|
13
|
+
;
|
|
14
|
+
/**
|
|
15
|
+
* Safely escape CSS selector values
|
|
16
|
+
*/ const cssEscape = (value)=>{
|
|
17
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
18
|
+
return CSS.escape(value);
|
|
19
|
+
}
|
|
20
|
+
return value.replace(/([ #;?%&,.+*~':"!^$[\]()=>|/@])/g, '\\$1');
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Find container from React Select dropdown elements
|
|
24
|
+
*/ const findContainerFromReactSelect = (target)=>{
|
|
25
|
+
const listbox = target.closest('[role="listbox"]');
|
|
26
|
+
if (!listbox?.id) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const id = cssEscape(listbox.id);
|
|
30
|
+
const selector = `[aria-controls="${id}"], [aria-owns="${id}"]`;
|
|
31
|
+
const control = document.querySelector(selector);
|
|
32
|
+
return control?.closest('.field-type') ?? null;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Check if a container has one of the allowed field type classes
|
|
36
|
+
*/ const isAllowedFieldType = (container)=>{
|
|
37
|
+
return ALLOWED_FIELD_TYPES.some((type)=>container.classList.contains(type) || container.classList.contains(`field-type-${type}`));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the .field-type container for a given event target
|
|
41
|
+
* Only returns containers that match allowed field types
|
|
42
|
+
*/ const resolveContainerFromTarget = (target)=>{
|
|
43
|
+
if (!(target instanceof HTMLElement)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Check for direct parent first
|
|
47
|
+
let container = target.closest('.field-type');
|
|
48
|
+
// If not found, fall back to React Select logic
|
|
49
|
+
if (!container) {
|
|
50
|
+
container = findContainerFromReactSelect(target);
|
|
51
|
+
}
|
|
52
|
+
// Only return if it's an allowed field type
|
|
53
|
+
if (container && isAllowedFieldType(container)) {
|
|
54
|
+
return container;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Update the active container and toggle CSS class
|
|
60
|
+
* - Avoids acting on disconnected nodes
|
|
61
|
+
* - Avoids redundant class work
|
|
62
|
+
*/ const setActiveContainer = (next)=>{
|
|
63
|
+
// Normalize both references against disconnected nodes
|
|
64
|
+
if (currentContainer && !currentContainer.isConnected) {
|
|
65
|
+
currentContainer = null;
|
|
66
|
+
}
|
|
67
|
+
if (next && !next.isConnected) {
|
|
68
|
+
next = null;
|
|
69
|
+
}
|
|
70
|
+
if (currentContainer === next) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
currentContainer?.classList.remove('ai-plugin-active');
|
|
74
|
+
if (next) {
|
|
75
|
+
next.classList.add('ai-plugin-active');
|
|
76
|
+
}
|
|
77
|
+
currentContainer = next;
|
|
78
|
+
};
|
|
79
|
+
const clearActiveContainer = ()=>{
|
|
80
|
+
if (currentContainer) {
|
|
81
|
+
currentContainer.classList.remove('ai-plugin-active');
|
|
82
|
+
currentContainer = null;
|
|
83
|
+
}
|
|
84
|
+
// Cancel any pending RAF
|
|
85
|
+
if (rafId !== null) {
|
|
86
|
+
cancelAnimationFrame(rafId);
|
|
87
|
+
rafId = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const isInteractiveElement = (element)=>{
|
|
91
|
+
const tagName = element.tagName.toLowerCase();
|
|
92
|
+
const interactiveTags = [
|
|
93
|
+
'input',
|
|
94
|
+
'textarea',
|
|
95
|
+
'select',
|
|
96
|
+
'button'
|
|
97
|
+
];
|
|
98
|
+
if (interactiveTags.includes(tagName)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// Check for contenteditable
|
|
102
|
+
if (element.isContentEditable) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
// Check for elements with role="textbox" or role="combobox" (React Select)
|
|
106
|
+
const role = element.getAttribute('role');
|
|
107
|
+
if (role && [
|
|
108
|
+
'combobox',
|
|
109
|
+
'listbox',
|
|
110
|
+
'searchbox',
|
|
111
|
+
'textbox'
|
|
112
|
+
].includes(role)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Handle focus events - only activate if focus is on an interactive element within .field-type
|
|
119
|
+
*/ const onFocusIn = (e)=>{
|
|
120
|
+
const target = e.target;
|
|
121
|
+
if (!(target instanceof HTMLElement)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Early exit if we're already inside the current container
|
|
125
|
+
if (currentContainer?.isConnected && currentContainer.contains(target)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Only activate if the focused element is actually interactive
|
|
129
|
+
if (!isInteractiveElement(target)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const container = resolveContainerFromTarget(target);
|
|
133
|
+
setActiveContainer(container);
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Handle pointer/mouse events - only switch when clicking a different .field-type
|
|
137
|
+
*/ const onPointerDown = (e)=>{
|
|
138
|
+
const target = e.target;
|
|
139
|
+
if (!(target instanceof HTMLElement)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Early exit if clicking within current container
|
|
143
|
+
if (currentContainer?.isConnected && currentContainer.contains(target)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const container = resolveContainerFromTarget(target);
|
|
147
|
+
setActiveContainer(container);
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Handle keyboard navigation (Tab key)
|
|
151
|
+
*/ const onKeyDown = (e)=>{
|
|
152
|
+
if (e.key !== 'Tab') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Cancel any pending RAF to prevent queuing
|
|
156
|
+
if (rafId !== null) {
|
|
157
|
+
cancelAnimationFrame(rafId);
|
|
158
|
+
}
|
|
159
|
+
// Defer until after focus has shifted
|
|
160
|
+
rafId = requestAnimationFrame(()=>{
|
|
161
|
+
rafId = null;
|
|
162
|
+
const container = resolveContainerFromTarget(document.activeElement);
|
|
163
|
+
setActiveContainer(container);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Handle visibility changes to properly cleanup when page is hidden
|
|
168
|
+
*/ const onVisibilityChange = ()=>{
|
|
169
|
+
if (typeof document !== 'undefined' && document.hidden) {
|
|
170
|
+
// Clear active state and cancel pending operations
|
|
171
|
+
clearActiveContainer();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Initialize document-level listeners to track the active field container.
|
|
176
|
+
* When a container is active, it receives the 'ai-plugin-active' class.
|
|
177
|
+
*/ export const useActiveFieldTracking = ()=>{
|
|
178
|
+
useEffect(()=>{
|
|
179
|
+
if (typeof window === 'undefined') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const pluginWindow = window;
|
|
183
|
+
// Track number of mounted users of the hook
|
|
184
|
+
pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 0) + 1;
|
|
185
|
+
// Initialize listeners only once
|
|
186
|
+
if (!pluginWindow.__aiComposeTracking) {
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
pluginWindow.__aiComposeTrackingController = controller;
|
|
189
|
+
// Use capture for early handling
|
|
190
|
+
document.addEventListener('focusin', onFocusIn, {
|
|
191
|
+
capture: true,
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
});
|
|
194
|
+
document.addEventListener('pointerdown', onPointerDown, {
|
|
195
|
+
capture: true,
|
|
196
|
+
passive: true,
|
|
197
|
+
signal: controller.signal
|
|
198
|
+
});
|
|
199
|
+
document.addEventListener('keydown', onKeyDown, {
|
|
200
|
+
capture: true,
|
|
201
|
+
signal: controller.signal
|
|
202
|
+
});
|
|
203
|
+
document.addEventListener('visibilitychange', onVisibilityChange, {
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
206
|
+
pluginWindow.__aiComposeTracking = true;
|
|
207
|
+
}
|
|
208
|
+
return ()=>{
|
|
209
|
+
// Decrement and cleanup when the last user unmounts
|
|
210
|
+
pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 1) - 1;
|
|
211
|
+
if ((pluginWindow.__aiComposeTrackingCount ?? 0) <= 0) {
|
|
212
|
+
// Atomically remove all listeners
|
|
213
|
+
pluginWindow.__aiComposeTrackingController?.abort();
|
|
214
|
+
pluginWindow.__aiComposeTrackingController = undefined;
|
|
215
|
+
// Clear active state and cancel pending operations
|
|
216
|
+
clearActiveContainer();
|
|
217
|
+
// Reset all state
|
|
218
|
+
pluginWindow.__aiComposeTracking = false;
|
|
219
|
+
pluginWindow.__aiComposeTrackingCount = 0;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}, []);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=useActiveFieldTracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/Compose/hooks/useActiveFieldTracking.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\n/**\n * Allowed field type classes that should show the active state\n */\nconst ALLOWED_FIELD_TYPES = ['upload', 'text', 'textarea', 'rich-text-lexical']\n\nlet currentContainer: HTMLElement | null = null\nlet rafId: null | number = null // Track RAF to cancel if needed\n\n/**\n * Safely escape CSS selector values\n */\nconst cssEscape = (value: string): string => {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {\n return CSS.escape(value)\n }\n return value.replace(/([ #;?%&,.+*~':\"!^$[\\]()=>|/@])/g, '\\\\$1')\n}\n\n/**\n * Find container from React Select dropdown elements\n */\nconst findContainerFromReactSelect = (target: HTMLElement): HTMLElement | null => {\n const listbox = target.closest<HTMLElement>('[role=\"listbox\"]')\n if (!listbox?.id) {\n return null\n }\n\n const id = cssEscape(listbox.id)\n const selector = `[aria-controls=\"${id}\"], [aria-owns=\"${id}\"]`\n const control = document.querySelector<HTMLElement>(selector)\n\n return control?.closest<HTMLElement>('.field-type') ?? null\n}\n\n/**\n * Check if a container has one of the allowed field type classes\n */\nconst isAllowedFieldType = (container: HTMLElement): boolean => {\n return ALLOWED_FIELD_TYPES.some(\n (type) =>\n container.classList.contains(type) || container.classList.contains(`field-type-${type}`),\n )\n}\n\n/**\n * Resolve the .field-type container for a given event target\n * Only returns containers that match allowed field types\n */\nconst resolveContainerFromTarget = (target: EventTarget | null): HTMLElement | null => {\n if (!(target instanceof HTMLElement)) {\n return null\n }\n\n // Check for direct parent first\n let container = target.closest<HTMLElement>('.field-type')\n\n // If not found, fall back to React Select logic\n if (!container) {\n container = findContainerFromReactSelect(target)\n }\n\n // Only return if it's an allowed field type\n if (container && isAllowedFieldType(container)) {\n return container\n }\n\n return null\n}\n\n/**\n * Update the active container and toggle CSS class\n * - Avoids acting on disconnected nodes\n * - Avoids redundant class work\n */\nconst setActiveContainer = (next: HTMLElement | null): void => {\n // Normalize both references against disconnected nodes\n if (currentContainer && !currentContainer.isConnected) {\n currentContainer = null\n }\n if (next && !next.isConnected) {\n next = null\n }\n\n if (currentContainer === next) {\n return\n }\n\n currentContainer?.classList.remove('ai-plugin-active')\n if (next) {\n next.classList.add('ai-plugin-active')\n }\n currentContainer = next\n}\n\nconst clearActiveContainer = (): void => {\n if (currentContainer) {\n currentContainer.classList.remove('ai-plugin-active')\n currentContainer = null\n }\n\n // Cancel any pending RAF\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n}\n\nconst isInteractiveElement = (element: HTMLElement): boolean => {\n const tagName = element.tagName.toLowerCase()\n const interactiveTags = ['input', 'textarea', 'select', 'button']\n\n if (interactiveTags.includes(tagName)) {\n return true\n }\n\n // Check for contenteditable\n if (element.isContentEditable) {\n return true\n }\n\n // Check for elements with role=\"textbox\" or role=\"combobox\" (React Select)\n const role = element.getAttribute('role')\n if (role && ['combobox', 'listbox', 'searchbox', 'textbox'].includes(role)) {\n return true\n }\n\n return false\n}\n\n/**\n * Handle focus events - only activate if focus is on an interactive element within .field-type\n */\nconst onFocusIn = (e: FocusEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n // Early exit if we're already inside the current container\n if (currentContainer?.isConnected && currentContainer.contains(target)) {\n return\n }\n\n // Only activate if the focused element is actually interactive\n if (!isInteractiveElement(target)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n setActiveContainer(container)\n}\n\n/**\n * Handle pointer/mouse events - only switch when clicking a different .field-type\n */\nconst onPointerDown = (e: PointerEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n // Early exit if clicking within current container\n if (currentContainer?.isConnected && currentContainer.contains(target)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n setActiveContainer(container)\n}\n\n/**\n * Handle keyboard navigation (Tab key)\n */\nconst onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== 'Tab') {\n return\n }\n\n // Cancel any pending RAF to prevent queuing\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n }\n\n // Defer until after focus has shifted\n rafId = requestAnimationFrame(() => {\n rafId = null\n const container = resolveContainerFromTarget(document.activeElement)\n setActiveContainer(container)\n })\n}\n\n/**\n * Handle visibility changes to properly cleanup when page is hidden\n */\nconst onVisibilityChange = (): void => {\n if (typeof document !== 'undefined' && document.hidden) {\n // Clear active state and cancel pending operations\n clearActiveContainer()\n }\n}\n\n/**\n * Initialize document-level listeners to track the active field container.\n * When a container is active, it receives the 'ai-plugin-active' class.\n */\nexport const useActiveFieldTracking = (): void => {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return\n }\n\n const pluginWindow = window as {\n __aiComposeTracking?: boolean\n __aiComposeTrackingController?: AbortController\n __aiComposeTrackingCount?: number\n } & Window\n\n // Track number of mounted users of the hook\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 0) + 1\n\n // Initialize listeners only once\n if (!pluginWindow.__aiComposeTracking) {\n const controller = new AbortController()\n pluginWindow.__aiComposeTrackingController = controller\n\n // Use capture for early handling\n document.addEventListener('focusin', onFocusIn, {\n capture: true,\n signal: controller.signal,\n })\n document.addEventListener('pointerdown', onPointerDown, {\n capture: true,\n passive: true,\n signal: controller.signal,\n })\n document.addEventListener('keydown', onKeyDown, {\n capture: true,\n signal: controller.signal,\n })\n document.addEventListener('visibilitychange', onVisibilityChange, {\n signal: controller.signal,\n })\n\n pluginWindow.__aiComposeTracking = true\n }\n\n return () => {\n // Decrement and cleanup when the last user unmounts\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 1) - 1\n\n if ((pluginWindow.__aiComposeTrackingCount ?? 0) <= 0) {\n // Atomically remove all listeners\n pluginWindow.__aiComposeTrackingController?.abort()\n pluginWindow.__aiComposeTrackingController = undefined\n\n // Clear active state and cancel pending operations\n clearActiveContainer()\n\n // Reset all state\n pluginWindow.__aiComposeTracking = false\n pluginWindow.__aiComposeTrackingCount = 0\n }\n }\n }, [])\n}\n"],"names":["useEffect","ALLOWED_FIELD_TYPES","currentContainer","rafId","cssEscape","value","CSS","escape","replace","findContainerFromReactSelect","target","listbox","closest","id","selector","control","document","querySelector","isAllowedFieldType","container","some","type","classList","contains","resolveContainerFromTarget","HTMLElement","setActiveContainer","next","isConnected","remove","add","clearActiveContainer","cancelAnimationFrame","isInteractiveElement","element","tagName","toLowerCase","interactiveTags","includes","isContentEditable","role","getAttribute","onFocusIn","e","onPointerDown","onKeyDown","key","requestAnimationFrame","activeElement","onVisibilityChange","hidden","useActiveFieldTracking","window","pluginWindow","__aiComposeTrackingCount","__aiComposeTracking","controller","AbortController","__aiComposeTrackingController","addEventListener","capture","signal","passive","abort","undefined"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { experimental_useObject as useObject } from '@ai-sdk/react';
|
|
2
2
|
import { useEditorConfigContext } from '@payloadcms/richtext-lexical/client';
|
|
3
3
|
import { toast, useConfig, useDocumentInfo, useField, useForm, useLocale } from '@payloadcms/ui';
|
|
4
4
|
import { jsonSchema } from 'ai';
|
|
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
6
6
|
import { PLUGIN_API_ENDPOINT_GENERATE, PLUGIN_API_ENDPOINT_GENERATE_UPLOAD, PLUGIN_INSTRUCTIONS_TABLE, PLUGIN_NAME } from '../../../defaults.js';
|
|
7
7
|
import { useFieldProps } from '../../../providers/FieldProvider/useFieldProps.js';
|
|
8
8
|
import { editorSchemaValidator } from '../../../utilities/editorSchemaValidator.js';
|
|
9
|
+
import { fieldToJsonSchema } from '../../../utilities/fieldToJsonSchema.js';
|
|
9
10
|
import { setSafeLexicalState } from '../../../utilities/setSafeLexicalState.js';
|
|
10
11
|
import { useHistory } from './useHistory.js';
|
|
11
12
|
export const useGenerate = ({ instructionId })=>{
|
|
@@ -17,7 +18,7 @@ export const useGenerate = ({ instructionId })=>{
|
|
|
17
18
|
}, [
|
|
18
19
|
instructionId
|
|
19
20
|
]);
|
|
20
|
-
const {
|
|
21
|
+
const { field, path: pathFromContext } = useFieldProps();
|
|
21
22
|
const editorConfigContext = useEditorConfigContext();
|
|
22
23
|
const { editor } = editorConfigContext;
|
|
23
24
|
const { config } = useConfig();
|
|
@@ -56,6 +57,24 @@ export const useGenerate = ({ instructionId })=>{
|
|
|
56
57
|
}), [
|
|
57
58
|
memoizedValidator
|
|
58
59
|
]);
|
|
60
|
+
// Active JSON schema for useObject based on field type
|
|
61
|
+
const activeSchema = useMemo(()=>{
|
|
62
|
+
const f = field;
|
|
63
|
+
const fieldType = f?.type;
|
|
64
|
+
if (fieldType === 'richText') {
|
|
65
|
+
return memoizedSchema;
|
|
66
|
+
}
|
|
67
|
+
if (f && f.name && fieldType) {
|
|
68
|
+
const schemaJson = fieldToJsonSchema(f);
|
|
69
|
+
if (schemaJson && Object.keys(schemaJson).length > 0) {
|
|
70
|
+
return jsonSchema(schemaJson);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}, [
|
|
75
|
+
field,
|
|
76
|
+
memoizedSchema
|
|
77
|
+
]);
|
|
59
78
|
const { isLoading: loadingObject, object, stop: objectStop, submit } = useObject({
|
|
60
79
|
api: `/api${PLUGIN_API_ENDPOINT_GENERATE}`,
|
|
61
80
|
onError: (error)=>{
|
|
@@ -63,50 +82,35 @@ export const useGenerate = ({ instructionId })=>{
|
|
|
63
82
|
console.error('Error generating object:', error);
|
|
64
83
|
},
|
|
65
84
|
onFinish: (result)=>{
|
|
66
|
-
if (result.object) {
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
if (result.object && field) {
|
|
86
|
+
if (field.type === 'richText') {
|
|
87
|
+
setHistory(result.object);
|
|
88
|
+
setValue(result.object);
|
|
89
|
+
} else if ('name' in field) {
|
|
90
|
+
setHistory(result.object[field.name]);
|
|
91
|
+
setValue(result.object[field.name]);
|
|
92
|
+
}
|
|
69
93
|
} else {
|
|
70
|
-
console.log('onFinish: result ', result);
|
|
94
|
+
console.log('onFinish: result, field ', result, field);
|
|
71
95
|
}
|
|
72
96
|
},
|
|
73
|
-
schema:
|
|
97
|
+
schema: activeSchema
|
|
74
98
|
});
|
|
75
99
|
useEffect(()=>{
|
|
76
100
|
if (!object) {
|
|
77
101
|
return;
|
|
78
102
|
}
|
|
79
103
|
requestAnimationFrame(()=>{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
if (field?.type === 'richText') {
|
|
105
|
+
setSafeLexicalState(object, editor);
|
|
106
|
+
} else if (field && 'name' in field && object[field.name]) {
|
|
107
|
+
setValue(object[field.name]);
|
|
108
|
+
}
|
|
85
109
|
});
|
|
86
110
|
}, [
|
|
87
111
|
object,
|
|
88
|
-
editor
|
|
89
|
-
|
|
90
|
-
const { complete, completion, isLoading: loadingCompletion, stop: completionStop } = useCompletion({
|
|
91
|
-
api: `${serverURL}${api}${PLUGIN_API_ENDPOINT_GENERATE}`,
|
|
92
|
-
onError: (error)=>{
|
|
93
|
-
toast.error(`Failed to generate: ${error.message}`);
|
|
94
|
-
console.error('Error generating text:', error);
|
|
95
|
-
},
|
|
96
|
-
onFinish: (prompt, result)=>{
|
|
97
|
-
setHistory(result);
|
|
98
|
-
},
|
|
99
|
-
streamProtocol: 'data'
|
|
100
|
-
});
|
|
101
|
-
useEffect(()=>{
|
|
102
|
-
if (!completion) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
requestAnimationFrame(()=>{
|
|
106
|
-
setValue(completion);
|
|
107
|
-
});
|
|
108
|
-
}, [
|
|
109
|
-
completion
|
|
112
|
+
editor,
|
|
113
|
+
field
|
|
110
114
|
]);
|
|
111
115
|
const streamObject = useCallback(({ action = 'Compose', params })=>{
|
|
112
116
|
const doc = getData();
|
|
@@ -130,31 +134,6 @@ export const useGenerate = ({ instructionId })=>{
|
|
|
130
134
|
instructionIdRef,
|
|
131
135
|
documentId
|
|
132
136
|
]);
|
|
133
|
-
const streamText = useCallback(async ({ action = 'Compose', params })=>{
|
|
134
|
-
const doc = getData();
|
|
135
|
-
const currentInstructionId = instructionIdRef.current;
|
|
136
|
-
const options = {
|
|
137
|
-
action,
|
|
138
|
-
actionParams: params,
|
|
139
|
-
instructionId: currentInstructionId
|
|
140
|
-
};
|
|
141
|
-
await complete('', {
|
|
142
|
-
body: {
|
|
143
|
-
doc: {
|
|
144
|
-
...doc,
|
|
145
|
-
id: documentId
|
|
146
|
-
},
|
|
147
|
-
locale: localFromContext?.code,
|
|
148
|
-
options
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}, [
|
|
152
|
-
getData,
|
|
153
|
-
localFromContext?.code,
|
|
154
|
-
instructionIdRef,
|
|
155
|
-
complete,
|
|
156
|
-
documentId
|
|
157
|
-
]);
|
|
158
137
|
const generateUpload = useCallback(async ()=>{
|
|
159
138
|
const doc = getData();
|
|
160
139
|
const currentInstructionId = instructionIdRef.current;
|
|
@@ -201,39 +180,27 @@ export const useGenerate = ({ instructionId })=>{
|
|
|
201
180
|
collectionSlug
|
|
202
181
|
]);
|
|
203
182
|
const generate = useCallback(async (options)=>{
|
|
204
|
-
if (type === '
|
|
205
|
-
return streamObject(options ?? {
|
|
206
|
-
action: 'Compose'
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
if ([
|
|
210
|
-
'text',
|
|
211
|
-
'textarea'
|
|
212
|
-
].includes(type ?? '') && type) {
|
|
213
|
-
return streamText(options ?? {
|
|
214
|
-
action: 'Compose'
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
if (type === 'upload') {
|
|
183
|
+
if (field?.type === 'upload') {
|
|
218
184
|
return generateUpload();
|
|
219
185
|
}
|
|
186
|
+
// All supported types use structured object generation when schema is provided server-side
|
|
187
|
+
return streamObject(options ?? {
|
|
188
|
+
action: 'Compose'
|
|
189
|
+
});
|
|
220
190
|
}, [
|
|
221
191
|
generateUpload,
|
|
222
192
|
streamObject,
|
|
223
|
-
|
|
224
|
-
type
|
|
193
|
+
field
|
|
225
194
|
]);
|
|
226
195
|
const stop = useCallback(()=>{
|
|
227
196
|
console.log('Stopping...');
|
|
228
197
|
objectStop();
|
|
229
|
-
completionStop();
|
|
230
198
|
}, [
|
|
231
|
-
objectStop
|
|
232
|
-
completionStop
|
|
199
|
+
objectStop
|
|
233
200
|
]);
|
|
234
201
|
return {
|
|
235
202
|
generate,
|
|
236
|
-
isLoading:
|
|
203
|
+
isLoading: loadingObject,
|
|
237
204
|
stop
|
|
238
205
|
};
|
|
239
206
|
};
|