From 5a7f14f7cfb4aba942e7d8dff91327c4341cdcc6 Mon Sep 17 00:00:00 2001 From: hyusap Date: Sun, 12 Jan 2025 18:23:05 -0500 Subject: [PATCH] add auto chat naming --- www/app/Chat.tsx | 268 +++++++++++++++++------------- www/app/actions/conversations.ts | 6 +- www/app/api/chat/summary/route.ts | 28 ++++ www/utils/ai.ts | 30 +++- www/utils/prompts/summary.ts | 17 ++ www/utils/types.ts | 2 +- 6 files changed, 230 insertions(+), 121 deletions(-) create mode 100644 www/app/api/chat/summary/route.ts create mode 100644 www/utils/prompts/summary.ts diff --git a/www/app/Chat.tsx b/www/app/Chat.tsx index d2a6e9b..9e112d1 100644 --- a/www/app/Chat.tsx +++ b/www/app/Chat.tsx @@ -15,9 +15,13 @@ import { Reaction } from '@/components/messagebox'; import { FiMenu } from 'react-icons/fi'; import Link from 'next/link'; import { getFreeMessageCount, useFreeTrial } from '@/utils/supabase/actions'; -import { getConversations, createConversation } from './actions/conversations'; +import { + getConversations, + createConversation, + updateConversation, +} from './actions/conversations'; import { getMessages, addOrRemoveReaction } from './actions/messages'; -import { type Message } from '@/utils/types'; +import { Conversation, Message } from '@/utils/types'; import { localStorageProvider } from '@/utils/swrCache'; import useAutoScroll from '@/hooks/autoscroll'; @@ -28,9 +32,6 @@ const Thoughts = dynamic(() => import('@/components/thoughts'), { ssr: false, }); -const MessageBox = dynamic(() => import('@/components/messagebox'), { - ssr: false, -}); const Sidebar = dynamic(() => import('@/components/sidebar'), { ssr: false, }); @@ -88,13 +89,13 @@ async function fetchStream( interface ChatProps { initialUserId: string; initialEmail: string | undefined; - initialConversations: any[]; + initialConversations: Conversation[]; initialChatAccess: { isSubscribed: boolean; freeMessages: number; canChat: boolean; }; - initialMessages: any[]; + initialMessages: Message[]; initialConversationId: string | null | undefined; } @@ -111,9 +112,7 @@ export default function Chat({ initialChatAccess, }: ChatProps) { const [userId] = useState(initialUserId); - const [isSubscribed, setIsSubscribed] = useState( - initialChatAccess.isSubscribed - ); + const [isSubscribed] = useState(initialChatAccess.isSubscribed); const [freeMessages, setFreeMessages] = useState( initialChatAccess.freeMessages ); @@ -134,7 +133,7 @@ export default function Chat({ const posthog = usePostHog(); const input = useRef>(null); const messageContainerRef = useRef>(null); - const [, scrollToBottom] = useAutoScroll(messageContainerRef); + useAutoScroll(messageContainerRef); const messageListRef = useRef(null); const firstChat = useMemo(() => { @@ -157,7 +156,7 @@ export default function Chat({ The more we chat, the more I learn about you as a person. That helps me adapt to your interests and needs. -What\'s on your mind? Let\'s dive in. 🌱`, +What's on your mind? Let's dive in. 🌱`, isUser: false, id: '', metadata: {}, @@ -245,7 +244,7 @@ What\'s on your mind? Let\'s dive in. 🌱`, revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 60000, - onSuccess: (data) => { + onSuccess: () => { if (conversationId?.startsWith('temp-')) { mutateMessages([], false); } @@ -282,101 +281,84 @@ What\'s on your mind? Let\'s dive in. 🌱`, } }; - async function chat(message?: string) { - const rawMessage = message || input.current?.value; - if (!userId || !rawMessage) return; - - // Process message to have double newline for markdown - const messageToSend = rawMessage.replace(/\n/g, '\n\n'); - - if (input.current) input.current.value = ''; - - setCanSend(false); - - const newMessages = [ - ...messages!, - { - content: messageToSend, - isUser: true, - id: '', - metadata: {}, - }, - { - content: '', - isUser: false, - id: '', - metadata: {}, - }, - ]; - await mutateMessages(newMessages, { revalidate: false }); - messageListRef.current?.scrollToBottom(); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - let thoughtReader: ReadableStreamDefaultReader | null = null; - let responseReader: ReadableStreamDefaultReader | null = null; - - try { - // Get thought stream - const thoughtStream = await fetchStream( - 'thought', - messageToSend, - conversationId! - ); + async function processThought(messageToSend: string, conversationId: string) { + // Get thought stream + const thoughtStream = await fetchStream( + 'thought', + messageToSend, + conversationId + ); - if (!thoughtStream) { - throw new Error('Failed to get thought stream'); - } + if (!thoughtStream) { + throw new Error('Failed to get thought stream'); + } - thoughtReader = thoughtStream.getReader(); - let thoughtText = ''; - setThought(''); + const thoughtReader = thoughtStream.getReader(); + let thoughtText = ''; + setThought(''); - // Process thought stream - while (true) { - const { done, value } = await thoughtReader.read(); - if (done) break; + // Process thought stream + while (true) { + const { done, value } = await thoughtReader.read(); + if (done) break; - thoughtText += new TextDecoder().decode(value); - setThought(thoughtText); - } + thoughtText += new TextDecoder().decode(value); + setThought(thoughtText); + } - // Cleanup thought stream - thoughtReader.releaseLock(); - thoughtReader = null; + // Cleanup thought stream + thoughtReader.releaseLock(); - const honchoResponse = await fetchStream( - 'honcho', - messageToSend, - conversationId!, - thoughtText - ); + return thoughtText; + } - const honchoContent = (await new Response( - honchoResponse - ).json()) as HonchoResponse; + async function processHoncho( + messageToSend: string, + conversationId: string, + thoughtText: string + ) { + // Get honcho response + const honchoResponse = await fetchStream( + 'honcho', + messageToSend, + conversationId, + thoughtText + ); - const pureThought = thoughtText; + const honchoContent = (await new Response( + honchoResponse + ).json()) as HonchoResponse; - thoughtText += - '\n\nHoncho Dialectic Response:\n\n' + honchoContent.content; - setThought(thoughtText); + const updatedThought = + thoughtText + + '\n\nHoncho Dialectic Response:\n\n' + + honchoContent.content; + setThought(updatedThought); - await new Promise((resolve) => setTimeout(resolve, 2000)); + return honchoContent; + } - // Get response stream using the thought and dialectic response - const responseStream = await fetchStream( - 'response', - messageToSend, - conversationId!, - pureThought, - honchoContent.content - ); - if (!responseStream) throw new Error('Failed to get response stream'); + async function processResponse( + messageToSend: string, + conversationId: string, + thoughtText: string, + honchoContent: string, + newMessages: Message[], + isSubscribed: boolean + ) { + const responseStream = await fetchStream( + 'response', + messageToSend, + conversationId, + thoughtText, + honchoContent + ); + if (!responseStream) throw new Error('Failed to get response stream'); - responseReader = responseStream.getReader(); - let currentModelOutput = ''; + const responseReader = responseStream.getReader(); + let currentModelOutput = ''; + try { // Process response stream while (true) { const { done, value } = await responseReader.read(); @@ -408,9 +390,85 @@ What\'s on your mind? Let\'s dive in. 🌱`, messageListRef.current?.scrollToBottom(); } - + } finally { responseReader.releaseLock(); - responseReader = null; + } + } + + async function processSummary(messageToSend: string, conversationId: string) { + const summaryResponse = await fetch('/api/chat/summary', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: messageToSend, + }), + }); + + if (summaryResponse.ok) { + const { summary } = await summaryResponse.json(); + await updateConversation(conversationId, summary); + await mutateConversations(); + } + } + + async function chat(message?: string) { + const rawMessage = message || input.current?.value; + if (!userId || !rawMessage) return; + + // Process message to have double newline for markdown + const messageToSend = rawMessage.replace(/\n/g, '\n\n'); + + if (input.current) input.current.value = ''; + + setCanSend(false); + + const newMessages = [ + ...messages!, + { + content: messageToSend, + isUser: true, + id: '', + metadata: {}, + }, + { + content: '', + isUser: false, + id: '', + metadata: {}, + }, + ]; + await mutateMessages(newMessages, { revalidate: false }); + messageListRef.current?.scrollToBottom(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + try { + // Process thought and summary in parallel if this is the first message + const [thoughtText] = await Promise.all([ + processThought(messageToSend, conversationId!), + ...(messages?.length === 0 + ? [processSummary(messageToSend, conversationId!)] + : []), + ]); + + // Process honcho response + const honchoContent = await processHoncho( + messageToSend, + conversationId!, + thoughtText + ); + + // Process response + await processResponse( + messageToSend, + conversationId!, + thoughtText, + honchoContent.content, + newMessages, + isSubscribed + ); await mutateMessages(); messageListRef.current?.scrollToBottom(); @@ -420,22 +478,6 @@ What\'s on your mind? Let\'s dive in. 🌱`, setCanSend(true); await mutateMessages(); messageListRef.current?.scrollToBottom(); - } finally { - // Cleanup - if (thoughtReader) { - try { - thoughtReader.releaseLock(); - } catch (e) { - console.error('Error releasing thought reader:', e); - } - } - if (responseReader) { - try { - responseReader.releaseLock(); - } catch (e) { - console.error('Error releasing response reader:', e); - } - } } } diff --git a/www/app/actions/conversations.ts b/www/app/actions/conversations.ts index b8b2fce..0e3fc44 100644 --- a/www/app/actions/conversations.ts +++ b/www/app/actions/conversations.ts @@ -3,14 +3,10 @@ import { createClient } from '@/utils/supabase/server'; import { honcho, getHonchoApp, getHonchoUser } from '@/utils/honcho'; import * as Sentry from '@sentry/nextjs'; +import { Conversation } from '@/utils/types'; // TODO add proper authorization check -type Conversation = { - conversationId: string; - name: string; -}; - export async function getConversations() { return Sentry.startSpan( { name: 'server-action.getConversations', op: 'server.action' }, diff --git a/www/app/api/chat/summary/route.ts b/www/app/api/chat/summary/route.ts new file mode 100644 index 0000000..0829383 --- /dev/null +++ b/www/app/api/chat/summary/route.ts @@ -0,0 +1,28 @@ +import { createCompletion, getUserData, user } from '@/utils/ai'; +import { summaryPrompt } from '@/utils/prompts/summary'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export async function POST(req: NextRequest) { + const { message } = await req.json(); + + const userData = await getUserData(); + if (!userData) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + const { userId } = userData; + + const finalMessage = user`${message}`; + const prompt = [...summaryPrompt, finalMessage]; + + const completion = await createCompletion(prompt, { + sessionId: 'summary', + userId, + type: 'summary', + }); + + return NextResponse.json({ summary: completion.text }); +} diff --git a/www/utils/ai.ts b/www/utils/ai.ts index 2092896..a610f2b 100644 --- a/www/utils/ai.ts +++ b/www/utils/ai.ts @@ -1,7 +1,7 @@ -import { getHonchoApp, getHonchoUser, honcho } from '@/utils/honcho'; +import { getHonchoApp, getHonchoUser } from '@/utils/honcho'; import { createClient } from '@/utils/supabase/server'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; -import { streamText } from 'ai'; +import { generateText, streamText } from 'ai'; import d from 'dedent-js'; import * as Sentry from '@sentry/nextjs'; @@ -197,3 +197,29 @@ export async function createStream( throw error; } } + +export async function createCompletion( + messages: Message[], + metadata: { + sessionId: string; + userId: string; + type: string; + } +) { + const result = generateText({ + model: openrouter(MODEL), + messages, + experimental_telemetry: { + isEnabled: true, + metadata: { + sessionId: metadata.sessionId, + userId: metadata.userId, + release: SENTRY_RELEASE, + environment: SENTRY_ENVIRONMENT, + tags: [metadata.type], + }, + }, + }); + + return result; +} diff --git a/www/utils/prompts/summary.ts b/www/utils/prompts/summary.ts new file mode 100644 index 0000000..620ba1c --- /dev/null +++ b/www/utils/prompts/summary.ts @@ -0,0 +1,17 @@ +import { Message, user, assistant } from '@/utils/ai'; +export const summaryPrompt: Message[] = [ + user`Your task is to create a 5-word or less summary of the conversation topic, starting with an action verb. + + Rules: + 1. Must start with an action verb + 2. Maximum 5 words + 3. Be specific but concise + 4. Focus on the core topic/goal + + Does that make sense?`, + assistant`Yes, it makes sense. Send the first message whenever you're ready.`, + user`I want to learn about quantum physics and understand the basic principles behind quantum mechanics`, + assistant`Exploring quantum physics fundamentals`, + user`Can you help me write a poem about love and loss? I want it to be meaningful and touching`, + assistant`Crafting emotional love poetry`, +]; diff --git a/www/utils/types.ts b/www/utils/types.ts index a403b4b..81f5b75 100644 --- a/www/utils/types.ts +++ b/www/utils/types.ts @@ -1,4 +1,4 @@ -import { type Reaction } from '@/components/messagebox'; +// import { type Reaction } from '@/components/messagebox'; export interface Message { id: string;