diff --git a/nx2/blocks/chat-ao/artifacts/index.js b/nx2/blocks/chat-ao/artifacts/index.js index 164ee7f4c..ac382c473 100644 --- a/nx2/blocks/chat-ao/artifacts/index.js +++ b/nx2/blocks/chat-ao/artifacts/index.js @@ -6,6 +6,7 @@ import './column.js'; import './card.js'; import './data-table.js'; import './metric-card.js'; +import './page-evaluation.js'; import './code-block.js'; import './alert.js'; diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css new file mode 100644 index 000000000..674d4a48f --- /dev/null +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -0,0 +1,466 @@ +:host { + display: block; + font-family: var(--s2-font-family); + color: var(--s2-gray-800); + font-size: var(--s2-body-size-s); + line-height: normal; +} + +.ui-artifact-pe { + display: flex; + flex-direction: column; + gap: var(--s2-spacing-200); +} + +.ui-artifact-pe-icon { + display: inline-block; + flex-shrink: 0; + width: 20px; + height: 20px; + background-color: currentcolor; + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-negative { + mask-image: url("/img/icons/s2-icon-closecircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-positive { + mask-image: url("/img/icons/s2-icon-checkmarkcircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-neutral { + mask-image: url("/img/icons/s2-icon-removecircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-informative { + mask-image: url("/img/icons/s2-icon-infocircle-20-n.svg"); +} + +/* --- tone text colors --- */ + +.ui-artifact-pe-tone-negative { color: var(--s2-orange-900); } +.ui-artifact-pe-tone-positive { color: var(--s2-green-900); } +.ui-artifact-pe-tone-neutral { color: var(--s2-gray-800); } + +/* --- tone chip backgrounds --- */ + +.ui-artifact-pe-tone-negative-bg { + background: var(--s2-orange-100); + color: var(--s2-orange-900); +} + +.ui-artifact-pe-tone-positive-bg { + background: var(--s2-green-100); + color: var(--s2-green-900); +} + +.ui-artifact-pe-tone-neutral-bg { + background: var(--s2-gray-100); + color: var(--s2-gray-800); +} + +/* --- summary tiles --- */ + +.ui-artifact-pe-summary { + display: flex; + overflow: hidden; +} + +.ui-artifact-pe-tile { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--s2-spacing-75); + padding: var(--s2-spacing-300); + + & + & { + border-left: 1px solid var(--s2-gray-200); + } +} + +.ui-artifact-pe-tile-label { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-800); +} + +.ui-artifact-pe-tile-value { + font-size: var(--s2-heading-size-s); + font-weight: var(--s2-heading-font-weight); + line-height: var(--s2-heading-line-height); +} + +.ui-artifact-pe-tile-desc { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); +} + +/* --- collapsible sections --- */ + +.ui-artifact-pe-section { + border: 1px solid var(--s2-gray-200); + border-radius: var(--s2-corner-radius-300); + overflow: hidden; + margin: 0; +} + +.ui-artifact-pe-section-header { + display: flex; + align-items: center; + gap: var(--s2-spacing-200); + padding: var(--s2-spacing-200) var(--s2-spacing-300); + cursor: pointer; + user-select: none; + list-style: none; + + &::marker, + &::-webkit-details-marker { + display: none; + } +} + +.ui-artifact-pe-section-chip { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--s2-corner-radius-300); + flex-shrink: 0; +} + +.ui-artifact-pe-section-labels { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--s2-spacing-50); + min-width: 0; +} + +.ui-artifact-pe-section-label { + font-size: var(--s2-body-size-s); + font-weight: bold; + color: var(--s2-gray-900); +} + +.ui-artifact-pe-section-sublabel { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); +} + +.ui-artifact-pe-chevron { + width: 16px; + height: 16px; + flex-shrink: 0; + background-color: var(--s2-gray-600); + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + transform: rotate(180deg); + transition: transform 0.15s ease; +} + +.ui-artifact-pe-section[open] > .ui-artifact-pe-section-header > .ui-artifact-pe-chevron { + transform: rotate(0deg); +} + +/* --- item rows --- */ + +.ui-artifact-pe-item-chip { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + flex-shrink: 0; +} + +.ui-artifact-pe-item { + display: flex; + align-items: flex-start; + gap: var(--s2-spacing-200); + padding: var(--s2-spacing-200) var(--s2-spacing-300); + + &[hidden] { + display: none; + } +} + +.ui-artifact-pe-item-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: var(--s2-spacing-50); +} + +.ui-artifact-pe-item-title { + font-size: var(--s2-body-size-s); + font-weight: bold; + color: var(--s2-gray-900); +} + +.ui-artifact-pe-item-desc { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ui-artifact-pe-item-action { + flex-shrink: 0; + align-self: center; + font-size: var(--s2-body-size-xs); + font-weight: 500; + color: var(--s2-gray-900); + background: none; + border: 1px solid var(--s2-gray-300); + border-radius: var(--s2-corner-radius-200); + padding: var(--s2-spacing-50) var(--s2-spacing-100); + cursor: pointer; + white-space: nowrap; + + &:hover { + background: var(--s2-gray-100); + } +} + +/* --- pagination --- */ + +.ui-artifact-pe-paging { + display: flex; + align-items: center; + justify-content: center; + gap: var(--s2-spacing-100); + padding: var(--s2-spacing-100) var(--s2-spacing-300) var(--s2-spacing-200); +} + +.ui-artifact-pe-paging-label { + font-size: var(--s2-body-size-xs); + font-weight: bold; + color: var(--s2-gray-800); +} + +.ui-artifact-pe-paging-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + border-radius: var(--s2-corner-radius-200); + background: none; + cursor: pointer; + color: var(--s2-gray-600); + padding: 0; + + &:disabled { + opacity: 0.4; + cursor: default; + } + + &:hover:not(:disabled) { + background: var(--s2-gray-100); + } + + &::after { + content: ""; + display: block; + width: 14px; + height: 14px; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } +} + +.ui-artifact-pe-paging-prev::after { + transform: rotate(-90deg); +} + +.ui-artifact-pe-paging-next::after { + transform: rotate(90deg); +} + +/* --- modal --- */ + +/* The modal spec carries several off-token-scale values (13px, 20px, 28px, 6px + gap, 10px, 12px radius, 1.6 line-height); those stay raw to match the design. */ + +.ui-artifact-pe-modal-content { + font-family: var(--s2-font-family, 'Adobe Clean', adobe-clean, sans-serif); +} + +.ui-artifact-pe-modal-asset-label { + font-size: var(--s2-body-size-l); + font-weight: bold; + color: var(--s2-gray-900); + margin: 0 0 var(--s2-spacing-300); +} + +.ui-artifact-pe-modal-section { + margin-bottom: 20px; +} + +.ui-artifact-pe-modal-heading { + display: flex; + align-items: center; + gap: var(--s2-spacing-100); + margin-bottom: var(--s2-spacing-100); +} + +.ui-artifact-pe-modal-heading-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + background-color: currentcolor; + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +.ui-artifact-pe-modal-heading-icon.issue { + color: var(--s2-orange-900); + mask-image: url("/img/icons/s2-icon-closecircle-20-n.svg"); +} + +.ui-artifact-pe-modal-heading-icon.fix { + color: var(--s2-green-900); + mask-image: url("/img/icons/s2-icon-checkmarkcircle-20-n.svg"); +} + +.ui-artifact-pe-modal-heading-text { + font-weight: bold; + font-size: var(--s2-body-size-s); + color: var(--s2-gray-900); +} + +.ui-artifact-pe-modal-body { + font-size: var(--s2-body-size-s); + line-height: 1.6; + color: var(--s2-gray-800); + margin: 0; +} + +.ui-artifact-pe-modal-copy { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--s2-gray-800); + background: none; + border: none; + cursor: pointer; + padding: var(--s2-spacing-75) 0; + margin-top: var(--s2-spacing-75); + + &:hover { + color: var(--s2-gray-900); + } +} + +.ui-artifact-pe-modal-copy-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-copy-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +.ui-artifact-pe-modal-context-title { + font-size: var(--s2-body-size-m); + font-weight: bold; + color: var(--s2-gray-900); + margin: var(--s2-spacing-400) 0 var(--s2-spacing-200); +} + +.ui-artifact-pe-modal-context-card { + border: 1px solid var(--s2-gray-200); + border-radius: 12px; + padding: var(--s2-spacing-300); +} + +.ui-artifact-pe-modal-chip { + display: inline-block; + background: var(--s2-gray-100); + border-radius: var(--s2-corner-radius-100); + padding: var(--s2-spacing-50) 10px; + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-800); + margin-bottom: var(--s2-spacing-200); +} + +.ui-artifact-pe-modal-context-desc { + font-size: var(--s2-body-size-s); + line-height: 1.6; + color: var(--s2-gray-800); + margin: 0; +} + +.ui-artifact-pe-modal-paging { + display: flex; + justify-content: center; + align-items: center; + gap: var(--s2-spacing-200); + margin-top: 20px; + padding-top: var(--s2-spacing-300); + border-top: 1px solid var(--s2-gray-200); +} + +.ui-artifact-pe-modal-paging-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: var(--s2-corner-radius-300); + background: none; + cursor: pointer; + color: var(--s2-gray-600); + padding: 0; + + &:disabled { + opacity: 0.3; + cursor: default; + } + + &:hover:not(:disabled) { + background: var(--s2-gray-100); + } + + &::after { + content: ""; + display: block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } +} + +.ui-artifact-pe-modal-paging-prev::after { + transform: rotate(-90deg); +} + +.ui-artifact-pe-modal-paging-next::after { + transform: rotate(90deg); +} + +.ui-artifact-pe-modal-paging-label { + font-size: 13px; + font-weight: bold; + color: var(--s2-gray-800); +} diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js new file mode 100644 index 000000000..8650573aa --- /dev/null +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -0,0 +1,305 @@ +/* + * Copyright 2026 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 { LitElement, html, nothing } from 'da-lit'; +import { loadStyle } from '../../../utils/utils.js'; +import { registerArtifact } from './registry.js'; + +const styles = await loadStyle(import.meta.url); + +const PAGE_SIZE = 5; + +const TONE_MAP = { + negative: { cls: 'negative' }, + positive: { cls: 'positive' }, + neutral: { cls: 'neutral' }, +}; +const TONE_FALLBACK = { cls: 'informative' }; + +const toneOf = (tone) => TONE_MAP[tone] ?? TONE_FALLBACK; + +const toneIcon = (tone) => { + const { cls } = toneOf(tone); + return html``; +}; + +/* ---- summary ---- */ + +function renderTile({ label = '', value = '', description = '', tone }) { + const { cls } = toneOf(tone); + return html` +
+ ${label} + ${value} + ${description ? html`${description}` : nothing} +
+ `; +} + +function renderSummary(summary) { + return html` +
${summary.map(renderTile)}
+ `; +} + +/* ---- modal bodies ---- */ + +function renderContextCard(context) { + if (!context) return nothing; + return html` +
+ ${context.category ? html`${context.category}` : nothing} + ${context.description ? html`

${context.description}

` : nothing} +
+ `; +} + +function renderSuggestionBody(item) { + const { suggestion } = item; + if (!suggestion) return nothing; + return html` + ${suggestion.label ? html`

${suggestion.label}

` : nothing} +
+
+ + Issue identified +
+ ${suggestion.issue ? html`

${suggestion.issue}

` : nothing} +
+
+
+ + Suggested change +
+ ${suggestion.suggested ? html`

${suggestion.suggested}

` : nothing} + +
+ ${suggestion.context ? html` +
Context
+ ${renderContextCard(suggestion.context)} + ` : nothing} + `; +} + +function renderCheckBody(item) { + const { check } = item; + if (!check) return nothing; + return html` + ${check.label ? html`

${check.label}

` : nothing} + ${item.description ? html` +
+

${item.description}

+
+ ` : nothing} + ${check.context ? html` +
Context
+ ${renderContextCard(check.context)} + ` : nothing} + `; +} + +/* ---- section labels ---- */ + +function sectionLabelFromTone(tone) { + if (tone === 'negative') return 'failures'; + if (tone === 'positive') return 'passes'; + return 'checks'; +} + +class NxPageEval extends LitElement { + static properties = { + data: { attribute: false }, + _pages: { state: true }, + _modal: { state: true }, + }; + + constructor() { + super(); + this._pages = {}; + } + + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [styles]; + } + + _setPage(sectionIndex, page, total) { + if (page < 0 || page * PAGE_SIZE >= total) return; + this._pages = { ...this._pages, [sectionIndex]: page }; + } + + async _openModal(item, items, tone, componentTitle) { + let kind; + let filtered; + if (item.suggestion) { + kind = 'suggestion'; + filtered = items.filter((it) => it.suggestion); + } else if (item.check) { + kind = 'check'; + filtered = items.filter((it) => it.check); + } else { + return; + } + if (!filtered.length) return; + const idx = filtered.indexOf(item); + await import('../../shared/dialog/dialog.js'); + this._modal = { + kind, + items: filtered, + index: idx >= 0 ? idx : 0, + sectionLabel: sectionLabelFromTone(tone), + title: componentTitle || 'Page evaluation', + }; + } + + _setModalIndex(index) { + if (!this._modal || index < 0 || index >= this._modal.items.length) return; + this._modal = { ...this._modal, index }; + } + + _renderItem(item, tone, hidden, onAction) { + return html` +
+ ${toneIcon(tone)} +
+ ${item.title ?? ''} + ${item.description + ? html`${item.description}` + : nothing} +
+ ${item.suggestion ? html` + + ` : nothing} + ${!item.suggestion && item.check ? html` + + ` : nothing} +
+ `; + } + + _renderPaging(sectionIndex, end, total) { + const page = this._pages[sectionIndex] ?? 0; + return html` +
+ + ${end} of ${total} checks + +
+ `; + } + + _renderItems(items, tone, sectionIndex, componentTitle) { + const total = items.length; + const page = this._pages[sectionIndex] ?? 0; + const start = page * PAGE_SIZE; + const end = Math.min(start + PAGE_SIZE, total); + return html` +
+ ${items.map((item, i) => this._renderItem( + item, + tone, + i < start || i >= end, + () => this._openModal(item, items, tone, componentTitle), + ))} + ${total > PAGE_SIZE ? this._renderPaging(sectionIndex, end, total) : nothing} +
+ `; + } + + _renderSection(section, sectionIndex, componentTitle) { + const { + label = '', subLabel = '', tone, defaultOpen = false, items = [], + } = section; + const { cls } = toneOf(tone); + return html` +
+ + + ${toneIcon(tone)} + + + + + ${items.length ? this._renderItems(items, tone, sectionIndex, componentTitle) : nothing} +
+ `; + } + + _renderModal() { + const { + kind, items, index, sectionLabel, title, + } = this._modal; + const total = items.length; + const renderBody = kind === 'suggestion' ? renderSuggestionBody : renderCheckBody; + return html` + { this._modal = undefined; }} + > +
+ ${renderBody(items[index])} +
+ + + ${index + 1} of ${total} ${sectionLabel} + + +
+
+
+ `; + } + + render() { + if (!this.data) return nothing; + const { title = '', summary = [], sections = [] } = this.data; + return html` +
+ ${summary.length ? renderSummary(summary) : nothing} + ${sections.map((s, i) => this._renderSection(s, i, title))} +
+ ${this._modal ? this._renderModal() : nothing} + `; + } +} + +if (!customElements.get('nx-page-eval')) customElements.define('nx-page-eval', NxPageEval); + +registerArtifact('PageEvaluationWithIcons', (props) => html` + +`);