From c3a56c7ef171d53995f999878cb2b53571d404f2 Mon Sep 17 00:00:00 2001 From: mariana-furyk Date: Fri, 30 May 2025 12:19:53 +0300 Subject: [PATCH] [LLM Prompt Artifact] Add a details overview --- src/api/artifacts-api.js | 30 ++- .../CopyToClipboard/CopyToClipboard.jsx | 2 +- src/common/Search/Search.jsx | 9 + .../SearchNavigator/SearchNavigator.jsx | 171 ++++++++++++++++++ .../SearchNavigator/searchNavigator.scss | 58 ++++++ .../DetailsPromptTemplate.jsx | 27 +++ .../DetailsPromptTemplate/PromptTab.jsx | 88 +++++++++ .../detailsPromptTemplate.scss | 42 +++++ .../DetailsTabsContent/DetailsTabsContent.jsx | 4 + src/components/Details/details.util.js | 1 + .../DetailsInfo/DetailsInfoView.jsx | 7 +- src/components/LLMPrompts/llmPrompts.util.jsx | 8 +- src/constants.js | 3 + src/elements/ContentMenu/ContentMenu.jsx | 4 +- src/elements/TableLinkCell/TableLinkCell.jsx | 11 +- src/scss/main.scss | 4 + 16 files changed, 453 insertions(+), 16 deletions(-) create mode 100644 src/common/SearchNavigator/SearchNavigator.jsx create mode 100644 src/common/SearchNavigator/searchNavigator.scss create mode 100644 src/components/Details/DetailsPromptTemplate/DetailsPromptTemplate.jsx create mode 100644 src/components/Details/DetailsPromptTemplate/PromptTab.jsx create mode 100644 src/components/Details/DetailsPromptTemplate/detailsPromptTemplate.scss diff --git a/src/api/artifacts-api.js b/src/api/artifacts-api.js index 0e9f581fc9..886fc65bda 100644 --- a/src/api/artifacts-api.js +++ b/src/api/artifacts-api.js @@ -157,7 +157,35 @@ const artifactsApi = { newConfig.params.iter = iter } - return mainHttpClientV2.get(`/projects/${projectName}/artifacts/${artifactName}`, newConfig) + return Promise.resolve({ + data: { + kind: 'dataset', + metadata: { + key: 'test3', + project: 'default', + tree: '3c9a5fe2-1ffc-4c4c-864b-a712a8899fe0', + description: '', + iter: null, + uid: 'c4dc3113a581a11ed69ef09258fdc0584d590f74', + updated: '2025-05-06 08:50:01.714000+00:00', + created: '2025-05-06 08:50:01.714000+00:00', + tag: 'latest' + }, + spec: { + target_path: 'v3io:///asd/asd', + producer: { + kind: 'api', + name: 'UI', + uri: 'dashboard.default-tenant.app.vmdev63.lab.iguazeng.com' + }, + db_key: 'test3' + }, + status: {}, + project: 'default' + } + }) + + // return mainHttpClientV2.get(`/projects/${projectName}/artifacts/${artifactName}`, newConfig) }, getArtifacts: (project, filters, config, withExactName) => { return fetchArtifacts(project, filters, config, false, withExactName) diff --git a/src/common/CopyToClipboard/CopyToClipboard.jsx b/src/common/CopyToClipboard/CopyToClipboard.jsx index 95ec2ee0e8..7c07a3fc0a 100644 --- a/src/common/CopyToClipboard/CopyToClipboard.jsx +++ b/src/common/CopyToClipboard/CopyToClipboard.jsx @@ -75,7 +75,7 @@ const CopyToClipboard = ({ } CopyToClipboard.propTypes = { - children: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), className: PropTypes.string, disabled: PropTypes.bool, textToCopy: PropTypes.string, diff --git a/src/common/Search/Search.jsx b/src/common/Search/Search.jsx index 1d7469a226..a798b8299a 100644 --- a/src/common/Search/Search.jsx +++ b/src/common/Search/Search.jsx @@ -41,6 +41,7 @@ const Search = ({ placeholder = '', searchWhileTyping = false, value = '', + withoutBorder = false, wrapperClassName = '' }) => { const [searchValue, setSearchValue] = useState(value ?? '') @@ -112,6 +113,12 @@ const Search = ({ } } + useEffect(() => { + if (searchValue.length > 0 && value !== searchValue) { + setSearchValue(value) + } + }, [searchValue, value]) + return (
{matches.length > 0 && label.length > 0 && inputIsFocused && ( { + const [matchCount, setMatchCount] = useState(0) + const [activeMatchIndex, setActiveMatchIndex] = useState(0) + const [matches, setMatches] = useState([]) + const [searchValue, setSearchValue] = useState('') + const navigatorCounterClassNames = classnames( + 'search-navigator__counter', + matchCount > 0 && 'search-navigator__counter_with-divider' + ) + + const clearResults = useCallback(() => { + setSearchResult(promptTemplate) + setMatches([]) + setActiveMatchIndex(0) + setSearchValue('') + setMatchCount(0) + }, [promptTemplate, setSearchResult]) + + const searchOnChangeHandler = useCallback( + value => { + if (value.length === 0) { + return clearResults() + } + + const regex = new RegExp(value, 'gi') + const allMatches = [...promptTemplate.matchAll(regex)] + const count = allMatches.length + let index = 0 + + const highlighted = promptTemplate.replace(regex, match => { + const marker = `${match}` + + index++ + + return marker + }) + + setSearchResult(highlighted) + setMatchCount(count) + setMatches(allMatches) + setActiveMatchIndex(0) + setSearchValue(value) + }, + [clearResults, promptTemplate, setSearchResult] + ) + + const highlightMatch = useCallback( + index => { + if (!matches.length) return + + const regex = new RegExp(promptTemplate.match(matches[0])[0], 'gi') + let current = 0 + + const highlighted = promptTemplate.replace(regex, match => { + const marker = `${match}` + + current++ + + return marker + }) + + setSearchResult(highlighted) + setActiveMatchIndex(index) + }, + [matches, promptTemplate, setSearchResult] + ) + + const goToPrevMatch = () => { + if (!matches.length) return + const prevIndex = (activeMatchIndex - 1 + matchCount) % matchCount + highlightMatch(prevIndex) + } + + const goToNextMatch = () => { + if (!matches.length) return + const nextIndex = (activeMatchIndex + 1) % matchCount + highlightMatch(nextIndex) + } + + useEffect(() => { + const activeMark = document.querySelector(`mark[data-index="${activeMatchIndex}"]`) + + if (activeMark) { + activeMark.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }) + } + }, [activeMatchIndex]) + + return ( +
+ +
+ {matchCount > 0 && `${activeMatchIndex + 1}/${matchCount}`} +
+ {searchValue.length > 0 && ( +
+ + + + + + + + + +
+ )} +
+ ) +} + +SearchNavigator.propTypes = { + promptTemplate: PropTypes.string.isRequired, + setSearchResult: PropTypes.func.isRequired +} + +export default SearchNavigator diff --git a/src/common/SearchNavigator/searchNavigator.scss b/src/common/SearchNavigator/searchNavigator.scss new file mode 100644 index 0000000000..020ed961fd --- /dev/null +++ b/src/common/SearchNavigator/searchNavigator.scss @@ -0,0 +1,58 @@ +@use 'igz-controls/scss/borders'; +@use 'igz-controls/scss/colors'; + +.search-navigator { + display: flex; + align-items: center; + min-width: 280px; + border: borders.$primaryBorder; + border-radius: 4px; + + &__counter { + display: flex; + font-size: 13px; + color: colors.$topaz; + + &:after { + display: none; + content: ''; + width: 5px; + height: 15px; + margin: 0 5px 0 8px; + border-right: borders.$primaryBorder; + } + + &_with-divider { + &:after { + display: block; + } + } + } + + &__buttons { + display: flex; + padding-right: 10px; + } + + &__button { + .round-icon-cp__circle { + width: 26px; + height: 26px; + } + + &_back { + rotate: -90deg; + } + + &_next { + rotate: 90deg; + } + + &_cancel { + .round-icon-cp__circle { + width: 24px; + height: 24px; + } + } + } +} \ No newline at end of file diff --git a/src/components/Details/DetailsPromptTemplate/DetailsPromptTemplate.jsx b/src/components/Details/DetailsPromptTemplate/DetailsPromptTemplate.jsx new file mode 100644 index 0000000000..1d1dceec11 --- /dev/null +++ b/src/components/Details/DetailsPromptTemplate/DetailsPromptTemplate.jsx @@ -0,0 +1,27 @@ +import { useCallback, useState } from 'react' +import { ARGUMENTS_TAB, PROMPT_TAB } from '../../../constants' +import PromptTab from './PromptTab' + +import './detailsPromptTemplate.scss' + +const DetailsPromptTemplate = () => { + const [selectedTab, setSelectedTab] = useState(PROMPT_TAB) + const tabs = [ + { id: PROMPT_TAB, label: 'Prompt' }, + { id: ARGUMENTS_TAB, label: 'Arguments' } + ] + + const handleTabChange = useCallback(tabName => { + setSelectedTab(tabName) + }, []) + + return ( +
+ {selectedTab === PROMPT_TAB ? ( + + ) : null} +
+ ) +} + +export default DetailsPromptTemplate diff --git a/src/components/Details/DetailsPromptTemplate/PromptTab.jsx b/src/components/Details/DetailsPromptTemplate/PromptTab.jsx new file mode 100644 index 0000000000..0b0887162a --- /dev/null +++ b/src/components/Details/DetailsPromptTemplate/PromptTab.jsx @@ -0,0 +1,88 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. 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 CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +import { useState } from 'react' +import PropTypes from 'prop-types' + +import ContentMenu from '../../../elements/ContentMenu/ContentMenu' +import SearchNavigator from '../../../common/SearchNavigator/SearchNavigator' +import CopyToClipboard from '../../../common/CopyToClipboard/CopyToClipboard' +import { RoundedIcon } from 'igz-controls/components' + +import Copy from 'igz-controls/images/copy-to-clipboard-icon.svg?react' + +const PromptTab = ({ handleTabChange, selectedTab, tabs }) => { + const [promptTemplate] = useState( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n' + + '\n' + + 'Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.' + ) + const [searchResult, setSearchResult] = useState('') + + const createMarkup = html => { + return { __html: html } + } + + return ( +
+
+ + + + + + Copy prompt + + +
+
+
+ ) +} + +PromptTab.propTypes = { + handleTabChange: PropTypes.func.isRequired, + selectedTab: PropTypes.string.isRequired, + tabs: PropTypes.array.isRequired +} + +export default PromptTab diff --git a/src/components/Details/DetailsPromptTemplate/detailsPromptTemplate.scss b/src/components/Details/DetailsPromptTemplate/detailsPromptTemplate.scss new file mode 100644 index 0000000000..f882c18c44 --- /dev/null +++ b/src/components/Details/DetailsPromptTemplate/detailsPromptTemplate.scss @@ -0,0 +1,42 @@ +@use 'igz-controls/scss/borders'; + +.prompt-template { + .prompt-tab { + &__header { + display: flex; + align-items: center; + margin-top: 20px; + padding-bottom: 10px; + background-color: white; + } + + &__template { + max-height: 425px; + padding: 25px 15px; + overflow-y: scroll; + border: borders.$primaryBorder; + border-radius: 4px; + } + + &__copy-btn { + display: flex; + align-items: center; + justify-content: space-between; + width: 135px; + padding: 11px 18px; + border: borders.$primaryBorder; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + + &-wrapper { + margin-left: 10px; + } + + svg { + width: 14px; + height: 14px; + } + } + } +} \ No newline at end of file diff --git a/src/components/Details/DetailsTabsContent/DetailsTabsContent.jsx b/src/components/Details/DetailsTabsContent/DetailsTabsContent.jsx index 93d349b59c..b326a2f61e 100644 --- a/src/components/Details/DetailsTabsContent/DetailsTabsContent.jsx +++ b/src/components/Details/DetailsTabsContent/DetailsTabsContent.jsx @@ -63,12 +63,14 @@ import { DETAILS_OVERVIEW_TAB, DETAILS_PODS_TAB, DETAILS_PREVIEW_TAB, + DETAILS_PROMPT_TEMPLATE_TAB, DETAILS_REQUESTED_FEATURES_TAB, DETAILS_RESULTS_TAB, DETAILS_RETURNED_FEATURES_TAB, DETAILS_STATISTICS_TAB, DETAILS_TRANSFORMATIONS_TAB } from '../../../constants' +import DetailsPromptTemplate from '../DetailsPromptTemplate/DetailsPromptTemplate' const DetailsTabsContent = ({ applyChangesRef, @@ -235,6 +237,8 @@ const DetailsTabsContent = ({ ) case DETAILS_COLLECTIONS_TAB: return + case DETAILS_PROMPT_TEMPLATE_TAB: + return default: return null } diff --git a/src/components/Details/details.util.js b/src/components/Details/details.util.js index 762e6c05ed..42df9adab4 100644 --- a/src/components/Details/details.util.js +++ b/src/components/Details/details.util.js @@ -115,6 +115,7 @@ export const generateArtifactsContent = ( } } } else { + console.log(selectedItem) return { hash: { value: selectedItem.hash ?? '', diff --git a/src/components/DetailsInfo/DetailsInfoView.jsx b/src/components/DetailsInfo/DetailsInfoView.jsx index 50075014d9..4b8defee30 100644 --- a/src/components/DetailsInfo/DetailsInfoView.jsx +++ b/src/components/DetailsInfo/DetailsInfoView.jsx @@ -37,6 +37,7 @@ import { FILES_PAGE, FUNCTIONS_PAGE, JOBS_PAGE, + LLM_PROMPTS_PAGE, MODELS_PAGE } from '../../constants' import { parseKeyValues } from '../../utils' @@ -90,7 +91,8 @@ const DetailsInfoView = React.forwardRef( pageData.page === FUNCTIONS_PAGE || pageData.page === MODELS_PAGE || pageData.page === FEATURE_STORE_PAGE || - pageData.page === DOCUMENTS_PAGE) && + pageData.page === DOCUMENTS_PAGE || + pageData.page === LLM_PROMPTS_PAGE) && params.pageTab !== FEATURE_SETS_TAB &&

General

}
    {pageData.details.infoHeaders?.map(header => { @@ -139,7 +141,8 @@ const DetailsInfoView = React.forwardRef( pageData.page === FILES_PAGE || pageData.page === MODELS_PAGE || pageData.page === FEATURE_STORE_PAGE || - pageData.page === DOCUMENTS_PAGE + pageData.page === DOCUMENTS_PAGE || + pageData.page === LLM_PROMPTS_PAGE ) { if (header.id === 'labels') { chipsData.validationRules = infoContent[header.id]?.validationRules diff --git a/src/components/LLMPrompts/llmPrompts.util.jsx b/src/components/LLMPrompts/llmPrompts.util.jsx index a00532803d..d4bb7a5bc1 100644 --- a/src/components/LLMPrompts/llmPrompts.util.jsx +++ b/src/components/LLMPrompts/llmPrompts.util.jsx @@ -49,12 +49,12 @@ export const detailsMenu = [ id: 'overview' }, { - label: 'prompt-template', - id: 'Prompt template' + label: 'Prompt template', + id: 'prompt-template' }, { - label: 'generation-configuration', - id: 'Generation configuration' + label: 'Generation configuration', + id: 'generation-configuration' } ] diff --git a/src/constants.js b/src/constants.js index a03e10ffc6..6991cac238 100644 --- a/src/constants.js +++ b/src/constants.js @@ -135,6 +135,8 @@ export const MONITORING_APP_PAGE = 'monitoring-app' export const DOCUMENTS_PAGE = 'documents' export const LLM_PROMPTS_PAGE = 'llm-prompts' +export const PROMPT_TAB = 'prompt' +export const ARGUMENTS_TAB = 'arguments' export const PROJECT_MONITOR = 'monitor' @@ -317,6 +319,7 @@ export const DETAILS_RESULTS_TAB = 'results' export const DETAILS_RETURNED_FEATURES_TAB = 'returned-features' export const DETAILS_STATISTICS_TAB = 'statistics' export const DETAILS_TRANSFORMATIONS_TAB = 'transformations' +export const DETAILS_PROMPT_TEMPLATE_TAB = 'prompt-template' export const FETCH_MODEL_FEATURE_VECTOR_BEGIN = 'FETCH_MODEL_FEATURE_VECTOR_BEGIN' export const FETCH_MODEL_FEATURE_VECTOR_FAILURE = 'FETCH_MODEL_FEATURE_VECTOR_FAILURE' export const FETCH_MODEL_FEATURE_VECTOR_SUCCESS = 'FETCH_MODEL_FEATURE_VECTOR_SUCCESS' diff --git a/src/elements/ContentMenu/ContentMenu.jsx b/src/elements/ContentMenu/ContentMenu.jsx index 3c4d01b514..f6f14abe3b 100644 --- a/src/elements/ContentMenu/ContentMenu.jsx +++ b/src/elements/ContentMenu/ContentMenu.jsx @@ -29,7 +29,7 @@ const ContentMenu = ({ disabled = false, fontSize = 'md', onClick = null, - screen= '', + screen = '', tabs = [] }) => { const params = useParams() @@ -93,7 +93,7 @@ const ContentMenu = ({ ContentMenu.propTypes = { activeTab: PropTypes.string.isRequired, disabled: PropTypes.bool, - fontSize: PropTypes.oneOf(['sm', 'md', 'lg']), + fontSize: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']), onClick: PropTypes.func, screen: PropTypes.string, tabs: CONTENT_MENU_TABS.isRequired diff --git a/src/elements/TableLinkCell/TableLinkCell.jsx b/src/elements/TableLinkCell/TableLinkCell.jsx index 6ac5539b31..8725530168 100644 --- a/src/elements/TableLinkCell/TableLinkCell.jsx +++ b/src/elements/TableLinkCell/TableLinkCell.jsx @@ -80,10 +80,7 @@ const TableLinkCell = ({ {data.showStatus && stateValue && stateLabel && ( - } - > + }> )} @@ -99,7 +96,8 @@ const TableLinkCell = ({ link.match(/models/) || link.match(/files/) || link.match(/datasets/) || - link.match(/documents/)) && + link.match(/documents/) || + link.match(/llm-prompts/)) && Object.values(selectedItem).length !== 0)) && (
    {(item.startTime || item.updated) && ( @@ -108,7 +106,8 @@ const TableLinkCell = ({ (link.match(/functions/) || link.match(/models/) || link.match(/files/) || - link.match(/datasets/) + link.match(/datasets/) || + link.match(/llm-prompts/) ? formatDatetime(item.updated, 'N/A') : formatDatetime( item.startTime || item.created, diff --git a/src/scss/main.scss b/src/scss/main.scss index 0487c07d94..10a31f8eac 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -78,6 +78,10 @@ body { padding-left: 0; } + &-xs { + font-size: 16px; + } + &-sm { font-size: 16px;