Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/ben 390 #148

Merged
merged 17 commits into from
Oct 18, 2024
Merged
51 changes: 50 additions & 1 deletion api/routers/chat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse

from api import schemas
Expand Down Expand Up @@ -80,3 +80,52 @@ async def get_thought(conversation_id: str, message_id: str, user_id: str):
)
# 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)

# Update the message metadata with the reaction
message = honcho.apps.users.sessions.messages.get(
app_id=app.id,
session_id=conversation_id,
user_id=user.id,
message_id=message_id
)

if not message:
raise HTTPException(status_code=404, detail="Message not found")

# Update the metadata
metadata = message.metadata or {}
metadata['reaction'] = reaction

honcho.apps.users.sessions.messages.update(
app_id=app.id,
session_id=conversation_id,
user_id=user.id,
message_id=message_id,
metadata=metadata
)

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)

message = honcho.apps.users.sessions.messages.get(
app_id=app.id,
session_id=conversation_id,
user_id=user.id,
message_id=message_id
)

if not message:
raise HTTPException(status_code=404, detail="Message not found")

reaction = message.metadata.get('reaction') if message.metadata else None
return {"reaction": reaction}
1 change: 1 addition & 0 deletions api/routers/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async def get_messages(user_id: str, conversation_id: uuid.UUID):
"id": message.id,
"content": message.content,
"isUser": message.is_user,
"metadata": message.metadata,
}
for message in honcho.apps.users.sessions.messages.list(
app_id=app.id, user_id=user.id, session_id=str(conversation_id)
Expand Down
84 changes: 70 additions & 14 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import dynamic from "next/dynamic";

import banner from "@/public/bloom2x1.svg";
import darkBanner from "@/public/bloom2x1dark.svg";
import MessageBox from "@/components/messagebox";
import Sidebar from "@/components/sidebar";
import MarkdownWrapper from "@/components/markdownWrapper";
import { DarkModeSwitch } from "react-toggle-dark-mode";
import { FaLightbulb, FaPaperPlane, FaBars } from "react-icons/fa";
import Swal from "sweetalert2";
Expand All @@ -20,9 +17,19 @@ import { usePostHog } from "posthog-js/react";
import { getSubscription } from "@/utils/supabase/queries";

import { API } from "@/utils/api";
import { Message } from "@/utils/api";
import { createClient } from "@/utils/supabase/client";

const Thoughts = dynamic(() => import("@/components/thoughts"));
import { Reaction } from "@/components/messagebox";

const Thoughts = dynamic(() => import("@/components/thoughts"), {
ssr: false,
});
const MessageBox = dynamic(() => import("@/components/messagebox"), {
ssr: false,
});
const Sidebar = dynamic(() => import("@/components/sidebar"), {
ssr: false,
});

const URL = process.env.NEXT_PUBLIC_API_URL;

Expand Down Expand Up @@ -79,11 +86,9 @@ export default function Home() {
const sub = await getSubscription(supabase);
setIsSubscribed(!!sub);
}

})();
}, [supabase, posthog, userId]);


useEffect(() => {
const messageContainer = messageContainerRef.current;
if (!messageContainer) return;
Expand Down Expand Up @@ -135,6 +140,44 @@ export default function Home() {
error: _,
} = useSWR(conversationId, messagesFetcher, { revalidateOnFocus: false });

const handleReactionAdded = async (
messageId: string,
reaction: Exclude<Reaction, null>,
) => {
if (!userId || !conversationId) return;

const api = new API({ url: URL!, userId });

try {
await api.addReaction(conversationId, messageId, reaction);

// Optimistically update the local data
mutateMessages((currentMessages) => {
if (!currentMessages) return currentMessages;
return currentMessages.map((msg) => {
console.log(`msgs: ${JSON.stringify(currentMessages)}`);
if (msg.id === messageId) {
return {
...msg,
metadata: {
...msg.metadata,
reaction,
},
};
}
console.log(`after update: ${JSON.stringify(currentMessages)}`);
return msg;
});
}, false); // Set to false to avoid revalidation immediately

// Trigger a revalidation to ensure data consistency
mutateMessages();
bLopata marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
console.error("Failed to add reaction:", error);
// Optionally, you can show an error message to the user
}
};

async function chat() {
if (!isSubscribed) {
Swal.fire({
Expand Down Expand Up @@ -204,7 +247,6 @@ export default function Home() {
isThinking = false;
continue;
}
console.log(value)
setThought((prev) => prev + value);
} else {
if (value.includes("❀")) {
Expand Down Expand Up @@ -300,20 +342,29 @@ export default function Home() {
isUser={message.isUser}
userId={userId}
URL={URL}
messageId={message.id}
text={message.text}
message={message}
loading={messagesLoading}
conversationId={conversationId}
setThought={setThought}
setIsThoughtsOpen={setIsThoughtsOpen}
onReactionAdded={handleReactionAdded}
/>
)) || (
<MessageBox
isUser={false}
text=""
message={{
text: "",
id: "",
isUser: false,
metadata: { reaction: null },
}}
loading={true}
setThought={setThought}
setIsThoughtsOpen={setIsThoughtsOpen}
onReactionAdded={handleReactionAdded}
userId={userId}
URL={URL}
conversationId={conversationId}
/>
)}
</section>
Expand All @@ -331,9 +382,14 @@ export default function Home() {
{/* TODO: validate input */}
<textarea
ref={input}
placeholder={isSubscribed ? "Type a message..." : "Subscribe to send messages"}
className={`flex-1 px-3 py-1 lg:px-5 lg:py-3 bg-gray-100 dark:bg-gray-800 text-gray-400 rounded-2xl border-2 resize-none ${canSend && isSubscribed ? "border-green-200" : "border-red-200 opacity-50"
}`}
placeholder={
isSubscribed ? "Type a message..." : "Subscribe to send messages"
}
className={`flex-1 px-3 py-1 lg:px-5 lg:py-3 bg-gray-100 dark:bg-gray-800 text-gray-400 rounded-2xl border-2 resize-none ${
canSend && isSubscribed
? "border-green-200"
: "border-red-200 opacity-50"
}`}
rows={1}
disabled={!isSubscribed}
onKeyDown={(e) => {
Expand Down
83 changes: 68 additions & 15 deletions www/components/messagebox.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,64 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import Image from "next/image";
import icon from "@/public/bloomicon.jpg";
import usericon from "@/public/usericon.svg";
import Skeleton from "react-loading-skeleton";
import { FaLightbulb } from "react-icons/fa";
import { API } from "@/utils/api";
import { FaLightbulb, FaThumbsDown, FaThumbsUp } from "react-icons/fa";
import { API, type Message } from "@/utils/api";
import Spinner from "./spinner";

export type Reaction = "thumbs_up" | "thumbs_down" | null;

interface MessageBoxProps {
isUser?: boolean;
userId?: string;
URL?: string;
messageId?: string;
conversationId?: string;
text: string;
message: Message;
loading?: boolean;
isThoughtsOpen?: boolean;
setIsThoughtsOpen: (isOpen: boolean) => void;
setThought: (thought: string) => void;
onReactionAdded: (
messageId: string,
reaction: Exclude<Reaction, null>,
) => Promise<void>;
}

export default function MessageBox({
isUser,
userId,
URL,
messageId,
text,
message,
loading = false,
setIsThoughtsOpen,
conversationId,
onReactionAdded,
setThought,
}: MessageBoxProps) {
const [isThoughtLoading, setIsThoughtLoading] = useState(false);
const [pendingReaction, setPendingReaction] = useState<Reaction>(null);
const [error, setError] = useState<string | null>(null);

const { id: messageId, text, metadata } = message;
const reaction = metadata?.reaction || null;
const shouldShowButtons = messageId !== "";

const handleReaction = async (newReaction: Exclude<Reaction, null>) => {
if (!messageId || !conversationId || !userId || !URL) return;

setPendingReaction(newReaction);

try {
await onReactionAdded(messageId, newReaction);
} catch (err) {
console.error(err);
setError("Failed to add reaction.");
} finally {
setPendingReaction(null);
}
};

const handleFetchThought = async () => {
if (!messageId || !conversationId || !userId || !URL) return;

Expand Down Expand Up @@ -82,23 +106,52 @@ export default function MessageBox({
<div className="message-content">{text}</div>
)}
{!loading && !isUser && shouldShowButtons && (
<div className="flex justify-left gap-2 mt-2">
{/* <button className="p-2 rounded-full bg-gray-200 dark:bg-gray-700">
<FaThumbsUp />
<div className="flex justify-start gap-2 mt-2">
<button
className={`p-2 rounded-full ${
reaction === "thumbs_up"
? "bg-blue-500 text-white"
: "bg-gray-200 dark:bg-gray-700"
} ${pendingReaction === "thumbs_up" ? "opacity-50" : ""}`}
onClick={() => handleReaction("thumbs_up")}
disabled={reaction !== null || pendingReaction !== null}
>
<div className="w-5 h-5 flex items-center justify-center">
{pendingReaction === "thumbs_up" ? (
<Spinner size={16} />
) : (
<FaThumbsUp />
)}
</div>
</button>
<button
className={`p-2 rounded-full ${
reaction === "thumbs_down"
? "bg-red-500 text-white"
: "bg-gray-200 dark:bg-gray-700"
} ${pendingReaction === "thumbs_down" ? "opacity-50" : ""}`}
onClick={() => handleReaction("thumbs_down")}
disabled={reaction !== null || pendingReaction !== null}
>
bLopata marked this conversation as resolved.
Show resolved Hide resolved
<div className="w-5 h-5 flex items-center justify-center">
{pendingReaction === "thumbs_down" ? (
<Spinner size={16} />
) : (
<FaThumbsDown />
)}
</div>
</button>
<button className="p-2 rounded-full bg-gray-200 dark:bg-gray-700">
<FaThumbsDown />
</button> */}
<button
className="p-2 rounded-full bg-gray-200 dark:bg-gray-700"
onClick={handleFetchThought}
disabled={isThoughtLoading}
>
<FaLightbulb />
<div className="w-5 h-5 flex items-center justify-center">
{isThoughtLoading ? <Spinner size={16} /> : <FaLightbulb />}
</div>
</button>
</div>
)}
{isThoughtLoading && <p>Loading thought...</p>}
{error && <p className="text-red-500">Error: {error}</p>}
</div>
</article>
Expand Down
26 changes: 26 additions & 0 deletions www/components/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { FaCircleNotch } from "react-icons/fa";

const Spinner = ({ size = 24, color = "#000000" }) => {
const spinnerStyle = {
animation: "spin 1s linear infinite",
color: color,
fontSize: `${size}px`,
};

return (
<div style={{ display: "inline-block" }}>
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
<FaCircleNotch style={spinnerStyle} />
</div>
);
};

export default Spinner;
Loading
Loading