Skip to content

Commit

Permalink
Merge pull request #148 from plastic-labs/dev/ben-390
Browse files Browse the repository at this point in the history
Dev/ben 390
  • Loading branch information
bLopata authored Oct 18, 2024
2 parents 01c1716 + 5fcd591 commit cd2f445
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 37 deletions.
48 changes: 47 additions & 1 deletion api/routers/chat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from fastapi import APIRouter
from typing import Optional
from fastapi import APIRouter, HTTPException, Body
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

from api import schemas
from api.dependencies import app, honcho
Expand Down Expand Up @@ -80,3 +82,47 @@ 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}

class ReactionBody(BaseModel):
reaction: Optional[str] = None

@router.post("/reaction/{message_id}")
async def add_or_remove_reaction(
conversation_id: str,
message_id: str,
user_id: str,
body: ReactionBody
):
reaction = body.reaction

if reaction is not None and 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)

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")

metadata = message.metadata or {}

if reaction is None:
metadata.pop('reaction', None)
else:
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 updated successfully"}
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
73 changes: 58 additions & 15 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ import useSWR from 'swr';

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';
import banner from "@/public/bloom2x1.svg";
import darkBanner from "@/public/bloom2x1dark.svg";
import { DarkModeSwitch } from "react-toggle-dark-mode";
import { FaLightbulb, FaPaperPlane, FaBars } from "react-icons/fa";
import Swal from "sweetalert2";

import { useRef, useEffect, useState, ElementRef } from 'react';
import { redirect } from 'next/navigation';
import { usePostHog } from 'posthog-js/react';

import { getSubscription } from '@/utils/supabase/queries';

import { API } from '@/utils/api';
import { createClient } from '@/utils/supabase/client';
import { API } from "@/utils/api";
import { createClient } from "@/utils/supabase/client";
import { Reaction } from "@/components/messagebox";

const Thoughts = dynamic(() => import('@/components/thoughts'));
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 @@ -133,6 +139,35 @@ export default function Home() {
error: _,
} = useSWR(conversationId, messagesFetcher, { revalidateOnFocus: false });

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

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

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

// Optimistically update the local data
mutateMessages((currentMessages) => {
if (!currentMessages) return currentMessages;
return currentMessages.map((msg) => {
if (msg.id === messageId) {
return {
...msg,
metadata: {
...msg.metadata,
reaction,
},
};
}
return msg;
});
}, true);
} catch (error) {
console.error("Failed to update reaction:", error);
}
};

async function chat() {
if (!isSubscribed) {
Swal.fire({
Expand Down Expand Up @@ -202,7 +237,6 @@ export default function Home() {
isThinking = false;
continue;
}
console.log(value);
setThought((prev) => prev + value);
} else {
if (value.includes('❀')) {
Expand Down Expand Up @@ -297,20 +331,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 Down
92 changes: 72 additions & 20 deletions www/components/messagebox.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,64 @@
import { 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 { 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, 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: Reaction) => 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 shouldShowButtons = messageId !== '';
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 {
const reactionToSend = reaction === newReaction ? null : newReaction;
await onReactionAdded(
messageId,
reactionToSend as Exclude<Reaction, null>,
);
} catch (err) {
console.error(err);
setError("Failed to update reaction.");
} finally {
setPendingReaction(null);
}
};

const handleFetchThought = async () => {
if (!messageId || !conversationId || !userId || !URL) return;
Expand Down Expand Up @@ -82,23 +107,50 @@ 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={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={pendingReaction !== null}
>
<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

0 comments on commit cd2f445

Please sign in to comment.