@automattic/jetpack-ai-client 0.26.2 → 0.27.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 CHANGED
@@ -5,9 +5,26 @@ 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.27.0] - 2025-03-03
9
+ ### Added
10
+ - AI Client: Move openBlockSidebar utility function. [#42016]
11
+
12
+ ### Changed
13
+ - AI Assistant: Add experimental functionality to test Chrome's built-in AI API with the AI excerpt. [#41922]
14
+ - AI Client: Move showAiAssistantSection function to AI Client. [#42158]
15
+ - AI Client: Refactor usePostContent hook to expose isEditedPostEmpty. [#42149]
16
+ - Update package dependencies. [#42163]
17
+
18
+ ## [0.26.3] - 2025-02-24
19
+ ### Changed
20
+ - Update package dependencies. [#41955]
21
+
22
+ ### Fixed
23
+ - Prevent Chrome AI requests from incrementing request count. [#41900]
24
+
8
25
  ## [0.26.2] - 2025-02-17
9
26
  ### Added
10
- - Jetpack AI: Adding translation support using Chrome's Gemini AI mini. [#41724]
27
+ - Add translation support using Chrome's Gemini AI mini. [#41724]
11
28
 
12
29
  ## [0.26.1] - 2025-02-11
13
30
  ### Changed
@@ -525,6 +542,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
525
542
  - AI Client: stop using smart document visibility handling on the fetchEventSource library, so it does not restart the completion when changing tabs. [#32004]
526
543
  - Updated package dependencies. [#31468] [#31659] [#31785]
527
544
 
545
+ [0.27.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.26.3...v0.27.0
546
+ [0.26.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.26.2...v0.26.3
528
547
  [0.26.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.26.1...v0.26.2
529
548
  [0.26.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.26.0...v0.26.1
530
549
  [0.26.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.7...v0.26.0
@@ -1,7 +1,5 @@
1
1
  import { getJetpackExtensionAvailability } from '@automattic/jetpack-shared-extension-utils';
2
- import { PROMPT_TYPE_CHANGE_LANGUAGE,
3
- //PROMPT_TYPE_SUMMARIZE,
4
- } from '../constants.js';
2
+ import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_SUMMARIZE } from '../constants.js';
5
3
  import ChromeAISuggestionsEventSource from './suggestions.js';
6
4
  /**
7
5
  * Check for the feature flag.
@@ -26,6 +24,8 @@ export default async function ChromeAIFactory(promptArg) {
26
24
  language: '',
27
25
  };
28
26
  let promptType = '';
27
+ let tone = null;
28
+ let wordCount = null;
29
29
  if (Array.isArray(promptArg)) {
30
30
  for (let i = 0; i < promptArg.length; i++) {
31
31
  const prompt = promptArg[i];
@@ -45,6 +45,12 @@ export default async function ChromeAIFactory(promptArg) {
45
45
  if (promptContext.content) {
46
46
  context.content = promptContext.content;
47
47
  }
48
+ if (promptContext.tone) {
49
+ tone = promptContext.tone;
50
+ }
51
+ if (promptContext.words) {
52
+ wordCount = promptContext.words;
53
+ }
48
54
  }
49
55
  }
50
56
  if (promptType.startsWith('ai-assistant-change-language')) {
@@ -84,16 +90,18 @@ export default async function ChromeAIFactory(promptArg) {
84
90
  });
85
91
  return chromeAI;
86
92
  }
87
- // TODO
88
- if (promptType.startsWith('ai-assistant-summarize')) {
89
- /*
93
+ // TODO: consider also using ChromeAI for ai-assistant-summarize
94
+ if (promptType.startsWith('ai-content-lens')) {
95
+ const summaryOpts = {
96
+ tone: tone,
97
+ wordCount: wordCount,
98
+ };
99
+ // TODO: detect if the content is in English and fallback if it's not
90
100
  return new ChromeAISuggestionsEventSource({
91
- content: "",
101
+ content: context.content,
92
102
  promptType: PROMPT_TYPE_SUMMARIZE,
93
- options: {},
94
- } );
95
- */
96
- return false;
103
+ options: summaryOpts,
104
+ });
97
105
  }
98
106
  return false;
99
107
  }
@@ -8,6 +8,8 @@ type ChromeAISuggestionsEventSourceConstructorArgs = {
8
8
  feature?: 'ai-assistant-experimental' | string | undefined;
9
9
  sourceLanguage?: string;
10
10
  targetLanguage?: string;
11
+ tone?: string;
12
+ wordCount?: number;
11
13
  functions?: Array<object>;
12
14
  model?: AiModelTypeProp;
13
15
  };
@@ -30,6 +32,7 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
30
32
  processEvent(e: EventSourceMessage): void;
31
33
  processErrorEvent(e: any): void;
32
34
  translate(text: string, target: string, source?: string): Promise<void>;
33
- summarize(text: string): Promise<string>;
35
+ private getSummarizerOptions;
36
+ summarize(text: string, tone?: string, wordCount?: number): Promise<void>;
34
37
  }
35
38
  export {};
@@ -24,7 +24,7 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
24
24
  this.translate(content, options.targetLanguage, options.sourceLanguage);
25
25
  }
26
26
  if (promptType === PROMPT_TYPE_SUMMARIZE) {
27
- this.summarize(content);
27
+ this.summarize(content, options.tone, options.wordCount);
28
28
  }
29
29
  }
30
30
  async initEventSource() { }
@@ -39,11 +39,11 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
39
39
  this.processErrorEvent(err);
40
40
  return;
41
41
  }
42
- if (e.event === 'translation') {
42
+ if (e.event === 'translation' || e.event === 'summary') {
43
43
  this.dispatchEvent(new CustomEvent('suggestion', { detail: data.message }));
44
44
  }
45
45
  if (data.complete) {
46
- this.dispatchEvent(new CustomEvent('done', { detail: data.message }));
46
+ this.dispatchEvent(new CustomEvent('done', { detail: { message: data.message, source: 'chromeAI' } }));
47
47
  }
48
48
  }
49
49
  processErrorEvent(e) {
@@ -80,8 +80,48 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
80
80
  this.processErrorEvent(error);
81
81
  }
82
82
  }
83
- // TODO
84
- async summarize(text) {
85
- return text;
83
+ // Helper function to format summarizer options
84
+ getSummarizerOptions(tone, wordCount) {
85
+ let sharedContext = `The summary you write should contain approximately ${wordCount ?? 50} words long. Strive for precision in word count without compromising clarity and significance`;
86
+ if (tone) {
87
+ sharedContext += `\n - Write with a ${tone} tone.\n`;
88
+ }
89
+ const options = {
90
+ sharedContext: sharedContext,
91
+ type: 'teaser',
92
+ format: 'plain-text',
93
+ length: 'medium',
94
+ };
95
+ return options;
96
+ }
97
+ // use the Chrome AI summarizer
98
+ async summarize(text, tone, wordCount) {
99
+ if (!('ai' in self) || !('summarizer' in self.ai)) {
100
+ return;
101
+ }
102
+ const available = (await self.ai.summarizer.capabilities()).available;
103
+ if (available === 'no') {
104
+ return;
105
+ }
106
+ const options = this.getSummarizerOptions(tone, wordCount);
107
+ const summarizer = await self.ai.summarizer.create(options);
108
+ if (available === 'after-download') {
109
+ await summarizer.ready;
110
+ }
111
+ try {
112
+ const context = `Write with a ${tone} tone.`;
113
+ const summary = await summarizer.summarize(text, { context: context });
114
+ this.processEvent({
115
+ id: '',
116
+ event: 'summary',
117
+ data: JSON.stringify({
118
+ message: summary,
119
+ complete: true,
120
+ }),
121
+ });
122
+ }
123
+ catch (error) {
124
+ this.processErrorEvent(error);
125
+ }
86
126
  }
87
127
  }
@@ -29,7 +29,6 @@ type AiImageModalProps = {
29
29
  isUnlimited: boolean;
30
30
  upgradeDescription: string;
31
31
  hasError: boolean;
32
- postContent?: string | boolean | null;
33
32
  handlePreviousImage: () => void;
34
33
  handleNextImage: () => void;
35
34
  acceptButton: React.JSX.Element;
@@ -30,7 +30,7 @@ const debug = debugFactory('jetpack-ai-client:featured-image');
30
30
  export default function FeaturedImage({ busy, disabled, placement, onClose = () => { }, }) {
31
31
  const [isFeaturedImageModalVisible, setIsFeaturedImageModalVisible] = useState(placement === PLACEMENT_MEDIA_SOURCE_DROPDOWN);
32
32
  const siteType = useSiteType();
33
- const postContent = usePostContent();
33
+ const { getPostContent, isEditedPostEmpty } = usePostContent();
34
34
  const { postTitle, postFeaturedMediaId, isEditorPanelOpened } = useSelect(select => {
35
35
  return {
36
36
  postTitle: select(editorStore).getEditedPostAttribute('title'),
@@ -80,9 +80,9 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
80
80
  * Handle the guess style for the image. It is reworked here to include the post content.
81
81
  */
82
82
  const handleGuessStyle = useCallback(userPrompt => {
83
- const content = postTitle + '\n\n' + postContent;
83
+ const content = postTitle + '\n\n' + getPostContent();
84
84
  return guessStyle(userPrompt, 'featured-image-guess-style', content);
85
- }, [postContent, postTitle, guessStyle]);
85
+ }, [postTitle, getPostContent, guessStyle]);
86
86
  const handleGenerate = useCallback(({ userPrompt, style, }) => {
87
87
  // track the generate image event
88
88
  recordEvent('jetpack_ai_featured_image_generation_generate_image', {
@@ -95,7 +95,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
95
95
  setIsFeaturedImageModalVisible(true);
96
96
  return processImageGeneration({
97
97
  userPrompt,
98
- postContent: postTitle + '\n\n' + postContent,
98
+ postContent: postTitle + '\n\n' + getPostContent(),
99
99
  notEnoughRequests,
100
100
  style,
101
101
  }).catch(error => {
@@ -113,7 +113,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
113
113
  featuredImageActiveModel,
114
114
  siteType,
115
115
  processImageGeneration,
116
- postContent,
116
+ getPostContent,
117
117
  notEnoughRequests,
118
118
  postTitle,
119
119
  ]);
@@ -138,7 +138,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
138
138
  setCurrent(() => images.length);
139
139
  processImageGeneration({
140
140
  userPrompt,
141
- postContent: postTitle + '\n\n' + postContent,
141
+ postContent: postTitle + '\n\n' + getPostContent(),
142
142
  notEnoughRequests,
143
143
  style,
144
144
  }).catch(error => {
@@ -159,7 +159,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
159
159
  setCurrent,
160
160
  processImageGeneration,
161
161
  postTitle,
162
- postContent,
162
+ getPostContent,
163
163
  notEnoughRequests,
164
164
  images,
165
165
  ]);
@@ -173,7 +173,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
173
173
  });
174
174
  processImageGeneration({
175
175
  userPrompt,
176
- postContent: postTitle + '\n\n' + postContent,
176
+ postContent: postTitle + '\n\n' + getPostContent(),
177
177
  notEnoughRequests,
178
178
  style,
179
179
  }).catch(error => {
@@ -191,7 +191,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
191
191
  featuredImageActiveModel,
192
192
  siteType,
193
193
  processImageGeneration,
194
- postContent,
194
+ getPostContent,
195
195
  notEnoughRequests,
196
196
  postTitle,
197
197
  ]);
@@ -261,7 +261,7 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
261
261
  ]);
262
262
  const generateAgainText = __('Generate another image', 'jetpack-ai-client');
263
263
  const generateText = __('Generate', 'jetpack-ai-client');
264
- const hasContent = postContent.trim?.() || postTitle.trim?.() ? true : false;
264
+ const hasContent = !isEditedPostEmpty() || postTitle.trim?.() ? true : false;
265
265
  const hasPrompt = hasContent ? prompt.length >= 0 : prompt.length >= 3;
266
266
  const disableInput = notEnoughRequests || currentPointer?.generating || requireUpgrade;
267
267
  const disableAction = disableInput || (!hasContent && !hasPrompt);
@@ -274,5 +274,5 @@ export default function FeaturedImage({ busy, disabled, placement, onClose = ()
274
274
  currentImage?.generating ||
275
275
  currentImage?.libraryId === postFeaturedMediaId, children: __('Set as featured image', 'jetpack-ai-client') }));
276
276
  return (_jsxs(_Fragment, { children: [(placement === PLACEMENT_JETPACK_SIDEBAR ||
277
- placement === PLACEMENT_DOCUMENT_SETTINGS) && (_jsxs(_Fragment, { children: [_jsx("p", { className: "jetpack-ai-assistant__help-text", children: __('Based on your post content.', 'jetpack-ai-client') }), _jsx(Button, { onClick: handleModalOpen, isBusy: busy, disabled: disabled || notEnoughRequests, variant: "secondary", __next40pxDefaultSize: true, children: __('Generate image', 'jetpack-ai-client') })] })), _jsx(AiImageModal, { postContent: hasContent, autoStart: hasContent && !postFeaturedMediaId, autoStartAction: handleFirstGenerate, images: images, currentIndex: current, title: __('Generate a featured image with AI', 'jetpack-ai-client'), cost: featuredImageCost, open: isFeaturedImageModalVisible, placement: placement, onClose: handleModalClose, onTryAgain: handleTryAgain, onGenerate: pointer?.current > 0 || postFeaturedMediaId ? handleRegenerate : handleGenerate, generating: currentPointer?.generating, notEnoughRequests: notEnoughRequests, requireUpgrade: requireUpgrade, upgradeDescription: upgradeDescription, currentLimit: requestsLimit, currentUsage: requestsCount, isUnlimited: isUnlimited, hasError: Boolean(currentPointer?.error), handlePreviousImage: handlePreviousImage, handleNextImage: handleNextImage, acceptButton: acceptButton, generateButtonLabel: pointer?.current > 0 ? generateAgainText : generateText, instructionsPlaceholder: __("Describe the featured image you'd like to create and select a style.", 'jetpack-ai-client'), imageStyles: imageStyles, onGuessStyle: handleGuessStyle, prompt: prompt, setPrompt: setPrompt, initialStyle: requestStyle, inputDisabled: disableInput, actionDisabled: disableAction })] }));
277
+ placement === PLACEMENT_DOCUMENT_SETTINGS) && (_jsxs(_Fragment, { children: [_jsx("p", { className: "jetpack-ai-assistant__help-text", children: __('Based on your post content.', 'jetpack-ai-client') }), _jsx(Button, { onClick: handleModalOpen, isBusy: busy, disabled: disabled || notEnoughRequests, variant: "secondary", __next40pxDefaultSize: true, children: __('Generate image', 'jetpack-ai-client') })] })), _jsx(AiImageModal, { autoStart: hasContent && !postFeaturedMediaId, autoStartAction: handleFirstGenerate, images: images, currentIndex: current, title: __('Generate a featured image with AI', 'jetpack-ai-client'), cost: featuredImageCost, open: isFeaturedImageModalVisible, placement: placement, onClose: handleModalClose, onTryAgain: handleTryAgain, onGenerate: pointer?.current > 0 || postFeaturedMediaId ? handleRegenerate : handleGenerate, generating: currentPointer?.generating, notEnoughRequests: notEnoughRequests, requireUpgrade: requireUpgrade, upgradeDescription: upgradeDescription, currentLimit: requestsLimit, currentUsage: requestsCount, isUnlimited: isUnlimited, hasError: Boolean(currentPointer?.error), handlePreviousImage: handlePreviousImage, handleNextImage: handleNextImage, acceptButton: acceptButton, generateButtonLabel: pointer?.current > 0 ? generateAgainText : generateText, instructionsPlaceholder: __("Describe the featured image you'd like to create and select a style.", 'jetpack-ai-client'), imageStyles: imageStyles, onGuessStyle: handleGuessStyle, prompt: prompt, setPrompt: setPrompt, initialStyle: requestStyle, inputDisabled: disableInput, actionDisabled: disableAction })] }));
278
278
  }
@@ -27,7 +27,7 @@ const debug = debugFactory('jetpack-ai:general-purpose-image');
27
27
  export default function GeneralPurposeImage({ placement, onClose = () => { }, onSetImage = () => { }, }) {
28
28
  const [isFeaturedImageModalVisible, setIsFeaturedImageModalVisible] = useState(true);
29
29
  const siteType = useSiteType();
30
- const postContent = usePostContent();
30
+ const { getPostContent } = usePostContent();
31
31
  const { saveToMediaLibrary } = useSaveToMediaLibrary();
32
32
  const { tracks } = useAnalytics();
33
33
  const { recordEvent } = tracks;
@@ -64,7 +64,12 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
64
64
  site_type: siteType,
65
65
  style,
66
66
  });
67
- processImageGeneration({ userPrompt, postContent, notEnoughRequests, style }).catch(error => {
67
+ processImageGeneration({
68
+ userPrompt,
69
+ postContent: getPostContent(),
70
+ notEnoughRequests,
71
+ style,
72
+ }).catch(error => {
68
73
  recordEvent('jetpack_ai_general_image_generation_error', {
69
74
  placement,
70
75
  error: error?.message,
@@ -79,7 +84,7 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
79
84
  generalImageActiveModel,
80
85
  siteType,
81
86
  processImageGeneration,
82
- postContent,
87
+ getPostContent,
83
88
  notEnoughRequests,
84
89
  ]);
85
90
  const handleRegenerate = useCallback(({ userPrompt, style }) => {
@@ -92,7 +97,12 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
92
97
  style,
93
98
  });
94
99
  setCurrent(crrt => crrt + 1);
95
- processImageGeneration({ userPrompt, postContent, notEnoughRequests, style }).catch(error => {
100
+ processImageGeneration({
101
+ userPrompt,
102
+ postContent: getPostContent(),
103
+ notEnoughRequests,
104
+ style,
105
+ }).catch(error => {
96
106
  recordEvent('jetpack_ai_general_image_generation_error', {
97
107
  placement,
98
108
  error: error?.message,
@@ -106,7 +116,7 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
106
116
  generalImageActiveModel,
107
117
  siteType,
108
118
  processImageGeneration,
109
- postContent,
119
+ getPostContent,
110
120
  notEnoughRequests,
111
121
  setCurrent,
112
122
  ]);
@@ -119,7 +129,12 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
119
129
  site_type: siteType,
120
130
  style,
121
131
  });
122
- processImageGeneration({ userPrompt, postContent, notEnoughRequests, style }).catch(error => {
132
+ processImageGeneration({
133
+ userPrompt,
134
+ postContent: getPostContent(),
135
+ notEnoughRequests,
136
+ style,
137
+ }).catch(error => {
123
138
  recordEvent('jetpack_ai_general_image_generation_error', {
124
139
  placement,
125
140
  error: error?.message,
@@ -133,7 +148,7 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
133
148
  generalImageActiveModel,
134
149
  siteType,
135
150
  processImageGeneration,
136
- postContent,
151
+ getPostContent,
137
152
  notEnoughRequests,
138
153
  ]);
139
154
  const handleAccept = useCallback(() => {
@@ -180,5 +195,5 @@ export default function GeneralPurposeImage({ placement, onClose = () => { }, on
180
195
  __("Image generation costs %d requests per image. You don't have enough requests to generate another image.", 'jetpack-ai-client'), generalImageCost)
181
196
  : null;
182
197
  const acceptButton = (_jsx(Button, { onClick: handleAccept, variant: "primary", disabled: !currentImage?.image || currentImage?.generating, children: __('Insert image', 'jetpack-ai-client') }));
183
- return (_jsx(AiImageModal, { postContent: true, images: images, currentIndex: current, title: __('Generate an image with AI', 'jetpack-ai-client'), cost: generalImageCost, open: isFeaturedImageModalVisible, placement: placement, onClose: handleModalClose, onTryAgain: handleTryAgain, onGenerate: pointer?.current > 0 ? handleRegenerate : handleGenerate, generating: currentPointer?.generating, notEnoughRequests: notEnoughRequests, requireUpgrade: requireUpgrade, upgradeDescription: upgradeDescription, currentLimit: requestsLimit, currentUsage: requestsCount, isUnlimited: isUnlimited, hasError: Boolean(currentPointer?.error), handlePreviousImage: handlePreviousImage, handleNextImage: handleNextImage, acceptButton: acceptButton, generateButtonLabel: pointer?.current > 0 ? generateAgainText : generateText, instructionsPlaceholder: __("Describe the image you'd like to create and select a style.", 'jetpack-ai-client'), imageStyles: imageStyles, onGuessStyle: guessStyle, prompt: prompt, setPrompt: setPrompt, inputDisabled: disableInput, actionDisabled: disableAction }));
198
+ return (_jsx(AiImageModal, { images: images, currentIndex: current, title: __('Generate an image with AI', 'jetpack-ai-client'), cost: generalImageCost, open: isFeaturedImageModalVisible, placement: placement, onClose: handleModalClose, onTryAgain: handleTryAgain, onGenerate: pointer?.current > 0 ? handleRegenerate : handleGenerate, generating: currentPointer?.generating, notEnoughRequests: notEnoughRequests, requireUpgrade: requireUpgrade, upgradeDescription: upgradeDescription, currentLimit: requestsLimit, currentUsage: requestsCount, isUnlimited: isUnlimited, hasError: Boolean(currentPointer?.error), handlePreviousImage: handlePreviousImage, handleNextImage: handleNextImage, acceptButton: acceptButton, generateButtonLabel: pointer?.current > 0 ? generateAgainText : generateText, instructionsPlaceholder: __("Describe the image you'd like to create and select a style.", 'jetpack-ai-client'), imageStyles: imageStyles, onGuessStyle: guessStyle, prompt: prompt, setPrompt: setPrompt, inputDisabled: disableInput, actionDisabled: disableAction }));
184
199
  }
@@ -18,7 +18,7 @@ type useAiSuggestionsOptions = {
18
18
  askQuestionOptions?: AskQuestionOptionsArgProps;
19
19
  initialRequestingState?: RequestingStateProp;
20
20
  onSuggestion?: (suggestion: string) => void;
21
- onDone?: (content: string) => void;
21
+ onDone?: (content: string, skipRequestCount?: boolean) => void;
22
22
  onStop?: () => void;
23
23
  onError?: (error: RequestingErrorProps) => void;
24
24
  onAllErrors?: (error: RequestingErrorProps) => void;
@@ -100,8 +100,8 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
100
100
  */
101
101
  const handleDone = useCallback((event) => {
102
102
  closeEventSource();
103
- const fullSuggestion = removeLlamaArtifact(event?.detail);
104
- onDone?.(fullSuggestion);
103
+ const fullSuggestion = removeLlamaArtifact(event?.detail?.message ?? event?.detail);
104
+ onDone?.(fullSuggestion, event?.detail?.source === 'chromeAI');
105
105
  setRequestingState('done');
106
106
  }, [onDone]);
107
107
  const handleAnyError = useCallback((event) => {
@@ -1,5 +1,5 @@
1
- /**
2
- * Internal dependencies
3
- */
4
- declare const usePostContent: () => string;
1
+ declare const usePostContent: () => {
2
+ getPostContent: () => string;
3
+ isEditedPostEmpty: () => boolean;
4
+ };
5
5
  export default usePostContent;
@@ -3,18 +3,28 @@
3
3
  */
4
4
  import { serialize } from '@wordpress/blocks';
5
5
  import { useSelect } from '@wordpress/data';
6
- /**
7
- * Types
8
- */
9
- import { renderMarkdownFromHTML } from '../libs/markdown/index.js';
6
+ import { store as editorStore } from '@wordpress/editor';
7
+ import { useCallback } from '@wordpress/element';
10
8
  /**
11
9
  * Internal dependencies
12
10
  */
11
+ import { renderMarkdownFromHTML } from '../libs/markdown/index.js';
13
12
  /*
14
13
  * Simple helper to get the post content as markdown
15
14
  */
16
15
  const usePostContent = () => {
17
- const blocks = useSelect(select => select('core/block-editor').getBlocks(), []);
18
- return blocks?.length ? renderMarkdownFromHTML({ content: serialize(blocks) }) : '';
16
+ const { getBlocks, isEditedPostEmpty } = useSelect(select => {
17
+ const blockEditorSelect = select('core/block-editor');
18
+ const coreEditorSelect = select(editorStore);
19
+ return {
20
+ getBlocks: blockEditorSelect.getBlocks,
21
+ isEditedPostEmpty: coreEditorSelect.isEditedPostEmpty,
22
+ };
23
+ }, []);
24
+ const getPostContent = useCallback(() => {
25
+ const blocks = getBlocks();
26
+ return blocks?.length ? renderMarkdownFromHTML({ content: serialize(blocks) }) : '';
27
+ }, [getBlocks]);
28
+ return { getPostContent, isEditedPostEmpty };
19
29
  };
20
30
  export default usePostContent;
@@ -1,3 +1,5 @@
1
1
  export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes, } from './markdown/index.js';
2
2
  export type { RenderHTMLRules } from './markdown/index.js';
3
3
  export { mapActionToHumanText } from './map-action-to-human-text.js';
4
+ export { openBlockSidebar } from './open-block-sidebar.js';
5
+ export { showAiAssistantSection } from './show-ai-assistant-section.js';
@@ -1,2 +1,4 @@
1
1
  export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, fixes, } from './markdown/index.js';
2
2
  export { mapActionToHumanText } from './map-action-to-human-text.js';
3
+ export { openBlockSidebar } from './open-block-sidebar.js';
4
+ export { showAiAssistantSection } from './show-ai-assistant-section.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Open the block sidebar for the given client ID.
3
+ *
4
+ * @param {string} clientId - The client ID of the block to open the sidebar for.
5
+ */
6
+ export declare function openBlockSidebar(clientId: string): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { store as blockEditorStore } from '@wordpress/block-editor';
5
+ import { dispatch } from '@wordpress/data';
6
+ /**
7
+ * Open the block sidebar for the given client ID.
8
+ *
9
+ * @param {string} clientId - The client ID of the block to open the sidebar for.
10
+ */
11
+ export function openBlockSidebar(clientId) {
12
+ if (!clientId) {
13
+ return;
14
+ }
15
+ const { selectBlock } = dispatch(blockEditorStore);
16
+ const { enableComplementaryArea } = dispatch('core/interface');
17
+ selectBlock(clientId);
18
+ // This only works for the post editor, as the SEO Assistant is only available there
19
+ enableComplementaryArea('core/edit-post', 'edit-post/block');
20
+ }
@@ -0,0 +1 @@
1
+ export declare const showAiAssistantSection: () => Promise<void>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { dispatch } from '@wordpress/data';
5
+ export const showAiAssistantSection = async () => {
6
+ const { clearSelectedBlock } = dispatch('core/block-editor');
7
+ const { enableComplementaryArea } = dispatch('core/interface');
8
+ // Clear any block selection, because selected blocks have precedence on settings sidebar
9
+ clearSelectedBlock();
10
+ await enableComplementaryArea('core/edit-post', 'jetpack-sidebar/jetpack');
11
+ const panel = document.querySelector('.jetpack-ai-assistant-panel');
12
+ const isAlreadyOpen = panel?.classList.contains('is-opened');
13
+ const button = panel?.querySelector('h2 button');
14
+ if (isAlreadyOpen) {
15
+ // Close it before opening it to ensure the content is scrolled to view
16
+ button?.click();
17
+ }
18
+ setTimeout(() => {
19
+ button?.click();
20
+ }, 50);
21
+ };
package/build/types.d.ts CHANGED
@@ -74,6 +74,22 @@ declare global {
74
74
  }[]>;
75
75
  }>;
76
76
  };
77
+ summarizer?: {
78
+ capabilities: () => Promise<{
79
+ available: 'no' | 'yes' | 'after-download';
80
+ }>;
81
+ create: (options: {
82
+ sharedContext?: string;
83
+ type?: string;
84
+ format?: string;
85
+ length?: string;
86
+ }) => Promise<{
87
+ ready: Promise<void>;
88
+ summarize: (text: string, summarizeOptions?: {
89
+ context?: string;
90
+ }) => Promise<string>;
91
+ }>;
92
+ };
77
93
  };
78
94
  }
79
95
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.26.2",
4
+ "version": "0.27.0",
5
5
  "description": "A JS client for consuming Jetpack AI services",
6
6
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme",
7
7
  "bugs": {
@@ -24,15 +24,15 @@
24
24
  },
25
25
  "type": "module",
26
26
  "devDependencies": {
27
- "@storybook/addon-actions": "8.4.7",
28
- "@storybook/blocks": "8.4.7",
29
- "@storybook/preview-api": "8.4.7",
30
- "@storybook/react": "8.4.7",
27
+ "@storybook/addon-actions": "8.5.8",
28
+ "@storybook/blocks": "8.5.8",
29
+ "@storybook/preview-api": "8.5.8",
30
+ "@storybook/react": "8.5.8",
31
31
  "@types/markdown-it": "14.1.2",
32
32
  "@types/turndown": "5.0.5",
33
33
  "jest": "^29.6.2",
34
34
  "jest-environment-jsdom": "29.7.0",
35
- "storybook": "8.4.7",
35
+ "storybook": "8.5.8",
36
36
  "typescript": "5.0.4"
37
37
  },
38
38
  "exports": {
@@ -44,28 +44,28 @@
44
44
  "main": "./build/index.js",
45
45
  "types": "./build/index.d.ts",
46
46
  "dependencies": {
47
- "@automattic/jetpack-base-styles": "^0.6.42",
48
- "@automattic/jetpack-components": "^0.67.0",
49
- "@automattic/jetpack-connection": "^0.36.7",
50
- "@automattic/jetpack-shared-extension-utils": "^0.17.2",
47
+ "@automattic/jetpack-base-styles": "^0.6.44",
48
+ "@automattic/jetpack-components": "^0.68.0",
49
+ "@automattic/jetpack-connection": "^0.38.0",
50
+ "@automattic/jetpack-shared-extension-utils": "^0.17.4",
51
51
  "@microsoft/fetch-event-source": "2.0.1",
52
52
  "@types/jest": "29.5.14",
53
53
  "@types/react": "18.3.18",
54
54
  "@types/wordpress__block-editor": "11.5.16",
55
- "@wordpress/api-fetch": "7.17.0",
56
- "@wordpress/base-styles": "5.17.0",
57
- "@wordpress/blob": "4.17.0",
58
- "@wordpress/blocks": "14.6.0",
59
- "@wordpress/block-editor": "14.12.0",
60
- "@wordpress/components": "29.3.0",
61
- "@wordpress/compose": "7.17.0",
62
- "@wordpress/data": "10.17.0",
63
- "@wordpress/editor": "14.17.0",
64
- "@wordpress/element": "6.17.0",
65
- "@wordpress/i18n": "5.17.0",
66
- "@wordpress/icons": "10.17.0",
67
- "@wordpress/primitives": "4.17.0",
68
- "@wordpress/url": "4.17.0",
55
+ "@wordpress/api-fetch": "7.19.0",
56
+ "@wordpress/base-styles": "5.19.0",
57
+ "@wordpress/blob": "4.19.0",
58
+ "@wordpress/blocks": "14.8.0",
59
+ "@wordpress/block-editor": "14.14.0",
60
+ "@wordpress/components": "29.5.0",
61
+ "@wordpress/compose": "7.19.0",
62
+ "@wordpress/data": "10.19.0",
63
+ "@wordpress/editor": "14.19.0",
64
+ "@wordpress/element": "6.19.0",
65
+ "@wordpress/i18n": "5.19.0",
66
+ "@wordpress/icons": "10.19.0",
67
+ "@wordpress/primitives": "4.19.0",
68
+ "@wordpress/url": "4.19.0",
69
69
  "clsx": "2.1.1",
70
70
  "debug": "4.4.0",
71
71
  "markdown-it": "14.1.0",
@@ -1,8 +1,5 @@
1
1
  import { getJetpackExtensionAvailability } from '@automattic/jetpack-shared-extension-utils';
2
- import {
3
- PROMPT_TYPE_CHANGE_LANGUAGE,
4
- //PROMPT_TYPE_SUMMARIZE,
5
- } from '../constants.js';
2
+ import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_SUMMARIZE } from '../constants.js';
6
3
  import { PromptProp, PromptItemProps } from '../types.js';
7
4
  import ChromeAISuggestionsEventSource from './suggestions.js';
8
5
 
@@ -19,6 +16,8 @@ interface PromptContext {
19
16
  type?: string;
20
17
  content?: string;
21
18
  language?: string;
19
+ tone?: string;
20
+ words?: number;
22
21
  }
23
22
 
24
23
  /**
@@ -36,7 +35,11 @@ export default async function ChromeAIFactory( promptArg: PromptProp ) {
36
35
  content: '',
37
36
  language: '',
38
37
  };
38
+
39
39
  let promptType = '';
40
+ let tone = null;
41
+ let wordCount = null;
42
+
40
43
  if ( Array.isArray( promptArg ) ) {
41
44
  for ( let i = 0; i < promptArg.length; i++ ) {
42
45
  const prompt: PromptItemProps = promptArg[ i ];
@@ -61,6 +64,14 @@ export default async function ChromeAIFactory( promptArg: PromptProp ) {
61
64
  if ( promptContext.content ) {
62
65
  context.content = promptContext.content;
63
66
  }
67
+
68
+ if ( promptContext.tone ) {
69
+ tone = promptContext.tone;
70
+ }
71
+
72
+ if ( promptContext.words ) {
73
+ wordCount = promptContext.words;
74
+ }
64
75
  }
65
76
  }
66
77
 
@@ -112,17 +123,19 @@ export default async function ChromeAIFactory( promptArg: PromptProp ) {
112
123
  return chromeAI;
113
124
  }
114
125
 
115
- // TODO
116
- if ( promptType.startsWith( 'ai-assistant-summarize' ) ) {
117
- /*
118
- return new ChromeAISuggestionsEventSource({
119
- content: "",
126
+ // TODO: consider also using ChromeAI for ai-assistant-summarize
127
+ if ( promptType.startsWith( 'ai-content-lens' ) ) {
128
+ const summaryOpts = {
129
+ tone: tone,
130
+ wordCount: wordCount,
131
+ };
132
+
133
+ // TODO: detect if the content is in English and fallback if it's not
134
+ return new ChromeAISuggestionsEventSource( {
135
+ content: context.content,
120
136
  promptType: PROMPT_TYPE_SUMMARIZE,
121
- options: {},
137
+ options: summaryOpts,
122
138
  } );
123
- */
124
-
125
- return false;
126
139
  }
127
140
 
128
141
  return false;
@@ -15,6 +15,10 @@ type ChromeAISuggestionsEventSourceConstructorArgs = {
15
15
  sourceLanguage?: string;
16
16
  targetLanguage?: string;
17
17
 
18
+ // summarization
19
+ tone?: string;
20
+ wordCount?: number;
21
+
18
22
  // not sure if we need these
19
23
  functions?: Array< object >;
20
24
  model?: AiModelTypeProp;
@@ -64,7 +68,7 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
64
68
  }
65
69
 
66
70
  if ( promptType === PROMPT_TYPE_SUMMARIZE ) {
67
- this.summarize( content );
71
+ this.summarize( content, options.tone, options.wordCount );
68
72
  }
69
73
  }
70
74
 
@@ -83,12 +87,14 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
83
87
  return;
84
88
  }
85
89
 
86
- if ( e.event === 'translation' ) {
90
+ if ( e.event === 'translation' || e.event === 'summary' ) {
87
91
  this.dispatchEvent( new CustomEvent( 'suggestion', { detail: data.message } ) );
88
92
  }
89
93
 
90
94
  if ( data.complete ) {
91
- this.dispatchEvent( new CustomEvent( 'done', { detail: data.message } ) );
95
+ this.dispatchEvent(
96
+ new CustomEvent( 'done', { detail: { message: data.message, source: 'chromeAI' } } )
97
+ );
92
98
  }
93
99
  }
94
100
 
@@ -132,8 +138,58 @@ export default class ChromeAISuggestionsEventSource extends EventTarget {
132
138
  }
133
139
  }
134
140
 
135
- // TODO
136
- async summarize( text: string ) {
137
- return text;
141
+ // Helper function to format summarizer options
142
+ private getSummarizerOptions( tone?: string, wordCount?: number ) {
143
+ let sharedContext = `The summary you write should contain approximately ${
144
+ wordCount ?? 50
145
+ } words long. Strive for precision in word count without compromising clarity and significance`;
146
+
147
+ if ( tone ) {
148
+ sharedContext += `\n - Write with a ${ tone } tone.\n`;
149
+ }
150
+
151
+ const options = {
152
+ sharedContext: sharedContext,
153
+ type: 'teaser',
154
+ format: 'plain-text',
155
+ length: 'medium',
156
+ };
157
+
158
+ return options;
159
+ }
160
+
161
+ // use the Chrome AI summarizer
162
+ async summarize( text: string, tone?: string, wordCount?: number ) {
163
+ if ( ! ( 'ai' in self ) || ! ( 'summarizer' in self.ai ) ) {
164
+ return;
165
+ }
166
+ const available = ( await self.ai.summarizer.capabilities() ).available;
167
+
168
+ if ( available === 'no' ) {
169
+ return;
170
+ }
171
+
172
+ const options = this.getSummarizerOptions( tone, wordCount );
173
+
174
+ const summarizer = await self.ai.summarizer.create( options );
175
+
176
+ if ( available === 'after-download' ) {
177
+ await summarizer.ready;
178
+ }
179
+
180
+ try {
181
+ const context = `Write with a ${ tone } tone.`;
182
+ const summary = await summarizer.summarize( text, { context: context } );
183
+ this.processEvent( {
184
+ id: '',
185
+ event: 'summary',
186
+ data: JSON.stringify( {
187
+ message: summary,
188
+ complete: true,
189
+ } ),
190
+ } );
191
+ } catch ( error ) {
192
+ this.processErrorEvent( error );
193
+ }
138
194
  }
139
195
  }
@@ -41,7 +41,6 @@ type AiImageModalProps = {
41
41
  isUnlimited: boolean;
42
42
  upgradeDescription: string;
43
43
  hasError: boolean;
44
- postContent?: string | boolean | null;
45
44
  handlePreviousImage: () => void;
46
45
  handleNextImage: () => void;
47
46
  acceptButton: React.JSX.Element;
@@ -57,7 +57,7 @@ export default function FeaturedImage( {
57
57
  placement === PLACEMENT_MEDIA_SOURCE_DROPDOWN
58
58
  );
59
59
  const siteType = useSiteType();
60
- const postContent = usePostContent();
60
+ const { getPostContent, isEditedPostEmpty } = usePostContent();
61
61
  const { postTitle, postFeaturedMediaId, isEditorPanelOpened } = useSelect( select => {
62
62
  return {
63
63
  postTitle: select( editorStore ).getEditedPostAttribute( 'title' ),
@@ -133,10 +133,10 @@ export default function FeaturedImage( {
133
133
  */
134
134
  const handleGuessStyle = useCallback(
135
135
  userPrompt => {
136
- const content = postTitle + '\n\n' + postContent;
136
+ const content = postTitle + '\n\n' + getPostContent();
137
137
  return guessStyle( userPrompt, 'featured-image-guess-style', content );
138
138
  },
139
- [ postContent, postTitle, guessStyle ]
139
+ [ postTitle, getPostContent, guessStyle ]
140
140
  );
141
141
 
142
142
  const handleGenerate = useCallback(
@@ -159,7 +159,7 @@ export default function FeaturedImage( {
159
159
  setIsFeaturedImageModalVisible( true );
160
160
  return processImageGeneration( {
161
161
  userPrompt,
162
- postContent: postTitle + '\n\n' + postContent,
162
+ postContent: postTitle + '\n\n' + getPostContent(),
163
163
  notEnoughRequests,
164
164
  style,
165
165
  } ).catch( error => {
@@ -178,7 +178,7 @@ export default function FeaturedImage( {
178
178
  featuredImageActiveModel,
179
179
  siteType,
180
180
  processImageGeneration,
181
- postContent,
181
+ getPostContent,
182
182
  notEnoughRequests,
183
183
  postTitle,
184
184
  ]
@@ -209,7 +209,7 @@ export default function FeaturedImage( {
209
209
  setCurrent( () => images.length );
210
210
  processImageGeneration( {
211
211
  userPrompt,
212
- postContent: postTitle + '\n\n' + postContent,
212
+ postContent: postTitle + '\n\n' + getPostContent(),
213
213
  notEnoughRequests,
214
214
  style,
215
215
  } ).catch( error => {
@@ -231,7 +231,7 @@ export default function FeaturedImage( {
231
231
  setCurrent,
232
232
  processImageGeneration,
233
233
  postTitle,
234
- postContent,
234
+ getPostContent,
235
235
  notEnoughRequests,
236
236
  images,
237
237
  ]
@@ -249,7 +249,7 @@ export default function FeaturedImage( {
249
249
 
250
250
  processImageGeneration( {
251
251
  userPrompt,
252
- postContent: postTitle + '\n\n' + postContent,
252
+ postContent: postTitle + '\n\n' + getPostContent(),
253
253
  notEnoughRequests,
254
254
  style,
255
255
  } ).catch( error => {
@@ -268,7 +268,7 @@ export default function FeaturedImage( {
268
268
  featuredImageActiveModel,
269
269
  siteType,
270
270
  processImageGeneration,
271
- postContent,
271
+ getPostContent,
272
272
  notEnoughRequests,
273
273
  postTitle,
274
274
  ]
@@ -346,7 +346,7 @@ export default function FeaturedImage( {
346
346
  const generateAgainText = __( 'Generate another image', 'jetpack-ai-client' );
347
347
  const generateText = __( 'Generate', 'jetpack-ai-client' );
348
348
 
349
- const hasContent = postContent.trim?.() || postTitle.trim?.() ? true : false;
349
+ const hasContent = ! isEditedPostEmpty() || postTitle.trim?.() ? true : false;
350
350
  const hasPrompt = hasContent ? prompt.length >= 0 : prompt.length >= 3;
351
351
  const disableInput = notEnoughRequests || currentPointer?.generating || requireUpgrade;
352
352
  const disableAction = disableInput || ( ! hasContent && ! hasPrompt );
@@ -396,7 +396,6 @@ export default function FeaturedImage( {
396
396
  </>
397
397
  ) }
398
398
  <AiImageModal
399
- postContent={ hasContent }
400
399
  autoStart={ hasContent && ! postFeaturedMediaId }
401
400
  autoStartAction={ handleFirstGenerate }
402
401
  images={ images }
@@ -54,7 +54,7 @@ export default function GeneralPurposeImage( {
54
54
  }: GeneralPurposeImageProps ) {
55
55
  const [ isFeaturedImageModalVisible, setIsFeaturedImageModalVisible ] = useState( true );
56
56
  const siteType = useSiteType();
57
- const postContent = usePostContent();
57
+ const { getPostContent } = usePostContent();
58
58
  const { saveToMediaLibrary } = useSaveToMediaLibrary();
59
59
  const { tracks } = useAnalytics();
60
60
  const { recordEvent } = tracks;
@@ -111,17 +111,20 @@ export default function GeneralPurposeImage( {
111
111
  site_type: siteType,
112
112
  style,
113
113
  } );
114
- processImageGeneration( { userPrompt, postContent, notEnoughRequests, style } ).catch(
115
- error => {
116
- recordEvent( 'jetpack_ai_general_image_generation_error', {
117
- placement,
118
- error: error?.message,
119
- model: generalImageActiveModel,
120
- site_type: siteType,
121
- style,
122
- } );
123
- }
124
- );
114
+ processImageGeneration( {
115
+ userPrompt,
116
+ postContent: getPostContent(),
117
+ notEnoughRequests,
118
+ style,
119
+ } ).catch( error => {
120
+ recordEvent( 'jetpack_ai_general_image_generation_error', {
121
+ placement,
122
+ error: error?.message,
123
+ model: generalImageActiveModel,
124
+ site_type: siteType,
125
+ style,
126
+ } );
127
+ } );
125
128
  },
126
129
  [
127
130
  recordEvent,
@@ -129,7 +132,7 @@ export default function GeneralPurposeImage( {
129
132
  generalImageActiveModel,
130
133
  siteType,
131
134
  processImageGeneration,
132
- postContent,
135
+ getPostContent,
133
136
  notEnoughRequests,
134
137
  ]
135
138
  );
@@ -146,16 +149,19 @@ export default function GeneralPurposeImage( {
146
149
  } );
147
150
 
148
151
  setCurrent( crrt => crrt + 1 );
149
- processImageGeneration( { userPrompt, postContent, notEnoughRequests, style } ).catch(
150
- error => {
151
- recordEvent( 'jetpack_ai_general_image_generation_error', {
152
- placement,
153
- error: error?.message,
154
- model: generalImageActiveModel,
155
- site_type: siteType,
156
- } );
157
- }
158
- );
152
+ processImageGeneration( {
153
+ userPrompt,
154
+ postContent: getPostContent(),
155
+ notEnoughRequests,
156
+ style,
157
+ } ).catch( error => {
158
+ recordEvent( 'jetpack_ai_general_image_generation_error', {
159
+ placement,
160
+ error: error?.message,
161
+ model: generalImageActiveModel,
162
+ site_type: siteType,
163
+ } );
164
+ } );
159
165
  },
160
166
  [
161
167
  recordEvent,
@@ -163,7 +169,7 @@ export default function GeneralPurposeImage( {
163
169
  generalImageActiveModel,
164
170
  siteType,
165
171
  processImageGeneration,
166
- postContent,
172
+ getPostContent,
167
173
  notEnoughRequests,
168
174
  setCurrent,
169
175
  ]
@@ -180,16 +186,19 @@ export default function GeneralPurposeImage( {
180
186
  style,
181
187
  } );
182
188
 
183
- processImageGeneration( { userPrompt, postContent, notEnoughRequests, style } ).catch(
184
- error => {
185
- recordEvent( 'jetpack_ai_general_image_generation_error', {
186
- placement,
187
- error: error?.message,
188
- model: generalImageActiveModel,
189
- site_type: siteType,
190
- } );
191
- }
192
- );
189
+ processImageGeneration( {
190
+ userPrompt,
191
+ postContent: getPostContent(),
192
+ notEnoughRequests,
193
+ style,
194
+ } ).catch( error => {
195
+ recordEvent( 'jetpack_ai_general_image_generation_error', {
196
+ placement,
197
+ error: error?.message,
198
+ model: generalImageActiveModel,
199
+ site_type: siteType,
200
+ } );
201
+ } );
193
202
  },
194
203
  [
195
204
  recordEvent,
@@ -197,7 +206,7 @@ export default function GeneralPurposeImage( {
197
206
  generalImageActiveModel,
198
207
  siteType,
199
208
  processImageGeneration,
200
- postContent,
209
+ getPostContent,
201
210
  notEnoughRequests,
202
211
  ]
203
212
  );
@@ -266,7 +275,6 @@ export default function GeneralPurposeImage( {
266
275
 
267
276
  return (
268
277
  <AiImageModal
269
- postContent={ true }
270
278
  images={ images }
271
279
  currentIndex={ current }
272
280
  title={ __( 'Generate an image with AI', 'jetpack-ai-client' ) }
@@ -70,7 +70,7 @@ type useAiSuggestionsOptions = {
70
70
  /*
71
71
  * onDone callback.
72
72
  */
73
- onDone?: ( content: string ) => void;
73
+ onDone?: ( content: string, skipRequestCount?: boolean ) => void;
74
74
 
75
75
  /*
76
76
  * onStop callback.
@@ -256,9 +256,9 @@ export default function useAiSuggestions( {
256
256
  ( event: CustomEvent ) => {
257
257
  closeEventSource();
258
258
 
259
- const fullSuggestion = removeLlamaArtifact( event?.detail );
259
+ const fullSuggestion = removeLlamaArtifact( event?.detail?.message ?? event?.detail );
260
260
 
261
- onDone?.( fullSuggestion );
261
+ onDone?.( fullSuggestion, event?.detail?.source === 'chromeAI' );
262
262
  setRequestingState( 'done' );
263
263
  },
264
264
  [ onDone ]
@@ -3,25 +3,38 @@
3
3
  */
4
4
  import { serialize } from '@wordpress/blocks';
5
5
  import { useSelect } from '@wordpress/data';
6
+ import { store as editorStore } from '@wordpress/editor';
7
+ import { useCallback } from '@wordpress/element';
6
8
  /**
7
- * Types
9
+ * Internal dependencies
8
10
  */
9
11
  import { renderMarkdownFromHTML } from '../libs/markdown/index.js';
10
- import type * as BlockEditorSelectors from '@wordpress/block-editor/store/selectors.js';
11
12
  /**
12
- * Internal dependencies
13
+ * Types
13
14
  */
15
+ import type * as BlockEditorSelectors from '@wordpress/block-editor/store/selectors.js';
14
16
 
15
17
  /*
16
18
  * Simple helper to get the post content as markdown
17
19
  */
18
20
  const usePostContent = () => {
19
- const blocks = useSelect(
20
- select => ( select( 'core/block-editor' ) as typeof BlockEditorSelectors ).getBlocks(),
21
- []
22
- );
21
+ const { getBlocks, isEditedPostEmpty } = useSelect( select => {
22
+ const blockEditorSelect = select( 'core/block-editor' ) as typeof BlockEditorSelectors;
23
+ const coreEditorSelect = select( editorStore );
24
+
25
+ return {
26
+ getBlocks: blockEditorSelect.getBlocks,
27
+ isEditedPostEmpty: coreEditorSelect.isEditedPostEmpty,
28
+ };
29
+ }, [] );
30
+
31
+ const getPostContent = useCallback( () => {
32
+ const blocks = getBlocks();
33
+
34
+ return blocks?.length ? renderMarkdownFromHTML( { content: serialize( blocks ) } ) : '';
35
+ }, [ getBlocks ] );
23
36
 
24
- return blocks?.length ? renderMarkdownFromHTML( { content: serialize( blocks ) } ) : '';
37
+ return { getPostContent, isEditedPostEmpty };
25
38
  };
26
39
 
27
40
  export default usePostContent;
package/src/libs/index.ts CHANGED
@@ -9,3 +9,7 @@ export {
9
9
  export type { RenderHTMLRules } from './markdown/index.js';
10
10
 
11
11
  export { mapActionToHumanText } from './map-action-to-human-text.js';
12
+
13
+ export { openBlockSidebar } from './open-block-sidebar.js';
14
+
15
+ export { showAiAssistantSection } from './show-ai-assistant-section.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { store as blockEditorStore } from '@wordpress/block-editor';
5
+ import { dispatch } from '@wordpress/data';
6
+
7
+ type CoreInterfaceDispatch = {
8
+ enableComplementaryArea: ( area: string, slot: string ) => Promise< void >;
9
+ };
10
+
11
+ /**
12
+ * Open the block sidebar for the given client ID.
13
+ *
14
+ * @param {string} clientId - The client ID of the block to open the sidebar for.
15
+ */
16
+ export function openBlockSidebar( clientId: string ) {
17
+ if ( ! clientId ) {
18
+ return;
19
+ }
20
+
21
+ const { selectBlock } = dispatch( blockEditorStore );
22
+ const { enableComplementaryArea } = dispatch( 'core/interface' ) as CoreInterfaceDispatch;
23
+
24
+ selectBlock( clientId );
25
+ // This only works for the post editor, as the SEO Assistant is only available there
26
+ enableComplementaryArea( 'core/edit-post', 'edit-post/block' );
27
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { dispatch } from '@wordpress/data';
5
+
6
+ export const showAiAssistantSection = async () => {
7
+ const { clearSelectedBlock } = dispatch( 'core/block-editor' );
8
+ const { enableComplementaryArea } = dispatch( 'core/interface' ) as {
9
+ enableComplementaryArea: ( area: string, slot: string ) => Promise< void >;
10
+ };
11
+
12
+ // Clear any block selection, because selected blocks have precedence on settings sidebar
13
+ clearSelectedBlock();
14
+ await enableComplementaryArea( 'core/edit-post', 'jetpack-sidebar/jetpack' );
15
+
16
+ const panel = document.querySelector( '.jetpack-ai-assistant-panel' );
17
+ const isAlreadyOpen = panel?.classList.contains( 'is-opened' );
18
+ const button: HTMLElement | null | undefined = panel?.querySelector( 'h2 button' );
19
+
20
+ if ( isAlreadyOpen ) {
21
+ // Close it before opening it to ensure the content is scrolled to view
22
+ button?.click();
23
+ }
24
+
25
+ setTimeout( () => {
26
+ button?.click();
27
+ }, 50 );
28
+ };
package/src/types.ts CHANGED
@@ -158,6 +158,20 @@ declare global {
158
158
  >;
159
159
  } >;
160
160
  };
161
+ summarizer?: {
162
+ capabilities: () => Promise< {
163
+ available: 'no' | 'yes' | 'after-download';
164
+ } >;
165
+ create: ( options: {
166
+ sharedContext?: string;
167
+ type?: string;
168
+ format?: string;
169
+ length?: string;
170
+ } ) => Promise< {
171
+ ready: Promise< void >;
172
+ summarize: ( text: string, summarizeOptions?: { context?: string } ) => Promise< string >;
173
+ } >;
174
+ };
161
175
  };
162
176
  }
163
177
  }