@automattic/jetpack-ai-client 0.33.32 → 0.34.1

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.
@@ -1,160 +0,0 @@
1
- import debugFactory from 'debug';
2
- import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_SUMMARIZE } from "../constants.js";
3
- import { getErrorData } from "../hooks/use-ai-suggestions/index.js";
4
- import { renderHTMLFromMarkdown, renderMarkdownFromHTML } from "../libs/markdown/index.js";
5
- import { ERROR_RESPONSE, ERROR_NETWORK } from "../types.js";
6
- const debug = debugFactory('ai-client:chrome-ai-suggestions');
7
- export default class ChromeAISuggestionsEventSource extends EventTarget {
8
- fullMessage;
9
- fullFunctionCall;
10
- isPromptClear;
11
- controller;
12
- errorUnclearPromptTriggered;
13
- constructor(data) {
14
- super();
15
- this.fullMessage = '';
16
- this.fullFunctionCall = {
17
- name: '',
18
- arguments: '',
19
- };
20
- this.isPromptClear = false;
21
- this.controller = new AbortController();
22
- this.initSource(data);
23
- }
24
- initSource({ content, promptType, options = {}, }) {
25
- debug('initSource', content, promptType, options);
26
- if (promptType === PROMPT_TYPE_CHANGE_LANGUAGE) {
27
- this.translate(content, options.targetLanguage, options.sourceLanguage);
28
- }
29
- if (promptType === PROMPT_TYPE_SUMMARIZE) {
30
- this.summarize(content, options.tone, options.wordCount);
31
- }
32
- }
33
- async initEventSource() { }
34
- close() { }
35
- checkForUnclearPrompt() { }
36
- processEvent(e) {
37
- let data;
38
- debug('processEvent', e);
39
- try {
40
- data = JSON.parse(e.data);
41
- }
42
- catch (err) {
43
- this.processErrorEvent(err);
44
- return;
45
- }
46
- if (e.event === 'translation' || e.event === 'summary') {
47
- this.dispatchEvent(new CustomEvent('suggestion', { detail: data.message }));
48
- }
49
- if (data.complete) {
50
- this.dispatchEvent(new CustomEvent('done', { detail: { message: data.message, source: 'chromeAI' } }));
51
- }
52
- }
53
- processErrorEvent(e) {
54
- debug('processErrorEvent', e);
55
- // Dispatch a generic network error event
56
- this.dispatchEvent(new CustomEvent(ERROR_NETWORK, { detail: e }));
57
- this.dispatchEvent(new CustomEvent(ERROR_RESPONSE, {
58
- detail: getErrorData(ERROR_NETWORK),
59
- }));
60
- }
61
- // use the Chrome AI translator
62
- async translate(text, target, source = '') {
63
- if (!('Translator' in self)) {
64
- return;
65
- }
66
- const translatorAvailability = await self.Translator.availability({
67
- sourceLanguage: source,
68
- targetLanguage: target,
69
- });
70
- if (translatorAvailability === 'unavailable') {
71
- debug('awaiting translator ready');
72
- this.processErrorEvent({
73
- message: 'Translator is unavailable',
74
- });
75
- return;
76
- }
77
- const translator = await self.Translator.create({
78
- sourceLanguage: source,
79
- targetLanguage: target,
80
- });
81
- if (!translator) {
82
- this.processErrorEvent({
83
- message: 'Translator failed to initialize',
84
- });
85
- return;
86
- }
87
- try {
88
- const translation = await translator.translate(renderHTMLFromMarkdown({ content: text }));
89
- this.processEvent({
90
- id: '',
91
- event: 'translation',
92
- data: JSON.stringify({
93
- message: renderMarkdownFromHTML({ content: translation }),
94
- complete: true,
95
- }),
96
- });
97
- }
98
- catch (error) {
99
- this.processErrorEvent(error);
100
- }
101
- }
102
- // Helper function to format summarizer options
103
- getSummarizerOptions(tone, wordCount) {
104
- let sharedContext = `The summary you write should contain strictly less than ${wordCount ?? 50} words. Strive for precision in word count without compromising clarity and significance`;
105
- if (tone) {
106
- sharedContext += `\n - Write with a ${tone} tone.\n`;
107
- }
108
- const options = {
109
- sharedContext: sharedContext,
110
- type: 'teaser',
111
- format: 'plain-text',
112
- length: 'medium',
113
- };
114
- return options;
115
- }
116
- // use the Chrome AI summarizer
117
- async summarize(text, tone, wordCount) {
118
- debug('summarize', text, tone, wordCount);
119
- if (!('Summarizer' in self)) {
120
- return;
121
- }
122
- const availability = await self.Summarizer.availability();
123
- if (availability === 'unavailable') {
124
- this.processErrorEvent({
125
- data: { message: 'Summarizer is unavailable' },
126
- });
127
- return;
128
- }
129
- const summarizerOptions = this.getSummarizerOptions(tone, wordCount);
130
- const summarizer = await self.Summarizer.create(summarizerOptions);
131
- if (availability !== 'available') {
132
- debug('awaiting summarizer ready');
133
- await summarizer.ready;
134
- }
135
- try {
136
- const context = `Write with a ${tone} tone.`;
137
- debug('context', context);
138
- let summary = await summarizer.summarize(text, { context: context });
139
- debug('summary', summary);
140
- wordCount = wordCount ?? 50;
141
- // gemini-nano has a tendency to exceed the word count, so we need to check and summarize again if necessary
142
- if (summary.split(' ').length > wordCount) {
143
- debug('summary exceeds word count');
144
- summary = await summarizer.summarize(summary, { context: context });
145
- }
146
- this.processEvent({
147
- id: '',
148
- event: 'summary',
149
- data: JSON.stringify({
150
- message: summary,
151
- complete: true,
152
- }),
153
- });
154
- }
155
- catch (error) {
156
- debug('error', error);
157
- this.processErrorEvent(error);
158
- }
159
- }
160
- }
@@ -1,199 +0,0 @@
1
- import debugFactory from 'debug';
2
- import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_SUMMARIZE } from '../constants.ts';
3
- import { PromptProp, PromptItemProps } from '../types.ts';
4
- import { isChromeAIAvailable } from './get-availability.ts';
5
- import ChromeAISuggestionsEventSource from './suggestions.ts';
6
-
7
- const debug = debugFactory( 'ai-client:chrome-ai-factory' );
8
-
9
- interface PromptContext {
10
- type?: string;
11
- content?: string;
12
- language?: string;
13
- tone?: string;
14
- words?: number;
15
- }
16
-
17
- /**
18
- * This will return an instance of ChromeAISuggestionsEventSource or false.
19
- *
20
- * @param promptArg - The messages array of the prompt.
21
- * @return ChromeAISuggestionsEventSource | bool
22
- */
23
- export default async function ChromeAIFactory( promptArg: PromptProp ) {
24
- if ( ! ( await isChromeAIAvailable() ) ) {
25
- debug( 'Chrome AI is not available' );
26
- return false;
27
- }
28
-
29
- const context = {
30
- content: '',
31
- language: '',
32
- };
33
-
34
- let promptType = '';
35
- let tone = null;
36
- let wordCount = null;
37
-
38
- debug( 'promptArg', promptArg );
39
- if ( Array.isArray( promptArg ) ) {
40
- for ( let i = 0; i < promptArg.length; i++ ) {
41
- const prompt: PromptItemProps = promptArg[ i ];
42
- if ( prompt.content ) {
43
- context.content = prompt.content;
44
- }
45
-
46
- if ( ! ( 'context' in prompt ) ) {
47
- continue;
48
- }
49
-
50
- const promptContext: PromptContext = prompt.context;
51
-
52
- if ( promptContext.type ) {
53
- promptType = promptContext.type;
54
- }
55
-
56
- if ( promptContext.language ) {
57
- context.language = promptContext.language;
58
- }
59
-
60
- if ( promptContext.content ) {
61
- context.content = promptContext.content;
62
- }
63
-
64
- if ( promptContext.tone ) {
65
- tone = promptContext.tone;
66
- }
67
-
68
- if ( promptContext.words ) {
69
- wordCount = promptContext.words;
70
- }
71
- }
72
- }
73
-
74
- debug( 'promptType', promptType );
75
- // Early return if the prompt type is not supported.
76
- if (
77
- ! promptType.startsWith( 'ai-assistant-change-language' ) &&
78
- ! promptType.startsWith( 'ai-content-lens' )
79
- ) {
80
- debug( 'promptType is not supported' );
81
- return false;
82
- }
83
-
84
- // If the languageDetector is not available, we can't use the translation or summary features—it's safer to fall back
85
- // to the default AI model than to risk an unexpected error.
86
- if (
87
- ! ( 'LanguageDetector' in self ) ||
88
- ! self.LanguageDetector.create ||
89
- ! self.LanguageDetector.availability
90
- ) {
91
- debug( 'LanguageDetector is not available' );
92
- return false;
93
- }
94
-
95
- const languageDetectorAvailability = await self.LanguageDetector.availability();
96
- if ( languageDetectorAvailability === 'unavailable' ) {
97
- debug( 'LanguageDetector is unavailable' );
98
- return false;
99
- }
100
-
101
- const detector = await self.LanguageDetector.create();
102
- if ( languageDetectorAvailability !== 'available' ) {
103
- debug( 'awaiting detector ready' );
104
- await detector.ready;
105
- }
106
-
107
- if ( promptType.startsWith( 'ai-assistant-change-language' ) ) {
108
- const [ language ] = context.language.split( ' ' );
109
-
110
- if (
111
- ! ( 'Translator' in self ) ||
112
- ! self.Translator.create ||
113
- ! self.Translator.availability
114
- ) {
115
- debug( 'Translator is not available' );
116
- return false;
117
- }
118
-
119
- const languageOpts = {
120
- sourceLanguage: 'en',
121
- targetLanguage: language,
122
- };
123
-
124
- const confidences = await detector.detect( context.content );
125
-
126
- for ( const confidence of confidences ) {
127
- // 75% confidence is just a value that was picked. Generally
128
- // 80% of higher is pretty safe, but the source language is
129
- // required for the translator to work at all, which is also
130
- // why en is the default language.
131
- if ( confidence.confidence > 0.75 ) {
132
- languageOpts.sourceLanguage = confidence.detectedLanguage;
133
- break;
134
- }
135
- }
136
-
137
- debug( 'languageOpts', languageOpts );
138
- const translationAvailability = await self.Translator.availability( languageOpts );
139
-
140
- debug( 'translationAvailability', translationAvailability );
141
- if ( translationAvailability === 'unavailable' ) {
142
- debug( 'Translator is unavailable' );
143
- return false;
144
- }
145
-
146
- const chromeAI = new ChromeAISuggestionsEventSource( {
147
- content: context.content,
148
- promptType: PROMPT_TYPE_CHANGE_LANGUAGE,
149
- options: languageOpts,
150
- } );
151
-
152
- return chromeAI;
153
- }
154
-
155
- if ( promptType.startsWith( 'ai-content-lens' ) ) {
156
- if ( ! ( 'Summarizer' in self ) ) {
157
- debug( 'Summarizer is not available' );
158
- return false;
159
- }
160
-
161
- if ( context.language && context.language !== 'en (English)' ) {
162
- debug( 'Summary is not English' );
163
- return false;
164
- }
165
-
166
- debug( 'awaiting detector detect' );
167
- const confidences = await detector.detect( context.content );
168
-
169
- // if it doesn't look like the content is in English, we can't use the summary feature
170
- for ( const confidence of confidences ) {
171
- // 75% confidence is just a value that was picked. Generally
172
- // 80% of higher is pretty safe, but the source language is
173
- // required for the translator to work at all, which is also
174
- // why en is the default language.
175
- if ( confidence.confidence > 0.75 && confidence.detectedLanguage !== 'en' ) {
176
- debug( 'Confidence for non-English content' );
177
- return false;
178
- }
179
- }
180
-
181
- const summaryOpts = {
182
- tone: tone,
183
- wordCount: wordCount,
184
- };
185
-
186
- debug( 'summaryOpts', summaryOpts );
187
-
188
- const chromeAiEventSourceOpts = {
189
- content: context.content,
190
- promptType: PROMPT_TYPE_SUMMARIZE,
191
- options: summaryOpts,
192
- };
193
-
194
- debug( 'chromeAiEventSourceOpts', chromeAiEventSourceOpts );
195
- return new ChromeAISuggestionsEventSource( chromeAiEventSourceOpts );
196
- }
197
-
198
- return false;
199
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { initializeExPlat, createExPlatClient } from '@automattic/jetpack-explat';
5
- import { select } from '@wordpress/data';
6
- import { addQueryArgs } from '@wordpress/url';
7
- import debugFactory from 'debug';
8
- /**
9
- * Internal dependencies
10
- */
11
- import apiFetch from '../api-fetch/index.ts';
12
-
13
- /**
14
- * Types
15
- */
16
- type FeatureControl = {
17
- enabled: boolean;
18
- };
19
-
20
- type PlansSelect = {
21
- getAiAssistantFeature: () => {
22
- currentTier?: { value: number };
23
- featuresControl?: Record< string, FeatureControl >;
24
- };
25
- };
26
-
27
- const debug = debugFactory( 'ai-client:chrome-ai-availability' );
28
-
29
- /**
30
- * Get the AI Assistant feature.
31
- *
32
- * @return {object} The AI Assistant feature.
33
- */
34
- function getAiAssistantFeature() {
35
- const { getAiAssistantFeature: getFeature } = select( 'wordpress-com/plans' ) as PlansSelect;
36
- return getFeature();
37
- }
38
-
39
- /**
40
- * Fetch an experiment assignment.
41
- *
42
- * @param {boolean} asConnectedUser - Whether the user is connected.
43
- * @return {Function} A function that fetches an experiment assignment.
44
- */
45
- const fetchExperimentAssignmentWithConnectedUser = async ( {
46
- experimentName,
47
- }: {
48
- experimentName: string;
49
- } ): Promise< unknown > => {
50
- const params = {
51
- experiment_name: experimentName,
52
- anon_id: undefined,
53
- as_connected_user: true,
54
- };
55
-
56
- debug( 'params', params );
57
-
58
- const assignmentsRequestUrl = addQueryArgs(
59
- 'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/jetpack',
60
- params
61
- );
62
-
63
- debug( 'assignmentsRequestUrl', assignmentsRequestUrl );
64
-
65
- return apiFetch( {
66
- url: assignmentsRequestUrl,
67
- credentials: 'include',
68
- mode: 'cors',
69
- global: true,
70
- } );
71
- };
72
-
73
- /**
74
- * Check if Chrome AI can be enabled.
75
- *
76
- * @return {boolean} Whether Chrome AI can be enabled.
77
- */
78
- export async function isChromeAIAvailable() {
79
- const { featuresControl } = getAiAssistantFeature();
80
-
81
- // Extra check if we want to control this via the feature flag for now
82
- if ( featuresControl?.[ 'chrome-ai' ]?.enabled !== true ) {
83
- debug( 'feature is disabled for this site/user' );
84
- return false;
85
- }
86
-
87
- initializeExPlat();
88
-
89
- const { loadExperimentAssignment: loadExperimentAssignmentWithAuth } = createExPlatClient( {
90
- fetchExperimentAssignment: fetchExperimentAssignmentWithConnectedUser,
91
- getAnonId: async () => null,
92
- logError: debug,
93
- isDevelopmentMode: false,
94
- } );
95
-
96
- const { variationName } = await loadExperimentAssignmentWithAuth(
97
- 'calypso_jetpack_ai_gemini_api_202503_v2'
98
- );
99
-
100
- debug( 'variationName', variationName );
101
-
102
- return variationName === 'treatment';
103
- }
104
-
105
- export default isChromeAIAvailable;
@@ -1,2 +0,0 @@
1
- export { default as ChromeAIFactory } from './factory.ts';
2
- export { default as ChromeAISuggestionsEventSource } from './suggestions.ts';
@@ -1,235 +0,0 @@
1
- import { EventSourceMessage } from '@microsoft/fetch-event-source';
2
- import debugFactory from 'debug';
3
- import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_SUMMARIZE } from '../constants.ts';
4
- import { getErrorData } from '../hooks/use-ai-suggestions/index.ts';
5
- import { renderHTMLFromMarkdown, renderMarkdownFromHTML } from '../libs/markdown/index.ts';
6
- import { AiModelTypeProp, ERROR_RESPONSE, ERROR_NETWORK } from '../types.ts';
7
-
8
- type ChromeAISuggestionsEventSourceConstructorArgs = {
9
- content: string;
10
- promptType: string;
11
- options?: {
12
- postId?: number | string;
13
- feature?: 'ai-assistant-experimental' | string | undefined;
14
-
15
- // translation
16
- sourceLanguage?: string;
17
- targetLanguage?: string;
18
-
19
- // summarization
20
- tone?: string;
21
- wordCount?: number;
22
-
23
- // not sure if we need these
24
- functions?: Array< object >;
25
- model?: AiModelTypeProp;
26
- };
27
- };
28
-
29
- type ChromeAIEvent = {
30
- type: string;
31
- message: string;
32
- complete?: boolean;
33
- };
34
-
35
- type FunctionCallProps = {
36
- name?: string;
37
- arguments?: string;
38
- };
39
-
40
- const debug = debugFactory( 'ai-client:chrome-ai-suggestions' );
41
-
42
- export default class ChromeAISuggestionsEventSource extends EventTarget {
43
- fullMessage: string;
44
- fullFunctionCall: FunctionCallProps;
45
- isPromptClear: boolean;
46
- controller: AbortController;
47
-
48
- errorUnclearPromptTriggered: boolean;
49
-
50
- constructor( data: ChromeAISuggestionsEventSourceConstructorArgs ) {
51
- super();
52
- this.fullMessage = '';
53
- this.fullFunctionCall = {
54
- name: '',
55
- arguments: '',
56
- };
57
- this.isPromptClear = false;
58
-
59
- this.controller = new AbortController();
60
-
61
- this.initSource( data );
62
- }
63
-
64
- initSource( {
65
- content,
66
- promptType,
67
- options = {},
68
- }: ChromeAISuggestionsEventSourceConstructorArgs ) {
69
- debug( 'initSource', content, promptType, options );
70
- if ( promptType === PROMPT_TYPE_CHANGE_LANGUAGE ) {
71
- this.translate( content, options.targetLanguage, options.sourceLanguage );
72
- }
73
-
74
- if ( promptType === PROMPT_TYPE_SUMMARIZE ) {
75
- this.summarize( content, options.tone, options.wordCount );
76
- }
77
- }
78
-
79
- async initEventSource() {}
80
-
81
- close() {}
82
-
83
- checkForUnclearPrompt() {}
84
-
85
- processEvent( e: EventSourceMessage ) {
86
- let data: ChromeAIEvent;
87
- debug( 'processEvent', e );
88
- try {
89
- data = JSON.parse( e.data );
90
- } catch ( err ) {
91
- this.processErrorEvent( err );
92
- return;
93
- }
94
-
95
- if ( e.event === 'translation' || e.event === 'summary' ) {
96
- this.dispatchEvent( new CustomEvent( 'suggestion', { detail: data.message } ) );
97
- }
98
-
99
- if ( data.complete ) {
100
- this.dispatchEvent(
101
- new CustomEvent( 'done', { detail: { message: data.message, source: 'chromeAI' } } )
102
- );
103
- }
104
- }
105
-
106
- processErrorEvent( e ) {
107
- debug( 'processErrorEvent', e );
108
- // Dispatch a generic network error event
109
- this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: e } ) );
110
- this.dispatchEvent(
111
- new CustomEvent( ERROR_RESPONSE, {
112
- detail: getErrorData( ERROR_NETWORK ),
113
- } )
114
- );
115
- }
116
-
117
- // use the Chrome AI translator
118
- async translate( text: string, target: string, source: string = '' ) {
119
- if ( ! ( 'Translator' in self ) ) {
120
- return;
121
- }
122
-
123
- const translatorAvailability = await self.Translator.availability( {
124
- sourceLanguage: source,
125
- targetLanguage: target,
126
- } );
127
-
128
- if ( translatorAvailability === 'unavailable' ) {
129
- debug( 'awaiting translator ready' );
130
- this.processErrorEvent( {
131
- message: 'Translator is unavailable',
132
- } );
133
- return;
134
- }
135
-
136
- const translator = await self.Translator.create( {
137
- sourceLanguage: source,
138
- targetLanguage: target,
139
- } );
140
-
141
- if ( ! translator ) {
142
- this.processErrorEvent( {
143
- message: 'Translator failed to initialize',
144
- } );
145
- return;
146
- }
147
-
148
- try {
149
- const translation = await translator.translate( renderHTMLFromMarkdown( { content: text } ) );
150
-
151
- this.processEvent( {
152
- id: '',
153
- event: 'translation',
154
- data: JSON.stringify( {
155
- message: renderMarkdownFromHTML( { content: translation } ),
156
- complete: true,
157
- } ),
158
- } );
159
- } catch ( error ) {
160
- this.processErrorEvent( error );
161
- }
162
- }
163
-
164
- // Helper function to format summarizer options
165
- private getSummarizerOptions( tone?: string, wordCount?: number ) {
166
- let sharedContext = `The summary you write should contain strictly less than ${
167
- wordCount ?? 50
168
- } words. Strive for precision in word count without compromising clarity and significance`;
169
-
170
- if ( tone ) {
171
- sharedContext += `\n - Write with a ${ tone } tone.\n`;
172
- }
173
-
174
- const options = {
175
- sharedContext: sharedContext,
176
- type: 'teaser',
177
- format: 'plain-text',
178
- length: 'medium',
179
- };
180
-
181
- return options;
182
- }
183
-
184
- // use the Chrome AI summarizer
185
- async summarize( text: string, tone?: string, wordCount?: number ) {
186
- debug( 'summarize', text, tone, wordCount );
187
- if ( ! ( 'Summarizer' in self ) ) {
188
- return;
189
- }
190
-
191
- const availability = await self.Summarizer.availability();
192
-
193
- if ( availability === 'unavailable' ) {
194
- this.processErrorEvent( {
195
- data: { message: 'Summarizer is unavailable' },
196
- } );
197
- return;
198
- }
199
-
200
- const summarizerOptions = this.getSummarizerOptions( tone, wordCount );
201
-
202
- const summarizer = await self.Summarizer.create( summarizerOptions );
203
-
204
- if ( availability !== 'available' ) {
205
- debug( 'awaiting summarizer ready' );
206
- await summarizer.ready;
207
- }
208
-
209
- try {
210
- const context = `Write with a ${ tone } tone.`;
211
- debug( 'context', context );
212
- let summary = await summarizer.summarize( text, { context: context } );
213
- debug( 'summary', summary );
214
- wordCount = wordCount ?? 50;
215
-
216
- // gemini-nano has a tendency to exceed the word count, so we need to check and summarize again if necessary
217
- if ( summary.split( ' ' ).length > wordCount ) {
218
- debug( 'summary exceeds word count' );
219
- summary = await summarizer.summarize( summary, { context: context } );
220
- }
221
-
222
- this.processEvent( {
223
- id: '',
224
- event: 'summary',
225
- data: JSON.stringify( {
226
- message: summary,
227
- complete: true,
228
- } ),
229
- } );
230
- } catch ( error ) {
231
- debug( 'error', error );
232
- this.processErrorEvent( error );
233
- }
234
- }
235
- }