Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b80a742
matrixbuilder: restore portable design checkpoint inputs
fsiyavud-cisco Sep 22, 2026
c04d6e6
matrixbuilder: use supplied AI summary authority
fsiyavud-cisco Sep 22, 2026
2bd1dd0
Consolidated single AI summary timeout in requirement
fsiyavud-cisco Sep 22, 2026
d396b1c
matrixbuilder: publish design
fsiyavud-cisco Sep 22, 2026
e098b7c
matrixbuilder: publish design_fix[0]
fsiyavud-cisco Sep 23, 2026
a0300be
matrixbuilder: publish design_fix[1]
fsiyavud-cisco Sep 23, 2026
38a1e1d
matrixbuilder: publish design_fix[2]
fsiyavud-cisco Sep 23, 2026
5183f93
matrixbuilder: publish design_fix[3]
fsiyavud-cisco Sep 23, 2026
7b6d51f
matrixbuilder: publish design_fix[4]
fsiyavud-cisco Sep 23, 2026
fe8320d
matrixbuilder: publish design_fix[5]
fsiyavud-cisco Sep 23, 2026
266269b
matrixbuilder: publish design_fix[6]
fsiyavud-cisco Sep 23, 2026
04108f0
matrixbuilder: publish design_fix[7]
fsiyavud-cisco Sep 23, 2026
2d691c7
matrixbuilder: publish design_fix[8]
fsiyavud-cisco Sep 23, 2026
2fd1dbe
matrixbuilder: publish design_fix[9]
fsiyavud-cisco Sep 23, 2026
2d8c3f3
matrixbuilder: publish design_fix[10]
fsiyavud-cisco Sep 23, 2026
fa0b090
matrixbuilder: publish design_fix[11]
fsiyavud-cisco Sep 23, 2026
696646d
matrixbuilder: publish design_fix[12]
fsiyavud-cisco Sep 23, 2026
36bd2d9
matrixbuilder: publish design_fix[13]
fsiyavud-cisco Sep 23, 2026
391939e
matrixbuilder: publish design_fix[14]
fsiyavud-cisco Sep 23, 2026
caa819a
matrixbuilder: publish design_fix[15]
fsiyavud-cisco Sep 23, 2026
a492428
matrixbuilder: publish design_fix[16]
fsiyavud-cisco Sep 23, 2026
76f84b3
matrixbuilder: publish design_fix[17]
fsiyavud-cisco Sep 23, 2026
3834ab2
matrixbuilder: publish design_fix[18]
fsiyavud-cisco Sep 23, 2026
49422ce
matrixbuilder: publish design_fix[19]
fsiyavud-cisco Sep 23, 2026
c56f95a
matrixbuilder: publish design_fix[20]
fsiyavud-cisco Sep 23, 2026
933dabe
matrixbuilder: publish design_fix[21]
fsiyavud-cisco Sep 23, 2026
1983585
matrixbuilder: publish design_fix[22]
fsiyavud-cisco Sep 23, 2026
bfcfd55
matrixbuilder: publish design_fix[23]
fsiyavud-cisco Sep 23, 2026
15cab1c
matrixbuilder: publish design_fix[24]
fsiyavud-cisco Sep 23, 2026
3f66333
matrixbuilder: publish design_fix[25]
fsiyavud-cisco Sep 23, 2026
3dac56b
matrixbuilder: publish design_fix[26]
fsiyavud-cisco Sep 23, 2026
4e8dd5d
matrixbuilder: publish design_fix[27]
fsiyavud-cisco Sep 23, 2026
e17fb7b
matrixbuilder: publish design_fix[28]
fsiyavud-cisco Sep 23, 2026
6be6683
matrixbuilder: publish design_fix[29]
fsiyavud-cisco Sep 23, 2026
07f0d02
matrixbuilder: publish design_fix[30]
fsiyavud-cisco Sep 23, 2026
ab3d34b
matrixbuilder: publish design_fix[31]
fsiyavud-cisco Sep 23, 2026
bab200c
matrixbuilder: publish design_fix[32]
fsiyavud-cisco Sep 23, 2026
314c48e
matrixbuilder: publish design_fix[33]
fsiyavud-cisco Sep 23, 2026
73849ac
matrixbuilder: publish design_fix[34]
fsiyavud-cisco Sep 23, 2026
ad6f77f
matrixbuilder: publish design_fix[35]
fsiyavud-cisco Sep 23, 2026
0325e7f
matrixbuilder: publish design_fix[36]
fsiyavud-cisco Sep 23, 2026
8e661af
matrixbuilder: publish design_fix[37]
fsiyavud-cisco Sep 23, 2026
ffec485
matrixbuilder: publish design_fix[38]
fsiyavud-cisco Sep 23, 2026
0368b5e
matrixbuilder: publish design_fix[39]
fsiyavud-cisco Sep 23, 2026
f573913
matrixbuilder: publish design_fix[40]
fsiyavud-cisco Sep 24, 2026
5b0f4f0
matrixbuilder: publish design_fix[41]
fsiyavud-cisco Sep 24, 2026
458a8f1
matrixbuilder: publish design_fix[42]
fsiyavud-cisco Sep 24, 2026
9f9d3be
matrixbuilder: publish design_fix[43]
fsiyavud-cisco Sep 24, 2026
bd8a674
matrixbuilder: publish design_fix[44]
fsiyavud-cisco Sep 24, 2026
4477b39
matrixbuilder: publish design_fix[45]
fsiyavud-cisco Sep 24, 2026
766d31e
matrixbuilder: publish design_fix[46]
fsiyavud-cisco Sep 24, 2026
d117c43
matrixbuilder: publish design_fix[47]
fsiyavud-cisco Sep 24, 2026
faf021a
matrixbuilder: publish design_fix[48]
fsiyavud-cisco Sep 24, 2026
93ba11a
matrixbuilder: publish design_fix[49]
fsiyavud-cisco Sep 24, 2026
6a1bc50
matrixbuilder: publish design_fix[50]
fsiyavud-cisco Sep 24, 2026
7ddf452
matrixbuilder: publish design_fix[51]
fsiyavud-cisco Sep 24, 2026
66c56a1
matrixbuilder: publish design_fix[52]
fsiyavud-cisco Sep 24, 2026
eeac84b
matrixbuilder: publish design_fix[53]
fsiyavud-cisco Sep 24, 2026
671d476
matrixbuilder: publish design_fix[54]
fsiyavud-cisco Sep 24, 2026
f127007
matrixbuilder: publish design_fix[55]
fsiyavud-cisco Sep 24, 2026
e9a104b
matrixbuilder: publish design_fix[56]
fsiyavud-cisco Sep 24, 2026
eaad089
design spec and AGENTS.md fixes
fsiyavud-cisco Sep 24, 2026
f5774ed
Repair implementation DAG schema violations
fsiyavud-cisco Sep 24, 2026
8111826
Fixed env prerequisites in impl DAG
fsiyavud-cisco Sep 24, 2026
7b96161
feat(cc): add AI summaries across Contact Center widgets
fsiyavud-cisco Sep 27, 2026
7b7c780
refactor(cc): simplify AI summaries to match Agent Desktop
rsarika Oct 7, 2026
1d3bc54
refactor(cc): trim AI summaries to the feature and fix review findings
rsarika Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ai-docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ The aggregator package `@webex/cc-widgets` re-exports every widget plus the `sto
| ui-logging.withMetrics | `@webex/cc-ui-logging` | `withMetrics` | `withMetrics<P extends object>(Component, widgetName: string): React.MemoExoticComponent` HOC that auto-emits mount/unmount/error metrics; every widget export is wrapped with it | stable semver; signature change is breaking | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | `packages/contact-center/ui-logging/src/index.ts` |
| ui-logging.WidgetMetrics | `@webex/cc-ui-logging` | `WidgetMetrics` (type) | `{ widgetName; event: 'WIDGET_MOUNTED' \| 'ERROR' \| 'WIDGET_UNMOUNTED' \| 'PROPS_UPDATED'; props?; timestamp; additionalContext? }` | stable semver; narrowing the `event` union or removing a field is breaking | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | `packages/contact-center/ui-logging/src/index.ts` |

## AI summary surfaces

| Contract ID | Package | Surface | Purpose | Compatibility | Detail |
| --- | --- | --- | --- | --- | --- |
| task.ai-summary-status | `@webex/cc-task`, `@webex/cc-widgets` | `CallControl.onAISummaryStatusChange`, `AISummaryStatusDetail` | Optional content-free status callback; the CallControl custom element accepts a function property, with inert callback attributes | additive optional callback and type | `packages/contact-center/task/ai-docs/task-spec.md`, `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` |
| store.ai-summary | `@webex/cc-store` | `aiSummaries`, SDK summary types, `getAISummarySurface`, and StoreWrapper summary actions | SDK requests/responses and per-interaction UI state, edits, copies and feedback | additive SDK boundary | `packages/contact-center/store/ai-docs/store-spec.md` |
| cc-components.ai-summary-props | `@webex/cc-components` | `CallControlComponentProps.aiSummary`, `AIAssistantComponentProps.receiverSummary` | Optional summary views and callbacks; the shared AISummary component is internal | additive optional props; wrapupCall supports void or Promise<boolean> | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` |

## Requires — what this repo depends on
| Dependency (service / package / datastore) | What is consumed | Schema / detail link | Availability assumption | Fallback on failure | Version floor |
|---|---|---|---|---|---|
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
},
"resolutions": {
"@momentum-design/components": "0.133.23",
"@momentum-design/icons": "0.17.0"
"@momentum-design/icons": "0.17.0",
"@webex/contact-center": "file:./vendor/contact-center-cc-summaries.tgz"
},
"scripts": {
"clean": "yarn workspaces foreach --all --topological --parallel run clean && rm -rf node_modules",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ views and the ErrorBoundary path. `tests/ai-assistant/feedback.tsx` covers like/
and the missing-`adaptiveCardId` warning. UI-level rendering (spinner, error text,
snapshots) is covered in `cc-components/tests/components/AIAssistant/`.

## Receiving-agent AI summaries

`getReceiverSummary` in `src/helper.ts` reads the current interaction's receiver entry
from `store.aiSummaries`, maps its surface with `getAISummarySurface`, and passes the
SDK-shaped content, revision, confirmed feedback and pending state to the presentation
component. Its copy and feedback callbacks pass the captured Task and displayed
revision to the store. The widget makes no summary SDK calls.

The receiver branch is independent of Real-time Assist enablement. A missing receiver
entry supplies no branch; failed or unsupported summaries supply the matching status
surface. Summary changes are observed by `AIAssistantInternal`.

Evidence: `src/helper.ts`, `src/ai-assistant/index.tsx`;
tests: receiver summary cases in `tests/ai-assistant/index.tsx`.

## Traceability
- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md`
- Coverage state & contracts baseline: `.sdd/manifest.json`
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {observer} from 'mobx-react-lite';
import {ErrorBoundary} from 'react-error-boundary';

import {AIAssistantComponent} from '@webex/cc-components';
import {useAiAssistant, REAL_TIME_ASSIST_FLAG} from '../helper';
import {getReceiverSummary, useAiAssistant, REAL_TIME_ASSIST_FLAG} from '../helper';
import {IAIAssistantProps} from '../ai-assistant.types';

const AIAssistantInternal: React.FunctionComponent<IAIAssistantProps> = observer((props) => {
Expand All @@ -24,6 +24,7 @@ const AIAssistantInternal: React.FunctionComponent<IAIAssistantProps> = observer
return (
<AIAssistantComponent
{...hookProps}
receiverSummary={getReceiverSummary(currentTask)}
isFeatureEnabled={isFeatureEnabled}
hasActiveInteraction={Boolean(interactionId)}
agentName={agentProfile?.agentName}
Expand Down
28 changes: 26 additions & 2 deletions packages/contact-center/ai-assistant/src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import store from '@webex/cc-store';
import type {RealTimeAssistPayload} from '@webex/cc-store';
import store, {getAISummarySurface} from '@webex/cc-store';
import type {ITask, RealTimeAssistPayload} from '@webex/cc-store';
import type {
AIAssistantActionEvent,
AIAssistantChatEntry,
AIAssistantChromeState,
AIAssistantReceiverSummary,
AIAssistantRequestStatus,
} from '@webex/cc-components';
import {
Expand Down Expand Up @@ -280,4 +281,27 @@ export const useAiAssistant = (input: UseAiAssistantInput) => {
return useMemo(() => ({...chrome, ...realTimeAssist}), [chrome, realTimeAssist]);
};

/** The receiving agent's consult/transfer summary, pushed by the backend for the task's interaction. */
export const getReceiverSummary = (task?: ITask): AIAssistantReceiverSummary | undefined => {
const interactionId = task?.data?.interactionId;
const entry = interactionId ? store.aiSummaries?.[interactionId]?.receiver : undefined;
const surface = getAISummarySurface(entry);
const branchKey = `mid-call:receiver:${interactionId}`;
if (surface === 'content' && entry.content.adaptiveCard) {
return {
surface,
branchKey,
content: entry.content,
contentRevision: entry.revision,
selectedFeedback: entry.feedback,
feedbackPending: Boolean(entry.feedbackPending),
recordReceiverSummaryCopied: (expectedRevision) =>
store.recordAISummaryCopied('receiver', expectedRevision, task),
setReceiverSummaryFeedback: (feedback, expectedRevision) =>
void store.setReceiverSummaryFeedback(feedback, expectedRevision, task),
};
}
return surface === 'unavailable' || surface === 'generic-error' ? {surface, branchKey} : undefined;
};

export {REAL_TIME_ASSIST_FLAG};
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jest.mock('@webex/cc-components', () => {

jest.mock('@webex/cc-store', () => ({
__esModule: true,
getAISummarySurface: jest.requireActual('../../../store/src/ai-summary').getAISummarySurface,
default: {
cc: {
apiAIAssistant: {
Expand All @@ -70,6 +71,7 @@ jest.mock('@webex/cc-store', () => ({
agentId: 'agent-1',
featureFlags: {isSuggestedResponsesEnabled: true},
realTimeAssist: {'interaction-1': []},
aiSummaries: {},
clearRealTimeAssist: jest.fn(),
logger: {error: jest.fn(), warn: jest.fn()},
},
Expand Down
193 changes: 178 additions & 15 deletions packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import React from 'react';
import {fireEvent, render, screen, within} from '@testing-library/react';
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
import {runInAction} from 'mobx';
import {AIAssistant} from '../../src';
import * as helper from '../../src/helper';
import store from '@webex/cc-store';
import type {AISummaryEntry} from '@webex/cc-store';
import '@testing-library/jest-dom';

jest.mock('@webex/cc-store', () => {
return {
__esModule: true,
default: {
cc: {
apiAIAssistant: {
getRealTimeAssistance: jest.fn(),
sendRealTimeAssistanceUserAction: jest.fn(),
},
const {observable} = jest.requireActual('mobx');
const storeMock = {
cc: {
apiAIAssistant: {
getRealTimeAssistance: jest.fn(),
sendRealTimeAssistanceUserAction: jest.fn(),
},
currentTask: {data: {interactionId: 'interaction-1'}},
agentId: 'agent-1',
featureFlags: {isSuggestedResponsesEnabled: true},
realTimeAssist: {},
onErrorCallback: undefined,
clearRealTimeAssist: jest.fn(),
},
currentTask: {data: {interactionId: 'interaction-1'}},
agentId: 'agent-1',
agentProfile: {},
featureFlags: {isSuggestedResponsesEnabled: true},
realTimeAssist: {},
aiSummaries: {},
onErrorCallback: undefined,
clearRealTimeAssist: jest.fn(),
recordAISummaryCopied: jest.fn(),
setReceiverSummaryFeedback: jest.fn(),
};

return {
__esModule: true,
getAISummarySurface: jest.requireActual('../../../store/src/ai-summary').getAISummarySurface,
default: observable.object(storeMock, {
cc: observable.ref,
currentTask: observable.ref,
agentProfile: observable.ref,
featureFlags: observable.ref,
realTimeAssist: observable.ref,
aiSummaries: observable.ref,
onErrorCallback: observable.ref,
clearRealTimeAssist: false,
recordAISummaryCopied: false,
setReceiverSummaryFeedback: false,
}),
};
});

Expand All @@ -34,23 +55,72 @@ type StoreMock = {
};
currentTask?: {data: {interactionId: string}};
agentId: string;
agentProfile: Record<string, unknown>;
featureFlags: {isSuggestedResponsesEnabled: boolean};
realTimeAssist: Record<string, unknown>;
aiSummaries: Record<string, {receiver?: AISummaryEntry}>;
onErrorCallback?: jest.Mock;
clearRealTimeAssist: jest.Mock;
recordAISummaryCopied: jest.Mock;
setReceiverSummaryFeedback: jest.Mock;
};
const storeMock = store as unknown as StoreMock;

const receiverEntry = (overrides: Partial<AISummaryEntry> = {}): AISummaryEntry => ({
status: 'ready',
revision: 7,
copied: 0,
edited: false,
feedback: 'none',
action: 'TRANSFER',
content: {
adaptiveCard: {
type: 'AdaptiveCard',
version: '1.5',
body: [{type: 'TextBlock', text: 'Receiver summary'}],
},
},
...overrides,
});

const receiverErrorEntry = (error: AISummaryEntry['error']): AISummaryEntry =>
receiverEntry({status: 'error', error, content: undefined});

const setReceiverEntry = (receiver?: AISummaryEntry): void => {
runInAction(() => {
storeMock.aiSummaries = receiver ? {'interaction-1': {receiver}} : {};
});
};

const deferred = <T,>(): {
promise: Promise<T>;
resolve: (value: T) => void;
} => {
let resolve: (value: T) => void = () => undefined;
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});
return {promise, resolve};
};

describe('AIAssistant widget', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
jest.spyOn(console, 'error').mockImplementation(() => {});
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {writeText: jest.fn().mockResolvedValue(undefined)},
});
storeMock.currentTask = {data: {interactionId: 'interaction-1'}};
storeMock.agentProfile = {};
storeMock.featureFlags = {isSuggestedResponsesEnabled: true};
storeMock.realTimeAssist = {};
storeMock.aiSummaries = {};
storeMock.onErrorCallback = undefined;
storeMock.recordAISummaryCopied.mockReturnValue(true);
storeMock.cc.apiAIAssistant.sendRealTimeAssistanceUserAction.mockResolvedValue(undefined);
storeMock.setReceiverSummaryFeedback.mockResolvedValue(undefined);
});

it('renders launcher when chrome is closed', () => {
Expand Down Expand Up @@ -106,6 +176,99 @@ describe('AIAssistant widget', () => {
expect(container.querySelector('.my-host-class')).toBeInTheDocument();
});

it('should omit the View summary trigger until a receiver summary arrives for the current interaction', () => {
runInAction(() => {
storeMock.aiSummaries = {'other-interaction': {receiver: receiverEntry()}};
});
render(<AIAssistant />);
expect(screen.queryByTestId('ai-assistant:view-summary')).not.toBeInTheDocument();
});

it('re-renders the receiver summary branch when the observable surface changes', async () => {
setReceiverEntry(receiverErrorEntry('unsupported'));
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));
expect(screen.getByTestId('ai-summary:unavailable')).toBeInTheDocument();

setReceiverEntry(receiverErrorEntry('failed'));

await waitFor(() => expect(screen.getByTestId('ai-summary:error')).toBeInTheDocument());
expect(screen.queryByTestId('ai-summary:unavailable')).not.toBeInTheDocument();
});

it('should open the receiver summary from the View summary trigger', async () => {
setReceiverEntry(receiverEntry());
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));

expect(await screen.findByTestId('ai-assistant:receiver-summary')).toBeInTheDocument();
});

it('opens a receiver summary even when Real-time Assist is disabled', async () => {
storeMock.featureFlags = {isSuggestedResponsesEnabled: false};
setReceiverEntry(receiverEntry());
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));

expect(await screen.findByTestId('ai-assistant:receiver-summary')).toBeInTheDocument();
expect(screen.queryByTestId('ai-assistant:landing')).not.toBeInTheDocument();
});

it('routes receiver copy and feedback through the store with the current revision', async () => {
setReceiverEntry(receiverEntry());
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));
fireEvent.click(await screen.findByRole('button', {name: 'Copy Summary'}));

await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalled());
expect(storeMock.recordAISummaryCopied).toHaveBeenCalledWith('receiver', 7, storeMock.currentTask);

fireEvent.click(screen.getByRole('button', {name: 'This is helpful'}));

await waitFor(() =>
expect(storeMock.setReceiverSummaryFeedback).toHaveBeenCalledWith('thumbs_up', 7, storeMock.currentTask)
);
});

it('projects receiver feedback pending state as disabled controls without painting a selection', async () => {
setReceiverEntry(receiverEntry({feedbackPending: true}));
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));

const like = await screen.findByRole('button', {name: 'This is helpful'});
expect(like).toBeDisabled();
expect(like).toHaveAttribute('aria-pressed', 'false');
});

it('replaces receiver feedback projections only after the store confirms the selection', async () => {
const feedbackSend = deferred<void>();
setReceiverEntry(receiverEntry());
storeMock.setReceiverSummaryFeedback.mockImplementationOnce(() => {
setReceiverEntry(receiverEntry({feedbackPending: true}));
return feedbackSend.promise;
});
render(<AIAssistant />);

fireEvent.click(screen.getByTestId('ai-assistant:view-summary'));
const like = await screen.findByRole('button', {name: 'This is helpful'});
fireEvent.click(like);

await waitFor(() => expect(like).toBeDisabled());
expect(like).toHaveAttribute('aria-pressed', 'false');

feedbackSend.resolve();
setReceiverEntry(receiverEntry({feedback: 'thumbs_up'}));

await waitFor(() =>
expect(screen.getByRole('button', {name: 'This is helpful'})).toHaveAttribute('aria-pressed', 'true')
);
});

it('routes errors thrown in the hook to store.onErrorCallback', () => {
const onErrorCallback = jest.fn();
storeMock.onErrorCallback = onErrorCallback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,37 @@ Each component is tested in isolation with React Testing Library: render from a
| `CC-COMPONENTS-R-017` | `tests/components/task/CallControlCAD/call-control-cad.tsx` (Momentum structure/configuration, sections, immediate/confirmed Drop, pending/consult disabled state, read-only rows, focus fallback, live regions) | Momentum overlay keyboard behavior and visual placement receive final manual coverage |
| `CC-COMPONENTS-R-018` | `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx` | None |

## AI summary presentation

The internal `components/AISummary` surface renders generating, unavailable, error,
and content states from props. Initiator and post-call summaries support keyed text
edits, copy and feedback. Post-call supports Retry and a read-only Outcome from SDK
`resolution`; the display labels never become SDK section keys. Receiver summaries
use copy and feedback actions around a display-only Adaptive Card.

CallControl's optional `aiSummary` prop contains the views and request callbacks.
For eligible voice tasks, opening consult/transfer requests a summary and renders
destination radios with the summary below the existing results. That layout stays
stable while the open popover loses summary eligibility; voice tasks without summary
eligibility retain the existing category buttons, reload control and list layout.
`WrapUpSummary` shows searchable reasons and the post-call summary. Complete Wrap-Up
remains available during generation; while completion is pending, summary edits and
completion are disabled. The existing wrap-up form keeps its previous submission behavior.

`AIAssistantComponent.receiverSummary` adds View summary to closed/minimized chrome
and opens the receiver branch independently of Real-time Assist enablement. Copy uses
visible card text; card actions remain inert. The receiver content revision resets
its renderer's error boundary when replacement content arrives. Summary components recover focus when a focused
summary control or branch is removed and no connected control has taken focus.

These components receive data and callbacks through props and make no SDK calls.
`AISummary` remains internal; it is not exported by the package's root React barrel.

Evidence: `components/AISummary/`, `components/AIAssistant/`, and
`components/task/CallControl/`; tests: the corresponding `tests/components/` suites,
including `AISummary/ai-summary.tsx`, `AIAssistant/ai-assistant.tsx`,
`CallControl/call-control.tsx`, `consult-transfer-popover.tsx`, and `wrap-up-summary.tsx`.

## Traceability

- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md)
Expand Down
Loading
Loading