@ai-stack/payloadcms
Version:
<p align="center"> <img alt="Payload AI Plugin" src="assets/payload-ai-intro.gif" width="100%" /> </p>
208 lines (207 loc) • 7.58 kB
JavaScript
import { experimental_useObject as useObject } from '@ai-sdk/react';
import { useEditorConfigContext } from '@payloadcms/richtext-lexical/client';
import { toast, useConfig, useDocumentInfo, useField, useForm, useLocale } from '@payloadcms/ui';
import { jsonSchema } from 'ai';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { PLUGIN_API_ENDPOINT_GENERATE, PLUGIN_API_ENDPOINT_GENERATE_UPLOAD, PLUGIN_INSTRUCTIONS_TABLE, PLUGIN_NAME } from '../../../defaults.js';
import { useFieldProps } from '../../../providers/FieldProvider/useFieldProps.js';
import { editorSchemaValidator } from '../../../utilities/editorSchemaValidator.js';
import { fieldToJsonSchema } from '../../../utilities/fieldToJsonSchema.js';
import { setSafeLexicalState } from '../../../utilities/setSafeLexicalState.js';
import { useHistory } from './useHistory.js';
export const useGenerate = ({ instructionId })=>{
// Create a ref to hold the current instructionId
const instructionIdRef = useRef(instructionId);
// Update the ref whenever instructionId changes
useEffect(()=>{
instructionIdRef.current = instructionId;
}, [
instructionId
]);
const { field, path: pathFromContext } = useFieldProps();
const editorConfigContext = useEditorConfigContext();
const { editor } = editorConfigContext;
const { config } = useConfig();
const { routes: { api }, serverURL } = config;
const { setValue } = useField({
path: pathFromContext ?? ''
});
const { set: setHistory } = useHistory();
const { getData } = useForm();
const { id: documentId, collectionSlug } = useDocumentInfo();
const localFromContext = useLocale();
const { config: { collections } } = useConfig();
const collection = collections.find((collection)=>collection.slug === PLUGIN_INSTRUCTIONS_TABLE);
const { custom: { [PLUGIN_NAME]: { editorConfig = {} } = {} } = {} } = collection?.admin ?? {};
const { schema: editorSchema = {} } = editorConfig;
const memoizedValidator = useMemo(()=>{
return editorSchemaValidator(editorSchema);
}, [
editorSchema
]);
const memoizedSchema = useMemo(()=>jsonSchema(editorSchema, {
validate: (value)=>{
const isValid = memoizedValidator(value);
if (isValid) {
return {
success: true,
value
};
} else {
return {
error: new Error('Invalid schema'),
success: false
};
}
}
}), [
memoizedValidator
]);
// Active JSON schema for useObject based on field type
const activeSchema = useMemo(()=>{
const f = field;
const fieldType = f?.type;
if (fieldType === 'richText') {
return memoizedSchema;
}
if (f && f.name && fieldType) {
const schemaJson = fieldToJsonSchema(f);
if (schemaJson && Object.keys(schemaJson).length > 0) {
return jsonSchema(schemaJson);
}
}
return undefined;
}, [
field,
memoizedSchema
]);
const { isLoading: loadingObject, object, stop: objectStop, submit } = useObject({
api: `/api${PLUGIN_API_ENDPOINT_GENERATE}`,
onError: (error)=>{
toast.error(`Failed to generate: ${error.message}`);
console.error('Error generating object:', error);
},
onFinish: (result)=>{
if (result.object && field) {
if (field.type === 'richText') {
setHistory(result.object);
setValue(result.object);
} else if ('name' in field) {
setHistory(result.object[field.name]);
setValue(result.object[field.name]);
}
} else {
console.log('onFinish: result, field ', result, field);
}
},
schema: activeSchema
});
useEffect(()=>{
if (!object) {
return;
}
requestAnimationFrame(()=>{
if (field?.type === 'richText') {
setSafeLexicalState(object, editor);
} else if (field && 'name' in field && object[field.name]) {
setValue(object[field.name]);
}
});
}, [
object,
editor,
field
]);
const streamObject = useCallback(({ action = 'Compose', params })=>{
const doc = getData();
const currentInstructionId = instructionIdRef.current;
const options = {
action,
actionParams: params,
instructionId: currentInstructionId
};
submit({
allowedEditorNodes: Array.from(editor?._nodes?.keys() || []),
doc: {
...doc,
id: documentId
},
locale: localFromContext?.code,
options
});
}, [
localFromContext?.code,
instructionIdRef,
documentId
]);
const generateUpload = useCallback(async ()=>{
const doc = getData();
const currentInstructionId = instructionIdRef.current;
return fetch(`${serverURL}${api}${PLUGIN_API_ENDPOINT_GENERATE_UPLOAD}`, {
body: JSON.stringify({
collectionSlug: collectionSlug ?? '',
doc,
documentId,
locale: localFromContext?.code,
options: {
instructionId: currentInstructionId
}
}),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}).then(async (uploadResponse)=>{
if (uploadResponse.ok) {
const { result } = await uploadResponse.json();
if (!result) {
throw new Error('generateUpload: Something went wrong');
}
setValue(result?.id);
setHistory(result?.id);
console.log('Image updated...', result);
} else {
const { errors = [] } = await uploadResponse.json();
const errStr = errors.map((error)=>error.message).join(', ');
throw new Error(errStr);
}
return uploadResponse;
}).catch((error)=>{
toast.error(`Failed to generate: ${error.message}`);
console.error('Error generating or setting your upload, please set it manually if its saved in your media files.', error);
});
}, [
getData,
localFromContext?.code,
instructionIdRef,
setValue,
documentId,
collectionSlug
]);
const generate = useCallback(async (options)=>{
if (field?.type === 'upload') {
return generateUpload();
}
// All supported types use structured object generation when schema is provided server-side
return streamObject(options ?? {
action: 'Compose'
});
}, [
generateUpload,
streamObject,
field
]);
const stop = useCallback(()=>{
console.log('Stopping...');
objectStop();
}, [
objectStop
]);
return {
generate,
isLoading: loadingObject,
stop
};
};
//# sourceMappingURL=useGenerate.js.map