contentoh-components-library 21.5.94 → 21.5.96

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 (70) hide show
  1. package/dist/ai/utils/compare-strings.js +45 -0
  2. package/dist/components/atoms/GeneralButton/styles.js +1 -1
  3. package/dist/components/atoms/GeneralInput/index.js +249 -54
  4. package/dist/components/atoms/GeneralInput/styles.js +7 -3
  5. package/dist/components/atoms/InputFormatter/index.js +223 -68
  6. package/dist/components/atoms/InputFormatter/styles.js +20 -4
  7. package/dist/components/molecules/StatusAsignationInfo/index.js +11 -1
  8. package/dist/components/molecules/TabsMenu/index.js +13 -1
  9. package/dist/components/molecules/TagAndInput/index.js +364 -24
  10. package/dist/components/molecules/TagAndInput/styles.js +2 -2
  11. package/dist/components/organisms/ChangeStatusModal/index.js +531 -0
  12. package/dist/components/organisms/ChangeStatusModal/styles.js +85 -0
  13. package/dist/components/organisms/FullProductNameHeader/index.js +6 -22
  14. package/dist/components/organisms/InputGroup/index.js +22 -18
  15. package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +150 -337
  16. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +15 -15
  17. package/dist/components/pages/ProviderProductEdition/index.js +395 -361
  18. package/dist/components/pages/ProviderProductEdition/utils.js +1 -0
  19. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +125 -211
  20. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +59 -260
  21. package/dist/components/pages/RetailerProductEdition/context/reducers/product.js +50 -38
  22. package/dist/components/pages/RetailerProductEdition/index.js +1741 -2239
  23. package/dist/components/pages/RetailerProductEdition/styles.js +4 -2
  24. package/dist/components/pages/RetailerProductEdition/utils.js +251 -2
  25. package/dist/contexts/AiProductEdition.js +244 -160
  26. package/dist/global-files/statusDictionary.js +103 -0
  27. package/package.json +4 -2
  28. package/src/ai/utils/compare-strings.js +45 -0
  29. package/src/assets/images/Icons/arrow.png +0 -0
  30. package/src/assets/images/Icons/cancel.png +0 -0
  31. package/src/assets/images/Icons/ia-icon.png +0 -0
  32. package/src/assets/images/Icons/loading.svg +5 -0
  33. package/src/assets/images/Icons/reload.png +0 -0
  34. package/src/components/atoms/GeneralButton/styles.js +4 -0
  35. package/src/components/atoms/GeneralInput/index.js +241 -60
  36. package/src/components/atoms/GeneralInput/styles.js +81 -0
  37. package/src/components/atoms/InputFormatter/index.js +200 -51
  38. package/src/components/atoms/InputFormatter/styles.js +284 -0
  39. package/src/components/atoms/RetailerSelector/RetailerSelector.stories.js +10 -0
  40. package/src/components/atoms/RetailerSelector/index.js +3 -0
  41. package/src/components/atoms/RetailerSelector/styles.js +0 -0
  42. package/src/components/molecules/StatusAsignationInfo/index.js +9 -1
  43. package/src/components/molecules/TabsMenu/index.js +12 -0
  44. package/src/components/molecules/TagAndInput/index.js +294 -21
  45. package/src/components/molecules/TagAndInput/styles.js +59 -17
  46. package/src/components/organisms/ChangeStatusModal/index.jsx +488 -0
  47. package/src/components/organisms/ChangeStatusModal/styles.js +333 -0
  48. package/src/components/organisms/FullProductNameHeader/index.js +4 -28
  49. package/src/components/organisms/FullTabsMenu/index.js +1 -1
  50. package/src/components/organisms/InputGroup/index.js +12 -4
  51. package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +174 -202
  52. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +14 -14
  53. package/src/components/pages/ProviderProductEdition/index.js +497 -437
  54. package/src/components/pages/ProviderProductEdition/utils.js +2 -2
  55. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +136 -243
  56. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +575 -0
  57. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +62 -0
  58. package/src/components/pages/RetailerProductEdition/context/reducers/active-state.js +344 -0
  59. package/src/components/pages/RetailerProductEdition/context/reducers/inputs.js +155 -0
  60. package/src/components/pages/RetailerProductEdition/context/reducers/product.js +114 -0
  61. package/src/components/pages/RetailerProductEdition/context/reducers/system.js +60 -0
  62. package/src/components/pages/RetailerProductEdition/index.js +1563 -1717
  63. package/src/components/pages/RetailerProductEdition/index_old.js +1979 -0
  64. package/src/components/pages/RetailerProductEdition/stories/Auditor.stories.js +101 -0
  65. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +115 -0
  66. package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +174 -0
  67. package/src/components/pages/RetailerProductEdition/styles.js +67 -2
  68. package/src/components/pages/RetailerProductEdition/utils.js +240 -0
  69. package/src/contexts/AiProductEdition.jsx +347 -0
  70. package/src/global-files/statusDictionary.js +103 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.5.94",
3
+ "version": "21.5.96",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -12,6 +12,7 @@
12
12
  "@fortawesome/free-regular-svg-icons": "^6.2.0",
13
13
  "@fortawesome/free-solid-svg-icons": "^6.2.0",
14
14
  "@fortawesome/react-fontawesome": "^0.2.0",
15
+ "@google/genai": "^1.35.0",
15
16
  "@mui/icons-material": "^5.11.16",
16
17
  "@mui/material": "^5.12.0",
17
18
  "@mui/styled-engine-sc": "^5.12.0",
@@ -51,7 +52,8 @@
51
52
  "styled-components": "^5.3.9",
52
53
  "swiper": "^8.4.4",
53
54
  "uuid": "^8.3.2",
54
- "web-vitals": "^1.0.1"
55
+ "web-vitals": "^1.0.1",
56
+ "zod": "^4.3.5"
55
57
  },
56
58
  "scripts": {
57
59
  "start": "start-storybook -p 6006",
@@ -0,0 +1,45 @@
1
+
2
+ //Calcula el porcentaje de similitud entre la descripción generada por IA y la versión editada por el usuario.
3
+
4
+ export function getTextSimilarityPercentage(originalText, candidateText) {
5
+
6
+ if(!originalText || !candidateText) return;
7
+
8
+ const normalize = (text) => {
9
+ return text
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/\s+/g, ' ');
13
+ };
14
+
15
+ const source = normalize(originalText);
16
+ const target = normalize(candidateText);
17
+
18
+ if (source === target) return 100;
19
+ if (source.length === 0 || target.length === 0) return 0;
20
+
21
+ const sourceLength = source.length;
22
+ const targetLength = target.length;
23
+ const distanceMatrix = Array(targetLength + 1).fill(null).map(() => []);
24
+
25
+ for (let i = 0; i <= sourceLength; i++) distanceMatrix[0][i] = i;
26
+ for (let j = 0; j <= targetLength; j++) distanceMatrix[j][0] = j;
27
+
28
+ for (let j = 1; j <= targetLength; j++) {
29
+ for (let i = 1; i <= sourceLength; i++) {
30
+ const substitutionCost = (source[i - 1] === target[j - 1]) ? 0 : 1;
31
+
32
+ distanceMatrix[j][i] = Math.min(
33
+ distanceMatrix[j - 1][i] + 1,
34
+ distanceMatrix[j][i - 1] + 1,
35
+ distanceMatrix[j - 1][i - 1] + substitutionCost
36
+ );
37
+ }
38
+ }
39
+
40
+ const editDistance = distanceMatrix[targetLength][sourceLength];
41
+ const maxLength = Math.max(sourceLength, targetLength);
42
+ const similarityScore = 1 - (editDistance / maxLength);
43
+
44
+ return similarityScore * 100;
45
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="283" height="283" style="shape-rendering: auto; display: block; background: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g>
2
+ <path stroke-width="10" stroke="#e13aa7" fill="none" d="M50 27A23 23 0 1 0 69.9785248320784 38.604450626054636"></path>
3
+ <path fill="#e13aa7" d="M49 15L49 39L61 27L49 15"></path>
4
+ <animateTransform keyTimes="0;1" values="0 50 50;360 50 50" dur="0.9803921568627451s" repeatCount="indefinite" type="rotate" attributeName="transform"></animateTransform>
5
+ </g><g></g></g><!-- [ldio] generated by https://loading.io --></svg>
@@ -38,6 +38,10 @@ export const Container = styled.button`
38
38
  background-color: #603888;
39
39
  }
40
40
 
41
+ &.general-pink-button {
42
+ background-color: #E33AA9;
43
+ }
44
+
41
45
  &.general-transparent-button {
42
46
  background-color: transparent;
43
47
  border: 1px solid #503d66;
@@ -3,6 +3,16 @@ import { Container } from "./styles";
3
3
  import { InputFormatter } from "../InputFormatter";
4
4
  import { CheckBox } from "../CheckBox";
5
5
 
6
+ import AiGenerationIcon from "../../../assets/images/Icons/ia-icon.png";
7
+ import LoadingIcon from "../../../assets/images/Icons/loading.svg";
8
+ import ArrowIcon from "../../../assets/images/Icons/arrow.png";
9
+ import CancelIcon from "../../../assets/images/Icons/cancel.png";
10
+ import ReloadIcon from "../../../assets/images/Icons/reload.png";
11
+
12
+ import { useAiProductEdition } from "../../../contexts/AiProductEdition";
13
+ import { InputContainer, BottomContainer, OptionsContainer, ButtonsContainer } from "../InputFormatter/styles";
14
+ import { getTextSimilarityPercentage } from "../../../ai/utils/compare-strings";
15
+
6
16
  export const GeneralInput = ({
7
17
  inputType,
8
18
  inputId,
@@ -27,45 +37,68 @@ export const GeneralInput = ({
27
37
  disabled,
28
38
  onKeyDown,
29
39
  auditClass,
40
+
41
+ //AI Generation
42
+ hasAiGeneration = false,
43
+ isBenefitInput = false,
44
+ isAiGenerationLoading = false,
45
+ isAiRegenerationLoading=false,
46
+ isAiActive = false,
47
+ isAiAvailable = false,
48
+ aiGenerated = false,
49
+
50
+ setIsAiActive = () => {},
51
+ handlerAiGeneration = () => {},
52
+ handlerRegenerateSuggestions = () => {},
53
+ handleChangeSuggestion = () => {}
54
+
30
55
  }) => {
56
+
57
+ const {
58
+ isCreators,
59
+ suggestions,
60
+ currentSuggestion,
61
+ setCurrentSuggestionValue
62
+ } = useAiProductEdition();
63
+
31
64
  const [textValue, setTextValue] = useState({
32
65
  value: inputValue,
33
66
  });
34
67
  const [requiredEmpty, setRequiredEmpty] = useState(false);
35
68
 
36
- const onHandleChange = (evt) => {
37
- if (validateInput) {
38
- setTextValue({ value: validateInput(evt, position, inputsArray) });
39
- } else if (
40
- updatedDatasheets ||
41
- updatedDescriptions ||
42
- inputType === "textarea"
43
- ) {
44
- let generalValue;
45
- if (optionList?.length > 0) {
46
- let valueSelected = evt.target.value;
47
- generalValue = valueSelected;
48
- setTextValue({ value: generalValue });
49
- } else {
50
- generalValue =
51
- inputType === "checkbox" ? evt.target.checked : evt.target.value;
52
- setTextValue({
53
- value: generalValue,
54
- });
55
- }
56
- let dataSave = updatedDatasheets?.slice();
69
+ const [aiSuggestionAccepted, setAiSuggestionAccepted] = useState(false);
70
+ const [valueAccepted, setValueAccepted] = useState(inputValue);
71
+
72
+ const updateParentData = (generalValue, isAiAccepted, newBaseValue = null) => {
73
+ setTextValue({ value: generalValue });
74
+
75
+ const baseToCompare = newBaseValue !== null ? newBaseValue : valueAccepted;
76
+
77
+ const similarity = getTextSimilarityPercentage(baseToCompare, generalValue);
78
+
79
+ const generatedWithAi = (isAiAccepted || aiGenerated) && similarity >= 50;
80
+
81
+ if (updatedDatasheets || updatedDescriptions || inputType === "textarea") {
82
+ let dataSave = updatedDatasheets?.slice() || [];
57
83
 
58
84
  if (dataSave?.length > 0) {
59
85
  const index = dataSave.findIndex((e) => e.attributeId === inputId);
60
86
  if (index !== -1) {
61
- if (generalValue !== inputValue) dataSave[index].value = generalValue;
62
- else dataSave.splice(index, 1);
87
+ if (generalValue !== inputValue) {
88
+ dataSave[index].value = generalValue;
89
+ dataSave[index].aiSuggestionAccepted = generatedWithAi;
90
+ } else {
91
+ dataSave.splice(index, 1);
92
+ }
63
93
  } else {
64
- dataSave.push({
65
- articleId: articleId,
66
- attributeId: inputId,
67
- value: generalValue,
68
- });
94
+ if (generalValue !== inputValue) {
95
+ dataSave.push({
96
+ articleId: articleId,
97
+ attributeId: inputId,
98
+ value: generalValue,
99
+ aiSuggestionAccepted: generatedWithAi,
100
+ });
101
+ }
69
102
  }
70
103
  } else {
71
104
  if (generalValue !== inputValue) {
@@ -73,16 +106,72 @@ export const GeneralInput = ({
73
106
  articleId: articleId,
74
107
  attributeId: inputId,
75
108
  value: generalValue,
109
+ aiSuggestionAccepted: generatedWithAi,
76
110
  });
77
111
  }
78
112
  }
113
+
79
114
  setUpdatedDatasheets(dataSave);
115
+ }
116
+ };
117
+
118
+ const onHandleChange = (evt) => {
119
+ if (validateInput) {
120
+ setTextValue({ value: validateInput(evt, position, inputsArray) });
121
+ } else if (
122
+ updatedDatasheets ||
123
+ updatedDescriptions ||
124
+ inputType === "textarea"
125
+ ) {
126
+ let generalValue;
127
+ if (optionList?.length > 0) {
128
+ generalValue = evt.target.value;
129
+ } else {
130
+ generalValue =
131
+ inputType === "checkbox" ? evt.target.checked : evt.target.value;
132
+ }
133
+
134
+ updateParentData(generalValue, aiSuggestionAccepted);
135
+
80
136
  } else {
81
137
  setTextValue({ value: evt.target.value });
82
138
  inputOnChange && inputOnChange(evt);
83
139
  }
84
140
  };
85
141
 
142
+ useEffect(() => {
143
+
144
+ if(!isCreators) return;
145
+
146
+ if(Object.keys(suggestions).length === 0) return;
147
+
148
+ if(currentSuggestion?.[inputId]) return
149
+
150
+
151
+ const firstSuggestion = suggestions?.[inputId]?.[0];
152
+
153
+ if(!firstSuggestion) return;
154
+
155
+ setCurrentSuggestionValue({
156
+ inputId,
157
+ index: 0,
158
+ value: firstSuggestion?.value
159
+ });
160
+
161
+ }, [suggestions]);
162
+
163
+ useEffect(() => {
164
+ if(!isAiActive || !isCreators) return;
165
+ setAiSuggestionAccepted(false);
166
+ }, [isAiActive]);
167
+
168
+ useEffect(() => {
169
+ if(!isCreators) return;
170
+ if(!isAiActive && !aiSuggestionAccepted) {
171
+ setTextValue({ value: valueAccepted });
172
+ }
173
+ }, [suggestions, isAiActive]);
174
+
86
175
  useEffect(() => {
87
176
  setRequiredEmpty(
88
177
  isRequired &&
@@ -90,14 +179,26 @@ export const GeneralInput = ({
90
179
  );
91
180
  }, [textValue]);
92
181
 
93
- const numberInputOnWheelPreventChange = (e) => {
94
- // Prevent the input value change
95
- e.target.blur();
96
- // Prevent the page/container scrolling
97
- e.stopPropagation();
98
- // Refocus immediately, on the next tick (after the current function is done)
99
- setTimeout(() => e.target.focus(), 0);
100
- };
182
+ // const numberInputOnWheelPreventChange = (e) => {
183
+ // // Prevent the input value change
184
+ // e.target.blur();
185
+ // // Prevent the page/container scrolling
186
+ // e.stopPropagation();
187
+ // // Refocus immediately, on the next tick (after the current function is done)
188
+ // setTimeout(() => e.target.focus(), 0);
189
+ // };
190
+
191
+ //AI Generation
192
+
193
+ const handleAcceptSuggestion = (suggestionValue) => {
194
+ if(!suggestionValue || !isCreators) return;
195
+
196
+ setValueAccepted(suggestionValue);
197
+ setAiSuggestionAccepted(true);
198
+ setIsAiActive(false);
199
+
200
+ updateParentData(suggestionValue, true, suggestionValue);
201
+ }
101
202
 
102
203
  return (
103
204
  <Container isRequired={requiredEmpty} className={auditClass}>
@@ -124,33 +225,113 @@ export const GeneralInput = ({
124
225
  disabled={disabled}
125
226
  />
126
227
  ) : inputType !== "textarea" ? (
127
- <input
128
- type={inputType}
129
- disabled={disabled}
130
- id={inputId}
131
- size={inputSize}
132
- className="general-input"
133
- placeholder={inputPlaceholder}
134
- value={textValue.value}
135
- onInput={(e) => onHandleChange(e)}
136
- maxLength={maxChar}
137
- required={isRequired}
138
- onKeyDown={onKeyDown}
139
- onWheel={numberInputOnWheelPreventChange}
140
- />
228
+ <div>
229
+ <InputContainer className={hasAiGeneration ? "ai-generation" : ""}>
230
+ <input
231
+ type={inputType}
232
+ disabled={disabled}
233
+ id={inputId}
234
+ size={inputSize}
235
+ className={`general-input ${isAiActive && isBenefitInput && "ia-input"}`}
236
+ placeholder={inputPlaceholder}
237
+ value={isAiActive ? (
238
+ currentSuggestion?.[inputId]?.value
239
+ ) : textValue.value}
240
+ onInput={(e) => onHandleChange(e)}
241
+ maxLength={maxChar}
242
+ required={isRequired}
243
+ onKeyDown={onKeyDown}
244
+ // onWheel={numberInputOnWheelPreventChange}
245
+ />
246
+ {
247
+
248
+ hasAiGeneration && isBenefitInput && (
249
+ <div className={`icon_container ${isAiAvailable ? "ai-available" : ''} ${isAiActive ? 'ai-active' : ''}`} title={!isAiAvailable ? 'Debes de completar ficha técnica e imágenes para desbloquear la generación con IA' : ''} onClick={() => {
250
+ handlerAiGeneration({ type: "attribute" })
251
+ }}>
252
+ <img className={`${isAiGenerationLoading ? 'loading' : ''}`} src={isAiGenerationLoading ? LoadingIcon : isAiActive ? CancelIcon : AiGenerationIcon} />
253
+ </div>
254
+ )
255
+
256
+ }
257
+ </InputContainer>
258
+ <BottomContainer className={isAiActive ? "with-ai" : ""}>
259
+ {
260
+ isAiActive && (
261
+ <div className="ai-options">
262
+
263
+ <OptionsContainer>
264
+ <div className={
265
+ `arrow ${currentSuggestion?.[inputId]?.index === 0 && "disabled"}`
266
+ } onClick={() => {
267
+ handleChangeSuggestion({ action: "prev" })
268
+ }}>
269
+ <img src={ArrowIcon} alt="" />
270
+ </div>
271
+ <p>
272
+ {(currentSuggestion?.[inputId]?.index + 1) || 1}
273
+ /
274
+ {suggestions?.[inputId]?.length}
275
+ </p>
276
+ <div className={`arrow right ${currentSuggestion?.[inputId]?.index === suggestions?.[inputId]?.length - 1 && "disabled"}`} onClick={() => {
277
+
278
+ }}>
279
+ <img onClick={() => {
280
+ handleChangeSuggestion({ action: "next" })
281
+ }} src={ArrowIcon} alt="ai icon" />
282
+ </div>
283
+ </OptionsContainer>
284
+
285
+ <ButtonsContainer>
286
+
287
+ <div className={`reload-suggestions ${isAiRegenerationLoading && "loading"}`} onClick={() => {
288
+ handlerRegenerateSuggestions({
289
+ type: "attribute"
290
+ });
291
+ }}>
292
+ <img className="" src={isAiRegenerationLoading ? LoadingIcon : ReloadIcon} />
293
+ </div>
294
+
295
+ <div className="accept-suggestion" onClick={() => {
296
+ handleAcceptSuggestion(currentSuggestion?.[inputId]?.value);
297
+ }}>
298
+ <p>Aceptar sugerencia</p>
299
+ </div>
300
+
301
+ </ButtonsContainer>
302
+
303
+ </div>
304
+ )
305
+ }
306
+ </BottomContainer>
307
+ </div>
141
308
  ) : (
142
309
  <InputFormatter
143
310
  name={inputName}
144
- inputId={inputId}
145
- placeholder={inputPlaceholder}
146
- mainValue={textValue.value}
147
- onChange={onHandleChange}
148
- articleId={articleId}
149
- updatedDescriptions={updatedDescriptions}
150
- setUpdatedDescriptions={setUpdatedDescriptions}
151
- maxChar={maxChar}
152
- isRequired={isRequired}
153
- disabled={disabled}
311
+ inputId={inputId}
312
+ placeholder={inputPlaceholder}
313
+ mainValue={textValue.value}
314
+ onChange={onHandleChange}
315
+ articleId={articleId}
316
+ updatedDescriptions={updatedDescriptions}
317
+ setUpdatedDescriptions={setUpdatedDescriptions}
318
+ maxChar={maxChar}
319
+ isRequired={isRequired}
320
+ disabled={disabled}
321
+ hasAiGeneration={hasAiGeneration && inputId != 'commentary-box'}
322
+ isAiAvailable={isAiAvailable}
323
+ aiGenerated={aiGenerated}
324
+ handlerAiGeneration={() => {
325
+ handlerAiGeneration({ type: "description" })
326
+ }}
327
+ handlerRegenerateSuggestions={() => {
328
+ handlerRegenerateSuggestions({ type: "description" })
329
+ }}
330
+ handleChangeSuggestion={handleChangeSuggestion}
331
+ isAiGenerationLoading={isAiGenerationLoading}
332
+ isAiRegenerationLoading={isAiRegenerationLoading}
333
+ isAiActive={isAiActive}
334
+ setIsAiActive={setIsAiActive}
154
335
  />
155
336
  )}
156
337
  {/* <p>{description}</p> */}
@@ -84,3 +84,84 @@ export const Container = styled.div`
84
84
  background: lightgray;
85
85
  }
86
86
  `;
87
+
88
+ export const InputContainer = styled.div`
89
+
90
+ &.ai-generation {
91
+ position: relative;
92
+
93
+ cursor: pointer;
94
+
95
+ .ia-input {
96
+ padding-right: 2.5rem;
97
+ }
98
+
99
+ .icon_container {
100
+ position: absolute;
101
+ right: 10px;
102
+ top: 50%;
103
+ transform: translateY(-50%);
104
+ width: 20px;
105
+ height: 20px;
106
+ padding: 4px;
107
+
108
+ border-radius: 50%;
109
+ overflow: hidden;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ z-index: 1;
114
+
115
+ &::before {
116
+ content: '';
117
+ position: absolute;
118
+ top: 0;
119
+ left: 0;
120
+ width: 100%;
121
+ height: 100%;
122
+
123
+ background: linear-gradient(
124
+ 120deg,
125
+ #ffffff 10%,
126
+ #ffe0f4 50%,
127
+ #ffffff 90%
128
+ );
129
+ background-size: 200% 200%;
130
+
131
+ animation: ai-shimmer 3s ease-in-out infinite alternate;
132
+ z-index: -1;
133
+ }
134
+
135
+ &.ai-available::before{
136
+
137
+ background: gray;
138
+ background-size: 200% 200%;
139
+
140
+ }
141
+
142
+ img {
143
+ width: 100%;
144
+ height: 100%;
145
+ object-fit: contain;
146
+ position: relative;
147
+ z-index: 2;
148
+ }
149
+
150
+ img.loading {
151
+ width: 20px;
152
+ height: 20px;
153
+ }
154
+
155
+ }
156
+ }
157
+
158
+ @keyframes ai-shimmer {
159
+ 0% {
160
+ background-position: 0% 50%;
161
+ }
162
+ 100% {
163
+ background-position: 100% 50%;
164
+ }
165
+ }
166
+
167
+ `;