datocms-plugin-alt-text-ai 0.1.0
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/README.md +4 -0
- package/docs/cover.png +0 -0
- package/docs/demo.mp4 +0 -0
- package/package.json +65 -0
- package/public/index.html +11 -0
- package/public/robots.txt +3 -0
- package/src/entrypoints/AltTextAIButton.tsx +102 -0
- package/src/entrypoints/ConfigScreen.tsx +66 -0
- package/src/entrypoints/styles.module.css +7 -0
- package/src/index.tsx +27 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/utils/render.tsx +9 -0
- package/tsconfig.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Alt Text AI
|
|
2
|
+
|
|
3
|
+
Alt Text AI is a plugin that serves as an integration bridge between DatoCMS and Alt Text AI. Its allow for the generation of alt text directly within the DatoCMS dashboard using the Alt Text AI API. Leveraging AI, it swiftly produces contextually relevant alt text for images: enhancing SEO, accessibility, and the overall user experience.
|
|
4
|
+
|
package/docs/cover.png
ADDED
|
Binary file
|
package/docs/demo.mp4
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "datocms-plugin-alt-text-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"homepage": "https://github.com/marcelofinamorvieira/datocms-plugin-alt-text-ai",
|
|
6
|
+
"description": "A plugin that creates daily and weekly backups of your main environment on DatoCMS automatically.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"datocms",
|
|
9
|
+
"datocms-plugin",
|
|
10
|
+
"ai",
|
|
11
|
+
"alt",
|
|
12
|
+
"media",
|
|
13
|
+
"assets"
|
|
14
|
+
],
|
|
15
|
+
"datoCmsPlugin": {
|
|
16
|
+
"title": "Alt Text AI",
|
|
17
|
+
"previewImage": "docs/demo.mp4",
|
|
18
|
+
"coverImage": "docs/cover.png",
|
|
19
|
+
"entryPoint": "build/index.html",
|
|
20
|
+
"permissions": [
|
|
21
|
+
"currentUserAccessToken"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@datocms/cma-client-browser": "^2.2.6",
|
|
26
|
+
"@types/node": "^18.18.6",
|
|
27
|
+
"@types/react": "^18.2.31",
|
|
28
|
+
"@types/react-dom": "^18.2.14",
|
|
29
|
+
"datocms-plugin-sdk": "^1.0.0",
|
|
30
|
+
"datocms-react-ui": "^1.0.0",
|
|
31
|
+
"lodash": "^4.17.21",
|
|
32
|
+
"react": "^18.2.0",
|
|
33
|
+
"react-dom": "^18.2.0",
|
|
34
|
+
"react-scripts": "5.0.1",
|
|
35
|
+
"typescript": "^4.9.5"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"start": "cross-env BROWSER='none' PUBLIC_URL='/' react-scripts start",
|
|
39
|
+
"build": "cross-env PUBLIC_URL='.' react-scripts build",
|
|
40
|
+
"test": "react-scripts test",
|
|
41
|
+
"eject": "react-scripts eject",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"eslintConfig": {
|
|
45
|
+
"extends": [
|
|
46
|
+
"react-app"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"browserslist": {
|
|
50
|
+
"production": [
|
|
51
|
+
">0.2%",
|
|
52
|
+
"not dead",
|
|
53
|
+
"not op_mini all"
|
|
54
|
+
],
|
|
55
|
+
"development": [
|
|
56
|
+
"last 1 chrome version",
|
|
57
|
+
"last 1 firefox version",
|
|
58
|
+
"last 1 safari version"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/lodash": "^4.14.200",
|
|
63
|
+
"cross-env": "^7.0.3"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Client, buildClient } from '@datocms/cma-client-browser';
|
|
2
|
+
import { RenderFieldExtensionCtx } from 'datocms-plugin-sdk';
|
|
3
|
+
import { Button, Canvas, Spinner } from 'datocms-react-ui';
|
|
4
|
+
import { isArray } from 'lodash';
|
|
5
|
+
import get from 'lodash/get';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
type PropTypes = {
|
|
9
|
+
ctx: RenderFieldExtensionCtx;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function fetchAlt(
|
|
13
|
+
apiKey: string,
|
|
14
|
+
client: Client,
|
|
15
|
+
asset: Record<string, string>
|
|
16
|
+
) {
|
|
17
|
+
const { url } = await client.uploads.find(asset.upload_id);
|
|
18
|
+
|
|
19
|
+
const result = await (
|
|
20
|
+
await fetch('https://alttext.ai/api/v1/images', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'X-API-Key': apiKey ?? '',
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
image: {
|
|
28
|
+
url,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
).json();
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function generateAlts(
|
|
38
|
+
currentFieldValue: unknown,
|
|
39
|
+
ctx: RenderFieldExtensionCtx,
|
|
40
|
+
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
|
|
41
|
+
) {
|
|
42
|
+
setIsLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
const client = buildClient({ apiToken: ctx.currentUserAccessToken || '' });
|
|
45
|
+
const isGallery = isArray(currentFieldValue);
|
|
46
|
+
if (isGallery) {
|
|
47
|
+
const newAssets = [];
|
|
48
|
+
for (const asset of currentFieldValue) {
|
|
49
|
+
const result = await fetchAlt(
|
|
50
|
+
ctx.plugin.attributes.parameters.apiKey as string,
|
|
51
|
+
client,
|
|
52
|
+
asset
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
asset.alt = result.alt_text;
|
|
56
|
+
newAssets.push(asset);
|
|
57
|
+
}
|
|
58
|
+
ctx.setFieldValue(ctx.fieldPath, newAssets);
|
|
59
|
+
} else {
|
|
60
|
+
const assetValue = currentFieldValue as Record<string, string>;
|
|
61
|
+
|
|
62
|
+
const result = await fetchAlt(
|
|
63
|
+
ctx.plugin.attributes.parameters.apiKey as string,
|
|
64
|
+
client,
|
|
65
|
+
assetValue
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
assetValue.alt = result.alt_text;
|
|
69
|
+
ctx.setFieldValue(ctx.fieldPath, assetValue);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.log(error);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const AltTextAIButton = ({ ctx }: PropTypes) => {
|
|
79
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
80
|
+
const currentFieldValue = get(ctx.formValues, ctx.fieldPath);
|
|
81
|
+
const isGallery = isArray(currentFieldValue);
|
|
82
|
+
const isGalleryWithItems =
|
|
83
|
+
isArray(currentFieldValue) && currentFieldValue.length;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Canvas ctx={ctx}>
|
|
87
|
+
{(isGalleryWithItems || (!isGallery && !!currentFieldValue)) && (
|
|
88
|
+
<Button
|
|
89
|
+
fullWidth
|
|
90
|
+
disabled={isLoading}
|
|
91
|
+
onClick={() => {
|
|
92
|
+
generateAlts(currentFieldValue, ctx, setIsLoading);
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
Generate Alt Text {isLoading && <Spinner size={24} />}
|
|
96
|
+
</Button>
|
|
97
|
+
)}
|
|
98
|
+
</Canvas>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default AltTextAIButton;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RenderConfigScreenCtx } from 'datocms-plugin-sdk';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Canvas,
|
|
5
|
+
ContextInspector,
|
|
6
|
+
Spinner,
|
|
7
|
+
TextField,
|
|
8
|
+
} from 'datocms-react-ui';
|
|
9
|
+
import s from './styles.module.css';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
ctx: RenderConfigScreenCtx;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function saveApiKey(
|
|
17
|
+
ctx: RenderConfigScreenCtx,
|
|
18
|
+
apiKey: string,
|
|
19
|
+
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
|
|
20
|
+
) {
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
await ctx.updatePluginParameters({ apiKey });
|
|
23
|
+
setIsLoading(false);
|
|
24
|
+
ctx.customToast({
|
|
25
|
+
type: 'notice',
|
|
26
|
+
message: 'API Key Saved!',
|
|
27
|
+
dismissOnPageChange: true,
|
|
28
|
+
dismissAfterTimeout: 5000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function ConfigScreen({ ctx }: Props) {
|
|
33
|
+
const [apiKey, setApiKey] = useState(
|
|
34
|
+
(ctx.plugin.attributes.parameters.apiKey as string) ?? ''
|
|
35
|
+
);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Canvas ctx={ctx}>
|
|
40
|
+
<div className={s.form}>
|
|
41
|
+
<TextField
|
|
42
|
+
required
|
|
43
|
+
name="altTextAPIKey"
|
|
44
|
+
id="altTextAPIKey"
|
|
45
|
+
label="Alt Text API Key"
|
|
46
|
+
value={apiKey}
|
|
47
|
+
onChange={(newValue) => setApiKey(newValue)}
|
|
48
|
+
/>
|
|
49
|
+
<p>
|
|
50
|
+
You can get your API key by going to{' '}
|
|
51
|
+
<a href="https://alttext.ai/account/api_keys" target="_blank">
|
|
52
|
+
https://alttext.ai/account/api_keys
|
|
53
|
+
</a>
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<Button
|
|
58
|
+
disabled={isLoading}
|
|
59
|
+
fullWidth
|
|
60
|
+
onClick={() => saveApiKey(ctx, apiKey, setIsLoading)}
|
|
61
|
+
>
|
|
62
|
+
Save {isLoading && <Spinner size={24} />}
|
|
63
|
+
</Button>
|
|
64
|
+
</Canvas>
|
|
65
|
+
);
|
|
66
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render } from './utils/render';
|
|
2
|
+
import ConfigScreen from './entrypoints/ConfigScreen';
|
|
3
|
+
import 'datocms-react-ui/styles.css';
|
|
4
|
+
import { connect, Field, RenderFieldExtensionCtx } from 'datocms-plugin-sdk';
|
|
5
|
+
import AltTextAIButton from './entrypoints/AltTextAIButton';
|
|
6
|
+
|
|
7
|
+
connect({
|
|
8
|
+
renderConfigScreen(ctx) {
|
|
9
|
+
return render(<ConfigScreen ctx={ctx} />);
|
|
10
|
+
},
|
|
11
|
+
overrideFieldExtensions(field: Field) {
|
|
12
|
+
if (
|
|
13
|
+
field.attributes.field_type === 'gallery' ||
|
|
14
|
+
field.attributes.field_type === 'file'
|
|
15
|
+
) {
|
|
16
|
+
return {
|
|
17
|
+
addons: [{ id: 'altTextAI' }],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
renderFieldExtension(fieldExtensionId: string, ctx: RenderFieldExtensionCtx) {
|
|
22
|
+
switch (fieldExtensionId) {
|
|
23
|
+
case 'altTextAI':
|
|
24
|
+
return render(<AltTextAIButton ctx={ctx} />);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="react-scripts" />
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
const container = document.getElementById('root');
|
|
5
|
+
const root = createRoot(container!);
|
|
6
|
+
|
|
7
|
+
export function render(component: React.ReactNode): void {
|
|
8
|
+
root.render(<StrictMode>{component}</StrictMode>);
|
|
9
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"module": "esnext",
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
"jsx": "react-jsx"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src"
|
|
25
|
+
]
|
|
26
|
+
}
|