Skip to content

Commit

Permalink
Merge pull request #268 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 12, 2024
2 parents 767cf3b + 49f549a commit 4cde690
Show file tree
Hide file tree
Showing 31 changed files with 4,214 additions and 1,364 deletions.
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
11 changes: 10 additions & 1 deletion amplify/data/ai-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import { a } from "@aws-amplify/backend";
const aiSchema = {
generalChat: a
.conversation({
aiModel: a.ai.model("Claude 3.5 Sonnet v2"),
aiModel: a.ai.model("Claude 3.5 Sonnet"),
systemPrompt: "You are a helpful assistant.",
})
.authorization((allow) => allow.owner()),
chatNamer: a
.generation({
aiModel: a.ai.model("Claude 3 Haiku"),
systemPrompt:
"You are a helpful assistant that writes descriptive names for conversations. Names should be 2-7 words long. The descriptive name for the conversation should be in the same language as the conversation",
})
.arguments({ content: a.string() })
.returns(a.customType({ name: a.string() }))
.authorization((allow) => [allow.authenticated()]),
};

export default aiSchema;
97 changes: 97 additions & 0 deletions api/useGeneralChat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Schema } from "@/amplify/data/resource";
import { createAIHooks } from "@aws-amplify/ui-react-ai";
import { generateClient } from "aws-amplify/api";
import { flow, identity, sortBy } from "lodash/fp";
import useSWR from "swr";
import { handleApiErrors } from "./globals";
const client = generateClient<Schema>({ authMode: "userPool" });

export const { useAIConversation } = createAIHooks(client);

type Message = {
content: {
text?: string;
}[];
};

const fetchConversations = async () => {
const { data, errors } = await client.conversations.generalChat.list();
if (errors) {
handleApiErrors(errors);
throw errors;
}
return flow(
identity<Schema["generalChat"]["type"][]>,
sortBy((c) => -new Date(c.updatedAt).getTime())
)(data);
};

export const useGeneralChat = () => {
const {
data: conversations,
error,
isLoading,
mutate,
} = useSWR("/api/chat/general", fetchConversations);

const createConversation = async () => {
const { data, errors } = await client.conversations.generalChat.create({});
if (errors) handleApiErrors(errors);
if (data) mutate([...(conversations || []), data]);
return data || undefined;
};

const updateConversation = async (
conversation: Partial<Schema["generalChat"]["type"]> & { id: string }
) => {
const updated = conversations?.map((c) =>
c.id !== conversation.id ? c : { ...c, ...conversation }
);
if (updated) mutate(updated, false);
const { data, errors } =
await client.conversations.generalChat.update(conversation);
if (errors) handleApiErrors(errors, "Error updating conversation");
if (updated) mutate(updated);
return data;
};

const deleteConversation = async (conversationId: string) => {
const updated = conversations?.filter((c) => c.id !== conversationId);
if (updated) mutate(updated, false);
const { data, errors } = await client.conversations.generalChat.delete({
id: conversationId,
});
if (errors) handleApiErrors(errors, "Error deleting conversation");
if (updated) mutate(updated);
return data;
};

const setConversationName = async (
conversationId: string,
messages: Message[]
) => {
if (!messages.length) return;
const name = await generateChatName(messages);
return updateConversation({ id: conversationId, name });
};

const generateChatName = async (messages: Message[]) => {
const content = messages
.map((m) => m.content.map((c) => c.text ?? "").join(""))
.join("\n");
const { data, errors } = await client.generations.chatNamer({
content,
});
if (errors) handleApiErrors(errors, "Error generating chat name");
return data?.name ?? "";
};

return {
conversations,
error,
isLoading,
createConversation,
setConversationName,
deleteConversation,
};
};
74 changes: 74 additions & 0 deletions components/chat/ChatConversation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useAIConversation, useGeneralChat } from "@/api/useGeneralChat";
import useCurrentUser from "@/api/useUser";
import { AIConversation, Avatars, SendMessage } from "@aws-amplify/ui-react-ai";
import { FC, useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import CircleProfileImg from "../profile/CircleProfileImg";

interface ChatConversationProps {
chatId: string;
}

const ChatConversation: FC<ChatConversationProps> = ({ chatId }) => {
const [
{
data: { messages, conversation },
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> });

useEffect(() => {
if (!user) return;
setAvatars({
user: {
username: user.userName,
avatar: (
<CircleProfileImg
user={user}
fallbackInitials="US"
className="w-8 h-8"
/>
),
},
});
}, [user]);

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

return (
<>
<AIConversation
{...{
handleSendMessage,
isLoading,
messages,
messageRenderer,
avatars,
}}
/>
{hasError &&
errors?.map((error, index) => (
<div key={index} className="text-sm p-2 text-red-600 font-semibold">
{error.message}
</div>
))}
</>
);
};

export default ChatConversation;
41 changes: 21 additions & 20 deletions components/header/ProfilePicture.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useCurrentUser from "@/api/useUser";
import useCurrentUser, { User } from "@/api/useUser";
import { setCurrentImgUrl } from "@/helpers/user/user";
import { signOut } from "aws-amplify/auth";
import { getUrl } from "aws-amplify/storage";
import { defaultTo, flow, get, identity, join, map, split } from "lodash/fp";
import { LogOut, UserCircle2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
Expand All @@ -20,22 +21,7 @@ const ProfilePicture = () => {
const [open, setOpen] = useState(false);
const [isCreatingProfile, setIsCreatingProfile] = useState(false);
const [imageUrl, setImgUrl] = useState<string | undefined>(undefined);

const setCurrentImgUrl = async (
key: string | undefined,
setUrl: (url: string | undefined) => void
) => {
if (!key) {
setUrl(undefined);
return;
}
const { url } = await getUrl({ path: key });
setUrl(url.toString());
};

useEffect(() => {
setCurrentImgUrl(user?.profilePicture, setImgUrl);
}, [user?.profilePicture]);
const [initials, setInitials] = useState<string | undefined>("NA");

useEffect(() => {
if (!open) return;
Expand All @@ -45,12 +31,28 @@ const ProfilePicture = () => {
createProfile(() => setIsCreatingProfile(false));
}, [createProfile, isCreatingProfile, open, user]);

useEffect(() => {
flow(
identity<User | undefined>,
get("userName"),
split(" "),
map(0),
join(""),
defaultTo("NA"),
setInitials
)(user);
}, [user]);

useEffect(() => {
setCurrentImgUrl(user?.profilePicture, setImgUrl);
}, [user?.profilePicture]);

return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Avatar className="cursor-pointer">
<AvatarImage src={imageUrl} />
<AvatarFallback>CK</AvatarFallback>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
Expand All @@ -73,7 +75,6 @@ const ProfilePicture = () => {
<DropdownMenuItem onClick={() => signOut()}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
{/* <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut> */}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
68 changes: 68 additions & 0 deletions components/layouts/ChatLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useContextContext } from "@/contexts/ContextContext";
import {
NavMenuContextProvider,
useNavMenuContext,
} from "@/contexts/NavMenuContext";
import { addKeyDownListener } from "@/helpers/keyboard-events/main-layout";
import Head from "next/head";
import { useRouter } from "next/router";
import { FC, ReactNode, useEffect, useState } from "react";
import Header from "../header/Header";
import CreateInboxItemDialog from "../inbox/CreateInboxItemDialog";
import NavigationMenu from "../navigation-menu/NavigationMenu";
import { SidebarProvider, SidebarTrigger } from "../ui/sidebar";
import { Toaster } from "../ui/toaster";
import ConversationsSidebar from "./misc/ConversationsSidebar";

export type ChatLayoutProps = {
children: ReactNode;
};

const ChatLayoutInner: FC<ChatLayoutProps> = ({ children }) => {
const { toggleMenu } = useNavMenuContext();
const [isOpen, setIsOpen] = useState(false);
const { context: storedContext, setContext } = useContextContext();
const context = storedContext || "family";
const router = useRouter();

useEffect(
() =>
addKeyDownListener(router, setContext, toggleMenu, () => setIsOpen(true)),
[router, setContext, toggleMenu]
);

return (
<>
<Head>
<title>Impulso – Chat</title>
</Head>
<div className="flex flex-col items-center justify-center w-full">
<Header context={context} />
<NavigationMenu />
<main className="w-full">
<div className="flex flex-col px-2 lg:pr-4 mb-4 md:mb-8">
<SidebarProvider>
<ConversationsSidebar />
<div className="w-full px-0 mx-0">
<header className="sticky top-12 md:top-16 py-1 z-40 bg-bgTransparent">
<SidebarTrigger />
</header>
<div>{children}</div>
</div>
</SidebarProvider>
</div>
<Toaster />
<CreateInboxItemDialog open={isOpen} onOpenChange={setIsOpen} />
</main>
</div>
</>
);
};

const ChatLayout: FC<ChatLayoutProps> = (props) => (
<NavMenuContextProvider>
<ChatLayoutInner {...props} />
</NavMenuContextProvider>
);

export default ChatLayout;
4 changes: 2 additions & 2 deletions components/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const MainLayoutInner: FC<MainLayoutProps> = ({
);

return (
<div>
<>
<Head>
<title>{`Impulso ${
recordName ? `- ${recordName}` : ""
Expand All @@ -62,7 +62,7 @@ const MainLayoutInner: FC<MainLayoutProps> = ({
<CreateInboxItemDialog open={isOpen} onOpenChange={setIsOpen} />
</main>
</div>
</div>
</>
);
};

Expand Down
Loading

0 comments on commit 4cde690

Please sign in to comment.