发布于 2026-01-05 9 阅读
0

How to Build: a Text-to-PowerPoint Application (LangChain, OpenAI, CopilotKit & Next.js) TL;DR

如何构建:文本转 PowerPoint 应用程序(LangChain、OpenAI、CopilotKit 和 Next.js)

太长不看

太长不看

在本文中,您将学习如何构建一个人工智能驱动的 PowerPoint 应用程序,该应用程序可以搜索网络并自动制作任何主题的演示文稿。

我们将介绍如何使用:

  • Next.js 用于应用程序框架🖥️
  • OpenAI for the LLM 🧠
  • LangChain 和 Tavily 共同打造网络搜索 AI 代理 🤖
  • 使用 CopilotKit 将 AI 集成到您的应用中 🪁


CopilotKit:为您的应用构建 AI 副驾驶

CopilotKit 是一个开源的 AI 辅助驾驶平台。我们让您轻松地将强大的 AI 集成到您的 React 应用中。

建造:

  • 聊天机器人:能够感知上下文并执行应用内操作的应用内聊天机器人💬

  • CopilotTextArea:AI驱动的文本框,具备上下文感知自动完成和插入功能📝

  • 协同代理:应用内人工智能代理,可以与您的应用和用户进行互动🤖

星际副驾驶套件⭐️

现在回到文章正文。

(本文是我们在三周前发表的一篇文章的后续,但您无需阅读前一篇文章即可理解本文。)


先决条件

在开始构建应用程序之前,我们先来看看构建应用程序需要哪些依赖项或软件包。

copilotkit/react-core:CopilotKit 前端包,包含 React Hooks,用于向 Copilot(AI 功能)提供应用状态和操作;
copilotkit/react-ui:CopilotKit 前端包,用于聊天机器人侧边栏 UI
copilotkit/react-textarea;:CopilotKit 前端包,用于在演示文稿的演讲者备注中实现 AI 辅助文本编辑;
LangChainJS:一个用于开发基于语言模型的应用程序的框架;
Tavily Search API:一个 API,用于帮助将语言模型和 AI 应用程序连接到可信的实时知识库。

安装所有项目包和依赖项

在安装所有项目包和依赖项之前,让我们先在终端运行以下命令来创建一个 Nextjs 项目。

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

接下来,系统会提示您选择一些选项。您可以根据下图所示进行标记。

创建 Nextjs 项目

之后,使用您选择的文本编辑器打开新创建的 Nextjs 项目。然后在命令行运行以下命令来安装所有项目包和依赖项。

npm i @copilotkit/backend @copilotkit/shared @langchain/langgraph @copilotkit/react-core 
@copilotkit/react-ui @copilotkit/react-textarea @heroicons/react
Enter fullscreen mode Exit fullscreen mode

创建 PowerPoint 应用程序前端

让我们首先创建一个名为 . 的文件Slide.tsx。该文件将包含用于显示和编辑幻灯片内容的代码,包括其titlebody textbackground imagespoken narration text

要创建该文件,请转到 /[root]/src/app并创建一个名为 的文件夹components。在 components 文件夹内,创建该Slide.tsx文件。

之后,在文件顶部添加以下代码。该代码定义了两个名为 `A`SlideModel和 `B` 的TypeScript 接口SlideProps

"use client";

// Define an interface for the model of a slide, specifying the expected structure of a slide object.
export interface SlideModel {
  title: string;
  content: string;
  backgroundImageDescription: string;
  spokenNarration: string;
}

// Define an interface for the properties of a component or function that manages slides.
export interface SlideProps {
  slide: SlideModel;
  partialUpdateSlide: (partialSlide: Partial<SlideModel>) => void;
}
Enter fullscreen mode Exit fullscreen mode

接下来,在上面的代码下方添加以下代码。该代码定义了一个名为 `<function_component>` 的函数式组件Slide,该组件接受类型为 `<props>` 的 props SlideProps

// Define a functional component named Slide that accepts props of type SlideProps.
export const Slide = (props: SlideProps) => {
    // Define a constant for the height of the area reserved for speaker notes.
    const heightOfSpeakerNotes = 150;

    // Construct a URL for the background image using the description from slide properties, dynamically fetching an image from Unsplash.
    const backgroundImage =
      'url("https://source.unsplash.com/featured/?' +
      encodeURIComponent(props.slide.backgroundImageDescription) +
      '")';

    // Return JSX for the slide component.
    return (
      <>
        {/* Slide content container with dynamic height calculation to account for speaker notes area. */}
        <div 
          className="w-full relative bg-slate-200"
          style={{
            height: `calc(100vh - ${heightOfSpeakerNotes}px)`, // Calculate height to leave space for speaker notes.
          }}
        >
          {/* Container for the slide title with centered alignment and styling. */}
          <div 
            className="h-1/5 flex items-center justify-center text-5xl text-white text-center z-10"
          >
            {/* Textarea for slide title input, allowing dynamic updates. */}
            <textarea
              className="text-2xl bg-transparent text-black p-4 text-center font-bold uppercase italic line-clamp-2 resize-none flex items-center"
              style={{
                border: "none",
                outline: "none",
              }}
              value={props.slide.title}
              placeholder="Title"
              onChange={(e) => {
                props.partialUpdateSlide({ title: e.target.value });
              }}
            />
          </div>

          {/* Container for the slide content with background image. */}
          <div className="h-4/5 flex"
            style={{
              backgroundImage,
              backgroundSize: "cover",
              backgroundPosition: "center",
            }}
          >
            {/* Textarea for slide content input, allowing dynamic updates and styled for readability. */}
            <textarea
              className="w-full text-3xl text-black font-medium p-10 resize-none bg-red mx-40 my-8 rounded-xl text-center"
              style={{
                lineHeight: "1.5",
              }}
              value={props.slide.content}
              placeholder="Body"
              onChange={(e) => {
                props.partialUpdateSlide({ content: e.target.value });
              }}
            />
          </div>
        </div>

        {/* Textarea for entering spoken narration with specified height and styling for consistency. */}
        <textarea
          className=" w-9/12 h-full bg-transparent text-5xl p-10 resize-none bg-gray-500 pr-36"
          style={{
            height: `${heightOfSpeakerNotes}px`,
            background: "none",
            border: "none",
            outline: "none",
            fontFamily: "inherit",
            fontSize: "inherit",
            lineHeight: "inherit",
          }}
          value={props.slide.spokenNarration}
          onChange={(e) => {
            props.partialUpdateSlide({ spokenNarration: e.target.value });
          }}
        />
      </>
    );
  };
Enter fullscreen mode Exit fullscreen mode

之后,我们现在创建一个名为“.”的文件Presentation.tsx

该文件将包含初始化和更新幻灯片状态、渲染当前幻灯片以及根据当前状态动态启用或禁用按钮来实现导航和幻灯片管理操作的代码。

要创建该文件,请在 components 文件夹中添加另一个文件,并将其命名为Presentation.tsxThen import React hooks, icons, SlideModel, and Slidecomponents 在文件顶部使用以下代码。

"use client";

import { useCallback, useMemo, useState } from "react";
import {
  BackwardIcon,
  ForwardIcon,
  PlusIcon,
  SparklesIcon,
  TrashIcon
} from "@heroicons/react/24/outline";
import { SlideModel, Slide } from "./Slide";
Enter fullscreen mode Exit fullscreen mode

之后,在上面的代码下方添加以下代码。该代码定义了一个ActionButton功能组件,该组件将渲染一个带有可自定义属性的按钮元素。

export const ActionButton = ({
  disabled, onClick, className, children,
}: {
  disabled: boolean;
  onClick: () => void;
  className?: string;
  children: React.ReactNode;
}) => {
  return (
    <button
      disabled={disabled}
      className={`bg-blue-500 text-white font-bold py-2 px-4 rounded
      ${disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"}
      ${className}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

然后在上面的代码下方添加以下代码。该代码定义了一个名为 Presentation 的功能组件,该组件初始化幻灯片的状态并定义了一个用于更新当前幻灯片的函数。

// Define the Presentation component as a functional component.
export const Presentation = () => {
    // Initialize state for slides with a default first slide and a state to track the current slide index.
    const [slides, setSlides] = useState<SlideModel[]>([
      {
        title: `Welcome to our presentation!`, // Title of the first slide.
        content: 'This is the first slide.', // Content of the first slide.
        backgroundImageDescription: "hello", // Description for background image retrieval.
        spokenNarration: "This is the first slide. Welcome to our presentation!", // Spoken narration text for the first slide.
      },
    ]);
    const [currentSlideIndex, setCurrentSlideIndex] = useState(0); // Current slide index, starting at 0.

    // Use useMemo to memoize the current slide object to avoid unnecessary recalculations.
    const currentSlide = useMemo(() => slides[currentSlideIndex], [slides, currentSlideIndex]);

    // Define a function to update the current slide. This function uses useCallback to memoize itself to prevent unnecessary re-creations.
    const updateCurrentSlide = useCallback(
      (partialSlide: Partial<SlideModel>) => {
        // Update the slides state by creating a new array with the updated current slide.
        setSlides((slides) => [
          ...slides.slice(0, currentSlideIndex), // Copy all slides before the current one.
          { ...slides[currentSlideIndex], ...partialSlide }, // Merge the current slide with the updates.
          ...slides.slice(currentSlideIndex + 1), // Copy all slides after the current one.
        ]);
      },
      [currentSlideIndex, setSlides] // Dependencies for useCallback.
    );

    // The JSX structure for the Presentation component.
    return (
      <div className="relative">
        {/* Render the current slide by passing the currentSlide and updateCurrentSlide function as props. */}
        <Slide slide={currentSlide} partialUpdateSlide={updateCurrentSlide} />

        {/* Container for action buttons located at the top-left corner of the screen. */}
        <div className="absolute top-0 left-0 mt-6 ml-4 z-30">
          {/* Action button to add a new slide. Disabled state is hardcoded to true for demonstration. */}
          <ActionButton
            disabled={true} 
            onClick={() => {
              // Define a new slide object.
              const newSlide: SlideModel = {
                title: "Title",
                content: "Body",
                backgroundImageDescription: "random",
                spokenNarration: "The speaker's notes for this slide.",
              };
              // Update the slides array to include the new slide.
              setSlides((slides) => [
                ...slides.slice(0, currentSlideIndex + 1),
                newSlide,
                ...slides.slice(currentSlideIndex + 1),
              ]);
              // Move to the new slide by updating the currentSlideIndex.
              setCurrentSlideIndex((i) => i + 1);
            }}
            className="rounded-r-none"
          >
            <PlusIcon className="h-6 w-6" /> {/* Icon for the button. */}
          </ActionButton>

          {/* Another action button, currently disabled and without functionality. */}
          <ActionButton
            disabled={true} 
            onClick={async () => { }} // Placeholder async function.
            className="rounded-l-none ml-[1px]"
          >
            <SparklesIcon className="h-6 w-6" /> {/* Icon for the button. */}
          </ActionButton>
        </div>

        {/* Container for action buttons at the top-right corner for deleting slides, etc. */}
        <div className="absolute top-0 right-0 mt-6 mr-24">
          <ActionButton
            disabled={slides.length === 1} // Disable button if there's only one slide.
            onClick={() => {}} // Placeholder function for the button action.
            className="ml-5 rounded-r-none"
          >
            <TrashIcon className="h-6 w-6" /> {/* Icon for the button. */}
          </ActionButton>
        </div>

        {/* Display current slide number and total slides at the bottom-right corner. */}
        <div
          className="absolute bottom-0 right-0 mb-20 mx-24 text-xl"
          style={{
            textShadow: "1px 1px 0 #ddd, -1px -1px 0 #ddd, 1px -1px 0 #ddd, -1px 1px 0 #ddd",
          }}
        >
          Slide {currentSlideIndex + 1} of {slides.length} {/* Current slide and total slides. */}
        </div>

        {/* Container for navigation buttons (previous and next) at the bottom-right corner. */}
        <div className="absolute bottom-0 right-0 mb-6 mx-24">
          {/* Button to navigate to the previous slide. */}
          <ActionButton
            className="rounded-r-none"
            disabled={
              currentSlideIndex === 0 ||
              true} // Example condition to disable button; 'true' is just for demonstration.
            onClick={() => {
              setCurrentSlideIndex((i) => i - 1); // Update currentSlideIndex to move to the previous slide.
            }}
          >
            <BackwardIcon className="h-6 w-6" /> {/* Icon for the button. */}
          </ActionButton>
          {/* Button to navigate to the next slide. */}
          <ActionButton
            className="mr-[1px] rounded-l-none"
            disabled={
              true ||
              currentSlideIndex + 1 === slides.length} // Example condition to disable button; 'true' is just for demonstration.
            onClick={async () => {
              setCurrentSlideIndex((i) => i + 1); // Update currentSlideIndex to move to the next slide.
            }}
          >
            <ForwardIcon className="h-6 w-6" /> {/* Icon for the button. */}
          </ActionButton>
        </div>
      </div>
    );
  };
Enter fullscreen mode Exit fullscreen mode

要在浏览器中呈现 PowerPoint 应用,请转到该/[root]/src/app/page.tsx文件并添加以下代码。

"use client";

import "./style.css";
import { Presentation } from "./components/Presentation";

export default function AIPresentation() {
  return (
    <Presentation  />
  );
}
Enter fullscreen mode Exit fullscreen mode

如果您想为 Powerpoint 应用程序前端添加样式,请style.css/[root]/src/app文件夹中创建一个名为 的文件。

然后找到这个 gist 文件,复制 CSS 代码,并将其添加到 style.css 文件中。

npm run dev 最后,在命令行 运行该命令 ,然后访问http://localhost:3000/

现在您应该可以在浏览器中看到 PowerPoint 应用程序,如下所示。

PowerPoint 应用程序

将 PowerPoint 应用与 CopilotKit 后端集成

首先,.env.local在根目录下创建一个名为 `.tavily.conf` 的文件。然后,将以下环境变量添加到该文件中,用于存放您的 ChatGPT 和 Tavily Search API 密钥。

OPENAI_API_KEY="Your ChatGPT API key"
TAVILY_API_KEY="Your Tavily Search API key"
Enter fullscreen mode Exit fullscreen mode

要获取 ChatGPT API 密钥,请访问https://platform.openai.com/api-keys

ChatGPT API 密钥

要获取 Tavily 搜索 API 密钥,请访问https://app.tavily.com/home

Tavily 搜索 API 密钥

之后,前往/[root]/src/app并创建一个名为 的文件夹api。在该api文件夹中,创建一个名为 的文件夹copilotkit

copilotkit文件夹中,创建一个名为 `.ts` 的文件research.ts。然后找到这个 `research.ts` gist 文件,复制其中的代码,并将其添加到该research.ts文件中。

route.ts接下来,在文件夹中创建一个名为 `<filename>` 的文件/[root]/src/app/api/copilotkit。该文件将包含用于设置后端功能以处理 POST 请求的代码。它会根据条件包含一个“研究”操作,该操作会对给定主题执行研究。

现在在文件顶部导入以下模块。


import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend"; // For backend functionality with CopilotKit.
import { researchWithLangGraph } from "./research"; // Import a custom function for conducting research.
import { AnnotatedFunction } from "@copilotkit/shared"; // For annotating functions with metadata.
Enter fullscreen mode Exit fullscreen mode

在上面的代码下方,使用下面的代码定义一个运行时环境变量和一个用于研究的带注释的函数。


// Define a runtime environment variable, indicating the environment where the code is expected to run.
export const runtime = "edge";

// Define an annotated function for research. This object includes metadata and an implementation for the function.
const researchAction: AnnotatedFunction<any> = {
  name: "research", // Function name.
  description: "Call this function to conduct research on a certain topic. Respect other notes about when to call this function", // Function description.
  argumentAnnotations: [ // Annotations for arguments that the function accepts.
    {
      name: "topic", // Argument name.
      type: "string", // Argument type.
      description: "The topic to research. 5 characters or longer.", // Argument description.
      required: true, // Indicates that the argument is required.
    },
  ],
  implementation: async (topic) => { // The actual function implementation.
    console.log("Researching topic: ", topic); // Log the research topic.
    return await researchWithLangGraph(topic); // Call the research function and return its result.
  },
};
Enter fullscreen mode Exit fullscreen mode

然后在上面的代码下方添加以下代码,以定义一个处理 POST 请求的异步函数。

// Define an asynchronous function that handles POST requests.
export async function POST(req: Request): Promise<Response> {
  const actions: AnnotatedFunction<any>[] = []; // Initialize an array to hold actions.

  // Check if a specific environment variable is set, indicating access to certain functionality.
  if (process.env["TAVILY_API_KEY"]) {
    actions.push(researchAction); // Add the research action to the actions array if the condition is true.
  }

  // Instantiate CopilotBackend with the actions defined above.
  const copilotKit = new CopilotBackend({
    actions: actions,
  });

  // Use the CopilotBackend instance to generate a response for the incoming request using an OpenAIAdapter.
  return copilotKit.response(req, new OpenAIAdapter());
}
Enter fullscreen mode Exit fullscreen mode

将 PowerPoint 应用与 CopilotKit 前端集成

useMakeCopilotActionable让我们首先在文件顶部导入钩子/[root]/src/app/components/Slide.tsx

import { useMakeCopilotActionable } from "@copilotkit/react-core";
Enter fullscreen mode Exit fullscreen mode

在 Slide 函数内部,添加以下代码,该代码使用 useMakeCopilotActionable 钩子设置一个 updateSlide 带有特定参数的操作,并实现一个根据提供的值更新幻灯片的功能。

useMakeCopilotActionable({
        // Defines the action name. This is a unique identifier for the action within the application.
        name: "updateSlide",
        // Describes what the action does. In this case, it updates the current slide.
        description: "Update the current slide.",
        // Details the arguments that the action accepts. Each argument has a name, type, description, and a flag indicating if it's required.
        argumentAnnotations: [
        {
            name: "title", // The argument name.
            type: "string", // The data type of the argument.
            description: "The title of the slide. Should be a few words long.", // Description of the argument.
            required: true, // Indicates that this argument must be provided for the action to execute.
        },
        {
            name: "content",
            type: "string",
            description: "The content of the slide. Should generally consists of a few bullet points.",
            required: true,
        },
        {
            name: "backgroundImageDescription",
            type: "string",
            description: "What to display in the background of the slide. For example, 'dog', 'house', etc.",
            required: true,
        },
        {
            name: "spokenNarration",
            type: "string",
            description: "The spoken narration for the slide. This is what the user will hear when the slide is shown.",
            required: true,
        },
        ],
        // The implementation of the action. This is a function that will be called when the action is executed.
        implementation: async (title, content, backgroundImageDescription, spokenNarration) => {
        // Calls a function passed in through props to partially update the slide with new values for the specified properties.
        props.partialUpdateSlide({
            title,
            content,
            backgroundImageDescription,
            spokenNarration,
        });
        },
    }, [props.partialUpdateSlide]); // Dependencies array for the custom hook or function. This ensures that the action is re-initialized only when `props.partialUpdateSlide` changes.

Enter fullscreen mode Exit fullscreen mode

之后,打开/[root]/src/app/components/Presentation.tsx文件,并在顶部使用以下代码导入 CopilotKit 前端包。

import { useCopilotContext } from "@copilotkit/react-core";
import { CopilotTask } from "@copilotkit/react-core";
import {
  useMakeCopilotActionable,
  useMakeCopilotReadable
} from "@copilotkit/react-core";
Enter fullscreen mode Exit fullscreen mode

在 Presentation 函数中,添加以下代码,该代码使用 钩子将幻灯片 数组作为上下文useMakeCopilotReadable 添加 到应用内聊天机器人。这些钩子使 Copilot 可以读取演示文稿中的所有幻灯片以及当前幻灯片的数据。SlidescurrentSlide

useMakeCopilotReadable("These are all the slides: " + JSON.stringify(slides));
  useMakeCopilotReadable(
    "This is the current slide: " + JSON.stringify(currentSlide)
  );
Enter fullscreen mode Exit fullscreen mode

在钩子下方useMakeCopilotReadable ,添加以下代码,该代码使用useCopilotActionable钩子设置一个名为“ appendSlide添加多个幻灯片”的操作,该操作带有描述和实现函数。

useMakeCopilotActionable(
        {
          // Defines the action's metadata.
          name: "appendSlide", // Action identifier.
          description: "Add a slide after all the existing slides. Call this function multiple times to add multiple slides.",
          // Specifies the arguments that the action takes, including their types, descriptions, and if they are required.
          argumentAnnotations: [
            {
              name: "title", // The title of the new slide.
              type: "string",
              description: "The title of the slide. Should be a few words long.",
              required: true,
            },
            {
              name: "content", // The main content or body of the new slide.
              type: "string",
              description: "The content of the slide. Should generally consist of a few bullet points.",
              required: true,
            },
            {
              name: "backgroundImageDescription", // Description for fetching or generating the background image of the new slide.
              type: "string",
              description: "What to display in the background of the slide. For example, 'dog', 'house', etc.",
              required: true,
            },
            {
              name: "spokenNarration", // Narration text that will be read aloud during the presentation of the slide.
              type: "string",
              description: "The text to read while presenting the slide. Should be distinct from the slide's content, and can include additional context, references, etc. Will be read aloud as-is. Should be a few sentences long, clear, and smooth to read.",
              required: true,
            },
          ],
          // The function to execute when the action is triggered. It creates a new slide with the provided details and appends it to the existing slides array.
          implementation: async (title, content, backgroundImageDescription, spokenNarration) => {
            const newSlide: SlideModel = { // Constructs the new slide object.
              title,
              content,
              backgroundImageDescription,
              spokenNarration,
            };

            // Updates the slides state by appending the new slide to the end of the current slides array.
            setSlides((slides) => [...slides, newSlide]);
          },
        },
        [setSlides] // Dependency array for the hook. This action is dependent on the `setSlides` function, ensuring it reinitializes if `setSlides` changes.
      );
Enter fullscreen mode Exit fullscreen mode

在上面的代码下方,定义一个名为 `context` 的变量context,该变量使用名为 `copilot` 的自定义钩子从副驾驶上下文中检索当前上下文useCopilotContext

const context = useCopilotContext();
Enter fullscreen mode Exit fullscreen mode

之后,定义一个名为 `getSlide()` 的函数,该函数generateSlideTask包含一个名为 `getSlide()` 的类CopilotTaskCopilotTask该类定义了生成与演示文稿整体主题相关的新幻灯片的指令。

const generateSlideTask = new CopilotTask({
   instructions: "Make the next slide related to the overall topic of the presentation. It will be inserted after the current slide. Do NOT carry any research",
});
Enter fullscreen mode Exit fullscreen mode

然后在上面的代码下方初始化一个名为 `state` 的状态变量,generateSlideTaskRunning默认值为 `false`。

const [generateSlideTaskRunning, **setGenerateSlideTaskRunning**] = useState(false);
Enter fullscreen mode Exit fullscreen mode

之后,使用下面的代码更新演示组件中的操作按钮,以通过添加、删除和导航幻灯片来添加动态交互。

// The JSX structure for the Presentation component.
    return (
        <div className="relative">
        {/* Renders the current slide using a Slide component with props for the slide data and a method to update it. */}
        <Slide slide={currentSlide} partialUpdateSlide={updateCurrentSlide} />

        {/* Container for action buttons positioned at the top left corner of the relative parent */}
        <div className="absolute top-0 left-0 mt-6 ml-4 z-30">
            {/* ActionButton to add a new slide. It is disabled when a generateSlideTask is running to prevent concurrent modifications. */}
            <ActionButton
            disabled={generateSlideTaskRunning}
            onClick={() => {
                const newSlide: SlideModel = {
                title: "Title",
                content: "Body",
                backgroundImageDescription: "random",
                spokenNarration: "The speaker's notes for this slide.",
                };
                // Inserts the new slide immediately after the current slide and updates the slide index to point to the new slide.
                setSlides((slides) => [
                ...slides.slice(0, currentSlideIndex + 1),
                newSlide,
                ...slides.slice(currentSlideIndex + 1),
                ]);
                setCurrentSlideIndex((i) => i + 1);
            }}
            className="rounded-r-none"
            >
            <PlusIcon className="h-6 w-6" />
            </ActionButton>

            {/* ActionButton to generate a new slide based on the current context, also disabled during task running. */}
            <ActionButton
            disabled={generateSlideTaskRunning}
            onClick={async () => { 
                setGenerateSlideTaskRunning(true); // Indicates the task is starting.
                await generateSlideTask.run(context); // Executes the task with the current context.
                setGenerateSlideTaskRunning(false); // Resets the flag when the task is complete.
            }}
            className="rounded-l-none ml-[1px]"
            >
            <SparklesIcon className="h-6 w-6" />
            </ActionButton>
        </div>

        {/* Container for action buttons at the top right, including deleting the current slide and potentially other actions. */}
        <div className="absolute top-0 right-0 mt-6 mr-24">
            {/* ActionButton for deleting the current slide, disabled if a task is running or only one slide remains. */}
            <ActionButton
            disabled={generateSlideTaskRunning || slides.length === 1}
            onClick={() => {
                console.log("delete slide");
                // Removes the current slide and resets the index to the beginning as a simple handling strategy.
                setSlides((slides) => [
                ...slides.slice(0, currentSlideIndex),
                ...slides.slice(currentSlideIndex + 1),
                ]);
                setCurrentSlideIndex((i) => 0);
            }}
            className="ml-5 rounded-r-none"
            >
            <TrashIcon className="h-6 w-6" />
            </ActionButton>
        </div>

        {/* Display showing the current slide index and the total number of slides. */}
        <div
            className="absolute bottom-0 right-0 mb-20 mx-24 text-xl"
            style={{
            textShadow: "1px 1px 0 #ddd, -1px -1px 0 #ddd, 1px -1px 0 #ddd, -1px 1px 0 #ddd",
            }}
        >
            Slide {currentSlideIndex + 1} of {slides.length}
        </div>

        {/* Navigation buttons to move between slides, disabled based on the slide index or if a task is running. */}
        <div className="absolute bottom-0 right-0 mb-6 mx-24">
            {/* Button to move to the previous slide, disabled if on the first slide or a task is running. */}
            <ActionButton
            className="rounded-r-none"
            disabled={generateSlideTaskRunning || currentSlideIndex === 0}
            onClick={() => {
                setCurrentSlideIndex((i) => i - 1);
            }}
            >
            <BackwardIcon className="h-6 w-6" />
            </ActionButton>
            {/* Button to move to the next slide, disabled if on the last slide or a task is running. */}
            <ActionButton
            className="mr-[1px] rounded-l-none"
            disabled={generateSlideTaskRunning || currentSlideIndex + 1 === slides.length}
            onClick={async () => {
                setCurrentSlideIndex((i) => i + 1);
            }}
            >
            <ForwardIcon className="h-6 w-6" />
            </ActionButton>
        </div>
        </div>
    );
Enter fullscreen mode Exit fullscreen mode

现在让我们打开/[root]/src/app/page.tsx文件,在文件顶部使用以下代码导入 CopilotKit 前端包和样式。

import {
  CopilotKit,
} from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";
Enter fullscreen mode Exit fullscreen mode

然后使用CopilotKit`and`CopilotSidebar来包裹 Presentation 组件,如下所示。

export default function AIPresentation() {

  return (
    <CopilotKit url="/api/copilotkit/">
      <CopilotSidebar
        instructions="Help the user create and edit a powerpoint-style presentation. IMPORTANT NOTE: SOMETIMES you may want to research a topic, before taking further action. BUT FIRST ASK THE USER if they would like you to research it. If they answer 'no', do your best WITHOUT researching the topic first."
        defaultOpen={true}
        labels={{
          title: "Presentation Copilot",
          initial:
            "Hi you! 👋 I can help you create a presentation on any topic.",
        }}
        clickOutsideToClose={false}
      >
        <Presentation  />
      </CopilotSidebar>
    </CopilotKit>
  );
}
Enter fullscreen mode Exit fullscreen mode

之后,运行开发服务器并访问 http://localhost:3000/。您应该可以看到应用内聊天机器人已集成到 PowerPoint Web 应用中。

应用内聊天机器人

最后,向右侧的聊天机器人发出类似“用 JavaScript 创建 PowerPoint 演示文稿”的提示。聊天机器人将开始生成回复,完成后,使用底部的前进按钮浏览生成的幻灯片。

注意:如果聊天机器人没有立即生成幻灯片,请根据其回复给予适当的后续提示。

PowerPoint演示文稿

结论

总之,您可以使用 CopilotKit 构建应用内 AI 聊天机器人,这些机器人可以感知应用的当前状态并在应用内执行操作。AI 聊天机器人可以与应用的前端、后端以及第三方服务进行通信。

完整源代码请访问:https://github.com/TheGreatBonnie/aipoweredpowerpointapp

文章来源:https://dev.to/copilotkit/how-to-build-an-ai-powered-powerpoint-generator-langchain-copilotkit-openai-nextjs-4c76