@atlaskit/emoji 71.6.6 → 71.7.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 (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/afm-cc/tsconfig.json +3 -0
  3. package/afm-products/tsconfig.json +3 -0
  4. package/dist/cjs/api/ai/generateEmojiImage.js +101 -0
  5. package/dist/cjs/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  6. package/dist/cjs/components/common/CreateEmojiWithRovo.js +150 -0
  7. package/dist/cjs/components/common/EmojiActions.js +6 -2
  8. package/dist/cjs/components/common/EmojiUploadPicker.js +30 -1
  9. package/dist/cjs/components/i18n.js +25 -0
  10. package/dist/cjs/components/picker/EmojiPickerComponent.compiled.css +3 -0
  11. package/dist/cjs/components/picker/EmojiPickerComponent.js +22 -3
  12. package/dist/cjs/components/picker/EmojiPickerList.js +6 -2
  13. package/dist/cjs/components/uploader/EmojiUploadComponent.js +5 -2
  14. package/dist/cjs/util/ai-emoji.js +72 -0
  15. package/dist/cjs/util/analytics/analytics.js +14 -2
  16. package/dist/cjs/util/analytics/index.js +18 -0
  17. package/dist/es2019/api/ai/generateEmojiImage.js +64 -0
  18. package/dist/es2019/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  19. package/dist/es2019/components/common/CreateEmojiWithRovo.js +113 -0
  20. package/dist/es2019/components/common/EmojiActions.js +6 -2
  21. package/dist/es2019/components/common/EmojiUploadPicker.js +28 -1
  22. package/dist/es2019/components/i18n.js +25 -0
  23. package/dist/es2019/components/picker/EmojiPickerComponent.compiled.css +3 -0
  24. package/dist/es2019/components/picker/EmojiPickerComponent.js +22 -3
  25. package/dist/es2019/components/picker/EmojiPickerList.js +6 -2
  26. package/dist/es2019/components/uploader/EmojiUploadComponent.js +5 -2
  27. package/dist/es2019/util/ai-emoji.js +64 -0
  28. package/dist/es2019/util/analytics/analytics.js +5 -1
  29. package/dist/es2019/util/analytics/index.js +1 -1
  30. package/dist/esm/api/ai/generateEmojiImage.js +94 -0
  31. package/dist/esm/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  32. package/dist/esm/components/common/CreateEmojiWithRovo.js +141 -0
  33. package/dist/esm/components/common/EmojiActions.js +6 -2
  34. package/dist/esm/components/common/EmojiUploadPicker.js +30 -1
  35. package/dist/esm/components/i18n.js +25 -0
  36. package/dist/esm/components/picker/EmojiPickerComponent.compiled.css +3 -0
  37. package/dist/esm/components/picker/EmojiPickerComponent.js +22 -3
  38. package/dist/esm/components/picker/EmojiPickerList.js +6 -2
  39. package/dist/esm/components/uploader/EmojiUploadComponent.js +5 -2
  40. package/dist/esm/util/ai-emoji.js +66 -0
  41. package/dist/esm/util/analytics/analytics.js +13 -1
  42. package/dist/esm/util/analytics/index.js +1 -1
  43. package/dist/types/api/ai/generateEmojiImage.d.ts +29 -0
  44. package/dist/types/components/common/CreateEmojiWithRovo.d.ts +24 -0
  45. package/dist/types/components/common/EmojiActions.d.ts +11 -0
  46. package/dist/types/components/common/EmojiUploadPicker.d.ts +9 -0
  47. package/dist/types/components/i18n.d.ts +25 -0
  48. package/dist/types/components/picker/EmojiPicker.d.ts +6 -0
  49. package/dist/types/components/picker/EmojiPickerComponent.d.ts +6 -0
  50. package/dist/types/components/picker/EmojiPickerList.d.ts +5 -0
  51. package/dist/types/components/uploader/EmojiUploadComponent.d.ts +2 -0
  52. package/dist/types/util/ai-emoji.d.ts +41 -0
  53. package/dist/types/util/analytics/analytics.d.ts +7 -0
  54. package/dist/types/util/analytics/index.d.ts +1 -1
  55. package/package.json +7 -2
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.wrapEmojiPrompt = exports.slugifyPrompt = exports.MAX_SHORTNAME_LENGTH = exports.EMOJI_PROMPT_PREFIX = void 0;
7
+ /**
8
+ * Pure utilities for the "Create an emoji with Rovo" (AI emoji generation) flow.
9
+ */
10
+
11
+ /**
12
+ * Maximum length of an auto-generated shortname. Matches the `maxNameLength`
13
+ * used by the manual emoji upload flow (see EmojiUploadPicker).
14
+ */
15
+ var MAX_SHORTNAME_LENGTH = exports.MAX_SHORTNAME_LENGTH = 50;
16
+ var LEADING_ARTICLES = ['a', 'an', 'the'];
17
+
18
+ /**
19
+ * Convert a free-text emoji description into a slugified shortname suitable for
20
+ * the `:shortname:` field expected by pf-emoji-service.
21
+ *
22
+ * Rules (v1):
23
+ * - Strip leading articles (a, an, the)
24
+ * - Lowercase
25
+ * - Replace runs of whitespace with a single underscore
26
+ * - Remove any character that is not a-z, 0-9 or underscore
27
+ * - Collapse repeated underscores and trim leading/trailing underscores
28
+ * - Truncate to {@link MAX_SHORTNAME_LENGTH} characters
29
+ *
30
+ * @example
31
+ * slugifyPrompt('a cat wearing a hard hat') // => 'cat_wearing_a_hard_hat'
32
+ */
33
+ var slugifyPrompt = exports.slugifyPrompt = function slugifyPrompt(prompt) {
34
+ if (!prompt) {
35
+ return '';
36
+ }
37
+ var words = prompt.trim().toLowerCase().split(/\s+/);
38
+
39
+ // Strip a single leading article only (e.g. "a cat" -> "cat").
40
+ if (words.length > 1 && LEADING_ARTICLES.includes(words[0])) {
41
+ words = words.slice(1);
42
+ }
43
+ var slug = words.join('_')
44
+ // remove everything that is not a word char (letters, digits, underscore)
45
+ .replace(/[^a-z0-9_]/g, '')
46
+ // collapse repeated underscores introduced by removed characters
47
+ .replace(/_+/g, '_')
48
+ // trim leading/trailing underscores
49
+ .replace(/^_+|_+$/g, '');
50
+ return slug.slice(0, MAX_SHORTNAME_LENGTH);
51
+ };
52
+
53
+ /**
54
+ * Prefix prepended to the user's description so the header-image backend
55
+ * produces clean, emoji-style output instead of a scene/photo.
56
+ *
57
+ * Kept as a single line (no newlines) — newlines in the prompt can degrade the
58
+ * model's output. Learnings from ShipIt 56 ("Emoji Pop") showed that a simple
59
+ * prompt prefix significantly improved emoji quality.
60
+ */
61
+ var EMOJI_PROMPT_PREFIX = exports.EMOJI_PROMPT_PREFIX = 'Generate a clean, simple, emoji-style icon of ';
62
+
63
+ /**
64
+ * Wrap the raw user prompt with the emoji-style prefix.
65
+ *
66
+ * @example
67
+ * wrapEmojiPrompt('a cat wearing a hard hat')
68
+ * // => 'Generate a clean, simple, emoji-style icon of a cat wearing a hard hat'
69
+ */
70
+ var wrapEmojiPrompt = exports.wrapEmojiPrompt = function wrapEmojiPrompt(userPrompt) {
71
+ return "".concat(EMOJI_PROMPT_PREFIX).concat(userPrompt.trim());
72
+ };
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.uploadSucceededEvent = exports.uploadFailedEvent = exports.uploadConfirmButton = exports.uploadCancelButton = exports.uploadBeginButton = exports.typeaheadSelectedEvent = exports.typeaheadRenderedEvent = exports.typeaheadCancelledEvent = exports.toneSelectorOpenedEvent = exports.toneSelectorClosedEvent = exports.toneSelectedEvent = exports.selectedFileEvent = exports.recordSucceededEmoji = exports.recordSucceeded = exports.recordSelectionSucceededSli = exports.recordSelectionFailedSli = exports.recordFailedEmoji = exports.recordFailed = exports.pickerSearchedEvent = exports.pickerClickedEvent = exports.openedPickerEvent = exports.extractErrorInfo = exports.deleteConfirmEvent = exports.deleteCancelEvent = exports.deleteBeginEvent = exports.createAndFireEventInElementsChannel = exports.closedPickerEvent = exports.categoryClickedEvent = void 0;
7
+ exports.uploadSucceededEvent = exports.uploadFailedEvent = exports.uploadConfirmButton = exports.uploadCancelButton = exports.uploadBeginButton = exports.typeaheadSelectedEvent = exports.typeaheadRenderedEvent = exports.typeaheadCancelledEvent = exports.toneSelectorOpenedEvent = exports.toneSelectorClosedEvent = exports.toneSelectedEvent = exports.selectedFileEvent = exports.recordSucceededEmoji = exports.recordSucceeded = exports.recordSelectionSucceededSli = exports.recordSelectionFailedSli = exports.recordFailedEmoji = exports.recordFailed = exports.pickerSearchedEvent = exports.pickerClickedEvent = exports.openedPickerEvent = exports.extractErrorInfo = exports.deleteConfirmEvent = exports.deleteCancelEvent = exports.deleteBeginEvent = exports.createAndFireEventInElementsChannel = exports.closedPickerEvent = exports.categoryClickedEvent = exports.aiGenerationStartedEvent = exports.aiGenerationFailedEvent = exports.aiGenerationCompletedEvent = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _analyticsNext = require("@atlaskit/analytics-next");
10
10
  var _types = require("../../types");
@@ -20,7 +20,7 @@ var createEvent = function createEvent(eventType, action, actionSubject, actionS
20
20
  actionSubjectId: actionSubjectId,
21
21
  attributes: _objectSpread({
22
22
  packageName: "@atlaskit/emoji",
23
- packageVersion: "71.6.5"
23
+ packageVersion: "71.6.6"
24
24
  }, attributes)
25
25
  };
26
26
  };
@@ -138,6 +138,18 @@ var uploadSucceededEvent = exports.uploadSucceededEvent = function uploadSucceed
138
138
  var uploadFailedEvent = exports.uploadFailedEvent = function uploadFailedEvent(attributes) {
139
139
  return createEvent('operational', 'failed', 'emojiUploader', undefined, attributes);
140
140
  };
141
+ var aiEmojiGenerationEvent = function aiEmojiGenerationEvent(action, actionSubjectId, attributes) {
142
+ return createEvent('ui', action, 'emojiPickerAiGeneration', actionSubjectId, attributes);
143
+ };
144
+ var aiGenerationStartedEvent = exports.aiGenerationStartedEvent = function aiGenerationStartedEvent(attributes) {
145
+ return aiEmojiGenerationEvent('started', 'generateButton', attributes);
146
+ };
147
+ var aiGenerationCompletedEvent = exports.aiGenerationCompletedEvent = function aiGenerationCompletedEvent(attributes) {
148
+ return createEvent('operational', 'completed', 'emojiPickerAiGeneration', undefined, attributes);
149
+ };
150
+ var aiGenerationFailedEvent = exports.aiGenerationFailedEvent = function aiGenerationFailedEvent(attributes) {
151
+ return createEvent('operational', 'failed', 'emojiPickerAiGeneration', undefined, attributes);
152
+ };
141
153
  var deleteBeginEvent = exports.deleteBeginEvent = function deleteBeginEvent(attributes) {
142
154
  return createEvent('ui', 'clicked', 'emojiPicker', 'deleteEmojiTrigger', attributes);
143
155
  };
@@ -3,6 +3,24 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "aiGenerationCompletedEvent", {
7
+ enumerable: true,
8
+ get: function get() {
9
+ return _analytics.aiGenerationCompletedEvent;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "aiGenerationFailedEvent", {
13
+ enumerable: true,
14
+ get: function get() {
15
+ return _analytics.aiGenerationFailedEvent;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "aiGenerationStartedEvent", {
19
+ enumerable: true,
20
+ get: function get() {
21
+ return _analytics.aiGenerationStartedEvent;
22
+ }
23
+ });
6
24
  Object.defineProperty(exports, "categoryClickedEvent", {
7
25
  enumerable: true,
8
26
  get: function get() {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Calls the Confluence "header image" AI backend to generate an emoji-style
3
+ * image (base64) from a text description.
4
+ */
5
+
6
+ import { wrapEmojiPrompt } from '../../util/ai-emoji';
7
+ import debug from '../../util/logger';
8
+ const HEADER_IMAGE_GENERATION_ENDPOINT = '/gateway/api/assist/api/ai/v2/ai-feature/confluence-header-image-generation';
9
+
10
+ /** Emojis are square, so we always request a 1:1 image from the BE. */
11
+ const EMOJI_ASPECT_RATIO = '1:1';
12
+
13
+ /** Shape of the `ai_feature_output` envelope returned by the BE. */
14
+
15
+ /**
16
+ * Generate an emoji-style image and return its base64 image data.
17
+ *
18
+ * @throws Error if the request fails, the BE returns a body-level error, or no
19
+ * image data is returned.
20
+ */
21
+ export const generateEmojiImage = async ({
22
+ contentId,
23
+ prompt
24
+ }) => {
25
+ const wrappedPrompt = wrapEmojiPrompt(prompt);
26
+ const response = await fetch(HEADER_IMAGE_GENERATION_ENDPOINT, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'content-type': 'application/json;charset=UTF-8',
30
+ 'X-Experience-Id': 'confluence-ai-first-creation',
31
+ 'X-Product': 'confluence'
32
+ },
33
+ body: JSON.stringify({
34
+ ai_feature_input: {
35
+ adfContent: wrappedPrompt,
36
+ contentId,
37
+ useRawPrompt: true,
38
+ // Emojis are square — request a 1:1 image from the BE.
39
+ aspectRatio: EMOJI_ASPECT_RATIO
40
+ }
41
+ })
42
+ });
43
+ if (!response.ok) {
44
+ debug('generateEmojiImage failed', response.status);
45
+ throw new Error(`Emoji image generation failed with status ${response.status}`);
46
+ }
47
+ const json = await response.json();
48
+ const output = json === null || json === void 0 ? void 0 : json.ai_feature_output;
49
+ if (!output) {
50
+ throw new Error('Emoji image generation returned no content');
51
+ }
52
+
53
+ // The BE can return 200 OK with an error in the body (e.g. moderation).
54
+ if (output.error) {
55
+ throw new Error(output.error.error_message || output.error.error_reason || 'Emoji image generation failed');
56
+ }
57
+ if (!output.imageData) {
58
+ throw new Error('Emoji image generation did not return image data');
59
+ }
60
+ return {
61
+ imageData: output.imageData,
62
+ mediaFileId: output.mediaFileId
63
+ };
64
+ };
@@ -0,0 +1,12 @@
1
+
2
+ ._zulp12x7{gap:var(--ds-space-075,6px)}
3
+ ._zulpu2gc{gap:var(--ds-space-100,8px)}
4
+ ._x3doia51{border-top:var(--ds-border-width,1px) solid var(--ds-border,#0b120e24)}._16jlkb7n{flex-grow:1}
5
+ ._19pku2gc{margin-top:var(--ds-space-100,8px)}
6
+ ._1e0c1txw{display:flex}
7
+ ._1o9zkb7n{flex-shrink:1}
8
+ ._2lx21bp4{flex-direction:column}
9
+ ._4cvr1h6o{align-items:center}
10
+ ._4cvr1y6m{align-items:flex-start}
11
+ ._ca0qutpp{padding-top:var(--ds-space-150,9pt)}
12
+ ._i0dlf1ug{flex-basis:0%}
@@ -0,0 +1,113 @@
1
+ /* CreateEmojiWithRovo.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import "./CreateEmojiWithRovo.compiled.css";
3
+ import * as React from 'react';
4
+ import { ax, ix } from "@compiled/react/runtime";
5
+ import { useCallback, useState } from 'react';
6
+ import { FormattedMessage, useIntl } from 'react-intl';
7
+ import { IconButton } from '@atlaskit/button/new';
8
+ import TextField from '@atlaskit/textfield';
9
+ import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
10
+ import { RovoIcon } from '@atlaskit/logo';
11
+ import { Text } from '@atlaskit/primitives/compiled';
12
+ import { generateEmojiImage } from '../../api/ai/generateEmojiImage';
13
+ import { slugifyPrompt } from '../../util/ai-emoji';
14
+ import { aiGenerationStartedEvent, aiGenerationCompletedEvent, aiGenerationFailedEvent } from '../../util/analytics';
15
+ import { messages } from '../i18n';
16
+ import EmojiErrorMessage from './EmojiErrorMessage';
17
+ export const createEmojiWithRovoTestId = 'create-emoji-with-rovo';
18
+ export const createEmojiWithRovoPromptTestId = 'create-emoji-with-rovo-prompt';
19
+ export const createEmojiWithRovoGenerateTestId = 'create-emoji-with-rovo-generate';
20
+ const sectionStyles = null;
21
+ const headerStyles = null;
22
+ const promptRowStyles = null;
23
+ const promptInputStyles = null;
24
+ const CreateEmojiWithRovo = props => {
25
+ const {
26
+ contentId,
27
+ fireAnalytics,
28
+ onEmojiGenerated
29
+ } = props;
30
+ const {
31
+ formatMessage
32
+ } = useIntl();
33
+ const [prompt, setPrompt] = useState('');
34
+ const [isGenerating, setIsGenerating] = useState(false);
35
+ const [hasError, setHasError] = useState(false);
36
+ const onPromptChange = useCallback(event => {
37
+ setPrompt(event.target.value);
38
+ // Clear any previous error as soon as the user edits the prompt to retry.
39
+ setHasError(false);
40
+ }, []);
41
+ const onGenerate = useCallback(async () => {
42
+ const trimmedPrompt = prompt.trim();
43
+ if (!trimmedPrompt || isGenerating) {
44
+ return;
45
+ }
46
+ setHasError(false);
47
+ setIsGenerating(true);
48
+ fireAnalytics === null || fireAnalytics === void 0 ? void 0 : fireAnalytics(aiGenerationStartedEvent({
49
+ promptLength: trimmedPrompt.length
50
+ }));
51
+ const startTime = Date.now();
52
+ try {
53
+ const {
54
+ imageData
55
+ } = await generateEmojiImage({
56
+ contentId,
57
+ prompt: trimmedPrompt
58
+ });
59
+ const dataURL = `data:image/png;base64,${imageData}`;
60
+ const suggestedName = slugifyPrompt(trimmedPrompt);
61
+ fireAnalytics === null || fireAnalytics === void 0 ? void 0 : fireAnalytics(aiGenerationCompletedEvent({
62
+ duration: Date.now() - startTime
63
+ }));
64
+ // Hand the generated image to the parent upload form.
65
+ onEmojiGenerated(dataURL, suggestedName);
66
+ } catch (error) {
67
+ setHasError(true);
68
+ fireAnalytics === null || fireAnalytics === void 0 ? void 0 : fireAnalytics(aiGenerationFailedEvent({
69
+ errorType: error instanceof Error ? error.message : 'generation_failed'
70
+ }));
71
+ } finally {
72
+ setIsGenerating(false);
73
+ }
74
+ }, [contentId, fireAnalytics, isGenerating, onEmojiGenerated, prompt]);
75
+ const generateDisabled = !prompt.trim() || isGenerating;
76
+ return /*#__PURE__*/React.createElement("div", {
77
+ "data-testid": createEmojiWithRovoTestId,
78
+ className: ax(["_zulpu2gc _x3doia51 _1e0c1txw _2lx21bp4 _ca0qutpp _19pku2gc"])
79
+ }, /*#__PURE__*/React.createElement("div", {
80
+ className: ax(["_zulp12x7 _1e0c1txw _4cvr1h6o"])
81
+ }, /*#__PURE__*/React.createElement(RovoIcon, {
82
+ appearance: "brand",
83
+ size: "xsmall",
84
+ label: formatMessage(messages.createEmojiWithRovoTitle)
85
+ }), /*#__PURE__*/React.createElement(Text, {
86
+ size: "medium",
87
+ weight: "bold"
88
+ }, /*#__PURE__*/React.createElement(FormattedMessage, messages.createEmojiWithRovoTitle))), /*#__PURE__*/React.createElement("div", {
89
+ className: ax(["_zulpu2gc _1e0c1txw _4cvr1y6m"])
90
+ }, /*#__PURE__*/React.createElement("span", {
91
+ className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug"])
92
+ }, /*#__PURE__*/React.createElement(TextField, {
93
+ value: prompt,
94
+ onChange: onPromptChange,
95
+ placeholder: formatMessage(messages.createEmojiWithRovoPromptPlaceholder),
96
+ "aria-label": formatMessage(messages.createEmojiWithRovoPromptAriaLabel),
97
+ isCompact: true,
98
+ isDisabled: isGenerating,
99
+ testId: createEmojiWithRovoPromptTestId
100
+ })), /*#__PURE__*/React.createElement(IconButton, {
101
+ appearance: "primary",
102
+ icon: ArrowUpIcon,
103
+ label: formatMessage(messages.createEmojiWithRovoGenerateLabel),
104
+ isDisabled: generateDisabled,
105
+ isLoading: isGenerating,
106
+ onClick: onGenerate,
107
+ testId: createEmojiWithRovoGenerateTestId
108
+ })), hasError && /*#__PURE__*/React.createElement(EmojiErrorMessage, {
109
+ errorStyle: "chooseFile",
110
+ message: /*#__PURE__*/React.createElement(FormattedMessage, messages.createEmojiWithRovoError)
111
+ }));
112
+ };
113
+ export default CreateEmojiWithRovo;
@@ -308,7 +308,9 @@ export const EmojiActions = props => {
308
308
  onUploadEmoji: onUploadEmoji,
309
309
  onFileChooserClicked: onFileChooserClicked,
310
310
  errorMessage: uploadErrorMessage,
311
- initialUploadName: initialUploadName
311
+ initialUploadName: initialUploadName,
312
+ contentId: props.contentId,
313
+ fireAnalytics: props.fireAnalytics
312
314
  })) : /*#__PURE__*/React.createElement("div", {
313
315
  className: ax(["_16jlidpf _1o9zidpf _i0dl1wug _n7zl1uh4 _16qsjgpa"])
314
316
  }, /*#__PURE__*/React.createElement(EmojiUploadPicker, {
@@ -316,7 +318,9 @@ export const EmojiActions = props => {
316
318
  onUploadEmoji: onUploadEmoji,
317
319
  onFileChooserClicked: onFileChooserClicked,
318
320
  errorMessage: uploadErrorMessage,
319
- initialUploadName: initialUploadName
321
+ initialUploadName: initialUploadName,
322
+ contentId: props.contentId,
323
+ fireAnalytics: props.fireAnalytics
320
324
  }));
321
325
  }
322
326
  if (emojiToDelete) {
@@ -10,6 +10,7 @@ import AkButton from '@atlaskit/button/standard-button';
10
10
  import { Text } from '@atlaskit/primitives/compiled';
11
11
  import FocusLock from 'react-focus-lock';
12
12
  import * as ImageUtil from '../../util/image';
13
+ import CreateEmojiWithRovo from './CreateEmojiWithRovo';
13
14
  import debug from '../../util/logger';
14
15
  import { messages } from '../i18n';
15
16
  import EmojiErrorMessage from './EmojiErrorMessage';
@@ -18,6 +19,7 @@ import FileChooser from './FileChooser';
18
19
  import { UploadStatus } from './internal-types';
19
20
  import { fg } from '@atlaskit/platform-feature-flags';
20
21
  import FeatureGates from '@atlaskit/feature-gate-js-client';
22
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
21
23
  import Button from '@atlaskit/button/new';
22
24
  import { Box } from '@atlaskit/primitives/compiled';
23
25
  import { getDocument } from '@atlaskit/browser-apis';
@@ -86,6 +88,7 @@ const ChooseEmojiFile = /*#__PURE__*/memo(props => {
86
88
  nameErrorMessage,
87
89
  previewImage,
88
90
  uploadStatus,
91
+ aiSection,
89
92
  intl
90
93
  } = props;
91
94
  const {
@@ -135,7 +138,7 @@ const ChooseEmojiFile = /*#__PURE__*/memo(props => {
135
138
  }, errorMessage && /*#__PURE__*/React.createElement(EmojiErrorMessage, {
136
139
  errorStyle: "chooseFile",
137
140
  message: errorMessage
138
- }))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
141
+ }))), aiSection, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
139
142
  htmlFor: "new-emoji-name-input",
140
143
  className: ax(["_1w90azsu", "_11c8wadc _k48p1pd9"])
141
144
  }, /*#__PURE__*/React.createElement(FormattedMessage, messages.emojiNameLabel)), /*#__PURE__*/React.createElement(TextField, {
@@ -231,6 +234,8 @@ const EmojiUploadPicker = props => {
231
234
  onFileChooserClicked,
232
235
  onUploadCancelled,
233
236
  disableFocusLock = false,
237
+ contentId,
238
+ fireAnalytics,
234
239
  intl
235
240
  } = props;
236
241
  const [uploadStatus, setUploadStatus] = useState(errorMessage ? UploadStatus.Error : UploadStatus.Waiting);
@@ -302,6 +307,16 @@ const EmojiUploadPicker = props => {
302
307
  const cancelChooseFile = useCallback(() => {
303
308
  setPreviewImage(undefined);
304
309
  }, []);
310
+
311
+ // When the Rovo section generates an image, feed it into the existing upload
312
+ // form (same preview, name field and "Add emoji" button as a manual upload).
313
+ const onEmojiGenerated = useCallback((dataURL, suggestedName) => {
314
+ setFilename('rovo-emoji.png');
315
+ setPreviewImage(dataURL);
316
+ setChooseEmojiErrorMessage(undefined);
317
+ // Only auto-populate the name if the user hasn't already typed one.
318
+ setName(current => current || sanitizeName(suggestedName));
319
+ }, []);
305
320
  const errorOnUpload = useCallback(event => {
306
321
  debug('File load error: ', event);
307
322
  setChooseEmojiErrorMessage( /*#__PURE__*/React.createElement(FormattedMessage, messages.emojiUploadFailed));
@@ -364,6 +379,17 @@ const EmojiUploadPicker = props => {
364
379
  onFileChooserClicked && onFileChooserClicked();
365
380
  };
366
381
  const isDuplicateNameError = errorMessage !== null && errorMessage !== undefined && isRefreshEmojiPickerEnabled();
382
+
383
+ // "Create an emoji with Rovo" AI generation section. Only rendered when the
384
+ // experiment is on AND a page content id is available (the image generation
385
+ // backend requires it). It is rendered inside ChooseEmojiFile (between the
386
+ // drop area and the Emoji name field) so the generated image reuses the
387
+ // single shared name field and "Add emoji" button.
388
+ const aiSection = contentId && expValEquals('confluence_ai_generated_emojis', 'isEnabled', true) ? /*#__PURE__*/React.createElement(CreateEmojiWithRovo, {
389
+ contentId: contentId,
390
+ fireAnalytics: fireAnalytics,
391
+ onEmojiGenerated: onEmojiGenerated
392
+ }) : null;
367
393
  const content = name && previewImage && !isRefreshEmojiPickerEnabled() ? /*#__PURE__*/React.createElement(EmojiUploadPreview, {
368
394
  errorMessage: errorMessage,
369
395
  name: name,
@@ -382,6 +408,7 @@ const EmojiUploadPicker = props => {
382
408
  uploadStatus: uploadStatus,
383
409
  errorMessage: chooseEmojiErrorMessage,
384
410
  nameErrorMessage: isDuplicateNameError ? errorMessage : undefined,
411
+ aiSection: aiSection,
385
412
  intl: intl
386
413
  });
387
414
  return disableFocusLock || isRefreshEmojiPickerEnabled() ? content : /*#__PURE__*/React.createElement(FocusLock, {
@@ -100,6 +100,31 @@ export const messages = defineMessages({
100
100
  defaultMessage: 'Add emoji',
101
101
  description: 'Label for the submit button in the custom emoji upload panel that saves the new emoji to the workspace.'
102
102
  },
103
+ createEmojiWithRovoTitle: {
104
+ id: 'fabric.emoji.ai.create.title',
105
+ defaultMessage: 'Create an emoji with Rovo',
106
+ description: 'Section heading for the AI emoji generation area where users describe an emoji and generate it with AI.'
107
+ },
108
+ createEmojiWithRovoPromptPlaceholder: {
109
+ id: 'fabric.emoji.ai.prompt.placeholder',
110
+ defaultMessage: 'Describe your emoji...',
111
+ description: 'Placeholder text for the input where the user describes the emoji they want the AI to generate.'
112
+ },
113
+ createEmojiWithRovoPromptAriaLabel: {
114
+ id: 'fabric.emoji.ai.prompt.ariaLabel',
115
+ defaultMessage: 'Describe the emoji you want to generate',
116
+ description: 'Accessible label for the AI emoji description input field.'
117
+ },
118
+ createEmojiWithRovoGenerateLabel: {
119
+ id: 'fabric.emoji.ai.generate.label',
120
+ defaultMessage: 'Generate',
121
+ description: 'Label for the button that triggers AI generation of the described emoji.'
122
+ },
123
+ createEmojiWithRovoError: {
124
+ id: 'fabric.emoji.ai.error',
125
+ defaultMessage: 'Something went wrong. Try a different description.',
126
+ description: 'Inline error message shown when AI emoji generation or upload fails, prompting the user to try again.'
127
+ },
103
128
  retryLabel: {
104
129
  id: 'fabric.emoji.retry.label',
105
130
  defaultMessage: 'Retry',
@@ -4,6 +4,7 @@
4
4
  ._2rkofajl{border-radius:var(--ds-radius-small,3px)}._16jlkb7n{flex-grow:1}
5
5
  ._16qs130s{box-shadow:var(--ds-shadow-overlay,0 8px 9pt #1e1f2126,0 0 1px #1e1f214f)}
6
6
  ._18m915vq{overflow-y:hidden}
7
+ ._18m91wug{overflow-y:auto}
7
8
  ._1bah1yb4{justify-content:space-between}
8
9
  ._1bsb1edt{width:350px}
9
10
  ._1e0c1txw{display:flex}
@@ -27,10 +28,12 @@
27
28
  ._4t3ibqjm{height:310px}
28
29
  ._4t3iihnn{height:434px}
29
30
  ._4t3iixjv{height:375px}
31
+ ._4t3ikbql{height:450px}
30
32
  ._4t3iqbeb{height:339px}
31
33
  ._4t3ivixp{height:349px}
32
34
  ._4t3ixt2k{height:509px}
33
35
  ._bfhk1bhr{background-color:var(--ds-surface-overlay,#fff)}
34
36
  ._c71l1y6z{max-height:calc(80vh - 86px)}
37
+ ._c71l8k0t{max-height:calc(100vh - 86px)}
35
38
  ._i0dlf1ug{flex-basis:0%}
36
39
  ._kqswh2mm{position:relative}
@@ -28,6 +28,7 @@ import { useIsMounted } from '../../hooks/useIsMounted';
28
28
  import { messages } from '../i18n';
29
29
  import { defaultProductivityColor, getStoredProductivityColor, storeProductivityColor } from '../../util/productivity-colors';
30
30
  import { filterHiddenEmojis } from '../../util/hidden-emojis';
31
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
31
32
  const isRefreshEmojiPickerEnabled = () => {
32
33
  if (!FeatureGates.initializeCompleted()) {
33
34
  return false;
@@ -48,6 +49,17 @@ const heightOffset = 80;
48
49
  const emojiPicker = null;
49
50
  const emojiPickerNew = null;
50
51
  const emojiPickerWrapper = null;
52
+
53
+ // When the "Create an emoji with Rovo" section is shown in the upload panel the
54
+ // picker needs more vertical room than the fixed upload height. Make it tall
55
+ // enough to fit the whole section without scrolling, but still let it scroll as
56
+ // a safety net on very short viewports.
57
+ const emojiPickerWrapperScrollable = null;
58
+
59
+ // When the AI emoji experiment is on, grow the upload panel from its default
60
+ // height to 450px so the manual upload form + the slim "Create an emoji with
61
+ // Rovo" section are both visible without scrolling.
62
+ const withAiUploadHeight = null;
51
63
  const withPreviewHeight = {
52
64
  small: "_4t3ivixp _1tkegx0z",
53
65
  medium: "_4t3i2300 _1tke5x59",
@@ -80,6 +92,7 @@ const EmojiPickerComponent = ({
80
92
  onPickerRef,
81
93
  hideToneSelector,
82
94
  createAnalyticsEvent,
95
+ contentId,
83
96
  size = defaultEmojiPickerSize
84
97
  }) => {
85
98
  const {
@@ -574,19 +587,23 @@ const EmojiPickerComponent = ({
574
587
  const showPreview = isRefreshEmojiPickerEnabled() ? !uploading : selectedEmoji && !uploading;
575
588
  const shouldRenderFooter = showPreview && !(emojiToDelete && isRefreshEmojiPickerEnabled()) && !(query && filteredEmojis.length === 0 && isRefreshEmojiPickerEnabled()) && (Boolean(selectedEmoji) || uploadEnabled || !isRefreshEmojiPickerEnabled());
576
589
  const useFooterSpaceForList = showPreview && !selectedEmoji && !uploadEnabled && !emojiToDelete && !(query && filteredEmojis.length === 0) && isRefreshEmojiPickerEnabled();
590
+
591
+ // When the AI emoji section is shown in the upload panel, grow the picker so
592
+ // the section isn't clipped by the fixed upload height.
593
+ const showAiUpload = uploading && !!contentId && expValEquals('confluence_ai_generated_emojis', 'isEnabled', true);
577
594
  return /*#__PURE__*/React.createElement("div", {
578
595
  ref: setPickerRef,
579
596
  "data-emoji-picker-container": true,
580
597
  role: "dialog",
581
598
  "aria-label": formatMessage(messages.emojiPickerTitle),
582
599
  "aria-modal": true,
583
- className: ax([isRefreshEmojiPickerEnabled() ? "_19itahnd _2rko1mok _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iaq3k _1bsb1edt _1ul91edt _c71l1y6z _kqswh2mm" : "_19itahnd _2rkofajl _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iaq3k _1bsb1edt _1ul91edt _c71l1y6z _kqswh2mm", !!emojiToDelete && isRefreshEmojiPickerEnabled() ? withDeleteRefreshHeight[size] : uploading && isRefreshEmojiPickerEnabled() ? withUploadRefreshHeight[size] : query && filteredEmojis.length === 0 && isRefreshEmojiPickerEnabled() ? withNoResultsRefreshHeight[size] : showPreview ? withPreviewHeight[size] : withoutPreviewHeight[size]])
600
+ className: ax([isRefreshEmojiPickerEnabled() ? "_19itahnd _2rko1mok _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iaq3k _1bsb1edt _1ul91edt _c71l1y6z _kqswh2mm" : "_19itahnd _2rkofajl _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iaq3k _1bsb1edt _1ul91edt _c71l1y6z _kqswh2mm", !!emojiToDelete && isRefreshEmojiPickerEnabled() ? withDeleteRefreshHeight[size] : uploading && isRefreshEmojiPickerEnabled() ? withUploadRefreshHeight[size] : query && filteredEmojis.length === 0 && isRefreshEmojiPickerEnabled() ? withNoResultsRefreshHeight[size] : showPreview ? withPreviewHeight[size] : withoutPreviewHeight[size], showAiUpload && "_4t3ikbql _c71l8k0t"])
584
601
  }, /*#__PURE__*/React.createElement("div", {
585
602
  role: "presentation",
586
603
  onKeyPress: suppressKeyPress,
587
604
  onKeyUp: suppressKeyPress,
588
605
  onKeyDown: suppressKeyPress,
589
- className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug _1reo15vq _18m915vq _1e0c1txw _2lx21bp4 _1bah1yb4 _1tkeidpf"])
606
+ className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug _1reo15vq _18m915vq _1e0c1txw _2lx21bp4 _1bah1yb4 _1tkeidpf", showAiUpload && "_1reo15vq _18m91wug"])
590
607
  }, /*#__PURE__*/React.createElement(CategorySelector, {
591
608
  activeCategoryId: (uploading || emojiToDelete) && isRefreshEmojiPickerEnabled() ? null : activeCategory,
592
609
  dynamicCategories: dynamicCategories,
@@ -623,7 +640,9 @@ const EmojiPickerComponent = ({
623
640
  onOpenUpload: onOpenUpload,
624
641
  size: size,
625
642
  activeCategoryId: activeCategory,
626
- useFooterSpaceForList: useFooterSpaceForList
643
+ useFooterSpaceForList: useFooterSpaceForList,
644
+ contentId: contentId,
645
+ fireAnalytics: fireAnalytics
627
646
  }), shouldRenderFooter && /*#__PURE__*/React.createElement(EmojiPickerFooter, {
628
647
  selectedEmoji: selectedEmoji,
629
648
  uploadEnabled: uploadEnabled,
@@ -77,7 +77,9 @@ export const EmojiPickerVirtualListInternal = /*#__PURE__*/React.forwardRef((pro
77
77
  onProductivityColorSelected,
78
78
  activeCategoryId,
79
79
  selectedProductivityColor,
80
- useFooterSpaceForList
80
+ useFooterSpaceForList,
81
+ contentId,
82
+ fireAnalytics
81
83
  } = props;
82
84
  const {
83
85
  formatMessage
@@ -468,7 +470,9 @@ export const EmojiPickerVirtualListInternal = /*#__PURE__*/React.forwardRef((pro
468
470
  onProductivityColorSelected: isTeamojiExperimentEnabled ? onProductivityColorSelected : undefined,
469
471
  query: query,
470
472
  onChange: onSearch,
471
- resultsCount: visibleEmojis.length
473
+ resultsCount: visibleEmojis.length,
474
+ contentId: contentId,
475
+ fireAnalytics: fireAnalytics
472
476
  }), /*#__PURE__*/React.createElement(EmojiPickerListContextProvider, {
473
477
  initialEmojisFocus: {
474
478
  rowIndex: 1,
@@ -18,7 +18,8 @@ const EmojiUploadComponent = props => {
18
18
  emojiProvider,
19
19
  createAnalyticsEvent,
20
20
  onUploaderRef,
21
- disableFocusLock
21
+ disableFocusLock,
22
+ contentId
22
23
  } = props;
23
24
  const [uploadErrorMessage, setUploadErrorMessage] = useState();
24
25
  useEffect(() => {
@@ -91,7 +92,9 @@ const EmojiUploadComponent = props => {
91
92
  onUploadCancelled: onUploadCancelled,
92
93
  onUploadEmoji: onUploadEmoji,
93
94
  errorMessage: uploadErrorMessage ? /*#__PURE__*/React.createElement(FormattedMessage, uploadErrorMessage) : null,
94
- disableFocusLock: disableFocusLock
95
+ disableFocusLock: disableFocusLock,
96
+ contentId: contentId,
97
+ fireAnalytics: fireAnalytics
95
98
  })));
96
99
  };
97
100
  const _default_1 = /*#__PURE__*/memo(EmojiUploadComponent);