@adobe/spacecat-shared-tokowaka-client 1.1.1 → 1.2.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.
- package/.releaserc.cjs +17 -0
- package/CHANGELOG.md +76 -1
- package/CODE_OF_CONDUCT.md +75 -0
- package/CONTRIBUTING.md +74 -0
- package/README.md +155 -15
- package/package.json +6 -6
- package/src/index.d.ts +120 -25
- package/src/index.js +481 -177
- package/src/mappers/base-mapper.js +41 -9
- package/src/mappers/content-summarization-mapper.js +38 -35
- package/src/mappers/faq-mapper.js +247 -0
- package/src/mappers/headings-mapper.js +37 -23
- package/src/mappers/mapper-registry.js +2 -0
- package/src/utils/custom-html-utils.js +195 -0
- package/src/utils/markdown-utils.js +24 -0
- package/src/utils/patch-utils.js +103 -0
- package/src/utils/s3-utils.js +117 -0
- package/src/utils/site-utils.js +25 -0
- package/src/utils/suggestion-utils.js +69 -0
- package/test/index.test.js +1268 -462
- package/test/mappers/base-mapper.test.js +250 -7
- package/test/mappers/content-mapper.test.js +26 -24
- package/test/mappers/faq-mapper.test.js +1428 -0
- package/test/mappers/headings-mapper.test.js +23 -17
- package/test/utils/html-utils.test.js +432 -0
- package/test/utils/patch-utils.test.js +409 -0
- package/test/utils/s3-utils.test.js +140 -0
- package/test/utils/site-utils.test.js +80 -0
- package/test/utils/suggestion-utils.test.js +187 -0
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { removePatchesBySuggestionIds } from '../utils/patch-utils.js';
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* Base class for opportunity mappers
|
|
15
17
|
* Each opportunity type should extend this class and implement the abstract methods
|
|
@@ -40,16 +42,17 @@ export default class BaseOpportunityMapper {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* Converts
|
|
45
|
+
* Converts suggestions to Tokowaka patches
|
|
44
46
|
* @abstract
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {
|
|
47
|
-
* @
|
|
47
|
+
* @param {string} _ - URL path for the suggestions
|
|
48
|
+
* @param {Array} __ - Array of suggestion entities for the same URL
|
|
49
|
+
* @param {string} ___ - Opportunity ID
|
|
50
|
+
* @returns {Array} - Array of Tokowaka patch objects
|
|
48
51
|
*/
|
|
49
52
|
// eslint-disable-next-line no-unused-vars
|
|
50
|
-
|
|
51
|
-
this.log.error('
|
|
52
|
-
throw new Error('
|
|
53
|
+
suggestionsToPatches(_, __, ___) {
|
|
54
|
+
this.log.error('suggestionsToPatches() must be implemented by subclass');
|
|
55
|
+
throw new Error('suggestionsToPatches() must be implemented by subclass');
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
@@ -73,8 +76,17 @@ export default class BaseOpportunityMapper {
|
|
|
73
76
|
* @returns {Object} - Base patch object
|
|
74
77
|
*/
|
|
75
78
|
createBasePatch(suggestion, opportunityId) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
79
|
+
const data = suggestion.getData();
|
|
80
|
+
const updatedAt = data?.scrapedAt
|
|
81
|
+
|| data?.transformRules?.scrapedAt
|
|
82
|
+
|| suggestion.getUpdatedAt();
|
|
83
|
+
|
|
84
|
+
// Parse timestamp, fallback to Date.now() if invalid
|
|
85
|
+
let lastUpdated = Date.now();
|
|
86
|
+
if (updatedAt) {
|
|
87
|
+
const parsed = new Date(updatedAt).getTime();
|
|
88
|
+
lastUpdated = Number.isNaN(parsed) ? Date.now() : parsed;
|
|
89
|
+
}
|
|
78
90
|
|
|
79
91
|
return {
|
|
80
92
|
opportunityId,
|
|
@@ -83,4 +95,24 @@ export default class BaseOpportunityMapper {
|
|
|
83
95
|
lastUpdated,
|
|
84
96
|
};
|
|
85
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes patches from configuration for given suggestions
|
|
101
|
+
* Default implementation simply removes patches matching the suggestion IDs.
|
|
102
|
+
* Override this method in subclasses if custom rollback logic is needed
|
|
103
|
+
* (e.g., FAQ mapper removes heading patch when no suggestions remain).
|
|
104
|
+
* @param {Object} config - Current Tokowaka configuration
|
|
105
|
+
* @param {Array<string>} suggestionIds - Suggestion IDs to remove
|
|
106
|
+
* @param {string} opportunityId - Opportunity ID
|
|
107
|
+
* @returns {Object} - Updated configuration with patches removed
|
|
108
|
+
*/
|
|
109
|
+
// eslint-disable-next-line no-unused-vars
|
|
110
|
+
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
111
|
+
if (!config || !config.patches) {
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.log.debug(`Removing patches for ${suggestionIds.length} suggestions`);
|
|
116
|
+
return removePatchesBySuggestionIds(config, suggestionIds);
|
|
117
|
+
}
|
|
86
118
|
}
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { toHast } from 'mdast-util-to-hast';
|
|
14
|
-
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
15
13
|
import { hasText } from '@adobe/spacecat-shared-utils';
|
|
16
14
|
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
17
15
|
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
|
+
import { markdownToHast } from '../utils/markdown-utils.js';
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Mapper for content opportunity
|
|
@@ -37,43 +36,47 @@ export default class ContentSummarizationMapper extends BaseOpportunityMapper {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
* Converts
|
|
41
|
-
* @param {string}
|
|
42
|
-
* @
|
|
39
|
+
* Converts suggestions to Tokowaka patches
|
|
40
|
+
* @param {string} urlPath - URL path for the suggestions
|
|
41
|
+
* @param {Array} suggestions - Array of suggestion entities for the same URL
|
|
42
|
+
* @param {string} opportunityId - Opportunity ID
|
|
43
|
+
* @returns {Array} - Array of Tokowaka patch objects
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const mdast = fromMarkdown(markdown);
|
|
47
|
-
return toHast(mdast);
|
|
48
|
-
}
|
|
45
|
+
suggestionsToPatches(urlPath, suggestions, opportunityId) {
|
|
46
|
+
const patches = [];
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
suggestions.forEach((suggestion) => {
|
|
49
|
+
const eligibility = this.canDeploy(suggestion);
|
|
50
|
+
if (!eligibility.eligible) {
|
|
51
|
+
this.log.warn(`Content-Summarization suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
const data = suggestion.getData();
|
|
56
|
+
const { summarizationText, transformRules } = data;
|
|
57
|
+
|
|
58
|
+
// Convert markdown to HAST
|
|
59
|
+
let hastValue;
|
|
60
|
+
try {
|
|
61
|
+
hastValue = markdownToHast(summarizationText);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.log.error(`Failed to convert markdown to HAST for suggestion ${suggestion.getId()}: ${error.message}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const patch = {
|
|
68
|
+
...this.createBasePatch(suggestion, opportunityId),
|
|
69
|
+
op: transformRules.action,
|
|
70
|
+
selector: transformRules.selector,
|
|
71
|
+
value: hastValue,
|
|
72
|
+
valueFormat: 'hast',
|
|
73
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
patches.push(patch);
|
|
77
|
+
});
|
|
68
78
|
|
|
69
|
-
return
|
|
70
|
-
...this.createBasePatch(suggestion, opportunityId),
|
|
71
|
-
op: transformRules.action,
|
|
72
|
-
selector: transformRules.selector,
|
|
73
|
-
value: hastValue,
|
|
74
|
-
valueFormat: 'hast',
|
|
75
|
-
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
76
|
-
};
|
|
79
|
+
return patches;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/**
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hasText, isValidUrl } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
15
|
+
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
|
+
import { markdownToHast } from '../utils/markdown-utils.js';
|
|
17
|
+
import { removePatchesBySuggestionIds } from '../utils/patch-utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mapper for FAQ opportunity
|
|
21
|
+
* Handles conversion of FAQ suggestions to Tokowaka patches
|
|
22
|
+
*/
|
|
23
|
+
export default class FaqMapper extends BaseOpportunityMapper {
|
|
24
|
+
constructor(log) {
|
|
25
|
+
super(log);
|
|
26
|
+
this.opportunityType = 'faq';
|
|
27
|
+
this.prerenderRequired = true;
|
|
28
|
+
this.validActions = ['insertAfter', 'insertBefore', 'appendChild'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getOpportunityType() {
|
|
32
|
+
return this.opportunityType;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
requiresPrerender() {
|
|
36
|
+
return this.prerenderRequired;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds FAQ item HTML structure (div with h3 and answer)
|
|
41
|
+
* Structure: <div><h3>question</h3>answer-content</div>
|
|
42
|
+
* @param {Object} suggestion - Suggestion entity
|
|
43
|
+
* @returns {Object} - HAST object for the FAQ item
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
// eslint-disable-next-line class-methods-use-this
|
|
47
|
+
buildFaqItemHast(suggestion) {
|
|
48
|
+
const data = suggestion.getData();
|
|
49
|
+
const { item } = data;
|
|
50
|
+
|
|
51
|
+
// Convert answer markdown to HAST
|
|
52
|
+
const answerHast = markdownToHast(item.answer);
|
|
53
|
+
|
|
54
|
+
// Build structure: <div><h3>question</h3>answer-hast-children</div>
|
|
55
|
+
return {
|
|
56
|
+
type: 'element',
|
|
57
|
+
tagName: 'div',
|
|
58
|
+
properties: {},
|
|
59
|
+
children: [
|
|
60
|
+
{
|
|
61
|
+
type: 'element',
|
|
62
|
+
tagName: 'h3',
|
|
63
|
+
properties: {},
|
|
64
|
+
children: [{ type: 'text', value: item.question }],
|
|
65
|
+
},
|
|
66
|
+
...answerHast.children, // Spread answer HAST children directly
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates individual patches for FAQ suggestions
|
|
73
|
+
* Always creates heading (h2) patch with latest timestamp, then individual FAQ divs
|
|
74
|
+
* @param {string} urlPath - URL path for current suggestions
|
|
75
|
+
* @param {Array} suggestions - Array of suggestion entities for the same URL (to be deployed)
|
|
76
|
+
* @param {string} opportunityId - Opportunity ID
|
|
77
|
+
* @returns {Array} - Array of patch objects
|
|
78
|
+
*/
|
|
79
|
+
suggestionsToPatches(
|
|
80
|
+
urlPath,
|
|
81
|
+
suggestions,
|
|
82
|
+
opportunityId,
|
|
83
|
+
) {
|
|
84
|
+
if (!urlPath || !Array.isArray(suggestions) || suggestions.length === 0) {
|
|
85
|
+
this.log.error('Invalid parameters for FAQ mapper.suggestionsToPatches');
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Filter eligible suggestions
|
|
90
|
+
const eligibleSuggestions = suggestions.filter((suggestion) => {
|
|
91
|
+
const eligibility = this.canDeploy(suggestion);
|
|
92
|
+
if (!eligibility.eligible) {
|
|
93
|
+
this.log.warn(`FAQ suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (eligibleSuggestions.length === 0) {
|
|
100
|
+
this.log.warn('No eligible FAQ suggestions to deploy');
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const patches = [];
|
|
105
|
+
|
|
106
|
+
// Get transformRules and headingText from first suggestion
|
|
107
|
+
const firstSuggestion = eligibleSuggestions[0];
|
|
108
|
+
const firstData = firstSuggestion.getData();
|
|
109
|
+
const { headingText = 'FAQs', transformRules } = firstData;
|
|
110
|
+
|
|
111
|
+
// Calculate the most recent lastUpdated from all eligible suggestions
|
|
112
|
+
// The heading patch should have the same timestamp as the newest FAQ
|
|
113
|
+
const maxLastUpdated = Math.max(...eligibleSuggestions.map((suggestion) => {
|
|
114
|
+
const data = suggestion.getData();
|
|
115
|
+
const updatedAt = data?.scrapedAt
|
|
116
|
+
|| data?.transformRules?.scrapedAt
|
|
117
|
+
|| suggestion.getUpdatedAt();
|
|
118
|
+
|
|
119
|
+
if (updatedAt) {
|
|
120
|
+
const parsed = new Date(updatedAt).getTime();
|
|
121
|
+
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
122
|
+
}
|
|
123
|
+
return Date.now();
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Always create/update heading patch with latest timestamp
|
|
127
|
+
// mergePatches will replace existing heading if it already exists
|
|
128
|
+
this.log.debug(`Creating/updating heading patch for ${urlPath}`);
|
|
129
|
+
|
|
130
|
+
const headingHast = {
|
|
131
|
+
type: 'element',
|
|
132
|
+
tagName: 'h2',
|
|
133
|
+
properties: {},
|
|
134
|
+
children: [{ type: 'text', value: headingText }],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
patches.push({
|
|
138
|
+
opportunityId,
|
|
139
|
+
// No suggestionId for FAQ heading patch
|
|
140
|
+
prerenderRequired: this.requiresPrerender(),
|
|
141
|
+
lastUpdated: maxLastUpdated,
|
|
142
|
+
op: transformRules.action,
|
|
143
|
+
selector: transformRules.selector,
|
|
144
|
+
value: headingHast,
|
|
145
|
+
valueFormat: 'hast',
|
|
146
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Create individual FAQ patches
|
|
150
|
+
eligibleSuggestions.forEach((suggestion) => {
|
|
151
|
+
try {
|
|
152
|
+
const faqItemHast = this.buildFaqItemHast(suggestion);
|
|
153
|
+
|
|
154
|
+
patches.push({
|
|
155
|
+
...this.createBasePatch(suggestion, opportunityId),
|
|
156
|
+
op: transformRules.action,
|
|
157
|
+
selector: transformRules.selector,
|
|
158
|
+
value: faqItemHast,
|
|
159
|
+
valueFormat: 'hast',
|
|
160
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.log.error(`Failed to build FAQ HAST for suggestion ${suggestion.getId()}: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return patches;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Checks if a FAQ suggestion can be deployed
|
|
172
|
+
* @param {Object} suggestion - Suggestion object
|
|
173
|
+
* @returns {Object} { eligible: boolean, reason?: string }
|
|
174
|
+
*/
|
|
175
|
+
canDeploy(suggestion) {
|
|
176
|
+
const data = suggestion.getData();
|
|
177
|
+
|
|
178
|
+
// Check shouldOptimize flag first
|
|
179
|
+
if (data?.shouldOptimize !== true) {
|
|
180
|
+
return { eligible: false, reason: 'shouldOptimize flag is not true' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!data?.item?.question || !data?.item?.answer) {
|
|
184
|
+
return { eligible: false, reason: 'item.question and item.answer are required' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!data.transformRules) {
|
|
188
|
+
return { eligible: false, reason: 'transformRules is required' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!hasText(data.transformRules.selector)) {
|
|
192
|
+
return { eligible: false, reason: 'transformRules.selector is required' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!this.validActions.includes(data.transformRules.action)) {
|
|
196
|
+
return { eligible: false, reason: 'transformRules.action must be insertAfter, insertBefore, or appendChild' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!isValidUrl(data.url)) {
|
|
200
|
+
return { eligible: false, reason: `url ${data.url} is not a valid URL` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { eligible: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes patches from configuration for FAQ suggestions
|
|
208
|
+
* FAQ-specific logic: Also removes the heading patch when no FAQ suggestions remain
|
|
209
|
+
* @param {Object} config - Current Tokowaka configuration
|
|
210
|
+
* @param {Array<string>} suggestionIds - Suggestion IDs to remove
|
|
211
|
+
* @param {string} opportunityId - Opportunity ID
|
|
212
|
+
* @returns {Object} - Updated configuration with patches removed
|
|
213
|
+
*/
|
|
214
|
+
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
215
|
+
if (!config || !config.patches) {
|
|
216
|
+
return config;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const suggestionIdsSet = new Set(suggestionIds);
|
|
220
|
+
const additionalPatchKeys = [];
|
|
221
|
+
|
|
222
|
+
// Find FAQ patches for this opportunity
|
|
223
|
+
const opportunityPatches = config.patches.filter((p) => p.opportunityId === opportunityId);
|
|
224
|
+
|
|
225
|
+
// Get FAQ suggestion IDs that will remain after rollback
|
|
226
|
+
const remainingSuggestionIds = opportunityPatches
|
|
227
|
+
.filter((p) => p.suggestionId && !suggestionIdsSet.has(p.suggestionId))
|
|
228
|
+
.map((p) => p.suggestionId);
|
|
229
|
+
|
|
230
|
+
// If no FAQ suggestions remain, remove the heading patch too
|
|
231
|
+
if (remainingSuggestionIds.length === 0) {
|
|
232
|
+
this.log.debug('No remaining FAQ suggestions, marking heading patch for removal');
|
|
233
|
+
// Add heading patch key (opportunityId only, no suggestionId)
|
|
234
|
+
additionalPatchKeys.push(opportunityId);
|
|
235
|
+
} else {
|
|
236
|
+
this.log.debug(`${remainingSuggestionIds.length} FAQ suggestions remain, keeping heading patch`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Remove FAQ suggestion patches and any orphaned heading patches
|
|
240
|
+
this.log.debug(
|
|
241
|
+
`Removing ${suggestionIds.length} FAQ suggestion patches `
|
|
242
|
+
+ `and ${additionalPatchKeys.length} heading patches`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return removePatchesBySuggestionIds(config, suggestionIds, additionalPatchKeys);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -33,30 +33,44 @@ export default class HeadingsMapper extends BaseOpportunityMapper {
|
|
|
33
33
|
return this.prerenderRequired;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Converts suggestions to Tokowaka patches
|
|
38
|
+
* @param {string} urlPath - URL path for the suggestions
|
|
39
|
+
* @param {Array} suggestions - Array of suggestion entities for the same URL
|
|
40
|
+
* @param {string} opportunityId - Opportunity ID
|
|
41
|
+
* @returns {Array} - Array of Tokowaka patch objects
|
|
42
|
+
*/
|
|
43
|
+
suggestionsToPatches(urlPath, suggestions, opportunityId) {
|
|
44
|
+
const patches = [];
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
suggestions.forEach((suggestion) => {
|
|
47
|
+
const eligibility = this.canDeploy(suggestion);
|
|
48
|
+
if (!eligibility.eligible) {
|
|
49
|
+
this.log.warn(`Headings suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = suggestion.getData();
|
|
54
|
+
const { checkType, transformRules } = data;
|
|
55
|
+
|
|
56
|
+
const patch = {
|
|
57
|
+
...this.createBasePatch(suggestion, opportunityId),
|
|
58
|
+
op: transformRules.action,
|
|
59
|
+
selector: transformRules.selector,
|
|
60
|
+
value: data.recommendedAction,
|
|
61
|
+
valueFormat: 'text',
|
|
62
|
+
...(data.currentValue !== null && { currValue: data.currentValue }),
|
|
63
|
+
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (checkType === 'heading-missing-h1' && transformRules.tag) {
|
|
67
|
+
patch.tag = transformRules.tag;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
patches.push(patch);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return patches;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import HeadingsMapper from './headings-mapper.js';
|
|
14
14
|
import ContentSummarizationMapper from './content-summarization-mapper.js';
|
|
15
|
+
import FaqMapper from './faq-mapper.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Registry for opportunity mappers
|
|
@@ -32,6 +33,7 @@ export default class MapperRegistry {
|
|
|
32
33
|
const defaultMappers = [
|
|
33
34
|
HeadingsMapper,
|
|
34
35
|
ContentSummarizationMapper,
|
|
36
|
+
FaqMapper,
|
|
35
37
|
// more mappers here
|
|
36
38
|
];
|
|
37
39
|
|