@capillarytech/creatives-library 9.0.67 → 9.0.68
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/package.json +1 -1
- package/v2Components/ErrorInfoNote/index.js +3 -2
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +2 -1
- package/v2Components/HtmlEditor/utils/__tests__/renderIssueMessage.test.js +74 -0
- package/v2Components/HtmlEditor/utils/renderIssueMessage.js +56 -0
- package/v2Components/HtmlEditor/utils/validationConstants.js +5 -0
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { ANDROID, GENERIC, IOS } from '../../v2Containers/CreativesContainer/con
|
|
|
18
18
|
import { ERROR_TAB_KEYS } from '../HtmlEditor/utils/validationConstants';
|
|
19
19
|
import { SEVERITY } from '../HtmlEditor/components/ValidationPanel/constants';
|
|
20
20
|
import { LIQUID_DOC_URL } from './constants';
|
|
21
|
+
import renderIssueMessage from '../HtmlEditor/utils/renderIssueMessage';
|
|
21
22
|
|
|
22
23
|
const { CapLabelInline } = CapLabel;
|
|
23
24
|
|
|
@@ -120,7 +121,7 @@ const TabContent = ({ issues, onErrorClick, intl }) => {
|
|
|
120
121
|
>
|
|
121
122
|
<CapRow useLegacy className="error-info-note__item-content">
|
|
122
123
|
<CapLabelInline type="label2" className="error-info-note__item-message">
|
|
123
|
-
{displayMessage}
|
|
124
|
+
{renderIssueMessage(displayMessage)}
|
|
124
125
|
</CapLabelInline>
|
|
125
126
|
{line && (
|
|
126
127
|
<CapLabelInline type="label2" className="error-info-note__item-location">
|
|
@@ -426,7 +427,7 @@ const ErrorSection = ({
|
|
|
426
427
|
{(errors || []).map((error) => (
|
|
427
428
|
<CapRow useLegacy key={`${error}`} className="error-list-legacy__item">
|
|
428
429
|
<CapLabel type="label2" className="cap-list-v2-error-item">
|
|
429
|
-
<CapLabel type="label2">{error}</CapLabel>
|
|
430
|
+
<CapLabel type="label2">{renderIssueMessage(error)}</CapLabel>
|
|
430
431
|
</CapLabel>
|
|
431
432
|
</CapRow>
|
|
432
433
|
))}
|
|
@@ -18,6 +18,7 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
|
18
18
|
// Messages
|
|
19
19
|
import messages from './messages';
|
|
20
20
|
import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../../constants';
|
|
21
|
+
import renderIssueMessage from '../../utils/renderIssueMessage';
|
|
21
22
|
|
|
22
23
|
// Styles
|
|
23
24
|
import './_validationTabs.scss';
|
|
@@ -109,7 +110,7 @@ const ValidationTabContent = ({
|
|
|
109
110
|
>
|
|
110
111
|
<div className="validation-tabs__item-content">
|
|
111
112
|
<span className="validation-tabs__item-message">
|
|
112
|
-
{message}
|
|
113
|
+
{renderIssueMessage(message)}
|
|
113
114
|
</span>
|
|
114
115
|
{line && (
|
|
115
116
|
<span className="validation-tabs__item-location">
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderIssueMessage Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for rendering AI-generated liquid validation messages, including tolerance for
|
|
5
|
+
* non-guaranteed LLM formatting quirks (lead-in text before the first label, markdown-wrapped
|
|
6
|
+
* labels).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { render } from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import renderIssueMessage from '../renderIssueMessage';
|
|
13
|
+
|
|
14
|
+
const renderMessage = (message) => render(<div>{renderIssueMessage(message)}</div>);
|
|
15
|
+
|
|
16
|
+
describe('renderIssueMessage', () => {
|
|
17
|
+
it('returns non-string input unchanged', () => {
|
|
18
|
+
const node = <span>already a node</span>;
|
|
19
|
+
expect(renderIssueMessage(node)).toBe(node);
|
|
20
|
+
expect(renderIssueMessage(null)).toBeNull();
|
|
21
|
+
expect(renderIssueMessage(undefined)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns a plain message unchanged when no known labels are present', () => {
|
|
25
|
+
expect(renderIssueMessage('Something went wrong')).toBe('Something went wrong');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders all three labels in order, bolding only "Issue text:" and stripping it from the body', () => {
|
|
29
|
+
const message = 'Issue text: bad tag Explanation: the tag is malformed Solution: fix the tag';
|
|
30
|
+
const { container } = renderMessage(message);
|
|
31
|
+
|
|
32
|
+
const strong = container.querySelector('strong');
|
|
33
|
+
expect(strong).toHaveTextContent('Issue text:');
|
|
34
|
+
expect(container).toHaveTextContent('Issue text: bad tag');
|
|
35
|
+
expect(container).toHaveTextContent('Explanation: the tag is malformed');
|
|
36
|
+
expect(container).toHaveTextContent('Solution: fix the tag');
|
|
37
|
+
// Only the "Issue text:" label should be wrapped in <strong>.
|
|
38
|
+
expect(container.querySelectorAll('strong')).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders correctly when only some labels are present', () => {
|
|
42
|
+
const message = 'Issue text: bad tag Solution: fix the tag';
|
|
43
|
+
const { container } = renderMessage(message);
|
|
44
|
+
|
|
45
|
+
expect(container).toHaveTextContent('Issue text: bad tag');
|
|
46
|
+
expect(container).toHaveTextContent('Solution: fix the tag');
|
|
47
|
+
expect(container).not.toHaveTextContent('Explanation:');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('preserves lead-in text before the first recognized label instead of dropping it', () => {
|
|
51
|
+
const message = "Here's what I found. Issue text: bad tag Explanation: the tag is malformed";
|
|
52
|
+
const { container } = renderMessage(message);
|
|
53
|
+
|
|
54
|
+
expect(container).toHaveTextContent("Here's what I found.");
|
|
55
|
+
expect(container).toHaveTextContent('Issue text: bad tag');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('strips markdown bold wrapping around a label instead of leaking asterisks', () => {
|
|
59
|
+
const message = '**Issue text:** bad tag **Explanation:** the tag is malformed';
|
|
60
|
+
const { container } = renderMessage(message);
|
|
61
|
+
|
|
62
|
+
expect(container.textContent).not.toContain('*');
|
|
63
|
+
expect(container.querySelector('strong')).toHaveTextContent('Issue text:');
|
|
64
|
+
expect(container).toHaveTextContent('Explanation: the tag is malformed');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('strips markdown underscore-bold wrapping around a label', () => {
|
|
68
|
+
const message = '__Issue text:__ bad tag';
|
|
69
|
+
const { container } = renderMessage(message);
|
|
70
|
+
|
|
71
|
+
expect(container.textContent).not.toContain('_');
|
|
72
|
+
expect(container.querySelector('strong')).toHaveTextContent('Issue text:');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { Fragment } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AI_ISSUE_TEXT_LABEL,
|
|
4
|
+
AI_ISSUE_MESSAGE_LABELS,
|
|
5
|
+
AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY,
|
|
6
|
+
} from './validationConstants';
|
|
7
|
+
|
|
8
|
+
// Renders askAiraForLiquid's LLM-generated messages, splitting on known labels rather than "\n"
|
|
9
|
+
// since the API doesn't guarantee newlines; tolerates lead-in text and markdown-wrapped labels.
|
|
10
|
+
|
|
11
|
+
const stripLabelMarkdown = (text) => AI_ISSUE_MESSAGE_LABELS.reduce(
|
|
12
|
+
(acc, label) => acc.split(`**${label}**`).join(label).split(`__${label}__`).join(label),
|
|
13
|
+
text
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const renderIssueMessage = (message) => {
|
|
17
|
+
if (typeof message !== 'string') {
|
|
18
|
+
return message;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizedMessage = stripLabelMarkdown(message);
|
|
22
|
+
|
|
23
|
+
const labelPositions = AI_ISSUE_MESSAGE_LABELS
|
|
24
|
+
.map((label) => ({ label, index: normalizedMessage.indexOf(label) }))
|
|
25
|
+
.filter(({ index }) => index !== -1)
|
|
26
|
+
.sort((a, b) => a.index - b.index);
|
|
27
|
+
|
|
28
|
+
if (labelPositions.length === 0) {
|
|
29
|
+
return normalizedMessage;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const segments = labelPositions.map(({ label, index }, i) => {
|
|
33
|
+
const end = i + 1 < labelPositions.length ? labelPositions[i + 1].index : normalizedMessage.length;
|
|
34
|
+
return { key: label, label, text: normalizedMessage.slice(index, end).trim() };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const leadingText = normalizedMessage.slice(0, labelPositions[0].index).trim();
|
|
38
|
+
if (leadingText) {
|
|
39
|
+
segments.unshift({ key: AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY, label: null, text: leadingText });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return segments.map(({ key, label, text }, index) => (
|
|
43
|
+
<Fragment key={key}>
|
|
44
|
+
{index > 0 && <br />}
|
|
45
|
+
{index > 0 && ' - '}
|
|
46
|
+
{label === AI_ISSUE_TEXT_LABEL ? (
|
|
47
|
+
<>
|
|
48
|
+
<strong>{label}</strong>
|
|
49
|
+
{text.slice(label.length)}
|
|
50
|
+
</>
|
|
51
|
+
) : text}
|
|
52
|
+
</Fragment>
|
|
53
|
+
));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default renderIssueMessage;
|
|
@@ -36,3 +36,8 @@ export const ERROR_TAB_KEYS = {
|
|
|
36
36
|
ERRORS: 'errors',
|
|
37
37
|
WARNINGS: 'warnings',
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
// Labels used to segment AI-generated liquid validation messages (see renderIssueMessage.js)
|
|
41
|
+
export const AI_ISSUE_TEXT_LABEL = 'Issue text:';
|
|
42
|
+
export const AI_ISSUE_MESSAGE_LABELS = [AI_ISSUE_TEXT_LABEL, 'Explanation:', 'Solution:'];
|
|
43
|
+
export const AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY = 'leading-text';
|