From e86cd7cb2e6a5ed11a8da31ac8c89eae37f52e17 Mon Sep 17 00:00:00 2001 From: aramos-adobe Date: Mon, 18 May 2026 16:40:44 -0400 Subject: [PATCH 1/4] feat(response-status): adding agent steps to response status --- 2nd-gen/packages/swc/.storybook/preview.ts | 7 +- .../response-status/ResponseStatus.ts | 413 +++++++++++++++--- .../response-status/index.ts | 4 + .../response-status/response-status.css | 129 +++++- .../conversational-ai/utils/icons/index.ts | 10 + 5 files changed, 492 insertions(+), 71 deletions(-) diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 73e4673e0ab..5b94e72866d 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -330,7 +330,10 @@ const preview = { 'Rendering and styling migration analysis', ], 'Action button', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Action group', ['Rendering and styling migration analysis'], 'Action menu', @@ -363,6 +366,8 @@ const preview = { ['Rendering and styling migration analysis'], 'Checkbox', ['Rendering and styling migration analysis'], + 'Close button', + ['Accessibility migration analysis'], 'Color field', ['Rendering and styling migration analysis'], 'Color loupe', diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts index 3823a92689d..0da57dfccda 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts @@ -21,55 +21,83 @@ import '@adobe/spectrum-wc/components/icon/swc-icon.js'; import '@adobe/spectrum-wc/components/progress-circle/swc-progress-circle.js'; import { uniqueId } from '../../../utils/id.js'; -import { CheckCircleIcon } from '../utils/icons/index.js'; +import { + CheckCircleIcon, + CircleOutlineIcon, + ThreeDotsIcon, +} from '../utils/icons/index.js'; +import { + ResponseStatusStep, + type ResponseStatusStepKind, + type ResponseStatusStepStatus, +} from './response-status-step/ResponseStatusStep.js'; import styles from './response-status.css'; +export type ResponseStatusPhase = 'initiating' | 'processing' | 'complete'; + +export type ResponseStatusStepData = { + title: string; + detail: string; + kind: ResponseStatusStepKind; + status: ResponseStatusStepStatus; +}; + /** * Displays the current status of an AI response generation. * - * While **`loading`** is `true`, reasoning is not shown. + * **Legacy mode** — default slot text + `loading` / `open` (progress circle + reasoning). + * + * **Agentic mode** — one or more `` children; uses `phase`, rolling + * header title, and an expandable step timeline. * * @element swc-response-status - * @slot - Optional reasoning content. Disclosure UI is shown only when slot has content - * and `loading` is `false`; content is visible when `open` is `true`. - * If slot reasoning content is removed while `open=true`, the component collapses itself - * by setting `open=false`. - * @fires swc-response-status-toggle - Dispatched when the user expands or collapses the reasoning panel. + * @slot - Reasoning prose (legacy) or `` elements (agentic) + * @fires swc-response-status-toggle - Dispatched when the user expands or collapses the panel. * Detail: `{ open: boolean }` */ export class ResponseStatus extends SpectrumElement { - private readonly reasoningPanelId = uniqueId('swc-reasoning-panel'); + private readonly panelId = uniqueId('swc-response-status-panel'); + @state() private _hasReasoningContent = false; - /** `true`: progress circle + status label, `false`: checkmark + status label. */ + @state() + private _steps: ResponseStatusStepData[] = []; + + /** `true`: progress circle + status label (legacy), or processing phase (agentic). */ @property({ type: Boolean, reflect: true }) public loading = false; /** - * Status row label text shown while `loading=true`. + * Agentic lifecycle phase. When unset, derived from `loading` and step children. */ @property({ type: String, reflect: true }) - public loadingLabel = 'Generating response'; + public phase: ResponseStatusPhase | '' = ''; /** - * Status row label text shown while `loading=false`. + * Elapsed seconds for the complete-phase summary (`Thought for N seconds`). */ + @property({ type: Number, reflect: true }) + public duration = 0; + + /** Label when `phase="initiating"` (or processing with no steps yet). */ + @property({ type: String, attribute: 'initiating-label', reflect: true }) + public initiatingLabel = 'Processing request'; + + /** Status row label text shown while loading / processing without an active step. */ + @property({ type: String, reflect: true }) + public loadingLabel = 'Generating response'; + + /** Status row label text when complete (overrides duration summary when set explicitly). */ @property({ type: String, reflect: true }) public completeLabel = 'Response generated'; - /** - * Accessible label for the reasoning content group. - */ + /** Accessible label for the step list / reasoning group. */ @property({ type: String, attribute: 'reasoning-label' }) public reasoningLabel = 'Reasoning'; - /** - * `true`: reasoning expanded; `false`: reasoning collapsed. - * Ignored while `loading` is `true`. If reasoning slot content is removed, - * `open` is automatically set to `false` and no `swc-response-status-toggle` event is emitted. - */ + /** `true`: step timeline or reasoning expanded. */ @property({ type: Boolean, reflect: true }) public open = false; @@ -78,11 +106,44 @@ export class ResponseStatus extends SpectrumElement { } protected override firstUpdated(): void { - this._syncReasoningContent(); + this._syncSlotContent(); + } + + private get _isAgentic(): boolean { + return ( + this._steps.length > 0 || + this.phase === 'initiating' || + this.phase === 'processing' || + this.phase === 'complete' + ); + } + + private get _effectivePhase(): ResponseStatusPhase { + if ( + this.phase === 'initiating' || + this.phase === 'processing' || + this.phase === 'complete' + ) { + return this.phase; + } + if (this.loading) { + return this._steps.length > 0 ? 'processing' : 'initiating'; + } + return 'complete'; + } + + private get _showPanel(): boolean { + if (this._isAgentic) { + return this._steps.length > 0; + } + return !this.loading && this._hasReasoningContent; } private _handleToggle(): void { - if (this.loading || !this._hasReasoningContent) { + if (!this._showPanel) { + return; + } + if (!this._isAgentic && this.loading) { return; } this.open = !this.open; @@ -95,15 +156,44 @@ export class ResponseStatus extends SpectrumElement { ); } - private _getStatusLabel(): string { + private _getLegacyStatusLabel(): string { return this.loading ? this.loadingLabel : this.completeLabel; } + private _getActiveStep(): ResponseStatusStepData | undefined { + const active = this._steps.filter((step) => step.status === 'active'); + if (active.length > 0) { + return active[active.length - 1]; + } + return this._steps.find((step) => step.status === 'pending'); + } + + private _getAgenticHeaderLabel(): string { + const phase = this._effectivePhase; + + if (phase === 'initiating') { + return this.initiatingLabel; + } + + if (phase === 'processing') { + return this._getActiveStep()?.title?.trim() || this.loadingLabel; + } + + if (this.duration > 0) { + return `Thought for ${this.duration} seconds`; + } + + return this.completeLabel; + } + private _slotHasReasoningContent(slot: HTMLSlotElement | null): boolean { if (!slot) { return false; } for (const node of slot.assignedNodes({ flatten: true })) { + if (node instanceof ResponseStatusStep) { + continue; + } if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) { return true; } @@ -115,23 +205,202 @@ export class ResponseStatus extends SpectrumElement { return false; } - private _syncReasoningContent(slot?: HTMLSlotElement): void { - const reasoningSlot = + private _readSteps(slot: HTMLSlotElement | null): ResponseStatusStepData[] { + if (!slot) { + return []; + } + return slot + .assignedElements({ flatten: true }) + .filter( + (element): element is ResponseStatusStep => + element instanceof ResponseStatusStep + ) + .map((step) => ({ + title: step.title.trim(), + detail: step.detail.trim(), + kind: step.kind, + status: step.status, + })); + } + + private _syncSlotContent(slot?: HTMLSlotElement): void { + const contentSlot = slot ?? this.shadowRoot?.querySelector( - '.swc-ResponseStatus-reasoning-slot' + '.swc-ResponseStatus-content-slot' ) ?? null; - const hasReasoningContent = this._slotHasReasoningContent(reasoningSlot); - if (!hasReasoningContent && this.open) { + const steps = this._readSteps(contentSlot); + const hasReasoningContent = this._slotHasReasoningContent(contentSlot); + + if (!steps.length && !hasReasoningContent && this.open) { this.open = false; } + + this._steps = steps; this._hasReasoningContent = hasReasoningContent; } - private _handleReasoningSlotChange(event: Event): void { - this._syncReasoningContent(event.target as HTMLSlotElement); + private _handleSlotChange(event: Event): void { + this._syncSlotContent(event.target as HTMLSlotElement); + } + + private _renderThreeDots(): TemplateResult { + return html` + + `; + } + + private _renderChevron(expanded: boolean): TemplateResult { + return html` + + `; + } + + private _renderCheckmark(): TemplateResult { + return html` + + `; + } + + private _renderAgenticHeader( + label: string, + showDisclosure: boolean + ): TemplateResult { + const phase = this._effectivePhase; + const expanded = this.open; + const isComplete = phase === 'complete'; + const rowClass = [ + 'swc-ResponseStatus-row', + showDisclosure ? 'swc-ResponseStatus-row--button' : '', + phase === 'processing' ? 'swc-ResponseStatus-row--processing' : '', + isComplete ? 'swc-ResponseStatus-row--complete' : '', + ] + .filter(Boolean) + .join(' '); + + const rowContent = html` + ${isComplete ? this._renderCheckmark() : this._renderThreeDots()} + ${label} + ${showDisclosure ? this._renderChevron(expanded) : ''} + `; + + if (showDisclosure) { + return html` + + `; + } + + return html` +
+ ${rowContent} +
+ `; + } + + private _renderStepIcon(status: ResponseStatusStepStatus): TemplateResult { + if (status === 'complete') { + return html` + + `; + } + + return html` + + `; + } + + private _renderStepDetail( + kind: ResponseStatusStepKind, + detail: string + ): TemplateResult | string { + if (!detail) { + return ''; + } + const prefix = kind === 'acting' ? 'Acting' : 'Thinking'; + return html` +

${prefix} ${detail}

+ `; + } + + private _renderStepTimeline(): TemplateResult { + const lastIndex = this._steps.length - 1; + + return html` +
    + ${this._steps.map( + (step, index) => html` +
  1. +
    + ${this._renderStepIcon(step.status)} + ${index < lastIndex + ? html` + + ` + : ''} +
    +
    +

    ${step.title}

    + ${this._renderStepDetail(step.kind, step.detail)} +
    +
  2. + ` + )} +
+ `; } private _renderLoadingRow(label: string): TemplateResult { @@ -161,64 +430,86 @@ export class ResponseStatus extends SpectrumElement { class="swc-ResponseStatus-row swc-ResponseStatus-row--button" aria-label=${label} aria-expanded=${expanded} - aria-controls=${this.reasoningPanelId} + aria-controls=${this.panelId} @click=${this._handleToggle} > - + ${this._renderChevron(expanded)} ${label} - + ${this._renderCheckmark()} `; } return html`
- + ${this._renderCheckmark()} ${label}
`; } + private _renderPanel(showPanel: boolean): TemplateResult { + const panelLabel = this.reasoningLabel; + + return html` +
+ ${this._isAgentic + ? this._renderStepTimeline() + : html` + + `} +
+ `; + } + protected override render(): TemplateResult { + const showPanel = this._showPanel; + const showDisclosure = showPanel; + + if (this._isAgentic) { + const headerLabel = this._getAgenticHeaderLabel(); + return html` +
+ ${this._renderAgenticHeader(headerLabel, showDisclosure)} + ${this._renderPanel(showPanel)} + +
+ `; + } + const isLoading = this.loading; - const statusLabel = this._getStatusLabel(); - const showDisclosure = !isLoading && this._hasReasoningContent; + const statusLabel = this._getLegacyStatusLabel(); + const legacyShowDisclosure = !isLoading && this._hasReasoningContent; return html`
${isLoading ? this._renderLoadingRow(statusLabel) - : this._renderCompleteRow(statusLabel, showDisclosure)} + : this._renderCompleteRow(statusLabel, legacyShowDisclosure)}
diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/index.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/index.ts index e6685d9f46d..d300d0a9a11 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/index.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/index.ts @@ -11,14 +11,18 @@ */ import { defineElement } from '@spectrum-web-components/core/element/index.js'; +import { ResponseStatusStep } from './response-status-step/ResponseStatusStep.js'; import { ResponseStatus } from './ResponseStatus.js'; export * from './ResponseStatus.js'; +export * from './response-status-step/ResponseStatusStep.js'; declare global { interface HTMLElementTagNameMap { 'swc-response-status': ResponseStatus; + 'swc-response-status-step': ResponseStatusStep; } } defineElement('swc-response-status', ResponseStatus); +defineElement('swc-response-status-step', ResponseStatusStep); diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status.css b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status.css index 58047d8b92b..63c6f403bf6 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status.css +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status.css @@ -39,7 +39,8 @@ /* When the row is a button (reasoning toggle) */ .swc-ResponseStatus-row--button { - inline-size: fit-content; + inline-size: 100%; + max-inline-size: 580px; padding: var(--swc-sources-toggle-padding, 3px) token("spacing-100"); background: transparent; border: none; @@ -48,14 +49,6 @@ transition: background token("animation-duration-100") token("animation-ease-in-out"); } -/* Progress circle — indeterminate loading indicator */ - -.swc-ResponseStatus-loadingSlot { - display: inline-flex; - flex-shrink: 0; - align-items: center; -} - .swc-ResponseStatus-label { font-family: token("sans-serif-font"); font-size: token("font-size-100"); @@ -64,6 +57,19 @@ color: token("neutral-subdued-content-color-default"); } +.swc-ResponseStatus-row--button .swc-ResponseStatus-label { + flex: 1; + text-align: start; +} + +/* Progress circle — indeterminate loading indicator */ + +.swc-ResponseStatus-loadingSlot { + display: inline-flex; + flex-shrink: 0; + align-items: center; +} + .swc-ResponseStatus-row--button:hover .swc-ResponseStatus-label { color: token("gray-800"); } @@ -110,3 +116,108 @@ visibility: visible; block-size: auto; } + +/* ───────────────────────────────────── + Agentic mode (spike) + ───────────────────────────────────── */ + +.swc-ResponseStatus--agentic { + inline-size: 100%; + max-inline-size: 580px; +} + +.swc-ResponseStatus-row--processing, +.swc-ResponseStatus-row--button.swc-ResponseStatus-row--processing { + padding-inline: token("spacing-100"); + background: token("gray-75"); + border-radius: token("corner-radius-medium-default"); +} + +.swc-ResponseStatus-row--complete.swc-ResponseStatus-row--button { + padding-inline: token("spacing-50"); + background: token("gray-100"); +} + +.swc-ResponseStatus-dots { + flex-shrink: 0; +} + +.swc-ResponseStatus-check { + flex-shrink: 0; +} + +.swc-ResponseStatus-panel { + interpolate-size: allow-keywords; + display: block; + visibility: hidden; + block-size: 0; + overflow: hidden; + transition: + block-size token("animation-duration-200") token("animation-ease-in-out"), + visibility token("animation-duration-200") linear; +} + +.swc-ResponseStatus-row--button[aria-expanded="true"] + .swc-ResponseStatus-panel { + visibility: visible; + block-size: auto; +} + +.swc-ResponseStatus-steps { + padding: token("spacing-100") 0 0; + margin: 0; + list-style: none; +} + +.swc-ResponseStatus-step { + display: flex; + gap: token("spacing-100"); + align-items: flex-start; +} + +.swc-ResponseStatus-step + .swc-ResponseStatus-step { + margin-block-start: token("spacing-200"); +} + +.swc-ResponseStatus-step-rail { + display: flex; + flex-shrink: 0; + flex-direction: column; + align-items: center; + inline-size: 20px; +} + +.swc-ResponseStatus-step-line { + flex: 1; + inline-size: 1px; + min-block-size: token("spacing-400"); + margin-block: token("spacing-50"); + background: token("gray-300"); +} + +.swc-ResponseStatus-step-icon--active { + color: token("gray-800"); +} + +.swc-ResponseStatus-step-body { + flex: 1; + min-inline-size: 0; + padding-block: 2px; +} + +.swc-ResponseStatus-step-title { + margin: 0; + font-family: token("sans-serif-font"); + font-size: token("font-size-75"); + font-weight: token("bold-font-weight"); + line-height: token("line-height-font-size-75"); + color: token("neutral-content-color-default"); +} + +.swc-ResponseStatus-step-detail { + margin: token("spacing-75") 0 0; + font-family: token("sans-serif-font"); + font-size: token("font-size-75"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-font-size-100"); + color: token("neutral-subdued-content-color-default"); +} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/utils/icons/index.ts b/2nd-gen/packages/swc/patterns/conversational-ai/utils/icons/index.ts index 4f36d07950e..5f8c7e463c5 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/utils/icons/index.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/utils/icons/index.ts @@ -69,6 +69,16 @@ export const ThreeDotsIcon = (): TemplateResult => html` `; +/** Circle outline — used for pending/active agentic steps. */ +export const CircleOutlineIcon = (): TemplateResult => html` + + + +`; + /** Check-circle icon — used for "response generated" status. */ export const CheckCircleIcon = (): TemplateResult => html` From a128b451d4cce678adeb4a9b4167403e0e6300bc Mon Sep 17 00:00:00 2001 From: aramos-adobe Date: Tue, 19 May 2026 05:19:46 -0400 Subject: [PATCH 2/4] feat(response-status): styling for design parity --- .../response-status/ResponseStatus.ts | 34 ++---- .../response-status/response-status.css | 102 ++++++++++++++---- .../stories/response-status.stories.ts | 6 +- 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts index 0da57dfccda..ea5396a43cb 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/ResponseStatus.ts @@ -18,14 +18,9 @@ import { Chevron75Icon } from '@adobe/spectrum-wc/icon/elements/index.js'; import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; import '@adobe/spectrum-wc/components/icon/swc-icon.js'; -import '@adobe/spectrum-wc/components/progress-circle/swc-progress-circle.js'; import { uniqueId } from '../../../utils/id.js'; -import { - CheckCircleIcon, - CircleOutlineIcon, - ThreeDotsIcon, -} from '../utils/icons/index.js'; +import { CheckCircleIcon, CircleOutlineIcon } from '../utils/icons/index.js'; import { ResponseStatusStep, type ResponseStatusStepKind, @@ -248,13 +243,11 @@ export class ResponseStatus extends SpectrumElement { private _renderThreeDots(): TemplateResult { return html` - + `; } @@ -335,11 +328,7 @@ export class ResponseStatus extends SpectrumElement { private _renderStepIcon(status: ResponseStatusStepStatus): TemplateResult { if (status === 'complete') { return html` -