@atlaskit/emoji 71.6.6 → 71.7.1

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 (59) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/afm-cc/tsconfig.json +3 -0
  3. package/afm-products/tsconfig.json +3 -0
  4. package/compass.yml +3 -0
  5. package/dist/cjs/api/EmojiResource.js +1 -1
  6. package/dist/cjs/api/ai/generateEmojiImage.js +101 -0
  7. package/dist/cjs/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  8. package/dist/cjs/components/common/CreateEmojiWithRovo.js +150 -0
  9. package/dist/cjs/components/common/EmojiActions.js +6 -2
  10. package/dist/cjs/components/common/EmojiUploadPicker.js +30 -1
  11. package/dist/cjs/components/i18n.js +25 -0
  12. package/dist/cjs/components/picker/EmojiPickerComponent.compiled.css +3 -0
  13. package/dist/cjs/components/picker/EmojiPickerComponent.js +22 -3
  14. package/dist/cjs/components/picker/EmojiPickerList.js +6 -2
  15. package/dist/cjs/components/uploader/EmojiUploadComponent.js +5 -2
  16. package/dist/cjs/util/ai-emoji.js +72 -0
  17. package/dist/cjs/util/analytics/analytics.js +14 -2
  18. package/dist/cjs/util/analytics/index.js +18 -0
  19. package/dist/es2019/api/EmojiResource.js +1 -1
  20. package/dist/es2019/api/ai/generateEmojiImage.js +64 -0
  21. package/dist/es2019/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  22. package/dist/es2019/components/common/CreateEmojiWithRovo.js +113 -0
  23. package/dist/es2019/components/common/EmojiActions.js +6 -2
  24. package/dist/es2019/components/common/EmojiUploadPicker.js +28 -1
  25. package/dist/es2019/components/i18n.js +25 -0
  26. package/dist/es2019/components/picker/EmojiPickerComponent.compiled.css +3 -0
  27. package/dist/es2019/components/picker/EmojiPickerComponent.js +22 -3
  28. package/dist/es2019/components/picker/EmojiPickerList.js +6 -2
  29. package/dist/es2019/components/uploader/EmojiUploadComponent.js +5 -2
  30. package/dist/es2019/util/ai-emoji.js +64 -0
  31. package/dist/es2019/util/analytics/analytics.js +5 -1
  32. package/dist/es2019/util/analytics/index.js +1 -1
  33. package/dist/esm/api/EmojiResource.js +1 -1
  34. package/dist/esm/api/ai/generateEmojiImage.js +94 -0
  35. package/dist/esm/components/common/CreateEmojiWithRovo.compiled.css +12 -0
  36. package/dist/esm/components/common/CreateEmojiWithRovo.js +141 -0
  37. package/dist/esm/components/common/EmojiActions.js +6 -2
  38. package/dist/esm/components/common/EmojiUploadPicker.js +30 -1
  39. package/dist/esm/components/i18n.js +25 -0
  40. package/dist/esm/components/picker/EmojiPickerComponent.compiled.css +3 -0
  41. package/dist/esm/components/picker/EmojiPickerComponent.js +22 -3
  42. package/dist/esm/components/picker/EmojiPickerList.js +6 -2
  43. package/dist/esm/components/uploader/EmojiUploadComponent.js +5 -2
  44. package/dist/esm/util/ai-emoji.js +66 -0
  45. package/dist/esm/util/analytics/analytics.js +13 -1
  46. package/dist/esm/util/analytics/index.js +1 -1
  47. package/dist/types/api/ai/generateEmojiImage.d.ts +29 -0
  48. package/dist/types/components/common/CreateEmojiWithRovo.d.ts +24 -0
  49. package/dist/types/components/common/EmojiActions.d.ts +11 -0
  50. package/dist/types/components/common/EmojiUploadPicker.d.ts +9 -0
  51. package/dist/types/components/i18n.d.ts +25 -0
  52. package/dist/types/components/picker/EmojiPicker.d.ts +6 -0
  53. package/dist/types/components/picker/EmojiPickerComponent.d.ts +6 -0
  54. package/dist/types/components/picker/EmojiPickerList.d.ts +5 -0
  55. package/dist/types/components/uploader/EmojiUploadComponent.d.ts +2 -0
  56. package/dist/types/util/ai-emoji.d.ts +41 -0
  57. package/dist/types/util/analytics/analytics.d.ts +7 -0
  58. package/dist/types/util/analytics/index.d.ts +1 -1
  59. package/package.json +7 -5
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure utilities for the "Create an emoji with Rovo" (AI emoji generation) flow.
3
+ */
4
+
5
+ /**
6
+ * Maximum length of an auto-generated shortname. Matches the `maxNameLength`
7
+ * used by the manual emoji upload flow (see EmojiUploadPicker).
8
+ */
9
+ export const MAX_SHORTNAME_LENGTH = 50;
10
+ const LEADING_ARTICLES = ['a', 'an', 'the'];
11
+
12
+ /**
13
+ * Convert a free-text emoji description into a slugified shortname suitable for
14
+ * the `:shortname:` field expected by pf-emoji-service.
15
+ *
16
+ * Rules (v1):
17
+ * - Strip leading articles (a, an, the)
18
+ * - Lowercase
19
+ * - Replace runs of whitespace with a single underscore
20
+ * - Remove any character that is not a-z, 0-9 or underscore
21
+ * - Collapse repeated underscores and trim leading/trailing underscores
22
+ * - Truncate to {@link MAX_SHORTNAME_LENGTH} characters
23
+ *
24
+ * @example
25
+ * slugifyPrompt('a cat wearing a hard hat') // => 'cat_wearing_a_hard_hat'
26
+ */
27
+ export const slugifyPrompt = prompt => {
28
+ if (!prompt) {
29
+ return '';
30
+ }
31
+ let words = prompt.trim().toLowerCase().split(/\s+/);
32
+
33
+ // Strip a single leading article only (e.g. "a cat" -> "cat").
34
+ if (words.length > 1 && LEADING_ARTICLES.includes(words[0])) {
35
+ words = words.slice(1);
36
+ }
37
+ const slug = words.join('_')
38
+ // remove everything that is not a word char (letters, digits, underscore)
39
+ .replace(/[^a-z0-9_]/g, '')
40
+ // collapse repeated underscores introduced by removed characters
41
+ .replace(/_+/g, '_')
42
+ // trim leading/trailing underscores
43
+ .replace(/^_+|_+$/g, '');
44
+ return slug.slice(0, MAX_SHORTNAME_LENGTH);
45
+ };
46
+
47
+ /**
48
+ * Prefix prepended to the user's description so the header-image backend
49
+ * produces clean, emoji-style output instead of a scene/photo.
50
+ *
51
+ * Kept as a single line (no newlines) — newlines in the prompt can degrade the
52
+ * model's output. Learnings from ShipIt 56 ("Emoji Pop") showed that a simple
53
+ * prompt prefix significantly improved emoji quality.
54
+ */
55
+ export const EMOJI_PROMPT_PREFIX = 'Generate a clean, simple, emoji-style icon of ';
56
+
57
+ /**
58
+ * Wrap the raw user prompt with the emoji-style prefix.
59
+ *
60
+ * @example
61
+ * wrapEmojiPrompt('a cat wearing a hard hat')
62
+ * // => 'Generate a clean, simple, emoji-style icon of a cat wearing a hard hat'
63
+ */
64
+ export const wrapEmojiPrompt = userPrompt => `${EMOJI_PROMPT_PREFIX}${userPrompt.trim()}`;
@@ -9,7 +9,7 @@ const createEvent = (eventType, action, actionSubject, actionSubjectId, attribut
9
9
  actionSubjectId,
10
10
  attributes: {
11
11
  packageName: "@atlaskit/emoji",
12
- packageVersion: "71.6.5",
12
+ packageVersion: "71.7.0",
13
13
  ...attributes
14
14
  }
15
15
  });
@@ -91,6 +91,10 @@ export const uploadConfirmButton = attributes => emojiUploaderEvent('clicked', '
91
91
  export const uploadCancelButton = () => emojiUploaderEvent('clicked', 'cancelButton');
92
92
  export const uploadSucceededEvent = attributes => createEvent('operational', 'finished', 'emojiUploader', undefined, attributes);
93
93
  export const uploadFailedEvent = attributes => createEvent('operational', 'failed', 'emojiUploader', undefined, attributes);
94
+ const aiEmojiGenerationEvent = (action, actionSubjectId, attributes) => createEvent('ui', action, 'emojiPickerAiGeneration', actionSubjectId, attributes);
95
+ export const aiGenerationStartedEvent = attributes => aiEmojiGenerationEvent('started', 'generateButton', attributes);
96
+ export const aiGenerationCompletedEvent = attributes => createEvent('operational', 'completed', 'emojiPickerAiGeneration', undefined, attributes);
97
+ export const aiGenerationFailedEvent = attributes => createEvent('operational', 'failed', 'emojiPickerAiGeneration', undefined, attributes);
94
98
  export const deleteBeginEvent = attributes => createEvent('ui', 'clicked', 'emojiPicker', 'deleteEmojiTrigger', attributes);
95
99
  export const deleteConfirmEvent = attributes => createEvent('ui', 'clicked', 'emojiPicker', 'deleteEmojiConfirm', attributes);
96
100
  export const deleteCancelEvent = attributes => createEvent('ui', 'clicked', 'emojiPicker', 'deleteEmojiCancel', attributes);
@@ -1,4 +1,4 @@
1
1
  export { ufoExperiencesSampled, clearSampled, isExperienceSampled, withSampling } from './samplingUfo';
2
- export { categoryClickedEvent, createAndFireEventInElementsChannel, closedPickerEvent, deleteBeginEvent, deleteCancelEvent, deleteConfirmEvent, recordFailed, recordFailedEmoji, recordSucceeded, recordSucceededEmoji, openedPickerEvent, pickerClickedEvent, pickerSearchedEvent, recordSelectionFailedSli, recordSelectionSucceededSli, selectedFileEvent, toneSelectedEvent, toneSelectorClosedEvent, toneSelectorOpenedEvent, typeaheadCancelledEvent, typeaheadRenderedEvent, typeaheadSelectedEvent, uploadBeginButton, uploadCancelButton, uploadConfirmButton, uploadFailedEvent, uploadSucceededEvent } from './analytics';
2
+ export { aiGenerationStartedEvent, aiGenerationCompletedEvent, aiGenerationFailedEvent, categoryClickedEvent, createAndFireEventInElementsChannel, closedPickerEvent, deleteBeginEvent, deleteCancelEvent, deleteConfirmEvent, recordFailed, recordFailedEmoji, recordSucceeded, recordSucceededEmoji, openedPickerEvent, pickerClickedEvent, pickerSearchedEvent, recordSelectionFailedSli, recordSelectionSucceededSli, selectedFileEvent, toneSelectedEvent, toneSelectorClosedEvent, toneSelectorOpenedEvent, typeaheadCancelledEvent, typeaheadRenderedEvent, typeaheadSelectedEvent, uploadBeginButton, uploadCancelButton, uploadConfirmButton, uploadFailedEvent, uploadSucceededEvent } from './analytics';
3
3
  export { sampledUfoRenderedEmoji, ufoExperiences } from './ufoExperiences';
4
4
  export { useSampledUFOComponentExperience } from './useSampledUFOComponentExperience';
@@ -780,7 +780,7 @@ var UploadingEmojiResource = /*#__PURE__*/function (_EmojiResource2) {
780
780
  value: function uploadCustomEmoji(upload) {
781
781
  var _this12 = this;
782
782
  var retry = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
783
- var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : fg('increase-emoji-client-upload-timeout') ? 30000 : 12000;
783
+ var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 30000;
784
784
  return this.isUploadSupported().then(function (supported) {
785
785
  if (!supported || !_this12.isRepositoryAvailable(_this12.siteEmojiResource)) {
786
786
  return Promise.reject('No media api support is configured');
@@ -0,0 +1,94 @@
1
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
+ import _regeneratorRuntime from "@babel/runtime/regenerator";
3
+ /**
4
+ * Calls the Confluence "header image" AI backend to generate an emoji-style
5
+ * image (base64) from a text description.
6
+ */
7
+
8
+ import { wrapEmojiPrompt } from '../../util/ai-emoji';
9
+ import debug from '../../util/logger';
10
+ var HEADER_IMAGE_GENERATION_ENDPOINT = '/gateway/api/assist/api/ai/v2/ai-feature/confluence-header-image-generation';
11
+
12
+ /** Emojis are square, so we always request a 1:1 image from the BE. */
13
+ var EMOJI_ASPECT_RATIO = '1:1';
14
+
15
+ /** Shape of the `ai_feature_output` envelope returned by the BE. */
16
+
17
+ /**
18
+ * Generate an emoji-style image and return its base64 image data.
19
+ *
20
+ * @throws Error if the request fails, the BE returns a body-level error, or no
21
+ * image data is returned.
22
+ */
23
+ export var generateEmojiImage = /*#__PURE__*/function () {
24
+ var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(_ref) {
25
+ var contentId, prompt, wrappedPrompt, response, json, output;
26
+ return _regeneratorRuntime.wrap(function (_context) {
27
+ while (1) switch (_context.prev = _context.next) {
28
+ case 0:
29
+ contentId = _ref.contentId, prompt = _ref.prompt;
30
+ wrappedPrompt = wrapEmojiPrompt(prompt);
31
+ _context.next = 1;
32
+ return fetch(HEADER_IMAGE_GENERATION_ENDPOINT, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'content-type': 'application/json;charset=UTF-8',
36
+ 'X-Experience-Id': 'confluence-ai-first-creation',
37
+ 'X-Product': 'confluence'
38
+ },
39
+ body: JSON.stringify({
40
+ ai_feature_input: {
41
+ adfContent: wrappedPrompt,
42
+ contentId: contentId,
43
+ useRawPrompt: true,
44
+ // Emojis are square — request a 1:1 image from the BE.
45
+ aspectRatio: EMOJI_ASPECT_RATIO
46
+ }
47
+ })
48
+ });
49
+ case 1:
50
+ response = _context.sent;
51
+ if (response.ok) {
52
+ _context.next = 2;
53
+ break;
54
+ }
55
+ debug('generateEmojiImage failed', response.status);
56
+ throw new Error("Emoji image generation failed with status ".concat(response.status));
57
+ case 2:
58
+ _context.next = 3;
59
+ return response.json();
60
+ case 3:
61
+ json = _context.sent;
62
+ output = json === null || json === void 0 ? void 0 : json.ai_feature_output;
63
+ if (output) {
64
+ _context.next = 4;
65
+ break;
66
+ }
67
+ throw new Error('Emoji image generation returned no content');
68
+ case 4:
69
+ if (!output.error) {
70
+ _context.next = 5;
71
+ break;
72
+ }
73
+ throw new Error(output.error.error_message || output.error.error_reason || 'Emoji image generation failed');
74
+ case 5:
75
+ if (output.imageData) {
76
+ _context.next = 6;
77
+ break;
78
+ }
79
+ throw new Error('Emoji image generation did not return image data');
80
+ case 6:
81
+ return _context.abrupt("return", {
82
+ imageData: output.imageData,
83
+ mediaFileId: output.mediaFileId
84
+ });
85
+ case 7:
86
+ case "end":
87
+ return _context.stop();
88
+ }
89
+ }, _callee);
90
+ }));
91
+ return function generateEmojiImage(_x) {
92
+ return _ref2.apply(this, arguments);
93
+ };
94
+ }();
@@ -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,141 @@
1
+ /* CreateEmojiWithRovo.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
3
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
4
+ import "./CreateEmojiWithRovo.compiled.css";
5
+ import * as React from 'react';
6
+ import { ax, ix } from "@compiled/react/runtime";
7
+ import _regeneratorRuntime from "@babel/runtime/regenerator";
8
+ import { useCallback, useState } from 'react';
9
+ import { FormattedMessage, useIntl } from 'react-intl';
10
+ import { IconButton } from '@atlaskit/button/new';
11
+ import TextField from '@atlaskit/textfield';
12
+ import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
13
+ import { RovoIcon } from '@atlaskit/logo';
14
+ import { Text } from '@atlaskit/primitives/compiled';
15
+ import { generateEmojiImage } from '../../api/ai/generateEmojiImage';
16
+ import { slugifyPrompt } from '../../util/ai-emoji';
17
+ import { aiGenerationStartedEvent, aiGenerationCompletedEvent, aiGenerationFailedEvent } from '../../util/analytics';
18
+ import { messages } from '../i18n';
19
+ import EmojiErrorMessage from './EmojiErrorMessage';
20
+ export var createEmojiWithRovoTestId = 'create-emoji-with-rovo';
21
+ export var createEmojiWithRovoPromptTestId = 'create-emoji-with-rovo-prompt';
22
+ export var createEmojiWithRovoGenerateTestId = 'create-emoji-with-rovo-generate';
23
+ var sectionStyles = null;
24
+ var headerStyles = null;
25
+ var promptRowStyles = null;
26
+ var promptInputStyles = null;
27
+ var CreateEmojiWithRovo = function CreateEmojiWithRovo(props) {
28
+ var contentId = props.contentId,
29
+ fireAnalytics = props.fireAnalytics,
30
+ onEmojiGenerated = props.onEmojiGenerated;
31
+ var _useIntl = useIntl(),
32
+ formatMessage = _useIntl.formatMessage;
33
+ var _useState = useState(''),
34
+ _useState2 = _slicedToArray(_useState, 2),
35
+ prompt = _useState2[0],
36
+ setPrompt = _useState2[1];
37
+ var _useState3 = useState(false),
38
+ _useState4 = _slicedToArray(_useState3, 2),
39
+ isGenerating = _useState4[0],
40
+ setIsGenerating = _useState4[1];
41
+ var _useState5 = useState(false),
42
+ _useState6 = _slicedToArray(_useState5, 2),
43
+ hasError = _useState6[0],
44
+ setHasError = _useState6[1];
45
+ var onPromptChange = useCallback(function (event) {
46
+ setPrompt(event.target.value);
47
+ // Clear any previous error as soon as the user edits the prompt to retry.
48
+ setHasError(false);
49
+ }, []);
50
+ var onGenerate = useCallback( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
51
+ var trimmedPrompt, startTime, _yield$generateEmojiI, imageData, dataURL, suggestedName, _t;
52
+ return _regeneratorRuntime.wrap(function (_context) {
53
+ while (1) switch (_context.prev = _context.next) {
54
+ case 0:
55
+ trimmedPrompt = prompt.trim();
56
+ if (!(!trimmedPrompt || isGenerating)) {
57
+ _context.next = 1;
58
+ break;
59
+ }
60
+ return _context.abrupt("return");
61
+ case 1:
62
+ setHasError(false);
63
+ setIsGenerating(true);
64
+ fireAnalytics === null || fireAnalytics === void 0 || fireAnalytics(aiGenerationStartedEvent({
65
+ promptLength: trimmedPrompt.length
66
+ }));
67
+ startTime = Date.now();
68
+ _context.prev = 2;
69
+ _context.next = 3;
70
+ return generateEmojiImage({
71
+ contentId: contentId,
72
+ prompt: trimmedPrompt
73
+ });
74
+ case 3:
75
+ _yield$generateEmojiI = _context.sent;
76
+ imageData = _yield$generateEmojiI.imageData;
77
+ dataURL = "data:image/png;base64,".concat(imageData);
78
+ suggestedName = slugifyPrompt(trimmedPrompt);
79
+ fireAnalytics === null || fireAnalytics === void 0 || fireAnalytics(aiGenerationCompletedEvent({
80
+ duration: Date.now() - startTime
81
+ }));
82
+ // Hand the generated image to the parent upload form.
83
+ onEmojiGenerated(dataURL, suggestedName);
84
+ _context.next = 5;
85
+ break;
86
+ case 4:
87
+ _context.prev = 4;
88
+ _t = _context["catch"](2);
89
+ setHasError(true);
90
+ fireAnalytics === null || fireAnalytics === void 0 || fireAnalytics(aiGenerationFailedEvent({
91
+ errorType: _t instanceof Error ? _t.message : 'generation_failed'
92
+ }));
93
+ case 5:
94
+ _context.prev = 5;
95
+ setIsGenerating(false);
96
+ return _context.finish(5);
97
+ case 6:
98
+ case "end":
99
+ return _context.stop();
100
+ }
101
+ }, _callee, null, [[2, 4, 5, 6]]);
102
+ })), [contentId, fireAnalytics, isGenerating, onEmojiGenerated, prompt]);
103
+ var generateDisabled = !prompt.trim() || isGenerating;
104
+ return /*#__PURE__*/React.createElement("div", {
105
+ "data-testid": createEmojiWithRovoTestId,
106
+ className: ax(["_zulpu2gc _x3doia51 _1e0c1txw _2lx21bp4 _ca0qutpp _19pku2gc"])
107
+ }, /*#__PURE__*/React.createElement("div", {
108
+ className: ax(["_zulp12x7 _1e0c1txw _4cvr1h6o"])
109
+ }, /*#__PURE__*/React.createElement(RovoIcon, {
110
+ appearance: "brand",
111
+ size: "xsmall",
112
+ label: formatMessage(messages.createEmojiWithRovoTitle)
113
+ }), /*#__PURE__*/React.createElement(Text, {
114
+ size: "medium",
115
+ weight: "bold"
116
+ }, /*#__PURE__*/React.createElement(FormattedMessage, messages.createEmojiWithRovoTitle))), /*#__PURE__*/React.createElement("div", {
117
+ className: ax(["_zulpu2gc _1e0c1txw _4cvr1y6m"])
118
+ }, /*#__PURE__*/React.createElement("span", {
119
+ className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug"])
120
+ }, /*#__PURE__*/React.createElement(TextField, {
121
+ value: prompt,
122
+ onChange: onPromptChange,
123
+ placeholder: formatMessage(messages.createEmojiWithRovoPromptPlaceholder),
124
+ "aria-label": formatMessage(messages.createEmojiWithRovoPromptAriaLabel),
125
+ isCompact: true,
126
+ isDisabled: isGenerating,
127
+ testId: createEmojiWithRovoPromptTestId
128
+ })), /*#__PURE__*/React.createElement(IconButton, {
129
+ appearance: "primary",
130
+ icon: ArrowUpIcon,
131
+ label: formatMessage(messages.createEmojiWithRovoGenerateLabel),
132
+ isDisabled: generateDisabled,
133
+ isLoading: isGenerating,
134
+ onClick: onGenerate,
135
+ testId: createEmojiWithRovoGenerateTestId
136
+ })), hasError && /*#__PURE__*/React.createElement(EmojiErrorMessage, {
137
+ errorStyle: "chooseFile",
138
+ message: /*#__PURE__*/React.createElement(FormattedMessage, messages.createEmojiWithRovoError)
139
+ }));
140
+ };
141
+ export default CreateEmojiWithRovo;
@@ -317,7 +317,9 @@ export var EmojiActions = function EmojiActions(props) {
317
317
  onUploadEmoji: onUploadEmoji,
318
318
  onFileChooserClicked: onFileChooserClicked,
319
319
  errorMessage: uploadErrorMessage,
320
- initialUploadName: initialUploadName
320
+ initialUploadName: initialUploadName,
321
+ contentId: props.contentId,
322
+ fireAnalytics: props.fireAnalytics
321
323
  })) : /*#__PURE__*/React.createElement("div", {
322
324
  className: ax(["_16jlidpf _1o9zidpf _i0dl1wug _n7zl1uh4 _16qsjgpa"])
323
325
  }, /*#__PURE__*/React.createElement(EmojiUploadPicker, {
@@ -325,7 +327,9 @@ export var EmojiActions = function EmojiActions(props) {
325
327
  onUploadEmoji: onUploadEmoji,
326
328
  onFileChooserClicked: onFileChooserClicked,
327
329
  errorMessage: uploadErrorMessage,
328
- initialUploadName: initialUploadName
330
+ initialUploadName: initialUploadName,
331
+ contentId: props.contentId,
332
+ fireAnalytics: props.fireAnalytics
329
333
  }));
330
334
  }
331
335
  if (emojiToDelete) {
@@ -13,6 +13,7 @@ import AkButton from '@atlaskit/button/standard-button';
13
13
  import { Text } from '@atlaskit/primitives/compiled';
14
14
  import FocusLock from 'react-focus-lock';
15
15
  import * as ImageUtil from '../../util/image';
16
+ import CreateEmojiWithRovo from './CreateEmojiWithRovo';
16
17
  import debug from '../../util/logger';
17
18
  import { messages } from '../i18n';
18
19
  import EmojiErrorMessage from './EmojiErrorMessage';
@@ -21,6 +22,7 @@ import FileChooser from './FileChooser';
21
22
  import { UploadStatus } from './internal-types';
22
23
  import { fg } from '@atlaskit/platform-feature-flags';
23
24
  import FeatureGates from '@atlaskit/feature-gate-js-client';
25
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
24
26
  import Button from '@atlaskit/button/new';
25
27
  import { Box } from '@atlaskit/primitives/compiled';
26
28
  import { getDocument } from '@atlaskit/browser-apis';
@@ -91,6 +93,7 @@ var ChooseEmojiFile = /*#__PURE__*/memo(function (props) {
91
93
  nameErrorMessage = props.nameErrorMessage,
92
94
  previewImage = props.previewImage,
93
95
  uploadStatus = props.uploadStatus,
96
+ aiSection = props.aiSection,
94
97
  intl = props.intl;
95
98
  var formatMessage = intl.formatMessage;
96
99
  var disableChooser = !name;
@@ -139,7 +142,7 @@ var ChooseEmojiFile = /*#__PURE__*/memo(function (props) {
139
142
  }, errorMessage && /*#__PURE__*/React.createElement(EmojiErrorMessage, {
140
143
  errorStyle: "chooseFile",
141
144
  message: errorMessage
142
- }))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
145
+ }))), aiSection, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
143
146
  htmlFor: "new-emoji-name-input",
144
147
  className: ax(["_1w90azsu", "_11c8wadc _k48p1pd9"])
145
148
  }, /*#__PURE__*/React.createElement(FormattedMessage, messages.emojiNameLabel)), /*#__PURE__*/React.createElement(TextField, {
@@ -237,6 +240,8 @@ var EmojiUploadPicker = function EmojiUploadPicker(props) {
237
240
  onUploadCancelled = props.onUploadCancelled,
238
241
  _props$disableFocusLo = props.disableFocusLock,
239
242
  disableFocusLock = _props$disableFocusLo === void 0 ? false : _props$disableFocusLo,
243
+ contentId = props.contentId,
244
+ fireAnalytics = props.fireAnalytics,
240
245
  intl = props.intl;
241
246
  var _useState = useState(errorMessage ? UploadStatus.Error : UploadStatus.Waiting),
242
247
  _useState2 = _slicedToArray(_useState, 2),
@@ -320,6 +325,18 @@ var EmojiUploadPicker = function EmojiUploadPicker(props) {
320
325
  var cancelChooseFile = useCallback(function () {
321
326
  setPreviewImage(undefined);
322
327
  }, []);
328
+
329
+ // When the Rovo section generates an image, feed it into the existing upload
330
+ // form (same preview, name field and "Add emoji" button as a manual upload).
331
+ var onEmojiGenerated = useCallback(function (dataURL, suggestedName) {
332
+ setFilename('rovo-emoji.png');
333
+ setPreviewImage(dataURL);
334
+ setChooseEmojiErrorMessage(undefined);
335
+ // Only auto-populate the name if the user hasn't already typed one.
336
+ setName(function (current) {
337
+ return current || sanitizeName(suggestedName);
338
+ });
339
+ }, []);
323
340
  var errorOnUpload = useCallback(function (event) {
324
341
  debug('File load error: ', event);
325
342
  setChooseEmojiErrorMessage( /*#__PURE__*/React.createElement(FormattedMessage, messages.emojiUploadFailed));
@@ -403,6 +420,17 @@ var EmojiUploadPicker = function EmojiUploadPicker(props) {
403
420
  onFileChooserClicked && onFileChooserClicked();
404
421
  };
405
422
  var isDuplicateNameError = errorMessage !== null && errorMessage !== undefined && isRefreshEmojiPickerEnabled();
423
+
424
+ // "Create an emoji with Rovo" AI generation section. Only rendered when the
425
+ // experiment is on AND a page content id is available (the image generation
426
+ // backend requires it). It is rendered inside ChooseEmojiFile (between the
427
+ // drop area and the Emoji name field) so the generated image reuses the
428
+ // single shared name field and "Add emoji" button.
429
+ var aiSection = contentId && expValEquals('confluence_ai_generated_emojis', 'isEnabled', true) ? /*#__PURE__*/React.createElement(CreateEmojiWithRovo, {
430
+ contentId: contentId,
431
+ fireAnalytics: fireAnalytics,
432
+ onEmojiGenerated: onEmojiGenerated
433
+ }) : null;
406
434
  var content = name && previewImage && !isRefreshEmojiPickerEnabled() ? /*#__PURE__*/React.createElement(EmojiUploadPreview, {
407
435
  errorMessage: errorMessage,
408
436
  name: name,
@@ -421,6 +449,7 @@ var EmojiUploadPicker = function EmojiUploadPicker(props) {
421
449
  uploadStatus: uploadStatus,
422
450
  errorMessage: chooseEmojiErrorMessage,
423
451
  nameErrorMessage: isDuplicateNameError ? errorMessage : undefined,
452
+ aiSection: aiSection,
424
453
  intl: intl
425
454
  });
426
455
  return disableFocusLock || isRefreshEmojiPickerEnabled() ? content : /*#__PURE__*/React.createElement(FocusLock, {
@@ -100,6 +100,31 @@ export var 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
  ._1bsb10mj{width:var(--_gsvyy7)}
9
10
  ._1e0c1txw{display:flex}
@@ -27,11 +28,13 @@
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
  ._4t3iuxo9{height:var(--_19dn98e)}
32
34
  ._4t3ivixp{height:349px}
33
35
  ._4t3ixt2k{height:509px}
34
36
  ._bfhk1bhr{background-color:var(--ds-surface-overlay,#fff)}
35
37
  ._c71l1y6z{max-height:calc(80vh - 86px)}
38
+ ._c71l8k0t{max-height:calc(100vh - 86px)}
36
39
  ._i0dlf1ug{flex-basis:0%}
37
40
  ._kqswh2mm{position:relative}
@@ -35,6 +35,7 @@ import { useIsMounted } from '../../hooks/useIsMounted';
35
35
  import { messages } from '../i18n';
36
36
  import { defaultProductivityColor, getStoredProductivityColor, storeProductivityColor } from '../../util/productivity-colors';
37
37
  import { filterHiddenEmojis } from '../../util/hidden-emojis';
38
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
38
39
  var isRefreshEmojiPickerEnabled = function isRefreshEmojiPickerEnabled() {
39
40
  if (!FeatureGates.initializeCompleted()) {
40
41
  return false;
@@ -55,6 +56,17 @@ var heightOffset = 80;
55
56
  var emojiPicker = null;
56
57
  var emojiPickerNew = null;
57
58
  var emojiPickerWrapper = null;
59
+
60
+ // When the "Create an emoji with Rovo" section is shown in the upload panel the
61
+ // picker needs more vertical room than the fixed upload height. Make it tall
62
+ // enough to fit the whole section without scrolling, but still let it scroll as
63
+ // a safety net on very short viewports.
64
+ var emojiPickerWrapperScrollable = null;
65
+
66
+ // When the AI emoji experiment is on, grow the upload panel from its default
67
+ // height to 450px so the manual upload form + the slim "Create an emoji with
68
+ // Rovo" section are both visible without scrolling.
69
+ var withAiUploadHeight = null;
58
70
  var withPreviewHeight = {
59
71
  small: "_4t3ivixp _1tkegx0z",
60
72
  medium: "_4t3i2300 _1tke5x59",
@@ -87,6 +99,7 @@ var EmojiPickerComponent = function EmojiPickerComponent(_ref) {
87
99
  onPickerRef = _ref.onPickerRef,
88
100
  hideToneSelector = _ref.hideToneSelector,
89
101
  createAnalyticsEvent = _ref.createAnalyticsEvent,
102
+ contentId = _ref.contentId,
90
103
  _ref$size = _ref.size,
91
104
  size = _ref$size === void 0 ? defaultEmojiPickerSize : _ref$size;
92
105
  var _useIntl = useIntl(),
@@ -660,13 +673,17 @@ var EmojiPickerComponent = function EmojiPickerComponent(_ref) {
660
673
  var showPreview = isRefreshEmojiPickerEnabled() ? !uploading : selectedEmoji && !uploading;
661
674
  var shouldRenderFooter = showPreview && !(emojiToDelete && isRefreshEmojiPickerEnabled()) && !(query && filteredEmojis.length === 0 && isRefreshEmojiPickerEnabled()) && (Boolean(selectedEmoji) || uploadEnabled || !isRefreshEmojiPickerEnabled());
662
675
  var useFooterSpaceForList = showPreview && !selectedEmoji && !uploadEnabled && !emojiToDelete && !(query && filteredEmojis.length === 0) && isRefreshEmojiPickerEnabled();
676
+
677
+ // When the AI emoji section is shown in the upload panel, grow the picker so
678
+ // the section isn't clipped by the fixed upload height.
679
+ var showAiUpload = uploading && !!contentId && expValEquals('confluence_ai_generated_emojis', 'isEnabled', true);
663
680
  return /*#__PURE__*/React.createElement("div", {
664
681
  ref: setPickerRef,
665
682
  "data-emoji-picker-container": true,
666
683
  role: "dialog",
667
684
  "aria-label": formatMessage(messages.emojiPickerTitle),
668
685
  "aria-modal": true,
669
- className: ax([isRefreshEmojiPickerEnabled() ? "_19itahnd _2rko1mok _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iuxo9 _1bsb10mj _1ul910mj _c71l1y6z _kqswh2mm" : "_19itahnd _2rkofajl _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iuxo9 _1bsb10mj _1ul910mj _c71l1y6z _kqswh2mm", !!emojiToDelete && isRefreshEmojiPickerEnabled() ? withDeleteRefreshHeight[size] : uploading && isRefreshEmojiPickerEnabled() ? withUploadRefreshHeight[size] : query && filteredEmojis.length === 0 && isRefreshEmojiPickerEnabled() ? withNoResultsRefreshHeight[size] : showPreview ? withPreviewHeight[size] : withoutPreviewHeight[size]]),
686
+ className: ax([isRefreshEmojiPickerEnabled() ? "_19itahnd _2rko1mok _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iuxo9 _1bsb10mj _1ul910mj _c71l1y6z _kqswh2mm" : "_19itahnd _2rkofajl _1e0c1txw _2lx21bp4 _1bah1yb4 _bfhk1bhr _16qs130s _4t3iuxo9 _1bsb10mj _1ul910mj _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"]),
670
687
  style: {
671
688
  "--_19dn98e": ix("".concat(emojiPickerHeight, "px")),
672
689
  "--_gsvyy7": ix("".concat(emojiPickerWidth, "px"))
@@ -676,7 +693,7 @@ var EmojiPickerComponent = function EmojiPickerComponent(_ref) {
676
693
  onKeyPress: suppressKeyPress,
677
694
  onKeyUp: suppressKeyPress,
678
695
  onKeyDown: suppressKeyPress,
679
- className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug _1reo15vq _18m915vq _1e0c1txw _2lx21bp4 _1bah1yb4 _1tkeidpf"])
696
+ className: ax(["_16jlkb7n _1o9zkb7n _i0dlf1ug _1reo15vq _18m915vq _1e0c1txw _2lx21bp4 _1bah1yb4 _1tkeidpf", showAiUpload && "_1reo15vq _18m91wug"])
680
697
  }, /*#__PURE__*/React.createElement(CategorySelector, {
681
698
  activeCategoryId: (uploading || emojiToDelete) && isRefreshEmojiPickerEnabled() ? null : activeCategory,
682
699
  dynamicCategories: dynamicCategories,
@@ -713,7 +730,9 @@ var EmojiPickerComponent = function EmojiPickerComponent(_ref) {
713
730
  onOpenUpload: onOpenUpload,
714
731
  size: size,
715
732
  activeCategoryId: activeCategory,
716
- useFooterSpaceForList: useFooterSpaceForList
733
+ useFooterSpaceForList: useFooterSpaceForList,
734
+ contentId: contentId,
735
+ fireAnalytics: fireAnalytics
717
736
  }), shouldRenderFooter && /*#__PURE__*/React.createElement(EmojiPickerFooter, {
718
737
  selectedEmoji: selectedEmoji,
719
738
  uploadEnabled: uploadEnabled,
@@ -86,7 +86,9 @@ export var EmojiPickerVirtualListInternal = /*#__PURE__*/React.forwardRef(functi
86
86
  onProductivityColorSelected = props.onProductivityColorSelected,
87
87
  activeCategoryId = props.activeCategoryId,
88
88
  selectedProductivityColor = props.selectedProductivityColor,
89
- useFooterSpaceForList = props.useFooterSpaceForList;
89
+ useFooterSpaceForList = props.useFooterSpaceForList,
90
+ contentId = props.contentId,
91
+ fireAnalytics = props.fireAnalytics;
90
92
  var _useIntl = useIntl(),
91
93
  formatMessage = _useIntl.formatMessage;
92
94
  var listRef = useRef(null);
@@ -505,7 +507,9 @@ export var EmojiPickerVirtualListInternal = /*#__PURE__*/React.forwardRef(functi
505
507
  onProductivityColorSelected: isTeamojiExperimentEnabled ? onProductivityColorSelected : undefined,
506
508
  query: query,
507
509
  onChange: onSearch,
508
- resultsCount: visibleEmojis.length
510
+ resultsCount: visibleEmojis.length,
511
+ contentId: contentId,
512
+ fireAnalytics: fireAnalytics
509
513
  }), /*#__PURE__*/React.createElement(EmojiPickerListContextProvider, {
510
514
  initialEmojisFocus: {
511
515
  rowIndex: 1,
@@ -20,7 +20,8 @@ var EmojiUploadComponent = function EmojiUploadComponent(props) {
20
20
  var emojiProvider = props.emojiProvider,
21
21
  createAnalyticsEvent = props.createAnalyticsEvent,
22
22
  onUploaderRef = props.onUploaderRef,
23
- disableFocusLock = props.disableFocusLock;
23
+ disableFocusLock = props.disableFocusLock,
24
+ contentId = props.contentId;
24
25
  var _useState = useState(),
25
26
  _useState2 = _slicedToArray(_useState, 2),
26
27
  uploadErrorMessage = _useState2[0],
@@ -122,7 +123,9 @@ var EmojiUploadComponent = function EmojiUploadComponent(props) {
122
123
  onUploadCancelled: onUploadCancelled,
123
124
  onUploadEmoji: onUploadEmoji,
124
125
  errorMessage: uploadErrorMessage ? /*#__PURE__*/React.createElement(FormattedMessage, uploadErrorMessage) : null,
125
- disableFocusLock: disableFocusLock
126
+ disableFocusLock: disableFocusLock,
127
+ contentId: contentId,
128
+ fireAnalytics: fireAnalytics
126
129
  })));
127
130
  };
128
131
  var _default_1 = /*#__PURE__*/memo(EmojiUploadComponent);