diff --git a/dev/ai-field-marker.html b/dev/ai-field-marker.html
index 084747bc025..0d035572107 100644
--- a/dev/ai-field-marker.html
+++ b/dev/ai-field-marker.html
@@ -74,6 +74,8 @@
AI-filled fields
// Show the "AI is working" state while the fill is in progress.
const marker = document.createElement('vaadin-ai-field-marker');
marker.working = true;
+ // Show the confidence level of the filled value in the helper section.
+ marker.confidence = ['low', 'medium', 'high'][Math.floor(3 * random())];
field.appendChild(marker);
});
diff --git a/packages/aura/aura.css b/packages/aura/aura.css
index b51f91e5729..7b18f6a7d04 100644
--- a/packages/aura/aura.css
+++ b/packages/aura/aura.css
@@ -11,6 +11,7 @@
@import './src/components/input-container.css';
@import './src/components/accordion-details.css';
+@import './src/components/ai-field-marker.css';
@import './src/components/app-layout.css';
@import './src/components/avatar.css';
@import './src/components/badge.css';
diff --git a/packages/aura/src/components/ai-field-marker.css b/packages/aura/src/components/ai-field-marker.css
new file mode 100644
index 00000000000..b0662401979
--- /dev/null
+++ b/packages/aura/src/components/ai-field-marker.css
@@ -0,0 +1,5 @@
+:has(> vaadin-ai-field-marker) {
+ --vaadin-ai-field-marker-confidence-low-color: var(--aura-red-text);
+ --vaadin-ai-field-marker-confidence-medium-color: var(--aura-yellow-text);
+ --vaadin-ai-field-marker-confidence-high-color: var(--aura-green-text);
+}
diff --git a/packages/field-highlighter/src/styles/vaadin-ai-field-marker-base-styles.js b/packages/field-highlighter/src/styles/vaadin-ai-field-marker-base-styles.js
index 02e1fff5403..237411946eb 100644
--- a/packages/field-highlighter/src/styles/vaadin-ai-field-marker-base-styles.js
+++ b/packages/field-highlighter/src/styles/vaadin-ai-field-marker-base-styles.js
@@ -181,6 +181,65 @@ export const aiFieldMarkerStyles = css`
}
}
+ /* The confidence indicator: a sibling of the marker slotted into the
+ field's helper text section. The level class name picks the color and how
+ much of the pie icon is filled. */
+ :has(> vaadin-ai-field-marker) > [slot='helper'].ai-confidence {
+ display: flex;
+ align-items: center;
+ gap: var(--vaadin-gap-s);
+ color: var(--_vaadin-ai-field-marker-confidence-color);
+
+ &::before {
+ content: '';
+ flex: none;
+ box-sizing: border-box;
+ width: var(--vaadin-icon-size, 1lh);
+ height: var(--vaadin-icon-size, 1lh);
+ /* The padding insets the pie from the ring: the wedge is clipped to the
+ content box, while the tint fills the whole circle behind it. */
+ padding: calc(var(--vaadin-icon-size, 1lh) / 12);
+ border: 1px solid color-mix(in srgb, currentColor 50%, transparent);
+ border-radius: 50%;
+ background-color: color-mix(in srgb, currentColor 15%, transparent);
+ background-image: conic-gradient(currentColor var(--_vaadin-ai-field-marker-confidence-fill, 0%), #0000 0%);
+ background-clip: content-box, border-box;
+ }
+
+ /* The levels are told apart by how much of the pie is filled, so the base
+ styles keep them in the same neutral color. The themes map the custom
+ properties to their own semantic colors. */
+ &.ai-confidence-low {
+ --_vaadin-ai-field-marker-confidence-color: var(
+ --vaadin-ai-field-marker-confidence-low-color,
+ var(--vaadin-text-color-secondary)
+ );
+ --_vaadin-ai-field-marker-confidence-fill: 25%;
+ }
+
+ &.ai-confidence-medium {
+ --_vaadin-ai-field-marker-confidence-color: var(
+ --vaadin-ai-field-marker-confidence-medium-color,
+ var(--vaadin-text-color-secondary)
+ );
+ --_vaadin-ai-field-marker-confidence-fill: 50%;
+ }
+
+ &.ai-confidence-high {
+ --_vaadin-ai-field-marker-confidence-color: var(
+ --vaadin-ai-field-marker-confidence-high-color,
+ var(--vaadin-text-color-secondary)
+ );
+ --_vaadin-ai-field-marker-confidence-fill: 75%;
+ }
+ }
+
+ /* While the AI is working, the confidence describes a value that is about
+ to be replaced, so hide it along with the marker. */
+ [ai-working] > [slot='helper'].ai-confidence {
+ display: none;
+ }
+
[ai-working],
:has(> vaadin-ai-field-marker)::part(input-field),
:has(> vaadin-ai-field-marker)::part(input-fields),
diff --git a/packages/field-highlighter/src/vaadin-ai-field-marker.d.ts b/packages/field-highlighter/src/vaadin-ai-field-marker.d.ts
index 47f251b9963..b91ca9f6dc9 100644
--- a/packages/field-highlighter/src/vaadin-ai-field-marker.d.ts
+++ b/packages/field-highlighter/src/vaadin-ai-field-marker.d.ts
@@ -28,6 +28,15 @@ export interface AiFieldMarkerI18n {
* The tooltip text of the badge button.
*/
badgeTooltip?: string;
+
+ /**
+ * The texts of the confidence indicator.
+ */
+ confidence?: {
+ low?: string;
+ medium?: string;
+ high?: string;
+ };
}
/**
@@ -53,20 +62,32 @@ export type AiFieldRevertEvent = CustomEvent<{ value: unknown }>;
* in progress, set the `working` property to show an "AI is working" shimmer
* on the field along with a client-side read-only guard.
*
+ * Set the `confidence` property to show the confidence level of the filled
+ * value (`low`, `medium` or `high`) as an indicator in the field's helper
+ * text section, ahead of a helper the field itself may have. While the
+ * indicator is shown, the field is marked with `has-helper`, so that the
+ * helper text section is laid out the same as for a helper of its own.
+ *
* ### Styling
*
- * The following state attribute is set on the field element for styling:
+ * The following state attributes are set on the field element for styling:
*
- * Attribute | Description
- * -------------|-------------
- * `ai-working` | Set while an AI is working on the field.
+ * Attribute | Description
+ * ----------------|-------------
+ * `ai-working` | Set while an AI is working on the field.
+ *
+ * The confidence indicator is rendered into the field's light DOM as a
+ * `` with the `ai-confidence` class name and the level as an additional
+ * `ai-confidence-low`, `ai-confidence-medium` or `ai-confidence-high` one.
*
* The following custom CSS properties are available for styling:
*
- * Custom CSS property |
- * :-------------------------------------------|
- * `--vaadin-ai-field-marker-badge-icon-color` |
- * `--vaadin-ai-field-marker-mask-pos` |
+ * Custom CSS property |
+ * :----------------------------------------------------|
+ * `--vaadin-ai-field-marker-badge-icon-color` |
+ * `--vaadin-ai-field-marker-confidence-high-color` |
+ * `--vaadin-ai-field-marker-confidence-low-color` |
+ * `--vaadin-ai-field-marker-confidence-medium-color` |
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
@@ -89,7 +110,13 @@ declare class AiFieldMarker extends I18nMixin` element the marker injects into a field's shadow root. */
const MARKER_STYLE_ATTRIBUTE = 'ai-field-marker-styles';
+/**
+ * The class name of the confidence indicator the marker adds to the field's
+ * light DOM; the level goes on a suffixed class name of its own. Prefixed
+ * with `ai-`, since the indicator sits among the application's own children
+ * of the field, where a plain `confidence` or `low` would be ambiguous.
+ */
+const CONFIDENCE_CLASS = 'ai-confidence';
+
// The position the shimmer's mask is at, animated by the marker's keyframes.
// Registered here rather than with an @property rule in the marker stylesheet,
// which is injected into the field's root node: a registration only takes effect
@@ -210,20 +227,32 @@ class DelayedFieldValue {
* directly into the marker's light DOM, so that document-level themes
* and user stylesheets can reach them.
*
+ * Set the `confidence` property to show the confidence level of the filled
+ * value (`low`, `medium` or `high`) as an indicator in the field's helper
+ * text section, ahead of a helper the field itself may have. While the
+ * indicator is shown, the field is marked with `has-helper`, so that the
+ * helper text section is laid out the same as for a helper of its own.
+ *
* ### Styling
*
- * The following state attribute is set on the field element for styling:
+ * The following state attributes are set on the field element for styling:
+ *
+ * Attribute | Description
+ * ----------------|-------------
+ * `ai-working` | Set while an AI is working on the field.
*
- * Attribute | Description
- * -------------|-------------
- * `ai-working` | Set while an AI is working on the field.
+ * The confidence indicator is rendered into the field's light DOM as a
+ * `` with the `ai-confidence` class name and the level as an additional
+ * `ai-confidence-low`, `ai-confidence-medium` or `ai-confidence-high` one.
*
* The following custom CSS properties are available for styling:
*
- * Custom CSS property |
- * :-------------------------------------------|
- * `--vaadin-ai-field-marker-badge-icon-color` |
- * `--vaadin-ai-field-marker-mask-pos` |
+ * Custom CSS property |
+ * :----------------------------------------------------|
+ * `--vaadin-ai-field-marker-badge-icon-color` |
+ * `--vaadin-ai-field-marker-confidence-high-color` |
+ * `--vaadin-ai-field-marker-confidence-low-color` |
+ * `--vaadin-ai-field-marker-confidence-medium-color` |
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
@@ -257,6 +286,17 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
type: Boolean,
value: false,
},
+
+ /**
+ * The confidence level of the AI-filled value, shown as an indicator
+ * in the field's helper text section. Possible values are `low`,
+ * `medium` and `high`; when not set, no indicator is shown. The
+ * indicator texts can be localized with the `i18n` property.
+ */
+ confidence: {
+ type: String,
+ value: null,
+ },
};
}
@@ -278,6 +318,20 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
/** The field value captured for the revert event detail. */
#capturedValue;
+ /**
+ * The confidence indicator added to the field's light DOM and rendered
+ * in the field's helper text section. Set while `confidence` is set on
+ * a marked field.
+ */
+ #confidenceNode = null;
+
+ /**
+ * Observes the field's `has-helper` attribute while the confidence
+ * indicator is shown, so that the marker can re-assert it if the field
+ * recomputes it from its own helper content. Created on first use.
+ */
+ #helperStateObserver = null;
+
/**
* While in the working state, the elements whose client-side `readonly`
* state was overridden — the field itself and, for a `vaadin-custom-field`,
@@ -367,7 +421,13 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
* // The accessible label of the badge button and the popover dialog.
* badgeLabel: 'AI-provided value',
* // The tooltip text of the badge button.
- * badgeTooltip: 'Field value modified by AI.\nClick for details'
+ * badgeTooltip: 'Field value modified by AI.\nClick for details',
+ * // The texts of the confidence indicator.
+ * confidence: {
+ * low: 'Low confidence',
+ * medium: 'Medium confidence',
+ * high: 'High confidence'
+ * }
* }
* ```
*
@@ -449,6 +509,8 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
this.#stopWorking(true);
+ this.#removeConfidenceNode();
+
if (this.#descNode) {
removeValuesFromAttribute(this.#describedElement, 'aria-describedby', this.#descNode.id);
this.#descNode.remove();
@@ -483,6 +545,10 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
return;
}
+ if (props.has('confidence') || props.has('__effectiveI18n')) {
+ this.#updateConfidence();
+ }
+
if (props.has('working')) {
if (this.working) {
this.#startWorking();
@@ -493,6 +559,12 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
this.#capturedValue = this.#annotatedValue();
this.#announcePending = true;
}
+
+ // The indicator is hidden while working, so the helper text section is
+ // only claimed for it — and the indicator described — once the working
+ // state ends.
+ this.#updateConfidenceDescription();
+ this.#updateFieldHelperState();
}
// Announce after the update so the announcement reflects a message set in
@@ -594,6 +666,10 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
this.#describedElement = describedElement;
}
+ // Apply the confidence indicator directly: on a reconnect no property
+ // change triggers updated(), which handles the first connect.
+ this.#updateConfidence();
+
// Capture the AI-filled value so the revert event can carry it.
this.#capturedValue = this.#annotatedValue();
@@ -619,6 +695,133 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
return this.#valueDelay ? this.#valueDelay.latestValue : field.value;
}
+ /**
+ * Syncs the confidence indicator in the field's helper text section with
+ * the `confidence` property: a `` slotted into the field's helper
+ * slot, with the `ai-confidence` and `ai-confidence-` class names
+ * and the localized level text as content.
+ */
+ #updateConfidence() {
+ const field = this.#field;
+ if (!field) {
+ return;
+ }
+
+ const level = this.confidence;
+ if (!level) {
+ this.#removeConfidenceNode();
+ return;
+ }
+
+ if (!this.#confidenceNode) {
+ const node = document.createElement('span');
+ node.setAttribute('slot', 'helper');
+ // Hide the indicator from the field's helper slot controller, which
+ // would otherwise evict the field's own helper element in favor of
+ // the indicator. The browser still renders it in the helper slot.
+ node.setAttribute('data-slot-ignore', '');
+ node.id = `ai-field-marker-confidence-${generateUniqueId()}`;
+ // Insert ahead of a helper the field already has, so that the indicator
+ // comes first in the helper text section. A helper added later ends up
+ // after the indicator, since the field appends it.
+ field.insertBefore(node, field.querySelector(':scope > [slot="helper"]'));
+ this.#confidenceNode = node;
+ }
+
+ this.#confidenceNode.className = `${CONFIDENCE_CLASS} ${CONFIDENCE_CLASS}-${level}`;
+ this.#confidenceNode.textContent = this.__effectiveI18n.confidence[level] ?? '';
+ this.#updateConfidenceDescription();
+ this.#updateFieldHelperState();
+ }
+
+ /**
+ * Keeps the indicator's id in the described element's `aria-describedby`
+ * only while the indicator is shown: a visually hidden indicator would
+ * still get read as part of the field's description, although it describes
+ * a value the AI is about to replace.
+ */
+ #updateConfidenceDescription() {
+ const node = this.#confidenceNode;
+ if (!node || !this.#describedElement) {
+ return;
+ }
+
+ if (this.working) {
+ removeValuesFromAttribute(this.#describedElement, 'aria-describedby', node.id);
+ } else {
+ addValuesToAttribute(this.#describedElement, 'aria-describedby', node.id);
+ }
+ }
+
+ /**
+ * Keeps the field's `has-helper` attribute set while the indicator is
+ * shown, since it is content in the field's helper text section although
+ * the field's own helper is not what provides it. The attribute is what
+ * both the field and the themes key their helper text section styles on —
+ * from showing the section at all to placing it above the field for the
+ * `helper-above-field` theme.
+ *
+ * The field recomputes the attribute from its own helper content, which
+ * never includes the indicator, so a recomputation can drop it while the
+ * indicator is still shown. An observer re-asserts it in that case.
+ */
+ #updateFieldHelperState() {
+ const field = this.#field;
+
+ // While the AI is working the indicator is hidden, so the field should
+ // only reserve the helper text section for a helper of its own.
+ if (this.#confidenceNode && !this.working) {
+ field.toggleAttribute('has-helper', true);
+
+ this.#helperStateObserver ??= new MutationObserver(() => {
+ // Read the field live: the observer is reused when the marker moves
+ // to another field, so a captured one could be a previous field.
+ const observedField = this.#field;
+ if (observedField && this.#confidenceNode && !this.working && !observedField.hasAttribute('has-helper')) {
+ observedField.toggleAttribute('has-helper', true);
+ }
+ });
+ this.#helperStateObserver.observe(field, { attributes: true, attributeFilter: ['has-helper'] });
+ return;
+ }
+
+ this.#helperStateObserver?.disconnect();
+
+ // The field keeps the attribute when its own helper provides content,
+ // which it may have gained while the indicator was shown.
+ if (!this.#hasFieldHelper()) {
+ field.removeAttribute('has-helper');
+ }
+ }
+
+ /**
+ * Whether the field has helper content of its own, i.e. helper slot content
+ * other than the indicator. Judged with the same content check the field
+ * itself uses for its `has-helper` attribute.
+ *
+ * @return {boolean}
+ */
+ #hasFieldHelper() {
+ return [...this.#field.querySelectorAll(':scope > [slot="helper"]')].some(
+ (node) => node !== this.#confidenceNode && hasNodeContent(node),
+ );
+ }
+
+ /** Removes the confidence indicator. */
+ #removeConfidenceNode() {
+ const node = this.#confidenceNode;
+ if (!node) {
+ return;
+ }
+
+ if (this.#describedElement) {
+ removeValuesFromAttribute(this.#describedElement, 'aria-describedby', node.id);
+ }
+ node.remove();
+ this.#confidenceNode = null;
+ this.#updateFieldHelperState();
+ }
+
/**
* Enters the "AI is working" state: shows the shimmer and makes the field
* read-only on the client so the user cannot edit a value the AI is about
diff --git a/packages/field-highlighter/test/ai-field-marker.test.ts b/packages/field-highlighter/test/ai-field-marker.test.ts
index ed2aaa1e2ab..b020ef478c9 100644
--- a/packages/field-highlighter/test/ai-field-marker.test.ts
+++ b/packages/field-highlighter/test/ai-field-marker.test.ts
@@ -91,6 +91,19 @@ class FocusSensitiveField extends HTMLElement {
customElements.define('focus-sensitive-field', FocusSensitiveField);
+/**
+ * A helper element that renders its content in its shadow root, so it has
+ * neither light-DOM text nor children although it shows helper content.
+ */
+class ShadowHelper extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' }).textContent = 'Shadow helper';
+ }
+}
+
+customElements.define('shadow-helper', ShadowHelper);
+
/**
* A field that exposes none of the elements the marker can describe: no
* `ariaTarget`, no `inputElement` and no `focusElement`.
@@ -634,6 +647,438 @@ describe('ai field marker', () => {
});
});
+ describe('confidence', () => {
+ let marker: AiFieldMarker;
+
+ function getConfidenceNode(host: HTMLElement = field): HTMLSpanElement | null {
+ return host.querySelector(':scope > [slot="helper"].ai-confidence');
+ }
+
+ beforeEach(async () => {
+ marker = mark(field, { confidence: 'low' });
+ await nextRender();
+ });
+
+ it('should have no confidence by default', () => {
+ expect(document.createElement('vaadin-ai-field-marker').confidence).to.be.null;
+ });
+
+ it('should render the confidence indicator into the field helper slot', () => {
+ const node = getConfidenceNode()!;
+ expect(node).to.exist;
+ expect(node.textContent).to.equal('Low confidence');
+ expect(node.assignedSlot).to.exist;
+ expect(node.assignedSlot!.name).to.equal('helper');
+ expect(node.assignedSlot!.getRootNode()).to.equal(field.shadowRoot);
+ });
+
+ it('should have the confidence level as a class name', () => {
+ expect(getConfidenceNode()!.classList.contains('ai-confidence-low')).to.be.true;
+ });
+
+ it('should show the helper text section for a field without a helper', () => {
+ const helperPart = field.shadowRoot!.querySelector('[part="helper-text"]')!;
+ expect(getComputedStyle(helperPart).display).to.not.equal('none');
+ });
+
+ describe('has-helper', () => {
+ it('should set has-helper on the field while the indicator is shown', () => {
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should toggle has-helper when the confidence is set and cleared', async () => {
+ marker.confidence = null;
+ await nextUpdate(marker);
+ expect(field.hasAttribute('has-helper')).to.be.false;
+
+ marker.confidence = 'high';
+ await nextUpdate(marker);
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should remove has-helper when the marker is removed', async () => {
+ marker.remove();
+ await nextRender();
+
+ expect(field.hasAttribute('has-helper')).to.be.false;
+ });
+
+ it('should not set has-helper while the AI is working', async () => {
+ marker.working = true;
+ await nextUpdate(marker);
+ expect(field.hasAttribute('has-helper')).to.be.false;
+
+ marker.working = false;
+ await nextUpdate(marker);
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should keep has-helper for the field helper text while the AI is working', async () => {
+ field.helperText = 'Keep it short';
+ await nextRender();
+
+ marker.working = true;
+ await nextUpdate(marker);
+
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should keep has-helper for a helper with only element content', async () => {
+ const childField = fixtureSync(
+ `
`,
+ );
+ await nextRender();
+ const childMarker = mark(childField, { confidence: 'low' });
+ await nextRender();
+
+ childMarker.working = true;
+ await nextUpdate(childMarker);
+
+ expect(childField.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should not keep has-helper for an empty slotted helper element', async () => {
+ const emptyField = fixtureSync(
+ ``,
+ );
+ await nextRender();
+ const emptyMarker = mark(emptyField, { confidence: 'low' });
+ await nextRender();
+
+ emptyMarker.working = true;
+ await nextUpdate(emptyMarker);
+
+ expect(emptyField.hasAttribute('has-helper')).to.be.false;
+ });
+
+ it('should not re-assert has-helper dropped as the working state begins', async () => {
+ field.helperText = 'Keep it short';
+ await nextRender();
+
+ // The drop and the working state land in the same update cycle: the
+ // re-assert observer must judge against the live working state, not
+ // the state at observation time.
+ field.removeAttribute('has-helper');
+ marker.working = true;
+ await nextUpdate(marker);
+
+ expect(field.hasAttribute('has-helper')).to.be.false;
+ });
+
+ it('should keep has-helper for a field that has a helper of its own', async () => {
+ field.helperText = 'Keep it short';
+ await nextRender();
+
+ marker.confidence = null;
+ await nextUpdate(marker);
+
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should re-assert has-helper when the field drops it', async () => {
+ field.helperText = 'Keep it short';
+ await nextRender();
+
+ // Clearing the helper makes the field recompute the attribute from its
+ // own helper content, which does not include the indicator.
+ field.helperText = '';
+ await nextRender();
+
+ expect(field.hasAttribute('has-helper')).to.be.true;
+ });
+
+ describe('field with a helper rendering in its shadow root', () => {
+ let helperField: TextField;
+
+ beforeEach(async () => {
+ // The field counts a defined custom element as helper content even
+ // without light-DOM text or children, since it may render content
+ // in its shadow root — the marker must judge it the same way.
+ helperField = fixtureSync(
+ ``,
+ );
+ await nextRender();
+ });
+
+ it('should keep has-helper while the AI is working', async () => {
+ const helperMarker = mark(helperField, { confidence: 'low' });
+ await nextRender();
+
+ helperMarker.working = true;
+ await nextUpdate(helperMarker);
+
+ expect(helperField.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should keep has-helper when the confidence is cleared', async () => {
+ const helperMarker = mark(helperField, { confidence: 'low' });
+ await nextRender();
+
+ helperMarker.confidence = null;
+ await nextUpdate(helperMarker);
+
+ expect(helperField.hasAttribute('has-helper')).to.be.true;
+ });
+
+ it('should not touch has-helper when no indicator was ever shown', async () => {
+ const helperMarker = mark(helperField);
+ await nextRender();
+
+ helperMarker.working = true;
+ await nextUpdate(helperMarker);
+ helperMarker.working = false;
+ await nextUpdate(helperMarker);
+
+ expect(helperField.hasAttribute('has-helper')).to.be.true;
+ });
+ });
+
+ it('should stop re-asserting has-helper once the indicator is gone', async () => {
+ marker.confidence = null;
+ await nextUpdate(marker);
+
+ field.helperText = 'Keep it short';
+ await nextRender();
+ field.helperText = '';
+ await nextRender();
+
+ expect(field.hasAttribute('has-helper')).to.be.false;
+ });
+ });
+
+ it('should update the indicator when the confidence changes', async () => {
+ marker.confidence = 'high';
+ await nextUpdate(marker);
+
+ const node = getConfidenceNode()!;
+ expect(node.classList.contains('ai-confidence-high')).to.be.true;
+ expect(node.classList.contains('ai-confidence-low')).to.be.false;
+ expect(node.textContent).to.equal('High confidence');
+ });
+
+ it('should reuse the indicator element when the confidence changes', async () => {
+ const node = getConfidenceNode();
+ marker.confidence = 'high';
+ await nextUpdate(marker);
+
+ expect(getConfidenceNode()).to.equal(node);
+ expect(field.querySelectorAll(':scope > [slot="helper"].ai-confidence')).to.have.lengthOf(1);
+ });
+
+ it('should clear the indicator on a field with no described element', async () => {
+ const bareField = fixtureSync(``);
+ await nextRender();
+ const bareMarker = mark(bareField, { confidence: 'low' });
+ await nextRender();
+ expect(bareField.hasAttribute('has-helper')).to.be.true;
+
+ bareMarker.confidence = null;
+ await nextUpdate(bareMarker);
+
+ expect(bareField.querySelector('.ai-confidence')).to.be.null;
+ expect(bareField.hasAttribute('has-helper')).to.be.false;
+ });
+
+ it('should remove the indicator when the confidence is cleared', async () => {
+ marker.confidence = null;
+ await nextUpdate(marker);
+
+ expect(getConfidenceNode()).to.be.null;
+ });
+
+ it('should remove the indicator when the marker is removed', async () => {
+ marker.remove();
+ await nextRender();
+
+ expect(getConfidenceNode()).to.be.null;
+ });
+
+ it('should render the indicator when confidence is set after adding', async () => {
+ const plainField = fixtureSync(``);
+ await nextRender();
+ const plainMarker = mark(plainField);
+ await nextRender();
+ expect(getConfidenceNode(plainField)).to.be.null;
+
+ plainMarker.confidence = 'medium';
+ await nextUpdate(plainMarker);
+
+ const node = getConfidenceNode(plainField)!;
+ expect(node.textContent).to.equal('Medium confidence');
+ });
+
+ it('should render the indicator again when the marker is re-added', async () => {
+ marker.remove();
+ await nextRender();
+ field.appendChild(marker);
+ await nextRender();
+
+ expect(getConfidenceNode()).to.exist;
+ expect(getConfidenceNode()!.classList.contains('ai-confidence-low')).to.be.true;
+ });
+
+ it('should keep the field helper text alongside the indicator', async () => {
+ field.helperText = 'Keep it short';
+ await nextRender();
+
+ const helper = field.querySelector(':scope > [slot="helper"]:not(.ai-confidence)')!;
+ expect(helper.textContent).to.equal('Keep it short');
+ expect(helper.assignedSlot).to.exist;
+ expect(getConfidenceNode()!.assignedSlot).to.exist;
+ });
+
+ it('should not evict an existing field helper', async () => {
+ const helperField = fixtureSync(
+ ``,
+ );
+ await nextRender();
+ const helper = helperField.querySelector(':scope > [slot="helper"]')!;
+
+ mark(helperField, { confidence: 'high' });
+ await nextRender();
+
+ expect(helper.isConnected).to.be.true;
+ expect(helper.assignedSlot).to.exist;
+ expect(getConfidenceNode(helperField)).to.exist;
+ });
+
+ describe('order', () => {
+ let helperField: TextField;
+
+ /** The helper slot content in rendered order. */
+ function getHelperNodes(host: HTMLElement = helperField): Element[] {
+ return [...host.querySelectorAll(':scope > [slot="helper"]')];
+ }
+
+ beforeEach(async () => {
+ helperField = fixtureSync(
+ ``,
+ );
+ await nextRender();
+ });
+
+ it('should render the indicator before an existing field helper', async () => {
+ mark(helperField, { confidence: 'low' });
+ await nextRender();
+
+ expect(getHelperNodes()[0]).to.equal(getConfidenceNode(helperField));
+ expect(getHelperNodes().map((node) => node.textContent)).to.eql(['Low confidence', 'Keep it short']);
+ });
+
+ it('should render the indicator before a helper added after marking', async () => {
+ const plainField = fixtureSync(``);
+ await nextRender();
+ mark(plainField, { confidence: 'low' });
+ await nextRender();
+
+ plainField.helperText = 'Keep it short';
+ await nextRender();
+
+ expect(getHelperNodes(plainField).map((node) => node.textContent)).to.eql(['Low confidence', 'Keep it short']);
+ });
+
+ it('should render the indicator before a custom slotted helper', async () => {
+ const customHelper = document.createElement('span');
+ customHelper.setAttribute('slot', 'helper');
+ customHelper.textContent = 'Custom helper';
+ helperField.appendChild(customHelper);
+ await nextRender();
+
+ mark(helperField, { confidence: 'low' });
+ await nextRender();
+
+ expect(getHelperNodes()[0]).to.equal(getConfidenceNode(helperField));
+ });
+
+ it('should keep the indicator first when the field is marked again', async () => {
+ mark(helperField, { confidence: 'low' }).remove();
+ // Re-marking right away is what a host does when a new AI fill starts.
+ mark(helperField, { confidence: 'high' });
+ await nextRender();
+
+ expect(getHelperNodes().map((node) => node.textContent)).to.eql(['High confidence', 'Keep it short']);
+ });
+
+ it('should keep the indicator first across consecutive AI fills', async () => {
+ // A host filling the same field twice: unmark, re-mark as working,
+ // then let the fill land.
+ for (const confidence of ['low', 'high'] as const) {
+ helperField.querySelector('vaadin-ai-field-marker')?.remove();
+ const marker = mark(helperField, { confidence, working: true });
+ await nextRender();
+ marker.working = false;
+ await nextUpdate(marker);
+ }
+
+ expect(getHelperNodes().map((node) => node.textContent)).to.eql(['High confidence', 'Keep it short']);
+ });
+ });
+
+ it('should describe the field input via aria-describedby', () => {
+ const ids = field.inputElement.getAttribute('aria-describedby')!.split(' ');
+ expect(ids).to.include(getConfidenceNode()!.id);
+ });
+
+ it('should remove the indicator description when the confidence is cleared', async () => {
+ const nodeId = getConfidenceNode()!.id;
+ marker.confidence = null;
+ await nextUpdate(marker);
+
+ expect(field.inputElement.getAttribute('aria-describedby') || '').to.not.contain(nodeId);
+ });
+
+ it('should remove the indicator description while the AI is working', async () => {
+ // A hidden indicator would still get read as part of the field's
+ // description, although it describes a value about to be replaced.
+ const nodeId = getConfidenceNode()!.id;
+
+ marker.working = true;
+ await nextUpdate(marker);
+ expect(field.inputElement.getAttribute('aria-describedby') || '').to.not.contain(nodeId);
+
+ marker.working = false;
+ await nextUpdate(marker);
+ expect(field.inputElement.getAttribute('aria-describedby')!.split(' ')).to.include(nodeId);
+ });
+
+ it('should not describe an indicator shown while the AI is working', async () => {
+ marker.confidence = null;
+ await nextUpdate(marker);
+ marker.working = true;
+ await nextUpdate(marker);
+
+ marker.confidence = 'high';
+ await nextUpdate(marker);
+ const nodeId = getConfidenceNode()!.id;
+ expect(field.inputElement.getAttribute('aria-describedby') || '').to.not.contain(nodeId);
+
+ marker.working = false;
+ await nextUpdate(marker);
+ expect(field.inputElement.getAttribute('aria-describedby')!.split(' ')).to.include(nodeId);
+ });
+
+ it('should hide the indicator while the AI is working', async () => {
+ marker.working = true;
+ await nextUpdate(marker);
+
+ expect(getComputedStyle(getConfidenceNode()!).display).to.equal('none');
+ });
+
+ it('should apply localized confidence texts', async () => {
+ marker.i18n = { confidence: { low: 'Matala luottamus' } };
+ await nextUpdate(marker);
+
+ expect(getConfidenceNode()!.textContent).to.equal('Matala luottamus');
+ });
+
+ it('should keep the default texts for levels not provided', async () => {
+ marker.i18n = { confidence: { high: 'Korkea luottamus' } };
+ await nextUpdate(marker);
+
+ expect(getConfidenceNode()!.textContent).to.equal('Low confidence');
+ });
+ });
+
describe('revert', () => {
let marker: AiFieldMarker;
let revertButton: HTMLButtonElement;
@@ -995,6 +1440,27 @@ describe('ai field marker', () => {
expect(spy.firstCall.args[0].detail.value).to.equal('Other value');
});
+ it('should re-assert has-helper on the field it was moved to', async () => {
+ const otherField = fixtureSync(``);
+ await nextRender();
+ const marker = mark(field, { confidence: 'low' });
+ await nextRender();
+
+ marker.remove();
+ otherField.appendChild(marker);
+ await nextRender();
+
+ // Make the new field recompute has-helper from its own helper content,
+ // which does not include the indicator.
+ otherField.helperText = 'Keep it short';
+ await nextRender();
+ otherField.helperText = '';
+ await nextRender();
+
+ expect(otherField.hasAttribute('has-helper')).to.be.true;
+ expect(field.hasAttribute('has-helper')).to.be.false;
+ });
+
describe('to a field with no described element', () => {
// The marker keeps no state of the field it was attached to before, so a
// field that provides none of its own can not end up with the previous
diff --git a/packages/field-highlighter/test/dom/__snapshots__/ai-field-marker.test.snap.js b/packages/field-highlighter/test/dom/__snapshots__/ai-field-marker.test.snap.js
index ef5b60ccb12..8e56e634264 100644
--- a/packages/field-highlighter/test/dom/__snapshots__/ai-field-marker.test.snap.js
+++ b/packages/field-highlighter/test/dom/__snapshots__/ai-field-marker.test.snap.js
@@ -69,3 +69,15 @@ snapshots["vaadin-ai-field-marker host unmarked"] =
`;
/* end snapshot vaadin-ai-field-marker host unmarked */
+snapshots["vaadin-ai-field-marker host confidence"] =
+`
+ High confidence
+
+`;
+/* end snapshot vaadin-ai-field-marker host confidence */
+
diff --git a/packages/field-highlighter/test/dom/ai-field-marker.test.ts b/packages/field-highlighter/test/dom/ai-field-marker.test.ts
index 5c952e1a957..f53b8f5fa2a 100644
--- a/packages/field-highlighter/test/dom/ai-field-marker.test.ts
+++ b/packages/field-highlighter/test/dom/ai-field-marker.test.ts
@@ -30,5 +30,13 @@ describe('vaadin-ai-field-marker', () => {
await nextRender();
await expect(marker).dom.to.equalSnapshot();
});
+
+ it('confidence', async () => {
+ marker.confidence = 'high';
+ await nextRender();
+ // The indicator is a sibling of the marker slotted into the field's
+ // helper text section.
+ await expect(field.querySelector(':scope > [slot="helper"]')!).dom.to.equalSnapshot();
+ });
});
});
diff --git a/packages/field-highlighter/test/typings/ai-field-marker.types.ts b/packages/field-highlighter/test/typings/ai-field-marker.types.ts
index 65f250d663c..5f1a5bccd97 100644
--- a/packages/field-highlighter/test/typings/ai-field-marker.types.ts
+++ b/packages/field-highlighter/test/typings/ai-field-marker.types.ts
@@ -12,6 +12,10 @@ assertType(aiFieldMarkerHostStyles);
const marker = document.createElement('vaadin-ai-field-marker');
assertType(marker.i18n);
assertType(marker.working);
+assertType<'high' | 'low' | 'medium' | null>(marker.confidence);
+assertType(marker.i18n.confidence?.low);
+assertType(marker.i18n.confidence?.medium);
+assertType(marker.i18n.confidence?.high);
// Revert event
assertType>({} as AiFieldRevertEvent);
diff --git a/packages/field-highlighter/test/visual/ai-field-marker.common.ts b/packages/field-highlighter/test/visual/ai-field-marker.common.ts
index f87841d6ceb..08c03b54c98 100644
--- a/packages/field-highlighter/test/visual/ai-field-marker.common.ts
+++ b/packages/field-highlighter/test/visual/ai-field-marker.common.ts
@@ -87,6 +87,44 @@ describe('ai-field-marker', () => {
});
});
+ describe('confidence', () => {
+ beforeEach(async () => {
+ await createField();
+ });
+
+ (['low', 'medium', 'high'] as const).forEach((confidence) => {
+ it(confidence, async () => {
+ mark({ confidence });
+ await nextRender();
+ await visualDiff(div, `ai-marker-confidence-${confidence}`);
+ });
+ });
+
+ it('helper text', async () => {
+ // The indicator comes first in the helper text section.
+ field.helperText = 'Keep it short, just one value';
+ mark({ confidence: 'medium' });
+ await nextRender();
+ await visualDiff(div, 'ai-marker-confidence-helper-text');
+ });
+
+ it('helper above field', async () => {
+ // The helper text section, indicator included, moves above the field.
+ field.setAttribute('theme', 'helper-above-field');
+ field.helperText = 'Keep it short, just one value';
+ mark({ confidence: 'medium' });
+ await nextRender();
+ await visualDiff(div, 'ai-marker-confidence-helper-above-field');
+ });
+
+ it('helper above field without helper text', async () => {
+ field.setAttribute('theme', 'helper-above-field');
+ mark({ confidence: 'medium' });
+ await nextRender();
+ await visualDiff(div, 'ai-marker-confidence-helper-above-field-only');
+ });
+ });
+
describe('working', () => {
beforeEach(async () => {
await createField();
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png
new file mode 100644
index 00000000000..45dfe96d403
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png
new file mode 100644
index 00000000000..b805f615087
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-text.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-text.png
new file mode 100644
index 00000000000..6c3e77006b4
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-helper-text.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-high.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-high.png
new file mode 100644
index 00000000000..6b218c42692
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-high.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-low.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-low.png
new file mode 100644
index 00000000000..fe9bbc1240b
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-low.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-medium.png b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-medium.png
new file mode 100644
index 00000000000..8a27fe85173
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/dark/ai-field-marker/baseline/ai-marker-confidence-medium.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png
new file mode 100644
index 00000000000..61a340fd436
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png
new file mode 100644
index 00000000000..d7ffa2f7c26
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-text.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-text.png
new file mode 100644
index 00000000000..b3f448422f2
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-helper-text.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-high.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-high.png
new file mode 100644
index 00000000000..28b4f01155e
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-high.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-low.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-low.png
new file mode 100644
index 00000000000..f5b4228cb07
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-low.png differ
diff --git a/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-medium.png b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-medium.png
new file mode 100644
index 00000000000..df31f800746
Binary files /dev/null and b/packages/field-highlighter/test/visual/aura/screenshots/default/ai-field-marker/baseline/ai-marker-confidence-medium.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png
new file mode 100644
index 00000000000..787cb6de520
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png
new file mode 100644
index 00000000000..869fd9edbba
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png
new file mode 100644
index 00000000000..eb6f41dd187
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png
new file mode 100644
index 00000000000..affd4e75c9d
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png
new file mode 100644
index 00000000000..f5b8a231a6f
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png differ
diff --git a/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png
new file mode 100644
index 00000000000..574cbe07f42
Binary files /dev/null and b/packages/field-highlighter/test/visual/base/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png
new file mode 100644
index 00000000000..2cb179fe788
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field-only.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png
new file mode 100644
index 00000000000..01dc2e983aa
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-above-field.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png
new file mode 100644
index 00000000000..13e901c8d04
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-helper-text.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png
new file mode 100644
index 00000000000..ee547088db6
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-high.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png
new file mode 100644
index 00000000000..7f881ce7c7f
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-low.png differ
diff --git a/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png
new file mode 100644
index 00000000000..53f62e302bf
Binary files /dev/null and b/packages/field-highlighter/test/visual/lumo/screenshots/ai-field-marker/baseline/ai-marker-confidence-medium.png differ
diff --git a/packages/vaadin-lumo-styles/src/components/ai-field-marker.css b/packages/vaadin-lumo-styles/src/components/ai-field-marker.css
index 481735dbd0b..278c33ffb6e 100644
--- a/packages/vaadin-lumo-styles/src/components/ai-field-marker.css
+++ b/packages/vaadin-lumo-styles/src/components/ai-field-marker.css
@@ -13,3 +13,9 @@ vaadin-ai-field-marker :is(.badge, .actions > button):focus-visible {
top: calc(var(--lumo-space-m) - 6px);
}
}
+
+:has(> vaadin-ai-field-marker) {
+ --vaadin-ai-field-marker-confidence-low-color: var(--lumo-error-text-color);
+ --vaadin-ai-field-marker-confidence-medium-color: var(--lumo-warning-text-color);
+ --vaadin-ai-field-marker-confidence-high-color: var(--lumo-success-text-color);
+}