Skip to content

Commit 812f4e3

Browse files
copilot.sidePanel APIs (#2806)
* initial changes for renderingSurface data * First commit interfaces * Modify comment * Adding handler * adding media * modifyignt he register action * adding user consent change * adding user consent change * adding error codes * build * change file * adding unit tests * adding test-app UI * addign an optional parameter to getContent * removing the register handler * build errors --------- Co-authored-by: Lakhveer Kaur <[email protected]>
1 parent b41cf30 commit 812f4e3

File tree

11 files changed

+770
-6
lines changed

11 files changed

+770
-6
lines changed

apps/teams-test-app/src/components/privateApis/CopilotAPIs.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { copilot, UUID } from '@microsoft/teams-js';
1+
import { copilot, sidePanelInterfaces, UUID } from '@microsoft/teams-js';
22
import React, { ReactElement } from 'react';
33

4+
import { generateRegistrationMsg } from '../../App';
45
import { ApiWithoutInput, ApiWithTextInput } from '../utils';
56
import { ModuleWrapper } from '../utils/ModuleWrapper';
67

@@ -30,12 +31,12 @@ const CopilotAPIs = (): ReactElement => {
3031
onClick: async () =>
3132
`Copilot.CustomTelemetry module ${copilot.customTelemetry.isSupported() ? 'is' : 'is not'} supported`,
3233
});
33-
interface InputType {
34+
interface CustomTelemetryInputType {
3435
stageNameIdentifier: string;
3536
timestamp?: number;
3637
}
3738
const SendCustomTelemetryData = (): ReactElement =>
38-
ApiWithTextInput<InputType>({
39+
ApiWithTextInput<CustomTelemetryInputType>({
3940
name: 'sendCustomTelemetryData',
4041
title: 'sendCustomTelemetryData',
4142
onClick: {
@@ -56,6 +57,58 @@ const CopilotAPIs = (): ReactElement => {
5657
}),
5758
});
5859

60+
const CheckCopilotSidePanelCapability = (): ReactElement =>
61+
ApiWithoutInput({
62+
name: 'checkCopilotSidePanelCapability',
63+
title: 'Check if Copilot.SidePanel is supported',
64+
onClick: async () => `Copilot.SidePanel module ${copilot.sidePanel.isSupported() ? 'is' : 'is not'} supported`,
65+
});
66+
67+
const GetContent = (): ReactElement =>
68+
ApiWithTextInput<sidePanelInterfaces.ContentRequest>({
69+
name: 'getContent',
70+
title: 'getContent',
71+
onClick: {
72+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
73+
validateInput: (_input) => {},
74+
submit: async (input) => {
75+
try {
76+
const result = input ? await copilot.sidePanel.getContent(input) : await copilot.sidePanel.getContent();
77+
return JSON.stringify(result);
78+
} catch (error) {
79+
return `Error: ${error}`;
80+
}
81+
},
82+
},
83+
defaultInput: JSON.stringify({
84+
localEndpointInfo: 'read',
85+
}),
86+
});
87+
88+
const PreCheckUserConsent = (): ReactElement =>
89+
ApiWithoutInput({
90+
name: 'preCheckUserConsent',
91+
title: 'Get User Consent',
92+
onClick: async () => {
93+
const result = await copilot.sidePanel.preCheckUserConsent();
94+
return JSON.stringify(result);
95+
},
96+
});
97+
98+
const RegisterUserActionContentSelect = (): React.ReactElement =>
99+
ApiWithoutInput({
100+
name: 'registerUserActionContentSelect',
101+
title: 'Register UserAction Content Select',
102+
onClick: async (setResult) => {
103+
const handler = (data: sidePanelInterfaces.Content): void => {
104+
const res = `UserAction Content Select called with data: ${JSON.stringify(data)}`;
105+
setResult(res);
106+
};
107+
copilot.sidePanel.registerUserActionContentSelect(handler);
108+
return generateRegistrationMsg('then the content is selected by the user');
109+
},
110+
});
111+
59112
return (
60113
<>
61114
<ModuleWrapper title="Copilot.Eligibility">
@@ -66,6 +119,12 @@ const CopilotAPIs = (): ReactElement => {
66119
<CheckCopilotCustomTelemetryCapability />
67120
<SendCustomTelemetryData />
68121
</ModuleWrapper>
122+
<ModuleWrapper title="Copilot.SidePanel">
123+
<CheckCopilotSidePanelCapability />
124+
<RegisterUserActionContentSelect />
125+
<GetContent />
126+
<PreCheckUserConsent />
127+
</ModuleWrapper>
69128
</>
70129
);
71130
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Added `{copilot.sidePanel}` capability that will help copilot to receive more context aware data from the hosts. The capability is still awaiting support in one or most host applications. To track availability of this capability across different hosts see https://aka.ms/capmatrix",
4+
"packageName": "@microsoft/teams-js",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/teams-js/src/internal/telemetry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export const enum ApiName {
109109
Conversations_RegisterStartConversationHandler = 'conversations.registerStartConversationHandler',
110110
Copilot_CustomTelemetry_SendCustomTelemetryData = 'copilot.customTelemetry.sendCustomTelemetryData',
111111
Copilot_Eligibility_GetEligibilityInfo = 'copilot.eligibility.getEligibilityInfo',
112+
Copilot_SidePanel_RegisterUserActionContentSelect = 'copilot.sidePanel.registerUserActionContentSelect',
113+
Copilot_SidePanel_RegisterOnUserConsentChange = 'copilot.sidePanel.registerOnUserConsentChange',
114+
Copilot_SidePanel_GetContent = 'copilot.sidePanel.getContent',
115+
Copilot_SidePanel_PreCheckUserConsent = 'copilot.sidePanel.preCheckUserConsent',
112116
Dialog_AdaptiveCard_Bot_Open = 'dialog.adaptiveCard.bot.open',
113117
Dialog_AdaptiveCard_Open = 'dialog.adaptiveCard.open',
114118
Dialog_RegisterMessageForChildHandler = 'dialog.registerMessageForChildHandler',
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as customTelemetry from './customTelemetry';
22
import * as eligibility from './eligibility';
3+
import * as sidePanel from './sidePanel';
34

4-
export { customTelemetry, eligibility };
5+
export { customTelemetry, eligibility, sidePanel };
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* @beta
3+
* @hidden
4+
* User information required by specific apps
5+
* @internal
6+
* Limited to Microsoft-internal use
7+
* @module
8+
*/
9+
10+
import { callFunctionInHostAndHandleResponse } from '../../internal/communication';
11+
import { registerHandlerHelper } from '../../internal/handlers';
12+
import { ensureInitialized } from '../../internal/internalAPIs';
13+
import { ResponseHandler } from '../../internal/responseHandler';
14+
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../../internal/telemetry';
15+
import { ISerializable } from '../../public';
16+
import { FrameContexts } from '../../public/constants';
17+
import { isSdkError, SdkError } from '../../public/interfaces';
18+
import { runtime } from '../../public/runtime';
19+
import {
20+
Content,
21+
ContentRequest,
22+
PreCheckContextResponse,
23+
SidePanelError,
24+
SidePanelErrorCode,
25+
SidePanelErrorImpl,
26+
} from './sidePanelInterfaces';
27+
28+
const copilotTelemetryVersionNumber: ApiVersionNumber = ApiVersionNumber.V_2;
29+
30+
/**
31+
* @hidden
32+
* @internal
33+
* Limited to Microsoft-internal use
34+
* @beta
35+
* @returns boolean to represent whether copilot.sidePanel capability is supported
36+
*
37+
* @throws Error if {@linkcode app.initialize} has not successfully completed
38+
*/
39+
export function isSupported(): boolean {
40+
return ensureInitialized(runtime) && !!runtime.supports.copilot?.sidePanel;
41+
}
42+
43+
/**
44+
* @beta
45+
* @hidden
46+
* Determines if the provided error object is an instance of SidePanelError or SdkError.
47+
* @internal
48+
* Limited to Microsoft-internal use
49+
* @param err The error object to check whether it is of SidePanelError type
50+
*/
51+
export function isResponseAReportableError(err: unknown): err is SidePanelError | SdkError {
52+
if (typeof err !== 'object' || err === null) {
53+
return false;
54+
}
55+
56+
const error = err as SidePanelError;
57+
58+
return (
59+
(Object.values(SidePanelErrorCode).includes(error.errorCode as SidePanelErrorCode) &&
60+
(error.message === undefined || typeof error.message === 'string')) ||
61+
isSdkError(err) // If the error is an SdkError, it can be considered a SidePanelError
62+
);
63+
}
64+
/**
65+
* Get user content data from the hub to send to copilot app.
66+
*
67+
* @returns { Promise<Content> } - promise resolves with a content object containing user content data
68+
* @throws { SidePanelError | SdkError } - Throws a SidePanelError or SdkError if host SDK returns an error as a response to this call
69+
*
70+
* @hidden
71+
* @beta
72+
* @internal
73+
* Limited to Microsoft-internal use
74+
*/
75+
export async function getContent(request?: ContentRequest): Promise<Content> {
76+
ensureInitialized(runtime);
77+
const input = request ? [new SerializableContentRequest(request)] : [];
78+
return callFunctionInHostAndHandleResponse(
79+
ApiName.Copilot_SidePanel_GetContent,
80+
input,
81+
new GetContentResponseHandler(),
82+
getApiVersionTag(copilotTelemetryVersionNumber, ApiName.Copilot_SidePanel_GetContent),
83+
isResponseAReportableError,
84+
);
85+
}
86+
87+
/**
88+
* When the copilot detects a contextual query it gets the user consent status before making the getContent call.
89+
*
90+
* @returns { Promise<PreCheckContextResponse> } - promise resolves with a content object containing user content data
91+
* @throws { SidePanelError | SdkError } - Throws a SidePanelError or SdkError if host SDK returns an error as a response to this call
92+
*
93+
* @hidden
94+
* @beta
95+
* @internal
96+
* Limited to Microsoft-internal use
97+
*/
98+
export async function preCheckUserConsent(): Promise<PreCheckContextResponse> {
99+
ensureInitialized(runtime);
100+
return callFunctionInHostAndHandleResponse(
101+
ApiName.Copilot_SidePanel_PreCheckUserConsent,
102+
[],
103+
new PreCheckContextResponseHandler(),
104+
getApiVersionTag(copilotTelemetryVersionNumber, ApiName.Copilot_SidePanel_PreCheckUserConsent),
105+
isResponseAReportableError,
106+
);
107+
}
108+
109+
/** Register user action content select handler function type */
110+
export type userActionHandlerType = (selectedContent: Content) => void;
111+
/**
112+
* @hidden
113+
* @beta
114+
* Registers a handler to get updated content data from the hub to send to copilot app.
115+
* This handler will be called when the user selects content in the application.
116+
* @param handler - The handler for getting user action content select.
117+
*
118+
* @internal
119+
* Limited to Microsoft-internal use
120+
*/
121+
export function registerUserActionContentSelect(handler: userActionHandlerType): void {
122+
registerHandlerHelper(
123+
getApiVersionTag(copilotTelemetryVersionNumber, ApiName.Copilot_SidePanel_RegisterUserActionContentSelect),
124+
'copilot.sidePanel.userActionContentSelect',
125+
handler,
126+
[FrameContexts.content],
127+
() => {
128+
if (!isSupported()) {
129+
throw copilotSidePanelNotSupportedOnPlatformError;
130+
}
131+
},
132+
);
133+
}
134+
135+
/**
136+
* @hidden
137+
* @beta
138+
* @internal
139+
* Limited to Microsoft-internal use
140+
*
141+
* Error thrown when the copilot side panel API is not supported on the current platform.
142+
*/
143+
export const copilotSidePanelNotSupportedOnPlatformError = new SidePanelErrorImpl(
144+
SidePanelErrorCode.NotSupportedOnPlatform,
145+
'This API is not supported on the current platform.',
146+
);
147+
class GetContentResponseHandler extends ResponseHandler<Content, Content> {
148+
public validate(response: Content): boolean {
149+
return response !== null && typeof response === 'object';
150+
}
151+
152+
public deserialize(response: Content): Content {
153+
return response;
154+
}
155+
}
156+
157+
class PreCheckContextResponseHandler extends ResponseHandler<PreCheckContextResponse, PreCheckContextResponse> {
158+
public validate(response: PreCheckContextResponse): boolean {
159+
return response !== null && typeof response === 'object';
160+
}
161+
162+
public deserialize(response: PreCheckContextResponse): PreCheckContextResponse {
163+
return response;
164+
}
165+
}
166+
167+
class SerializableContentRequest implements ISerializable {
168+
public constructor(private contentRequest: ContentRequest) {}
169+
public serialize(): object {
170+
return this.contentRequest;
171+
}
172+
}

0 commit comments

Comments
 (0)