@automattic/jetpack-ai-client 0.7.0 → 0.8.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,6 +5,15 @@ 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.8.0] - 2024-02-26
9
+ ### Added
10
+ - Add upgrade message for free tier [#35794]
11
+
12
+ ### Changed
13
+ - Updated package dependencies. [#35793]
14
+ - Voice to Content: Add audio analyser to media recording hook [#35877]
15
+ - Voice to Content: Make transcriptions cancelable and add onProcess callback [#35737]
16
+
8
17
  ## [0.7.0] - 2024-02-19
9
18
  ### Added
10
19
  - AI Client: add support for audio transcriptions. [#35691]
@@ -220,6 +229,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
220
229
  - Updated package dependencies. [#31659]
221
230
  - Updated package dependencies. [#31785]
222
231
 
232
+ [0.8.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.7.0...v0.8.0
223
233
  [0.7.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.6.1...v0.7.0
224
234
  [0.6.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.6.0...v0.6.1
225
235
  [0.6.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.5.1...v0.6.0
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Types
3
+ */
4
+ import { CancelablePromise } from '../types.js';
1
5
  /**
2
6
  * A function that takes an audio blob and transcribes it.
3
7
  *
@@ -5,4 +9,4 @@
5
9
  * @param {string} feature - The feature name that is calling the transcription.
6
10
  * @returns {Promise<string>} - The promise of a string containing the transcribed audio.
7
11
  */
8
- export default function transcribeAudio(audio: Blob, feature?: string): Promise<string>;
12
+ export default function transcribeAudio(audio: Blob, feature?: string): CancelablePromise<string>;
@@ -15,7 +15,9 @@ const debug = debugFactory('jetpack-ai-client:audio-transcription');
15
15
  * @param {string} feature - The feature name that is calling the transcription.
16
16
  * @returns {Promise<string>} - The promise of a string containing the transcribed audio.
17
17
  */
18
- export default async function transcribeAudio(audio, feature) {
18
+ export default async function transcribeAudio(audio, feature
19
+ // @ts-expect-error Promises are not cancelable by default
20
+ ) {
19
21
  debug('Transcribing audio: %o. Feature: %o', audio, feature);
20
22
  // Get a token to use the transcription service
21
23
  let token = '';
@@ -7,6 +7,7 @@ import './style.scss';
7
7
  * Types
8
8
  */
9
9
  import type { RequestingStateProp } from '../../types.js';
10
+ import type { ReactElement } from 'react';
10
11
  type AiControlProps = {
11
12
  disabled?: boolean;
12
13
  value: string;
@@ -17,23 +18,23 @@ type AiControlProps = {
17
18
  isTransparent?: boolean;
18
19
  state?: RequestingStateProp;
19
20
  showGuideLine?: boolean;
20
- customFooter?: React.ReactElement;
21
+ customFooter?: ReactElement;
21
22
  onChange?: (newValue: string) => void;
22
23
  onSend?: (currentValue: string) => void;
23
24
  onStop?: () => void;
24
25
  onAccept?: () => void;
25
26
  onDiscard?: () => void;
26
27
  showRemove?: boolean;
27
- bannerComponent?: React.ReactElement;
28
- errorComponent?: React.ReactElement;
28
+ bannerComponent?: ReactElement;
29
+ errorComponent?: ReactElement;
29
30
  };
30
31
  /**
31
32
  * AI Control component.
32
33
  *
33
34
  * @param {AiControlProps} props - Component props.
34
35
  * @param {React.MutableRefObject} ref - Ref to the component.
35
- * @returns {React.ReactElement} Rendered component.
36
+ * @returns {ReactElement} Rendered component.
36
37
  */
37
- export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showGuideLine, customFooter, onChange, onSend, onStop, onAccept, onDiscard, showRemove, bannerComponent, errorComponent, }: AiControlProps, ref: React.MutableRefObject<null>): React.ReactElement;
38
+ export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showGuideLine, customFooter, onChange, onSend, onStop, onAccept, onDiscard, showRemove, bannerComponent, errorComponent, }: AiControlProps, ref: React.MutableRefObject<null>): ReactElement;
38
39
  declare const _default: React.ForwardRefExoticComponent<AiControlProps & React.RefAttributes<null>>;
39
40
  export default _default;
@@ -5,10 +5,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
5
5
  import { PlainText } from '@wordpress/block-editor';
6
6
  import { Button, ButtonGroup } from '@wordpress/components';
7
7
  import { useKeyboardShortcut } from '@wordpress/compose';
8
- import { forwardRef, useImperativeHandle, useRef, useEffect, useCallback, } from '@wordpress/element';
8
+ import { useImperativeHandle, useRef, useEffect, useCallback } from '@wordpress/element';
9
9
  import { __ } from '@wordpress/i18n';
10
10
  import { Icon, closeSmall, check, arrowUp, trash, reusableBlock } from '@wordpress/icons';
11
11
  import classNames from 'classnames';
12
+ import { forwardRef } from 'react';
12
13
  import React from 'react';
13
14
  /**
14
15
  * Internal dependencies
@@ -23,10 +24,9 @@ const noop = () => { };
23
24
  *
24
25
  * @param {AiControlProps} props - Component props.
25
26
  * @param {React.MutableRefObject} ref - Ref to the component.
26
- * @returns {React.ReactElement} Rendered component.
27
+ * @returns {ReactElement} Rendered component.
27
28
  */
28
- export function AIControl({ disabled = false, value = '', placeholder = '', showAccept = false, acceptLabel = __('Accept', 'jetpack-ai-client'), showButtonLabels = true, isTransparent = false, state = 'init', showGuideLine = false, customFooter = null, onChange = noop, onSend = noop, onStop = noop, onAccept = noop, onDiscard = null, showRemove = false, bannerComponent = null, errorComponent = null, }, ref // eslint-disable-line @typescript-eslint/ban-types
29
- ) {
29
+ export function AIControl({ disabled = false, value = '', placeholder = '', showAccept = false, acceptLabel = __('Accept', 'jetpack-ai-client'), showButtonLabels = true, isTransparent = false, state = 'init', showGuideLine = false, customFooter = null, onChange = noop, onSend = noop, onStop = noop, onAccept = noop, onDiscard = null, showRemove = false, bannerComponent = null, errorComponent = null, }, ref) {
30
30
  const promptUserInputRef = useRef(null);
31
31
  const loading = state === 'requesting' || state === 'suggesting';
32
32
  const [editRequest, setEditRequest] = React.useState(false);
@@ -27,4 +27,14 @@ export default function Message({ severity, icon, children, }: MessageProps): Re
27
27
  * @returns {React.ReactElement } - Message component.
28
28
  */
29
29
  export declare function GuidelineMessage(): React.ReactElement;
30
+ /**
31
+ * React component to render a upgrade message.
32
+ *
33
+ * @param {number} requestsRemaining - Number of requests remaining.
34
+ * @returns {React.ReactElement } - Message component.
35
+ */
36
+ export declare function UpgradeMessage({ requestsRemaining, onUpgradeClick, }: {
37
+ requestsRemaining: number;
38
+ onUpgradeClick: () => void;
39
+ }): React.ReactElement;
30
40
  export {};
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * External dependencies
4
4
  */
5
- import { ExternalLink } from '@wordpress/components';
5
+ import { ExternalLink, Button } from '@wordpress/components';
6
6
  import { createInterpolateElement } from '@wordpress/element';
7
- import { __ } from '@wordpress/i18n';
7
+ import { __, sprintf } from '@wordpress/i18n';
8
8
  import { Icon, warning, info, cancelCircleFilled as error, check as success, } from '@wordpress/icons';
9
9
  import './style.scss';
10
10
  export const MESSAGE_SEVERITY_WARNING = 'warning';
@@ -42,3 +42,16 @@ export function GuidelineMessage() {
42
42
  link: _jsx(ExternalLink, { href: "https://automattic.com/ai-guidelines" }),
43
43
  }) }));
44
44
  }
45
+ /**
46
+ * React component to render a upgrade message.
47
+ *
48
+ * @param {number} requestsRemaining - Number of requests remaining.
49
+ * @returns {React.ReactElement } - Message component.
50
+ */
51
+ export function UpgradeMessage({ requestsRemaining, onUpgradeClick, }) {
52
+ return (_jsx(Message, { severity: MESSAGE_SEVERITY_INFO, children: createInterpolateElement(sprintf(
53
+ // translators: %1$d: number of requests remaining
54
+ __('You have %1$d free requests remaining. <link>Upgrade</link> and avoid interruptions', 'jetpack-ai-client'), requestsRemaining), {
55
+ link: _jsx(Button, { variant: "link", onClick: onUpgradeClick }),
56
+ }) }));
57
+ }
@@ -1,4 +1,4 @@
1
1
  export { default as AIControl } from './ai-control/index.js';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
- export { GuidelineMessage, default as FooterMessage } from './ai-control/message.js';
4
+ export { GuidelineMessage, UpgradeMessage, default as FooterMessage, } from './ai-control/message.js';
@@ -1,4 +1,4 @@
1
1
  export { default as AIControl } from './ai-control/index.js';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
- export { GuidelineMessage, default as FooterMessage } from './ai-control/message.js';
4
+ export { GuidelineMessage, UpgradeMessage, default as FooterMessage, } from './ai-control/message.js';
@@ -1,3 +1,6 @@
1
+ /**
2
+ * External dependencies
3
+ */
1
4
  import React from 'react';
2
5
  /**
3
6
  * Types & Constants
@@ -24,7 +27,7 @@ type AiDataContextProviderProps = {
24
27
  *
25
28
  * @returns {AiDataContextProps} Context.
26
29
  */
27
- export declare const AiDataContext: React.Context<AiDataContextProps>;
30
+ export declare const AiDataContext: React.Context<object | AiDataContextProps>;
28
31
  /**
29
32
  * AI Data Context Provider
30
33
  *
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * External dependencies
4
4
  */
5
- import { createContext } from '@wordpress/element';
5
+ import { createContext } from 'react';
6
6
  /**
7
7
  * AI Data Context
8
8
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { useCallback, useContext, useEffect } from '@wordpress/element';
4
+ import { useCallback, useContext, useEffect } from 'react';
5
5
  /**
6
6
  * Internal dependencies
7
7
  */
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Types
3
+ */
4
+ import type { CancelablePromise } from '../../types.js';
1
5
  /**
2
6
  * The response from the audio transcription hook.
3
7
  */
@@ -5,7 +9,7 @@ export type UseAudioTranscriptionReturn = {
5
9
  transcriptionResult: string;
6
10
  isTranscribingAudio: boolean;
7
11
  transcriptionError: string;
8
- transcribeAudio: (audio: Blob) => void;
12
+ transcribeAudio: (audio: Blob) => CancelablePromise;
9
13
  };
10
14
  /**
11
15
  * The props for the audio transcription hook.
@@ -29,16 +29,23 @@ export default function useAudioTranscription({ feature, onReady, onError, }) {
29
29
  /**
30
30
  * Call the audio transcription library.
31
31
  */
32
- transcribeAudio(audio, feature)
32
+ const promise = transcribeAudio(audio, feature)
33
33
  .then(transcriptionText => {
34
+ if (promise.canceled) {
35
+ return;
36
+ }
34
37
  setTranscriptionResult(transcriptionText);
35
38
  onReady?.(transcriptionText);
36
39
  })
37
40
  .catch(error => {
41
+ if (promise.canceled) {
42
+ return;
43
+ }
38
44
  setTranscriptionError(error.message);
39
45
  onError?.(error.message);
40
46
  })
41
47
  .finally(() => setIsTranscribingAudio(false));
48
+ return promise;
42
49
  }, [transcribeAudio, setTranscriptionResult, setTranscriptionError, setIsTranscribingAudio]);
43
50
  return {
44
51
  transcriptionResult,
@@ -1,20 +1,16 @@
1
- type RecordingStateProp = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
1
+ export type RecordingState = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
2
2
  type UseMediaRecordingProps = {
3
- onDone?: (blob: Blob, url: string) => void;
3
+ onDone?: (blob: Blob) => void;
4
4
  };
5
5
  type UseMediaRecordingReturn = {
6
6
  /**
7
7
  * The current recording state
8
8
  */
9
- state: RecordingStateProp;
9
+ state: RecordingState;
10
10
  /**
11
11
  * The recorded blob
12
12
  */
13
13
  blob: Blob | null;
14
- /**
15
- * The recorded blob url
16
- */
17
- url: string | null;
18
14
  /**
19
15
  * The error message
20
16
  */
@@ -23,10 +19,18 @@ type UseMediaRecordingReturn = {
23
19
  * The duration of the recorded audio
24
20
  */
25
21
  duration: number;
22
+ /**
23
+ * The audio analyser node
24
+ */
25
+ analyser?: AnalyserNode;
26
26
  /**
27
27
  * The error handler
28
28
  */
29
29
  onError: (err: string | Error) => void;
30
+ /**
31
+ * The processing handler
32
+ */
33
+ onProcessing: () => void;
30
34
  controls: {
31
35
  /**
32
36
  * `start` recording handler
@@ -24,6 +24,7 @@ export default function useMediaRecording({ onDone, } = {}) {
24
24
  // Store the recorded chunks
25
25
  const recordedChunks = useRef([]).current;
26
26
  const [error, setError] = useState(null);
27
+ const analyser = useRef(null);
27
28
  /**
28
29
  * Get the recorded blob.
29
30
  *
@@ -104,10 +105,14 @@ export default function useMediaRecording({ onDone, } = {}) {
104
105
  if (!navigator.mediaDevices?.getUserMedia) {
105
106
  return;
106
107
  }
108
+ const audioCtx = new AudioContext();
109
+ analyser.current = audioCtx.createAnalyser();
107
110
  const constraints = { audio: true };
108
111
  navigator.mediaDevices
109
112
  .getUserMedia(constraints)
110
113
  .then(stream => {
114
+ const source = audioCtx.createMediaStreamSource(stream);
115
+ source.connect(analyser.current);
111
116
  mediaRecordRef.current = new MediaRecorder(stream);
112
117
  mediaRecordRef.current.addEventListener('start', onStartListener);
113
118
  mediaRecordRef.current.addEventListener('stop', onStopListener);
@@ -126,6 +131,10 @@ export default function useMediaRecording({ onDone, } = {}) {
126
131
  setError(typeof err === 'string' ? err : err.message);
127
132
  setState('error');
128
133
  }, []);
134
+ // manually set the state to `processing` for the file upload case
135
+ const onProcessing = useCallback(() => {
136
+ setState('processing');
137
+ }, []);
129
138
  /**
130
139
  * `start` event listener for the media recorder instance.
131
140
  */
@@ -141,8 +150,7 @@ export default function useMediaRecording({ onDone, } = {}) {
141
150
  function onStopListener() {
142
151
  setState('processing');
143
152
  const lastBlob = getBlob();
144
- const url = URL.createObjectURL(lastBlob);
145
- onDone?.(lastBlob, url);
153
+ onDone?.(lastBlob);
146
154
  // Clear the recorded chunks
147
155
  recordedChunks.length = 0;
148
156
  }
@@ -194,10 +202,11 @@ export default function useMediaRecording({ onDone, } = {}) {
194
202
  return {
195
203
  state,
196
204
  blob,
197
- url: blob ? URL.createObjectURL(blob) : null,
198
205
  error,
199
206
  duration,
207
+ analyser: analyser.current,
200
208
  onError,
209
+ onProcessing,
201
210
  controls: {
202
211
  start,
203
212
  pause,
package/build/types.d.ts CHANGED
@@ -23,6 +23,10 @@ export type RequestingStateProp = (typeof REQUESTING_STATES)[number];
23
23
  export declare const AI_MODEL_GPT_3_5_Turbo_16K: "gpt-3.5-turbo-16k";
24
24
  export declare const AI_MODEL_GPT_4: "gpt-4";
25
25
  export type AiModelTypeProp = typeof AI_MODEL_GPT_3_5_Turbo_16K | typeof AI_MODEL_GPT_4;
26
+ export type { RecordingState } from './hooks/use-media-recording/index.js';
27
+ export type CancelablePromise<T = void> = Promise<T> & {
28
+ canceled?: boolean;
29
+ };
26
30
  interface JPConnectionInitialState {
27
31
  apiNonce: string;
28
32
  siteSuffix: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.7.0",
4
+ "version": "0.8.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": {
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "type": "module",
24
24
  "devDependencies": {
25
- "@storybook/addon-actions": "7.6.5",
26
- "@storybook/blocks": "7.6.5",
27
- "@storybook/react": "7.6.5",
25
+ "@storybook/addon-actions": "7.6.17",
26
+ "@storybook/blocks": "7.6.17",
27
+ "@storybook/react": "7.6.17",
28
28
  "jest": "^29.6.2",
29
29
  "jest-environment-jsdom": "29.7.0",
30
30
  "typescript": "5.0.4"
@@ -39,7 +39,7 @@
39
39
  "types": "./build/index.d.ts",
40
40
  "dependencies": {
41
41
  "@automattic/jetpack-base-styles": "^0.6.17",
42
- "@automattic/jetpack-connection": "^0.32.2",
42
+ "@automattic/jetpack-connection": "^0.32.3",
43
43
  "@automattic/jetpack-shared-extension-utils": "^0.14.2",
44
44
  "@microsoft/fetch-event-source": "2.0.1",
45
45
  "@types/react": "18.2.33",
@@ -7,6 +7,10 @@ import debugFactory from 'debug';
7
7
  */
8
8
  import apiFetch from '../api-fetch/index.js';
9
9
  import requestJwt from '../jwt/index.js';
10
+ /**
11
+ * Types
12
+ */
13
+ import { CancelablePromise } from '../types.js';
10
14
 
11
15
  const debug = debugFactory( 'jetpack-ai-client:audio-transcription' );
12
16
 
@@ -27,7 +31,11 @@ type AudioTranscriptionResponse = {
27
31
  * @param {string} feature - The feature name that is calling the transcription.
28
32
  * @returns {Promise<string>} - The promise of a string containing the transcribed audio.
29
33
  */
30
- export default async function transcribeAudio( audio: Blob, feature?: string ): Promise< string > {
34
+ export default async function transcribeAudio(
35
+ audio: Blob,
36
+ feature?: string
37
+ // @ts-expect-error Promises are not cancelable by default
38
+ ): CancelablePromise< string > {
31
39
  debug( 'Transcribing audio: %o. Feature: %o', audio, feature );
32
40
 
33
41
  // Get a token to use the transcription service
@@ -4,16 +4,11 @@
4
4
  import { PlainText } from '@wordpress/block-editor';
5
5
  import { Button, ButtonGroup } from '@wordpress/components';
6
6
  import { useKeyboardShortcut } from '@wordpress/compose';
7
- import {
8
- forwardRef,
9
- useImperativeHandle,
10
- useRef,
11
- useEffect,
12
- useCallback,
13
- } from '@wordpress/element';
7
+ import { useImperativeHandle, useRef, useEffect, useCallback } from '@wordpress/element';
14
8
  import { __ } from '@wordpress/i18n';
15
9
  import { Icon, closeSmall, check, arrowUp, trash, reusableBlock } from '@wordpress/icons';
16
10
  import classNames from 'classnames';
11
+ import { forwardRef } from 'react';
17
12
  import React from 'react';
18
13
  /**
19
14
  * Internal dependencies
@@ -25,6 +20,7 @@ import { GuidelineMessage } from './message.js';
25
20
  * Types
26
21
  */
27
22
  import type { RequestingStateProp } from '../../types.js';
23
+ import type { ReactElement } from 'react';
28
24
  type AiControlProps = {
29
25
  disabled?: boolean;
30
26
  value: string;
@@ -35,15 +31,15 @@ type AiControlProps = {
35
31
  isTransparent?: boolean;
36
32
  state?: RequestingStateProp;
37
33
  showGuideLine?: boolean;
38
- customFooter?: React.ReactElement;
34
+ customFooter?: ReactElement;
39
35
  onChange?: ( newValue: string ) => void;
40
36
  onSend?: ( currentValue: string ) => void;
41
37
  onStop?: () => void;
42
38
  onAccept?: () => void;
43
39
  onDiscard?: () => void;
44
40
  showRemove?: boolean;
45
- bannerComponent?: React.ReactElement;
46
- errorComponent?: React.ReactElement;
41
+ bannerComponent?: ReactElement;
42
+ errorComponent?: ReactElement;
47
43
  };
48
44
 
49
45
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -54,7 +50,7 @@ const noop = () => {};
54
50
  *
55
51
  * @param {AiControlProps} props - Component props.
56
52
  * @param {React.MutableRefObject} ref - Ref to the component.
57
- * @returns {React.ReactElement} Rendered component.
53
+ * @returns {ReactElement} Rendered component.
58
54
  */
59
55
  export function AIControl(
60
56
  {
@@ -77,8 +73,8 @@ export function AIControl(
77
73
  bannerComponent = null,
78
74
  errorComponent = null,
79
75
  }: AiControlProps,
80
- ref: React.MutableRefObject< null > // eslint-disable-line @typescript-eslint/ban-types
81
- ): React.ReactElement {
76
+ ref: React.MutableRefObject< null >
77
+ ): ReactElement {
82
78
  const promptUserInputRef = useRef( null );
83
79
  const loading = state === 'requesting' || state === 'suggesting';
84
80
  const [ editRequest, setEditRequest ] = React.useState( false );
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { ExternalLink } from '@wordpress/components';
4
+ import { ExternalLink, Button } from '@wordpress/components';
5
5
  import { createInterpolateElement } from '@wordpress/element';
6
- import { __ } from '@wordpress/i18n';
6
+ import { __, sprintf } from '@wordpress/i18n';
7
7
  import {
8
8
  Icon,
9
9
  warning,
@@ -84,3 +84,35 @@ export function GuidelineMessage(): React.ReactElement {
84
84
  </Message>
85
85
  );
86
86
  }
87
+
88
+ /**
89
+ * React component to render a upgrade message.
90
+ *
91
+ * @param {number} requestsRemaining - Number of requests remaining.
92
+ * @returns {React.ReactElement } - Message component.
93
+ */
94
+ export function UpgradeMessage( {
95
+ requestsRemaining,
96
+ onUpgradeClick,
97
+ }: {
98
+ requestsRemaining: number;
99
+ onUpgradeClick: () => void;
100
+ } ): React.ReactElement {
101
+ return (
102
+ <Message severity={ MESSAGE_SEVERITY_INFO }>
103
+ { createInterpolateElement(
104
+ sprintf(
105
+ // translators: %1$d: number of requests remaining
106
+ __(
107
+ 'You have %1$d free requests remaining. <link>Upgrade</link> and avoid interruptions',
108
+ 'jetpack-ai-client'
109
+ ),
110
+ requestsRemaining
111
+ ),
112
+ {
113
+ link: <Button variant="link" onClick={ onUpgradeClick } />,
114
+ }
115
+ ) }
116
+ </Message>
117
+ );
118
+ }
@@ -124,5 +124,11 @@
124
124
  .components-external-link {
125
125
  color: var( --jp-gray-50 );
126
126
  }
127
+
128
+ // Force padding 0 in link buttons, since default Gutenberg version in WordPress doesn't use iframe and
129
+ // Buttons receive styles from edit-post-visual-editor.
130
+ .components-button.is-link {
131
+ padding: 0;
132
+ }
127
133
  }
128
134
  }
@@ -1,4 +1,8 @@
1
1
  export { default as AIControl } from './ai-control/index.js';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
- export { GuidelineMessage, default as FooterMessage } from './ai-control/message.js';
4
+ export {
5
+ GuidelineMessage,
6
+ UpgradeMessage,
7
+ default as FooterMessage,
8
+ } from './ai-control/message.js';
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { createContext } from '@wordpress/element';
5
- import React from 'react';
4
+ import React, { createContext } from 'react';
6
5
  /**
7
6
  * Types & Constants
8
7
  */
@@ -61,7 +60,7 @@ type AiDataContextProviderProps = {
61
60
  *
62
61
  * @returns {AiDataContextProps} Context.
63
62
  */
64
- export const AiDataContext = createContext( {} as AiDataContextProps );
63
+ export const AiDataContext = createContext< AiDataContextProps | object >( {} );
65
64
 
66
65
  /**
67
66
  * AI Data Context Provider
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { useCallback, useContext, useEffect } from '@wordpress/element';
4
+ import { useCallback, useContext, useEffect } from 'react';
5
5
  /**
6
6
  * Internal dependencies
7
7
  */
@@ -48,7 +48,7 @@ export default function useAiContext( {
48
48
  onSuggestion,
49
49
  onError,
50
50
  }: UseAiContextOptions = {} ): AiDataContextProps {
51
- const context = useContext( AiDataContext );
51
+ const context = useContext( AiDataContext ) as AiDataContextProps;
52
52
  const { eventSource } = context;
53
53
 
54
54
  const done = useCallback( ( event: CustomEvent ) => onDone?.( event?.detail ), [ onDone ] );
@@ -7,6 +7,10 @@ import debugFactory from 'debug';
7
7
  * Internal dependencies
8
8
  */
9
9
  import transcribeAudio from '../../audio-transcription/index.js';
10
+ /**
11
+ * Types
12
+ */
13
+ import type { CancelablePromise } from '../../types.js';
10
14
 
11
15
  const debug = debugFactory( 'jetpack-ai-client:use-audio-transcription' );
12
16
 
@@ -17,7 +21,7 @@ export type UseAudioTranscriptionReturn = {
17
21
  transcriptionResult: string;
18
22
  isTranscribingAudio: boolean;
19
23
  transcriptionError: string;
20
- transcribeAudio: ( audio: Blob ) => void;
24
+ transcribeAudio: ( audio: Blob ) => CancelablePromise;
21
25
  };
22
26
 
23
27
  /**
@@ -58,16 +62,26 @@ export default function useAudioTranscription( {
58
62
  /**
59
63
  * Call the audio transcription library.
60
64
  */
61
- transcribeAudio( audio, feature )
65
+ const promise: CancelablePromise = transcribeAudio( audio, feature )
62
66
  .then( transcriptionText => {
67
+ if ( promise.canceled ) {
68
+ return;
69
+ }
70
+
63
71
  setTranscriptionResult( transcriptionText );
64
72
  onReady?.( transcriptionText );
65
73
  } )
66
74
  .catch( error => {
75
+ if ( promise.canceled ) {
76
+ return;
77
+ }
78
+
67
79
  setTranscriptionError( error.message );
68
80
  onError?.( error.message );
69
81
  } )
70
82
  .finally( () => setIsTranscribingAudio( false ) );
83
+
84
+ return promise;
71
85
  },
72
86
  [ transcribeAudio, setTranscriptionResult, setTranscriptionError, setIsTranscribingAudio ]
73
87
  );
@@ -5,27 +5,22 @@ import { useRef, useState, useEffect, useCallback } from '@wordpress/element';
5
5
  /*
6
6
  * Types
7
7
  */
8
- type RecordingStateProp = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
8
+ export type RecordingState = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
9
9
  type UseMediaRecordingProps = {
10
- onDone?: ( blob: Blob, url: string ) => void;
10
+ onDone?: ( blob: Blob ) => void;
11
11
  };
12
12
 
13
13
  type UseMediaRecordingReturn = {
14
14
  /**
15
15
  * The current recording state
16
16
  */
17
- state: RecordingStateProp;
17
+ state: RecordingState;
18
18
 
19
19
  /**
20
20
  * The recorded blob
21
21
  */
22
22
  blob: Blob | null;
23
23
 
24
- /**
25
- * The recorded blob url
26
- */
27
- url: string | null;
28
-
29
24
  /**
30
25
  * The error message
31
26
  */
@@ -36,11 +31,21 @@ type UseMediaRecordingReturn = {
36
31
  */
37
32
  duration: number;
38
33
 
34
+ /**
35
+ * The audio analyser node
36
+ */
37
+ analyser?: AnalyserNode;
38
+
39
39
  /**
40
40
  * The error handler
41
41
  */
42
42
  onError: ( err: string | Error ) => void;
43
43
 
44
+ /**
45
+ * The processing handler
46
+ */
47
+ onProcessing: () => void;
48
+
44
49
  controls: {
45
50
  /**
46
51
  * `start` recording handler
@@ -86,7 +91,7 @@ export default function useMediaRecording( {
86
91
  const mediaRecordRef = useRef( null );
87
92
 
88
93
  // Recording state: `inactive`, `recording`, `paused`, `processing`, `error`
89
- const [ state, setState ] = useState< RecordingStateProp >( 'inactive' );
94
+ const [ state, setState ] = useState< RecordingState >( 'inactive' );
90
95
 
91
96
  // reference to the paused state to be used in the `onDataAvailable` event listener,
92
97
  // as the `mediaRecordRef.current.state` is already `inactive` when the recorder is stopped,
@@ -104,6 +109,8 @@ export default function useMediaRecording( {
104
109
 
105
110
  const [ error, setError ] = useState< string | null >( null );
106
111
 
112
+ const analyser = useRef< AnalyserNode >( null );
113
+
107
114
  /**
108
115
  * Get the recorded blob.
109
116
  *
@@ -201,13 +208,18 @@ export default function useMediaRecording( {
201
208
  return;
202
209
  }
203
210
 
211
+ const audioCtx = new AudioContext();
212
+ analyser.current = audioCtx.createAnalyser();
213
+
204
214
  const constraints = { audio: true };
205
215
 
206
216
  navigator.mediaDevices
207
217
  .getUserMedia( constraints )
208
218
  .then( stream => {
209
- mediaRecordRef.current = new MediaRecorder( stream );
219
+ const source = audioCtx.createMediaStreamSource( stream );
220
+ source.connect( analyser.current );
210
221
 
222
+ mediaRecordRef.current = new MediaRecorder( stream );
211
223
  mediaRecordRef.current.addEventListener( 'start', onStartListener );
212
224
  mediaRecordRef.current.addEventListener( 'stop', onStopListener );
213
225
  mediaRecordRef.current.addEventListener( 'pause', onPauseListener );
@@ -227,6 +239,11 @@ export default function useMediaRecording( {
227
239
  setState( 'error' );
228
240
  }, [] );
229
241
 
242
+ // manually set the state to `processing` for the file upload case
243
+ const onProcessing = useCallback( () => {
244
+ setState( 'processing' );
245
+ }, [] );
246
+
230
247
  /**
231
248
  * `start` event listener for the media recorder instance.
232
249
  */
@@ -243,8 +260,7 @@ export default function useMediaRecording( {
243
260
  function onStopListener(): void {
244
261
  setState( 'processing' );
245
262
  const lastBlob = getBlob();
246
- const url = URL.createObjectURL( lastBlob );
247
- onDone?.( lastBlob, url );
263
+ onDone?.( lastBlob );
248
264
 
249
265
  // Clear the recorded chunks
250
266
  recordedChunks.length = 0;
@@ -306,10 +322,11 @@ export default function useMediaRecording( {
306
322
  return {
307
323
  state,
308
324
  blob,
309
- url: blob ? URL.createObjectURL( blob ) : null,
310
325
  error,
311
326
  duration,
327
+ analyser: analyser.current,
312
328
  onError,
329
+ onProcessing,
313
330
 
314
331
  controls: {
315
332
  start,
package/src/types.ts CHANGED
@@ -78,6 +78,16 @@ export const AI_MODEL_GPT_4 = 'gpt-4' as const;
78
78
 
79
79
  export type AiModelTypeProp = typeof AI_MODEL_GPT_3_5_Turbo_16K | typeof AI_MODEL_GPT_4;
80
80
 
81
+ /*
82
+ * Media recording types
83
+ */
84
+ export type { RecordingState } from './hooks/use-media-recording/index.js';
85
+
86
+ /*
87
+ * Utility types
88
+ */
89
+ export type CancelablePromise< T = void > = Promise< T > & { canceled?: boolean };
90
+
81
91
  // Connection initial state
82
92
  // @todo: it should be provided by the connection package
83
93
  interface JPConnectionInitialState {