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

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

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

form.tsx

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

_actions.ts

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

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

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

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

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

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.