diff --git a/README.md b/README.md index 4f8c9ec..ea29995 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ The component uses a `PostMessageHandler` utility class that: The React component (`CortiEmbeddedReact`) is available as an additional export and provides: -- **Hook-based API access**: `useCortiEmbeddedApi(ref)` exposes instance-bound methods (`auth`, `navigate`, `createInteraction`, etc.) +- **Hook-based API access**: `useCortiEmbeddedApi(ref)` exposes instance-bound methods (`auth`, `navigate`, `createInteraction`, `showDeviceLinkQR`, etc.) - **Generic event stream**: `onEvent` receives the wrapper's `event` `CustomEvent`, with `event.detail` shaped as `{ name, payload }` - **Status hook**: `useCortiEmbeddedStatus(ref)` keeps latest status/reactive state - **Multi-instance safety**: API methods are scoped to the ref you pass diff --git a/demo/demo.js b/demo/demo.js index 42354bb..d0ecb13 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -51,6 +51,37 @@ window.testAuthentication = async () => { } }; +window.showDeviceLinkQR = async () => { + if (component?.showDeviceLinkQR) { + try { + const payloadText = document.getElementById( + "device-link-qr-payload", + ).value; + let payload; + try { + payload = JSON.parse(payloadText); + } catch (jsonError) { + window.addLogEntry( + `Invalid JSON in payload: ${jsonError.message}`, + "error", + ); + return; + } + + window.addLogEntry("Showing device-link QR", "info"); + const response = await component.showDeviceLinkQR(payload); + window.addLogEntry( + `Device-link QR finished with status: ${response.status}`, + "success", + ); + } catch (error) { + window.addLogEntry(`Device-link QR failed: ${error.message}`, "error"); + } + } else { + window.addLogEntry("Component not ready for showDeviceLinkQR", "error"); + } +}; + window.configureApp = async () => { if (component?.configureApp) { try { @@ -384,6 +415,9 @@ document.addEventListener("DOMContentLoaded", () => { document .getElementById("auth-btn") .addEventListener("click", window.testAuthentication); + document + .getElementById("device-link-qr-btn") + .addEventListener("click", window.showDeviceLinkQR); document .getElementById("clear-log-btn") .addEventListener("click", window.clearLog); diff --git a/demo/demo.ts b/demo/demo.ts index 4a0c53a..cd04178 100644 --- a/demo/demo.ts +++ b/demo/demo.ts @@ -1,6 +1,7 @@ import type { ConfigureApplicationPayload, ConfigurePayload, + DeviceLinkTokenResponse, Fact, InteractionDetails, CreateInteractionPayload, @@ -114,6 +115,40 @@ export const testAuthentication = async (): Promise => { } }; +export const showDeviceLinkQR = async (): Promise => { + if (component?.showDeviceLinkQR) { + try { + const payloadElement = document.getElementById( + "device-link-qr-payload", + ) as HTMLTextAreaElement; + const payloadText = payloadElement.value; + let payload: DeviceLinkTokenResponse; + + try { + payload = JSON.parse(payloadText) as DeviceLinkTokenResponse; + } catch (jsonError) { + const errorMessage = + jsonError instanceof Error ? jsonError.message : "Unknown JSON error"; + addLogEntry(`Invalid JSON in payload: ${errorMessage}`, "error"); + return; + } + + addLogEntry("Showing device-link QR", "info"); + const response = await component.showDeviceLinkQR(payload); + addLogEntry( + `Device-link QR finished with status: ${response.status}`, + "success", + ); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(`Device-link QR failed: ${errorMessage}`); + } + } else { + addLogEntry("Component not ready for showDeviceLinkQR", "error"); + } +}; + export const setInteractionOptions = async (): Promise => { if (component?.setInteractionOptions) { try { @@ -482,6 +517,7 @@ declare global { showCorti: typeof showCorti; hideCorti: typeof hideCorti; testAuthentication: typeof testAuthentication; + showDeviceLinkQR: typeof showDeviceLinkQR; setInteractionOptions: typeof setInteractionOptions; addFacts: typeof addFacts; navigate: typeof navigate; @@ -504,6 +540,7 @@ Object.assign(window, { showCorti, hideCorti, testAuthentication, + showDeviceLinkQR, setInteractionOptions, addFacts, navigate, @@ -551,6 +588,9 @@ document.addEventListener("DOMContentLoaded", () => { document .getElementById("auth-btn") ?.addEventListener("click", testAuthentication); + document + .getElementById("device-link-qr-btn") + ?.addEventListener("click", showDeviceLinkQR); document.getElementById("clear-log-btn")?.addEventListener("click", clearLog); document .getElementById("create-interaction-btn") diff --git a/demo/index.html b/demo/index.html index aae23cd..02a1a12 100644 --- a/demo/index.html +++ b/demo/index.html @@ -45,6 +45,28 @@

Corti Embedded Web Component Demo

+
+
Device Link QR
+
+
+ ⚙️ Settings + + +
+ +
+
+
Configure App
@@ -55,7 +77,28 @@

Corti Embedded Web Component Demo

id="configure-app-payload" placeholder='{"ui": {"aiChat": false}}' > -{"ui": {"aiChat": false, "navigation": true}}
+
+
Device Link QR
+
+
+ ⚙️ Settings + + +
+ +
+
+
Set Credentials
@@ -104,6 +126,7 @@

Corti Embedded Web Component Demo (TypeScript)

placeholder='{"appearance": {"primaryColor": "#0066cc"}, "ui": {"interactionTitle": true, "aiChat": true}}' > { + "debug": false, "appearance": { "primaryColor": "#0066cc" }, @@ -113,8 +136,15 @@

Corti Embedded Web Component Demo (TypeScript)

"documentFeedback": true, "navigation": true }, + "companionApp": { + "enabled": true + }, "locale": { - "interfaceLanguage": "en" + "interfaceLanguage": "en", + "dictationLanguage": "en", + "overrides": { + "assistant.header.title": "Embedded Assistant" + } } } @@ -158,16 +188,51 @@

Corti Embedded Web Component Demo (TypeScript)

"options": ["in-person", "virtual"] }, "spokenLanguage": { - "fallback": "en" + "fallback": "en", + "options": ["en", "da"] }, "templates": { + "sources": { + "personal": { + "enabled": true, + "sectionFields": { + "heading": {"editable": true}, + "description": {"editable": true}, + "contentPrompt": {"visible": true, "editable": true}, + "writingStylePrompt": {"visible": true, "editable": true}, + "miscPrompt": {"visible": true, "editable": true}, + "outputSchema": {"visible": true, "editable": false} + } + }, + "standard": { + "enabled": true, + "include": { + "regions": ["en"], + "families": ["soap"] + } + }, + "project": { + "enabled": true, + "exclude": { + "ids": ["deprecated-template"] + } + } + }, "defaultTemplate": { "behaviour": "fallback", "template": { "source": "standard", "id": "soap_note-en" - } + }, + "allowUserSelection": true } + }, + "documents": { + "actions": { + "sync": true + }, + "allowedLanguages": ["en"], + "maxGenerated": "unlimited" } } diff --git a/docs/react-usage.md b/docs/react-usage.md index 11fb047..916028d 100644 --- a/docs/react-usage.md +++ b/docs/react-usage.md @@ -106,6 +106,11 @@ function ApiExample() { await api.stopRecording(); const status = await api.getStatus(); await api.configureApp({ ui: { aiChat: false } }); + await api.showDeviceLinkQR({ + access_token: "...", + token_type: "Bearer", + refresh_token: "...", + }); await api.setCredentials({ password: "..." }); api.show(); api.hide(); diff --git a/src/CortiEmbedded.ts b/src/CortiEmbedded.ts index b2e6686..48a383b 100644 --- a/src/CortiEmbedded.ts +++ b/src/CortiEmbedded.ts @@ -12,6 +12,7 @@ import type { ConfigureSessionPayload, CreateInteractionPayload, CreateInteractionResponse, + DeviceLinkTokenResponse, NavigatePayload, SetInteractionOptionsPayload, SetCredentialsPayload, @@ -22,6 +23,7 @@ import type { GetStatusResponse, GetTemplatesResponse, KeycloakTokenResponse, + ShowDeviceLinkQRResponse, } from "./public-types.js"; import { baseStyles } from "./styles/base.js"; import { containerStyles } from "./styles/container-styles.js"; @@ -684,6 +686,39 @@ export class CortiEmbedded extends LitElement implements CortiEmbeddedAPI { } } + /** + * Show the device-link QR code in the embedded UI + * @param payload Device-link token response to render as a QR code + * @returns Promise resolving to the rendered QR code state + */ + async showDeviceLinkQR( + payload: DeviceLinkTokenResponse, + ): Promise { + if (!this.postMessageHandler) { + throw new Error("Component not ready"); + } + + try { + const response = await this.postMessageHandler.postMessage({ + type: "CORTI_EMBEDDED", + version: "v1", + action: "showDeviceLinkQR", + payload, + }); + + if (response.success && response.payload) { + return response.payload as ShowDeviceLinkQRResponse; + } + throw new Error(response.error); + } catch (error) { + const formattedError = formatError( + error, + "Failed to show device link QR", + ); + throw new Error(JSON.stringify(formattedError)); + } + } + /** * Check the current status of the iframe and PostMessageHandler * Useful for debugging diff --git a/src/public-types.ts b/src/public-types.ts index 9d804a8..1738136 100644 --- a/src/public-types.ts +++ b/src/public-types.ts @@ -1,11 +1,7 @@ import type { - AppearanceConfig, ConfigureAppPayload as GeneratedConfigureApplicationPayload, ConfigureFeaturesConfig, ConfigurePayload, - LocaleConfig, - NetworkConfig, - UIConfig, } from "./types/config.js"; import type { AuthChangedEventPayload, @@ -19,6 +15,7 @@ import type { AddFactsPayload, ConfigureSessionPayload, CreateInteractionPayload, + DeviceLinkTokenResponse, Fact, KeycloakTokenResponse, NavigatePayload, @@ -33,6 +30,7 @@ import type { CreateInteractionResponse, GetStatusResponse, GetTemplatesResponse, + ShowDeviceLinkQRResponse, } from "./types/responses.js"; export type { @@ -55,6 +53,7 @@ export type { AddFactsPayload, ConfigureSessionPayload, CreateInteractionPayload, + DeviceLinkTokenResponse, Fact, KeycloakTokenResponse, NavigatePayload, @@ -83,6 +82,7 @@ export type { EmbeddedTemplate, GetStatusResponse, GetTemplatesResponse, + ShowDeviceLinkQRResponse, UserInfo, } from "./types/responses.js"; @@ -103,7 +103,8 @@ export type ConfigureApplicationPayload = GeneratedConfigureApplicationPayload; */ export type ConfigureAppResponse = ConfigureResponse; -export type ConfigureApplicationResponse = GeneratedConfigureApplicationResponse; +export type ConfigureApplicationResponse = + GeneratedConfigureApplicationResponse; /** * User information returned from authentication @@ -167,6 +168,9 @@ export interface CortiEmbeddedV1API { setCredentials(payload: SetCredentialsPayload): Promise; getStatus(): Promise; getTemplates(): Promise; + showDeviceLinkQR( + payload: DeviceLinkTokenResponse, + ): Promise; } export interface CortiEmbeddedWindowAPI { @@ -270,9 +274,9 @@ export interface CortiEmbeddedAPI { /** * Set one-shot interaction options for the embedded instance. * - * Each call replaces the full interaction-options snapshot for the embedded - * instance. Omitted branches reset to their defaults rather than preserving - * values from a previous call. + * Each call applies the provided interaction-options branches to the current + * snapshot for the embedded instance. Omitted branches preserve their existing + * values from previous calls. * @param config Interaction/session-level options * @returns Promise that resolves when options are applied */ @@ -294,4 +298,13 @@ export interface CortiEmbeddedAPI { * Hide the embedded UI */ hide(): void; -} \ No newline at end of file + + /** + * Show the device-link QR code in the embedded UI + * @param payload Device-link token response to render as a QR code + * @returns Promise resolving to the rendered QR code state + */ + showDeviceLinkQR( + payload: DeviceLinkTokenResponse, + ): Promise; +} diff --git a/src/react/CortiEmbeddedReact.ts b/src/react/CortiEmbeddedReact.ts index 457a523..20a1ede 100644 --- a/src/react/CortiEmbeddedReact.ts +++ b/src/react/CortiEmbeddedReact.ts @@ -239,6 +239,9 @@ export interface UseCortiEmbeddedApiResult { setCredentials: ( ...args: Parameters ) => ReturnType; + showDeviceLinkQR: ( + ...args: Parameters + ) => ReturnType; show: ( ...args: Parameters ) => ReturnType; @@ -283,6 +286,7 @@ export function useCortiEmbeddedApi( getInstance().setInteractionOptions(...args), getTemplates: (...args) => getInstance().getTemplates(...args), setCredentials: (...args) => getInstance().setCredentials(...args), + showDeviceLinkQR: (...args) => getInstance().showDeviceLinkQR(...args), show: (...args) => getInstance().show(...args), hide: (...args) => getInstance().hide(...args), }), diff --git a/src/types/api.ts b/src/types/api.ts index a7175d2..450d8c1 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -13,6 +13,7 @@ import type { AddFactsPayload, ConfigureSessionPayload, CreateInteractionPayload, + DeviceLinkTokenResponse, Fact, KeycloakTokenResponse, NavigatePayload, @@ -27,11 +28,12 @@ import type { CreateInteractionResponse, GetStatusResponse, GetTemplatesResponse, + ShowDeviceLinkQRResponse, } from "./responses.js"; export type { ConfigureAppPayload, ConfigurePayload } from "./config.js"; // Re-export common types for public API -export type { UserInfo } from "./responses.js"; +export type { ShowDeviceLinkQRResponse, UserInfo } from "./responses.js"; /** * User information returned from authentication @@ -79,7 +81,9 @@ export interface EmbeddedEventData { // Window API Types export interface CortiEmbeddedV1API { auth(payload: KeycloakTokenResponse): Promise; - createInteraction(payload: CreateInteractionPayload): Promise; + createInteraction( + payload: CreateInteractionPayload, + ): Promise; addFacts(payload: AddFactsPayload): Promise; configureApp(payload: ConfigureAppPayload): Promise; configureSession(payload: ConfigureSessionPayload): Promise; @@ -91,6 +95,9 @@ export interface CortiEmbeddedV1API { setCredentials(payload: SetCredentialsPayload): Promise; getStatus(): Promise; getTemplates(): Promise; + showDeviceLinkQR( + payload: DeviceLinkTokenResponse, + ): Promise; } export interface CortiEmbeddedWindowAPI { v1: CortiEmbeddedV1API; @@ -124,7 +131,9 @@ export interface CortiEmbeddedAPI { * @param encounter Encounter request data * @returns Promise resolving to interaction details */ - createInteraction(encounter: CreateInteractionPayload): Promise; + createInteraction( + encounter: CreateInteractionPayload, + ): Promise; /** * Configure the current session @@ -212,4 +221,13 @@ export interface CortiEmbeddedAPI { * Hide the embedded UI */ hide(): void; + + /** + * Show the device-link QR code in the embedded UI + * @param payload Device-link token response to render as a QR code + * @returns Promise resolving to the rendered QR code state + */ + showDeviceLinkQR( + payload: DeviceLinkTokenResponse, + ): Promise; } diff --git a/src/types/config.ts b/src/types/config.ts index 68ec05c..f1e8d80 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -11,6 +11,10 @@ export interface UIConfig { navigation: boolean; } +export interface CompanionAppConfig { + enabled: boolean; +} + export interface ConfigureFeaturesConfig { interactionTitle: boolean; aiChat: boolean; @@ -42,6 +46,7 @@ export interface ConfigurePayload { export interface ConfigureAppPayload { debug?: boolean; ui?: Partial; + companionApp?: CompanionAppConfig; appearance?: Partial; locale?: Partial; network?: Partial; diff --git a/src/types/generated/interview-details.ts b/src/types/generated/interview-details.ts index e1bf76c..990e2ac 100644 --- a/src/types/generated/interview-details.ts +++ b/src/types/generated/interview-details.ts @@ -7,7 +7,14 @@ export type EmbeddedInterviewDetails = { /** Interview title */ title: string | null; /** Current state of the interaction */ - state: "planned" | "ongoing" | "paused" | "disconnected" | "ending" | "parsing" | "ended"; + state: + | "planned" + | "ongoing" + | "paused" + | "disconnected" + | "ending" + | "parsing" + | "ended"; /** ISO 8601 timestamp when interview started */ startedAt: string; /** ISO 8601 timestamp when interview ended */ diff --git a/src/types/payloads.ts b/src/types/payloads.ts index eb31ae4..8dea3c3 100644 --- a/src/types/payloads.ts +++ b/src/types/payloads.ts @@ -22,6 +22,10 @@ export interface KeycloakTokenResponse { }; } +export type DeviceLinkTokenResponse = KeycloakTokenResponse & { + refresh_token: string; +}; + // Fact structure export interface Fact { text: Corti.FactsCreateInput["text"]; @@ -64,27 +68,74 @@ export interface ConfigureSessionPayload { } export interface InteractionModeOptions { - fallback: DefaultMode; + fallback?: DefaultMode; options: DefaultMode[]; } export interface SpokenLanguageOptions { - fallback: string; + fallback?: string; + options?: string[]; +} + +export interface InlineTemplateLabel { + key: string; + value: string; +} + +export interface InlineTemplateSectionInstructions { + contentPrompt: string; + writingStylePrompt?: string; + miscPrompt?: string; +} + +export interface InlineTemplateSection { + heading: string; + labels?: InlineTemplateLabel[]; + instructions: InlineTemplateSectionInstructions; + outputSchema?: Record; +} + +export interface InlineTemplate { + id?: string; + name: string; + labels?: InlineTemplateLabel[]; + generation: { + instructions: { + prompt: string; + }; + sections: InlineTemplateSection[]; + }; } export interface InteractionTemplateReference { - source: "standard"; + source: "standard" | "project"; id: string; } export interface DefaultInteractionTemplateOptions { - behaviour: "fallback"; - template: InteractionTemplateReference; + behaviour?: "fallback"; + template?: InteractionTemplateReference; + allowUserSelection?: boolean; +} + +export interface PersonalTemplateSectionFieldConfig { + visible?: boolean; + editable?: boolean; +} + +export interface PersonalTemplateSectionFields { + heading?: { editable?: boolean }; + description?: { editable?: boolean }; + contentPrompt?: PersonalTemplateSectionFieldConfig; + writingStylePrompt?: PersonalTemplateSectionFieldConfig; + miscPrompt?: PersonalTemplateSectionFieldConfig; + outputSchema?: PersonalTemplateSectionFieldConfig; } export interface InteractionTemplateSources { personal?: { - enabled: boolean; + enabled?: boolean; + sectionFields?: PersonalTemplateSectionFields; }; standard?: { enabled?: boolean; @@ -114,6 +165,7 @@ export interface InteractionDocumentOptions { sync?: boolean; }; allowedLanguages?: string[]; + maxGenerated?: number | "unlimited"; } export interface SetInteractionOptionsPayload { diff --git a/src/types/protocol.ts b/src/types/protocol.ts index 8f9574e..4d346dd 100644 --- a/src/types/protocol.ts +++ b/src/types/protocol.ts @@ -2,7 +2,10 @@ export type APIVersion = "v1"; -export type MessageType = "CORTI_EMBEDDED" | "CORTI_EMBEDDED_RESPONSE" | "CORTI_EMBEDDED_EVENT"; +export type MessageType = + | "CORTI_EMBEDDED" + | "CORTI_EMBEDDED_RESPONSE" + | "CORTI_EMBEDDED_EVENT"; export type DefaultMode = "virtual" | "in-person"; @@ -19,7 +22,8 @@ export type EmbeddedAction = | "getStatus" | "getTemplates" | "configure" - | "setCredentials"; + | "setCredentials" + | "showDeviceLinkQR"; export type DeprecatedEmbeddedEvent = | "ready" @@ -127,6 +131,10 @@ export interface SetCredentialsRequest extends EmbeddedRequest { action: "setCredentials"; } +export interface ShowDeviceLinkQRRequest extends EmbeddedRequest { + action: "showDeviceLinkQR"; +} + // Event Types export interface ReadyEvent extends DeprecatedEmbeddedEventMessage { event: "ready"; @@ -186,7 +194,8 @@ export type AnyEmbeddedRequest = | StopRecordingRequest | GetStatusRequest | ConfigureRequest - | SetCredentialsRequest; + | SetCredentialsRequest + | ShowDeviceLinkQRRequest; export type AnyEmbeddedResponse = EmbeddedResponse; @@ -205,4 +214,7 @@ export type AnyDeprecatedEmbeddedEvent = export type AnyEvent = EmbeddedEventMessage | AnyDeprecatedEmbeddedEvent; -export type AnyEmbeddedMessage = AnyEmbeddedRequest | AnyEmbeddedResponse | AnyEvent; +export type AnyEmbeddedMessage = + | AnyEmbeddedRequest + | AnyEmbeddedResponse + | AnyEvent; diff --git a/src/types/responses.ts b/src/types/responses.ts index 0821d43..3b93bdc 100644 --- a/src/types/responses.ts +++ b/src/types/responses.ts @@ -2,6 +2,7 @@ import type { AppearanceConfig, + CompanionAppConfig, ConfigureFeaturesConfig, LocaleConfig, NetworkConfig, @@ -75,6 +76,11 @@ export interface ConfigureAppResponse { debug?: boolean; appearance: AppearanceConfig; ui: UIConfig; + companionApp: CompanionAppConfig; locale: LocaleConfig; network: NetworkConfig; } + +export interface ShowDeviceLinkQRResponse { + status: "approved" | "denied" | "expired" | "dismissed"; +}