Skip to content

Commit

Permalink
Guess next digest name (#41)
Browse files Browse the repository at this point in the history
* Guess next digest name

* Fix storage

* Fix env var

* Fix for key

* Save digest predicted title on digest publication

* Remove DigestGuessTitle Component

* Add conditions to open ai search
  • Loading branch information
Lucieo authored Oct 3, 2023
1 parent f39b83f commit 0f304fd
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
TYPEFULLY_API_URL="https://api.typefully.com/v1"
JWT_SECRET="secret"
JWT_SECRET="secret"
OPENAI_API_KEY='my apiKey'
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@tremor/react": "^3.4.1",
"@types/jsonwebtoken": "^9.0.2",
"@vercel/og": "^0.4.0",
"ai": "^2.2.13",
"axios": "^1.3.4",
"class-variance-authority": "^0.4.0",
"contentlayer": "^0.3.2",
Expand All @@ -61,6 +62,7 @@
"next-contentlayer": "^0.3.2",
"next-superjson-plugin": "^0.5.8",
"nodemailer": "^6.9.1",
"openai": "^4.10.0",
"plaiceholder": "^2.5.0",
"prisma": "^4.11.0",
"prisma-json-schema-generator": "^3.1.3",
Expand Down
6 changes: 6 additions & 0 deletions prisma/json-schema/json-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@
"string",
"null"
]
},
"nextSuggestedDigestTitle": {
"type": [
"string",
"null"
]
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "teams" ADD COLUMN "nextSuggestedDigestTitle" TEXT;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ model Team {
github String?
createdAt DateTime @default(now())
apiKey String?
nextSuggestedDigestTitle String?
@@unique([id, slackTeamId])
@@unique(slug)
Expand Down
26 changes: 26 additions & 0 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';

// Create an OpenAI API client (that's edge friendly!)
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
});

// IMPORTANT! Set the runtime to edge
export const runtime = 'edge';

export async function POST(req: Request) {
// Extract the `messages` from the body of the request
const { messages } = await req.json();

// Ask OpenAI for a streaming chat completion given the prompt
const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
stream: true,
messages,
});
// Convert the response into a friendly text-stream
const stream = OpenAIStream(response);
// Respond with the stream
return new StreamingTextResponse(stream);
}
59 changes: 35 additions & 24 deletions src/components/digests/DigestCreateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import useCustomToast from '@/hooks/useCustomToast';
import useTransitionRefresh from '@/hooks/useTransitionRefresh';
import api from '@/lib/api';
Expand All @@ -14,12 +16,19 @@ import { Input } from '../Input';
type Props = {
teamId: string;
teamSlug: string;
predictedDigestTitle: string | null;
};

export const DigestCreateInput = ({ teamId, teamSlug }: Props) => {
export const DigestCreateInput = ({
teamId,
teamSlug,
predictedDigestTitle,
}: Props) => {
const router = useRouter();
const { successToast, errorToast } = useCustomToast();
const [newDigestTiltle, setNewDigestTitle] = useState('');
const [newDigestTiltle, setNewDigestTitle] = useState(
predictedDigestTitle ?? ''
);
const { isRefreshing, refresh } = useTransitionRefresh();

const { mutate: createDigest, isLoading } = useMutation<
Expand Down Expand Up @@ -58,28 +67,30 @@ export const DigestCreateInput = ({ teamId, teamSlug }: Props) => {
};

return (
<form
onSubmit={handleSubmit}
className={clsx('w-full flex', isRefreshing && 'opacity-80')}
>
<Input
className="px-4 rounded-r-none"
type="text"
placeholder="Digest name"
value={newDigestTiltle}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setNewDigestTitle(e.target.value)
}
required
/>
<Button
className="py-2 px-4 bg-violet-600 text-white border-violet-600 !rounded-l-none ring-0"
type="submit"
disabled={!newDigestTiltle || isLoading}
isLoading={isLoading || isRefreshing}
<div>
<form
onSubmit={handleSubmit}
className={clsx('w-full flex', isRefreshing && 'opacity-80')}
>
Create
</Button>
</form>
<Input
className="px-4 rounded-r-none"
type="text"
placeholder="Digest name"
value={newDigestTiltle}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setNewDigestTitle(e.target.value)
}
required
/>
<Button
className="py-2 px-4 bg-violet-600 text-white border-violet-600 !rounded-l-none ring-0"
type="submit"
disabled={!newDigestTiltle || isLoading}
isLoading={isLoading || isRefreshing}
>
Create
</Button>
</form>
</div>
);
};
6 changes: 5 additions & 1 deletion src/components/pages/Team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ const Team = ({ team, linkCount, teamLinks, digests, search }: Props) => {
}
>
<div className="flex flex-col gap-4 w-full">
<DigestCreateInput teamId={team.id} teamSlug={team.slug} />
<DigestCreateInput
teamId={team.id}
teamSlug={team.slug}
predictedDigestTitle={team?.nextSuggestedDigestTitle}
/>
<Digests digests={digests} teamId={team.id} teamSlug={team.slug} />
</div>
</Card>
Expand Down
35 changes: 34 additions & 1 deletion src/pages/api/teams/[teamId]/digests/[digestId]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AuthApiRequest, errorHandler } from '@/lib/router';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import urlSlug from 'url-slug';
import { openAiCompletion } from '@/utils/openai';

export type ApiDigestResponseSuccess = Digest;

Expand All @@ -18,7 +19,7 @@ router

try {
let digest = await client.digest.findUnique({
select: { publishedAt: true },
select: { publishedAt: true, teamId: true },
where: { id: digestId?.toString() },
});
if (req.body.title !== undefined && !req.body.title.trim()) {
Expand All @@ -27,6 +28,38 @@ router
error: 'Title cannot be empty',
});
}
const isFirstPublication = !digest?.publishedAt && !!req.body.publishedAt;

if (isFirstPublication) {
const lastDigests = await client.digest.findMany({
select: { title: true },
where: { teamId: digest?.teamId },
});

const lastDigestTitles = [
...lastDigests?.map((digest) => digest?.title),
req.body.title,
].filter((title) => !!title);

if (!!lastDigestTitles?.length) {
const prompt = `
Here is a list of document titles sorted from most recent to oldest, separared by ; signs : ${lastDigestTitles.join(
';'
)}
Just guess the next document title. Don't add any other sentence in your response.
`;

const response = await openAiCompletion(prompt);
const guessedTitle = response[0]?.message?.content;

await client.team.update({
where: { id: digest?.teamId },
data: {
nextSuggestedDigestTitle: guessedTitle,
},
});
}
}

digest = await client.digest.update({
where: {
Expand Down
14 changes: 14 additions & 0 deletions src/utils/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import OpenAI from 'openai';

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

export const openAiCompletion = async (prompt: string) => {
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: 'user', content: prompt }],
model: 'gpt-3.5-turbo',
});

return chatCompletion.choices;
};
Loading

1 comment on commit 0f304fd

@vercel
Copy link

@vercel vercel bot commented on 0f304fd Oct 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

digestclub – ./

digestclub-git-main-premieroctet.vercel.app
digestclub-premieroctet.vercel.app
digestclub.vercel.app

Please sign in to comment.