Skip to content

Commit

Permalink
refactor: move summary i/o to endpoint (#85)
Browse files Browse the repository at this point in the history
- decreases the payload of requests sent from the client
- moves `saveEpisodeContentSummary` to the server, so that the client doesn't have to send the entire episode content to the server
  • Loading branch information
altaywtf authored Dec 16, 2023
1 parent c8bcac4 commit 0e2f2d9
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 59 deletions.
62 changes: 62 additions & 0 deletions app/api/ai/summarize/[episode-id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
DatabaseError,
HttpBadRequestError,
HttpInternalServerError,
} from '@/lib/errors';
import { openai } from '@/lib/services/ai/openai';
import { saveEpisodeContentSummary } from '@/lib/services/episode-content';
import { createSupabaseServerClient } from '@/lib/services/supabase/server';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { cookies } from 'next/headers';

export const runtime = 'edge';

export async function POST(
req: Request,
{ params }: { params: { 'episode-id': string } },
) {
const episodeId = parseInt(params['episode-id'], 10);

const supabase = createSupabaseServerClient(cookies());

const { data, error } = await supabase
.from('episode_content')
.select('transcription, episode(title)')
.eq('episode', episodeId)
.single();

if (error) {
return new HttpInternalServerError({
error: new DatabaseError(error),
}).toNextResponse();
}

if (!data.transcription) {
return new HttpBadRequestError({
message: 'Episode has no transcription',
}).toNextResponse();
}

const response = await openai.chat.completions.create({
messages: [
{
content: `Summarize the following transcription from ${data.episode?.title} episode of the podcast`,
role: 'system',
},
{
content: data.transcription,
role: 'system',
},
],
model: 'gpt-4-1106-preview',
stream: true,
});

const stream = OpenAIStream(response, {
onCompletion: async (completion) => {
await saveEpisodeContentSummary(episodeId, completion);
},
});

return new StreamingTextResponse(stream);
}
22 changes: 0 additions & 22 deletions app/api/ai/summarize/route.ts

This file was deleted.

13 changes: 3 additions & 10 deletions components/episode-ai-summary/episode-ai-summary-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,23 @@ type State =
}
| {
status: 'summarizing';
transcription: string;
}
| {
status: 'transcribing';
};

export function EpisodeAISummaryGenerator({
id,
title,
}: {
id: Tables<'episode'>['id'];
title: Tables<'episode'>['title'];
}) {
const [state, setState] = useState<State>({ status: 'idle' });

const generate = useCallback(async () => {
try {
setState({ status: 'transcribing' });
const transcription = await transcribeEpisode(id);
setState({ status: 'summarizing', transcription });
await transcribeEpisode(id);
setState({ status: 'summarizing' });
} catch (error) {
setState({ message: 'Failed to transcribe episode', status: 'error' });
}
Expand Down Expand Up @@ -73,11 +70,7 @@ export function EpisodeAISummaryGenerator({
case 'summarizing':
return (
<EpisodeAISummaryPanel>
<EpisodeAISummaryStreamer
id={id}
title={title}
transcription={state.transcription}
/>
<EpisodeAISummaryStreamer id={id} />
</EpisodeAISummaryPanel>
);

Expand Down
45 changes: 21 additions & 24 deletions components/episode-ai-summary/episode-ai-summary-streamer.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
'use client';

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

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

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

export function EpisodeAISummaryStreamer({ id, title, transcription }: Props) {
export function EpisodeAISummaryStreamer({ id }: 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);
},
api: `/api/ai/summarize/${id}`,
});

useEffect(() => {
Expand All @@ -33,21 +25,26 @@ export function EpisodeAISummaryStreamer({ id, title, transcription }: Props) {

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

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

const assistantMessages = messages.filter((m) => m.role === 'assistant');

const summaryMessage = messages.find((message) => message.role !== 'system');
if (assistantMessages.length === 0) {
return 'Summarizing episode...';
}

return summaryMessage ? summaryMessage.content : 'Summarizing episode...';
return (
<>
{assistantMessages.map((message) => (
<React.Fragment key={message.id}>{message.content}</React.Fragment>
))}
</>
);
}
3 changes: 1 addition & 2 deletions components/episode-ai-summary/episode-ai-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { EpisodeAISummaryPlaceholder } from './episode-ai-summary-placeholder';

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

export async function EpisodeAISummary(props: Props) {
Expand Down Expand Up @@ -42,7 +41,7 @@ export async function EpisodeAISummary(props: Props) {
);
}

return <EpisodeAISummaryGenerator id={props.id} title={props.title} />;
return <EpisodeAISummaryGenerator id={props.id} />;
}

const [episodeContent] = data;
Expand Down
2 changes: 1 addition & 1 deletion components/episode-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async function EpisodeDetailPage(props: { id: Tables<'episode'>['id'] }) {
show={data.show}
title={data.title}
>
<EpisodeAISummary id={data.id} title={data.title} />
<EpisodeAISummary id={data.id} />
</EpisodeDetailContent>
);
}
Expand Down

1 comment on commit 0e2f2d9

@vercel
Copy link

@vercel vercel bot commented on 0e2f2d9 Dec 16, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.