Skip to content

Commit

Permalink
Merge pull request #269 from cabcookie:add-ai-features
Browse files Browse the repository at this point in the history
Implementiere einen einfachen Chatbot ohne Spezialwissen
  • Loading branch information
cabcookie authored Dec 13, 2024
2 parents 3f675fe + 2d27ce7 commit bb57ac0
Show file tree
Hide file tree
Showing 10 changed files with 1,544 additions and 1,314 deletions.
80 changes: 37 additions & 43 deletions components/chat/ChatConversation.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Schema } from "@/amplify/data/resource";
import { useAIConversation, useGeneralChat } from "@/api/useGeneralChat";
import useCurrentUser from "@/api/useUser";
import { AIConversation, Avatars, SendMessage } from "@aws-amplify/ui-react-ai";
import { find, flow, get, identity, last } from "lodash/fp";
import { FC, useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import CircleProfileImg from "../profile/CircleProfileImg";
import ConversationName from "./ConversationName";
import Errors from "./Errors";
import MessageInput from "./MessageInput";
import Messages from "./Messages";

type Conversation = Schema["generalChat"]["type"];

interface ChatConversationProps {
chatId: string;
Expand All @@ -12,62 +16,52 @@ interface ChatConversationProps {
const ChatConversation: FC<ChatConversationProps> = ({ chatId }) => {
const [
{
data: { messages, conversation },
data: { messages },
messages: errors,
hasError,
isLoading,
},
sendMessage,
] = useAIConversation("generalChat", { id: chatId });
const { setConversationName } = useGeneralChat();
const { user } = useCurrentUser();
const [avatars, setAvatars] = useState<Avatars | undefined>();
const [messageRenderer] = useState<
Parameters<typeof AIConversation>[number]["messageRenderer"]
>({ text: ({ text }) => <ReactMarkdown>{text}</ReactMarkdown> });
const { conversations, setConversationName } = useGeneralChat();
const [conversation, setConversation] = useState<Conversation | undefined>();

const getInputFieldKey = () =>
`${get("id")(conversation) ?? "NA"}-${flow(identity<typeof messages>, last, get("id"))(messages)}`;

useEffect(() => {
if (!user) return;
setAvatars({
user: {
username: user.userName,
avatar: (
<CircleProfileImg
user={user}
fallbackInitials="US"
className="w-8 h-8"
/>
),
},
});
}, [user]);
flow(
identity<Conversation[] | undefined>,
find<Conversation>(["id", chatId]),
setConversation
)(conversations);
}, [chatId, conversations]);

const handleSendMessage: SendMessage = async (message) => {
const handleSendMessage = async (prompt: string) => {
if (!chatId) return;
if (!conversation) return;
const message = { content: [{ text: prompt }] };
await sendMessage(message);
if (conversation.name) return;
await setConversationName(chatId, [...messages, message]);
};

return (
<>
<AIConversation
{...{
handleSendMessage,
isLoading,
messages,
messageRenderer,
avatars,
}}
<div className="space-y-4">
<ConversationName
name={conversation?.name}
className="sticky md:static top-[5.25rem] bg-bgTransparent pb-2 z-30"
/>

<Messages {...{ messages }} />

<Errors {...{ hasError, errors }} />

<MessageInput
id={getInputFieldKey()}
onSend={handleSendMessage}
className="sticky bottom-0 left-0 right-0 md:left-1 md:right-1"
/>
{hasError &&
errors?.map((error, index) => (
<div key={index} className="text-sm p-2 text-red-600 font-semibold">
{error.message}
</div>
))}
</>
</div>
);
};

Expand Down
20 changes: 20 additions & 0 deletions components/chat/ConversationName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { cn } from "@/lib/utils";
import { FC } from "react";

interface ConversationNameProps {
name?: string;
className?: string;
}

const ConversationName: FC<ConversationNameProps> = ({ name, className }) => (
<h2
className={cn(
"text-xl md:text-2xl md:text-center font-semibold",
className
)}
>
{name || "New chat"}
</h2>
);

export default ConversationName;
16 changes: 16 additions & 0 deletions components/chat/Errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FC } from "react";

interface ErrorsProps {
hasError?: boolean;
errors?: { message?: string }[];
}

const Errors: FC<ErrorsProps> = ({ hasError, errors }) =>
hasError &&
errors?.map((error, index) => (
<div key={index} className="text-sm p-2 text-red-600 font-semibold">
{error.message}
</div>
));

export default Errors;
26 changes: 26 additions & 0 deletions components/chat/Message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { User } from "@/api/useUser";
import { ConversationMessage } from "@aws-amplify/ui-react-ai";
import { FC } from "react";
import ReactMarkdown from "react-markdown";
import MessageAvatar from "./MessageAvatar";

interface MessageProps {
message: ConversationMessage & { isLoading?: boolean };
user?: User;
}

const Message: FC<MessageProps> = ({ message, user }) => (
<div key={message.id} className="w-full space-y-4">
<MessageAvatar
messageDate={message.createdAt}
role={message.role}
user={user}
/>

{message.content.map((content, index) => (
<ReactMarkdown key={index}>{content.text}</ReactMarkdown>
))}
</div>
);

export default Message;
36 changes: 36 additions & 0 deletions components/chat/MessageAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { User } from "@/api/useUser";
import { differenceInHours, format, formatDistanceToNow } from "date-fns";
import { Bot } from "lucide-react";
import { FC } from "react";
import CircleProfileImg from "../profile/CircleProfileImg";

interface MessageAvatarProps {
user?: User;
role: "user" | "assistant";
messageDate: string | Date;
}

const MessageAvatar: FC<MessageAvatarProps> = ({ user, role, messageDate }) => (
<div className="flex flex-row gap-2 items-center">
{role === "user" ? (
<CircleProfileImg user={user} fallbackInitials="US" className="w-8 h-8" />
) : (
<div className="w-8 h-8 rounded-full bg-[--context-color] p-1">
<Bot className="w-6 h-6 text-slate-600" />
</div>
)}

<div className="flex flex-row gap-2 items-baseline">
<div className="font-semibold">
{role === "user" ? (user?.userName ?? "User") : "Assistant"}
</div>
<div className="text-muted-foreground text-xs">
{differenceInHours(new Date(), messageDate) < 4
? formatDistanceToNow(messageDate, { addSuffix: true })
: format(messageDate, "p")}
</div>
</div>
</div>
);

export default MessageAvatar;
26 changes: 26 additions & 0 deletions components/chat/MessageDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { cn } from "@/lib/utils";
import {
format,
isSameISOWeek,
isToday,
isYesterday,
startOfToday,
} from "date-fns";
import { FC } from "react";

interface MessageDateProps {
date: string | Date;
className?: string;
}

const MessageDate: FC<MessageDateProps> = ({ date, className }) => (
<div className={cn(className)}>
{isToday(date)
? "Today"
: isYesterday(date)
? "Yesterday"
: format(date, isSameISOWeek(date, startOfToday()) ? "eeee" : "PPP")}
</div>
);

export default MessageDate;
50 changes: 50 additions & 0 deletions components/chat/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cn } from "@/lib/utils";
import { JSONContent } from "@tiptap/core";
import { ArrowUp } from "lucide-react";
import { FC, useState } from "react";
import { emptyDocument } from "../ui-elements/editors/helpers/document";
import { getTextFromJsonContent } from "../ui-elements/editors/helpers/text-generation";
import InboxEditor from "../ui-elements/editors/inbox-editor/InboxEditor";
import { Button } from "../ui/button";

interface MessageInputProps {
id: string;
onSend: (prompt: string) => void;
className?: string;
}

const MessageInput: FC<MessageInputProps> = ({ id, onSend, className }) => {
const [prompt, setPrompt] = useState<JSONContent>(emptyDocument);

const handleSend = () => {
onSend(getTextFromJsonContent(prompt));
setPrompt(emptyDocument);
};

return (
<div className={cn(className)}>
<div className="w-full h-8 bg-gradient-to-t from-background/95 via-background/80 to-background/0" />

<div className="relativ bg-white/95 pb-2">
<InboxEditor
key={id}
notes={prompt}
saveNotes={(editor) => setPrompt(editor.getJSON())}
autoFocus
placeholder="Send a message"
saveAtCmdEnter={handleSend}
showSaveStatus={false}
className="pb-12 max-h-60 md:max-h-80 overflow-y-auto"
/>
<Button
className="rounded-full w-10 h-10 absolute bottom-3 right-1"
onClick={handleSend}
>
<ArrowUp />
</Button>
</div>
</div>
);
};

export default MessageInput;
45 changes: 45 additions & 0 deletions components/chat/Messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import useCurrentUser from "@/api/useUser";
import { ConversationMessage } from "@aws-amplify/ui-react-ai";
import { format } from "date-fns";
import { flow, identity, map, uniq } from "lodash/fp";
import { FC, useEffect, useState } from "react";
import Message from "./Message";
import MessageDate from "./MessageDate";

interface MessagesProps {
messages: ConversationMessage[];
}

const Messages: FC<MessagesProps> = ({ messages }) => {
const [messageDates, setMessageDates] = useState<string[]>([]);
const { user } = useCurrentUser();

useEffect(() => {
flow(
identity<typeof messages>,
map("createdAt"),
map((date) => format(date, "yyyy-MM-dd")),
uniq,
setMessageDates
)(messages);
}, [messages]);

return messageDates?.map((date) => (
<div key={date} className="flex flex-col items-center space-y-2">
<MessageDate
date={date}
className="flex text-center text-xs text-muted-foreground bg-bgTransparent w-fit rounded-xl py-0.5 px-2 sticky top-[8rem] md:top-[7rem]"
/>

<div className="space-y-8">
{messages
.filter((m) => format(m.createdAt, "yyyy-MM-dd") === date)
.map((message) => (
<Message key={message.id} message={message} user={user} />
))}
</div>
</div>
));
};

export default Messages;
2 changes: 1 addition & 1 deletion docs/releases/next.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Implementiere einen einfachen Chatbot ohne Spezialwissen (Version :VERSION)

- [Allgemeines AI Chat Backend eingerichtet](https://docs.amplify.aws/react/ai/set-up-ai/)
- Oberfläche für einen Chatbot eingerichtet.
- Eine eigene Oberfläche für einen Chatbot eingerichtet.
- Eine Seitenleiste zeigt die vergangenen Conversationen an.

## Bekannte Fehler
Expand Down
Loading

0 comments on commit bb57ac0

Please sign in to comment.