Skip to content

Commit

Permalink
feat: stream openai responses
Browse files Browse the repository at this point in the history
  • Loading branch information
altaywtf committed Dec 15, 2023
1 parent f5eb546 commit a4b4245
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 59 deletions.
22 changes: 22 additions & 0 deletions app/api/ai/summarize/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { OpenAI } from 'openai';

import { openai } from '@/lib/services/ai/openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';

export const runtime = 'edge';

export async function POST(req: Request) {
const { messages } = (await req.json()) as {
messages: OpenAI.Chat.ChatCompletionCreateParamsStreaming['messages'];
};

const response = await openai.chat.completions.create({
messages,
model: 'gpt-4-1106-preview',
stream: true,
});

const stream = OpenAIStream(response);

return new StreamingTextResponse(stream);
}
13 changes: 0 additions & 13 deletions components/episode-ai-thingy/episode-ai-summary-stream.tsx

This file was deleted.

57 changes: 57 additions & 0 deletions components/episode-ai-thingy/episode-ai-summary-streamer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Tables } from '@/types/supabase/database';

import { saveEpisodeContentSummary } from '@/lib/services/episode-content';
import { useChat } from 'ai/react';
import { useEffect, useRef } from 'react';

type Props = {
id: Tables<'episode'>['id'];
title: Tables<'episode'>['title'];
transcription: string;
};

export function EpisodeAISummaryStreamer({ id, title, transcription }: Props) {
const startedRef = useRef(false);

const { messages, reload, setMessages } = useChat({
api: '/api/ai/summarize',
onFinish: (message) => {
if (message.role === 'system') {
return;
}

void saveEpisodeContentSummary(id, message.content);
},
});

useEffect(() => {
if (startedRef.current) {
return;
}

startedRef.current = true;

setMessages([
{
content: `Summarize the podcast episode titled '${title}' in a short paragraph.`,
id: 'system-0',
role: 'system',
},
{
content: transcription,
id: 'system-1',
role: 'system',
},
]);

void reload();
}, [reload, setMessages, title, transcription]);

const summaryMessage = messages.find((message) => message.role !== 'system');

return (
<div>
{summaryMessage ? summaryMessage.content : 'Summarizing episode...'}
</div>
);
}
53 changes: 32 additions & 21 deletions components/episode-ai-thingy/episode-ai-thingy-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { useCallback, useState } from 'react';
import { FaExclamationTriangle } from 'react-icons/fa';
import { PiRobotBold } from 'react-icons/pi';

import { CollapsiblePanel } from '../ui/collapsible-panel';
import { Panel } from '../ui/panel';
import { EpisodeAISummaryStreamer } from './episode-ai-summary-streamer';
import { EpisodeAIThingyPlaceholder } from './episode-ai-thingy-placeholder';

type State =
Expand All @@ -33,6 +34,7 @@ type State =

export function EpisodeAIThingyGenerator({
id,
title,
}: {
id: Tables<'episode'>['id'];
title: Tables<'episode'>['title'];
Expand Down Expand Up @@ -60,11 +62,37 @@ export function EpisodeAIThingyGenerator({
}, [id]);

switch (state.status) {
case 'idle':
return (
<EpisodeAIThingyPlaceholder>
<Button highContrast onClick={generate} size="2">
<Flex align="center" gap="2" justify="center">
<Text mt="1" size="4" trim="both">
<PiRobotBold />
</Text>

<Text style={{ textTransform: 'uppercase' }} weight="bold">
Do the AI thingy!
</Text>
</Flex>
</Button>
</EpisodeAIThingyPlaceholder>
);

case 'transcribing':
return <Panel title="Episode summary">Transcribing episode...</Panel>;

case 'summarizing':
return (
<CollapsiblePanel open title="Episode transcription">
<Box style={{ whiteSpace: 'pre-wrap' }}>{state.transcription}</Box>
</CollapsiblePanel>
<Panel title="Episode summary">
<Box style={{ whiteSpace: 'pre-wrap' }}>
<EpisodeAISummaryStreamer
id={id}
title={title}
transcription={state.transcription}
/>
</Box>
</Panel>
);

case 'error':
Expand All @@ -79,22 +107,5 @@ export function EpisodeAIThingyGenerator({
</Callout.Root>
</EpisodeAIThingyPlaceholder>
);

default:
return (
<EpisodeAIThingyPlaceholder>
<Button highContrast onClick={generate} size="2">
<Flex align="center" gap="2" justify="center">
<Text mt="1" trim="both">
<PiRobotBold />
</Text>

<Text style={{ textTransform: 'uppercase' }} weight="medium">
{state.status}
</Text>
</Flex>
</Button>
</EpisodeAIThingyPlaceholder>
);
}
}
21 changes: 21 additions & 0 deletions components/ui/panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PropsWithChildren } from 'react';

import { Card, Heading, Text } from '@radix-ui/themes';

type Props = PropsWithChildren<{
title: string;
}>;

export function Panel({ children, title }: Props) {
return (
<Card>
<Heading mb="2" size="2">
{title}
</Heading>

<Text color="gray" size="2">
{children}
</Text>
</Card>
);
}
26 changes: 1 addition & 25 deletions lib/services/ai/openai.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,4 @@
import { env } from '@/env.mjs';
import { OpenAI } from 'openai';

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

export const summarizeEpisodeTranscript = async ({
title,
transcript,
}: {
title: string;
transcript: string;
}) => {
const response = await openai.chat.completions.create({
messages: [
{
content: `Summarize the following transcript from a podcast episode titled as ${title}`,
role: 'system',
},
{
content: transcript,
role: 'system',
},
],
model: 'gpt-4-1106-preview',
});

return response.choices[0].message.content;
};
export const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY });
31 changes: 31 additions & 0 deletions lib/services/episode-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use server';

import type { Tables } from '@/types/supabase/database';

import { cookies } from 'next/headers';

import { DatabaseError } from '../errors';
import { getAccountId } from './account';
import { createSupabaseServerClient } from './supabase/server';

export const saveEpisodeContentSummary = async (
id: Tables<'episode'>['id'],
summary: string,
) => {
const supabase = createSupabaseServerClient(cookies());

const { error } = await supabase.from('episode_content').upsert(
{
account: await getAccountId(),
episode: id,
text_summary: summary,
},
{
onConflict: 'episode',
},
);

if (error) {
throw new DatabaseError(error);
}
};

0 comments on commit a4b4245

Please sign in to comment.