Skip to content

Commit

Permalink
feat: improve the Smart SEO AI code and fix the issue where the conte…
Browse files Browse the repository at this point in the history
…nt input field was flickering (#172)

Thanks to Pavel!
  • Loading branch information
swapnilmmane authored Jan 2, 2025
1 parent 4267c88 commit 3c73e54
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
"webiny-extension-type:admin"
],
"dependencies": {
"openai": "^4.33.0",
"react": "18.2.0"
"react": "18.2.0",
"graphql-tag": "2.12.6",
"@material-design-icons/svg": "0.14.13",
"@webiny/app-admin": "5.41.4",
"@webiny/app-headless-cms": "5.41.4",
"@webiny/form": "5.41.4",
"@webiny/ui": "5.41.4",
"@webiny/lexical-converter": "5.41.4"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ export const DecorateContentEntryFormBind = useBind.createDecorator(baseHook =>
useEffect(() => {
// Track rich-text fields and SEO fields
if (field.type === "rich-text") {
trackField(field.label, field.type, params.name, bind.value, bind.onChange);
trackField(field.label, field.type, params.name, bind.value);
}

if (seoFields.includes(field.fieldId) && !parent) {
trackField(field.label, field.fieldId, params.name, bind.value, bind.onChange);
trackField(field.label, field.fieldId, params.name, bind.value);
}
}, [bind.value]);
}, [JSON.stringify(bind.value)]);

return bind;
} catch {
return baseHook(params);
}
};
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface FieldWithValue {
path: string;
type: string;
label: string;
onChange: (value: any) => void;
}

interface FieldTrackerContext {
Expand All @@ -20,7 +19,6 @@ interface FieldTrackerContext {
type: string,
path: string,
value: any,
onChange: (value: any) => void
) => void;
}

Expand All @@ -33,14 +31,13 @@ const FieldTrackerContext = React.createContext<FieldTrackerContext | undefined>
export const FieldTracker = ({ children }: FieldTrackerProps) => {
const [fields, setFields] = useState<FieldWithValue[]>([]);

const trackField = useCallback((label:string, type:string, path:string, value:any, onChange: any) => {
const trackField = useCallback((label:string, type:string, path:string, value:any) => {
setFields(fields => {
const newValue: FieldWithValue = {
label,
type,
path,
value,
onChange
};

const index = fields.findIndex(trackedField => trackedField.path === path);
Expand All @@ -66,4 +63,4 @@ export const useFieldTracker = () => {
}

return context;
};
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from "react";
import { ContentEntryEditorConfig, useQuery } from "@webiny/app-headless-cms";
import React, { useState } from "react";
import gql from "graphql-tag";
import { useForm } from "@webiny/form";
import { ContentEntryEditorConfig, useApolloClient } from "@webiny/app-headless-cms";
import { ButtonSecondary, ButtonIcon } from "@webiny/ui/Button";
import { ReactComponent as MagicIcon } from "@material-design-icons/svg/round/school.svg";
import { useSnackbar } from "@webiny/app-admin";
import { FieldWithValue, useFieldTracker } from "./FieldTracker";
import { extractRichTextHtml } from "./extractFromRichText";
import gql from "graphql-tag";

const { Actions } = ContentEntryEditorConfig;

Expand All @@ -20,51 +21,42 @@ const GENERATE_SEO_QUERY = gql`
`;

const GetSeoData = () => {
const client = useApolloClient();
const form = useForm();
const { showSnackbar } = useSnackbar();
const [triggerQuery, setTriggerQuery] = useState(false);
const [loading, setLoading] = useState(false);
const { fields } = useFieldTracker();

const { data, error, refetch } = useQuery(GENERATE_SEO_QUERY, {
variables: {
input: {
content: extractRichTextHtml(fields).join("\n"),
},
},
skip: true, // Prevent automatic execution of the query
});

useEffect(() => {
if (triggerQuery) {
setLoading(true);
refetch()
.then(({ data }) => {
const seo = data?.generateSeo;
if (!seo) {
console.error("Invalid response received from AI.");
showSnackbar("No valid data received from AI.");
return;
const askChatGpt = async () => {
setLoading(true);
try {
const { data } = await client.query({
query: GENERATE_SEO_QUERY,
variables: {
input: {
content: extractRichTextHtml(fields).join("\n")
}

populateSeoTitle(fields, seo.title);
populateSeoDescription(fields, seo.description);
populateSeoKeywords(fields, seo.keywords);

showSnackbar("Success! We've populated the SEO fields with the recommended values.");
})
.catch((err) => {
console.error("Error during SEO generation:", err);
showSnackbar("We were unable to get a recommendation from AI at this point.");
})
.finally(() => {
setLoading(false);
setTriggerQuery(false);
});
}
});

const seo = data?.generateSeo;
if (!seo) {
console.error("Invalid response received from AI.");
showSnackbar("No valid data received from AI.");
return;
}

populateSeoTitle(form, fields, seo.title);
populateSeoDescription(form, fields, seo.description);
populateSeoKeywords(form, fields, seo.keywords);

showSnackbar("Success! We've populated the SEO fields with the recommended values.");
} catch (err) {
console.error("Error during SEO generation:", err);
showSnackbar("We were unable to get a recommendation from AI at this point.");
} finally {
setLoading(false);
}
}, [triggerQuery, refetch, fields, showSnackbar]);

const askChatGpt = () => {
setTriggerQuery(true);
};

return (
Expand All @@ -74,35 +66,53 @@ const GetSeoData = () => {
);
};

const populateSeoTitle = (fields: FieldWithValue[], value: string) => {
const field = fields.find((field) => field.type === "seoTitle");
if (field) field.onChange(value);
const populateSeoTitle = (
form: ReturnType<typeof useForm>,
fields: FieldWithValue[],
value: string
) => {
const field = fields.find(field => field.type === "seoTitle");
if (field) {
form.setValue(field.path, value);
}
};

const populateSeoDescription = (fields: FieldWithValue[], value: string) => {
const field = fields.find((field) => field.type === "seoDescription");
if (field) field.onChange(value);
const populateSeoDescription = (
form: ReturnType<typeof useForm>,
fields: FieldWithValue[],
value: string
) => {
const field = fields.find(field => field.type === "seoDescription");
if (field) {
form.setValue(field.path, value);
}
};

interface Tag {
tagName: string;
tagValue: string;
}

const populateSeoKeywords = (fields: FieldWithValue[], keywords: string[]) => {
const field = fields.find((field) => field.type === "seoMetaTags");
const populateSeoKeywords = (
form: ReturnType<typeof useForm>,
fields: FieldWithValue[],
keywords: string[]
) => {
const field = fields.find(field => field.type === "seoMetaTags");
if (!field) {
console.warn("No meta tags field!");
return;
}

const tags: Tag[] = Array.isArray(field.value) ? field.value : [];
const tagsWithoutKeywords = tags.filter((tag) => tag.tagName !== "keywords");
const tagsWithoutKeywords = tags.filter(tag => tag.tagName !== "keywords");

field.onChange([
...tagsWithoutKeywords,
{ tagName: "keywords", tagValue: keywords.join(", ") },
]);
if (field) {
form.setValue(field.path, [
...tagsWithoutKeywords,
{ tagName: "keywords", tagValue: keywords.join(", ") }
]);
}
};

export const SmartSeo = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
],
"version": "1.0.0",
"dependencies": {
"@webiny/api-headless-cms": "5.41.1"
"@webiny/api-headless-cms": "5.41.4",
"@webiny/api-serverless-cms": "5.41.4",
"openai": "^4.33.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import {
createCmsGroupPlugin,
createCmsModelPlugin,
createModelField
} from "@webiny/api-headless-cms";

export const Article = () => {
return [
// Defines a new "Smart SEO" content model group.
createCmsGroupPlugin({
id: "smart-seo",
name: "Smart SEO",
description: "Smart SEO content model group",
slug: "smart-seo",
icon: "fas/magnifying-glass"
}),

// Defines a new "Article - Smart SEO" content model.
createCmsModelPlugin({
name: "Article - Smart SEO",
modelId: "article-smart-seo",
description: "Article content model for Smart SEO",
group: {id: "", name: ""},
group: {id: "smart-seo", name: "Smart SEO"},
fields: [
createModelField({
fieldId: "content",
Expand All @@ -21,7 +32,7 @@ export const Article = () => {
fieldId: "seoTitle",
type: "text",
label: "SEO - Title",
renderer: { name: "text-input" },
renderer: { name: "text-input" }
}),
createModelField({
fieldId: "seoDescription",
Expand All @@ -34,6 +45,7 @@ export const Article = () => {
type: "object",
label: "SEO - Meta tags",
renderer: { name: "objects" },
multipleValues: true,
settings: {
fields: [
createModelField({
Expand All @@ -56,7 +68,7 @@ export const Article = () => {
})
],
layout: [["content"], ["seoTitle"], ["seoDescription"], ["seoMetaTags"]],
titleFieldId: "content"
titleFieldId: "seoTitle"
})
];
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms";
import OpenAI from "openai";
import { Context } from "@webiny/api-serverless-cms"

/*
* This file adds a GraphQL schema for generating SEO metadata.
Expand All @@ -15,7 +16,7 @@ const OPENAI_API_KEY = process.env["WEBINY_API_OPEN_AI_API_KEY"];
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

export const generateSeo = () => [
new CmsGraphQLSchemaPlugin({
new CmsGraphQLSchemaPlugin<Context>({
typeDefs: `
type SeoData {
title: String
Expand Down

0 comments on commit 3c73e54

Please sign in to comment.