Partilhar via


Introdução às chamadas da biblioteca de IU dos Serviços de Comunicação do Azure para as Aplicações de Voz do Teams

Este projeto tem como objetivo orientar os desenvolvedores a iniciar uma chamada do SDK da Web de Chamada dos Serviços de Comunicação do Azure para a Fila de Chamadas do Teams e o Atendedor Automático usando a Biblioteca da Interface do Usuário de Comunicação do Azure.

De acordo com suas necessidades, você pode precisar oferecer aos seus clientes uma maneira fácil de entrar em contato com você sem qualquer configuração complexa.

Ligar para o Teams Call Queue e Auto Attendant é um conceito simples, mas eficaz, que facilita a interação instantânea com o suporte ao cliente, consultor financeiro e outras equipes voltadas para o cliente. O objetivo deste tutorial é ajudá-lo a iniciar interações com seus clientes quando eles clicam em um botão na web.

Se você deseja experimentá-lo, você pode baixar o código do GitHub.

Seguir este tutorial irá:

  • Permitir que você controle a experiência de áudio e vídeo de seus clientes, dependendo do cenário do cliente
  • Ensine como criar um widget para iniciar chamadas em seu aplicativo Web usando a biblioteca da interface do usuário.

Página inicial do aplicativo de exemplo Chamando widget

Pré-requisitos

Estas etapas são necessárias para seguir este tutorial. Entre em contato com o administrador do Teams para obter os dois últimos itens para se certificar de que você está configurado adequadamente.

Verificando o nó e o código do Visual Studio

Você pode verificar se o Node foi instalado corretamente com este comando.

node -v

A saída informa a versão que você tem, ela falha se o Node não foi instalado e adicionado ao seu PATH. Assim como com o Node você pode verificar se o VS Code foi instalado com este comando.

code --version

Tal como acontece com o Node, este comando falha se houver um problema ao instalar o VS Code na sua máquina.

Introdução

Este tutorial tem 7 passos e no final o aplicativo será capaz de chamar um aplicativo de voz do Teams. Os passos são:

  1. Configurar o projeto
  2. Obtenha as suas dependências
  3. Configuração inicial do aplicativo
  4. Criar o widget
  5. Estilizar o widget
  6. Configurar valores de identidade
  7. Executar o aplicativo

1. Configurar o projeto

Use esta etapa somente se estiver criando um novo aplicativo.

Para configurar o aplicativo react, usamos a ferramenta de linha de create-react-app comando. Esta ferramenta cria um aplicativo TypeScript fácil de executar alimentado por React.

Para certificar-se de que você tem o Node instalado em sua máquina, execute este comando no PowerShell ou no terminal para ver sua versão do Node:

node -v

Se você não tiver create-react-app instalado em sua máquina, execute o seguinte comando para instalá-lo como um comando global:

npm install -g create-react-app

Depois que o comando for instalado, execute este próximo comando para criar um novo aplicativo react para compilar o exemplo em:

# 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

Após a conclusão desses comandos, você deseja abrir o projeto criado no VS Code. Você pode abrir o projeto com o seguinte comando.

code .

2. Obtenha as suas dependências

Em seguida, você precisa atualizar a matriz de dependência no package.json para incluir alguns pacotes dos Serviços de Comunicação do Azure para a experiência de widget que vamos criar para funcionar:

"@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 os pacotes necessários, execute o seguinte comando do Gerenciador de Pacotes de Nó.

npm install

Depois de instalar esses pacotes, você está pronto para começar a escrever o código que cria o aplicativo. Neste tutorial, estamos modificando os arquivos no src diretório.

3. Configuração inicial do aplicativo

Para começar, substituímos o conteúdo fornecido App.tsx por uma página principal que:

  • Armazene todas as informações de Comunicação do Azure de que precisamos para criar um CallAdapter para potencializar nossa experiência de Chamada
  • Exiba nosso widget que está exposto ao usuário final.

O seu App.tsx ficheiro deve ter o seguinte aspeto:

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;

Neste trecho, registramos dois novos ícones <Dismiss20Regular/> e <CallAdd20Regular>. Esses novos ícones são usados dentro do componente de widget que estamos criando na próxima seção.

4. Crie o widget

Agora precisamos fazer um widget que pode mostrar em três modos diferentes:

  • Esperando: Este estado do widget é como o componente estará antes e depois de uma chamada ser feita
  • Configuração: Este estado é quando o widget pede informações do usuário, como seu nome.
  • Em uma chamada: o widget é substituído aqui pela biblioteca da interface do usuário Call Composite. Este modo de widget é quando o usuário está chamando o aplicativo de voz ou falando com um agente.

Vamos criar uma pasta chamada src/components. Nesta pasta, crie um novo arquivo chamado CallingWidgetComponent.tsx. Esse arquivo deve se parecer com o seguinte trecho:

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>
  );
};

CallAdapterOptionsNo , vemos alguns arquivos de som referenciados, esses arquivos devem usar o recurso Chamando sons no CallComposite. Se você estiver interessado em usar os sons, consulte o código concluído para baixar os arquivos de som.

5. Estilize o widget

Precisamos escrever alguns estilos para garantir que o widget pareça apropriado e possa conter nosso composto de chamadas. Esses estilos já devem ser usados no widget se copiar o trecho que adicionamos ao arquivo CallingWidgetComponent.tsx.

Vamos fazer uma nova pasta chamada src/styles nesta pasta, criar um arquivo chamado CallingWidgetComponent.styles.ts. O arquivo deve se parecer com o seguinte trecho:

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. Configurar valores de identidade

Antes de executarmos o aplicativo, vá e App.tsx substitua os valores de espaço reservado por suas Identidades dos Serviços de Comunicação do Azure e o identificador de conta de recurso para seu aplicativo Teams Voice. Aqui estão os valores de entrada para o token, userId e 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. Execute o aplicativo

Finalmente podemos executar o aplicativo para fazer nossas chamadas! Execute os seguintes comandos para instalar nossas dependências e executar nosso aplicativo.

# Install the new dependencies
npm install

# run the React app
npm run start

Quando o aplicativo estiver em execução, você poderá vê-lo em http://localhost:3000 seu navegador. Você verá a seguinte tela inicial:

Captura de tela do widget de chamada de exemplo de widget widget página inicial widget fechado.

Em seguida, quando você acionar o botão do widget, você verá um pequeno menu:

Captura de tela do widget de chamada de exemplo de widget widget página inicial widget aberto.

Depois de preencher o seu nome, clique em iniciar chamada e a chamada deve começar. O widget deve ter essa aparência depois de iniciar uma chamada:

Captura de tela da página inicial do aplicativo de clique para chamar de exemplo com a experiência de chamada incorporada no widget.

Próximos passos

Para obter mais informações sobre os aplicativos de voz do Teams, confira nossa documentação sobre atendedores automáticos do Teams e filas de chamadas do Teams. Ou também veja nosso tutorial sobre como criar uma experiência semelhante com pacotes JavaScript.

Guia de início rápido: associe seu aplicativo de chamadas a uma fila de chamadas do Teams

Guia de início rápido: associe seu aplicativo de chamadas a um Atendedor Automático do Teams

Guia de início rápido: introdução aos pacotes JavaScript da biblioteca de interface do usuário dos Serviços de Comunicação do Azure chamando para a Fila de Chamadas do Teams e o Atendedor Automático