Skip to content

Commit

Permalink
Merge branch 'main' into ben/hotfix-markdown-wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
bLopata authored Oct 18, 2024
2 parents 0024a33 + cd2f445 commit 40eeb23
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 30 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
78 changes: 65 additions & 13 deletions www/components/messagebox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,59 @@ import { FaLightbulb } from 'react-icons/fa';
import { API } from '@/utils/api';
import MarkdownWrapper from './markdownWrapper';

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 @@ -79,23 +104,50 @@ export default function MessageBox({
<div className="flex flex-col gap-2 w-full">
{loading ? <Skeleton count={4} /> : <MarkdownWrapper text={text} />}
{!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;
33 changes: 32 additions & 1 deletion www/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type Reaction } from "@/components/messagebox";

const defaultMessage: Message = {
text: `I&apos;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&apos;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&apos;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,
id: '',
};
Expand All @@ -8,6 +10,7 @@ export interface Message {
text: string;
isUser: boolean;
id: string;
metadata?: { reaction?: Reaction };
}

export class Conversation {
Expand Down Expand Up @@ -154,6 +157,7 @@ export class API {
text: rawMessage.content,
isUser: rawMessage.isUser,
id: rawMessage.id,
metadata: rawMessage.metadata,
};
});

Expand Down Expand Up @@ -186,4 +190,31 @@ export class API {
return null;
}
}

async addOrRemoveReaction(
conversationId: string,
messageId: string,
reaction: Reaction,
): Promise<{ status: string }> {
try {
const response = await fetch(
`${this.url}/api/reaction/${messageId}?user_id=${this.userId}&conversation_id=${conversationId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ reaction: reaction || undefined }),
},
);
if (!response.ok) {
throw new Error("Failed to update reaction");
}

return await response.json();
} catch (error) {
console.error("Error updating reaction:", error);
throw error;
}
}
}

0 comments on commit 40eeb23

Please sign in to comment.