Compartilhar via


Comece a usar a biblioteca de interface do usuário dos Serviços de Comunicação do Azure fazendo uma chamada para os Aplicativos de Voz do Teams

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

De acordo com seus requisitos, você talvez precise oferecer aos seus clientes uma maneira fácil de entrar em contato com você sem nenhuma configuração complexa.

Fazer uma chamada para a Fila de Chamadas e Atendente Automático do Teams é um conceito simples, mas eficaz, que facilita a interação instantânea com o atendimento ao cliente, um consultor financeiro e outras equipes voltadas para o cliente. O objetivo desse tutorial é ajudar você a iniciar interações com seus clientes quando estes clicarem em um botão na web.

Se você quiser experimentar, baixe 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
  • Ensina a criar um widget para iniciar chamadas em seu aplicativo Web usando a biblioteca de interface do usuário.

Página inicial do aplicativo de amostra do Widget de Chamada

Pré-requisitos

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

Verificação do código do Node e 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 e falha se o Node não tiver sido instalado e adicionado ao seu PATH. Assim como com o Node, você pode verificar se o VS Code foi instalado com esse comando.

code --version

Como no caso do Node, esse comando falhará se houver algum problema na instalação do VS Code em seu computador.

Introdução

Este tutorial tem 7 etapas e, no final, o aplicativo poderá chamar um aplicativo de voz do Teams. As etapas são:

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

1. Configurar o projeto

Use esta etapa somente se você estiver criando um novo aplicativo.

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

Para garantir que você tenha o Nó instalado em seu computador, execute este comando no PowerShell ou no terminal para ver sua versão do Nó:

node -v

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

npm install -g create-react-app

Depois que esse comando for instalado, execute este próximo comando para criar um novo aplicativo react para criar 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ê deverá abrir o projeto criado no VS Code. Você pode abrir o projeto com o seguinte comando.

code .

2. Obter suas dependências

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

"@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 do Node.

npm install

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

3. Configuração inicial do aplicativo

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

  • Armazenar todas as informações de Comunicação do Azure necessárias para criar um CallAdapter para alimentar nossa experiência de chamada
  • Exiba o nosso widget que é exposto ao usuário final.

O arquivo App.tsx deve ter esta aparência:

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 snippet, registramos dois novos ícones <Dismiss20Regular/> e <CallAdd20Regular>. Estes novos ícones são usados dentro do componente do widget que estamos criando na próxima seção.

4. Criar o widget

Agora, precisamos criar um widget que possa ser exibido em três modos diferentes:

  • Aguardando: esse estado do widget é a forma como o componente irá aparecer antes e depois de uma chamada ser efetuada
  • Configuração: esse estado aparece quando o widget solicita informações do usuário, como o nome.
  • Em uma chamada: aqui o widget é substituído pelos Elementos de Chamadas da biblioteca da interface do usuário. Este modo de widget é quando o usuário está chamando o aplicativo do Serviço de Voz ou conversando com um agente.

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

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

No CallAdapterOptions, vemos alguns arquivos de som referenciados, esses arquivos são para usar o recurso Sons de Chamada no CallComposite. Se você estiver interessado em usar os sons, veja o código completo para baixar os arquivos de som.

5. Criar o estilo do widget

Precisamos escrever alguns estilos para garantir que o widget tenha a aparência apropriada e possa reter nossos elementos de chamadas. Esses estilos já devem ser usados no widget se copiarmos o snippet que adicionamos ao arquivo CallingWidgetComponent.tsx.

Vamos criar uma nova pasta chamada src/styles nesta pasta, crie um arquivo chamado CallingWidgetComponent.styles.ts. O arquivo deverá se parecer com o seguinte snippet:

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á para App.tsx e substitua os valores de espaço reservado com suas Identidades dos Serviços de Comunicação do Azure e o identificador da conta do recurso para seu aplicativo Teams Voice. Aqui estão os valores de entrada para 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. Executar o aplicativo

Para terminar, podemos executar o aplicativo para fazer nossas chamadas! Execute os comandos a seguir para instalar nossas dependências e executar nosso aplicativo.

# Install the newe dependencies
npm install

# run the React app
npm run start

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

Captura de tela do widget da página inicial do aplicativo de amostra do widget de chamada fechado.

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

Captura de tela do widget da página inicial do aplicativo de amostra do widget de chamada aberto.

Depois de preencher seu nome, clique em iniciar a chamada e a chamada deverá começar. O widget deve ter a seguinte aparência após iniciar uma chamada:

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

Próximas etapas

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

Início Rápido: ingressar seu aplicativo de chamadas a uma fila de chamadas do Teams

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

Início Rápido: Introdução aos pacotes JavaScript da biblioteca da interface do usuário dos Serviços de Comunicação do Azure para fazer chamadas para a Fila de Chamadas e Atendedor Automático do Teams