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

✨ caching and skeletons 2 #130

Merged
merged 3 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions www/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import "./globals.css";
import "react-loading-skeleton/dist/skeleton.css";
import type { Metadata } from "next";
import { Space_Grotesk } from "next/font/google";
import { PHProvider, PostHogPageview } from './providers'
import { Suspense } from 'react'
import { PHProvider, PostHogPageview } from "./providers";
import { Suspense } from "react";

const spacegrotesk = Space_Grotesk({ subsets: ["latin"] });

Expand Down Expand Up @@ -58,6 +59,6 @@ export default function RootLayout({
<PHProvider>
<body className={spacegrotesk.className}>{children}</body>
</PHProvider>
</html >
</html>
);
}
177 changes: 107 additions & 70 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import Image from "next/image";
import useSWR from "swr";

import banner from "@/public/bloom2x1.svg";
import darkBanner from "@/public/bloom2x1dark.svg";
Expand All @@ -8,41 +9,33 @@ import Thoughts from "@/components/thoughts";
import Sidebar from "@/components/sidebar";

import { FaLightbulb, FaPaperPlane, FaBars } from "react-icons/fa";
import {
useRef,
useEffect,
useState,
ElementRef,
} from "react";

import Swal from "sweetalert2"
import { useRef, useEffect, useState, ElementRef } from "react";

import Swal from "sweetalert2";
import { useRouter } from "next/navigation";
import { usePostHog } from 'posthog-js/react'
import { usePostHog } from "posthog-js/react";

import Link from "next/link";
import MarkdownWrapper from "@/components/markdownWrapper";
import { DarkModeSwitch } from "react-toggle-dark-mode";
import { Message, Conversation, API } from "@/utils/api";
import { getId } from "@/utils/supabase";
import { data } from "autoprefixer";
import { Session } from "@supabase/supabase-js";

const URL = process.env.NEXT_PUBLIC_API_URL;
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 😊.`,
isUser: false,
};

export default function Home() {
const [userId, setUserId] = useState<string>();
const [session, setSession] = useState<Session | null>(null);

const [isThoughtsOpen, setIsThoughtsOpen] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);

const [thought, setThought] = useState("");
const [canSend, setCanSend] = useState(false);

const [api, setApi] = useState<API>();

const [messages, setMessages] = useState<Message[]>([defaultMessage]);
const [conversations, setConversations] = useState<Conversation[]>([]);
const [currentConversation, setCurrentConversation] =
useState<Conversation>();
const [conversationId, setConversationId] = useState<string>();

const router = useRouter();
const posthog = usePostHog();
Expand All @@ -58,44 +51,26 @@ export default function Home() {

useEffect(() => {
(async () => {
setIsDarkMode(
window.matchMedia("(prefers-color-scheme: dark)").matches
);
const api = await API.create(URL!);
if (!api.session) {
const { userId, session } = await getId();
setUserId(userId);
setSession(session);
setIsDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
if (!session) {
Swal.fire({
title: "Notice: Bloombot now requires signing in for usage",
text: "Due to surging demand for Bloom we are requiring users to stay signed in to user Bloom",
icon: "warning",
confirmButtonColor: "#3085d6",
confirmButtonText: "Sign In"
})
.then((res) => {
router.push("/auth")
})
confirmButtonText: "Sign In",
}).then((res) => {
router.push("/auth");
});
} else {
posthog?.identify(
api.userId,
{ "email": api.session.user.email }
);
setApi(api);
const conversations = await api.getConversations();
setConversations(conversations);
setCurrentConversation(conversations[0]);
setCanSend(true);
posthog?.identify(userId, { email: session.user.email });
}
})();
}, []);

useEffect(() => {
(async () => {
if (!currentConversation) return;
const messages = await currentConversation.getMessages();
setMessages([defaultMessage, ...messages]);
// console.log("set messages", messages);
})();
}, [currentConversation]);

useEffect(() => {
const messageContainer = messageContainerRef.current;
if (!messageContainer) return;
Expand All @@ -115,7 +90,36 @@ export default function Home() {
};
}, []);

const conversationsFetcher = async (userId: string) => {
const api = new API({ url: URL!, userId });
return api.getConversations();
};

const {
data: conversations,
mutate: mutateConversations,
error,
} = useSWR(userId, conversationsFetcher, {
onSuccess: (conversations) => {
setConversationId(conversations[0].conversationId);
setCanSend(true);
},
});

const messagesFetcher = async (conversationId: string) => {
if (!userId) return Promise.resolve([]);
if (!conversationId) return Promise.resolve([]);

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

const {
data: messages,
mutate: mutateMessages,
isLoading: messagesLoading,
error: _,
} = useSWR(conversationId, messagesFetcher);

async function chat() {
const textbox = input.current!;
Expand All @@ -125,8 +129,8 @@ export default function Home() {

setCanSend(false); // Disable sending more messages until the current generation is done

setMessages((prev) => [
...prev,
const newMessages = [
...messages!,
{
text: message,
isUser: true,
Expand All @@ -135,9 +139,16 @@ export default function Home() {
text: "",
isUser: false,
},
]);
];
mutateMessages(newMessages, { revalidate: false });

// sleep for 1 second to give the user the illusion of typing
await new Promise((resolve) => setTimeout(resolve, 1000));

const reader = await currentConversation!.chat(message);
// const reader = await currentConversation!.chat(message);
const reader = await conversations!
.find((conversation) => conversation.conversationId === conversationId)!
.chat(message);

const messageContainer = messageContainerRef.current;
if (messageContainer) {
Expand All @@ -147,6 +158,8 @@ export default function Home() {
let isThinking = true;
setThought("");

let currentModelOutput = "";

while (true) {
const { done, value } = await reader.read();
if (done) {
Expand All @@ -162,15 +175,29 @@ export default function Home() {
continue;
}
setThought((prev) => prev + value);
mutateMessages(newMessages, { revalidate: false });
} else {
if (value.includes("❀")) {
setCanSend(true); // Bloom delimeter
continue;
}
setMessages((prev) => {
prev[prev.length - 1].text += value;
return [...prev];
});
// setMessages((prev) => {
// prev[prev.length - 1].text += value;
// return [...prev];
// });

currentModelOutput += value;

mutateMessages(
[
...newMessages?.slice(0, -1)!,
{
text: currentModelOutput,
isUser: false,
},
],
{ revalidate: false }
);

if (isAtBottom.current) {
const messageContainer = messageContainerRef.current;
Expand All @@ -180,21 +207,25 @@ export default function Home() {
}
}
}

mutateMessages();
}

return (
<main
className={`flex h-[100dvh] w-screen flex-col pb-[env(keyboard-inset-height)] text-sm lg:text-base overflow-hidden relative ${isDarkMode ? "dark" : ""
}`}
className={`flex h-[100dvh] w-screen flex-col pb-[env(keyboard-inset-height)] text-sm lg:text-base overflow-hidden relative ${
isDarkMode ? "dark" : ""
}`}
>
<Sidebar
conversations={conversations}
currentConversation={currentConversation}
setCurrentConversation={setCurrentConversation}
setConversations={setConversations}
api={api}
conversations={conversations || []}
mutateConversations={mutateConversations}
conversationId={conversationId}
setConversationId={setConversationId}
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
api={new API({ url: URL!, userId: userId! })}
session={session}
/>
<div className="flex flex-col w-full h-[100dvh] lg:pl-60 xl:pl-72 dark:bg-gray-900">
<nav className="flex justify-between items-center p-4 border-b border-gray-300 dark:border-gray-700">
Expand Down Expand Up @@ -235,11 +266,15 @@ export default function Home() {
className="flex flex-col flex-1 overflow-y-auto lg:px-5 dark:text-white"
ref={messageContainerRef}
>
{messages.map((message, i) => (
<MessageBox isUser={message.isUser} key={i}>
<MarkdownWrapper text={message.text} />
</MessageBox>
))}
{messagesLoading || messages === undefined ? (
<MessageBox loading />
) : (
messages.map((message, i) => (
<MessageBox isUser={message.isUser} key={i}>
<MarkdownWrapper text={message.text} />
</MessageBox>
))
)}
</section>
<form
id="send"
Expand All @@ -256,7 +291,9 @@ export default function Home() {
<textarea
ref={input}
placeholder="Type a message..."
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 ? " border-green-200" : "border-red-200 opacity-50"}`}
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 ? " border-green-200" : "border-red-200 opacity-50"
}`}
disabled={!canSend}
rows={1}
onKeyDown={(e) => {
Expand All @@ -276,12 +313,12 @@ export default function Home() {
<FaPaperPlane className="inline" />
</button>
</form>
</div >
</div>
<Thoughts
thought={thought}
setIsThoughtsOpen={setIsThoughtsOpen}
isThoughtsOpen={isThoughtsOpen}
/>
</main >
</main>
);
}
65 changes: 65 additions & 0 deletions www/components/conversationtab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Conversation } from "@/utils/api";
import { FaEdit, FaTrash } from "react-icons/fa";
import Skeleton from "react-loading-skeleton";

interface ConversationTabRegularProps {
conversation: Conversation;
select: () => void;
selected: boolean;
edit: () => void;
del: () => void;
loading?: false;
}

interface ConversationTabLoadingProps {
conversation?: undefined;
select?: undefined;
selected?: undefined;
edit?: undefined;
del?: undefined;
loading: true;
}

type ConversationTabProps =
| ConversationTabRegularProps
| ConversationTabLoadingProps;

export function ConversationTab({
conversation,
select,
selected,
edit,
del,
loading,
}: ConversationTabProps) {
return (
<div
className={`flex justify-between items-center p-4 cursor-pointer hover:bg-gray-200 hover:dark:bg-gray-800 ${
selected ? "bg-gray-200 dark:bg-gray-800" : ""
}`}
onClick={select}
>
{loading ? (
<div className="flex-1">
<Skeleton />
</div>
) : (
<>
<div>
<h2 className="font-bold overflow-ellipsis overflow-hidden ">
{conversation.name || "Untitled"}
</h2>
</div>
<div className="flex flex-row justify-end gap-2 items-center w-1/5">
<button className="text-gray-500" onClick={edit}>
<FaEdit />
</button>
<button className="text-red-500" onClick={del}>
<FaTrash />
</button>
</div>
</>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion www/components/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React from "react";

const Loading = () => {
return (
Expand Down
Loading
Loading