From c2b5d9b491e3421a6d3ba06015bf1c09e4eb66bb Mon Sep 17 00:00:00 2001 From: Ben Lopata Date: Mon, 7 Oct 2024 13:39:25 -0500 Subject: [PATCH 01/18] Adds like/dislike message functionality --- api/routers/chat.py | 65 +++++++++++++++- www/components/messagebox.tsx | 140 ++++++++++++++++++++++++++++++++++ www/utils/api.ts | 97 ++++++++++++++++++++++- 3 files changed, 300 insertions(+), 2 deletions(-) diff --git a/api/routers/chat.py b/api/routers/chat.py index e73d15a..c66a5f8 100644 --- a/api/routers/chat.py +++ b/api/routers/chat.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from api import schemas @@ -67,4 +67,67 @@ def convo_turn(): content=response, ) +<<<<<<< Updated upstream return StreamingResponse(convo_turn()) +======= +@router.get("/thought/{message_id}") +async def get_thought(conversation_id: str, message_id: str, user_id: str): + user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id) + thought = honcho.apps.users.sessions.metamessages.list( + session_id=conversation_id, + app_id=app.id, + user_id=user.id, + message_id=message_id, + metamessage_type="thought" + ) + # In practice, there should only be one thought per message + return {"thought": thought.items[0].content if thought.items else None} + +@router.post("/reaction/{message_id}") +async def add_reaction(conversation_id: str, message_id: str, user_id: str, reaction: str): + if reaction not in ["thumbs_up", "thumbs_down"]: + raise HTTPException(status_code=400, detail="Invalid reaction type") + + user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id) + + # Check if a reaction already exists + existing_reaction = honcho.apps.users.sessions.metamessages.list( + session_id=conversation_id, + app_id=app.id, + user_id=user.id, + message_id=message_id, + metamessage_type="reaction" + ) + + if existing_reaction.items: + return {"status": "Reaction already exists"} + + # Create new reaction + honcho.apps.users.sessions.metamessages.create( + app_id=app.id, + session_id=conversation_id, + user_id=user.id, + message_id=message_id, + metamessage_type="reaction", + content=reaction + ) + + return {"status": "Reaction added successfully"} + +@router.get("/reaction/{message_id}") +async def get_reaction(conversation_id: str, message_id: str, user_id: str): + user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id) + + existing_reaction = honcho.apps.users.sessions.metamessages.list( + session_id=conversation_id, + app_id=app.id, + user_id=user.id, + message_id=message_id, + metamessage_type="reaction" + ) + + if existing_reaction.items: + return {"reaction": existing_reaction.items[0].content} + else: + return {"reaction": None} +>>>>>>> Stashed changes diff --git a/www/components/messagebox.tsx b/www/components/messagebox.tsx index fda94d8..0102e16 100644 --- a/www/components/messagebox.tsx +++ b/www/components/messagebox.tsx @@ -1,7 +1,16 @@ +<<<<<<< Updated upstream +======= +import { useEffect, useState } from "react"; +>>>>>>> Stashed changes import Image from "next/image"; import icon from "@/public/bloomicon.jpg"; import usericon from "@/public/usericon.svg"; import Skeleton from "react-loading-skeleton"; +<<<<<<< Updated upstream +======= +import { FaLightbulb, FaThumbsDown, FaThumbsUp } from "react-icons/fa"; +import { API } from "@/utils/api"; +>>>>>>> Stashed changes interface MessageBoxRegularProps { children: React.ReactNode; @@ -9,6 +18,7 @@ interface MessageBoxRegularProps { loading?: false; } +<<<<<<< Updated upstream interface MessageBoxLoadingProps { children?: React.ReactNode; isUser?: boolean; @@ -17,12 +27,92 @@ interface MessageBoxLoadingProps { // merge the two types type MessageBoxProps = MessageBoxRegularProps | MessageBoxLoadingProps; +======= +export type Reaction = "thumbs_up" | "thumbs_down" | null; +>>>>>>> Stashed changes export default function MessageBox({ children, isUser, loading, }: MessageBoxProps) { +<<<<<<< Updated upstream +======= + const [isThoughtLoading, setIsThoughtLoading] = useState(false); + const [error, setError] = useState(null); + const [reaction, setReaction] = useState(null); + const [isReactionLoading, setIsReactionLoading] = useState(false); + const [isReactionPending, setIsReactionPending] = useState(false); + const shouldShowButtons = messageId !== ""; + + useEffect(() => { + if (shouldShowButtons && !isUser) { + fetchExistingReaction(); + } + }, [messageId, conversationId, userId, URL]); + + const fetchExistingReaction = async () => { + if (!messageId || !conversationId || !userId || !URL) return; + + setIsReactionLoading(true); + try { + const api = new API({ url: URL, userId }); + const { reaction: existingReaction } = await api.getReaction( + conversationId, + messageId, + ); + setReaction(existingReaction); + } catch (err) { + console.error(err); + setError("Failed to fetch existing reaction."); + } finally { + setIsReactionLoading(false); + } + }; + + const handleReaction = async (newReaction: Exclude) => { + if (!messageId || !conversationId || !userId || !URL) return; + + setReaction(newReaction); + setIsReactionPending(true); + + try { + const api = new API({ url: URL, userId }); + await api.addReaction(conversationId, messageId, newReaction); + } catch (err) { + console.error(err); + setError("Failed to add reaction."); + setReaction(null); + } finally { + setIsReactionPending(false); + } + }; + + const handleFetchThought = async () => { + if (!messageId || !conversationId || !userId || !URL) return; + + setIsThoughtLoading(true); + setError(null); + + try { + const api = new API({ url: URL, userId }); + const thought = await api.getThoughtById(conversationId, messageId); + + if (thought) { + setIsThoughtsOpen(true); + setThought(thought); + } else { + setError("No thought found."); + } + } catch (err) { + setError("Failed to fetch thought."); + console.error(err); + } finally { + setIsThoughtLoading(false); + } + }; + +>>>>>>> Stashed changes return (
)}
+<<<<<<< Updated upstream {loading ? : children} {/* */} +======= + {loading ? ( + + ) : ( +
{text}
+ )} + {!loading && !isUser && shouldShowButtons && ( +
+ + + +
+ )} + {isReactionPending && ( +

Saving reaction...

+ )} + {isThoughtLoading &&

Loading thought...

} + {error &&

Error: {error}

} +>>>>>>> Stashed changes
); diff --git a/www/utils/api.ts b/www/utils/api.ts index 0432647..f2df82e 100644 --- a/www/utils/api.ts +++ b/www/utils/api.ts @@ -1,5 +1,7 @@ +import { Reaction } from "@/components/messagebox"; + const defaultMessage: Message = { - text: `I'm your Aristotelian learning companion — here to help you follow your curiosity in whatever direction you like. My engineering makes me extremely receptive to your needs and interests. You can reply normally, and I’ll always respond!\n\nIf I'm off track, just say so!\n\nNeed to leave or just done chatting? Let me know! I’m conversational by design so I’ll say goodbye 😊.`, + text: `I'm your Aristotelian learning companion — here to help you follow your curiosity in whatever direction you like. My engineering makes me extremely receptive to your needs and interests. You can reply normally, and I’ll always respond!\n\nIf I'm off track, just say so!\n\nNeed to leave or just done chatting? Let me know! I’m conversational by design so I’ll say goodbye 😊.`, isUser: false, }; @@ -156,4 +158,97 @@ export class API { return [defaultMessage, ...messages]; } +<<<<<<< Updated upstream +======= + + async getThoughtById( + conversationId: string, + messageId: string, + ): Promise { + try { + const response = await fetch( + `${this.url}/api/thought/${messageId}?user_id=${this.userId}&conversation_id=${conversationId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch thought"); + } + + const data = await response.json(); + return data.thought; + } catch (error) { + console.error("Error fetching thought:", error); + return null; + } + } + + async addReaction( + conversationId: string, + messageId: string, + reaction: Exclude, + ): Promise<{ status: string }> { + try { + const response = await fetch( + `${this.url}/api/reaction/${messageId}?user_id=${this.userId}&conversation_id=${conversationId}&reaction=${reaction}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to add reaction"); + } + + return await response.json(); + } catch (error) { + console.error("Error adding reaction:", error); + throw error; + } + } + + async getReaction( + conversationId: string, + messageId: string, + ): Promise<{ reaction: Reaction }> { + try { + const response = await fetch( + `${this.url}/api/reaction/${messageId}?user_id=${this.userId}&conversation_id=${conversationId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to get reaction"); + } + + const data = await response.json(); + + // Validate the reaction + if ( + data.reaction !== null && + !["thumbs_up", "thumbs_down"].includes(data.reaction) + ) { + throw new Error("Invalid reaction received from server"); + } + + return data as { reaction: Reaction }; + } catch (error) { + console.error("Error getting reaction:", error); + throw error; + } + } +>>>>>>> Stashed changes } From 54c7c0405914d21ebb1ecb148e51b2280035d9d6 Mon Sep 17 00:00:00 2001 From: Ben Lopata Date: Mon, 7 Oct 2024 13:57:24 -0500 Subject: [PATCH 02/18] Formatting. --- www/components/messagebox.tsx | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/www/components/messagebox.tsx b/www/components/messagebox.tsx index 55b8a54..22855dc 100644 --- a/www/components/messagebox.tsx +++ b/www/components/messagebox.tsx @@ -39,30 +39,32 @@ export default function MessageBox({ const [isReactionPending, setIsReactionPending] = useState(false); const shouldShowButtons = messageId !== ""; + useEffect(() => { if (shouldShowButtons && !isUser) { + const fetchExistingReaction = async () => { + if (!messageId || !conversationId || !userId || !URL) return; + + setIsReactionLoading(true); + try { + const api = new API({ url: URL, userId }); + const { reaction: existingReaction } = await api.getReaction( + conversationId, + messageId, + ); + setReaction(existingReaction); + } catch (err) { + console.error(err); + setError("Failed to fetch existing reaction."); + } finally { + setIsReactionLoading(false); + } + }; fetchExistingReaction(); } - }, [messageId, conversationId, userId, URL]); + }, [messageId, conversationId, userId, URL, isUser, shouldShowButtons]); - const fetchExistingReaction = async () => { - if (!messageId || !conversationId || !userId || !URL) return; - setIsReactionLoading(true); - try { - const api = new API({ url: URL, userId }); - const { reaction: existingReaction } = await api.getReaction( - conversationId, - messageId, - ); - setReaction(existingReaction); - } catch (err) { - console.error(err); - setError("Failed to fetch existing reaction."); - } finally { - setIsReactionLoading(false); - } - }; const handleReaction = async (newReaction: Exclude) => { if (!messageId || !conversationId || !userId || !URL) return; @@ -131,8 +133,8 @@ export default function MessageBox({