Summarize a block of text using OpenAI

Summarize a block of text using OpenAI

Trigger: eventTrigger

An app which uses OpenAI to summarize an article and then post the result to Slack.

Framework:

APIs used:

Repository:

/openai-text-summarizer

Categories:

AI

Overview

This app uses OpenAI to summarize a block of text and then posts the result to Slack.

To get started with this project follow the instructions on the GitHub README page (~5 mins).

OpenAI summarizer gif

Key features:

  • Uses Next.js experimental serverActions
  • Uses both our OpenAI and Slack integrations
  • Uses React hooks to show the live status of the Job in the UI

Using Next.js Server Actions

This project uses Next.js Server Actions to handle the form submission. The feature is currently experimental but can be enabled in your next.config.js file using the serverActions flag.

next.config.js

_10
/** @type {import('next').NextConfig} */
_10
module.exports = {
_10
reactStrictMode: true,
_10
experimental: {
_10
serverActions: true,
_10
},
_10
};

Here we are using the sendText action to handle the form submission. This action is defined in the _actions.ts file.

form.tsx

_29
"use client";
_29
import { sendText } from "../_actions";
_29
_29
...
_29
_29
<form
_29
action={sendText}
_29
onSubmit={handleSubmit}
_29
className="flex w-full max-w-2xl flex-col gap-y-4"
_29
>
_29
<textarea
_29
rows={16}
_29
name="text"
_29
value={formState.text}
_29
onChange={(e) =>
_29
setFormState({ status: "idle", text: e.target.value })
_29
}
_29
disabled={formState.status === "submitting"}
_29
placeholder="Paste some long text here or an article and click Summarize."
_29
className="w-full rounded border border-slate-700 bg-slate-800 px-6 py-4 text-white"
_29
/>
_29
<Button
_29
disabled={formState.text === "" || formState.status === "submitting"}
_29
>
_29
{formState.status === "idle" ? "✨ Summarize ✨" : "Loading..."}
_29
</Button>
_29
</form>
_29
_29
...

_actions.ts

_17
"use server";
_17
_17
import { client } from "@/trigger";
_17
import { redirect } from "next/navigation";
_17
_17
export async function sendText(data: FormData) {
_17
const text = data.get("text");
_17
_17
const event = await client.sendEvent({
_17
name: "summarize.text",
_17
payload: {
_17
text,
_17
},
_17
});
_17
_17
redirect(`/summarize/${event.id}`);
_17
}

Using our OpenAI and Slack integrations

The Job code itself is very simple. It uses an eventTrigger which is triggered by the summarize.text event. The text is then summarized using our OpenAI integration (ChatGPT-3.5 in this case), and then posted to a specific Slack channel using our Slack integration.

textSummarizer.ts

_49
// use Open AI to summarize text from the form
_49
client.defineJob({
_49
id: "openai-summarizer",
_49
name: "OpenAI – Text Summarizer",
_49
version: "0.0.1",
_49
trigger: eventTrigger({
_49
name: "summarize.text",
_49
schema: z.object({
_49
text: z.string(),
_49
}),
_49
}),
_49
integrations: {
_49
openai,
_49
slack,
_49
},
_49
run: async (payload, io) => {
_49
const result = await io.openai.backgroundCreateChatCompletion(
_49
"Generating summary",
_49
{
_49
model: "gpt-3.5-turbo-16k",
_49
messages: [
_49
{
_49
role: "user",
_49
content: `Summarize the following text with the most unique and helpful points, into a numbered list of key points and takeaways: \n ${payload.text}`,
_49
},
_49
],
_49
}
_49
);
_49
_49
if (!result.choices || !result.choices[0] || !result.choices[0].message) {
_49
io.logger.error(
_49
"Failed to post your message to Slack. The content is undefined."
_49
);
_49
return;
_49
}
_49
_49
const summary = result.choices[0].message.content;
_49
_49
await io.slack.postMessage("Posting to Slack", {
_49
// replace this with your own channel ID
_49
channel: "C05HNRBV22H",
_49
text: summary,
_49
});
_49
_49
return {
_49
summary,
_49
};
_49
},
_49
});

Using React hooks to show the live status of the Job in the UI

Summarizer in progress

Another cool feature of this project is that it uses React hooks to show the live status of the Job using the useEventRunDetails hook from the @trigger.dev/react package.

Summary.tsx

_81
"use client";
_81
_81
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
_81
import { CompanyIcon } from "@trigger.dev/companyicons";
_81
import { useEventRunDetails } from "@trigger.dev/react";
_81
import { ButtonLink } from "./Button";
_81
import { Spinner } from "./Spinner";
_81
_81
export function Summary({ eventId }: { eventId: string }) {
_81
const { isLoading, isError, data, error } = useEventRunDetails(eventId);
_81
_81
if (isError) {
_81
return <p>Error</p>;
_81
}
_81
_81
return (
_81
<div className="flex w-full flex-col gap-4">
_81
<div className="flex flex-col gap-2">
_81
<>
_81
<ProgressItem
_81
state={
_81
data?.tasks === undefined || data.tasks.length === 0
_81
? "progress"
_81
: "completed"
_81
}
_81
name="Starting up"
_81
/>
_81
{data?.tasks?.map((task) => (
_81
<ProgressItem
_81
key={task.id}
_81
state={
_81
task.status === "COMPLETED"
_81
? "completed"
_81
: task.status === "ERRORED"
_81
? "failed"
_81
: "progress"
_81
}
_81
name={task.displayKey ?? task.name ?? ""}
_81
icon={task.icon}
_81
/>
_81
))}
_81
</>
_81
</div>
_81
{data?.output && data.status === "SUCCESS" && (
_81
<div className="flex flex-col gap-0.5">
_81
<h4 className="text-base font-semibold">Posted to Slack</h4>
_81
<p className="mb-4 text-sm text-slate-400">{data.output.summary}</p>
_81
</div>
_81
)}
_81
{(data?.status === "SUCCESS" || data?.status === "FAILURE") && (
_81
<ButtonLink href={"/"}>Summarize another</ButtonLink>
_81
)}
_81
</div>
_81
);
_81
}
_81
_81
type ProgressItemProps = {
_81
icon?: string;
_81
state: "progress" | "completed" | "failed";
_81
name: string;
_81
};
_81
_81
function ProgressItem({ icon, state, name }: ProgressItemProps) {
_81
return (
_81
<div className="flex items-center gap-2">
_81
{state === "progress" ? (
_81
<Spinner className="h-6 w-6" />
_81
) : state === "completed" ? (
_81
<CheckCircleIcon className="h-6 w-6 text-emerald-600" />
_81
) : (
_81
<XCircleIcon className="h-6 w-6 text-red-600" />
_81
)}
_81
<div className="flex items-center gap-1.5">
_81
{icon && (
_81
<CompanyIcon name={icon} className="h-5 w-5" variant="light" />
_81
)}
_81
<h4 className="text-base">{name}</h4>
_81
</div>
_81
</div>
_81
);
_81
}

This is a simple example of how you can use the hook to show the job status, but of course this can be adapted for different use cases depending on the job.

Get started with this project

You can get started with this project in minutes. Simply follow the instructions on the GitHub README page.