@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 +10 -0
- package/build/audio-transcription/index.d.ts +5 -1
- package/build/audio-transcription/index.js +3 -1
- package/build/components/ai-control/index.d.ts +6 -5
- package/build/components/ai-control/index.js +4 -4
- package/build/components/ai-control/message.d.ts +10 -0
- package/build/components/ai-control/message.js +15 -2
- package/build/components/index.d.ts +1 -1
- package/build/components/index.js +1 -1
- package/build/data-flow/context.d.ts +4 -1
- package/build/data-flow/context.js +1 -1
- package/build/data-flow/use-ai-context.js +1 -1
- package/build/hooks/use-audio-transcription/index.d.ts +5 -1
- package/build/hooks/use-audio-transcription/index.js +8 -1
- package/build/hooks/use-media-recording/index.d.ts +11 -7
- package/build/hooks/use-media-recording/index.js +12 -3
- package/build/types.d.ts +4 -0
- package/package.json +5 -5
- package/src/audio-transcription/index.ts +9 -1
- package/src/components/ai-control/index.tsx +9 -13
- package/src/components/ai-control/message.tsx +34 -2
- package/src/components/ai-control/style.scss +6 -0
- package/src/components/index.ts +5 -1
- package/src/data-flow/context.tsx +2 -3
- package/src/data-flow/use-ai-context.ts +2 -2
- package/src/hooks/use-audio-transcription/index.ts +16 -2
- package/src/hooks/use-media-recording/index.ts +30 -13
- package/src/types.ts +10 -0
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):
|
|
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?:
|
|
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?:
|
|
28
|
-
errorComponent?:
|
|
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 {
|
|
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>):
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
*
|
|
@@ -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) =>
|
|
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
|
|
1
|
+
export type RecordingState = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
|
|
2
2
|
type UseMediaRecordingProps = {
|
|
3
|
-
onDone?: (blob: Blob
|
|
3
|
+
onDone?: (blob: Blob) => void;
|
|
4
4
|
};
|
|
5
5
|
type UseMediaRecordingReturn = {
|
|
6
6
|
/**
|
|
7
7
|
* The current recording state
|
|
8
8
|
*/
|
|
9
|
-
state:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
26
|
-
"@storybook/blocks": "7.6.
|
|
27
|
-
"@storybook/react": "7.6.
|
|
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.
|
|
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(
|
|
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?:
|
|
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?:
|
|
46
|
-
errorComponent?:
|
|
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 {
|
|
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 >
|
|
81
|
-
):
|
|
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
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -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 {
|
|
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 '
|
|
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( {}
|
|
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 '
|
|
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 ) =>
|
|
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
|
|
8
|
+
export type RecordingState = 'inactive' | 'recording' | 'paused' | 'processing' | 'error';
|
|
9
9
|
type UseMediaRecordingProps = {
|
|
10
|
-
onDone?: ( blob: Blob
|
|
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:
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|