Skip to content

Commit b7ff34f

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 503bd9d + e4ac713 commit b7ff34f

File tree

10 files changed

+207
-106
lines changed

10 files changed

+207
-106
lines changed

cms-live-preview/5.40.x/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To enable these extensions in your existing Webiny project, follow these steps:
1313
In your project, run:
1414

1515
```shell
16-
yarn webiny scaffold extension cms-live-preview
16+
yarn webiny extension cms-live-preview
1717
```
1818

1919
### 2. Configure the `website` app
@@ -30,14 +30,15 @@ Add component styles to the `website` app, in `apps/website/public/index.html`:
3030

3131
Add live preview route in `apps/website/src/App.tsx`:
3232

33-
```tsx
33+
```diff tsx
3434
import React from "react";
3535
import { Website } from "@webiny/app-website";
36-
import { createLivePreviewRoute } from "@demo/live-preview-website";
36+
+ import { createLivePreviewRoute } from "@demo/website";
3737
import "./App.scss";
3838

3939
export const App = () => {
40-
return <Website routes={[createLivePreviewRoute()]} />;
40+
- return <Website/>;
41+
+ return <Website routes={[createLivePreviewRoute()]} />;
4142
};
4243
```
4344

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/admin/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
"webiny-extension-type:admin"
88
],
99
"dependencies": {
10-
"openai": "^4.33.0",
11-
"react": "18.2.0"
10+
"react": "18.2.0",
11+
"graphql-tag": "2.12.6",
12+
"@material-design-icons/svg": "0.14.13",
13+
"@webiny/app-admin": "5.41.4",
14+
"@webiny/app-headless-cms": "5.41.4",
15+
"@webiny/form": "5.41.4",
16+
"@webiny/ui": "5.41.4",
17+
"@webiny/lexical-converter": "5.41.4"
1218
}
1319
}

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/admin/src/DecorateContentEntryFormBind.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ export const DecorateContentEntryFormBind = useBind.createDecorator(baseHook =>
3333
useEffect(() => {
3434
// Track rich-text fields and SEO fields
3535
if (field.type === "rich-text") {
36-
trackField(field.label, field.type, params.name, bind.value, bind.onChange);
36+
trackField(field.label, field.type, params.name, bind.value);
3737
}
3838

3939
if (seoFields.includes(field.fieldId) && !parent) {
40-
trackField(field.label, field.fieldId, params.name, bind.value, bind.onChange);
40+
trackField(field.label, field.fieldId, params.name, bind.value);
4141
}
42-
}, [bind.value]);
42+
}, [JSON.stringify(bind.value)]);
4343

4444
return bind;
4545
} catch {
4646
return baseHook(params);
4747
}
4848
};
49-
});
49+
});

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/admin/src/FieldTracker.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export interface FieldWithValue {
99
path: string;
1010
type: string;
1111
label: string;
12-
onChange: (value: any) => void;
1312
}
1413

1514
interface FieldTrackerContext {
@@ -20,7 +19,6 @@ interface FieldTrackerContext {
2019
type: string,
2120
path: string,
2221
value: any,
23-
onChange: (value: any) => void
2422
) => void;
2523
}
2624

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

36-
const trackField = useCallback((label:string, type:string, path:string, value:any, onChange: any) => {
34+
const trackField = useCallback((label:string, type:string, path:string, value:any) => {
3735
setFields(fields => {
3836
const newValue: FieldWithValue = {
3937
label,
4038
type,
4139
path,
4240
value,
43-
onChange
4441
};
4542

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

6865
return context;
69-
};
66+
};
Lines changed: 86 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,120 @@
11
import React, { useState } from "react";
2-
import OpenAI from "openai";
3-
import { ReactComponent as MagicIcon } from "@material-design-icons/svg/round/school.svg";
4-
import { ContentEntryEditorConfig } from "@webiny/app-headless-cms";
2+
import gql from "graphql-tag";
3+
import { useForm } from "@webiny/form";
4+
import { ContentEntryEditorConfig, useApolloClient } from "@webiny/app-headless-cms";
55
import { ButtonSecondary, ButtonIcon } from "@webiny/ui/Button";
6+
import { ReactComponent as MagicIcon } from "@material-design-icons/svg/round/school.svg";
7+
import { useSnackbar } from "@webiny/app-admin";
68
import { FieldWithValue, useFieldTracker } from "./FieldTracker";
79
import { extractRichTextHtml } from "./extractFromRichText";
8-
import { useSnackbar } from "@webiny/app-admin";
9-
10-
const OPENAI_API_KEY = String(process.env["WEBINY_ADMIN_OPEN_AI_API_KEY"]);
11-
12-
const openai = new OpenAI({ apiKey: OPENAI_API_KEY, dangerouslyAllowBrowser: true });
13-
14-
const prompt = `You will be provided with one or more paragraphs of HTML, and you need to extract a SEO optimized page title, a page summary, and up to 5 keywords. Response should be returned as a plain JSON object, with "title" field for the page title, "description" field for page summary, and "keywords" field as an array of keywords.`;
1510

1611
const { Actions } = ContentEntryEditorConfig;
1712

18-
const populateSeoTitle = (fields: FieldWithValue[], value: string) => {
19-
const field = fields.find(field => field.type === "seoTitle");
20-
if (!field) {
21-
return;
22-
}
23-
24-
field.onChange(value);
25-
};
26-
27-
const populateSeoDescription = (fields: FieldWithValue[], value: string) => {
28-
const field = fields.find(field => field.type === "seoDescription");
29-
if (!field) {
30-
return;
31-
}
32-
33-
field.onChange(value);
34-
};
35-
36-
interface Tag {
37-
tagName: string;
38-
tagValue: string;
39-
}
40-
41-
const populateSeoKeywords = (fields: FieldWithValue[], keywords: string[]) => {
42-
const field = fields.find(field => field.type === "seoMetaTags");
43-
if (!field) {
44-
console.warn("no meta tags field!");
45-
return;
13+
const GENERATE_SEO_QUERY = gql`
14+
query GenerateSeo($input: GenerateSeoInput!) {
15+
generateSeo(input: $input) {
16+
title
17+
description
18+
keywords
19+
}
4620
}
47-
const tags: Tag[] = Array.isArray(field.value) ? field.value : [];
48-
const tagsWithoutKeywords = tags.filter(tag => tag.tagName !== "keywords");
21+
`;
4922

50-
field.onChange([
51-
...tagsWithoutKeywords,
52-
{ tagName: "keywords", tagValue: keywords.join(", ") }
53-
]);
54-
};
55-
56-
/**
57-
* A button component to trigger OpenAI's GPT model to generate SEO fields.
58-
* Extracts rich-text content, sends it to GPT, and updates the form fields with the AI's suggestions.
59-
*/
6023
const GetSeoData = () => {
24+
const client = useApolloClient();
25+
const form = useForm();
6126
const { showSnackbar } = useSnackbar();
6227
const [loading, setLoading] = useState(false);
6328
const { fields } = useFieldTracker();
6429

6530
const askChatGpt = async () => {
66-
let response;
6731
setLoading(true);
6832
try {
69-
response = await openai.chat.completions.create({
70-
model: "gpt-3.5-turbo",
71-
messages: [
72-
{
73-
role: "system",
74-
content: prompt
75-
},
76-
{
77-
role: "user",
33+
const { data } = await client.query({
34+
query: GENERATE_SEO_QUERY,
35+
variables: {
36+
input: {
7837
content: extractRichTextHtml(fields).join("\n")
7938
}
80-
],
81-
temperature: 0.5,
82-
max_tokens: 128,
83-
top_p: 1
39+
}
8440
});
85-
} catch (e) {
86-
console.log(e);
87-
}
88-
setLoading(false);
8941

90-
console.log("ChatGPT response", response);
91-
try {
92-
// const seo = {
93-
// title: "Node.js, Yarn, and AWS Setup Guide for Webiny",
94-
// description:
95-
// "Learn how to set up Node.js, yarn, and AWS account and user credentials for deploying Webiny. Make sure you have the required versions installed.",
96-
// keywords: ["Node.js", "Yarn", "AWS", "Webiny", "setup"]
97-
// };
98-
const seo = JSON.parse(response?.choices[0].message.content as string);
99-
console.log("parsed response", seo);
100-
populateSeoTitle(fields, seo.title);
101-
populateSeoDescription(fields, seo.description);
102-
populateSeoKeywords(fields, seo.keywords);
42+
const seo = data?.generateSeo;
43+
if (!seo) {
44+
console.error("Invalid response received from AI.");
45+
showSnackbar("No valid data received from AI.");
46+
return;
47+
}
48+
49+
populateSeoTitle(form, fields, seo.title);
50+
populateSeoDescription(form, fields, seo.description);
51+
populateSeoKeywords(form, fields, seo.keywords);
52+
10353
showSnackbar("Success! We've populated the SEO fields with the recommended values.");
104-
} catch (e) {
105-
console.log(e);
54+
} catch (err) {
55+
console.error("Error during SEO generation:", err);
10656
showSnackbar("We were unable to get a recommendation from AI at this point.");
57+
} finally {
58+
setLoading(false);
10759
}
10860
};
10961

11062
return (
111-
<ButtonSecondary onClick={() => askChatGpt()} disabled={loading}>
63+
<ButtonSecondary onClick={askChatGpt} disabled={loading}>
11264
<ButtonIcon icon={<MagicIcon />} /> AI-optimized SEO
11365
</ButtonSecondary>
11466
);
11567
};
11668

69+
const populateSeoTitle = (
70+
form: ReturnType<typeof useForm>,
71+
fields: FieldWithValue[],
72+
value: string
73+
) => {
74+
const field = fields.find(field => field.type === "seoTitle");
75+
if (field) {
76+
form.setValue(field.path, value);
77+
}
78+
};
79+
80+
const populateSeoDescription = (
81+
form: ReturnType<typeof useForm>,
82+
fields: FieldWithValue[],
83+
value: string
84+
) => {
85+
const field = fields.find(field => field.type === "seoDescription");
86+
if (field) {
87+
form.setValue(field.path, value);
88+
}
89+
};
90+
91+
interface Tag {
92+
tagName: string;
93+
tagValue: string;
94+
}
95+
96+
const populateSeoKeywords = (
97+
form: ReturnType<typeof useForm>,
98+
fields: FieldWithValue[],
99+
keywords: string[]
100+
) => {
101+
const field = fields.find(field => field.type === "seoMetaTags");
102+
if (!field) {
103+
console.warn("No meta tags field!");
104+
return;
105+
}
106+
107+
const tags: Tag[] = Array.isArray(field.value) ? field.value : [];
108+
const tagsWithoutKeywords = tags.filter(tag => tag.tagName !== "keywords");
109+
110+
if (field) {
111+
form.setValue(field.path, [
112+
...tagsWithoutKeywords,
113+
{ tagName: "keywords", tagValue: keywords.join(", ") }
114+
]);
115+
}
116+
};
117+
117118
export const SmartSeo = () => {
118119
return (
119120
<Actions.ButtonAction
@@ -123,4 +124,4 @@ export const SmartSeo = () => {
123124
modelIds={["article-smart-seo"]}
124125
/>
125126
);
126-
};
127+
};

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/admin/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DecorateContentEntryFormBind} from "./DecorateContentEntryFormBind";
55
import { SmartSeo } from "./SmartSeo";
66

77
export const Extension = () => {
8-
return <>{/* Your code here. */}
8+
return <>
99
<DecorateContentEntryForm />
1010
<ContentEntryEditorConfig>
1111
<SmartSeo />

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/api/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
],
88
"version": "1.0.0",
99
"dependencies": {
10-
"@webiny/api-headless-cms": "5.41.1"
10+
"@webiny/api-headless-cms": "5.41.4",
11+
"@webiny/api-serverless-cms": "5.41.4",
12+
"openai": "^4.33.0"
1113
}
1214
}

headless-cms/smart-seo-open-ai/5.41.x/extensions/smartSeoOpenAi/api/src/Article.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import {
2+
createCmsGroupPlugin,
23
createCmsModelPlugin,
34
createModelField
45
} from "@webiny/api-headless-cms";
56

67
export const Article = () => {
78
return [
9+
// Defines a new "Smart SEO" content model group.
10+
createCmsGroupPlugin({
11+
id: "smart-seo",
12+
name: "Smart SEO",
13+
description: "Smart SEO content model group",
14+
slug: "smart-seo",
15+
icon: "fas/magnifying-glass"
16+
}),
17+
18+
// Defines a new "Article - Smart SEO" content model.
819
createCmsModelPlugin({
920
name: "Article - Smart SEO",
1021
modelId: "article-smart-seo",
1122
description: "Article content model for Smart SEO",
12-
group: {id: "", name: ""},
23+
group: {id: "smart-seo", name: "Smart SEO"},
1324
fields: [
1425
createModelField({
1526
fieldId: "content",
@@ -21,7 +32,7 @@ export const Article = () => {
2132
fieldId: "seoTitle",
2233
type: "text",
2334
label: "SEO - Title",
24-
renderer: { name: "text-input" },
35+
renderer: { name: "text-input" }
2536
}),
2637
createModelField({
2738
fieldId: "seoDescription",
@@ -34,6 +45,7 @@ export const Article = () => {
3445
type: "object",
3546
label: "SEO - Meta tags",
3647
renderer: { name: "objects" },
48+
multipleValues: true,
3749
settings: {
3850
fields: [
3951
createModelField({
@@ -56,7 +68,7 @@ export const Article = () => {
5668
})
5769
],
5870
layout: [["content"], ["seoTitle"], ["seoDescription"], ["seoMetaTags"]],
59-
titleFieldId: "content"
71+
titleFieldId: "seoTitle"
6072
})
6173
];
6274
};

0 commit comments

Comments
 (0)