Skip to content
18 changes: 18 additions & 0 deletions packages/spacecat-shared-tokowaka-client/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ export class ContentSummarizationMapper extends BaseOpportunityMapper {
markdownToHast(markdown: string): object;
}

/**
* FAQ opportunity mapper
* Handles conversion of FAQ suggestions to Tokowaka patches with HAST format
*/
export class FaqMapper extends BaseOpportunityMapper {
constructor(log: any);

getOpportunityType(): string;
requiresPrerender(): boolean;
suggestionToPatch(suggestion: Suggestion, opportunityId: string): TokawakaPatch | null;
canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };

/**
* Converts markdown text to HAST (Hypertext Abstract Syntax Tree) format
*/
markdownToHast(markdown: string): object;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer markdowntohast as a util method instead of part of this interface. Its an internal inpl detail of suggestion to patch conversion that shouldnt be exposed in the mapper interface.

}

/**
* Registry for opportunity mappers
*/
Expand Down
106 changes: 106 additions & 0 deletions packages/spacecat-shared-tokowaka-client/src/mappers/faq-mapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { toHast } from 'mdast-util-to-hast';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { hasText } from '@adobe/spacecat-shared-utils';
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
import BaseOpportunityMapper from './base-mapper.js';

/**
* Mapper for FAQ opportunity
* Handles conversion of FAQ suggestions to Tokowaka patches
*/
export default class FaqMapper extends BaseOpportunityMapper {
constructor(log) {
super(log);
this.opportunityType = 'faq';
this.prerenderRequired = true;
this.validActions = ['insertAfter', 'insertBefore', 'appendChild'];
}

getOpportunityType() {
return this.opportunityType;
}

requiresPrerender() {
return this.prerenderRequired;
}

/**
* Converts markdown text to HAST (Hypertext Abstract Syntax Tree) format
* @param {string} markdown - Markdown text
* @returns {Object} - HAST object
*/
// eslint-disable-next-line class-methods-use-this
markdownToHast(markdown) {
const mdast = fromMarkdown(markdown);
return toHast(mdast);
}

suggestionToPatch(suggestion, opportunityId) {
const eligibility = this.canDeploy(suggestion);
if (!eligibility.eligible) {
this.log.warn(`FAQ suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
return null;
}

const data = suggestion.getData();
const { text, transformRules } = data;

// Convert markdown to HAST
let hastValue;
try {
hastValue = this.markdownToHast(text);
} catch (error) {
this.log.error(`Failed to convert markdown to HAST for suggestion ${suggestion.getId()}: ${error.message}`);
return null;
}

return {
...this.createBasePatch(suggestion, opportunityId),
op: transformRules.action,
selector: transformRules.selector,
value: hastValue,
valueFormat: 'hast',
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
};
}

/**
* Checks if a FAQ suggestion can be deployed
* @param {Object} suggestion - Suggestion object
* @returns {Object} { eligible: boolean, reason?: string }
*/
canDeploy(suggestion) {
const data = suggestion.getData();

// Validate required fields
if (!data?.text) {
return { eligible: false, reason: 'text is required' };
}

if (!data.transformRules) {
return { eligible: false, reason: 'transformRules is required' };
}

if (!hasText(data.transformRules.selector)) {
return { eligible: false, reason: 'transformRules.selector is required' };
}

if (!this.validActions.includes(data.transformRules.action)) {
return { eligible: false, reason: 'transformRules.action must be insertAfter, insertBefore, or appendChild' };
}

return { eligible: true };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import HeadingsMapper from './headings-mapper.js';
import ContentSummarizationMapper from './content-summarization-mapper.js';
import FaqMapper from './faq-mapper.js';

/**
* Registry for opportunity mappers
Expand All @@ -32,6 +33,7 @@ export default class MapperRegistry {
const defaultMappers = [
HeadingsMapper,
ContentSummarizationMapper,
FaqMapper,
// more mappers here
];

Expand Down
Loading