Skip to content

Commit

Permalink
[FEATURE] Team Chat (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
nphivu414 authored Jun 26, 2024
1 parent c781260 commit af85c1e
Show file tree
Hide file tree
Showing 46 changed files with 3,658 additions and 1,946 deletions.
52 changes: 33 additions & 19 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { cookies } from "next/headers";
import { env } from "@/env.mjs";
import { OpenAIStream, StreamingTextResponse } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { Message, streamText } from "ai";
import { pick } from "lodash";
import { AxiomRequest, withAxiom } from "next-axiom";
import OpenAI from "openai";

import { getAppBySlug } from "@/lib/db/apps";
import { createNewChatMember } from "@/lib/db/chat-members";
import { createNewChat } from "@/lib/db/chats";
import {
createNewMessage,
Expand All @@ -19,7 +20,7 @@ export const dynamic = "force-dynamic";
export const runtime = "edge";
export const preferredRegion = "home";

const openai = new OpenAI({
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});

Expand All @@ -43,6 +44,7 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
isRegenerate,
regenerateMessageId,
isNewChat,
enableChatAssistant = true,
} = params;

const user = await getCurrentUser(supabase);
Expand All @@ -61,6 +63,10 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
app_id: currentApp.id,
name: lastMessage.content,
});
await createNewChatMember(supabase, {
chat_id: chatId,
member_id: user.id,
});
}
await createNewMessage(supabase, {
chat_id: chatId,
Expand All @@ -75,28 +81,36 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
}
}

const response = await openai.chat.completions.create({
model,
temperature,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
messages: messages.map((message: any) => pick(message, "content", "role")),
max_tokens: maxTokens,
top_p: topP,
frequency_penalty: frequencyPenalty,
presence_penalty: presencePenalty,
stream: true,
});
log.debug("Create stream");
if (!enableChatAssistant) {
return new Response(null, {
status: 200,
headers: {
"Content-Type": "application/json",
"should-redirect-to-new-chat": "true",
},
});
}

const stream = OpenAIStream(response, {
onCompletion: async (completion: string) => {
log.debug("Start stream text");
const response = await streamText({
model: openai(model),
temperature,
messages: messages.map((message: Message) =>
pick(message, "content", "role")
),
maxTokens,
topP,
frequencyPenalty,
presencePenalty,
onFinish: async ({ text }) => {
await createNewMessage(supabase, {
chat_id: chatId,
content: completion,
content: text,
role: "assistant",
});
},
});
log.debug("End stream text");

return new StreamingTextResponse(stream);
return response.toAIStreamResponse();
});
30 changes: 30 additions & 0 deletions app/apps/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React from "react";
import { Metadata } from "next";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Message } from "ai";

import {
CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE,
DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT,
} from "@/lib/contants";
import { getAppBySlug } from "@/lib/db/apps";
import { getChatMembers } from "@/lib/db/chat-members";
import { getChatById, getChats } from "@/lib/db/chats";
import { getMessages } from "@/lib/db/message";
import { getCurrentUser } from "@/lib/session";
Expand Down Expand Up @@ -37,14 +43,26 @@ export default async function ChatPage({ params }: { params: { id: string } }) {
const dbMessages = await getMessages(supabase, chatId);

const chatDetails = await getChatById(supabase, chatId);
if (!chatDetails) {
redirect("/apps/chat");
}
const chatParams = chatDetails?.settings as ChatParams | undefined;
const isChatHost = chatDetails?.profile_id === user.id;

const initialChatMessages: Message[] = dbMessages?.length
? dbMessages.map((message) => {
return {
id: message.id,
role: message.role || "system",
content: message.content || "",
data: {
profile_id: message.profile_id,
chat_id: message.chat_id,
chatBubleDirection:
message.role === "user" && message.profile_id === user.id
? "end"
: "start",
},
};
})
: [];
Expand All @@ -57,12 +75,24 @@ export default async function ChatPage({ params }: { params: { id: string } }) {
});
}

const chatMembers = await getChatMembers(supabase, chatId);

const memberSidebarLayout = cookies().get(CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE);

let defaultMemberSidebarLayout = DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT;
if (memberSidebarLayout) {
defaultMemberSidebarLayout = JSON.parse(memberSidebarLayout.value);
}

return (
<ChatPanel
chatId={chatId}
chats={chats}
initialMessages={initialChatMessages}
chatParams={chatParams}
isChatHost={isChatHost}
chatMembers={chatMembers}
defaultMemberSidebarLayout={defaultMemberSidebarLayout}
/>
);
}
9 changes: 8 additions & 1 deletion app/apps/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export default async function NewChatPage() {
const chats = await getChats(supabase, currentApp.id);

return (
<ChatPanel chatId={chatId} initialMessages={[]} chats={chats} isNewChat />
<ChatPanel
chatId={chatId}
initialMessages={[]}
chats={chats}
isNewChat
chatMembers={null}
defaultMemberSidebarLayout={[]}
/>
);
}
2 changes: 1 addition & 1 deletion app/apps/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function AppLayout({ children }: AppLayoutProps) {
<MainLayout>
<div className="flex h-screen flex-1 flex-row pt-16">
<div className="flex flex-1 flex-row">
<div className="flex flex-1 flex-col overflow-y-auto">
<div className="flex flex-1 flex-col overflow-y-hidden">
<div className="relative flex flex-1 bg-background">
<div className="flex size-0 flex-col justify-between overflow-x-hidden transition-[width] lg:h-auto lg:max-h-[calc(100vh_-_65px)] lg:w-[300px] lg:border-r">
<ChatHistory data={chats} />
Expand Down
4 changes: 2 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@import url('./styles/custom.css');

@tailwind base;
@tailwind components;
@tailwind utilities;

@import url('./styles/custom.css');

.drawer-toggle:checked ~ .drawer-side {
backdrop-filter: blur(5px);
}
Expand Down
78 changes: 78 additions & 0 deletions components/modules/apps/chat/ChatForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react";
import { SendHorizonal } from "lucide-react";
import { MentionsInputProps, SuggestionDataItem } from "react-mentions";

import { Chat, ChatMemberProfile } from "@/lib/db";
import { useProfileStore } from "@/lib/stores/profile";
import { useEnterSubmit } from "@/hooks/useEnterSubmit";
import { Button } from "@/components/ui/Button";
import { ChatInput } from "@/components/ui/chat";

import { MobileDrawerControl } from "./MobileDrawerControls";

type ChatFormProps = {
chatInput: string;
chats: Chat[] | null;
isChatStreamming: boolean;
chatMembers: ChatMemberProfile[] | null;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onInputChange: (
e:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => void;
};

export const ChatForm = ({
chats,
isChatStreamming,
chatMembers,
chatInput,
onSubmit,
onInputChange,
}: ChatFormProps) => {
const { formRef, onKeyDown } = useEnterSubmit();
const currentProfile = useProfileStore((state) => state.profile);

const mentionData: SuggestionDataItem[] = React.useMemo(() => {
const mentionData = [{ id: "assistant", display: "Assistant" }];

if (!chatMembers) return mentionData;

chatMembers.forEach((member) => {
if (!member.profiles) return;
mentionData.push({
id: member.profiles.id,
display: member.profiles.username || "",
});
});

return mentionData.filter((mention) => mention.id !== currentProfile?.id);
}, [chatMembers, currentProfile?.id]);

const handleOnChange: MentionsInputProps["onChange"] = (e) => {
onInputChange({
target: { value: e.target.value },
} as React.ChangeEvent<HTMLTextAreaElement>);
};

return (
<div className="fixed bottom-0 left-0 w-full bg-background p-4 lg:relative lg:mt-2 lg:bg-transparent lg:py-0">
<form onSubmit={onSubmit} className="relative" ref={formRef}>
<ChatInput
value={chatInput}
onKeyDown={onKeyDown}
onChange={handleOnChange}
mentionData={mentionData}
/>
<MobileDrawerControl chats={chats} />
<div className="absolute bottom-[2px] right-1 flex w-1/2 justify-end bg-background px-2 pb-2">
<Button size="sm" type="submit" disabled={isChatStreamming}>
Send
<SendHorizonal size={14} className="ml-1" />
</Button>
</div>
</form>
</div>
);
};
10 changes: 6 additions & 4 deletions components/modules/apps/chat/ChatHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import React from "react";
import { usePathname } from "next/navigation";

import { Chat } from "@/lib/db";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import { Separator } from "@/components/ui/Separator";
import { Paragraph } from "@/components/ui/typography";

import { ChatHistoryItem } from "./ChatHistoryItem";
import { NewChatButton } from "./NewChatButton";
Expand All @@ -15,8 +16,7 @@ type ChatHistoryProps = {
};

export const ChatHistory = ({ data, closeDrawer }: ChatHistoryProps) => {
const pathname = usePathname();
const chatId = pathname.split("/").pop();
const chatId = useChatIdFromPathName();

return (
<aside className="pb-4">
Expand All @@ -31,7 +31,9 @@ export const ChatHistory = ({ data, closeDrawer }: ChatHistoryProps) => {
<Separator className="sticky top-16" />
<ul className="mt-2 lg:px-2">
{data?.length ? null : (
<p className="text-sm text-muted-foreground">No data</p>
<Paragraph className="text-center text-sm text-muted-foreground">
No chats found
</Paragraph>
)}
{data?.map((chat) => {
return (
Expand Down
28 changes: 28 additions & 0 deletions components/modules/apps/chat/ChatLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MainLayout } from "@/components/ui/common/MainLayout";

interface ChatLayoutProps {
children: React.ReactNode;
leftSidebarElement: React.ReactNode;
}

export const ChatLayout = ({
children,
leftSidebarElement,
}: ChatLayoutProps) => {
return (
<MainLayout>
<div className="flex h-screen flex-1 flex-row pt-16">
<div className="flex flex-1 flex-row">
<div className="flex flex-1 flex-col overflow-y-hidden">
<div className="relative flex flex-1 bg-background">
<div className="flex size-0 flex-col justify-between overflow-x-hidden transition-[width] lg:h-auto lg:max-h-[calc(100vh_-_65px)] lg:w-[300px] lg:border-r">
{leftSidebarElement}
</div>
{children}
</div>
</div>
</div>
</div>
</MainLayout>
);
};
Loading

0 comments on commit af85c1e

Please sign in to comment.