构建您自己的 AI Meme 生成器并学习如何使用 OpenAI 的函数调用☎️ TL;DR 简介第 1 部分待续……

2025-06-07

构建你自己的 AI Meme 生成器并学习如何使用 OpenAI 的函数调用☎️

TL;DR

简介

第 1 部分

待续…

目录

TL;DR

在这个由两部分组成的教程中,我们将使用以下内容构建一个全栈即时 Meme Generator 应用程序:

您可以在这里查看我们将要构建的应用程序的部署版本:Memerator

如果您只想查看完成的应用程序的代码,请查看Memerator 的 GitHub Repo

开始之前

我们正在Wasp上努力帮助您尽可能轻松地构建 Web 应用程序 - 包括制作这些每周发布的教程!

如果您能通过在 GitHub 上关注我们的 repo 来帮助我们,我们将非常感激:https://www.github.com/wasp-lang/wasp 🙏

https://media1.giphy.com/media/ZfK4cXKJTTay1Ava29/giphy.gif?cid=7941fdc6pmqo30ll0e4rzdiisbtagx97sx5t0znx4lk0auju&ep=v1_gifs_search&rid=giphy.gif&ct=g

⭐️感谢您的支持🙏

简介

打电话给我,可能的话

借助OpenAI 的聊天完成 API,开发者现在可以实现一些非常酷炫的功能。它基本上实现了 ChatGPT 功能,但以可调用 API 的形式,可以集成到任何应用中。

但是在使用 API 时,许多开发人员希望 GPT 以 JSON 等格式返回数据,以便他们可以在其应用程序的功能中使用。

不幸的是,如果你要求 ChatGPT 以特定格式返回数据,它并不总是正确的。因此,OpenAI 发布了函数调用

正如他们所描述的,函数调用允许开发人员“......向 GPT 描述函数,并让模型智能地选择输出包含参数的 JSON 对象来调用这些函数。”

这是将自然语言转换为 API 调用的好方法。

那么,还有什么比使用它来调用Imgflip.com 的 meme 创建器 API更好的方法来学习如何使用 GPT 的函数调用功能呢?

图片描述

让我们一起构建

在这个由两部分组成的教程中,我们将使用以下内容构建一个全栈 React/NodeJS 应用程序:

  • 验证
  • 通过 OpenAI 的函数调用和 ImgFlip.com 的 API 生成表情包
  • 每日 cron 任务来获取新的 meme 模板
  • 模因编辑和删除
  • 以及更多!

图片描述

我已经部署了此应用程序的工作版本,您可以在此处试用:https://damemerator.netlify.app — 尝试一下,让我们开始吧。

在本教程的第 1 部分中,我们将设置应用程序并生成和显示模因。

在第 2 部分中,我们将添加更多功能,例如重复的 cron 作业以获取更多 meme 模板,以及编辑和删除 meme 的功能。

顺便说一下,两个快速提示:

  1. 如果您需要随时参考该应用程序的完成代码来帮助您完成本教程,您可以在此处查看该应用程序的 GitHub Repo
  2. 如果您有任何疑问,请随时进入Wasp Discord 服务器并询问我们!

第 1 部分

配置

我们要把它做成一个全栈的 React/NodeJS Web 应用,所以需要先完成相关设置。不过不用担心,这根本不会花很长时间,因为我们会使用Wasp作为框架。

Wasp 帮我们完成了所有繁重的工作。你马上就会明白我的意思。

设置 Wasp 项目

首先,通过在终端中运行以下命令来安装 Wasp:

curl -sSL <https://get.wasp-lang.dev/installer.sh> | sh
Enter fullscreen mode Exit fullscreen mode

接下来,让我们克隆我为您准备的Memeratorstart应用程序分支:

git clone -b start https://github.com/vincanger/memerator.git
Enter fullscreen mode Exit fullscreen mode

然后导航到Memerator目录并在 VS Code 中打开项目:

cd Memerator && code .
Enter fullscreen mode Exit fullscreen mode

您会注意到 Wasp 使用如下文件结构设置您的全栈应用程序:

.
├── main.wasp             # The wasp config file.
└── src
    ├── client            # Your React client code (JS/CSS/HTML) goes here.
    ├── server            # Your server code (Node JS) goes here.
    └── shared            # Your shared (runtime independent) code goes here.

Enter fullscreen mode Exit fullscreen mode

我们先来看看这个main.wasp文件。你可以把它看作是应用的“骨架”,或者说是指令。这个文件为你配置了大部分全栈应用的功能:

app Memerator {
  wasp: {
    version: "^0.11.3"
  },
  title: "Memerator",
  client: {
    rootComponent: import { Layout } from "@client/Layout",
  },
  db: {
    system: PostgreSQL,
    prisma: {
      clientPreviewFeatures: ["extendedWhereUnique"]
    }
  },
  auth: {
    userEntity: User,
    methods: {
      usernameAndPassword: {}
    },
    onAuthFailedRedirectTo: "/login",
    onAuthSucceededRedirectTo: "/"
  },
  dependencies: [
    ("openai", "4.2.0"),
    ("axios", "^1.4.0"),
    ("react-icons", "4.10.1"),
  ]
}

entity User {=psl
    id       Int    @id @default(autoincrement())
    username String @unique
    password String
    memes    Meme[]
    isAdmin  Boolean @default(false)
    credits  Int     @default(2)
psl=}

entity Meme {=psl
    id         String @id @default(uuid())
    url        String 
    text0      String
    text1      String
    topics     String 
    audience   String 
    template   Template @relation(fields: [templateId], references: [id])
    templateId String
    user       User   @relation(fields: [userId], references: [id])
    userId     Int    
    createdAt  DateTime @default(now())
psl=}

entity Template {=psl
    id       String @id @unique
    name     String
    url      String
    width    Int
    height   Int
    boxCount Int
    memes    Meme[]
psl=}

route HomePageRoute { path: "/", to: HomePage }
page HomePage {
  component: import { HomePage } from "@client/pages/Home",
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
  component: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
  component: import Signup from "@client/pages/auth/Signup"
}
Enter fullscreen mode Exit fullscreen mode

如您所见,我们的main.wasp配置文件包含:

  • 依赖项,
  • 身份验证方法,
  • 数据库类型,以及
  • 数据库模型(“实体”)
  • 客户端页面和路由

您可能还注意到了{=psl psl=}上面实体中的这种语法。这表示这些psl括号之间的任何内容实际上都代表着另一种语言,在本例中是Prisma Schema Language。Wasp 内部使用了 Prisma,因此如果您以前使用过 Prisma,应该很容易理解。

另外,请确保安装了Wasp VS 代码扩展,以便获得良好的语法突出显示和最佳的整体开发体验。

设置数据库

我们仍然需要设置 Postgres 数据库。

通常这会非常烦人,但有了 Wasp 就变得非常容易。

  1. 只需安装并运行Docker Deskop ,
  2. 打开一个单独的终端选项卡/窗口,
  3. cd进入Memerator目录,然后运行
wasp start db
Enter fullscreen mode Exit fullscreen mode

这将启动你的应用并将其连接到 Postgres 数据库。无需执行任何其他操作!🤯 

只需让此终端选项卡与 docker 桌面一起在后台打开并运行即可。

现在,在另一个终端选项卡中运行

wasp db migrate-dev
Enter fullscreen mode Exit fullscreen mode

并确保为数据库迁移命名,例如init

环境变量

在项目的根目录中,您将找到一个.env.server.example如下所示的文件:

# set up your own credentials on https://imgflip.com/signup and rename this file to .env.server
# NOTE: make sure you register with Username and Password (not google) 
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=

# get your api key from https://platform.openai.com/
OPENAI_API_KEY=

JWT_SECRET=asecretphraseatleastthirtytwocharacterslong
Enter fullscreen mode Exit fullscreen mode

将此文件重命名为.env.server并按照其中的说明获取:

因为我们需要它们来生成我们的表情包🤡

启动你的应用程序

一切设置正确后,您现在应该可以运行

wasp start
Enter fullscreen mode Exit fullscreen mode

运行时wasp start,Wasp 将安装所有必要的 npm 包,在端口 3001 上启动我们的 NodeJS 服务器,并在端口 3000 上启动我们的 React 客户端。

在浏览器中访问localhost:3000进行查看。我们的应用基础应该如下所示:

图片描述

生成模因

样板代码已经设置了用于生成模因的客户端表单:

  • 主题
  • 目标受众

这些信息将发送到后端,以便使用函数调用来调用 OpenAI API。然后,我们将此信息发送到imglfip.com API来生成 meme。

但是imgflip API 的/caption_image端点需要 meme 模板 ID。为了获取该 ID,我们首先需要从 imgflip 的/get_memes端点获取可用的 meme 模板。

那么我们现在就开始设置吧。

服务器端代码

src/server/创建一个名为的新文件utils.ts

import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';

type GenerateMemeArgs = {
  text0: string;
  text1: string;
  templateId: string;
};

export const fetchMemeTemplates = async () => {
  try {
    const response = await axios.get('https://api.imgflip.com/get_memes');
    return response.data.data.memes;
  } catch (error) {
    console.error(error);
    throw new HttpError(500, 'Error fetching meme templates');
  }
};

export const generateMemeImage = async (args: GenerateMemeArgs) => {
  console.log('args: ', args);

  try {
    const data = stringify({
      template_id: args.templateId,
      username: process.env.IMGFLIP_USERNAME,
      password: process.env.IMGFLIP_PASSWORD,
      text0: args.text0,
      text1: args.text1,
    });

    // Implement the generation of meme using the Imgflip API
    const res = await axios.post('https://api.imgflip.com/caption_image', data, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });

    const url = res.data.data.url;

    console.log('generated meme url: ', url);

    return url as string;
  } catch (error) {
    console.error(error);
    throw new HttpError(500, 'Error generating meme image');
  }
};
Enter fullscreen mode Exit fullscreen mode

这为我们提供了一些实用函数来帮助我们获取所有可以用来生成 meme 图像的 meme 模板。

请注意,对/caption_image端点的 POST 请求需要以下数据:

  • 我们的 imgflip用户名密码
  • 我们将使用的 meme 模板的ID
  • meme 顶部的文本,即text0
  • meme 底部的文本,即text1

图片描述

text0 和 text1 参数将由我们可爱的朋友 ChatGPT 生成。但为了让 GPT 做到这一点,我们还必须设置它的 API 调用。

为此,请创建一个src/server/名为 的新文件actions.ts

然后返回到您的main.wasp配置文件并在文件底部添加以下 Wasp Action:

//...

action createMeme {
  fn: import { createMeme } from "@server/actions.js",
  entities: [Meme, Template, User]
}
Enter fullscreen mode Exit fullscreen mode

Action是 Wasp 操作的一种,用于更改后端的某些状态。它本质上是一个在服务器上调用的 NodeJS 函数,但 Wasp 会帮你完成所有设置。

这意味着您无需担心为 Action 构建 HTTP API、管理服务器端请求处理,甚至无需处理客户端响应和缓存。您只需编写业务逻辑!

图片描述

如果您已安装 Wasp VS Code 扩展,则会看到如上所示的错误。将鼠标悬停在错误上方,然后点击Quick Fix> create function createMeme

如果文件存在,这将createMeme在你的文件中构建一个函数(如下所示) 。太酷了!actions.ts

import { CreateMeme } from '@wasp/actions/types'

type CreateMemeInput = void
type CreateMemeOutput = void

export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
  // Implementation goes here
}
Enter fullscreen mode Exit fullscreen mode

您可以看到它也为您导入了 Action 类型。

因为我们将从前端表单发送meme 的topics数组和预期字符串,最后我们将返回新创建的实体,这就是我们应该定义的类型。audienceMeme

请记住,Meme实体是我们在配置文件中定义的数据库模型main.wasp

知道了这一点,我们可以将的内容改为actions.ts

import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
  // Implementation goes here
}
Enter fullscreen mode Exit fullscreen mode

在我们实现其余逻辑之前,让我们先了解一下我们的createMeme函数应该如何工作以及我们的Meme意志是如何生成的:

  1. 获取我们要使用的 imgflip meme 模板
  2. 将其名称、主题和目标受众发送到 OpenAI 的聊天完成 API
  3. 告诉 OpenAI 我们希望将结果作为参数返回,以便以 JSON 格式传递给下一个函数,即 OpenAI 的函数调用
  4. 将这些参数传递给 imgflip /caption-image端点并获取我们创建的 meme 的 url
  5. 将 meme url 和其他信息作为Meme实体保存到我们的数据库中

考虑到所有这些,继续actions.ts用已完成的createMeme操作完全替换我们中的内容:

import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } from './utils.js';

import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
  if (!context.user) {
    throw new HttpError(401, 'You must be logged in');
  }

  if (context.user.credits === 0 && !context.user.isAdmin) {
    throw new HttpError(403, 'You have no credits left');
  }

  const topicsStr = topics.join(', ');

  let templates: Template[] = await context.entities.Template.findMany({});

    if (templates.length === 0) {
    const memeTemplates = await fetchMemeTemplates();
    templates = await Promise.all(
      memeTemplates.map(async (template: any) => {
        const addedTemplate = await context.entities.Template.upsert({
          where: { id: template.id },
          create: {
            id: template.id,
            name: template.name,
            url: template.url,
            width: template.width,
            height: template.height,
            boxCount: template.box_count
          },
          update: {}
        });

        return addedTemplate;
      })
    );
  }

  // filter out templates with box_count > 2
  templates = templates.filter((template) => template.boxCount <= 2);
  const randomTemplate = templates[Math.floor(Math.random() * templates.length)];

  console.log('random template: ', randomTemplate);

  const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided`;
  const userPrompt = `Topics: ${topicsStr} \n Intended Audience: ${audience} \n Template: ${randomTemplate.name} \n`;

  let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
  try {
    openAIResponse = await openai.chat.completions.create({
      messages: [
        { role: 'system', content: sysPrompt },
        { role: 'user', content: userPrompt },
      ],
      functions: [
        {
          name: 'generateMemeImage',
          description: 'Generate meme via the imgflip API based on the given idea',
          parameters: {
            type: 'object',
            properties: {
              text0: { type: 'string', description: 'The text for the top caption of the meme' },
              text1: { type: 'string', description: 'The text for the bottom caption of the meme' },
            },
            required: ['templateName', 'text0', 'text1'],
          },
        },
      ],
      function_call: {
        name: 'generateMemeImage',
      },
      model: 'gpt-4-0613',
    });
  } catch (error: any) {
    console.error('Error calling openAI: ', error);
    throw new HttpError(500, 'Error calling openAI');
  }

  console.log(openAIResponse.choices[0]);

  /**
   * the Function call returned by openAI looks like this:
   */
  // {
  //   index: 0,
  //   message: {
  //     role: 'assistant',
  //     content: null,
  //     function_call: {
  //       name: 'generateMeme',
  //       arguments: '{\n' +
  //         `  "text0": "CSS you've been writing all day",\n` +
  //         '  "text1": "This looks horrible"\n' +
  //         '}'
  //     }
  //   },
  //   finish_reason: 'stop'
  // }
  if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');

  const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
  console.log('gptArgs: ', gptArgs);

  const memeIdeaText0 = gptArgs.text0;
  const memeIdeaText1 = gptArgs.text1;

  console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);

  const memeUrl = await generateMemeImage({
    templateId: randomTemplate.id,
    text0: memeIdeaText0,
    text1: memeIdeaText1,
  });

  const newMeme = await context.entities.Meme.create({
    data: {
      text0: memeIdeaText0,
      text1: memeIdeaText1,
      topics: topicsStr,
      audience: audience,
      url: memeUrl,
      template: { connect: { id: randomTemplate.id } },
      user: { connect: { id: context.user.id } },
    },
  });

  return newMeme;
};
Enter fullscreen mode Exit fullscreen mode

此时,上面的代码应该是不言自明的,但我想强调几点:

  1. context对象会被 Wasp 传递给所有操作和查询。它包含Prisma客户端,可以访问你在main.wasp配置文件中定义的数据库实体。
  2. 我们首先在数据库中查找 imgflip meme 模板。如果没有找到,则使用fetchTemplates之前定义的实用函数获取它们。然后将upsert它们存入数据库。
  3. 有些 meme 模板需要超过 2 个文本框,但在本教程中,我们仅使用带有 2 个文本输入的 meme 模板以使其更容易。
  4. 我们从此列表中选择一个随机模板作为我们模因的基础(这实际上是一种偶然生成一些有趣的模因内容的好方法)。
  5. functions我们通过和属性向 OpenAI API 提供有关其可以创建参数的函数的信息function_call,这告诉它始终为我们的函数返回 JSON 参数,generateMemeImage

太棒了!但是一旦我们开始生成表情包,我们就需要一种方法来在前端显示它们。

现在让我们创建一个 Wasp 查询。查询的工作原理与操作类似,只是它仅用于读取数据。

转到src/server并创建一个名为 的新文件queries.ts

接下来,在您的main.wasp文件中添加以下代码:

//...

query getAllMemes {
  fn: import { getAllMemes } from "@server/queries.js",
  entities: [Meme]
}
Enter fullscreen mode Exit fullscreen mode

然后在您的queries.ts文件中添加以下getAllMemes函数:

import HttpError from '@wasp/core/HttpError.js';

import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';

export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
  const memeIdeas = await context.entities.Meme.findMany({
    orderBy: { createdAt: 'desc' },
    include: { template: true },
  });

  return memeIdeas;
};
Enter fullscreen mode Exit fullscreen mode

客户端代码

现在我们已经有了服务器端实现的createMeme代码getAllMemes,让我们将其连接到我们的客户端。

Wasp 使得我们能够非常轻松地导入刚刚创建的操作并在前端调用它们。

您可以通过转到src/client/pages/Home.tsx文件顶部并添加以下代码来实现:

//...other imports...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';

export function HomePage() {
  const [topics, setTopics] = useState(['']);
  const [audience, setAudience] = useState('');
  const [isMemeGenerating, setIsMemeGenerating] = useState(false);

  // 😎 😎 😎
  const { data: user } = useAuth();
  const { data: memes, isLoading, error } = useQuery(getAllMemes); 

  const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault();
    if (!user) {
      history.push('/login');
      return;
    }
    if (topics.join('').trim().length === 0 || audience.length === 0) {
      alert('Please provide topic and audience');
      return;
    }
    try {
      setIsMemeGenerating(true);
      await createMeme({ topics, audience }); // <--- 😎 😎 😎
    } catch (error: any) {
      alert('Error generating meme: ' + error.message);
    } finally {
      setIsMemeGenerating(false);
    }
  };

//...
Enter fullscreen mode Exit fullscreen mode

如您所见,我们已经导入createMemegetAllMemes(😎)。

对于getAllMemes,我们将其包装在useQuery钩子中,以便我们可以获取并缓存数据。另一方面,我们的createMemeAction 会被调用,handleGenerateMeme我们将在提交表单时调用它。

无需逐个添加代码Home.tsx,以下是生成和显示表情包的所有代码。请Home.tsx用以下代码替换所有代码,我将在下面更详细地解释:

import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
import {
  AiOutlinePlusCircle,
  AiOutlineMinusCircle,
  AiOutlineRobot,
} from 'react-icons/ai';

export function HomePage() {
  const [topics, setTopics] = useState(['']);
  const [audience, setAudience] = useState('');
  const [isMemeGenerating, setIsMemeGenerating] = useState(false);

  const history = useHistory();
  const { data: user } = useAuth();
  const { data: memes, isLoading, error } = useQuery(getAllMemes);

  const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault();
    if (!user) {
      history.push('/login');
      return;
    }
    if (topics.join('').trim().length === 0 || audience.length === 0) {
      alert('Please provide topic and audience');
      return;
    }
    try {
      setIsMemeGenerating(true);
      await createMeme({ topics, audience });
    } catch (error: any) {
      alert('Error generating meme: ' + error.message);
    } finally {
      setIsMemeGenerating(false);
    }
  };

  const handleDeleteMeme = async (id: string) => {
    //...
  };

  if (isLoading) return 'Loading...';
  if (error) return 'Error: ' + error;

  return (
    <div className='p-4'>
      <h1 className='text-3xl font-bold mb-4'>Welcome to Memerator!</h1>
      <p className='mb-4'>Start generating meme ideas by providing topics and intended audience.</p>
      <form onSubmit={handleGenerateMeme}>
        <div className='mb-4 max-w-[500px]'>
          <label htmlFor='topics' className='block font-bold mb-2'>
            Topics:
          </label>
          {topics.map((topic, index) => (
            <input
              key={index}
              type='text'
              id='topics'
              value={topic}
              onChange={(e) => {
                const updatedTopics = [...topics];
                updatedTopics[index] = e.target.value;
                setTopics(updatedTopics);
              }}
              className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
            />
          ))}
          <div className='flex items-center my-2 gap-1'>
            <button
              type='button'
              onClick={() => topics.length < 3 && setTopics([...topics, ''])}
              className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
            >
              <AiOutlinePlusCircle /> Add Topic
            </button>
            {topics.length > 1 && (
              <button
                onClick={() => setTopics(topics.slice(0, -1))}
                className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
              >
                <AiOutlineMinusCircle /> Remove Topic
              </button>
            )}
          </div>
        </div>
        <div className='mb-4'>
          <label htmlFor='audience' className='block font-bold mb-2'>
            Intended Audience:
          </label>
          <input
            type='text'
            id='audience'
            value={audience}
            onChange={(e) => setAudience(e.target.value)}
            className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
          />
        </div>
        <button
          type='submit'
          className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${
            isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
          } $}`}
        >
          <AiOutlineRobot />
          {!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
        </button>
      </form>

      {!!memes && memes.length > 0 ? (
        memes.map((memeIdea) => (
          <div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
            <img src={memeIdea.url} width='500px' />
            <div className='flex flex-col items-start mt-2'>
              <div>
                <span className='text-sm text-gray-700'>Topics: </span>
                <span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>
              </div>
              <div>
                <span className='text-sm text-gray-700'>Audience: </span>
                <span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>
              </div>
            </div>
            {/* TODO: implement edit and delete meme features */}
          </div>
        ))
      ) : (
        <div className='flex justify-center mt-5'> :( no memes found</div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

关于这段代码,我想指出两点:

  1. 组件挂载时,钩子useQuery会调用我们的getAllMemes查询。它还会为我们缓存结果,并且每当我们通过 向数据库添加新的 Meme 时,都会自动重新获取结果createMeme。这意味着每当生成新的 Meme 时,我们的页面都会自动重新加载。
  2. 这个useAuth钩子允许我们获取已登录用户的信息。如果用户尚未登录,我们会强制他们登录后才能生成表情包。

这些真的很酷的 Wasp 功能,让你作为开发人员的生活变得更加轻松 🙂

那么,现在就尝试生成一个表情包吧。这是我刚刚生成的:

图片描述

哈哈。不错!

现在,如果我们可以编辑和删除表情包,那不是很酷吗?如果我们可以扩展表情包生成器可用的模板集,那又如何?那不是也很酷吗?

是的,确实如此。那就这么办吧。

待续…

在本教程的第 2 部分中,我们将处理应用程序的其余部分,例如添加重复的 cron 作业以获取更多 meme 模板,以及编辑、删除和共享 meme 的功能。

如果您觉得这篇文章对您有帮助,请在 GitHub 上给我们一个 star来表示支持 !这将帮助我们继续制作更多类似的东西。

https://res.cloudinary.com/practicaldev/image/fetch/s--OCpry2p9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bky8z46ii7ayejprrqw3.gif

⭐️感谢您的支持🙏

文章来源:https://dev.to/wasp/build-your-own-ai-meme-generator-learn-how-to-use-openais-function-calls-1p21
PREV
我是如何打造 CoverLetterGPT 并将其发展到拥有 5,000 名用户和 200 美元月经常性收入的?嗨,我是 Vince……CoverLetterGPT 到底是什么?它有哪些技术栈?我是如何营销它的?我是如何盈利的?未来计划 结语 + 更多资源
NEXT
使用 WebSockets、React 和 TypeScript 构建实时投票应用程序🔌⚡️