diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 73e4673e0ab..49abc731597 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', @@ -360,9 +363,14 @@ const preview = { 'Rendering and styling migration analysis', ], 'Button group', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + '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', @@ -391,12 +399,16 @@ const preview = { 'Rendering and styling migration analysis', ], 'Infield button', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Infield progress circle', ['Rendering and styling migration analysis'], 'Link', [ 'Accessibility migration analysis', + 'Migration plan', 'Rendering and styling migration analysis', ], 'Menu', diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/agentic-video-flow-script.ts b/2nd-gen/packages/swc/patterns/conversational-ai/agentic-video-flow-script.ts new file mode 100644 index 00000000000..b68480581c2 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/agentic-video-flow-script.ts @@ -0,0 +1,64 @@ +/** + * 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 type { ResponseStatusStepKind } from './response-status/response-status-step/ResponseStatusStep.js'; + +export type AgenticVideoFlowStepData = { + title: string; + detail: string; + kind: ResponseStatusStepKind; +}; + +/** Step titles and details from the reference agentic flow recording. */ +export const AGENTIC_VIDEO_FLOW_STEPS: AgenticVideoFlowStepData[] = [ + { + title: 'Looked through documentation', + detail: + 'Scanned 12 internal knowledge base articles matching the query context and extracted key sections.', + kind: 'thinking', + }, + { + title: 'Searching web for: Carnival cruise trip packages Europe Asia', + detail: + 'Found 8 relevant results across travel aggregators and official cruise line sites.', + kind: 'acting', + }, + { + title: 'Searching repositories for Europe trips', + detail: + 'Checked 3 internal repositories for previously compiled trip package data and pricing templates.', + kind: 'acting', + }, + { + title: 'Compose response', + detail: + 'Synthesizing findings into a structured comparison of available packages with pricing and availability.', + kind: 'thinking', + }, +]; + +/** Milliseconds from generation start (initiating). */ +export const AGENTIC_VIDEO_FLOW_TIMING = { + processing: 2000, + streamText: 3500, + step1: 4500, + step2: 7500, + step3: 10000, + expand: 10000, + step4: 12000, + collapse: 17000, + complete: 18000, + loopRestart: 23000, +} as const; + +export const agenticVideoGreeting = (_prompt: string): string => + 'Hello! How can I help you today?'; diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/conversation-thread/stories/agentic-conversation-flow.stories.ts b/2nd-gen/packages/swc/patterns/conversational-ai/conversation-thread/stories/agentic-conversation-flow.stories.ts new file mode 100644 index 00000000000..f6853908c9c --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/conversation-thread/stories/agentic-conversation-flow.stories.ts @@ -0,0 +1,474 @@ +/** + * 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 { html, LitElement } from 'lit'; +import { state } from 'lit/decorators.js'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; + +import '../index.js'; +import '../../conversation-turn/index.js'; +import '../../system-message/index.js'; +import '../../user-message/index.js'; +import '../../response-status/index.js'; +import '../../prompt-field/index.js'; + +import { + AGENTIC_VIDEO_FLOW_STEPS, + AGENTIC_VIDEO_FLOW_TIMING, + agenticVideoGreeting, +} from '../../agentic-video-flow-script.js'; +import type { + ResponseStatusStepKind, + ResponseStatusStepStatus, +} from '../../response-status/response-status-step/ResponseStatusStep.js'; +import type { ResponseStatusPhase } from '../../response-status/ResponseStatus.js'; + +type AgenticStep = { + title: string; + detail: string; + kind: ResponseStatusStepKind; + status: ResponseStatusStepStatus; +}; + +type DemoTurn = { + id: string; + role: 'user' | 'system'; + text: string; + loading?: boolean; + agenticPhase?: ResponseStatusPhase | ''; + agenticSteps?: AgenticStep[]; + agenticDuration?: number; + statusOpen?: boolean; +}; + +const buildAssistantReply = (prompt: string): string => { + const greeting = agenticVideoGreeting(prompt); + const normalized = prompt.trim() || 'your request'; + if (/^hello\b/i.test(normalized)) { + return greeting; + } + return `Great direction. Based on "${normalized}", I suggest a 12-slide structure with a clear narrative arc, three supporting proof points, and a concise close with next steps.`; +}; + +const AGENTIC_STEP_SCRIPT: Omit[] = + AGENTIC_VIDEO_FLOW_STEPS; + +class AgenticConversationFlowDemo extends LitElement { + @state() + private turns: DemoTurn[] = [ + { + id: 'user-1', + role: 'user', + text: 'Can you help me create a 45-minute presentation?', + }, + { + id: 'system-1', + role: 'system', + text: 'I interpreted your request as an executive narrative task and prioritized a concise, audience-ready structure.', + agenticPhase: 'complete', + agenticDuration: 16, + agenticSteps: AGENTIC_STEP_SCRIPT.map((step) => ({ + ...step, + status: 'complete', + })), + }, + ]; + + @state() + private promptValue = ''; + + @state() + private isGenerating = false; + + private generationTimers: number[] = []; + private generationStartedAt = 0; + private responseTargetId: string | null = null; + private lastPrompt = ''; + + public override disconnectedCallback(): void { + this._clearGenerationTimers(); + super.disconnectedCallback(); + } + + protected override createRenderRoot(): HTMLElement { + return this; + } + + protected override updated(): void { + requestAnimationFrame(() => { + const scrollEl = this.querySelector( + '.swc-AgenticConversationFlowDemo-scroll' + ); + if (scrollEl) { + scrollEl.scrollTop = scrollEl.scrollHeight; + } + }); + } + + private _clearGenerationTimers(): void { + for (const timerId of this.generationTimers) { + window.clearTimeout(timerId); + } + this.generationTimers = []; + } + + private _schedule(delayMs: number, run: () => void): void { + const timerId = window.setTimeout(run, delayMs); + this.generationTimers.push(timerId); + } + + private _patchTurn(targetId: string, patch: Partial): void { + this.turns = this.turns.map((turn) => + turn.id === targetId ? { ...turn, ...patch } : turn + ); + } + + private _stepsThroughActive(activeIndex: number): AgenticStep[] { + return AGENTIC_STEP_SCRIPT.map((step, index) => { + let status: ResponseStatusStepStatus = 'pending'; + if (index < activeIndex) { + status = 'complete'; + } else if (index === activeIndex) { + status = 'active'; + } + return { ...step, status }; + }); + } + + private _startAgenticGeneration(targetId: string): void { + this._clearGenerationTimers(); + this.generationStartedAt = Date.now(); + + this._patchTurn(targetId, { + loading: true, + agenticPhase: 'initiating', + agenticSteps: [], + agenticDuration: 0, + statusOpen: false, + }); + + const { + processing, + streamText, + step1, + step2, + step3, + step4, + collapse, + complete, + } = AGENTIC_VIDEO_FLOW_TIMING; + + this._schedule(processing, () => { + this._patchTurn(targetId, { + agenticPhase: 'processing', + agenticSteps: this._stepsThroughActive(0), + }); + }); + + this._schedule(streamText, () => { + this._patchTurn(targetId, { + text: agenticVideoGreeting(this.lastPrompt), + }); + }); + + this._schedule(step1, () => { + this._patchTurn(targetId, { + agenticSteps: this._stepsThroughActive(1), + }); + }); + + this._schedule(step2, () => { + this._patchTurn(targetId, { + agenticSteps: this._stepsThroughActive(2), + }); + }); + + this._schedule(step3, () => { + this._patchTurn(targetId, { + statusOpen: true, + }); + }); + + this._schedule(step4, () => { + this._patchTurn(targetId, { + agenticSteps: this._stepsThroughActive(3), + }); + }); + + this._schedule(collapse, () => { + this._patchTurn(targetId, { + statusOpen: false, + }); + }); + + this._schedule(complete, () => { + const duration = Math.max( + 1, + Math.round((Date.now() - this.generationStartedAt) / 1000) + ); + this._patchTurn(targetId, { + loading: false, + agenticPhase: 'complete', + agenticDuration: duration, + agenticSteps: AGENTIC_STEP_SCRIPT.map((step) => ({ + ...step, + status: 'complete', + })), + text: buildAssistantReply(this.lastPrompt), + statusOpen: false, + }); + this.isGenerating = false; + this.responseTargetId = null; + this._clearGenerationTimers(); + }); + } + + private submitPrompt(rawValue: string): void { + const value = rawValue.trim(); + if (!value || this.isGenerating) { + return; + } + + const userTurn: DemoTurn = { + id: `user-${Date.now()}`, + role: 'user', + text: value, + }; + const systemTurn: DemoTurn = { + id: `system-${Date.now() + 1}`, + role: 'system', + text: '', + loading: true, + }; + + this.turns = [...this.turns, userTurn, systemTurn]; + this.isGenerating = true; + this.lastPrompt = value; + this.responseTargetId = systemTurn.id; + this.promptValue = ''; + this._startAgenticGeneration(systemTurn.id); + } + + private stopGeneration = (): void => { + if (!this.isGenerating || !this.responseTargetId) { + return; + } + + this._clearGenerationTimers(); + const targetId = this.responseTargetId; + this._patchTurn(targetId, { + loading: false, + agenticPhase: 'complete', + agenticDuration: Math.max( + 1, + Math.round((Date.now() - this.generationStartedAt) / 1000) + ), + text: 'Generation stopped. Update the prompt to continue.', + agenticSteps: ( + this.turns.find((t) => t.id === targetId)?.agenticSteps ?? [] + ).map((step) => + step.status === 'active' ? { ...step, status: 'complete' } : step + ), + }); + this.responseTargetId = null; + this.isGenerating = false; + }; + + private handlePromptInput = (event: Event): void => { + const inputEvent = event as CustomEvent<{ value?: string }>; + this.promptValue = inputEvent.detail?.value ?? ''; + }; + + private handlePromptSubmit = (event: Event): void => { + const submitEvent = event as CustomEvent<{ value?: string }>; + this.submitPrompt(submitEvent.detail?.value ?? ''); + }; + + private handleStatusToggle = (event: Event): void => { + const toggleEvent = event as CustomEvent<{ open?: boolean }>; + const statusHost = event.target as HTMLElement | null; + const turnId = statusHost?.getAttribute('data-status-id'); + const open = toggleEvent.detail?.open; + if (!turnId || typeof open !== 'boolean') { + return; + } + this.turns = this.turns.map((turn) => + turn.id === turnId ? { ...turn, statusOpen: open } : turn + ); + }; + + private renderAgenticStatus(turn: DemoTurn) { + const phase = + turn.agenticPhase ?? (turn.loading ? 'processing' : 'complete'); + return html` + + ${(turn.agenticSteps ?? []).map( + (step) => html` + + ` + )} + + `; + } + + private renderTurns() { + return this.turns.map((turn) => { + if (turn.role === 'user') { + return html` + + ${turn.text} + + `; + } + + const showAgentic = + turn.loading || (turn.agenticSteps && turn.agenticSteps.length > 0); + + return html` + + + ${showAgentic + ? this.renderAgenticStatus(turn) + : html` + + Draft complete. I used your latest prompt to generate this + response. + + `} + ${turn.text + ? html` +
+

${turn.text}

+
+ ` + : ''} +
+
+ `; + }); + } + + protected override render() { + this.style.cssText = + 'display:flex;flex-direction:column;block-size:90vb;max-block-size:100vh;overflow:hidden;box-sizing:border-box;'; + + return html` + +
+

+ Send a prompt (try “hello”) to match the reference flow — Processing + request (~2s), rolling step titles while collapsed, panel auto-expands + mid-run, then “Thought for N seconds”. +

+
+ + ${this.renderTurns()} + +
+
+ +
+
+ `; + } +} + +if (!customElements.get('swc-agentic-conversation-flow-demo')) { + customElements.define( + 'swc-agentic-conversation-flow-demo', + AgenticConversationFlowDemo + ); +} + +const meta: Meta = { + title: 'Conversational AI/Conversation thread/Agentic flow (demo)', + tags: ['dev'], + parameters: { + layout: 'fullscreen', + docs: { + subtitle: + 'Prompt submit drives agentic response status with timed step progression and rolling header titles.', + }, + }, +}; + +export default meta; +export { meta }; + +export const LiveFlow: Story = { + render: () => html` + + `, + parameters: { + docs: { + story: { + height: '640px', + }, + }, + }, +}; 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..bef1312fb1f 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 @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import { CSSResultArray, html, TemplateResult } from 'lit'; +import { CSSResultArray, html, PropertyValues, TemplateResult } from 'lit'; import { property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -18,58 +18,116 @@ 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 } from '../utils/icons/index.js'; +import { CheckCircleIcon, CircleOutlineIcon } 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[] = []; + + /** Active step title last shown in the header (after roll completes). */ + @state() + private _headerActiveTitle = ''; + + @state() + private _rollFrom = ''; + + @state() + private _rollTo = ''; + + @state() + private _rolling = false; + + /** Holds initiating copy before the first step roll when processing starts cold. */ + @state() + private _awaitingInitiatingRoll = false; + + private _rollTimeoutId: number | null = null; + + private _initiatingDwellTimeoutId: number | null = null; + + /** Set when the component has shown `phase="initiating"` in this run. */ + private _sawInitiatingPhase = false; + + private _prefersReducedMotion = + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + /** `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: String, reflect: true }) - public completeLabel = 'Response generated'; + @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'; /** - * Accessible label for the reasoning content group. + * Milliseconds to show {@link initiatingLabel} before the first step title roll + * when entering processing without a prior initiating phase. */ + @property({ type: Number, attribute: 'initiating-dwell-ms' }) + public initiatingDwellMs = 1000; + + /** 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 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; @@ -77,12 +135,339 @@ export class ResponseStatus extends SpectrumElement { return [styles]; } + private _handleStepChildChange = (): void => { + this._syncSlotContent(); + }; + + public override connectedCallback(): void { + super.connectedCallback(); + this.addEventListener( + 'swc-response-status-step-change', + this._handleStepChildChange + ); + } + protected override firstUpdated(): void { - this._syncReasoningContent(); + this._syncSlotContent(); + } + + private _stepsEqual( + left: ResponseStatusStepData[], + right: ResponseStatusStepData[] + ): boolean { + if (left.length !== right.length) { + return false; + } + + return left.every( + (step, index) => + step.title === right[index]?.title && + step.detail === right[index]?.detail && + step.kind === right[index]?.kind && + step.status === right[index]?.status + ); + } + + private _getActiveStepFrom( + steps: ResponseStatusStepData[] + ): ResponseStatusStepData | undefined { + const active = steps.filter((step) => step.status === 'active'); + if (active.length > 0) { + return active[active.length - 1]; + } + return steps.find((step) => step.status === 'pending'); + } + + private _getActiveStepTitle(steps: ResponseStatusStepData[]): string { + return this._getActiveStepFrom(steps)?.title?.trim() ?? ''; + } + + private _isStepElement(element: Element): element is ResponseStatusStep { + return ( + element instanceof ResponseStatusStep || + element.localName === 'swc-response-status-step' + ); + } + + private _readStepElement(element: Element): ResponseStatusStepData { + const step = element as ResponseStatusStep; + const kind = + step.kind || + (element.getAttribute('kind') as ResponseStatusStepKind | null) || + 'thinking'; + const status = + step.status || + (element.getAttribute('status') as ResponseStatusStepStatus | null) || + 'pending'; + + return { + title: (step.title || element.getAttribute('title') || '').trim(), + detail: (step.detail || element.getAttribute('detail') || '').trim(), + kind, + status, + }; + } + + private _clearRollTimeout(): void { + if (this._rollTimeoutId !== null) { + window.clearTimeout(this._rollTimeoutId); + this._rollTimeoutId = null; + } + } + + private _clearInitiatingDwell(): void { + if (this._initiatingDwellTimeoutId !== null) { + window.clearTimeout(this._initiatingDwellTimeoutId); + this._initiatingDwellTimeoutId = null; + } + this._awaitingInitiatingRoll = false; + } + + private _scheduleInitiatingDwell(toLabel: string): void { + if (this._initiatingDwellTimeoutId !== null) { + return; + } + + this._awaitingInitiatingRoll = true; + this._initiatingDwellTimeoutId = window.setTimeout(() => { + this._initiatingDwellTimeoutId = null; + this._awaitingInitiatingRoll = false; + + if (this._effectivePhase !== 'processing' || this._rollFrom) { + return; + } + + this._startHeaderRoll(this.initiatingLabel, toLabel); + }, this.initiatingDwellMs); + } + + private _startHeaderRoll(fromLabel: string, toLabel: string): void { + this._clearRollTimeout(); + this._rollFrom = fromLabel; + this._rollTo = toLabel; + this._rolling = false; + + if (this._prefersReducedMotion) { + this._headerActiveTitle = this._getActiveStepTitle(this._steps); + this._rollFrom = ''; + this._rollTo = ''; + return; + } + + this._rollTimeoutId = window.setTimeout(() => { + this._rollTimeoutId = null; + if (this._rollFrom) { + this._rolling = false; + this._finishHeaderRoll(); + } + }, 750); + } + + private _finishHeaderRoll(): void { + this._clearRollTimeout(); + this._rolling = false; + + const nextActiveTitle = this._getActiveStepTitle(this._steps); + const nextLabel = this._getAgenticHeaderLabel(); + + if ( + this._effectivePhase === 'processing' && + nextActiveTitle && + nextActiveTitle !== this._headerActiveTitle && + !this._prefersReducedMotion + ) { + this._startHeaderRoll( + this._headerActiveTitle || this.initiatingLabel, + nextLabel + ); + return; + } + + this._headerActiveTitle = nextActiveTitle; + this._rollFrom = ''; + this._rollTo = ''; + } + + private _onlyDisclosureChanged(changed: PropertyValues): boolean { + return changed.has('open') && changed.size === 1; + } + + protected override willUpdate(_changed: PropertyValues): void { + this._syncSlotContent(); + + if (!this._isAgentic) { + return; + } + + if (this._onlyDisclosureChanged(_changed)) { + return; + } + + const phase = this._effectivePhase; + const nextLabel = this._getAgenticHeaderLabel(); + + if (phase === 'initiating') { + this._sawInitiatingPhase = true; + this._clearInitiatingDwell(); + this._clearRollTimeout(); + this._headerActiveTitle = ''; + this._rollFrom = ''; + this._rollTo = ''; + this._rolling = false; + return; + } + + if (phase !== 'processing') { + this._sawInitiatingPhase = false; + this._clearInitiatingDwell(); + this._clearRollTimeout(); + this._headerActiveTitle = ''; + this._rollFrom = ''; + this._rollTo = ''; + this._rolling = false; + return; + } + + if (this._rollFrom || this._initiatingDwellTimeoutId !== null) { + if (this._rollFrom && nextLabel !== this._rollTo) { + this._rollTo = nextLabel; + } + return; + } + + const nextActiveTitle = this._getActiveStepTitle(this._steps); + + if (nextActiveTitle && nextActiveTitle !== this._headerActiveTitle) { + const isFirstStepRoll = !this._headerActiveTitle; + + if ( + isFirstStepRoll && + !this._sawInitiatingPhase && + this.initiatingDwellMs > 0 && + !this._prefersReducedMotion + ) { + this._scheduleInitiatingDwell(nextLabel); + return; + } + + this._startHeaderRoll( + this._headerActiveTitle || this.initiatingLabel, + nextLabel + ); + } + } + + protected override updated(): void { + if (this._rollFrom && !this._rolling && !this._prefersReducedMotion) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (this._rollFrom && !this._rolling) { + this._rolling = true; + } + }); + }); + } + } + + public override disconnectedCallback(): void { + this._clearRollTimeout(); + this._clearInitiatingDwell(); + this.removeEventListener( + 'swc-response-status-step-change', + this._handleStepChildChange + ); + super.disconnectedCallback(); + } + + private _handleHeaderLabelTransitionEnd(event: TransitionEvent): void { + if (event.target !== event.currentTarget || !this._rolling) { + return; + } + if (event.propertyName !== 'transform') { + return; + } + this._rolling = false; + this._finishHeaderRoll(); + } + + private _getVisibleAgenticLabel(): string { + if ( + this._effectivePhase === 'processing' && + this._awaitingInitiatingRoll && + !this._rollFrom + ) { + return this.initiatingLabel; + } + + return this._getAgenticHeaderLabel(); + } + + private _renderAgenticLabel(): TemplateResult { + const label = this._getVisibleAgenticLabel(); + + if ( + this._effectivePhase === 'processing' && + this._rollFrom && + !this._prefersReducedMotion + ) { + const incoming = this._rollTo || label; + return html` + + + ${this._rollFrom} + ${incoming} + + + `; + } + + return html`${label}`; + } + + 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 +480,43 @@ export class ResponseStatus extends SpectrumElement { ); } - private _getStatusLabel(): string { + private _getLegacyStatusLabel(): string { return this.loading ? this.loadingLabel : this.completeLabel; } + private _getActiveStep(): ResponseStatusStepData | undefined { + return this._getActiveStepFrom(this._steps); + } + + 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 || + (node instanceof Element && node.localName === 'swc-response-status-step') + ) { + continue; + } if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) { return true; } @@ -115,35 +528,213 @@ 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 => + this._isStepElement(element) + ) + .map((element) => this._readStepElement(element)); + } + + 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._hasReasoningContent = hasReasoningContent; + + if (!this._stepsEqual(steps, this._steps)) { + this._steps = steps; + } + + if (this._hasReasoningContent !== hasReasoningContent) { + this._hasReasoningContent = hasReasoningContent; + } + } + + 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()} + ${this._renderAgenticLabel()} + ${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}

+ `; + } + + /** + * During processing, only steps that have started (active or complete) appear in + * the panel; pending steps stay in the slot but are not shown until they activate. + */ + private _getTimelineSteps(): ResponseStatusStepData[] { + if (this._isAgentic && this._effectivePhase === 'processing') { + return this._steps.filter((step) => step.status !== 'pending'); + } + + return this._steps; } - private _handleReasoningSlotChange(event: Event): void { - this._syncReasoningContent(event.target as HTMLSlotElement); + private _renderStepTimeline(): TemplateResult { + const steps = this._getTimelineSteps(); + const lastIndex = steps.length - 1; + + return html` +
    + ${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 { return html`
- - - + ${this._renderThreeDots()} ${label}
`; @@ -161,64 +752,89 @@ 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; + const panelOpen = showPanel && this.open; + + return html` +
+ ${this._isAgentic + ? this._renderStepTimeline() + : html` + + `} +
+ `; + } + protected override render(): TemplateResult { + const showPanel = this._showPanel; + const showDisclosure = showPanel; + + if (this._isAgentic) { + const headerLabel = this._getVisibleAgenticLabel(); + 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-step/ResponseStatusStep.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/ResponseStatusStep.ts new file mode 100644 index 00000000000..4f5d1626530 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/ResponseStatusStep.ts @@ -0,0 +1,71 @@ +/** + * 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 { CSSResultArray, html, PropertyValues, TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; + +import styles from './response-status-step.css'; + +export type ResponseStatusStepKind = 'thinking' | 'acting'; +export type ResponseStatusStepStatus = 'pending' | 'active' | 'complete'; + +/** + * One agentic execution step inside ``. + * + * Light DOM marker only — the parent renders the visible timeline from these attributes. + * + * @element swc-response-status-step + */ +export class ResponseStatusStep extends SpectrumElement { + /** Primary step title (shown in the header when `status="active"`). */ + @property({ type: String, reflect: true }) + public override title = ''; + + /** Secondary context shown in the expanded step list. */ + @property({ type: String, reflect: true }) + public detail = ''; + + /** Distinguishes thinking vs acting copy in the expanded list. */ + @property({ type: String, reflect: true }) + public kind: ResponseStatusStepKind = 'thinking'; + + /** Timeline state for connector icons. */ + @property({ type: String, reflect: true }) + public status: ResponseStatusStepStatus = 'pending'; + + public static override get styles(): CSSResultArray { + return [styles]; + } + + protected override updated(changed: PropertyValues): void { + if ( + changed.has('title') || + changed.has('detail') || + changed.has('kind') || + changed.has('status') + ) { + this.dispatchEvent( + new CustomEvent('swc-response-status-step-change', { + bubbles: true, + }) + ); + } + } + + protected override render(): TemplateResult { + return html` + + `; + } +} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/index.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/index.ts new file mode 100644 index 00000000000..d071c02f8c7 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/index.ts @@ -0,0 +1,24 @@ +/** + * 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 { defineElement } from '@spectrum-web-components/core/element/index.js'; + +import { ResponseStatusStep } from './ResponseStatusStep.js'; + +export * from './ResponseStatusStep.js'; + +declare global { + interface HTMLElementTagNameMap { + 'swc-response-status-step': ResponseStatusStep; + } +} + +defineElement('swc-response-status-step', ResponseStatusStep); diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/response-status-step.css b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/response-status-step.css new file mode 100644 index 00000000000..184c40e7f13 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/response-status-step.css @@ -0,0 +1,15 @@ +/** + * 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. + */ + +:host { + display: none; +} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/stories/response-status-step.stories.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/stories/response-status-step.stories.ts new file mode 100644 index 00000000000..ed4436388dc --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/response-status-step/stories/response-status-step.stories.ts @@ -0,0 +1,66 @@ +/** + * 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 { html } from 'lit'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; + +import '../../index.js'; + +/** + * Data marker for one agentic execution step. Steps are not rendered on their own; + * `` reads these attributes and paints the timeline. + * + * For full agentic UI states, see **Response status → Agentic states (spike)**. + */ +const meta: Meta = { + title: 'Conversational AI/Response status/Response status step', + component: 'swc-response-status-step', + parameters: { + docs: { + subtitle: + 'Light DOM step descriptor slotted into ``.', + }, + layout: 'padded', + }, +}; + +export default meta; + +/** Typical usage — steps inside response status (only the parent is visible). */ +export const Playground: Story = { + tags: ['dev'], + render: () => html` + + + + + `, +}; + +export const Overview: Story = { + tags: ['overview'], + ...Playground, +}; 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 aac1790efa0..a513bc85cd1 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; @@ -47,20 +48,110 @@ transition: background token("animation-duration-100") token("animation-ease-in-out"); } -/* Progress circle — indeterminate loading indicator */ +.swc-ResponseStatus-label { + font-family: token("sans-serif-font"); + font-size: token("font-size-100"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-font-size-100"); + color: token("neutral-subdued-content-color-default"); +} -.swc-ResponseStatus-loadingSlot { - display: inline-flex; - flex-shrink: 0; - align-items: center; +.swc-ResponseStatus-row--button .swc-ResponseStatus-label { + flex: 1; + text-align: start; } -.swc-ResponseStatus-label { +/* Rolling header title (processing phase) */ + +.swc-ResponseStatus-labelViewport { + flex: 1; + min-inline-size: 0; + block-size: var(--swc-response-status-label-line-size, 1.33em); + overflow: hidden; +} + +.swc-ResponseStatus-labelStrip { + display: flex; + flex-direction: column; + transform: translateY(0); + transition: transform var(--swc-response-status-roll-duration, 500ms) token("animation-ease-in-out"); +} + +.swc-ResponseStatus-labelStrip--rolling { + transform: translateY(-50%); +} + +.swc-ResponseStatus-labelLine { + display: block; + block-size: var(--swc-response-status-label-line-size, 1.33em); font-family: token("sans-serif-font"); font-size: token("font-size-100"); font-weight: token("regular-font-weight"); line-height: token("line-height-font-size-100"); color: token("neutral-subdued-content-color-default"); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (prefers-reduced-motion: reduce) { + .swc-ResponseStatus-labelStrip { + transition: none; + } +} + +/* Three-dot loading indicator */ + +.swc-ResponseStatus-dots { + display: inline-flex; + flex-shrink: 0; + gap: token("spacing-75"); + align-items: center; + block-size: 6px; + color: token("gray-800"); +} + +.swc-ResponseStatus-dot { + inline-size: 6px; + block-size: 6px; + background: currentcolor; + border-radius: 50%; + opacity: 0.45; + animation: swc-response-status-dot-pulse 1.1s token("animation-ease-in-out") infinite; +} + +.swc-ResponseStatus-dot:nth-child(1) { + color: token("gray-500"); + animation-delay: 0s; +} + +.swc-ResponseStatus-dot:nth-child(2) { + color: token("gray-600"); + animation-delay: 0.15s; +} + +.swc-ResponseStatus-dot:nth-child(3) { + color: token("gray-800"); + animation-delay: 0.3s; +} + +@keyframes swc-response-status-dot-pulse { + 0%, + 70%, + 100% { + opacity: 0.35; + } + + 35% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .swc-ResponseStatus-dot { + opacity: 0.7; + animation: none; + } } .swc-ResponseStatus-row--button:hover .swc-ResponseStatus-label { @@ -109,3 +200,153 @@ visibility: visible; block-size: auto; } + +/* ───────────────────────────────────── + Agentic mode (spike) + ───────────────────────────────────── */ + +.swc-ResponseStatus--agentic { + --swc-response-status-roll-duration: 650ms; + + gap: token("spacing-75"); + 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-100"); + background: token("gray-100"); +} + +.swc-ResponseStatus--agentic .swc-ResponseStatus-dots { + margin-inline-end: 1px; +} + +.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-panel--open { + visibility: visible; + block-size: auto; +} + +.swc-ResponseStatus-steps { + padding-inline: token("spacing-100"); + margin: 0; + overflow: visible; + list-style: none; +} + +.swc-ResponseStatus-step { + display: grid; + grid-template-columns: 20px 1fr; + align-items: stretch; + column-gap: token("spacing-100"); +} + +.swc-ResponseStatus-step:last-child { + animation: swc-response-status-step-enter var(--swc-response-status-roll-duration, 500ms) token("animation-ease-in-out"); +} + +@keyframes swc-response-status-step-enter { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .swc-ResponseStatus-step:last-child { + animation: none; + } +} + +.swc-ResponseStatus-step-rail { + display: flex; + flex-direction: column; + align-items: center; + block-size: 100%; + min-block-size: 100%; + overflow: visible; +} + +/* Extend rail (and line) through the gap before the next step icon */ +.swc-ResponseStatus-step:not(:last-child) .swc-ResponseStatus-step-rail { + box-sizing: content-box; + padding-block-end: token("spacing-200"); +} + +.swc-ResponseStatus-step-icon { + flex-shrink: 0; + inline-size: token("workflow-icon-size-75"); + block-size: token("workflow-icon-size-75"); + transition: color token("animation-duration-200") token("animation-ease-in-out"); +} + +.swc-ResponseStatus-step[data-status="complete"] .swc-ResponseStatus-step-icon { + transition: + color token("animation-duration-200") token("animation-ease-in-out"), + opacity token("animation-duration-200") token("animation-ease-in-out"); +} + +.swc-ResponseStatus-step-line { + flex: 1 1 auto; + inline-size: 1px; + min-block-size: token("spacing-100"); + margin-block-start: token("spacing-50"); + background: token("gray-300"); +} + +.swc-ResponseStatus-step-icon--active { + color: token("gray-800"); +} + +.swc-ResponseStatus-step-body { + min-inline-size: 0; +} + +.swc-ResponseStatus-step:not(:last-child) .swc-ResponseStatus-step-body { + padding-block-end: token("spacing-100"); +} + +.swc-ResponseStatus-step-title { + margin: 0; + font-family: token("sans-serif-font"); + font-size: token("font-size-75"); + font-weight: token("medium-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/response-status/stories/agentic-states.stories.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/agentic-states.stories.ts new file mode 100644 index 00000000000..39d50f6020a --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/agentic-states.stories.ts @@ -0,0 +1,446 @@ +/** + * 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 { html, LitElement } from 'lit'; +import { state } from 'lit/decorators.js'; +import { keyed } from 'lit/directives/keyed.js'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; + +import '../index.js'; +import '../../conversation-turn/index.js'; +import '../../system-message/index.js'; + +import { + AGENTIC_VIDEO_FLOW_STEPS, + AGENTIC_VIDEO_FLOW_TIMING, +} from '../../agentic-video-flow-script.js'; +import type { ResponseStatusStepStatus } from '../response-status-step/ResponseStatusStep.js'; +import type { ResponseStatusPhase } from '../ResponseStatus.js'; + +const figmaSteps = html` + + + + +`; + +const figmaStepsAllComplete = html` + + + + +`; + +const variantStack = (content: unknown) => html` +
+ ${content} +
+`; + +const variantBlock = (label: string, story: unknown) => html` +
+ ${story} + ${label} +
+`; + +/** + * Spike for the [Agentic states](https://www.figma.com/design/2TYz3uRKwfWGuVywqFIlp0/Taniya-Aziz-working-file?node-id=29-189) + * spec — uses `phase`, `duration`, and `` children. + */ +const meta: Meta = { + title: 'Conversational AI/Response status/Agentic states (spike)', + tags: ['dev'], + parameters: { + docs: { + subtitle: + 'Prototype API for agentic execution steps. Not yet final API or visual spec sign-off.', + }, + layout: 'padded', + }, +}; + +export default meta; + +/** Initiation — prompt received, process started. */ +export const Initiating: Story = { + render: () => + variantStack( + variantBlock( + 'Initiation / planning', + html` + + ` + ) + ), +}; + +/** Processing collapsed — rolling title from the active step. */ +export const ProcessingCollapsed: Story = { + render: () => + variantStack( + variantBlock( + 'Processing (collapsed)', + html` + + ${figmaSteps} + + ` + ) + ), +}; + +/** Processing expanded — full step timeline while still running. */ +export const ProcessingExpanded: Story = { + render: () => + variantStack( + variantBlock( + 'Processing (expanded)', + html` + + ${figmaSteps} + + ` + ) + ), +}; + +/** Completed collapsed — duration summary in the header. */ +export const CompletedCollapsed: Story = { + render: () => + variantStack( + variantBlock( + 'Completed (collapsed)', + html` + + ${figmaStepsAllComplete} + + ` + ) + ), +}; + +/** Completed expanded — post-hoc step review. */ +export const CompletedExpanded: Story = { + render: () => + variantStack( + variantBlock( + 'Completed (expanded)', + html` + + ${figmaStepsAllComplete} + + ` + ) + ), +}; + +/** All Figma-aligned variants on one page for design review. */ +export const AllStates: Story = { + render: () => + variantStack(html` + ${variantBlock( + '1. Initiation / planning', + html` + + ` + )} + ${variantBlock( + '2. Processing (collapsed)', + html` + + ${figmaSteps} + + ` + )} + ${variantBlock( + '3. Processing (expanded)', + html` + + ${figmaSteps} + + ` + )} + ${variantBlock( + '4. Completed (collapsed)', + html` + + ${figmaStepsAllComplete} + + ` + )} + ${variantBlock( + '5. Completed (expanded)', + html` + + ${figmaStepsAllComplete} + + ` + )} + `), + tags: ['overview'], +}; + +class AgenticStatusSimulationDemo extends LitElement { + @state() + private phase: ResponseStatusPhase = 'initiating'; + + @state() + private activeIndex = 0; + + @state() + private statusOpen = false; + + @state() + private duration = 0; + + @state() + private runId = 0; + + private timers: number[] = []; + private runToken = 0; + private startedAt = 0; + + public override disconnectedCallback(): void { + this._clearTimers(); + this.runToken += 1; + super.disconnectedCallback(); + } + + /** Called on story (re)load so timers and UI state always restart cleanly. */ + public reset(): void { + this._restart(); + } + + private _clearTimers(): void { + for (const id of this.timers) { + window.clearTimeout(id); + } + this.timers = []; + } + + private _schedule(delayMs: number, run: () => void): void { + const token = this.runToken; + const timerId = window.setTimeout(() => { + if (token !== this.runToken) { + return; + } + run(); + }, delayMs); + this.timers.push(timerId); + } + + private _restart(): void { + this._clearTimers(); + this.runToken += 1; + this.runId += 1; + this.startedAt = Date.now(); + const { + processing, + step1, + step2, + step3, + step4, + collapse, + complete, + loopRestart, + } = AGENTIC_VIDEO_FLOW_TIMING; + + this.phase = 'initiating'; + this.activeIndex = 0; + this.statusOpen = false; + this.duration = 0; + + this._schedule(processing, () => { + this.phase = 'processing'; + this.activeIndex = 0; + }); + this._schedule(step1, () => { + this.activeIndex = 1; + }); + this._schedule(step2, () => { + this.activeIndex = 2; + }); + this._schedule(step3, () => { + this.statusOpen = true; + }); + this._schedule(step4, () => { + this.activeIndex = 3; + }); + this._schedule(collapse, () => { + this.statusOpen = false; + }); + this._schedule(complete, () => { + this.phase = 'complete'; + this.duration = Math.max( + 16, + Math.round((Date.now() - this.startedAt) / 1000) + ); + this.activeIndex = AGENTIC_VIDEO_FLOW_STEPS.length; + }); + this._schedule(loopRestart, () => this._restart()); + } + + private _stepStatus(index: number): ResponseStatusStepStatus { + if (this.phase === 'initiating') { + return 'pending'; + } + if (this.phase === 'complete' || index < this.activeIndex) { + return 'complete'; + } + if (index === this.activeIndex) { + return 'active'; + } + return 'pending'; + } + + protected override render() { + return keyed( + this.runId, + html` + + ${AGENTIC_VIDEO_FLOW_STEPS.map( + (step, index) => html` + + ` + )} + + ` + ); + } +} + +if (!customElements.get('swc-agentic-status-simulation-demo')) { + customElements.define( + 'swc-agentic-status-simulation-demo', + AgenticStatusSimulationDemo + ); +} + +/** + * Starts **collapsed** in processing; expand the header to reveal the step timeline. + * Steps appear one at a time in the panel as each becomes active. + */ +export const LiveSimulation: Story = { + tags: ['dev'], + render: () => html` +
+ +

+ Matches the reference video — “Processing request” (~2s), rolling titles + while collapsed, auto-expands at ~10s, completes as “Thought for N + seconds”. Loops every ~23s. +

+
+ `, + play: async ({ canvasElement }) => { + await customElements.whenDefined('swc-agentic-status-simulation-demo'); + const demo = canvasElement.querySelector( + 'swc-agentic-status-simulation-demo' + ); + (demo as AgenticStatusSimulationDemo | null)?.reset(); + }, +}; + +/** Composed inside `swc-system-message` as in production. */ +export const InSystemMessage: Story = { + tags: ['dev'], + render: () => html` + + + + ${figmaSteps} + +
+

+ According to the assets, there is a clear journey from beginning to + end. Let's start with overarching themes and build from there. +

+
+
+
+ `, +}; diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/response-status.stories.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/response-status.stories.ts index c810dd1df64..c36b06a3e1e 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/response-status.stories.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/stories/response-status.stories.ts @@ -64,7 +64,7 @@ argTypes.completeLabel = { }; /** - * Displays AI response progress with an indeterminate progress circle and optional reasoning disclosure. + * Displays AI response progress with an animated three-dot indicator and optional reasoning disclosure. */ const meta: Meta = { title: 'Conversational AI/Response status', @@ -119,7 +119,7 @@ export const Overview: Story = { /** * A response status indicator consists of: * - * 1. **Status row** — An indeterminate progress circle (loading) or checkmark (complete) with a label + * 1. **Status row** — Animated three dots (loading) or checkmark (complete) with a label * 2. **Reasoning toggle** — Optional expandable disclosure for chain-of-thought content */ export const Anatomy: Story = { @@ -139,7 +139,7 @@ export const Anatomy: Story = { /** * The `loading` attribute controls which indicator is shown: * - * - **`loading=true`** — Indeterminate progress circle + "Thinking…" label + * - **`loading=true`** — Animated three dots + "Thinking…" label * - **`loading=false`** — Checkmark + "Response generated" label * - Set `loading-label` or `complete-label` to customize row text per state */ diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/test/response-status.test.ts b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/test/response-status.test.ts index 5b9f51afc7d..a49682c833a 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/response-status/test/response-status.test.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/response-status/test/response-status.test.ts @@ -162,10 +162,10 @@ export const InteractionTest: Story = { '.swc-ResponseStatus-row--button' ); const panel = el.shadowRoot?.querySelector( - '[id^="swc-reasoning-panel-"]' + '[id^="swc-response-status-panel-"]' ); - expect(panel?.id).toMatch(/^swc-reasoning-panel-[a-f0-9]+$/); + expect(panel?.id).toMatch(/^swc-response-status-panel-[a-f0-9]+$/); expect(button?.getAttribute('aria-controls')).toBe(panel?.id); expect(button?.getAttribute('aria-expanded')).toBe('false'); expect(panel).toBeTruthy(); @@ -184,7 +184,7 @@ export const InteractionTest: Story = { '.swc-ResponseStatus-row--button' ); const panel = el.shadowRoot?.querySelector( - '[id^="swc-reasoning-panel-"]' + '[id^="swc-response-status-panel-"]' ); const row = el.shadowRoot?.querySelector('.swc-ResponseStatus-row'); @@ -205,10 +205,10 @@ export const InteractionTest: Story = { el.open = true; await el.updateComplete; const panel = el.shadowRoot?.querySelector( - '[id^="swc-reasoning-panel-"]' + '[id^="swc-response-status-panel-"]' ) as HTMLElement | null; const slot = el.shadowRoot?.querySelector( - '[id^="swc-reasoning-panel-"] slot' + '.swc-ResponseStatus-reasoning-panel .swc-ResponseStatus-reasoning-slot' ); const assigned = slot?.assignedNodes({ flatten: true }); 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`