Compartir a través de


Introducción a la llamada de la biblioteca de interfaz de usuario de Azure Communication Services a las aplicaciones de voz de Teams

Este proyecto tiene como objetivo guiar a los desarrolladores a iniciar una llamada desde el SDK web de llamadas de Azure Communication Services a la cola de llamadas y el operador automático de Teams mediante la biblioteca de UI de Azure Communication.

En función de los requisitos, es posible que tenga que ofrecer a los clientes una forma sencilla de ponerse en contacto con usted sin ninguna configuración compleja.

La llamada a la cola de llamadas y el operador automático de Teams es un concepto sencillo pero eficaz que facilita la interacción instantánea con el servicio de atención al cliente, el asesor financiero y otros equipos de atención al cliente. El objetivo de este tutorial es ayudarle a iniciar interacciones con los clientes cuando hagan clic en un botón de la web.

Si desea probarlo, puede descargar el código de GitHub.

Después de este tutorial:

  • Le permite controlar la experiencia de audio y vídeo de los clientes en función del escenario del cliente
  • Enseñarle a crear un widget para iniciar llamadas en la aplicación web mediante la biblioteca de interfaz de usuario.

Página de inicio de la aplicación de ejemplo de Widget de llamada

Requisitos previos

Estos pasos son necesarios para seguir este tutorial. Póngase en contacto con el administrador de Teams con relación a los dos últimos elementos para asegurarse de estar configurado correctamente.

Comprobación de Node y Visual Studio Code

Puede comprobar que Node se instaló correctamente con este comando.

node -v

La salida indica la versión que tiene, se produce un error si Node no se instaló y se agregó a la PATH. Al igual que con Node, puede comprobar si VS Code se instaló con este comando.

code --version

Al igual que con Node, este comando produce un error si se produjo un problema al instalar VS Code en el equipo.

Introducción

Este tutorial tiene 7 pasos y, al final, la aplicación podrá llamar a una aplicación de voz de Teams. Los pasos son:

  1. Configurar el proyecto
  2. Obtener las dependencias
  3. Configuración inicial de la aplicación
  4. Creación del widget
  5. Estilo del widget
  6. Configuración de valores de identidad
  7. Ejecución de la aplicación

1. Configuración del proyecto

Utilice este paso solo si está creando una nueva aplicación.

Para configurar la aplicación React, usamos la herramienta de línea de comandos create-react-app. Esta herramienta crea una aplicación TypeScript fácil de ejecutar con tecnología de React.

Para asegurarse de que tiene Node instalado en la máquina, ejecute este comando en PowerShell o el terminal para ver la versión del nodo:

node -v

Si no tiene create-react-app instalado en la máquina, ejecute el siguiente comando para instalarlo como un comando global:

npm install -g create-react-app

Una vez instalado ese comando, ejecute el siguiente comando para crear una nueva aplicación react para compilar el ejemplo en:

# Create an Azure Communication Services App powered by React.
npx create-react-app ui-library-calling-widget-app --template typescript

# Change to the directory of the newly created App.
cd ui-library-calling-widget-app

Una vez completados estos comandos, quiere abrir el proyecto creado en VS Code. Puede abrir el proyecto con el siguiente comando.

code .

2. Obtención de las dependencias

Después, debe actualizar la matriz de dependencias en package.json para incluir algunos paquetes de Azure Communication Services a fin de que funcione la experiencia de widget que se va a crear:

"@azure/communication-calling": "^1.23.1",
"@azure/communication-chat": "^1.4.0",
"@azure/communication-react": "^1.15.0",
"@azure/communication-calling-effects": "1.0.1",
"@azure/communication-common": "2.3.0",
"@fluentui/react-icons": "~2.0.203",
"@fluentui/react": "~8.98.3",

Para instalar los paquetes necesarios, ejecute el siguiente comando del Administrador de paquetes de Node.

npm install

Después de instalar estos paquetes, todos están configurados para empezar a escribir el código que compila la aplicación. En este tutorial, vamos a modificar los archivos en el directorio de src.

3. Configuración inicial de la aplicación

Para empezar, reemplazamos el contenido proporcionado App.tsx por una página principal que:

  • Almacene toda la información de Azure Communication que necesitamos para crear un CallAdapter para impulsar nuestra experiencia de llamada
  • Muestra del widget que se expone al usuario final.

El archivo App.tsx debería tener este aspecto:

src/App.tsx

import "./App.css";
import {
  CommunicationIdentifier,
  MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
  Spinner,
  Stack,
  initializeIcons,
  registerIcons,
  Text,
} from "@fluentui/react";
import { CallAdd20Regular, Dismiss20Regular } from "@fluentui/react-icons";
import logo from "./logo.svg";

import { CallingWidgetComponent } from "./components/CallingWidgetComponent";

registerIcons({
  icons: { dismiss: <Dismiss20Regular />, callAdd: <CallAdd20Regular /> },
});
initializeIcons();
function App() {
  /**
   * Token for local user.
   */
  const token = "<Enter your ACS Token here>";

  /**
   * User identifier for local user.
   */
  const userId: CommunicationIdentifier = {
    communicationUserId: "Enter your ACS Id here",
  };

  /**
   * Enter your Teams voice app identifier from the Teams admin center here
   */
  const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
    teamsAppId: "<Enter your Teams Voice app id here>",
    cloud: "public",
  };

  const widgetParams = {
    userId,
    token,
    teamsAppIdentifier,
  };

  if (!token || !userId || !teamsAppIdentifier) {
    return (
      <Stack verticalAlign="center" style={{ height: "100%", width: "100%" }}>
        <Spinner
          label={"Getting user credentials from server"}
          ariaLive="assertive"
          labelPosition="top"
        />
      </Stack>
    );
  }

  return (
    <Stack
      style={{ height: "100%", width: "100%", padding: "3rem" }}
      tokens={{ childrenGap: "1.5rem" }}
    >
      <Stack tokens={{ childrenGap: "1rem" }} style={{ margin: "auto" }}>
        <Stack
          style={{ padding: "3rem" }}
          horizontal
          tokens={{ childrenGap: "2rem" }}
        >
          <Text style={{ marginTop: "auto" }} variant="xLarge">
            Welcome to a Calling Widget sample
          </Text>
          <img
            style={{ width: "7rem", height: "auto" }}
            src={logo}
            alt="logo"
          />
        </Stack>

        <Text>
          Welcome to a Calling Widget sample for the Azure Communication
          Services UI Library. Sample has the ability to connect you through
          Teams voice apps to a agent to help you.
        </Text>
        <Text>
          As a user all you need to do is click the widget below, enter your
          display name for the call - this will act as your caller id, and
          action the <b>start call</b> button.
        </Text>
      </Stack>
      <Stack
        horizontal
        tokens={{ childrenGap: "1.5rem" }}
        style={{ overflow: "hidden", margin: "auto" }}
      >
        <CallingWidgetComponent
          widgetAdapterArgs={widgetParams}
          onRenderLogo={() => {
            return (
              <img
                style={{ height: "4rem", width: "4rem", margin: "auto" }}
                src={logo}
                alt="logo"
              />
            );
          }}
        />
      </Stack>
    </Stack>
  );
}

export default App;

En este fragmento de código se registran dos nuevos iconos, <Dismiss20Regular/> y <CallAdd20Regular>. Estos iconos se usan dentro del componente de widget que se creará en la sección siguiente.

4. Creación del widget

Ahora es necesario crear un widget que pueda mostrarse en tres modos diferentes:

  • En espera: este estado del widget es el que tendrá el componente antes y después de realizar una llamada
  • Configuración: este estado es cuando el widget solicita información del usuario, como su nombre.
  • En una llamada: aquí el widget se reemplaza por la biblioteca de interfaz de usuario Call Composite. Este modo de widget se da cuando el usuario llama a la aplicación de voz o habla con un agente.

Ahora se creará una carpeta llamada src/components. En esta carpeta, cree un nuevo archivo denominado CallingWidgetComponent.tsx. El archivo debería parecerse al siguiente fragmento de código:

CallingWidgetComponent.tsx

import {
  IconButton,
  PrimaryButton,
  Stack,
  TextField,
  useTheme,
  Checkbox,
  Icon,
  Spinner,
} from "@fluentui/react";
import React, { useEffect, useRef, useState } from "react";
import {
  callingWidgetSetupContainerStyles,
  checkboxStyles,
  startCallButtonStyles,
  callingWidgetContainerStyles,
  callIconStyles,
  logoContainerStyles,
  collapseButtonStyles,
} from "../styles/CallingWidgetComponent.styles";

import {
  AzureCommunicationTokenCredential,
  CommunicationUserIdentifier,
  MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
  CallAdapter,
  CallAdapterState,
  CallComposite,
  CommonCallAdapterOptions,
  StartCallIdentifier,
  createAzureCommunicationCallAdapter,
} from "@azure/communication-react";
// lets add to our react imports as well
import { useMemo } from "react";

import { callingWidgetInCallContainerStyles } from "../styles/CallingWidgetComponent.styles";

/**
 * Properties needed for our widget to start a call.
 */
export type WidgetAdapterArgs = {
  token: string;
  userId: CommunicationUserIdentifier;
  teamsAppIdentifier: MicrosoftTeamsAppIdentifier;
};

export interface CallingWidgetComponentProps {
  /**
   *  arguments for creating an AzureCommunicationCallAdapter for your Calling experience
   */
  widgetAdapterArgs: WidgetAdapterArgs;
  /**
   * Custom render function for displaying logo.
   * @returns
   */
  onRenderLogo?: () => JSX.Element;
}

/**
 * Widget for Calling Widget
 * @param props
 */
export const CallingWidgetComponent = (
  props: CallingWidgetComponentProps
): JSX.Element => {
  const { onRenderLogo, widgetAdapterArgs } = props;

  const [widgetState, setWidgetState] = useState<"new" | "setup" | "inCall">(
    "new"
  );
  const [displayName, setDisplayName] = useState<string>();
  const [consentToData, setConsentToData] = useState<boolean>(false);
  const [useLocalVideo, setUseLocalVideo] = useState<boolean>(false);
  const [adapter, setAdapter] = useState<CallAdapter>();

  const callIdRef = useRef<string>();

  const theme = useTheme();

  // add this before the React template
  const credential = useMemo(() => {
    try {
      return new AzureCommunicationTokenCredential(widgetAdapterArgs.token);
    } catch {
      console.error("Failed to construct token credential");
      return undefined;
    }
  }, [widgetAdapterArgs.token]);

  const adapterOptions: CommonCallAdapterOptions = useMemo(
    () => ({
      callingSounds: {
        callEnded: { url: "/sounds/callEnded.mp3" },
        callRinging: { url: "/sounds/callRinging.mp3" },
        callBusy: { url: "/sounds/callBusy.mp3" },
      },
    }),
    []
  );

  const callAdapterArgs = useMemo(() => {
    return {
      userId: widgetAdapterArgs.userId,
      credential: credential,
      targetCallees: [
        widgetAdapterArgs.teamsAppIdentifier,
      ] as StartCallIdentifier[],
      displayName: displayName,
      options: adapterOptions,
    };
  }, [
    widgetAdapterArgs.userId,
    widgetAdapterArgs.teamsAppIdentifier.teamsAppId,
    credential,
    displayName,
  ]);

  useEffect(() => {
    if (adapter) {
      adapter.on("callEnded", () => {
        /**
         * We only want to reset the widget state if the call that ended is the same as the current call.
         */
        if (
          adapter.getState().acceptedTransferCallState &&
          adapter.getState().acceptedTransferCallState?.id !== callIdRef.current
        ) {
          return;
        }
        setDisplayName(undefined);
        setWidgetState("new");
        setConsentToData(false);
        setAdapter(undefined);
        adapter.dispose();
      });

      adapter.on("transferAccepted", (e) => {
        console.log("transferAccepted", e);
      });

      adapter.onStateChange((state: CallAdapterState) => {
        if (state?.call?.id && callIdRef.current !== state?.call?.id) {
          callIdRef.current = state?.call?.id;
          console.log(`Call Id: ${callIdRef.current}`);
        }
      });
    }
  }, [adapter]);

  /** widget template for when widget is open, put any fields here for user information desired */
  if (widgetState === "setup") {
    return (
      <Stack
        styles={callingWidgetSetupContainerStyles(theme)}
        tokens={{ childrenGap: "1rem" }}
      >
        <IconButton
          styles={collapseButtonStyles}
          iconProps={{ iconName: "Dismiss" }}
          onClick={() => {
            setDisplayName(undefined);
            setConsentToData(false);
            setUseLocalVideo(false);
            setWidgetState("new");
          }}
        />
        <Stack tokens={{ childrenGap: "1rem" }} styles={logoContainerStyles}>
          <Stack style={{ transform: "scale(1.8)" }}>
            {onRenderLogo && onRenderLogo()}
          </Stack>
        </Stack>
        <TextField
          label={"Name"}
          required={true}
          placeholder={"Enter your name"}
          onChange={(_, newValue) => {
            setDisplayName(newValue);
          }}
        />
        <Checkbox
          styles={checkboxStyles(theme)}
          label={
            "Use video - Checking this box will enable camera controls and screen sharing"
          }
          onChange={(_, checked?: boolean | undefined) => {
            setUseLocalVideo(!!checked);
            setUseLocalVideo(true);
          }}
        ></Checkbox>
        <Checkbox
          required={true}
          styles={checkboxStyles(theme)}
          disabled={displayName === undefined}
          label={
            "By checking this box, you are consenting that we will collect data from the call for customer support reasons"
          }
          onChange={async (_, checked?: boolean | undefined) => {
            setConsentToData(!!checked);
            if (callAdapterArgs && callAdapterArgs.credential) {
              setAdapter(
                await createAzureCommunicationCallAdapter({
                  displayName: displayName ?? "",
                  userId: callAdapterArgs.userId,
                  credential: callAdapterArgs.credential,
                  targetCallees: callAdapterArgs.targetCallees,
                  options: callAdapterArgs.options,
                })
              );
            }
          }}
        ></Checkbox>
        <PrimaryButton
          styles={startCallButtonStyles(theme)}
          onClick={() => {
            if (displayName && consentToData && adapter) {
              setWidgetState("inCall");
              adapter?.startCall(callAdapterArgs.targetCallees, {
                audioOptions: { muted: false },
              });
            }
          }}
        >
          {!consentToData && `Enter your name`}
          {consentToData && !adapter && (
            <Spinner ariaLive="assertive" labelPosition="top" />
          )}
          {consentToData && adapter && `StartCall`}
        </PrimaryButton>
      </Stack>
    );
  }

  if (widgetState === "inCall" && adapter) {
    return (
      <Stack styles={callingWidgetInCallContainerStyles(theme)}>
        <CallComposite
          adapter={adapter}
          options={{
            callControls: {
              cameraButton: useLocalVideo,
              screenShareButton: useLocalVideo,
              moreButton: false,
              peopleButton: false,
              displayType: "compact",
            },
            localVideoTile: !useLocalVideo ? false : { position: "floating" },
          }}
        />
      </Stack>
    );
  }

  return (
    <Stack
      horizontalAlign="center"
      verticalAlign="center"
      styles={callingWidgetContainerStyles(theme)}
      onClick={() => {
        setWidgetState("setup");
      }}
    >
      <Stack
        horizontalAlign="center"
        verticalAlign="center"
        style={{
          height: "4rem",
          width: "4rem",
          borderRadius: "50%",
          background: theme.palette.themePrimary,
        }}
      >
        <Icon iconName="callAdd" styles={callIconStyles(theme)} />
      </Stack>
    </Stack>
  );
};

En el CallAdapterOptions, vemos algunos archivos de sonido a los que se hace referencia, estos archivos son para usar la característica Sonidos de llamada en la CallComposite. Si está interesado en usar los sonidos, vea el código completado para descargar los archivos de sonido.

5. Estilo del widget

Será necesario escribir algunos estilos para asegurarse de que el widget tiene el aspecto adecuado y puede contener la composición de la llamada. Estos estilos ya deben usarse en el widget si se copia el fragmento de código que hemos agregado al archivo CallingWidgetComponent.tsx.

Ahora, cree una carpeta denominada src/styles y, en ella, cree un archivo denominado CallingWidgetComponent.styles.ts. El archivo debería parecerse al siguiente fragmento de código:

import {
  IButtonStyles,
  ICheckboxStyles,
  IIconStyles,
  IStackStyles,
  Theme,
} from "@fluentui/react";

export const checkboxStyles = (theme: Theme): ICheckboxStyles => {
  return {
    label: {
      color: theme.palette.neutralPrimary,
    },
  };
};

export const callingWidgetContainerStyles = (theme: Theme): IStackStyles => {
  return {
    root: {
      width: "5rem",
      height: "5rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: "50%",
      bottom: "1rem",
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      ":hover": {
        boxShadow: theme.effects.elevation64,
      },
    },
  };
};

export const callingWidgetSetupContainerStyles = (
  theme: Theme
): IStackStyles => {
  return {
    root: {
      width: "18rem",
      minHeight: "20rem",
      maxHeight: "25rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: theme.effects.roundedCorner6,
      bottom: 0,
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      background: theme.palette.white,
    },
  };
};

export const callIconStyles = (theme: Theme): IIconStyles => {
  return {
    root: {
      paddingTop: "0.2rem",
      color: theme.palette.white,
      transform: "scale(1.6)",
    },
  };
};

export const startCallButtonStyles = (theme: Theme): IButtonStyles => {
  return {
    root: {
      background: theme.palette.themePrimary,
      borderRadius: theme.effects.roundedCorner6,
      borderColor: theme.palette.themePrimary,
    },
    textContainer: {
      color: theme.palette.white,
    },
  };
};

export const logoContainerStyles: IStackStyles = {
  root: {
    margin: "auto",
    padding: "0.2rem",
    height: "5rem",
    width: "10rem",
    zIndex: 0,
  },
};

export const collapseButtonStyles: IButtonStyles = {
  root: {
    position: "absolute",
    top: "0.2rem",
    right: "0.2rem",
    zIndex: 1,
  },
};

export const callingWidgetInCallContainerStyles = (
  theme: Theme
): IStackStyles => {
  return {
    root: {
      width: "35rem",
      height: "25rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: theme.effects.roundedCorner6,
      bottom: 0,
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      background: theme.semanticColors.bodyBackground,
    },
  };
};

6. Configuración de valores de identidad

Antes de ejecutar la aplicación, vaya a App.tsx y reemplace los valores de marcador de posición allí por sus Identidades de Azure Communication Services y el Identificador de cuenta de recursos para la aplicación de Voz de Teams. Estos son los valores de entrada para token, userId y teamsAppIdentifier.

./src/App.tsx

/**
 * Token for local user.
 */
const token = "<Enter your ACS Token here>";

/**
 * User identifier for local user.
 */
const userId: CommunicationIdentifier = {
  communicationUserId: "Enter your ACS Id here",
};

/**
 * Enter your Teams voice app identifier from the Teams admin center here
 */
const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
  teamsAppId: "<Enter your Teams Voice app id here>",
  cloud: "public",
};

7. Ejecución de la aplicación

Por último, se puede ejecutar la aplicación para realizar las llamadas. Ejecute los comandos siguientes para instalar las dependencias y ejecutar la aplicación.

# Install the newe dependencies
npm install

# run the React app
npm run start

Una vez ejecutada la aplicación, podrá verla en http://localhost:3000 en su navegador. Debería ver la siguiente pantalla de presentación:

Captura de pantalla de ejemplo del widget de llamada de la aplicación de la página de inicio del widget cerrado

Después, al accionar el botón del widget, debería ver un pequeño menú:

Captura de pantalla de ejemplo del widget de llamada de la aplicación de la página de inicio del widget abierto

Después de rellenar el nombre, haga clic en Iniciar llamada y la llamada debería comenzar. El widget debe tener un aspecto similar al siguiente después de iniciar una llamada:

Captura de pantalla de la página de inicio de la aplicación de ejemplo de tipo

Pasos siguientes

Para obtener más información sobre las aplicaciones de voz de Teams, consulte nuestra documentación sobre operadores automáticos de Teams y colas de llamadas de Teams. También vea nuestro tutorial sobre cómo crear una experiencia similar con paquetes de JavaScript.

Inicio rápido: Incorporación de una aplicación de llamadas a una cola de llamadas de Teams

Inicio rápido: Unión de la aplicación de llamadas a un operador automático de Teams

Inicio rápido: Introducción con los paquetes JavaScript de la biblioteca de interfaz de usuario de Azure Communication Services que llaman a la cola de llamadas de Teams y al operador automático