@automattic/jetpack-ai-client 0.6.1 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/build/api-fetch/index.d.ts +7 -0
  3. package/build/api-fetch/index.js +6 -0
  4. package/build/ask-question/index.d.ts +2 -2
  5. package/build/ask-question/index.js +1 -1
  6. package/build/audio-transcription/index.d.ts +12 -0
  7. package/build/audio-transcription/index.js +51 -0
  8. package/build/components/ai-control/index.d.ts +7 -6
  9. package/build/components/ai-control/index.js +6 -6
  10. package/build/components/ai-control/message.d.ts +10 -0
  11. package/build/components/ai-control/message.js +15 -2
  12. package/build/components/ai-status-indicator/index.d.ts +1 -1
  13. package/build/components/audio-duration-display/index.d.ts +3 -2
  14. package/build/components/audio-duration-display/index.js +3 -14
  15. package/build/components/audio-duration-display/lib/media.d.ts +13 -14
  16. package/build/components/audio-duration-display/lib/media.js +7 -32
  17. package/build/components/index.d.ts +4 -4
  18. package/build/components/index.js +4 -4
  19. package/build/data-flow/context.d.ts +9 -6
  20. package/build/data-flow/context.js +1 -1
  21. package/build/data-flow/index.d.ts +3 -3
  22. package/build/data-flow/index.js +3 -3
  23. package/build/data-flow/use-ai-context.d.ts +3 -3
  24. package/build/data-flow/use-ai-context.js +3 -3
  25. package/build/data-flow/with-ai-assistant-data.js +2 -2
  26. package/build/hooks/use-ai-suggestions/index.d.ts +4 -4
  27. package/build/hooks/use-ai-suggestions/index.js +2 -2
  28. package/build/hooks/use-audio-transcription/index.d.ts +28 -0
  29. package/build/hooks/use-audio-transcription/index.js +56 -0
  30. package/build/hooks/use-media-recording/index.d.ts +34 -12
  31. package/build/hooks/use-media-recording/index.js +116 -41
  32. package/build/hooks/use-transcription-post-processing/index.d.ts +30 -0
  33. package/build/hooks/use-transcription-post-processing/index.js +84 -0
  34. package/build/icons/index.d.ts +7 -7
  35. package/build/icons/index.js +7 -7
  36. package/build/icons/mic.js +2 -2
  37. package/build/icons/player-pause.js +1 -1
  38. package/build/index.d.ts +12 -9
  39. package/build/index.js +12 -9
  40. package/build/jwt/index.js +4 -1
  41. package/build/suggestions-event-source/index.d.ts +1 -1
  42. package/build/suggestions-event-source/index.js +3 -3
  43. package/build/types.d.ts +9 -2
  44. package/build/types.js +4 -0
  45. package/package.json +5 -5
  46. package/src/api-fetch/index.ts +13 -0
  47. package/src/ask-question/index.ts +2 -2
  48. package/src/audio-transcription/index.ts +75 -0
  49. package/src/components/ai-control/index.tsx +12 -16
  50. package/src/components/ai-control/message.tsx +34 -2
  51. package/src/components/ai-control/style.scss +6 -0
  52. package/src/components/ai-status-indicator/index.tsx +1 -1
  53. package/src/components/audio-duration-display/index.tsx +6 -17
  54. package/src/components/audio-duration-display/lib/media.ts +17 -40
  55. package/src/components/index.ts +8 -4
  56. package/src/data-flow/context.tsx +7 -8
  57. package/src/data-flow/index.ts +3 -3
  58. package/src/data-flow/use-ai-context.ts +6 -6
  59. package/src/data-flow/with-ai-assistant-data.tsx +2 -2
  60. package/src/hooks/use-ai-suggestions/index.ts +6 -6
  61. package/src/hooks/use-audio-transcription/index.ts +95 -0
  62. package/src/hooks/use-media-recording/Readme.md +2 -2
  63. package/src/hooks/use-media-recording/index.ts +179 -59
  64. package/src/hooks/use-transcription-post-processing/index.ts +135 -0
  65. package/src/icons/index.ts +7 -7
  66. package/src/icons/mic.tsx +14 -16
  67. package/src/icons/player-pause.tsx +5 -5
  68. package/src/index.ts +12 -9
  69. package/src/jwt/index.ts +4 -1
  70. package/src/suggestions-event-source/index.ts +4 -4
  71. package/src/types.ts +29 -2
package/build/types.d.ts CHANGED
@@ -13,13 +13,20 @@ export type PromptItemProps = {
13
13
  };
14
14
  export type PromptMessagesProp = Array<PromptItemProps>;
15
15
  export type PromptProp = PromptMessagesProp | string;
16
- export type { UseAiContextOptions } from './data-flow/use-ai-context';
17
- export type { RequestingErrorProps } from './hooks/use-ai-suggestions';
16
+ export type { UseAiContextOptions } from './data-flow/use-ai-context.js';
17
+ export type { RequestingErrorProps } from './hooks/use-ai-suggestions/index.js';
18
+ export type { UseAudioTranscriptionProps, UseAudioTranscriptionReturn, } from './hooks/use-audio-transcription/index.js';
19
+ export type { UseTranscriptionPostProcessingProps, UseTranscriptionPostProcessingReturn, PostProcessingAction, } from './hooks/use-transcription-post-processing/index.js';
20
+ export { TRANSCRIPTION_POST_PROCESSING_ACTION_SIMPLE_DRAFT } from './hooks/use-transcription-post-processing/index.js';
18
21
  export declare const REQUESTING_STATES: readonly ["init", "requesting", "suggesting", "done", "error"];
19
22
  export type RequestingStateProp = (typeof REQUESTING_STATES)[number];
20
23
  export declare const AI_MODEL_GPT_3_5_Turbo_16K: "gpt-3.5-turbo-16k";
21
24
  export declare const AI_MODEL_GPT_4: "gpt-4";
22
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
+ };
23
30
  interface JPConnectionInitialState {
24
31
  apiNonce: string;
25
32
  siteSuffix: string;
package/build/types.js CHANGED
@@ -5,6 +5,10 @@ export const ERROR_CONTEXT_TOO_LARGE = 'error_context_too_large';
5
5
  export const ERROR_NETWORK = 'error_network';
6
6
  export const ERROR_UNCLEAR_PROMPT = 'error_unclear_prompt';
7
7
  export const ERROR_RESPONSE = 'error_response';
8
+ /*
9
+ * Hook constants
10
+ */
11
+ export { TRANSCRIPTION_POST_PROCESSING_ACTION_SIMPLE_DRAFT } from './hooks/use-transcription-post-processing/index.js';
8
12
  /*
9
13
  * Requests types
10
14
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.6.1",
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.1",
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",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import apiFetchMod from '@wordpress/api-fetch';
5
+
6
+ // @wordpress/api-fetch (as of 6.47.0) declares itself in such a way that tsc and node see the function at apiFetchMod.default
7
+ // while some other environments (including code running inside WordPress itself) see it at apiFetch.
8
+ // See https://arethetypeswrong.github.io/?p=@wordpress/api-fetch@6.47.0
9
+ // This is a helper to simplify the usage of the api-fetch module on the ai-client package.
10
+ type ApiFetchType = typeof apiFetchMod.default;
11
+ const apiFetch: ApiFetchType = ( apiFetchMod.default ?? apiFetchMod ) as ApiFetchType;
12
+
13
+ export default apiFetch;
@@ -2,11 +2,11 @@
2
2
  * External dependencies
3
3
  */
4
4
  import debugFactory from 'debug';
5
- import SuggestionsEventSource from '../suggestions-event-source';
5
+ import SuggestionsEventSource from '../suggestions-event-source/index.js';
6
6
  /*
7
7
  * Types & constants
8
8
  */
9
- import type { AiModelTypeProp, PromptProp } from '../types';
9
+ import type { AiModelTypeProp, PromptProp } from '../types.js';
10
10
 
11
11
  export type AskQuestionOptionsArgProps = {
12
12
  /*
@@ -0,0 +1,75 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import debugFactory from 'debug';
5
+ /**
6
+ * Internal dependencies
7
+ */
8
+ import apiFetch from '../api-fetch/index.js';
9
+ import requestJwt from '../jwt/index.js';
10
+ /**
11
+ * Types
12
+ */
13
+ import { CancelablePromise } from '../types.js';
14
+
15
+ const debug = debugFactory( 'jetpack-ai-client:audio-transcription' );
16
+
17
+ /**
18
+ * The response from the audio transcription service.
19
+ */
20
+ type AudioTranscriptionResponse = {
21
+ /**
22
+ * The transcribed text.
23
+ */
24
+ text: string;
25
+ };
26
+
27
+ /**
28
+ * A function that takes an audio blob and transcribes it.
29
+ *
30
+ * @param {Blob} audio - The audio to be transcribed, from a recording or from a file.
31
+ * @param {string} feature - The feature name that is calling the transcription.
32
+ * @returns {Promise<string>} - The promise of a string containing the transcribed audio.
33
+ */
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 > {
39
+ debug( 'Transcribing audio: %o. Feature: %o', audio, feature );
40
+
41
+ // Get a token to use the transcription service
42
+ let token = '';
43
+ try {
44
+ token = ( await requestJwt() ).token;
45
+ } catch ( error ) {
46
+ debug( 'Error getting token: %o', error );
47
+ return Promise.reject( error );
48
+ }
49
+
50
+ // Build a FormData object to hold the audio file
51
+ const formData = new FormData();
52
+ formData.append( 'audio_file', audio );
53
+
54
+ try {
55
+ const headers = {
56
+ Authorization: `Bearer ${ token }`,
57
+ };
58
+
59
+ const response: AudioTranscriptionResponse = await apiFetch( {
60
+ url: `https://public-api.wordpress.com/wpcom/v2/jetpack-ai-transcription${
61
+ feature ? `?feature=${ feature }` : ''
62
+ }`,
63
+ method: 'POST',
64
+ body: formData,
65
+ headers,
66
+ } );
67
+
68
+ debug( 'Transcription response: %o', response );
69
+
70
+ return response.text;
71
+ } catch ( error ) {
72
+ debug( 'Transcription error response: %o', error );
73
+ return Promise.reject( error );
74
+ }
75
+ }
@@ -4,27 +4,23 @@
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
20
15
  */
21
16
  import './style.scss';
22
- import AiStatusIndicator from '../ai-status-indicator';
23
- import { GuidelineMessage } from './message';
17
+ import AiStatusIndicator from '../ai-status-indicator/index.js';
18
+ import { GuidelineMessage } from './message.js';
24
19
  /**
25
20
  * Types
26
21
  */
27
- import type { RequestingStateProp } from '../../types';
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
  }
@@ -6,7 +6,7 @@ import classNames from 'classnames';
6
6
  /*
7
7
  * Types
8
8
  */
9
- import type { RequestingStateProp } from '../../types';
9
+ import type { RequestingStateProp } from '../../types.js';
10
10
  export type AiStatusIndicatorIconSize = 24 | 32 | 48 | 64;
11
11
  import type React from 'react';
12
12
 
@@ -1,18 +1,15 @@
1
- /*
2
- * External dependencies
3
- */
4
- import { useState, useEffect } from '@wordpress/element';
5
1
  /*
6
2
  * Internal dependencies
7
3
  */
8
- import { formatTime, getDuration } from './lib/media';
4
+ import { formatTime } from './lib/media.js';
9
5
  /*
10
6
  * Types
11
7
  */
12
8
  import type React from 'react';
13
9
 
14
10
  type AudioDurationDisplayProps = {
15
- url: string;
11
+ duration: number;
12
+ className?: string | null;
16
13
  };
17
14
 
18
15
  /**
@@ -22,16 +19,8 @@ type AudioDurationDisplayProps = {
22
19
  * @returns {React.ReactElement} Rendered component.
23
20
  */
24
21
  export default function AudioDurationDisplay( {
25
- url,
22
+ duration,
23
+ className,
26
24
  }: AudioDurationDisplayProps ): React.ReactElement {
27
- const [ duration, setDuration ] = useState( 0 );
28
- useEffect( () => {
29
- if ( ! url ) {
30
- return;
31
- }
32
-
33
- getDuration( url ).then( setDuration );
34
- }, [ url ] );
35
-
36
- return <span>{ formatTime( duration, { addDecimalPart: false } ) }</span>;
25
+ return <span className={ className }>{ formatTime( duration, { addDecimalPart: false } ) }</span>;
37
26
  }
@@ -1,67 +1,44 @@
1
- /**
2
- * Function to get duration of audio file
3
- *
4
- * @param {string} url - The url of the audio file
5
- * @returns {Promise<number>} The duration of the audio file
6
- * @see https://stackoverflow.com/questions/21522036/html-audio-tag-duration-always-infinity
7
- */
8
- export function getDuration( url: string ): Promise< number > {
9
- return new Promise( next => {
10
- const tmpAudioInstance = new Audio( url );
11
- tmpAudioInstance.addEventListener(
12
- 'durationchange',
13
- function () {
14
- if ( this.duration === Infinity ) {
15
- return;
16
- }
17
-
18
- const duration = this.duration;
19
- tmpAudioInstance.remove(); // remove instance from memory
20
- next( duration );
21
- },
22
- false
23
- );
24
-
25
- tmpAudioInstance.load();
26
- tmpAudioInstance.currentTime = 24 * 60 * 60; // Fake big time
27
- tmpAudioInstance.volume = 0;
28
- tmpAudioInstance.play(); // This will call `durationchange` event
29
- } );
30
- }
31
-
32
1
  type FormatTimeOptions = {
33
2
  /**
34
3
  * Whether to add the decimal part to the formatted time.
35
- *
36
4
  */
37
5
  addDecimalPart?: boolean;
6
+
7
+ /**
8
+ * Whether to show the minutes part of the formatted time even when it's 0.
9
+ */
10
+ showMinutes?: boolean;
11
+
12
+ /**
13
+ * Whether to show the hours part of the formatted time even when it's 0.
14
+ */
15
+ showHours?: boolean;
38
16
  };
39
17
 
40
18
  /**
41
19
  * Formats the given time in milliseconds into a string with the format HH:MM:SS.DD,
42
20
  * adding hours and minutes only when needed.
43
21
  *
44
- * @param {number} time - The time in seconds to format.
22
+ * @param {number} time - The time in milliseconds to format.
45
23
  * @param {FormatTimeOptions} options - The arguments.
46
24
  * @returns {string} The formatted time string.
47
25
  * @example
48
- * const formattedTime1 = formatTime( 1234567 ); // Returns "20:34.56"
49
- * const formattedTime2 = formatTime( 45123 ); // Returns "45.12"
50
- * const formattedTime3 = formatTime( 64, { addDecimalPart: false } ); // Returns "01:04"
26
+ * const formattedTime1 = formatTime( 1234567, { addDecimalPart: true } ); // Returns "20:34.56"
27
+ * const formattedTime2 = formatTime( 45123 ); // Returns "00.45"
28
+ * const formattedTime3 = formatTime( 1200, { showHours: true } ); // Returns "00:00:01"
51
29
  */
52
30
  export function formatTime(
53
31
  time: number,
54
- { addDecimalPart = true }: FormatTimeOptions = {}
32
+ { addDecimalPart = false, showMinutes = true, showHours = false }: FormatTimeOptions = {}
55
33
  ): string {
56
- time = time * 1000;
57
34
  const hours = Math.floor( time / 3600000 );
58
35
  const minutes = Math.floor( time / 60000 ) % 60;
59
36
  const seconds = Math.floor( time / 1000 ) % 60;
60
37
  const deciseconds = Math.floor( time / 10 ) % 100;
61
38
 
62
39
  const parts = [
63
- hours > 0 ? hours.toString().padStart( 2, '0' ) + ':' : '',
64
- hours > 0 || minutes > 0 ? minutes.toString().padStart( 2, '0' ) + ':' : '',
40
+ hours > 0 || showHours ? hours.toString().padStart( 2, '0' ) + ':' : '',
41
+ hours > 0 || minutes > 0 || showMinutes ? minutes.toString().padStart( 2, '0' ) + ':' : '',
65
42
  seconds.toString().padStart( 2, '0' ),
66
43
  ];
67
44
 
@@ -1,4 +1,8 @@
1
- export { default as AIControl } from './ai-control';
2
- export { default as AiStatusIndicator } from './ai-status-indicator';
3
- export { default as AudioDurationDisplay } from './audio-duration-display';
4
- export { GuidelineMessage, default as FooterMessage } from './ai-control/message';
1
+ export { default as AIControl } from './ai-control/index.js';
2
+ export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
+ export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
+ export {
5
+ GuidelineMessage,
6
+ UpgradeMessage,
7
+ default as FooterMessage,
8
+ } from './ai-control/message.js';
@@ -1,16 +1,15 @@
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
  */
9
- import SuggestionsEventSource from '../suggestions-event-source';
10
- import type { AskQuestionOptionsArgProps } from '../ask-question';
11
- import type { RequestingErrorProps } from '../hooks/use-ai-suggestions';
12
- import type { PromptProp } from '../types';
13
- import type { RequestingStateProp } from '../types';
8
+ import SuggestionsEventSource from '../suggestions-event-source/index.js';
9
+ import type { AskQuestionOptionsArgProps } from '../ask-question/index.js';
10
+ import type { RequestingErrorProps } from '../hooks/use-ai-suggestions/index.js';
11
+ import type { PromptProp } from '../types.js';
12
+ import type { RequestingStateProp } from '../types.js';
14
13
 
15
14
  export type AiDataContextProps = {
16
15
  /*
@@ -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,3 +1,3 @@
1
- export { AiDataContext, AiDataContextProvider } from './context';
2
- export { default as withAiDataProvider } from './with-ai-assistant-data';
3
- export { default as useAiContext } from './use-ai-context';
1
+ export { AiDataContext, AiDataContextProvider } from './context.js';
2
+ export { default as withAiDataProvider } from './with-ai-assistant-data.js';
3
+ export { default as useAiContext } from './use-ai-context.js';
@@ -1,17 +1,17 @@
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
  */
8
- import { ERROR_RESPONSE, RequestingErrorProps } from '../types';
9
- import { AiDataContext } from '.';
8
+ import { ERROR_RESPONSE, RequestingErrorProps } from '../types.js';
9
+ import { AiDataContext } from './index.js';
10
10
  /**
11
11
  * Types & constants
12
12
  */
13
- import type { AiDataContextProps } from './context';
14
- import type { AskQuestionOptionsArgProps } from '../ask-question';
13
+ import type { AiDataContextProps } from './context.js';
14
+ import type { AskQuestionOptionsArgProps } from '../ask-question/index.js';
15
15
 
16
16
  export type UseAiContextOptions = {
17
17
  /*
@@ -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,8 +7,8 @@ import React from 'react';
7
7
  /**
8
8
  * Internal Dependencies
9
9
  */
10
- import useAiSuggestions from '../hooks/use-ai-suggestions';
11
- import { AiDataContextProvider } from '.';
10
+ import useAiSuggestions from '../hooks/use-ai-suggestions/index.js';
11
+ import { AiDataContextProvider } from './index.js';
12
12
 
13
13
  /**
14
14
  * High Order Component that provides the
@@ -7,21 +7,21 @@ import debugFactory from 'debug';
7
7
  /**
8
8
  * Internal dependencies
9
9
  */
10
- import askQuestion from '../../ask-question';
10
+ import askQuestion from '../../ask-question/index.js';
11
11
  import {
12
12
  ERROR_MODERATION,
13
13
  ERROR_NETWORK,
14
14
  ERROR_QUOTA_EXCEEDED,
15
15
  ERROR_SERVICE_UNAVAILABLE,
16
16
  ERROR_UNCLEAR_PROMPT,
17
- } from '../../types';
17
+ } from '../../types.js';
18
18
  /**
19
19
  * Types & constants
20
20
  */
21
- import type { AskQuestionOptionsArgProps } from '../../ask-question';
22
- import type SuggestionsEventSource from '../../suggestions-event-source';
23
- import type { PromptProp, SuggestionErrorCode } from '../../types';
24
- import type { RequestingStateProp } from '../../types';
21
+ import type { AskQuestionOptionsArgProps } from '../../ask-question/index.js';
22
+ import type SuggestionsEventSource from '../../suggestions-event-source/index.js';
23
+ import type { PromptProp, SuggestionErrorCode } from '../../types.js';
24
+ import type { RequestingStateProp } from '../../types.js';
25
25
 
26
26
  export type RequestingErrorProps = {
27
27
  /*
@@ -0,0 +1,95 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useCallback, useState } from '@wordpress/element';
5
+ import debugFactory from 'debug';
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import transcribeAudio from '../../audio-transcription/index.js';
10
+ /**
11
+ * Types
12
+ */
13
+ import type { CancelablePromise } from '../../types.js';
14
+
15
+ const debug = debugFactory( 'jetpack-ai-client:use-audio-transcription' );
16
+
17
+ /**
18
+ * The response from the audio transcription hook.
19
+ */
20
+ export type UseAudioTranscriptionReturn = {
21
+ transcriptionResult: string;
22
+ isTranscribingAudio: boolean;
23
+ transcriptionError: string;
24
+ transcribeAudio: ( audio: Blob ) => CancelablePromise;
25
+ };
26
+
27
+ /**
28
+ * The props for the audio transcription hook.
29
+ */
30
+ export type UseAudioTranscriptionProps = {
31
+ feature: string;
32
+ onReady?: ( transcription: string ) => void;
33
+ onError?: ( error: string ) => void;
34
+ };
35
+
36
+ /**
37
+ * A hook to handle audio transcription.
38
+ *
39
+ * @param {string} feature - The feature name that is calling the transcription.
40
+ * @returns {UseAudioTranscriptionReturn} - Object with properties to get the transcription data.
41
+ */
42
+ export default function useAudioTranscription( {
43
+ feature,
44
+ onReady,
45
+ onError,
46
+ }: UseAudioTranscriptionProps ): UseAudioTranscriptionReturn {
47
+ const [ transcriptionResult, setTranscriptionResult ] = useState< string >( '' );
48
+ const [ transcriptionError, setTranscriptionError ] = useState< string >( '' );
49
+ const [ isTranscribingAudio, setIsTranscribingAudio ] = useState( false );
50
+
51
+ const handleAudioTranscription = useCallback(
52
+ ( audio: Blob ) => {
53
+ debug( 'Transcribing audio' );
54
+
55
+ /**
56
+ * Reset the transcription result and error.
57
+ */
58
+ setTranscriptionResult( '' );
59
+ setTranscriptionError( '' );
60
+ setIsTranscribingAudio( true );
61
+
62
+ /**
63
+ * Call the audio transcription library.
64
+ */
65
+ const promise: CancelablePromise = transcribeAudio( audio, feature )
66
+ .then( transcriptionText => {
67
+ if ( promise.canceled ) {
68
+ return;
69
+ }
70
+
71
+ setTranscriptionResult( transcriptionText );
72
+ onReady?.( transcriptionText );
73
+ } )
74
+ .catch( error => {
75
+ if ( promise.canceled ) {
76
+ return;
77
+ }
78
+
79
+ setTranscriptionError( error.message );
80
+ onError?.( error.message );
81
+ } )
82
+ .finally( () => setIsTranscribingAudio( false ) );
83
+
84
+ return promise;
85
+ },
86
+ [ transcribeAudio, setTranscriptionResult, setTranscriptionError, setIsTranscribingAudio ]
87
+ );
88
+
89
+ return {
90
+ transcriptionResult,
91
+ isTranscribingAudio,
92
+ transcriptionError,
93
+ transcribeAudio: handleAudioTranscription,
94
+ };
95
+ }