@automattic/jetpack-ai-client 0.14.6 → 0.15.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/CHANGELOG.md +8 -0
- package/build/ask-question/sync.d.ts +2 -8
- package/build/ask-question/sync.js +20 -19
- package/build/hooks/use-image-generator/index.js +1 -1
- package/build/hooks/use-save-to-media-library/index.d.ts +12 -0
- package/build/hooks/use-save-to-media-library/index.js +74 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +5 -0
- package/build/libs/index.d.ts +1 -1
- package/build/libs/index.js +1 -1
- package/build/libs/markdown/index.d.ts +2 -2
- package/build/libs/markdown/index.js +2 -2
- package/build/libs/markdown/markdown-to-html.d.ts +8 -1
- package/build/libs/markdown/markdown-to-html.js +10 -1
- package/build/logo-generator/assets/icons/ai.d.ts +6 -0
- package/build/logo-generator/assets/icons/ai.js +8 -0
- package/build/logo-generator/assets/icons/check.d.ts +6 -0
- package/build/logo-generator/assets/icons/check.js +8 -0
- package/build/logo-generator/assets/icons/logo.d.ts +6 -0
- package/build/logo-generator/assets/icons/logo.js +8 -0
- package/build/logo-generator/assets/icons/media.d.ts +6 -0
- package/build/logo-generator/assets/icons/media.js +8 -0
- package/build/logo-generator/components/feature-fetch-failure-screen.d.ts +8 -0
- package/build/logo-generator/components/feature-fetch-failure-screen.js +10 -0
- package/build/logo-generator/components/first-load-screen.d.ts +5 -0
- package/build/logo-generator/components/first-load-screen.js +16 -0
- package/build/logo-generator/components/generator-modal.d.ts +7 -0
- package/build/logo-generator/components/generator-modal.js +184 -0
- package/build/logo-generator/components/history-carousel.d.ts +6 -0
- package/build/logo-generator/components/history-carousel.js +36 -0
- package/build/logo-generator/components/image-loader.d.ts +7 -0
- package/build/logo-generator/components/image-loader.js +12 -0
- package/build/logo-generator/components/logo-presenter.d.ts +4 -0
- package/build/logo-generator/components/logo-presenter.js +106 -0
- package/build/logo-generator/components/prompt.d.ts +5 -0
- package/build/logo-generator/components/prompt.js +96 -0
- package/build/logo-generator/components/upgrade-nudge.d.ts +2 -0
- package/build/logo-generator/components/upgrade-nudge.js +30 -0
- package/build/logo-generator/components/upgrade-screen.d.ts +9 -0
- package/build/logo-generator/components/upgrade-screen.js +24 -0
- package/build/logo-generator/components/visit-site-banner.d.ts +10 -0
- package/build/logo-generator/components/visit-site-banner.js +16 -0
- package/build/logo-generator/constants.d.ts +16 -0
- package/build/logo-generator/constants.js +19 -0
- package/build/logo-generator/hooks/use-checkout.d.ts +4 -0
- package/build/logo-generator/hooks/use-checkout.js +26 -0
- package/build/logo-generator/hooks/use-logo-generator.d.ts +46 -0
- package/build/logo-generator/hooks/use-logo-generator.js +286 -0
- package/build/logo-generator/hooks/use-request-errors.d.ts +16 -0
- package/build/logo-generator/hooks/use-request-errors.js +46 -0
- package/build/logo-generator/index.d.ts +1 -0
- package/build/logo-generator/index.js +1 -0
- package/build/logo-generator/lib/logo-storage.d.ts +58 -0
- package/build/logo-generator/lib/logo-storage.js +123 -0
- package/build/logo-generator/lib/media-exists.d.ts +12 -0
- package/build/logo-generator/lib/media-exists.js +33 -0
- package/build/logo-generator/lib/set-site-logo.d.ts +13 -0
- package/build/logo-generator/lib/set-site-logo.js +26 -0
- package/build/logo-generator/lib/wpcom-limited-request.d.ts +7 -0
- package/build/logo-generator/lib/wpcom-limited-request.js +33 -0
- package/build/logo-generator/store/actions.d.ts +105 -0
- package/build/logo-generator/store/actions.js +193 -0
- package/build/logo-generator/store/constants.d.ts +44 -0
- package/build/logo-generator/store/constants.js +44 -0
- package/build/logo-generator/store/index.d.ts +1 -0
- package/build/logo-generator/store/index.js +19 -0
- package/build/logo-generator/store/initial-state.d.ts +3 -0
- package/build/logo-generator/store/initial-state.js +40 -0
- package/build/logo-generator/store/reducer.d.ts +347 -0
- package/build/logo-generator/store/reducer.js +293 -0
- package/build/logo-generator/store/selectors.d.ts +119 -0
- package/build/logo-generator/store/selectors.js +173 -0
- package/build/logo-generator/store/types.d.ts +164 -0
- package/build/logo-generator/store/types.js +1 -0
- package/build/logo-generator/types.d.ts +82 -0
- package/build/logo-generator/types.js +1 -0
- package/build/types.d.ts +6 -0
- package/package.json +5 -3
- package/src/ask-question/sync.ts +22 -27
- package/src/hooks/use-image-generator/index.ts +1 -1
- package/src/hooks/use-save-to-media-library/index.ts +95 -0
- package/src/index.ts +6 -0
- package/src/libs/index.ts +1 -0
- package/src/libs/markdown/index.ts +2 -2
- package/src/libs/markdown/markdown-to-html.ts +20 -3
- package/src/logo-generator/assets/icons/ai.tsx +21 -0
- package/src/logo-generator/assets/icons/check.tsx +23 -0
- package/src/logo-generator/assets/icons/icons.scss +5 -0
- package/src/logo-generator/assets/icons/logo.tsx +23 -0
- package/src/logo-generator/assets/icons/media.tsx +24 -0
- package/src/logo-generator/assets/images/jetpack-logo.svg +4 -0
- package/src/logo-generator/assets/images/loader.gif +0 -0
- package/src/logo-generator/assets/index.d.ts +3 -0
- package/src/logo-generator/components/feature-fetch-failure-screen.tsx +35 -0
- package/src/logo-generator/components/first-load-screen.scss +12 -0
- package/src/logo-generator/components/first-load-screen.tsx +32 -0
- package/src/logo-generator/components/generator-modal.scss +92 -0
- package/src/logo-generator/components/generator-modal.tsx +291 -0
- package/src/logo-generator/components/history-carousel.scss +36 -0
- package/src/logo-generator/components/history-carousel.tsx +57 -0
- package/src/logo-generator/components/image-loader.tsx +22 -0
- package/src/logo-generator/components/logo-presenter.scss +116 -0
- package/src/logo-generator/components/logo-presenter.tsx +234 -0
- package/src/logo-generator/components/prompt.scss +102 -0
- package/src/logo-generator/components/prompt.tsx +211 -0
- package/src/logo-generator/components/upgrade-nudge.scss +43 -0
- package/src/logo-generator/components/upgrade-nudge.tsx +58 -0
- package/src/logo-generator/components/upgrade-screen.tsx +67 -0
- package/src/logo-generator/components/visit-site-banner.scss +29 -0
- package/src/logo-generator/components/visit-site-banner.tsx +50 -0
- package/src/logo-generator/constants.ts +22 -0
- package/src/logo-generator/hooks/use-checkout.ts +37 -0
- package/src/logo-generator/hooks/use-logo-generator.ts +389 -0
- package/src/logo-generator/hooks/use-request-errors.ts +70 -0
- package/src/logo-generator/index.ts +1 -0
- package/src/logo-generator/lib/logo-storage.ts +166 -0
- package/src/logo-generator/lib/media-exists.ts +42 -0
- package/src/logo-generator/lib/set-site-logo.ts +32 -0
- package/src/logo-generator/lib/wpcom-limited-request.ts +41 -0
- package/src/logo-generator/store/actions.ts +251 -0
- package/src/logo-generator/store/constants.ts +49 -0
- package/src/logo-generator/store/index.ts +25 -0
- package/src/logo-generator/store/initial-state.ts +43 -0
- package/src/logo-generator/store/reducer.ts +387 -0
- package/src/logo-generator/store/selectors.ts +201 -0
- package/src/logo-generator/store/types.ts +207 -0
- package/src/logo-generator/types.ts +97 -0
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.0] - 2024-07-22
|
|
9
|
+
### Added
|
|
10
|
+
- Jetpack AI: Add logo generator codebase to the ai-client package. [#38391]
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Update and export askQuestionSync. [#38344]
|
|
14
|
+
|
|
8
15
|
## [0.14.6] - 2024-07-15
|
|
9
16
|
### Added
|
|
10
17
|
- AI Client: Filter suggestions starting with llama artifacts [#38208]
|
|
@@ -351,6 +358,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
351
358
|
- Updated package dependencies. [#31659]
|
|
352
359
|
- Updated package dependencies. [#31785]
|
|
353
360
|
|
|
361
|
+
[0.15.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.6...v0.15.0
|
|
354
362
|
[0.14.6]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.5...v0.14.6
|
|
355
363
|
[0.14.5]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.4...v0.14.5
|
|
356
364
|
[0.14.4]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.3...v0.14.4
|
|
@@ -3,13 +3,7 @@ import type { PromptProp } from '../types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* The response data from the AI assistant when doing a sync, not-streamed question.
|
|
5
5
|
*/
|
|
6
|
-
export type ResponseData =
|
|
7
|
-
choices: Array<{
|
|
8
|
-
message: {
|
|
9
|
-
content: string;
|
|
10
|
-
};
|
|
11
|
-
}>;
|
|
12
|
-
};
|
|
6
|
+
export type ResponseData = string;
|
|
13
7
|
/**
|
|
14
8
|
* A function that asks a question without streaming.
|
|
15
9
|
*
|
|
@@ -27,4 +21,4 @@ export type ResponseData = {
|
|
|
27
21
|
* const content = responseData.choices[ 0 ].message.content;
|
|
28
22
|
* } );
|
|
29
23
|
*/
|
|
30
|
-
export default function askQuestionSync(question: PromptProp,
|
|
24
|
+
export default function askQuestionSync(question: PromptProp, options?: AskQuestionOptionsArgProps): Promise<ResponseData>;
|
|
@@ -24,12 +24,8 @@ const debug = debugFactory('jetpack-ai-client:ask-question-sync');
|
|
|
24
24
|
* const content = responseData.choices[ 0 ].message.content;
|
|
25
25
|
* } );
|
|
26
26
|
*/
|
|
27
|
-
export default async function askQuestionSync(question,
|
|
28
|
-
debug('Asking question with no streaming: %o. options: %o', question,
|
|
29
|
-
postId,
|
|
30
|
-
feature,
|
|
31
|
-
model,
|
|
32
|
-
});
|
|
27
|
+
export default async function askQuestionSync(question, options = {}) {
|
|
28
|
+
debug('Asking question with no streaming: %o. options: %o', question, options);
|
|
33
29
|
/**
|
|
34
30
|
* The URL to the AI assistant query endpoint.
|
|
35
31
|
*/
|
|
@@ -42,25 +38,30 @@ export default async function askQuestionSync(question, { postId = null, feature
|
|
|
42
38
|
debug('Error getting token: %o', error);
|
|
43
39
|
return Promise.reject(error);
|
|
44
40
|
}
|
|
41
|
+
const messages = Array.isArray(question) ? { messages: question } : { question: question };
|
|
45
42
|
const body = {
|
|
46
|
-
|
|
43
|
+
...messages,
|
|
44
|
+
...options,
|
|
47
45
|
stream: false,
|
|
48
|
-
postId,
|
|
49
|
-
feature,
|
|
50
|
-
model,
|
|
51
46
|
};
|
|
52
47
|
const headers = {
|
|
53
48
|
Authorization: `Bearer ${token}`,
|
|
54
49
|
'Content-Type': 'application/json',
|
|
55
50
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
try {
|
|
52
|
+
const data = await fetch(URL, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
}).then(response => response.json());
|
|
57
|
+
if (data?.data?.status && data?.data?.status > 200) {
|
|
58
|
+
debug('Error generating prompt: %o', data);
|
|
59
|
+
return Promise.reject(data);
|
|
60
|
+
}
|
|
61
|
+
return data.choices?.[0]?.message?.content;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
debug('Error asking question: %o', error);
|
|
65
|
+
return Promise.reject(error);
|
|
64
66
|
}
|
|
65
|
-
return data;
|
|
66
67
|
}
|
|
@@ -126,7 +126,7 @@ const getStableDiffusionImageGenerationPrompt = async (postContent, userPrompt,
|
|
|
126
126
|
* Request the prompt on the AI Assistant endpoint
|
|
127
127
|
*/
|
|
128
128
|
const data = await askQuestionSync(prompt, { feature });
|
|
129
|
-
return data
|
|
129
|
+
return data;
|
|
130
130
|
};
|
|
131
131
|
const useImageGenerator = () => {
|
|
132
132
|
const executeImageGeneration = async function (parameters) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to save an image to the media library.
|
|
3
|
+
*
|
|
4
|
+
* @returns {object} Object with the loading state and the function to save the image to the media library.
|
|
5
|
+
*/
|
|
6
|
+
export default function useSaveToMediaLibrary(): {
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
saveToMediaLibrary: (url: string, name?: string) => Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { isBlobURL } from '@wordpress/blob';
|
|
5
|
+
import { useSelect } from '@wordpress/data';
|
|
6
|
+
import { useState } from '@wordpress/element';
|
|
7
|
+
import debugFactory from 'debug';
|
|
8
|
+
const debug = debugFactory('ai-client:save-to-media-library');
|
|
9
|
+
/**
|
|
10
|
+
* Hook to save an image to the media library.
|
|
11
|
+
*
|
|
12
|
+
* @returns {object} Object with the loading state and the function to save the image to the media library.
|
|
13
|
+
*/
|
|
14
|
+
export default function useSaveToMediaLibrary() {
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const { getSettings } = useSelect(select => select('core/block-editor'), []);
|
|
17
|
+
const saveToMediaLibrary = (url, name) => {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const settings = getSettings();
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
debug('Fetching image from URL');
|
|
23
|
+
fetch(url)
|
|
24
|
+
.then(response => {
|
|
25
|
+
debug('Transforming response to blob');
|
|
26
|
+
response
|
|
27
|
+
.blob()
|
|
28
|
+
.then((blob) => {
|
|
29
|
+
debug('Uploading blob to media library');
|
|
30
|
+
const filesList = Array();
|
|
31
|
+
if (name) {
|
|
32
|
+
filesList.push(new File([blob], name));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
filesList.push(blob);
|
|
36
|
+
}
|
|
37
|
+
settings.mediaUpload({
|
|
38
|
+
allowedTypes: ['image'],
|
|
39
|
+
filesList,
|
|
40
|
+
onFileChange([image]) {
|
|
41
|
+
if (isBlobURL(image?.url)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (image) {
|
|
45
|
+
debug('Image uploaded to media library', image);
|
|
46
|
+
resolve(image);
|
|
47
|
+
}
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
},
|
|
50
|
+
onError(message) {
|
|
51
|
+
debug('Error uploading image to media library:', message);
|
|
52
|
+
reject(message);
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
.catch(e => {
|
|
58
|
+
debug('Error transforming response to blob:', e?.message);
|
|
59
|
+
reject(e?.message);
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
.catch(e => {
|
|
64
|
+
debug('Error fetching image from URL:', e?.message);
|
|
65
|
+
reject(e?.message);
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
isLoading,
|
|
72
|
+
saveToMediaLibrary,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as requestJwt } from './jwt/index.js';
|
|
2
2
|
export { default as SuggestionsEventSource } from './suggestions-event-source/index.js';
|
|
3
3
|
export { default as askQuestion } from './ask-question/index.js';
|
|
4
|
+
export { default as askQuestionSync } from './ask-question/sync.js';
|
|
4
5
|
export { default as transcribeAudio } from './audio-transcription/index.js';
|
|
5
6
|
export { default as useAiSuggestions, getErrorData } from './hooks/use-ai-suggestions/index.js';
|
|
6
7
|
export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
|
|
@@ -13,3 +14,4 @@ export * from './components/index.js';
|
|
|
13
14
|
export * from './data-flow/index.js';
|
|
14
15
|
export * from './types.js';
|
|
15
16
|
export * from './libs/index.js';
|
|
17
|
+
export * from './logo-generator/index.js';
|
package/build/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export { default as requestJwt } from './jwt/index.js';
|
|
5
5
|
export { default as SuggestionsEventSource } from './suggestions-event-source/index.js';
|
|
6
6
|
export { default as askQuestion } from './ask-question/index.js';
|
|
7
|
+
export { default as askQuestionSync } from './ask-question/sync.js';
|
|
7
8
|
export { default as transcribeAudio } from './audio-transcription/index.js';
|
|
8
9
|
/*
|
|
9
10
|
* Hooks
|
|
@@ -34,3 +35,7 @@ export * from './types.js';
|
|
|
34
35
|
* Libs
|
|
35
36
|
*/
|
|
36
37
|
export * from './libs/index.js';
|
|
38
|
+
/*
|
|
39
|
+
* Logo Generator
|
|
40
|
+
*/
|
|
41
|
+
export * from './logo-generator/index.js';
|
package/build/libs/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, } from './markdown/index.js';
|
|
1
|
+
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes, } from './markdown/index.js';
|
|
2
2
|
export type { RenderHTMLRules } from './markdown/index.js';
|
package/build/libs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, } from './markdown/index.js';
|
|
1
|
+
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes, } from './markdown/index.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Internal dependencies
|
|
3
3
|
*/
|
|
4
4
|
import HTMLToMarkdown from './html-to-markdown.js';
|
|
5
|
-
import MarkdownToHTML from './markdown-to-html.js';
|
|
5
|
+
import MarkdownToHTML, { fixes } from './markdown-to-html.js';
|
|
6
6
|
/**
|
|
7
7
|
* Types
|
|
8
8
|
*/
|
|
@@ -16,4 +16,4 @@ declare const renderHTMLFromMarkdown: ({ content, rules, extension, }: {
|
|
|
16
16
|
declare const renderMarkdownFromHTML: ({ content }: {
|
|
17
17
|
content: string;
|
|
18
18
|
}) => string;
|
|
19
|
-
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML };
|
|
19
|
+
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Internal dependencies
|
|
3
3
|
*/
|
|
4
4
|
import HTMLToMarkdown from './html-to-markdown.js';
|
|
5
|
-
import MarkdownToHTML from './markdown-to-html.js';
|
|
5
|
+
import MarkdownToHTML, { fixes } from './markdown-to-html.js';
|
|
6
6
|
const defaultMarkdownConverter = new MarkdownToHTML();
|
|
7
7
|
const defaultHTMLConverter = new HTMLToMarkdown();
|
|
8
8
|
const renderHTMLFromMarkdown = ({ content, rules, extension, }) => {
|
|
@@ -11,4 +11,4 @@ const renderHTMLFromMarkdown = ({ content, rules, extension, }) => {
|
|
|
11
11
|
const renderMarkdownFromHTML = ({ content }) => {
|
|
12
12
|
return defaultHTMLConverter.render({ content });
|
|
13
13
|
};
|
|
14
|
-
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML };
|
|
14
|
+
export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes };
|
|
@@ -6,7 +6,13 @@ import MarkdownIt from 'markdown-it';
|
|
|
6
6
|
* Types
|
|
7
7
|
*/
|
|
8
8
|
import type { Options } from 'markdown-it';
|
|
9
|
-
export type Fix = 'list' | 'paragraph' | 'listItem';
|
|
9
|
+
export type Fix = 'list' | 'paragraph' | 'listItem' | 'table';
|
|
10
|
+
type Fixes = {
|
|
11
|
+
[key in Fix]: (content: string, extension?: boolean, options?: {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}) => string;
|
|
14
|
+
};
|
|
15
|
+
export declare const fixes: Fixes;
|
|
10
16
|
export default class MarkdownToHTML {
|
|
11
17
|
markdownConverter: MarkdownIt;
|
|
12
18
|
constructor(options?: Options);
|
|
@@ -24,3 +30,4 @@ export default class MarkdownToHTML {
|
|
|
24
30
|
extension?: boolean;
|
|
25
31
|
}): string;
|
|
26
32
|
}
|
|
33
|
+
export {};
|
|
@@ -19,7 +19,7 @@ const addListComments = (content) => {
|
|
|
19
19
|
.replaceAll('<ul>', '<!-- wp:list --><ul>')
|
|
20
20
|
.replaceAll('</ul>', '</ul><!-- /wp:list -->'));
|
|
21
21
|
};
|
|
22
|
-
const fixes = {
|
|
22
|
+
export const fixes = {
|
|
23
23
|
list: (content, extension = false) => {
|
|
24
24
|
// Fix list indentation
|
|
25
25
|
const fixedIndentation = content
|
|
@@ -43,6 +43,15 @@ const fixes = {
|
|
|
43
43
|
// Fix encoding of <br /> tags
|
|
44
44
|
return content.replaceAll(/\s*<br \/>\s*/g, '<br />');
|
|
45
45
|
},
|
|
46
|
+
table: (content, extension = false, { hasFixedLayout = false }) => {
|
|
47
|
+
if (!extension) {
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
if (content.startsWith('<!-- wp:table')) {
|
|
51
|
+
return content;
|
|
52
|
+
}
|
|
53
|
+
return `<!-- wp:table { "hasFixedLayout":${hasFixedLayout ? 'true' : 'false'} } -->${content}<!-- /wp:table -->`;
|
|
54
|
+
},
|
|
46
55
|
};
|
|
47
56
|
const defaultMarkdownItOptions = {
|
|
48
57
|
breaks: true,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Internal dependencies
|
|
4
|
+
*/
|
|
5
|
+
import './icons.scss';
|
|
6
|
+
export default () => {
|
|
7
|
+
return (_jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "jetpack-ai-logo-generator-icon", children: [_jsx("path", { d: "M6.99976 3.99994L7.84828 6.15141L9.99976 6.99994L7.84828 7.84847L6.99976 9.99994L6.15123 7.84847L3.99976 6.99994L6.15123 6.15141L6.99976 3.99994Z" }), _jsx("path", { d: "M16 4L17.1314 6.86863L20 8L17.1314 9.13137L16 12L14.8686 9.13137L12 8L14.8686 6.86863L16 4Z" }), _jsx("path", { d: "M11 10L12.4142 13.5858L16 15L12.4142 16.4142L11 20L9.58579 16.4142L6 15L9.58579 13.5858L11 10Z" })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Internal dependencies
|
|
4
|
+
*/
|
|
5
|
+
import './icons.scss';
|
|
6
|
+
export default () => {
|
|
7
|
+
return (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "jetpack-ai-logo-generator-icon", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M17.9291 7.96836L10.7308 17.6492L6.2145 14.2911L7.10952 13.0873L10.4221 15.5504L16.7253 7.07333L17.9291 7.96836Z" }) }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Internal dependencies
|
|
4
|
+
*/
|
|
5
|
+
import './icons.scss';
|
|
6
|
+
export default () => {
|
|
7
|
+
return (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "jetpack-ai-logo-generator-icon", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M19.2927 13.7485C18.5014 17.0423 15.5366 19.4901 12 19.4901C8.92508 19.4901 6.28236 17.6396 5.12469 14.9915L8.79556 12.8139L12.2508 14.0309C12.482 14.1123 12.7383 14.0756 12.9374 13.9327L15.8243 11.8601L15.9039 11.8992C16.1998 12.0451 16.6072 12.249 17.0533 12.4807C17.8331 12.8857 18.6946 13.3572 19.2927 13.7485ZM19.499 12.1129C18.9341 11.7788 18.3001 11.4379 17.7447 11.1495C17.287 10.9118 16.8698 10.7031 16.5672 10.5539C16.4158 10.4792 16.2928 10.4193 16.2074 10.378L16.1085 10.3303L16.0824 10.3177L16.0729 10.3132C15.8261 10.1954 15.5347 10.2214 15.3126 10.3809L12.3802 12.4861L8.9634 11.2827C8.75395 11.2089 8.52258 11.2318 8.3316 11.3451L4.65716 13.5248C4.55414 13.0294 4.5 12.5161 4.5 11.9901C4.5 7.84798 7.85786 4.49011 12 4.49011C16.1421 4.49011 19.5 7.84798 19.5 11.9901C19.5 12.0311 19.4997 12.072 19.499 12.1129ZM21 11.9901C21 16.9607 16.9706 20.9901 12 20.9901C7.02944 20.9901 3 16.9607 3 11.9901C3 7.01955 7.02944 2.99011 12 2.99011C16.9706 2.99011 21 7.01955 21 11.9901Z" }) }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Internal dependencies
|
|
4
|
+
*/
|
|
5
|
+
import './icons.scss';
|
|
6
|
+
export default () => {
|
|
7
|
+
return (_jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "jetpack-ai-logo-generator-icon", children: [_jsx("path", { d: "M7 6.49011L11 8.99011L7 11.4901V6.49011Z" }), _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5 2.99011C3.89543 2.99011 3 3.88554 3 4.99011V18.9901C3 20.0947 3.89543 20.9901 5 20.9901H19C20.1046 20.9901 21 20.0947 21 18.9901V4.99011C21 3.88554 20.1046 2.99011 19 2.99011H5ZM19 4.49011H5C4.72386 4.49011 4.5 4.71397 4.5 4.99011V15.6973L8.12953 13.0508C8.38061 12.8677 8.71858 12.8584 8.97934 13.0274L11.906 14.9243L15.4772 11.4524C15.7683 11.1694 16.2317 11.1694 16.5228 11.4524L19.5 14.3469V4.99011C19.5 4.71397 19.2761 4.49011 19 4.49011ZM4.5 18.9901V17.5537L8.59643 14.5667L11.5921 16.5084C11.8857 16.6987 12.2719 16.6607 12.5228 16.4167L16 13.0361L19.4772 16.4167L19.5 16.3933V18.9901C19.5 19.2663 19.2761 19.4901 19 19.4901H5C4.72386 19.4901 4.5 19.2663 4.5 18.9901Z" })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* External dependencies
|
|
4
|
+
*/
|
|
5
|
+
import { Button } from '@wordpress/components';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
export const FeatureFetchFailureScreen = ({ onCancel, onRetry }) => {
|
|
8
|
+
const errorMessage = __('We are sorry. There was an error loading your Jetpack AI account settings. Please, try again.', 'jetpack-ai-client');
|
|
9
|
+
return (_jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-message-wrapper", children: [_jsx("div", { className: "jetpack-ai-logo-generator-modal__notice-message", children: _jsx("span", { className: "jetpack-ai-logo-generator-modal__loading-message", children: errorMessage }) }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-actions", children: [_jsx(Button, { variant: "tertiary", onClick: onCancel, children: __('Cancel', 'jetpack-ai-client') }), _jsx(Button, { variant: "primary", onClick: onRetry, children: __('Try again', 'jetpack-ai-client') })] })] }));
|
|
10
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* External dependencies
|
|
4
|
+
*/
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { ImageLoader } from './image-loader.js';
|
|
10
|
+
import './first-load-screen.scss';
|
|
11
|
+
export const FirstLoadScreen = ({ state = 'loadingFeature' }) => {
|
|
12
|
+
const loadingLabel = __('Loading…', 'jetpack-ai-client');
|
|
13
|
+
const analyzingLabel = __('Analyzing your site to create the perfect logo…', 'jetpack-ai-client');
|
|
14
|
+
const generatingLabel = __('Generating logo…', 'jetpack-ai-client');
|
|
15
|
+
return (_jsxs("div", { className: "jetpack-ai-logo-generator-modal__loading-wrapper", children: [_jsx(ImageLoader, {}), _jsxs("span", { className: "jetpack-ai-logo-generator-modal__loading-message", children: [state === 'loadingFeature' && loadingLabel, state === 'analyzing' && analyzingLabel, state === 'generating' && generatingLabel] })] }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* External dependencies
|
|
4
|
+
*/
|
|
5
|
+
import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
|
|
6
|
+
import { Modal, Button } from '@wordpress/components';
|
|
7
|
+
import { useDispatch } from '@wordpress/data';
|
|
8
|
+
import { __ } from '@wordpress/i18n';
|
|
9
|
+
import { external, Icon } from '@wordpress/icons';
|
|
10
|
+
import clsx from 'clsx';
|
|
11
|
+
import debugFactory from 'debug';
|
|
12
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
13
|
+
/**
|
|
14
|
+
* Internal dependencies
|
|
15
|
+
*/
|
|
16
|
+
import { DEFAULT_LOGO_COST, EVENT_MODAL_OPEN, EVENT_FEEDBACK, EVENT_MODAL_CLOSE, EVENT_PLACEMENT_QUICK_LINKS, EVENT_GENERATE, } from '../constants.js';
|
|
17
|
+
import useLogoGenerator from '../hooks/use-logo-generator.js';
|
|
18
|
+
import useRequestErrors from '../hooks/use-request-errors.js';
|
|
19
|
+
import { isLogoHistoryEmpty, clearDeletedMedia } from '../lib/logo-storage.js';
|
|
20
|
+
import { STORE_NAME } from '../store/index.js';
|
|
21
|
+
import { FeatureFetchFailureScreen } from './feature-fetch-failure-screen.js';
|
|
22
|
+
import { FirstLoadScreen } from './first-load-screen.js';
|
|
23
|
+
import { HistoryCarousel } from './history-carousel.js';
|
|
24
|
+
import { LogoPresenter } from './logo-presenter.js';
|
|
25
|
+
import { Prompt } from './prompt.js';
|
|
26
|
+
import { UpgradeScreen } from './upgrade-screen.js';
|
|
27
|
+
import { VisitSiteBanner } from './visit-site-banner.js';
|
|
28
|
+
import './generator-modal.scss';
|
|
29
|
+
const debug = debugFactory('jetpack-ai-calypso:generator-modal');
|
|
30
|
+
export const GeneratorModal = ({ isOpen, onClose, siteDetails, context, }) => {
|
|
31
|
+
const { tracks } = useAnalytics();
|
|
32
|
+
const { recordEvent: recordTracksEvent } = tracks;
|
|
33
|
+
const { setSiteDetails, fetchAiAssistantFeature, loadLogoHistory } = useDispatch(STORE_NAME);
|
|
34
|
+
const [loadingState, setLoadingState] = useState(null);
|
|
35
|
+
const [initialPrompt, setInitialPrompt] = useState();
|
|
36
|
+
const needsToHandleModalOpen = useRef(true);
|
|
37
|
+
const requestedFeatureData = useRef(false);
|
|
38
|
+
const [needsFeature, setNeedsFeature] = useState(false);
|
|
39
|
+
const [needsMoreRequests, setNeedsMoreRequests] = useState(false);
|
|
40
|
+
const [upgradeURL, setUpgradeURL] = useState('');
|
|
41
|
+
const { selectedLogo, getAiAssistantFeature, generateFirstPrompt, generateLogo, setContext } = useLogoGenerator();
|
|
42
|
+
const { featureFetchError, firstLogoPromptFetchError, clearErrors } = useRequestErrors();
|
|
43
|
+
const siteId = siteDetails?.ID;
|
|
44
|
+
const siteURL = siteDetails?.URL;
|
|
45
|
+
const [logoAccepted, setLogoAccepted] = useState(false);
|
|
46
|
+
// First fetch the feature data so we have the most up-to-date info from the backend.
|
|
47
|
+
const feature = getAiAssistantFeature();
|
|
48
|
+
const generateFirstLogo = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
// First generate the prompt based on the site's data.
|
|
51
|
+
setLoadingState('analyzing');
|
|
52
|
+
recordTracksEvent(EVENT_GENERATE, { context, tool: 'first-prompt' });
|
|
53
|
+
const prompt = await generateFirstPrompt();
|
|
54
|
+
setInitialPrompt(prompt);
|
|
55
|
+
// Then generate the logo based on the prompt.
|
|
56
|
+
setLoadingState('generating');
|
|
57
|
+
await generateLogo({ prompt });
|
|
58
|
+
setLoadingState(null);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
debug('Error generating first logo', error);
|
|
62
|
+
setLoadingState(null);
|
|
63
|
+
}
|
|
64
|
+
}, [context, generateFirstPrompt, generateLogo]);
|
|
65
|
+
/*
|
|
66
|
+
* Called ONCE to check the feature data to make sure the site is allowed to do the generation.
|
|
67
|
+
* Also, checks site history and trigger a new generation in case there are no logos to present.
|
|
68
|
+
*/
|
|
69
|
+
const initializeModal = useCallback(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const hasHistory = !isLogoHistoryEmpty(String(siteId));
|
|
72
|
+
const logoCost = feature?.costs?.['jetpack-ai-logo-generator']?.logo ?? DEFAULT_LOGO_COST;
|
|
73
|
+
const promptCreationCost = 1;
|
|
74
|
+
const currentLimit = feature?.currentTier?.value || 0;
|
|
75
|
+
const currentUsage = feature?.usagePeriod?.requestsCount || 0;
|
|
76
|
+
const isUnlimited = currentLimit === 1;
|
|
77
|
+
const hasNoNextTier = !feature?.nextTier; // If there is no next tier, the user cannot upgrade.
|
|
78
|
+
// The user needs an upgrade immediately if they have no logos and not enough requests remaining for one prompt and one logo generation.
|
|
79
|
+
const siteNeedsMoreRequests = !isUnlimited &&
|
|
80
|
+
!hasNoNextTier &&
|
|
81
|
+
!hasHistory &&
|
|
82
|
+
currentLimit - currentUsage < logoCost + promptCreationCost;
|
|
83
|
+
// If the site requires an upgrade, set the upgrade URL and show the upgrade screen immediately.
|
|
84
|
+
setNeedsFeature(!feature?.hasFeature ?? true);
|
|
85
|
+
setNeedsMoreRequests(siteNeedsMoreRequests);
|
|
86
|
+
if (!feature?.hasFeature || siteNeedsMoreRequests) {
|
|
87
|
+
const siteUpgradeURL = new URL(`${location.origin}/checkout/${siteDetails?.domain}/${feature?.nextTier?.slug}`);
|
|
88
|
+
siteUpgradeURL.searchParams.set('redirect_to', location.href);
|
|
89
|
+
setUpgradeURL(siteUpgradeURL.toString());
|
|
90
|
+
setLoadingState(null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Load the logo history and clear any deleted media.
|
|
94
|
+
await clearDeletedMedia(String(siteId));
|
|
95
|
+
loadLogoHistory(siteId);
|
|
96
|
+
// If there is any logo, we do not need to generate a first logo again.
|
|
97
|
+
if (!isLogoHistoryEmpty(String(siteId))) {
|
|
98
|
+
setLoadingState(null);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// If the site does not require an upgrade and has no logos stored, generate the first prompt based on the site's data.
|
|
102
|
+
generateFirstLogo();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
debug('Error fetching feature', error);
|
|
106
|
+
setLoadingState(null);
|
|
107
|
+
}
|
|
108
|
+
}, [
|
|
109
|
+
feature,
|
|
110
|
+
generateFirstLogo,
|
|
111
|
+
loadLogoHistory,
|
|
112
|
+
clearDeletedMedia,
|
|
113
|
+
isLogoHistoryEmpty,
|
|
114
|
+
siteId,
|
|
115
|
+
]);
|
|
116
|
+
const handleModalOpen = useCallback(async () => {
|
|
117
|
+
setContext(context);
|
|
118
|
+
recordTracksEvent(EVENT_MODAL_OPEN, { context, placement: EVENT_PLACEMENT_QUICK_LINKS });
|
|
119
|
+
initializeModal();
|
|
120
|
+
}, [setContext, context, initializeModal]);
|
|
121
|
+
const closeModal = () => {
|
|
122
|
+
// Reset the state when the modal is closed, so we trigger the modal initialization again when it's opened.
|
|
123
|
+
needsToHandleModalOpen.current = true;
|
|
124
|
+
onClose();
|
|
125
|
+
setLoadingState(null);
|
|
126
|
+
setNeedsFeature(false);
|
|
127
|
+
setNeedsMoreRequests(false);
|
|
128
|
+
clearErrors();
|
|
129
|
+
setLogoAccepted(false);
|
|
130
|
+
recordTracksEvent(EVENT_MODAL_CLOSE, { context, placement: EVENT_PLACEMENT_QUICK_LINKS });
|
|
131
|
+
};
|
|
132
|
+
const handleApplyLogo = () => {
|
|
133
|
+
setLogoAccepted(true);
|
|
134
|
+
};
|
|
135
|
+
const handleCloseAndReload = () => {
|
|
136
|
+
closeModal();
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
// Reload the page to update the logo.
|
|
139
|
+
window.location.reload();
|
|
140
|
+
}, 1000);
|
|
141
|
+
};
|
|
142
|
+
const handleFeedbackClick = () => {
|
|
143
|
+
recordTracksEvent(EVENT_FEEDBACK, { context });
|
|
144
|
+
};
|
|
145
|
+
// Set site details when siteId changes
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (siteId) {
|
|
148
|
+
setSiteDetails(siteDetails);
|
|
149
|
+
}
|
|
150
|
+
// When the site details are set, we need to fetch the feature data.
|
|
151
|
+
if (!requestedFeatureData.current) {
|
|
152
|
+
requestedFeatureData.current = true;
|
|
153
|
+
fetchAiAssistantFeature();
|
|
154
|
+
}
|
|
155
|
+
}, [siteId, siteDetails, setSiteDetails]);
|
|
156
|
+
// Handles modal opening logic
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
// While the modal is not open, the siteId is not set, or the feature data is not available, do nothing.
|
|
159
|
+
if (!isOpen || !siteId || !feature?.costs) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Prevent multiple calls of the handleModalOpen function
|
|
163
|
+
if (needsToHandleModalOpen.current) {
|
|
164
|
+
needsToHandleModalOpen.current = false;
|
|
165
|
+
handleModalOpen();
|
|
166
|
+
}
|
|
167
|
+
}, [isOpen, siteId, handleModalOpen, feature]);
|
|
168
|
+
let body;
|
|
169
|
+
if (loadingState) {
|
|
170
|
+
body = _jsx(FirstLoadScreen, { state: loadingState });
|
|
171
|
+
}
|
|
172
|
+
else if (featureFetchError || firstLogoPromptFetchError) {
|
|
173
|
+
body = _jsx(FeatureFetchFailureScreen, { onCancel: closeModal, onRetry: initializeModal });
|
|
174
|
+
}
|
|
175
|
+
else if (needsFeature || needsMoreRequests) {
|
|
176
|
+
body = (_jsx(UpgradeScreen, { onCancel: closeModal, upgradeURL: upgradeURL, reason: needsFeature ? 'feature' : 'requests' }));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
body = (_jsxs(_Fragment, { children: [!logoAccepted && _jsx(Prompt, { initialPrompt: initialPrompt }), _jsx(LogoPresenter, { logo: selectedLogo, onApplyLogo: handleApplyLogo, logoAccepted: logoAccepted, siteId: String(siteId) }), logoAccepted ? (_jsxs("div", { className: "jetpack-ai-logo-generator__accept", children: [_jsx(VisitSiteBanner, { siteURL: siteURL, onVisitBlankTarget: handleCloseAndReload }), _jsxs("div", { className: "jetpack-ai-logo-generator__accept-actions", children: [_jsx(Button, { variant: "link", onClick: handleCloseAndReload, children: __('Close and refresh', 'jetpack-ai-client') }), _jsx(Button, { href: siteURL, variant: "primary", children: __('Visit site', 'jetpack-ai-client') })] })] })) : (_jsxs(_Fragment, { children: [_jsx(HistoryCarousel, {}), _jsx("div", { className: "jetpack-ai-logo-generator__footer", children: _jsxs(Button, { variant: "link", className: "jetpack-ai-logo-generator__feedback-button", href: "https://jetpack.com/redirect/?source=jetpack-ai-feedback", target: "_blank", onClick: handleFeedbackClick, children: [_jsx("span", { children: __('Provide feedback', 'jetpack-ai-client') }), _jsx(Icon, { icon: external, className: "icon" })] }) })] }))] }));
|
|
180
|
+
}
|
|
181
|
+
return (_jsx(_Fragment, { children: isOpen && (_jsx(Modal, { className: "jetpack-ai-logo-generator-modal", onRequestClose: logoAccepted ? handleCloseAndReload : closeModal, shouldCloseOnClickOutside: false, shouldCloseOnEsc: false, title: __('Jetpack AI Logo Generator', 'jetpack-ai-client'), children: _jsx("div", { className: clsx('jetpack-ai-logo-generator-modal__body', {
|
|
182
|
+
'notice-modal': needsFeature || needsMoreRequests || featureFetchError || firstLogoPromptFetchError,
|
|
183
|
+
}), children: body }) })) }));
|
|
184
|
+
};
|