@automattic/jetpack-ai-client 0.14.6 → 0.15.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 (128) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/build/ask-question/sync.d.ts +2 -8
  3. package/build/ask-question/sync.js +20 -19
  4. package/build/hooks/use-image-generator/index.js +1 -1
  5. package/build/hooks/use-save-to-media-library/index.d.ts +12 -0
  6. package/build/hooks/use-save-to-media-library/index.js +74 -0
  7. package/build/index.d.ts +2 -0
  8. package/build/index.js +5 -0
  9. package/build/libs/index.d.ts +1 -1
  10. package/build/libs/index.js +1 -1
  11. package/build/libs/markdown/index.d.ts +2 -2
  12. package/build/libs/markdown/index.js +2 -2
  13. package/build/libs/markdown/markdown-to-html.d.ts +8 -1
  14. package/build/libs/markdown/markdown-to-html.js +10 -1
  15. package/build/logo-generator/assets/icons/ai.d.ts +6 -0
  16. package/build/logo-generator/assets/icons/ai.js +8 -0
  17. package/build/logo-generator/assets/icons/check.d.ts +6 -0
  18. package/build/logo-generator/assets/icons/check.js +8 -0
  19. package/build/logo-generator/assets/icons/logo.d.ts +6 -0
  20. package/build/logo-generator/assets/icons/logo.js +8 -0
  21. package/build/logo-generator/assets/icons/media.d.ts +6 -0
  22. package/build/logo-generator/assets/icons/media.js +8 -0
  23. package/build/logo-generator/components/feature-fetch-failure-screen.d.ts +8 -0
  24. package/build/logo-generator/components/feature-fetch-failure-screen.js +10 -0
  25. package/build/logo-generator/components/first-load-screen.d.ts +5 -0
  26. package/build/logo-generator/components/first-load-screen.js +16 -0
  27. package/build/logo-generator/components/generator-modal.d.ts +7 -0
  28. package/build/logo-generator/components/generator-modal.js +184 -0
  29. package/build/logo-generator/components/history-carousel.d.ts +6 -0
  30. package/build/logo-generator/components/history-carousel.js +36 -0
  31. package/build/logo-generator/components/image-loader.d.ts +7 -0
  32. package/build/logo-generator/components/image-loader.js +12 -0
  33. package/build/logo-generator/components/logo-presenter.d.ts +4 -0
  34. package/build/logo-generator/components/logo-presenter.js +106 -0
  35. package/build/logo-generator/components/prompt.d.ts +5 -0
  36. package/build/logo-generator/components/prompt.js +96 -0
  37. package/build/logo-generator/components/upgrade-nudge.d.ts +2 -0
  38. package/build/logo-generator/components/upgrade-nudge.js +30 -0
  39. package/build/logo-generator/components/upgrade-screen.d.ts +9 -0
  40. package/build/logo-generator/components/upgrade-screen.js +24 -0
  41. package/build/logo-generator/components/visit-site-banner.d.ts +10 -0
  42. package/build/logo-generator/components/visit-site-banner.js +16 -0
  43. package/build/logo-generator/constants.d.ts +16 -0
  44. package/build/logo-generator/constants.js +19 -0
  45. package/build/logo-generator/hooks/use-checkout.d.ts +4 -0
  46. package/build/logo-generator/hooks/use-checkout.js +26 -0
  47. package/build/logo-generator/hooks/use-logo-generator.d.ts +46 -0
  48. package/build/logo-generator/hooks/use-logo-generator.js +286 -0
  49. package/build/logo-generator/hooks/use-request-errors.d.ts +16 -0
  50. package/build/logo-generator/hooks/use-request-errors.js +46 -0
  51. package/build/logo-generator/index.d.ts +1 -0
  52. package/build/logo-generator/index.js +1 -0
  53. package/build/logo-generator/lib/logo-storage.d.ts +58 -0
  54. package/build/logo-generator/lib/logo-storage.js +123 -0
  55. package/build/logo-generator/lib/media-exists.d.ts +12 -0
  56. package/build/logo-generator/lib/media-exists.js +33 -0
  57. package/build/logo-generator/lib/set-site-logo.d.ts +13 -0
  58. package/build/logo-generator/lib/set-site-logo.js +26 -0
  59. package/build/logo-generator/lib/wpcom-limited-request.d.ts +7 -0
  60. package/build/logo-generator/lib/wpcom-limited-request.js +33 -0
  61. package/build/logo-generator/store/actions.d.ts +105 -0
  62. package/build/logo-generator/store/actions.js +193 -0
  63. package/build/logo-generator/store/constants.d.ts +44 -0
  64. package/build/logo-generator/store/constants.js +44 -0
  65. package/build/logo-generator/store/index.d.ts +1 -0
  66. package/build/logo-generator/store/index.js +19 -0
  67. package/build/logo-generator/store/initial-state.d.ts +3 -0
  68. package/build/logo-generator/store/initial-state.js +40 -0
  69. package/build/logo-generator/store/reducer.d.ts +347 -0
  70. package/build/logo-generator/store/reducer.js +293 -0
  71. package/build/logo-generator/store/selectors.d.ts +119 -0
  72. package/build/logo-generator/store/selectors.js +173 -0
  73. package/build/logo-generator/store/types.d.ts +164 -0
  74. package/build/logo-generator/store/types.js +1 -0
  75. package/build/logo-generator/types.d.ts +82 -0
  76. package/build/logo-generator/types.js +1 -0
  77. package/build/types.d.ts +6 -0
  78. package/package.json +5 -3
  79. package/src/ask-question/sync.ts +22 -27
  80. package/src/hooks/use-image-generator/index.ts +1 -1
  81. package/src/hooks/use-save-to-media-library/index.ts +95 -0
  82. package/src/index.ts +6 -0
  83. package/src/libs/index.ts +1 -0
  84. package/src/libs/markdown/index.ts +2 -2
  85. package/src/libs/markdown/markdown-to-html.ts +20 -3
  86. package/src/logo-generator/assets/icons/ai.tsx +21 -0
  87. package/src/logo-generator/assets/icons/check.tsx +23 -0
  88. package/src/logo-generator/assets/icons/icons.scss +5 -0
  89. package/src/logo-generator/assets/icons/logo.tsx +23 -0
  90. package/src/logo-generator/assets/icons/media.tsx +24 -0
  91. package/src/logo-generator/assets/images/jetpack-logo.svg +4 -0
  92. package/src/logo-generator/assets/images/loader.gif +0 -0
  93. package/src/logo-generator/assets/index.d.ts +3 -0
  94. package/src/logo-generator/components/feature-fetch-failure-screen.tsx +35 -0
  95. package/src/logo-generator/components/first-load-screen.scss +12 -0
  96. package/src/logo-generator/components/first-load-screen.tsx +32 -0
  97. package/src/logo-generator/components/generator-modal.scss +92 -0
  98. package/src/logo-generator/components/generator-modal.tsx +291 -0
  99. package/src/logo-generator/components/history-carousel.scss +36 -0
  100. package/src/logo-generator/components/history-carousel.tsx +57 -0
  101. package/src/logo-generator/components/image-loader.tsx +22 -0
  102. package/src/logo-generator/components/logo-presenter.scss +116 -0
  103. package/src/logo-generator/components/logo-presenter.tsx +234 -0
  104. package/src/logo-generator/components/prompt.scss +102 -0
  105. package/src/logo-generator/components/prompt.tsx +211 -0
  106. package/src/logo-generator/components/upgrade-nudge.scss +43 -0
  107. package/src/logo-generator/components/upgrade-nudge.tsx +58 -0
  108. package/src/logo-generator/components/upgrade-screen.tsx +67 -0
  109. package/src/logo-generator/components/visit-site-banner.scss +29 -0
  110. package/src/logo-generator/components/visit-site-banner.tsx +50 -0
  111. package/src/logo-generator/constants.ts +22 -0
  112. package/src/logo-generator/hooks/use-checkout.ts +37 -0
  113. package/src/logo-generator/hooks/use-logo-generator.ts +389 -0
  114. package/src/logo-generator/hooks/use-request-errors.ts +70 -0
  115. package/src/logo-generator/index.ts +1 -0
  116. package/src/logo-generator/lib/logo-storage.ts +166 -0
  117. package/src/logo-generator/lib/media-exists.ts +42 -0
  118. package/src/logo-generator/lib/set-site-logo.ts +32 -0
  119. package/src/logo-generator/lib/wpcom-limited-request.ts +41 -0
  120. package/src/logo-generator/store/actions.ts +251 -0
  121. package/src/logo-generator/store/constants.ts +49 -0
  122. package/src/logo-generator/store/index.ts +25 -0
  123. package/src/logo-generator/store/initial-state.ts +43 -0
  124. package/src/logo-generator/store/reducer.ts +387 -0
  125. package/src/logo-generator/store/selectors.ts +201 -0
  126. package/src/logo-generator/store/types.ts +207 -0
  127. package/src/logo-generator/types.ts +97 -0
  128. package/src/types.ts +8 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
5
+ import { Button, Icon } from '@wordpress/components';
6
+ import { useDispatch } from '@wordpress/data';
7
+ import { __ } from '@wordpress/i18n';
8
+ import debugFactory from 'debug';
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import CheckIcon from '../assets/icons/check.js';
13
+ import LogoIcon from '../assets/icons/logo.js';
14
+ import MediaIcon from '../assets/icons/media.js';
15
+ import { EVENT_SAVE, EVENT_USE } from '../constants.js';
16
+ import useLogoGenerator from '../hooks/use-logo-generator.js';
17
+ import useRequestErrors from '../hooks/use-request-errors.js';
18
+ import { updateLogo } from '../lib/logo-storage.js';
19
+ import { STORE_NAME } from '../store/index.js';
20
+ import { ImageLoader } from './image-loader.js';
21
+ import './logo-presenter.scss';
22
+ /**
23
+ * Types
24
+ */
25
+ import type { Logo } from '../store/types.js';
26
+ import type { LogoPresenterProps } from '../types.js';
27
+ import type React from 'react';
28
+
29
+ const debug = debugFactory( 'jetpack-ai-calypso:logo-presenter' );
30
+
31
+ const SaveInLibraryButton: React.FC< { siteId: string } > = ( { siteId } ) => {
32
+ const { tracks } = useAnalytics();
33
+ const { recordEvent: recordTracksEvent } = tracks;
34
+ const {
35
+ saveLogo,
36
+ selectedLogo,
37
+ isSavingLogoToLibrary: saving,
38
+ logos,
39
+ selectedLogoIndex,
40
+ context,
41
+ } = useLogoGenerator();
42
+ const saved = !! selectedLogo?.mediaId;
43
+
44
+ const { loadLogoHistory } = useDispatch( STORE_NAME );
45
+
46
+ const handleClick = async () => {
47
+ if ( ! saved && ! saving ) {
48
+ recordTracksEvent( EVENT_SAVE, {
49
+ context,
50
+ logos_count: logos.length,
51
+ selected_logo: selectedLogoIndex ? selectedLogoIndex + 1 : 0,
52
+ } );
53
+
54
+ try {
55
+ const savedLogo = await saveLogo( selectedLogo );
56
+
57
+ // Update localStorage
58
+ updateLogo( {
59
+ siteId,
60
+ url: selectedLogo.url,
61
+ newUrl: savedLogo.mediaURL,
62
+ mediaId: savedLogo.mediaId,
63
+ } );
64
+
65
+ // Update state
66
+ loadLogoHistory( siteId );
67
+ } catch ( error ) {
68
+ debug( 'Error saving logo', error );
69
+ }
70
+ }
71
+ };
72
+
73
+ const savingLabel = __( 'Saving…', 'jetpack-ai-client' );
74
+ const savedLabel = __( 'Saved', 'jetpack-ai-client' );
75
+
76
+ return ! saving && ! saved ? (
77
+ <Button className="jetpack-ai-logo-generator-modal-presenter__action" onClick={ handleClick }>
78
+ <Icon icon={ <MediaIcon /> } />
79
+ <span className="action-text">{ __( 'Save in Library', 'jetpack-ai-client' ) }</span>
80
+ </Button>
81
+ ) : (
82
+ <button className="jetpack-ai-logo-generator-modal-presenter__action">
83
+ <Icon icon={ saving ? <MediaIcon /> : <CheckIcon /> } />
84
+ <span className="action-text">{ saving ? savingLabel : savedLabel }</span>
85
+ </button>
86
+ );
87
+ };
88
+
89
+ const UseOnSiteButton: React.FC< { onApplyLogo: () => void } > = ( { onApplyLogo } ) => {
90
+ const { tracks } = useAnalytics();
91
+ const { recordEvent: recordTracksEvent } = tracks;
92
+ const {
93
+ applyLogo,
94
+ isSavingLogoToLibrary,
95
+ isApplyingLogo,
96
+ selectedLogo,
97
+ logos,
98
+ selectedLogoIndex,
99
+ context,
100
+ } = useLogoGenerator();
101
+
102
+ const handleClick = async () => {
103
+ if ( ! isApplyingLogo && ! isSavingLogoToLibrary ) {
104
+ recordTracksEvent( EVENT_USE, {
105
+ context,
106
+ logos_count: logos.length,
107
+ selected_logo: selectedLogoIndex != null ? selectedLogoIndex + 1 : 0,
108
+ } );
109
+
110
+ try {
111
+ await applyLogo();
112
+ onApplyLogo();
113
+ } catch ( error ) {
114
+ debug( 'Error applying logo', error );
115
+ }
116
+ }
117
+ };
118
+
119
+ return isApplyingLogo && ! isSavingLogoToLibrary ? (
120
+ <button className="jetpack-ai-logo-generator-modal-presenter__action">
121
+ <Icon icon={ <LogoIcon /> } />
122
+ <span className="action-text">{ __( 'Applying logo…', 'jetpack-ai-client' ) }</span>
123
+ </button>
124
+ ) : (
125
+ <Button
126
+ className="jetpack-ai-logo-generator-modal-presenter__action"
127
+ onClick={ handleClick }
128
+ disabled={ isSavingLogoToLibrary || ! selectedLogo?.mediaId }
129
+ >
130
+ <Icon icon={ <LogoIcon /> } />
131
+ <span className="action-text">{ __( 'Use on Site', 'jetpack-ai-client' ) }</span>
132
+ </Button>
133
+ );
134
+ };
135
+
136
+ const LogoLoading: React.FC = () => {
137
+ return (
138
+ <>
139
+ <ImageLoader className="jetpack-ai-logo-generator-modal-presenter__logo" />
140
+ <span className="jetpack-ai-logo-generator-modal-presenter__loading-text">
141
+ { __( 'Generating new logo…', 'jetpack-ai-client' ) }
142
+ </span>
143
+ </>
144
+ );
145
+ };
146
+
147
+ const LogoReady: React.FC< { siteId: string; logo: Logo; onApplyLogo: () => void } > = ( {
148
+ siteId,
149
+ logo,
150
+ onApplyLogo,
151
+ } ) => {
152
+ return (
153
+ <>
154
+ <img
155
+ src={ logo.url }
156
+ alt={ logo.description }
157
+ className="jetpack-ai-logo-generator-modal-presenter__logo"
158
+ />
159
+ <div className="jetpack-ai-logo-generator-modal-presenter__action-wrapper">
160
+ <span className="jetpack-ai-logo-generator-modal-presenter__description">
161
+ { logo.description }
162
+ </span>
163
+ <div className="jetpack-ai-logo-generator-modal-presenter__actions">
164
+ <SaveInLibraryButton siteId={ siteId } />
165
+ <UseOnSiteButton onApplyLogo={ onApplyLogo } />
166
+ </div>
167
+ </div>
168
+ </>
169
+ );
170
+ };
171
+
172
+ const LogoUpdated: React.FC< { logo: Logo } > = ( { logo } ) => {
173
+ return (
174
+ <>
175
+ <img
176
+ src={ logo.url }
177
+ alt={ logo.description }
178
+ className="jetpack-ai-logo-generator-modal-presenter__logo"
179
+ />
180
+ <div className="jetpack-ai-logo-generator-modal-presenter__success-wrapper">
181
+ <Icon icon={ <CheckIcon /> } />
182
+ <span>{ __( 'Your logo has been successfully updated!', 'jetpack-ai-client' ) }</span>
183
+ </div>
184
+ </>
185
+ );
186
+ };
187
+
188
+ export const LogoPresenter: React.FC< LogoPresenterProps > = ( {
189
+ logo = null,
190
+ loading = false,
191
+ onApplyLogo,
192
+ logoAccepted = false,
193
+ siteId,
194
+ } ) => {
195
+ const { isRequestingImage } = useLogoGenerator();
196
+ const { saveToLibraryError, logoUpdateError } = useRequestErrors();
197
+
198
+ if ( ! logo ) {
199
+ return null;
200
+ }
201
+
202
+ let logoContent: React.ReactNode;
203
+
204
+ if ( loading || isRequestingImage ) {
205
+ logoContent = <LogoLoading />;
206
+ } else if ( logoAccepted ) {
207
+ logoContent = <LogoUpdated logo={ logo } />;
208
+ } else {
209
+ logoContent = (
210
+ <LogoReady siteId={ String( siteId ) } logo={ logo } onApplyLogo={ onApplyLogo } />
211
+ );
212
+ }
213
+
214
+ return (
215
+ <div className="jetpack-ai-logo-generator-modal-presenter__wrapper">
216
+ <div className="jetpack-ai-logo-generator-modal-presenter">
217
+ <div className="jetpack-ai-logo-generator-modal-presenter__content">{ logoContent }</div>
218
+ { ! logoAccepted && (
219
+ <div className="jetpack-ai-logo-generator-modal-presenter__rectangle" />
220
+ ) }
221
+ </div>
222
+ { saveToLibraryError && (
223
+ <div className="jetpack-ai-logo-generator__prompt-error">
224
+ { __( 'Error saving the logo to your library. Please try again.', 'jetpack-ai-client' ) }
225
+ </div>
226
+ ) }
227
+ { logoUpdateError && (
228
+ <div className="jetpack-ai-logo-generator__prompt-error">
229
+ { __( 'Error applying the logo to your site. Please try again.', 'jetpack-ai-client' ) }
230
+ </div>
231
+ ) }
232
+ </div>
233
+ );
234
+ };
@@ -0,0 +1,102 @@
1
+ @import '@automattic/jetpack-base-styles/root-variables';
2
+
3
+ .jetpack-ai-logo-generator__prompt {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 8px;
7
+ font-size: var(--font-body-small);
8
+ }
9
+
10
+ .jetpack-ai-logo-generator__prompt-header {
11
+ display: flex;
12
+ justify-content: space-between;
13
+ align-items: flex-start;
14
+ align-self: stretch;
15
+
16
+ .jetpack-ai-logo-generator__prompt-label {
17
+ font-weight: 500;
18
+ }
19
+
20
+ .jetpack-ai-logo-generator__prompt-actions {
21
+ display: flex;
22
+ font-size: var(--font-body-extra-small);
23
+ line-height: 20px;
24
+
25
+ .jetpack-ai-logo-generator-icon {
26
+ margin-right: 4px;
27
+ }
28
+ }
29
+ }
30
+
31
+ .jetpack-ai-logo-generator__prompt-query {
32
+ display: flex;
33
+ padding: 8px 8px 8px var(--grid-unit-15, 16px);
34
+ justify-content: space-between;
35
+ align-items: flex-end;
36
+ align-self: stretch;
37
+ border-radius: calc(4px * 2);
38
+ border: 1px solid var(--studio-gray-10, #ccc);
39
+ background: var(--studio-white, #fff);
40
+ gap: 8px;
41
+
42
+ @media (min-width: 700px) {
43
+ gap: 48px;
44
+ }
45
+
46
+ .prompt-query__input {
47
+ border: 0;
48
+ resize: none;
49
+ flex-grow: 1;
50
+ padding: 6px 0;
51
+ vertical-align: baseline;
52
+ color: var(--studio-gray-100);
53
+ line-height: 1.6;
54
+ word-break: break-word;
55
+
56
+ &:focus,
57
+ &:active {
58
+ outline: 0;
59
+ }
60
+
61
+ &[contentEditable="false"] {
62
+ color: var(--studio-gray-50, #646970);
63
+ }
64
+
65
+ &[data-placeholder]:empty::before {
66
+ content: attr(data-placeholder);
67
+ color: var(--studio-gray-50, #646970);
68
+ }
69
+
70
+ &[data-placeholder]:empty:focus::before {
71
+ content: "";
72
+ }
73
+ }
74
+ }
75
+
76
+ .jetpack-ai-logo-generator__prompt-footer {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 8px;
80
+ }
81
+
82
+ .jetpack-ai-logo-generator__prompt-requests {
83
+ color: var(--studio-gray-50, #646970);
84
+ font-size: var(--font-body-extra-small);
85
+ line-height: 21px;
86
+ display: flex;
87
+
88
+ & .prompt-footer__icon {
89
+ height: 20px;
90
+ width: 20px;
91
+
92
+ path {
93
+ fill: var(--studio-gray-20, #a7aaad);
94
+ }
95
+ }
96
+ }
97
+
98
+ .jetpack-ai-logo-generator__prompt-error {
99
+ color: var(--studio-red-50, #d63638);
100
+ font-size: var(--font-body-extra-small);
101
+ line-height: 21px;
102
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
5
+ import { Button, Tooltip } from '@wordpress/components';
6
+ import { __, sprintf } from '@wordpress/i18n';
7
+ import { Icon, info } from '@wordpress/icons';
8
+ import debugFactory from 'debug';
9
+ import { useCallback, useEffect, useState, useRef } from 'react';
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import AiIcon from '../assets/icons/ai.js';
14
+ import {
15
+ EVENT_GENERATE,
16
+ MINIMUM_PROMPT_LENGTH,
17
+ EVENT_UPGRADE,
18
+ EVENT_PLACEMENT_INPUT_FOOTER,
19
+ } from '../constants.js';
20
+ import { useCheckout } from '../hooks/use-checkout.js';
21
+ import useLogoGenerator from '../hooks/use-logo-generator.js';
22
+ import useRequestErrors from '../hooks/use-request-errors.js';
23
+ import { UpgradeNudge } from './upgrade-nudge.js';
24
+ import './prompt.scss';
25
+
26
+ const debug = debugFactory( 'jetpack-ai-calypso:prompt-box' );
27
+
28
+ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt = '' } ) => {
29
+ const { tracks } = useAnalytics();
30
+ const { recordEvent: recordTracksEvent } = tracks;
31
+ const [ prompt, setPrompt ] = useState< string >( initialPrompt );
32
+ const [ requestsRemaining, setRequestsRemaining ] = useState( 0 );
33
+ const { enhancePromptFetchError, logoFetchError } = useRequestErrors();
34
+ const { nextTierCheckoutURL: checkoutUrl, hasNextTier } = useCheckout();
35
+ const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH;
36
+
37
+ const {
38
+ generateLogo,
39
+ enhancePrompt,
40
+ setIsEnhancingPrompt,
41
+ isBusy,
42
+ isEnhancingPrompt,
43
+ site,
44
+ getAiAssistantFeature,
45
+ requireUpgrade,
46
+ context,
47
+ } = useLogoGenerator();
48
+
49
+ const enhancingLabel = __( 'Enhancing…', 'jetpack-ai-client' );
50
+ const enhanceLabel = __( 'Enhance prompt', 'jetpack-ai-client' );
51
+ const enhanceButtonLabel = isEnhancingPrompt ? enhancingLabel : enhanceLabel;
52
+
53
+ const inputRef = useRef< HTMLDivElement | null >( null );
54
+
55
+ const onEnhance = useCallback( async () => {
56
+ debug( 'Enhancing prompt', prompt );
57
+ setIsEnhancingPrompt( true );
58
+ recordTracksEvent( EVENT_GENERATE, { context, tool: 'enhance-prompt' } );
59
+
60
+ try {
61
+ const enhancedPrompt = await enhancePrompt( { prompt } );
62
+ setPrompt( enhancedPrompt );
63
+ setIsEnhancingPrompt( false );
64
+ } catch ( error ) {
65
+ debug( 'Error enhancing prompt', error );
66
+ setIsEnhancingPrompt( false );
67
+ }
68
+ }, [ context, enhancePrompt, prompt, setIsEnhancingPrompt ] );
69
+
70
+ const featureData = getAiAssistantFeature( String( site?.id || '' ) );
71
+
72
+ const currentLimit = featureData?.currentTier?.value || 0;
73
+ const currentUsage = featureData?.usagePeriod?.requestsCount || 0;
74
+ const isUnlimited = currentLimit === 1;
75
+
76
+ useEffect( () => {
77
+ if ( currentLimit - currentUsage <= 0 ) {
78
+ setRequestsRemaining( 0 );
79
+ } else {
80
+ setRequestsRemaining( currentLimit - currentUsage );
81
+ }
82
+ }, [ currentLimit, currentUsage ] );
83
+
84
+ useEffect( () => {
85
+ // Update prompt text node after enhancement
86
+ if ( inputRef.current && inputRef.current.textContent !== prompt ) {
87
+ inputRef.current.textContent = prompt;
88
+ }
89
+ }, [ prompt ] );
90
+
91
+ const onGenerate = useCallback( async () => {
92
+ recordTracksEvent( EVENT_GENERATE, { context, tool: 'image' } );
93
+ generateLogo( { prompt } );
94
+ }, [ context, generateLogo, prompt ] );
95
+
96
+ const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
97
+ setPrompt( event.target.textContent || '' );
98
+ };
99
+
100
+ const onPromptPaste = ( event: React.ClipboardEvent< HTMLInputElement > ) => {
101
+ event.preventDefault();
102
+
103
+ // Paste plain text only
104
+ const text = event.clipboardData.getData( 'text/plain' );
105
+
106
+ const selection = window.getSelection();
107
+ if ( ! selection || ! selection.rangeCount ) {
108
+ return;
109
+ }
110
+ selection.deleteFromDocument();
111
+ const range = selection.getRangeAt( 0 );
112
+ range.insertNode( document.createTextNode( text ) );
113
+ selection.collapseToEnd();
114
+
115
+ setPrompt( inputRef.current?.textContent || '' );
116
+ };
117
+
118
+ const onUpgradeClick = () => {
119
+ recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER } );
120
+ };
121
+
122
+ return (
123
+ <div className="jetpack-ai-logo-generator__prompt">
124
+ <div className="jetpack-ai-logo-generator__prompt-header">
125
+ <div className="jetpack-ai-logo-generator__prompt-label">
126
+ { __( 'Describe your site:', 'jetpack-ai-client' ) }
127
+ </div>
128
+ <div className="jetpack-ai-logo-generator__prompt-actions">
129
+ <Button
130
+ variant="link"
131
+ disabled={ isBusy || requireUpgrade || ! hasPrompt }
132
+ onClick={ onEnhance }
133
+ >
134
+ <AiIcon />
135
+ <span>{ enhanceButtonLabel }</span>
136
+ </Button>
137
+ </div>
138
+ </div>
139
+ <div className="jetpack-ai-logo-generator__prompt-query">
140
+ <div
141
+ ref={ inputRef }
142
+ contentEditable={ ! isBusy && ! requireUpgrade }
143
+ // The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
144
+ suppressContentEditableWarning
145
+ className="prompt-query__input"
146
+ onInput={ onPromptInput }
147
+ onPaste={ onPromptPaste }
148
+ data-placeholder={ __(
149
+ 'Describe your site or simply ask for a logo specifying some details about it',
150
+ 'jetpack-ai-client'
151
+ ) }
152
+ ></div>
153
+ <Button
154
+ variant="primary"
155
+ className="jetpack-ai-logo-generator__prompt-submit"
156
+ onClick={ onGenerate }
157
+ disabled={ isBusy || requireUpgrade || ! hasPrompt }
158
+ >
159
+ { __( 'Generate', 'jetpack-ai-client' ) }
160
+ </Button>
161
+ </div>
162
+ <div className="jetpack-ai-logo-generator__prompt-footer">
163
+ { ! isUnlimited && ! requireUpgrade && (
164
+ <div className="jetpack-ai-logo-generator__prompt-requests">
165
+ <div>
166
+ { sprintf(
167
+ // translators: %u is the number of requests
168
+ __( '%u requests remaining.', 'jetpack-ai-client' ),
169
+ requestsRemaining
170
+ ) }
171
+ </div>
172
+ { hasNextTier && (
173
+ <>
174
+ &nbsp;
175
+ <Button
176
+ variant="link"
177
+ href={ checkoutUrl }
178
+ target="_blank"
179
+ onClick={ onUpgradeClick }
180
+ >
181
+ { __( 'Upgrade', 'jetpack-ai-client' ) }
182
+ </Button>
183
+ </>
184
+ ) }
185
+ &nbsp;
186
+ <Tooltip
187
+ text={ __(
188
+ 'Logo generation costs 10 requests; prompt enhancement costs 1 request each',
189
+ 'jetpack-ai-client'
190
+ ) }
191
+ placement="bottom"
192
+ >
193
+ <Icon className="prompt-footer__icon" icon={ info } />
194
+ </Tooltip>
195
+ </div>
196
+ ) }
197
+ { ! isUnlimited && requireUpgrade && <UpgradeNudge /> }
198
+ { enhancePromptFetchError && (
199
+ <div className="jetpack-ai-logo-generator__prompt-error">
200
+ { __( 'Error enhancing prompt. Please try again.', 'jetpack-ai-client' ) }
201
+ </div>
202
+ ) }
203
+ { logoFetchError && (
204
+ <div className="jetpack-ai-logo-generator__prompt-error">
205
+ { __( 'Error generating logo. Please try again.', 'jetpack-ai-client' ) }
206
+ </div>
207
+ ) }
208
+ </div>
209
+ </div>
210
+ );
211
+ };
@@ -0,0 +1,43 @@
1
+ @import '@automattic/jetpack-base-styles/root-variables';
2
+
3
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper {
4
+ display: flex;
5
+ justify-content: space-between;
6
+ align-items: center;
7
+ font-size: var(--font-body-small);
8
+ background: var(--jp-black);
9
+ padding: 8px 16px;
10
+ border-radius: 2px;
11
+ }
12
+
13
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .jetpack-upgrade-plan-banner__banner-description {
14
+ color: var(--jp-white);
15
+ line-height: 21px;
16
+ word-wrap: break-word;
17
+ vertical-align: middle;
18
+ }
19
+
20
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .jetpack-upgrade-plan-banner__icon {
21
+ width: 24px;
22
+ height: 24px;
23
+ position: relative;
24
+ vertical-align: middle;
25
+ margin-right: 8px;
26
+
27
+ path {
28
+ fill: var(--jp-gray-30);
29
+ }
30
+ }
31
+
32
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button {
33
+ height: auto;
34
+ line-height: 20px;
35
+ font-size: var(--font-body-extra-small);
36
+ font-weight: 600;
37
+ padding: 4px 8px;
38
+ }
39
+
40
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button.is-primary {
41
+ background: var(--jp-white);
42
+ color: var(--jp-black);
43
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
5
+ import { Button } from '@wordpress/components';
6
+ import { createInterpolateElement } from '@wordpress/element';
7
+ import { __ } from '@wordpress/i18n';
8
+ import { Icon, warning } from '@wordpress/icons';
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import { EVENT_PLACEMENT_UPGRADE_PROMPT, EVENT_UPGRADE } from '../constants.js';
13
+ import { useCheckout } from '../hooks/use-checkout.js';
14
+ import useLogoGenerator from '../hooks/use-logo-generator.js';
15
+ import './upgrade-nudge.scss';
16
+
17
+ export const UpgradeNudge = () => {
18
+ const { tracks } = useAnalytics();
19
+ const { recordEvent: recordTracksEvent } = tracks;
20
+ const buttonText = __( 'Upgrade', 'jetpack-ai-client' );
21
+ const upgradeMessage = createInterpolateElement(
22
+ __(
23
+ 'Not enough requests left to generate a logo. <strong>Upgrade now to increase it.</strong>',
24
+ 'jetpack-ai-client'
25
+ ),
26
+ {
27
+ strong: <strong />,
28
+ }
29
+ );
30
+
31
+ const { nextTierCheckoutURL: checkoutUrl } = useCheckout();
32
+ const { context } = useLogoGenerator();
33
+
34
+ const handleUpgradeClick = () => {
35
+ recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_UPGRADE_PROMPT } );
36
+ };
37
+
38
+ return (
39
+ <div className="jetpack-upgrade-plan-banner">
40
+ <div className="jetpack-upgrade-plan-banner__wrapper">
41
+ <div>
42
+ <Icon className="jetpack-upgrade-plan-banner__icon" icon={ warning } />
43
+ <span className="jetpack-upgrade-plan-banner__banner-description">
44
+ { upgradeMessage }
45
+ </span>
46
+ </div>
47
+ <Button
48
+ href={ checkoutUrl }
49
+ target="_blank"
50
+ className="is-primary"
51
+ onClick={ handleUpgradeClick }
52
+ >
53
+ { buttonText }
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ );
58
+ };