بدء استخدام مكتبة واجهة مستخدم Azure Communication Services التي تتصل بتطبيقات Teams الصوتية

يهدف هذا المشروع إلى توجيه المطورين لبدء مكالمة من Azure Communication Services Calling Web SDK إلى Teams Call Queue والردود التلقائية باستخدام مكتبة واجهة مستخدم اتصالات Azure.

وفقا لمتطلباتك، قد تحتاج إلى تقديم طريقة سهلة لعملائك للتواصل معك دون أي إعداد معقد.

يعد الاتصال بقائمة انتظار المكالمات والردود التلقائية في Teams مفهوما بسيطا وفعالا يسهل التفاعل الفوري مع دعم العملاء والمستشار المالي والفرق الأخرى التي تواجه العملاء. الهدف من هذا البرنامج التعليمي هو مساعدتك في بدء التفاعلات مع عملائك عند النقر فوق زر على الويب.

إذا كنت ترغب في تجربتها، يمكنك تنزيل التعليمات البرمجية من GitHub.

بعد هذا البرنامج التعليمي سوف:

  • السماح لك بالتحكم في تجربة الصوت والفيديو للعملاء وفقا لسيناريو العميل
  • تعلمك كيفية إنشاء عنصر واجهة مستخدم لبدء المكالمات على تطبيق الويب الخاص بك باستخدام مكتبة واجهة المستخدم.

الصفحة الرئيسية لتطبيق عينة عنصر واجهة المستخدم للاتصال

المتطلبات الأساسية

هذه الخطوات مطلوبة من أجل اتباع هذا البرنامج التعليمي. اتصل بمسؤول Teams للحصول على العنصرين الأخيرين للتأكد من إعدادك بشكل مناسب.

التحقق من العقدة وVisual Studio Code

يمكنك التحقق من تثبيت Node بشكل صحيح باستخدام هذا الأمر.

node -v

يخبرك الإخراج بالإصدار الذي لديك، فإنه يفشل إذا لم يتم تثبيت العقدة وإضافتها إلى .PATH تماما كما هو الحال مع Node، يمكنك التحقق لمعرفة ما إذا كان قد تم تثبيت VS Code مع هذا الأمر.

code --version

مثل العقدة يفشل هذا الأمر إذا كانت هناك مشكلة في تثبيت VS Code على جهازك.

الشروع في العمل

يحتوي هذا البرنامج التعليمي على 7 خطوات وفي النهاية سيتمكن التطبيق من الاتصال بتطبيق صوت Teams. الخطوات هي:

  1. إعداد المشروع
  2. الحصول على تبعياتك
  3. الإعداد الأولي للتطبيق
  4. إنشاء عنصر واجهة المستخدم
  5. نمط عنصر واجهة المستخدم
  6. إعداد قيم الهوية
  7. تشغيل التطبيق

1. إعداد المشروع

استخدم هذه الخطوة فقط إذا كنت تقوم بإنشاء تطبيق جديد.

لإنشاء تطبيق react، نستخدم vite. وهي أداة بناء تهدف إلى توفير تجربة تطوير أسرع وأكثر رنين لمشاريع الويب الحديثة. يمكنك قراءة المزيد حول vite هنا إنشاء تطبيق React من البدايةوالبدء باستخدام Vite

لاحظ أن مكتبة واجهة مستخدم Azure Communication Service تدعم فقط ما يصل إلى React 18. تحقق من أنك تستخدم الإصدار المدعوم بعد إنشاء تطبيق react عن طريق التحقق package.json

للتأكد من تثبيت Node على جهازك، قم بتشغيل هذا الأمر في PowerShell أو المحطة الطرفية لمشاهدة إصدار Node الخاص بك:

node -v

2. الحصول على تبعياتك

بعد ذلك، تحتاج إلى تحديث صفيف التبعية في لتضمين package.json بعض الحزم من Azure Communication Services لتجربة عنصر واجهة المستخدم التي سننشئها للعمل:

"@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",

لتثبيت الحزم المطلوبة، قم بتشغيل الأمر مدير الحِزَم Node التالي.

npm install

بعد تثبيت هذه الحزم، يتم تعيينك جميعا لبدء كتابة التعليمات البرمجية التي تنشئ التطبيق. في هذا البرنامج التعليمي، نقوم بتعديل الملفات في src الدليل.

3. الإعداد الأولي للتطبيق

للبدء، نقوم باستبدال المحتوى المقدم App.tsx بصفحة رئيسية من شأنها:

  • قم بتخزين جميع معلومات اتصالات Azure التي نحتاجها لإنشاء CallAdapter لتشغيل تجربة الاتصال الخاصة بنا
  • عرض عنصر واجهة المستخدم الذي يتم عرضه للمستخدم النهائي.

App.tsx يجب أن يبدو الملف كما يلي:

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;

في هذه القصاصة البرمجية، نسجل رمزين جديدين <Dismiss20Regular/> و <CallAdd20Regular>. يتم استخدام هذه الأيقونات الجديدة داخل مكون عنصر واجهة المستخدم الذي نقوم بإنشاءه في القسم التالي.

4. إنشاء عنصر واجهة المستخدم

الآن نحن بحاجة إلى إنشاء عنصر واجهة مستخدم يمكن أن يظهر في ثلاثة أوضاع مختلفة:

  • الانتظار: حالة عنصر واجهة المستخدم هذه هي الطريقة التي سيكون بها المكون قبل إجراء استدعاء وبعده
  • الإعداد: هذه الحالة هي عندما يطلب عنصر واجهة المستخدم معلومات من المستخدم مثل اسمه.
  • في مكالمة: يتم استبدال عنصر واجهة المستخدم هنا بمكتبة واجهة المستخدم Call Composite. يكون وضع عنصر واجهة المستخدم هذا عندما يتصل المستخدم بتطبيق الصوت أو يتحدث مع وكيل.

يتيح إنشاء مجلد يسمى src/components. في هذا المجلد، قم بإنشاء ملف جديد يسمى CallingWidgetComponent.tsx. يجب أن يبدو هذا الملف مثل القصاصة البرمجية التالية:

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

CallAdapterOptionsفي ، نرى بعض ملفات الصوت المشار إليها، هذه الملفات هي لاستخدام ميزة Calling Sounds في CallComposite. إذا كنت مهتما باستخدام الأصوات، فشاهد التعليمات البرمجية المكتملة لتنزيل ملفات الصوت.

5. نمط عنصر واجهة المستخدم

نحن بحاجة إلى كتابة بعض الأنماط للتأكد من أن عنصر واجهة المستخدم يبدو مناسبا ويمكنه الاحتفاظ بمركب المكالمة. يجب استخدام هذه الأنماط بالفعل في عنصر واجهة المستخدم في حالة نسخ القصاصة البرمجية التي أضفناها إلى الملف CallingWidgetComponent.tsx.

يتيح إنشاء مجلد جديد يسمى src/styles في هذا المجلد، قم بإنشاء ملف يسمى CallingWidgetComponent.styles.ts. يجب أن يبدو الملف مثل القصاصة البرمجية التالية:

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. إعداد قيم الهوية

قبل تشغيل التطبيق، انتقل إلى App.tsx قيم العنصر النائب هناك واستبدلها بهوياتAzure Communication Services ومعرف حساب المورد لتطبيق Teams Voice الخاص بك. فيما يلي قيم الإدخال ل tokenوuserId.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. تشغيل التطبيق

وأخيرا يمكننا تشغيل التطبيق لإجراء مكالماتنا! قم بتشغيل الأوامر التالية لتثبيت تبعياتنا وتشغيل تطبيقنا.

# Install the new dependencies
npm install

# run the React app
npm run dev

بمجرد تشغيل التطبيق، يمكنك رؤيته في http://localhost:3000 المستعرض. يجب أن تشاهد شاشة البداية التالية:

لقطة شاشة لاستدعاء عنصر واجهة مستخدم عينة الصفحة الرئيسية للتطبيق مغلقة.

ثم عند إجراء زر عنصر واجهة المستخدم، يجب أن تشاهد قائمة صغيرة:

لقطة شاشة لفتح عنصر واجهة مستخدم عينة تطبيق الصفحة الرئيسية.

بعد ملء اسمك، انقر فوق بدء المكالمة ومن المفترض أن تبدأ المكالمة. يجب أن يبدو عنصر واجهة المستخدم كما يلي بعد بدء مكالمة:

لقطة شاشة للنقر لاستدعاء نموذج الصفحة الرئيسية للتطبيق مع تجربة الاتصال المضمنة في عنصر واجهة المستخدم.

الخطوات التالية

لمزيد من المعلومات حول تطبيقات Teams الصوتية، راجع وثائقنا حول الردود التلقائية ل Teams وقوائم انتظار المكالمات في Teams. أو راجع أيضا برنامجنا التعليمي حول كيفية بناء تجربة مماثلة مع حزم JavaScript.

التشغيل السريع: انضم إلى تطبيق المكالمات إلى قائمة انتظار مكالمات Teams

التشغيل السريع: انضم إلى تطبيق المكالمات إلى "الرد التلقائي ل Teams"

التشغيل السريع: ابدأ باستخدام حزم JavaScript لمكتبة واجهة مستخدم Azure Communication Services التي تتصل بقائمة انتظار مكالمات Teams والردود التلقائية