diff --git a/client/src/app/(app)/_layout.tsx b/client/src/app/(app)/_layout.tsx index 81a8092..cb23acc 100644 --- a/client/src/app/(app)/_layout.tsx +++ b/client/src/app/(app)/_layout.tsx @@ -1,13 +1,62 @@ -import { Redirect } from "expo-router"; +import { Redirect, withLayoutContext } from "expo-router"; +import { + createDrawerNavigator, + DrawerContentComponentProps, + type DrawerNavigationOptions, +} from "@react-navigation/drawer"; +import { Platform } from "react-native"; +import { useEffect } from "react"; -import Drawer from "@/components/DrawerNav/Drawer"; import { useUserData } from "@/hooks/stores/useUserData"; +import { useBreakpoints } from "@/hooks/useBreakpoints"; +import ThreadHistory from "@/components/ThreadDrawer/ThreadHistory"; +import NativeSafeAreaView from "@/components/NativeSafeAreaView"; + +export const Drawer = withLayoutContext(createDrawerNavigator().Navigator); export default function HomeLayout() { const session = useUserData.use.session(); + const bp = useBreakpoints(); + + const screenOptions: DrawerNavigationOptions = { + drawerType: "slide", + drawerPosition: "left", + headerShown: false, + // TODO: "rgba(0, 0, 0, 0.5)" | "rgba(255, 255, 255, 0.1)" + overlayColor: Platform.OS === "web" ? "transparent" : "rgba(0, 0, 0, 0.5)", + drawerStyle: { ...(Platform.OS === "web" && bp.md && { width: 300 }) }, + }; if (!session) return ; - return ; + return ( + } + screenOptions={screenOptions} + initialRouteName="index" + /> + ); +} + +function DrawerContent(props: DrawerContentComponentProps) { + const { md } = useBreakpoints(); + + useEffect(() => { + if (Platform.OS === "web" && md) { + props.navigation.openDrawer(); + } else { + props.navigation.closeDrawer(); + } + }, [md]); + + return ( + + + + ); } export { ErrorBoundary } from "expo-router"; diff --git a/client/src/app/(app)/settings.tsx b/client/src/app/(app)/settings.tsx new file mode 100644 index 0000000..11f892d --- /dev/null +++ b/client/src/app/(app)/settings.tsx @@ -0,0 +1,7 @@ +import { SettingsView } from "@/views/settings/SettingsView"; + +export default function SettingsPage() { + return ; +} + +export { ErrorBoundary } from "expo-router"; diff --git a/client/src/app/(app)/tools.tsx b/client/src/app/(app)/tools.tsx new file mode 100644 index 0000000..9494bb4 --- /dev/null +++ b/client/src/app/(app)/tools.tsx @@ -0,0 +1,7 @@ +import { ToolView } from "@/views/tools/ToolView"; + +export default function Tools() { + return ; +} + +export { ErrorBoundary } from "expo-router"; diff --git a/client/src/app/+not-found.tsx b/client/src/app/+not-found.tsx index 7df34f9..67015f1 100644 --- a/client/src/app/+not-found.tsx +++ b/client/src/app/+not-found.tsx @@ -4,16 +4,16 @@ import { View } from "react-native"; import { Text } from "@/components/ui/Text"; export default function NotFoundScreen() { - return ( - <> - - - This screen doesn't exist. + return ( + <> + + + This screen doesn't exist. - - Go to home screen! - - - - ); + + Go to home screen! + + + + ); } diff --git a/client/src/components/ChatHistory.tsx b/client/src/components/ChatHistory.tsx index 32154f6..0db4f2f 100644 --- a/client/src/components/ChatHistory.tsx +++ b/client/src/components/ChatHistory.tsx @@ -2,76 +2,76 @@ import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, View } from "react import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/Button"; -import { AntDesign } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { - MessageGroup, - useGroupedMessages, + MessageGroup, + useGroupedMessages, } from "@/components/MessageGroups/MessageGroup"; export default function ChatHistory({ - isLoading, - threadId, + isLoading, + threadId, }: { - isLoading: boolean; - threadId: string; + isLoading: boolean; + threadId: string; }) { - const viewRef = useRef(null); - const scrollViewRef = useRef(null); - const [showScrollButton, setShowScrollButton] = useState(false); - const { messageGroups, isError } = useGroupedMessages(threadId); + const viewRef = useRef(null); + const scrollViewRef = useRef(null); + const [showScrollButton, setShowScrollButton] = useState(false); + const { messageGroups, isError } = useGroupedMessages(threadId); - const onScroll = (event: NativeSyntheticEvent) => { - const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; - const isCloseToBottom = - contentOffset.y + layoutMeasurement.height > contentSize.height - 50; - setShowScrollButton(!isCloseToBottom); - }; + const onScroll = (event: NativeSyntheticEvent) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; + const isCloseToBottom = + contentOffset.y + layoutMeasurement.height > contentSize.height - 50; + setShowScrollButton(!isCloseToBottom); + }; - const scrollToBottom = () => - messageGroups.length > 0 && - scrollViewRef.current?.scrollToEnd({ animated: true }); + const scrollToBottom = () => + messageGroups.length > 0 && + scrollViewRef.current?.scrollToEnd({ animated: true }); - useEffect(() => { - if (!viewRef.current) return; - viewRef.current.measure((x, y, width, height) => { - if (height > 0) scrollToBottom(); - }); - }, []); + useEffect(() => { + if (!viewRef.current) return; + viewRef.current.measure((x, y, width, height) => { + if (height > 0) scrollToBottom(); + }); + }, []); - if (isError) return null; - return ( - - {messageGroups.length > 0 && ( - - {messageGroups.map((item, index) => ( - - ))} - - )} - {showScrollButton && ( - - - - )} - - ); + if (isError) return null; + return ( + + {messageGroups.length > 0 && ( + + {messageGroups.map((item, index) => ( + + ))} + + )} + {showScrollButton && ( + + + + )} + + ); } function ScrollButton({ onPress }: { onPress: () => void }) { - return ( - - ); + return ( + + ); } diff --git a/client/src/components/ChatInput/ChatInputContainer.tsx b/client/src/components/ChatInput/ChatInputContainer.tsx index e20f0ba..57e540b 100644 --- a/client/src/components/ChatInput/ChatInputContainer.tsx +++ b/client/src/components/ChatInput/ChatInputContainer.tsx @@ -2,83 +2,83 @@ import { Platform, Pressable, View } from "react-native"; import { useEffect, useState } from "react"; import { FormSubmission } from "@/hooks/useChat"; -import { MaterialIcons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import ChatInput from "./ChatInput"; import { CommandTray } from "../CommandTray"; import { FileTray, FileInputButton } from "../FileTray"; export function ChatInputContainer({ - handleSubmit, - abort, - loading = false, - threadId, + handleSubmit, + abort, + loading = false, + threadId, }: { - handleSubmit: FormSubmission; - abort: () => void; - loading?: boolean; - threadId: string | null; + handleSubmit: FormSubmission; + abort: () => void; + loading?: boolean; + threadId: string | null; }) { - const [input, setInput] = useState(""); - useEffect(() => setInput(""), [threadId]); + const [input, setInput] = useState(""); + useEffect(() => setInput(""), [threadId]); - const handleSend = async () => { - try { - await handleSubmit(input); - setInput(""); - } catch (error) { - alert(error); - } - }; + const handleSend = async () => { + try { + await handleSubmit(input); + setInput(""); + } catch (error) { + alert(error); + } + }; - return ( - - - - - - - - {loading ? ( - - ) : ( - - )} - - - - ); + return ( + + + + + + + + {loading ? ( + + ) : ( + + )} + + + + ); } function SendButton({ handleSend }: { handleSend: () => void }) { - return ( - - - - ); + return ( + + + + ); } function StopButton({ abort }: { abort: () => void }) { - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/Dialogs/Command.web.tsx b/client/src/components/Dialogs/Command.web.tsx index b47d6f9..bd2828b 100644 --- a/client/src/components/Dialogs/Command.web.tsx +++ b/client/src/components/Dialogs/Command.web.tsx @@ -3,64 +3,66 @@ import * as React from "react"; import { - CommandDialog as CommandDialogComponent, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, + CommandDialog as CommandDialogComponent, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, } from "@/components/ui/Command"; -import { FontAwesome } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { useConfigStore } from "@/hooks/stores/configStore"; import { useAction } from "@/hooks/useAction"; export function CommandDialog({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: React.Dispatch>; + open: boolean; + setOpen: React.Dispatch>; }) { - const { threadId } = useConfigStore(); - const deleteThread = useAction("deleteThread")(); - const resetDb = useAction("resetDb")(); + const { threadId } = useConfigStore(); + const deleteThread = useAction("deleteThread")(); + const resetDb = useAction("resetDb")(); - const items = [ - { - label: "Delete Active Thread", - Icon: FontAwesome, - iconName: "trash" as const, - onClick: () => deleteThread.action(threadId!), - hidden: !threadId, - }, - { - label: "Reset DB", - Icon: FontAwesome, - iconName: "database" as const, - onClick: resetDb.action, - }, - ]; + const items = [ + { + label: "Delete Active Thread", + Icon: Icon, + type: "FontAwesome" as const, + iconName: "trash" as const, + onClick: () => deleteThread.action(threadId!), + hidden: !threadId, + }, + { + label: "Reset DB", + Icon: Icon, + type: "FontAwesome" as const, + iconName: "database" as const, + onClick: resetDb.action, + }, + ]; - return ( - - - - No results found. - - {items.map(({ label, Icon, iconName, onClick, hidden }, i) => - !hidden ? ( - - - {label} - - ) : null - )} - - - - ); + return ( + + + + No results found. + + {items.map(({ label, Icon, type, iconName, onClick, hidden }, i) => + !hidden ? ( + + + {label} + + ) : null + )} + + + + ); } diff --git a/client/src/components/DrawerNav/Drawer.tsx b/client/src/components/DrawerNav/Drawer.tsx deleted file mode 100644 index c4702e3..0000000 --- a/client/src/components/DrawerNav/Drawer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { createDrawerNavigator, DrawerNavigationOptions } from "@react-navigation/drawer"; -import { Platform } from "react-native"; -import { withLayoutContext } from "expo-router"; - -import { useBreakpoints } from "@/hooks/useBreakpoints"; -import DrawerContent from "./DrawerContent"; - -export const Drawer = withLayoutContext(createDrawerNavigator().Navigator); - -export default function CustomDrawer() { - const bp = useBreakpoints(); - - const screenOptions: DrawerNavigationOptions = { - drawerType: "slide", - drawerPosition: "left", - headerShown: false, - // TODO: "rgba(0, 0, 0, 0.5)" | "rgba(255, 255, 255, 0.1)" - overlayColor: Platform.OS === "web" ? "transparent" : "rgba(0, 0, 0, 0.5)", - drawerStyle: { ...(Platform.OS === "web" && bp.md && { width: 300 }) }, - }; - - return ( - } - screenOptions={screenOptions} - initialRouteName="index" - > - - - - ); -} diff --git a/client/src/components/DrawerNav/DrawerContent.tsx b/client/src/components/DrawerNav/DrawerContent.tsx deleted file mode 100644 index faa3794..0000000 --- a/client/src/components/DrawerNav/DrawerContent.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { type DrawerContentComponentProps } from "@react-navigation/drawer"; -import { useEffect } from "react"; -import { Platform } from "react-native"; - -import { useBreakpoints } from "@/hooks/useBreakpoints"; -import ThreadHistory from "../ThreadDrawer/ThreadHistory"; -import NativeSafeAreaView from "../NativeSafeAreaView"; - -export default function DrawerContent(props: DrawerContentComponentProps) { - const { md } = useBreakpoints(); - - useEffect(() => { - if (Platform.OS === "web" && md) { - props.navigation.openDrawer(); - } else { - props.navigation.closeDrawer(); - } - }, [md]); - - return ( - - - - ); -} diff --git a/client/src/components/FileTray/DeleteButton.tsx b/client/src/components/FileTray/DeleteButton.tsx index dbdb3bf..0c06501 100644 --- a/client/src/components/FileTray/DeleteButton.tsx +++ b/client/src/components/FileTray/DeleteButton.tsx @@ -1,56 +1,58 @@ import { Pressable } from "react-native"; import { cn } from "@/lib/utils"; -import { MaterialIcons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { useFileStore } from "@/hooks/stores/fileStore"; import { useHoverHelper } from "@/hooks/useHoverHelper"; import { FileInformation } from "@/hooks/useFileInformation"; export function RemoveFileButton({ file }: { file: FileInformation }) { - const removeFile = useFileStore((state) => state.removeFile); - const { isHover, ...helpers } = useHoverHelper(); - return ( - { - e.stopPropagation(); - removeFile(file); - }} - className={cn( - "absolute z-10 flex items-center justify-center w-4 h-4 rounded-full -top-2 -right-2 group", - !isHover ? "bg-background" : "bg-foreground/20" - )} - > - - - ); + const removeFile = useFileStore((state) => state.removeFile); + const { isHover, ...helpers } = useHoverHelper(); + return ( + { + e.stopPropagation(); + removeFile(file); + }} + className={cn( + "absolute z-10 flex items-center justify-center w-4 h-4 rounded-full -top-2 -right-2 group", + !isHover ? "bg-background" : "bg-foreground/20" + )} + > + + + ); } export function RemoveFolderButton({ files }: { files: FileInformation[] }) { - const removeFiles = useFileStore((state) => state.removeFiles); - const { isHover, ...helpers } = useHoverHelper(); + const removeFiles = useFileStore((state) => state.removeFiles); + const { isHover, ...helpers } = useHoverHelper(); - return ( - { - e.stopPropagation(); - removeFiles(files); - }} - className={cn( - "absolute z-10 flex items-center justify-center w-4 h-4 rounded-full -top-2 -right-2", - !isHover ? "bg-background" : "bg-foreground/20" - )} - > - - - ); + return ( + { + e.stopPropagation(); + removeFiles(files); + }} + className={cn( + "absolute z-10 flex items-center justify-center w-4 h-4 rounded-full -top-2 -right-2", + !isHover ? "bg-background" : "bg-foreground/20" + )} + > + + + ); } diff --git a/client/src/components/FileTray/FileTray.tsx b/client/src/components/FileTray/FileTray.tsx index 9b1f4b7..9cd3ac4 100644 --- a/client/src/components/FileTray/FileTray.tsx +++ b/client/src/components/FileTray/FileTray.tsx @@ -3,40 +3,45 @@ import * as DocumentPicker from "expo-document-picker"; import { useFileStore } from "@/hooks/stores/fileStore"; import { FileButton } from "./FileButton"; -import { FontAwesome } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { parseLocalFiles } from "@/hooks/useFileInformation"; export function FileTray() { - const fileList = useFileStore((state) => state.fileList); + const fileList = useFileStore((state) => state.fileList); - if (!fileList.length) return null; - return ( - - {fileList.map((file, index) => ( - - ))} - - ); + if (!fileList.length) return null; + return ( + + {fileList.map((file, index) => ( + + ))} + + ); } export function FileInputButton() { - const addFiles = useFileStore((state) => state.addFiles); - const triggerFileInput = async () => { - const res = await DocumentPicker.getDocumentAsync({ multiple: true }); - if (!res.assets) return console.warn("No files selected"); - const files = await parseLocalFiles(res.assets); - if (res.assets && res.assets.length > 0) addFiles(files); - if (res.canceled) console.log({ res }); - }; + const addFiles = useFileStore((state) => state.addFiles); + const triggerFileInput = async () => { + const res = await DocumentPicker.getDocumentAsync({ multiple: true }); + if (!res.assets) return console.warn("No files selected"); + const files = await parseLocalFiles(res.assets); + if (res.assets && res.assets.length > 0) addFiles(files); + if (res.canceled) console.log({ res }); + }; - // TODO: Fix uploads on native - return null; - return ( - - - - ); + // TODO: Fix uploads on native + return null; + return ( + + + + ); } diff --git a/client/src/components/FileTray/FileTray.web.tsx b/client/src/components/FileTray/FileTray.web.tsx index c8a0cd6..7be927c 100644 --- a/client/src/components/FileTray/FileTray.web.tsx +++ b/client/src/components/FileTray/FileTray.web.tsx @@ -2,7 +2,7 @@ import React, { useRef } from "react"; import { Pressable, View } from "react-native"; import { useFileStore } from "@/hooks/stores/fileStore"; -import { Feather, FontAwesome } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { FileRouter } from "../FileRouter"; import { parseLocalFiles } from "@/hooks/useFileInformation"; import { FolderButton } from "./FolderButton"; @@ -72,14 +72,24 @@ export function FileInputButton() { onPress={() => fileInputRef.current?.click()} className="absolute left-0 p-1 bg-transparent rounded-full md:left-2" > - + directoryInputRef.current?.click()} className="absolute p-1 bg-transparent rounded-full left-6 md:left-8" > - + ); diff --git a/client/src/components/Markdown/CodeBlock.tsx b/client/src/components/Markdown/CodeBlock.tsx index 4c01ea4..a68094d 100644 --- a/client/src/components/Markdown/CodeBlock.tsx +++ b/client/src/components/Markdown/CodeBlock.tsx @@ -3,7 +3,7 @@ import * as Clipboard from "expo-clipboard"; import { Text } from "../ui/Text"; import SyntaxHighlighter from "./SyntaxHighlighter"; -import { Feather } from "../ui/Icon"; +import { Icon } from "../ui/Icon"; import { useState } from "react"; export function CodeBlock({ @@ -43,7 +43,7 @@ function CopyButton({ content }: { content: string }) { className="flex flex-row items-center gap-1 text-secondary-foreground/60 hover:text-secondary-foreground" > <> - {" "} + {" "} {copied ? "Copied!" : "Copy"} diff --git a/client/src/components/MessageGroups/Message/MessageActions.web.tsx b/client/src/components/MessageGroups/Message/MessageActions.web.tsx index 4e741c7..1373e4b 100644 --- a/client/src/components/MessageGroups/Message/MessageActions.web.tsx +++ b/client/src/components/MessageGroups/Message/MessageActions.web.tsx @@ -2,7 +2,7 @@ import { Pressable, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { useGroupStore } from "../GroupStore"; -import { FontAwesome6, Octicons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { Message } from "@/types"; import { useMessageDelete } from "@/hooks/fetchers/Message/useMessageDelete"; import { ChatMessageGroup } from "../MessageGroup"; @@ -37,21 +37,21 @@ export function MessageActions({ const actions = [ { - IconProvider: Octicons, + iconType: "FontAwesome6", icon: "pencil", onPress: toggleEditMode, - }, + } as const, { - IconProvider: FontAwesome6, + iconType: "FontAwesome6", icon: "clipboard", onPress: copyToClipboard, hidden: message.content === null, - }, + } as const, { - IconProvider: Octicons, + iconType: "FontAwesome6", icon: "trash", onPress: () => deleteMessage(), - }, + } as const, ]; return ( @@ -60,17 +60,18 @@ export function MessageActions({ {!editMode && ( - {actions.map(({ IconProvider, icon, onPress, hidden }, i) => + {actions.map(({ icon, iconType, onPress, hidden }, i) => !hidden ? ( - ) : null diff --git a/client/src/components/ThreadDrawer/AgentsButton.tsx b/client/src/components/ThreadDrawer/AgentsButton.tsx index 454a8c5..b50e627 100644 --- a/client/src/components/ThreadDrawer/AgentsButton.tsx +++ b/client/src/components/ThreadDrawer/AgentsButton.tsx @@ -1,11 +1,15 @@ +import { usePathname } from "expo-router"; + import { Text } from "@/components/ui/Text"; import LinkButton from "./LinkButton"; -import { Ionicons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; export function AgentsButton() { + const path = usePathname(); return ( - - + { - href: Href; - children?: React.ReactNode; - className?: string; + href: Href; + children?: React.ReactNode; + className?: string; + active?: boolean; } export default function LinkButton({ - href, - children, - className, + href, + children, + className, + active, }: LinkButtonProps) { - return ( - - - - ); + return ( + + + + {children} + + + + ); } diff --git a/client/src/components/ThreadDrawer/NewChatButton.tsx b/client/src/components/ThreadDrawer/NewChatButton.tsx index d1ba37d..81adf9e 100644 --- a/client/src/components/ThreadDrawer/NewChatButton.tsx +++ b/client/src/components/ThreadDrawer/NewChatButton.tsx @@ -1,11 +1,12 @@ import { Text } from "@/components/ui/Text"; import LinkButton from "./LinkButton"; -import { MaterialIcons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; export default function NewChatButton() { return ( - + + Settings + + ); +} diff --git a/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.tsx b/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.tsx deleted file mode 100644 index 1015959..0000000 --- a/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { MaterialIcons } from "@/components/ui/Icon"; -import LinkButton from "../LinkButton"; -import { Text } from "@/components/ui/Text"; - -export function SettingsButton() { - return ( - - - Settings - - ); -} diff --git a/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.web.tsx b/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.web.tsx deleted file mode 100644 index 2a3dc3d..0000000 --- a/client/src/components/ThreadDrawer/SettingsButton/SettingsButton.web.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { MaterialIcons } from "@/components/ui/Icon"; -import { Text } from "@/components/ui/Text"; -import SettingsDialog from "@/views/settings/SettingsDialog.web"; -import { Button } from "@/components/ui/Button"; - -export function SettingsButton() { - return ( - - - - ); -} diff --git a/client/src/components/ThreadDrawer/SettingsButton/index.ts b/client/src/components/ThreadDrawer/SettingsButton/index.ts deleted file mode 100644 index 0c0763d..0000000 --- a/client/src/components/ThreadDrawer/SettingsButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SettingsButton } from "./SettingsButton"; diff --git a/client/src/components/ThreadDrawer/ThreadButton/ThreadButton.web.tsx b/client/src/components/ThreadDrawer/ThreadButton/ThreadButton.web.tsx index 86b74de..98eb1ea 100644 --- a/client/src/components/ThreadDrawer/ThreadButton/ThreadButton.web.tsx +++ b/client/src/components/ThreadDrawer/ThreadButton/ThreadButton.web.tsx @@ -6,11 +6,14 @@ import { Text } from "@/components/ui/Text"; import LinkButton from "../LinkButton"; import { ThreadButtonPopover } from "./ThreadButtonPopover"; +import { useConfigStore } from "@/hooks/stores/configStore"; export function ThreadButton({ thread }: { thread: Thread }) { + const threadId = useConfigStore.use.threadId(); return ( diff --git a/client/src/components/ThreadDrawer/ThreadButton/ThreadButtonPopover.tsx b/client/src/components/ThreadDrawer/ThreadButton/ThreadButtonPopover.tsx index 246e050..944f986 100644 --- a/client/src/components/ThreadDrawer/ThreadButton/ThreadButtonPopover.tsx +++ b/client/src/components/ThreadDrawer/ThreadButton/ThreadButtonPopover.tsx @@ -5,55 +5,55 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { Thread } from "@/types"; import { useUserData } from "@/hooks/stores/useUserData"; import { useAction } from "@/hooks/useAction"; -import { FontAwesome } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { Text } from "@/components/ui/Text"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover"; import { Button } from "@/components/ui/Button"; -import { AntDesign } from "@/components/ui/Icon"; export function ThreadButtonPopover({ thread }: { thread: Thread }) { - const session = useUserData((s) => s.session); - const deleteThread = useAction("deleteThread")(); + const session = useUserData((s) => s.session); + const deleteThread = useAction("deleteThread")(); - const insets = useSafeAreaInsets(); - const contentInsets = { - top: insets.top, - bottom: insets.bottom, - left: 12, - right: 12, - }; + const insets = useSafeAreaInsets(); + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; - const items1 = [ - { - icon: , - label: "Delete Thread", - onClick: async () => { - if (!thread.id || !session) return console.error("No threadId or userId"); - deleteThread.action(thread.id); - }, - }, - ]; + const items1 = [ + { + icon: , + label: "Delete Thread", + onClick: async () => { + if (!thread.id || !session) return console.error("No threadId or userId"); + deleteThread.action(thread.id); + }, + }, + ]; - return ( - - - - - - {items1.map((item, i) => ( - - ))} - - - ); + return ( + + + + + + {items1.map((item, i) => ( + + ))} + + + ); } diff --git a/client/src/components/ThreadDrawer/ThreadHistory.tsx b/client/src/components/ThreadDrawer/ThreadHistory.tsx index 9a191d0..e2dc950 100644 --- a/client/src/components/ThreadDrawer/ThreadHistory.tsx +++ b/client/src/components/ThreadDrawer/ThreadHistory.tsx @@ -7,6 +7,7 @@ import NewChatButton from "./NewChatButton"; import { AgentsButton } from "./AgentsButton"; import { useThreadListQuery } from "@/hooks/fetchers/Thread/useThreadListQuery"; import { useThreadGroups, ThreadGroup } from "./ThreadGroups"; +import { ToolsButton } from "./ToolsButton"; export default function ThreadHistory() { return ( @@ -14,6 +15,7 @@ export default function ThreadHistory() { + diff --git a/client/src/components/ThreadDrawer/ToolsButton.tsx b/client/src/components/ThreadDrawer/ToolsButton.tsx new file mode 100644 index 0000000..3cca000 --- /dev/null +++ b/client/src/components/ThreadDrawer/ToolsButton.tsx @@ -0,0 +1,22 @@ +import { usePathname } from "expo-router"; + +import { Text } from "@/components/ui/Text"; +import LinkButton from "./LinkButton"; +import { Icon } from "@/components/ui/Icon"; + +export function ToolsButton() { + const path = usePathname(); + return ( + + + + Tools + + + ); +} diff --git a/client/src/components/ui/Command.tsx b/client/src/components/ui/Command.tsx index 6ca05a9..22911e0 100644 --- a/client/src/components/ui/Command.tsx +++ b/client/src/components/ui/Command.tsx @@ -4,148 +4,148 @@ import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; -import { FontAwesome } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/Dialog"; const Command = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); Command.displayName = CommandPrimitive.displayName; interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - - - - {children} - - - - ); + return ( + + + + {children} + + + + ); }; const CommandInput = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
- - -
+
+ + +
)); CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >((props, ref) => ( - + )); CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => { - return ( - - ); + return ( + + ); }; CommandShortcut.displayName = "CommandShortcut"; export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, }; diff --git a/client/src/components/ui/Dialog.tsx b/client/src/components/ui/Dialog.tsx index 71da658..3e64eee 100644 --- a/client/src/components/ui/Dialog.tsx +++ b/client/src/components/ui/Dialog.tsx @@ -6,6 +6,7 @@ import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import * as DialogPrimitive from "@/components/primitives/dialog"; import { cn } from "@/lib/utils"; import { useColorScheme } from "@/hooks/useColorScheme"; +import { ToastPortal } from "@/providers/ToastProvider"; const Dialog = DialogPrimitive.Root; @@ -16,147 +17,148 @@ const DialogPortal = DialogPrimitive.Portal; const DialogClose = DialogPrimitive.Close; const DialogOverlayWeb = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - const { open } = DialogPrimitive.useRootContext(); - return ( - - ); + const { open } = DialogPrimitive.useRootContext(); + return ( + + ); }); DialogOverlayWeb.displayName = "DialogOverlayWeb"; const DialogOverlayNative = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => { - return ( - - - <>{children} - - - ); + return ( + + + <>{children} + + + ); }); DialogOverlayNative.displayName = "DialogOverlayNative"; const DialogOverlay = Platform.select({ - web: DialogOverlayWeb, - default: DialogOverlayNative, + web: DialogOverlayWeb, + default: DialogOverlayNative, }); const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => { - const { open } = DialogPrimitive.useRootContext(); - const { themeStyles } = useColorScheme(); - return ( - - - - {children} - - - - ); + const { open } = DialogPrimitive.useRootContext(); + const { themeStyles } = useColorScheme(); + return ( + + + + {children} + + + + + ); }); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ - className, - ...props + className, + ...props }: React.ComponentPropsWithoutRef) => ( - + ); DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ - className, - ...props + className, + ...props }: React.ComponentPropsWithoutRef) => ( - + ); DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogDescription.displayName = DialogPrimitive.Description.displayName; export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, }; diff --git a/client/src/components/ui/Icon.tsx b/client/src/components/ui/Icon.tsx index 920c555..4eb0852 100644 --- a/client/src/components/ui/Icon.tsx +++ b/client/src/components/ui/Icon.tsx @@ -1,46 +1,49 @@ import { cssInterop } from "nativewind"; import { - AntDesign, - EvilIcons, - Entypo, - Feather, - FontAwesome, - FontAwesome5, - FontAwesome6, - Fontisto, - Foundation, - Ionicons, - MaterialCommunityIcons, - MaterialIcons, - Octicons, - SimpleLineIcons, - Zocial, + AntDesign, + EvilIcons, + Entypo, + Feather, + FontAwesome, + FontAwesome5, + FontAwesome6, + Fontisto, + Foundation, + Ionicons, + MaterialCommunityIcons, + MaterialIcons, + Octicons, + SimpleLineIcons, + Zocial, } from "@expo/vector-icons"; import { IconProps as baseProps } from "@expo/vector-icons/build/createIconSet"; +import { useContext } from "react"; +import { TextClassContext } from "./Text"; +import { cn } from "@/lib/utils"; const IconProviders = { - AntDesign, - EvilIcons, - Entypo, - Feather, - FontAwesome, - FontAwesome5, - FontAwesome6, - Fontisto, - Foundation, - Ionicons, - MaterialCommunityIcons, - MaterialIcons, - Octicons, - SimpleLineIcons, - Zocial, -}; + AntDesign, + EvilIcons, + Entypo, + Feather, + FontAwesome, + FontAwesome5, + FontAwesome6, + Fontisto, + Foundation, + Ionicons, + MaterialCommunityIcons, + MaterialIcons, + Octicons, + SimpleLineIcons, + Zocial, +} as const; type IconProviders = typeof IconProviders; type IconType = keyof IconProviders; // Discriminated Union Type type IconComponentType = { - [K in IconType]: { type: K; name: keyof IconProviders[K]["glyphMap"] }; + [K in IconType]: { type: K; name: keyof IconProviders[K]["glyphMap"] }; }[IconType]; export type IconProps = IconComponentType & baseProps; @@ -51,32 +54,28 @@ export type IconProps = IconComponentType & baseProps; * @param name - Icon name * @param props - Icon props * */ -export function Icon({ type, name, ...props }: IconProps) { - const IconProvider = IconProviders[type]; - return ; +export function Icon({ type, name, className, ...props }: IconProps) { + const IconProvider = IconProviders[type]; + const textClass = useContext(TextClassContext); + return ( + + ); } Object.values(IconProviders).forEach((provider) => { - cssInterop(provider, { - className: { - target: "style", - nativeStyleToProp: { - color: true, - }, - }, - }); + cssInterop(provider, { + className: { + target: "style", + nativeStyleToProp: { + color: true, + }, + }, + }); }); // test types //export const b = ; - -export { - Feather, - FontAwesome, - FontAwesome6, - MaterialIcons, - AntDesign, - Entypo, - Octicons, - Ionicons, -}; diff --git a/client/src/components/ui/Select.tsx b/client/src/components/ui/Select.tsx index 70bcf4e..bcb8a17 100644 --- a/client/src/components/ui/Select.tsx +++ b/client/src/components/ui/Select.tsx @@ -3,7 +3,7 @@ import { Platform, StyleSheet, View } from "react-native"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import * as SelectPrimitive from "@/components/primitives/select"; import { cn } from "@/lib/utils"; -import { AntDesign } from "./Icon"; +import { Icon } from "./Icon"; type Option = SelectPrimitive.Option; @@ -14,26 +14,27 @@ const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - span]:line-clamp-1", - props.disabled && "web:cursor-not-allowed opacity-50", - className - )} - {...props} - > - <>{children} - - + span]:line-clamp-1", + props.disabled && "web:cursor-not-allowed opacity-50", + className + )} + {...props} + > + <>{children} + + )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; @@ -41,161 +42,161 @@ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; * Platform: WEB ONLY */ const SelectScrollUpButton = ({ - className, - ...props + className, + ...props }: React.ComponentPropsWithoutRef) => { - if (Platform.OS !== "web") { - return null; - } - return ( - - - - ); + if (Platform.OS !== "web") { + return null; + } + return ( + + + + ); }; /** * Platform: WEB ONLY */ const SelectScrollDownButton = ({ - className, - ...props + className, + ...props }: React.ComponentPropsWithoutRef) => { - if (Platform.OS !== "web") { - return null; - } - return ( - - - - ); + if (Platform.OS !== "web") { + return null; + } + return ( + + + + ); }; const SelectContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - container?: HTMLElement | null; - } + React.ElementRef, + React.ComponentPropsWithoutRef & { + container?: HTMLElement | null; + } >(({ className, children, position = "popper", container, ...props }, ref) => { - const { open } = SelectPrimitive.useRootContext(); + const { open } = SelectPrimitive.useRootContext(); - return ( - - - - - - - {children} - - - - - - - ); + return ( + + + + + + + {children} + + + + + + + ); }); SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - - - - + + + + + + - - + + )); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, - type Option, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, + type Option, }; diff --git a/client/src/hooks/fetchers/Agent/useAgentQuery.ts b/client/src/hooks/fetchers/Agent/useAgentQuery.ts index d5c5028..70dac1b 100644 --- a/client/src/hooks/fetchers/Agent/useAgentQuery.ts +++ b/client/src/hooks/fetchers/Agent/useAgentQuery.ts @@ -1,7 +1,7 @@ import { queryOptions, useQuery } from "@tanstack/react-query"; import { useUserData } from "@/hooks/stores/useUserData"; -import type { Agent, ToolName } from "@/types"; +import type { Agent } from "@/types"; import { fetcher } from "@/lib/fetcher"; /** Fetch agent by ID */ @@ -13,20 +13,7 @@ export const agentQueryOptions = (apiKey: string, agentId: string) => { }); }; -/** Fetch list of available models */ -export const toolQueryOptions = (apiKey: string) => { - return queryOptions({ - queryKey: ["tools", apiKey], - queryFn: () => fetcher(`/agents/tools`, { apiKey }), - }); -}; - export const useAgentQuery = (agentId: string) => { const apiKey = useUserData((s) => s.apiKey); return useQuery(agentQueryOptions(apiKey, agentId)); }; - -export const useToolsQuery = () => { - const apiKey = useUserData((s) => s.apiKey); - return useQuery(toolQueryOptions(apiKey)); -}; diff --git a/client/src/hooks/fetchers/AgentTool/useAgentToolPatch.ts b/client/src/hooks/fetchers/AgentTool/useAgentToolPatch.ts new file mode 100644 index 0000000..3b46fd4 --- /dev/null +++ b/client/src/hooks/fetchers/AgentTool/useAgentToolPatch.ts @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useUserData } from "@/hooks/stores/useUserData"; +import { fetcher } from "@/lib/fetcher"; +import { AgentTool, AgentToolUpdateSchema } from "@/types"; +import { agentToolQueryOptions } from "./useAgentToolQuery"; +import { agentsQueryOptions } from "../Agent/useAgentsQuery"; +import { agentQueryOptions } from "../Agent/useAgentQuery"; + +export type PatchAgentToolOptions = { + agentId: string; + toolId: string; + agentToolConfig: AgentToolUpdateSchema; +}; + +const fetch = async ( + { agentId, toolId, agentToolConfig }: PatchAgentToolOptions, + apiKey: string +) => + fetcher(`/agents/${agentId}/tool/${toolId}`, { + apiKey, + method: "PATCH", + body: JSON.stringify(agentToolConfig), + }); + +/** Patch an Agent object */ +export const useAgentToolPatch = () => { + const apiKey = useUserData((s) => s.apiKey); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["patchAgentTool"], + mutationFn: async (opts: PatchAgentToolOptions) => fetch(opts, apiKey), + onMutate: async ({ agentId, toolId, agentToolConfig }: PatchAgentToolOptions) => { + if (!agentToolConfig.type || !agentToolConfig.value) + return console.error("Invalid config"); + const agentQuery = agentQueryOptions(apiKey, agentId); + const agentsQuery = agentsQueryOptions(apiKey); + const agentToolQuery = agentToolQueryOptions(apiKey, agentId, toolId); + + const prevAgentTool = queryClient.getQueryData(agentToolQuery.queryKey); + if (!prevAgentTool) return console.error("No cached agent tool found"); + await Promise.all([ + queryClient.cancelQueries(agentQuery), + queryClient.cancelQueries(agentsQuery), + queryClient.cancelQueries(agentToolQuery), + ]); + + const agentTool = { + ...prevAgentTool, + ...{ [agentToolConfig.type as any]: agentToolConfig.value }, + } as AgentToolUpdateSchema; + queryClient.setQueryData(agentToolQuery.queryKey, agentTool); + + return { agentTool }; + }, + onError: (error, { agentId, toolId }, context) => { + if (agentId && context?.agentTool) + queryClient.setQueryData( + agentToolQueryOptions(apiKey, agentId, toolId).queryKey, + context?.agentTool + ); + console.error(error); + }, + onSettled: async (res, err, { agentId }) => { + await Promise.all([ + queryClient.invalidateQueries(agentQueryOptions(apiKey, agentId)), + queryClient.invalidateQueries(agentsQueryOptions(apiKey)), + ]); + }, + }); +}; diff --git a/client/src/hooks/fetchers/AgentTool/useAgentToolQuery.ts b/client/src/hooks/fetchers/AgentTool/useAgentToolQuery.ts new file mode 100644 index 0000000..bfea2b2 --- /dev/null +++ b/client/src/hooks/fetchers/AgentTool/useAgentToolQuery.ts @@ -0,0 +1,37 @@ +import { queryOptions, useQuery } from "@tanstack/react-query"; + +import { useUserData } from "@/hooks/stores/useUserData"; +import type { AgentTool, ToolName } from "@/types"; +import { fetcher } from "@/lib/fetcher"; + +/** Fetch agent by ID */ +export const agentToolQueryOptions = ( + apiKey: string, + agentId: string, + toolId: string +) => { + return queryOptions({ + queryKey: ["agentTool", agentId, toolId, apiKey], + enabled: !!agentId && !!toolId, + queryFn: () => + fetcher(`/agents/${agentId}/tool/${toolId}`, { apiKey }), + }); +}; + +/** Fetch list of available models */ +export const toolQueryOptions = (apiKey: string) => { + return queryOptions({ + queryKey: ["tools", apiKey], + queryFn: () => fetcher(`/agents/tools`, { apiKey }), + }); +}; + +export const useAgentToolQuery = (agentId: string, toolId: string) => { + const apiKey = useUserData((s) => s.apiKey); + return useQuery(agentToolQueryOptions(apiKey, agentId, toolId)); +}; + +export const useToolsQuery = () => { + const apiKey = useUserData((s) => s.apiKey); + return useQuery(toolQueryOptions(apiKey)); +}; diff --git a/client/src/providers/AuthProvider.tsx b/client/src/providers/AuthProvider.tsx index 4160ffc..0fc9654 100644 --- a/client/src/providers/AuthProvider.tsx +++ b/client/src/providers/AuthProvider.tsx @@ -32,12 +32,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return; } } - console.error(error); - return Toast.show({ - type: "error", - text1: "User Error", - text2: error.message, - }); }; useEffect(() => { diff --git a/client/src/providers/QueryClientProvider.tsx b/client/src/providers/QueryClientProvider.tsx index 588d154..9240d5d 100644 --- a/client/src/providers/QueryClientProvider.tsx +++ b/client/src/providers/QueryClientProvider.tsx @@ -22,22 +22,53 @@ const queryClient = new QueryClient({ queries: { retry: false, gcTime: 1000 * 60 * 60 * 24, // 24 hours + throwOnError: (error) => !isFetchError(error), }, }, queryCache: new QueryCache({ - onError: (error) => { + onError: async (error, query) => { if (!isFetchError(error)) return; - if (error.status && error.status >= 500) { - Toast.show({ - type: "error", - text1: error.name, - text2: error.message, - }); + + if (error.status) { + if (error.status >= 500) { + Toast.show({ + type: "error", + text1: `Server Error ${error.status}`, + text2: error.message, + }); + await queryClient.cancelQueries(); + console.log(`Query Error Code: ${error.status}`); + console.log(`Cancelled query: ${query.options.queryKey}`); + } else if (error.status === 429) { + Toast.show({ + type: "error", + text1: "Rate Limit Exceeded", + text2: "Retrrying in 1 minute.", + }); + await queryClient.cancelQueries(); + setTimeout(() => { + console.log( + "Retrying query after rate limit", + query.options.queryKey + ); + queryClient.invalidateQueries(); + }, 1000 * 60); + } else { + Toast.show({ + type: "error", + text1: error.name, + text2: error.message, + }); + await queryClient.cancelQueries(); + console.log("providerCache DEFAULT", { error, query }); + console.log(`Query Error Code: ${error.status}`); + console.log(`Cancelled query: ${query.options.queryKey}`); + } } }, }), mutationCache: new MutationCache({ - onError: (error) => { + onError: async (error, vars, ctx, mut) => { if (!isFetchError(error)) return; if (error.status && error.status >= 500) { Toast.show({ diff --git a/client/src/providers/ToastProvider.tsx b/client/src/providers/ToastProvider.tsx index 21f10f8..2342c51 100644 --- a/client/src/providers/ToastProvider.tsx +++ b/client/src/providers/ToastProvider.tsx @@ -8,60 +8,67 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/Alert"; import { IconProps } from "@/components/ui/Icon"; const SuccessIconProps: IconProps = { - type: "AntDesign", - name: "check", + type: "AntDesign", + name: "check", }; const ErrorIconProps: IconProps = { - type: "AntDesign", - name: "closecircle", + type: "AntDesign", + name: "closecircle", }; const InfoIconProps: IconProps = { - type: "AntDesign", - name: "infocirlce", + type: "AntDesign", + name: "infocirlce", }; /** * @docs https://github.com/calintamas/react-native-toast-message */ const TOAST_CONFIG: ToastConfig = { - success: ({ text1, text2, onPress, props: { iconProps = SuccessIconProps } }) => ( - - - {text1} - {text2} - - - ), - error: ({ text1, text2, onPress, props: { iconProps = ErrorIconProps } }) => ( - - - {text1} - {text2} - - - ), - base: ({ text1, text2, onPress, props: { iconProps = InfoIconProps } }) => ( - - - {text1} - {text2} - - - ), + success: ({ text1, text2, onPress, props: { iconProps = SuccessIconProps } }) => ( + + + {text1} + {text2} + + + ), + error: ({ text1, text2, onPress, props: { iconProps = ErrorIconProps } }) => ( + + + {text1} + {text2} + + + ), + base: ({ text1, text2, onPress, props: { iconProps = InfoIconProps } }) => ( + + + {text1} + {text2} + + + ), }; +export function ToastPortal() { + const insets = useSafeAreaInsets(); + + return ( + + ); +} + export function ToastProvider({ children }: { children: React.ReactNode }) { - const insets = useSafeAreaInsets(); - return ( - - {children} - - - ); + return ( + + {children} + + + ); } diff --git a/client/src/types/index.ts b/client/src/types/index.ts index f20a253..6e3f1b8 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -14,9 +14,13 @@ export type { AgentObjectSchema as Agent, AgentCreateSchema, AgentUpdateSchema, - AgentToolSchema as AgentTool, } from "@db/Agent/AgentSchema"; +export type { + AgentToolSchema as AgentTool, + AgentToolUpdateSchema, +} from "@db/AgentTool/AgentToolSchema"; + export type { ToolName } from "@db/LLMNexus/Tools"; export type { diff --git a/client/src/views/DrawerScreenWrapper.tsx b/client/src/views/DrawerScreenWrapper.tsx new file mode 100644 index 0000000..8253220 --- /dev/null +++ b/client/src/views/DrawerScreenWrapper.tsx @@ -0,0 +1,22 @@ +import { + KeyboardAvoidingView, + Platform, + SafeAreaView, + TouchableWithoutFeedback, + Keyboard, +} from "react-native"; + +export function DrawerScreenWrapper({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} diff --git a/client/src/views/DrawerScreenWrapper.web.tsx b/client/src/views/DrawerScreenWrapper.web.tsx new file mode 100644 index 0000000..232fd98 --- /dev/null +++ b/client/src/views/DrawerScreenWrapper.web.tsx @@ -0,0 +1,38 @@ +import { Pressable, View } from "react-native"; +import { useDrawerStatus } from "@react-navigation/drawer"; +import { DrawerActions, ParamListBase, useNavigation } from "@react-navigation/native"; +import { DrawerNavigationProp } from "@react-navigation/drawer"; + +import { Icon } from "@/components/ui/Icon"; + +export function DrawerScreenWrapper({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + ); +} + +function CollapseDrawer() { + const navigation = useNavigation>(); + const isDrawerOpen = useDrawerStatus() === "open"; + + return ( + + navigation.dispatch(DrawerActions.toggleDrawer())} + > + + + + ); +} diff --git a/client/src/views/agent/helpers/ToolSection.tsx b/client/src/views/agent/helpers/ToolSection.tsx index 561a53a..094ec53 100644 --- a/client/src/views/agent/helpers/ToolSection.tsx +++ b/client/src/views/agent/helpers/ToolSection.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Pressable, View } from "react-native"; import { Agent, AgentUpdateSchema, ToolName } from "@/types"; -import { useToolsQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; +import { useToolsQuery } from "@/hooks/fetchers/AgentTool/useAgentToolQuery"; import { Section } from "@/components/ui/Section"; import { Text } from "@/components/ui/Text"; import { Checkbox } from "@/components/ui/Checkboz"; diff --git a/client/src/views/agents/AgentsView.tsx b/client/src/views/agents/AgentsView.tsx index 30b275c..18ef144 100644 --- a/client/src/views/agents/AgentsView.tsx +++ b/client/src/views/agents/AgentsView.tsx @@ -1,54 +1,42 @@ -import { - KeyboardAvoidingView, - Platform, - SafeAreaView, - TouchableWithoutFeedback, - Keyboard, - View, - Pressable, -} from "react-native"; +import { View, Pressable } from "react-native"; import { Text } from "@/components/ui/Text"; import { useAgentsQuery } from "@/hooks/fetchers/Agent/useAgentsQuery"; import { Link } from "expo-router"; import { Agent } from "@/types"; +import { DrawerScreenWrapper } from "../DrawerScreenWrapper"; export function AgentsView() { - const { data: agents, isSuccess, error } = useAgentsQuery(); + const { data: agents, isSuccess, error } = useAgentsQuery(); - if (!isSuccess) { - if (error) console.error(error); - return null; - } - return ( - - - - - Agents - - - {agents.length > 0 ? ( - agents.map((a) => ) - ) : ( - No agents found - )} - - - - - ); + if (!isSuccess) { + if (error) console.error(error); + return null; + } + return ( + + + + Agents + + + {agents.length > 0 ? ( + agents.map((a) => ) + ) : ( + No agents found + )} + + + + ); } function AgentButton({ agent }: { agent: Agent }) { - return ( - - - {agent.name} - - - ); + return ( + + + {agent.name} + + + ); } diff --git a/client/src/views/agents/AgentsView.web.tsx b/client/src/views/agents/AgentsView.web.tsx index 18100e7..d2420cd 100644 --- a/client/src/views/agents/AgentsView.web.tsx +++ b/client/src/views/agents/AgentsView.web.tsx @@ -1,15 +1,13 @@ -import { Keyboard, Pressable, View } from "react-native"; -import { useDrawerStatus } from "@react-navigation/drawer"; -import { DrawerActions, ParamListBase, useNavigation } from "@react-navigation/native"; -import { DrawerNavigationProp } from "@react-navigation/drawer"; +import { Pressable, View } from "react-native"; import { Link } from "expo-router"; -import { AntDesign, Entypo, Octicons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { Text } from "@/components/ui/Text"; import { useAgentsQuery } from "@/hooks/fetchers/Agent/useAgentsQuery"; import { Agent } from "@/types"; import { Button } from "@/components/ui/Button"; import { AgentDialog } from "@/views/agent/AgentDialog.web"; +import { DrawerScreenWrapper } from "../DrawerScreenWrapper"; export function AgentsView() { const { data, isSuccess, error } = useAgentsQuery(); @@ -19,13 +17,12 @@ export function AgentsView() { } const agents = data || []; return ( - - - - - Agents + + + + Agents - + {agents.length > 0 ? ( agents.map((a, i) => ) ) : ( @@ -33,10 +30,10 @@ export function AgentsView() { )} - + - + ); } @@ -44,9 +41,10 @@ function NewAgentButton() { return ( @@ -57,7 +55,7 @@ function NewAgentButton() { function AgentButton({ agent }: { agent: Agent }) { return ( - + {agent.name} Description @@ -65,7 +63,8 @@ function AgentButton({ agent }: { agent: Agent }) { - - ); } - -function CollapseDrawer() { - const navigation = useNavigation>(); - const isDrawerOpen = useDrawerStatus() === "open"; - - return ( - - { - navigation.dispatch(DrawerActions.toggleDrawer()); - Keyboard.dismiss(); - }} - hitSlop={{ top: 16, right: 16, bottom: 16, left: 16 }} - > - - - - ); -} diff --git a/client/src/views/chat/ChatHeader/CenterButton.tsx b/client/src/views/chat/ChatHeader/CenterButton.tsx index 817657b..5fee7f8 100644 --- a/client/src/views/chat/ChatHeader/CenterButton.tsx +++ b/client/src/views/chat/ChatHeader/CenterButton.tsx @@ -3,7 +3,7 @@ import { Pressable } from "react-native"; import { ContextMenuButton, type MenuConfig } from "react-native-ios-context-menu"; import { Text } from "@/components/ui/Text"; -import { AntDesign } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { useConfigStore } from "@/hooks/stores/configStore"; import { useAgentQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; import { useMessagesQuery } from "@/hooks/fetchers/Message/useMessagesQuery"; @@ -11,64 +11,64 @@ import { useTokenCount } from "@/hooks/useTokenCount"; import { useAction } from "@/hooks/useAction"; export function CenterButton({ threadId }: { threadId: string | null }) { - const router = useRouter(); - const { defaultAgent } = useConfigStore(); + const router = useRouter(); + const { defaultAgent } = useConfigStore(); - const { data: messages } = useMessagesQuery(threadId); - const { data: agent } = useAgentQuery(defaultAgent.id); - const deleteThread = useAction("deleteThread")(); + const { data: messages } = useMessagesQuery(threadId); + const { data: agent } = useAgentQuery(defaultAgent.id); + const deleteThread = useAction("deleteThread")(); - const tokenInput = messages?.map((m) => m.content).join(" ") || ""; - const tokens = useTokenCount(tokenInput); + const tokenInput = messages?.map((m) => m.content).join(" ") || ""; + const tokens = useTokenCount(tokenInput); - const menuConfig: MenuConfig = { - menuTitle: "", - menuItems: [ - { - actionKey: "tokens", - actionTitle: `Tokens: ${tokens}`, - }, - { - actionKey: "delete", - actionTitle: "Delete Thread", - menuAttributes: threadId ? undefined : ["hidden"], - }, - { - actionKey: "agent", - actionTitle: "Agent", - }, - ], - }; + const menuConfig: MenuConfig = { + menuTitle: "", + menuItems: [ + { + actionKey: "tokens", + actionTitle: `Tokens: ${tokens}`, + }, + { + actionKey: "delete", + actionTitle: "Delete Thread", + menuAttributes: threadId ? undefined : ["hidden"], + }, + { + actionKey: "agent", + actionTitle: "Agent", + }, + ], + }; - const onMenuAction = (actionKey: string) => { - switch (actionKey) { - case "delete": - deleteThread.action(threadId!); - break; - case "agent": - router.push({ - pathname: "/agent/", - ...(agent && { params: { id: agent.id } }), - }); - break; - } - }; + const onMenuAction = (actionKey: string) => { + switch (actionKey) { + case "delete": + deleteThread.action(threadId!); + break; + case "agent": + router.push({ + pathname: "/agent/", + ...(agent && { params: { id: agent.id } }), + }); + break; + } + }; - return ( - onMenuAction(nativeEvent.actionKey)} - > - - - myChat - - - - - ); + return ( + onMenuAction(nativeEvent.actionKey)} + > + + + myChat + + + + + ); } diff --git a/client/src/views/chat/ChatHeader/CenterButton.web.tsx b/client/src/views/chat/ChatHeader/CenterButton.web.tsx index 01e7ef4..04657e0 100644 --- a/client/src/views/chat/ChatHeader/CenterButton.web.tsx +++ b/client/src/views/chat/ChatHeader/CenterButton.web.tsx @@ -1,25 +1,30 @@ import { View } from "react-native"; import { Text } from "@/components/ui/Text"; -import { AntDesign } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { Dropdown } from "./Dropdown"; export function CenterButton({ threadId }: { threadId: string | null }) { - return ( - - - - myChat - - - - - ); + return ( + + + + myChat + + + + + ); } diff --git a/client/src/views/chat/ChatHeader/Dropdown.tsx b/client/src/views/chat/ChatHeader/Dropdown.tsx index 5aadf23..6bdd706 100644 --- a/client/src/views/chat/ChatHeader/Dropdown.tsx +++ b/client/src/views/chat/ChatHeader/Dropdown.tsx @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/DropdownMenu"; import { Text } from "@/components/ui/Text"; -import { Entypo } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { AgentDialog } from "@/views/agent/AgentDialog.web"; export function Dropdown({ @@ -50,9 +50,9 @@ export function Dropdown({ { label: "View Agent", onPress: openAgentMenu, - icon: Entypo, + icon: "Entypo", iconLabel: "chevron-right", - }, + } as const, { label: "Delete Thread", onPress: () => deleteThread.action(threadId!), @@ -88,7 +88,10 @@ export function Dropdown({ {action.label} {action.icon && ( - + )} diff --git a/client/src/views/chat/ChatHeader/LeftButton.tsx b/client/src/views/chat/ChatHeader/LeftButton.tsx index 55aadcf..3a7077b 100644 --- a/client/src/views/chat/ChatHeader/LeftButton.tsx +++ b/client/src/views/chat/ChatHeader/LeftButton.tsx @@ -1,25 +1,25 @@ import { Keyboard, Pressable } from "react-native"; import { - type ParamListBase, - useNavigation, - DrawerActions, + type ParamListBase, + useNavigation, + DrawerActions, } from "@react-navigation/native"; import { type DrawerNavigationProp } from "@react-navigation/drawer"; -import { Ionicons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; export default function LeftButton() { - const navigation = useNavigation>(); + const navigation = useNavigation>(); - return ( - { - navigation.dispatch(DrawerActions.toggleDrawer()); - Keyboard.dismiss(); - }} - > - - - ); + return ( + { + navigation.dispatch(DrawerActions.toggleDrawer()); + Keyboard.dismiss(); + }} + > + + + ); } diff --git a/client/src/views/chat/ChatHeader/RightButton.tsx b/client/src/views/chat/ChatHeader/RightButton.tsx index 26646e9..bc2752a 100644 --- a/client/src/views/chat/ChatHeader/RightButton.tsx +++ b/client/src/views/chat/ChatHeader/RightButton.tsx @@ -1,7 +1,7 @@ import { Link } from "expo-router"; import { View } from "react-native"; -import { MaterialIcons } from "@/components/ui/Icon"; +import { Icon } from "@/components/ui/Icon"; import { useConfigStore } from "@/hooks/stores/configStore"; export default function RightButton() { @@ -9,7 +9,7 @@ export default function RightButton() { if (!threadId) return ; return ( - + ); } diff --git a/client/src/views/chat/ChatView.tsx b/client/src/views/chat/ChatView.tsx index 4d70b37..f6de8ac 100644 --- a/client/src/views/chat/ChatView.tsx +++ b/client/src/views/chat/ChatView.tsx @@ -2,20 +2,20 @@ import { useChat } from "@/hooks/useChat"; import ChatHistory from "@/components/ChatHistory"; import { ChatInputContainer } from "@/components/ChatInput"; import { ChatHeader } from "./ChatHeader"; -import { ChatViewWrapper } from "./ChatViewWrapper"; +import { DrawerScreenWrapper } from "../DrawerScreenWrapper"; export function ChatView({ threadId }: { threadId: string | null }) { - const { loading, handleSubmit, abort } = useChat(threadId); - return ( - - - {threadId && } - - - ); + const { loading, handleSubmit, abort } = useChat(threadId); + return ( + + + {threadId && } + + + ); } diff --git a/client/src/views/chat/ChatViewWrapper.tsx b/client/src/views/chat/ChatViewWrapper.tsx deleted file mode 100644 index cdb40af..0000000 --- a/client/src/views/chat/ChatViewWrapper.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - KeyboardAvoidingView, - Platform, - SafeAreaView, - TouchableWithoutFeedback, - Keyboard, -} from "react-native"; - -export function ChatViewWrapper({ children }: { children: React.ReactNode }) { - return ( - - - - {children} - - - - ); -} diff --git a/client/src/views/chat/ChatViewWrapper.web.tsx b/client/src/views/chat/ChatViewWrapper.web.tsx deleted file mode 100644 index aefa18d..0000000 --- a/client/src/views/chat/ChatViewWrapper.web.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Pressable, View } from "react-native"; -import { useDrawerStatus } from "@react-navigation/drawer"; -import { DrawerActions, ParamListBase, useNavigation } from "@react-navigation/native"; -import { DrawerNavigationProp } from "@react-navigation/drawer"; - -import { Entypo } from "@/components/ui/Icon"; - -export function ChatViewWrapper({ children }: { children: React.ReactNode }) { - return ( - - - - {children} - - - ); -} - -function CollapseDrawer() { - const navigation = useNavigation>(); - const isDrawerOpen = useDrawerStatus() === "open"; - - return ( - - navigation.dispatch(DrawerActions.toggleDrawer())} - > - - - - ); -} diff --git a/client/src/views/settings/SettingsDialog.web.tsx b/client/src/views/settings/SettingsView.tsx similarity index 63% rename from client/src/views/settings/SettingsDialog.web.tsx rename to client/src/views/settings/SettingsView.tsx index cc7d0d8..4d44f28 100644 --- a/client/src/views/settings/SettingsDialog.web.tsx +++ b/client/src/views/settings/SettingsView.tsx @@ -1,27 +1,19 @@ -import { useState } from "react"; import { Pressable, View } from "react-native"; +import { useState } from "react"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogTitle, - DialogTrigger, -} from "@/components/ui/Dialog"; -import { Section } from "@/components/ui/Section"; import { Text } from "@/components/ui/Text"; +import { Section } from "@/components/ui/Section"; +import { useHoverHelper } from "@/hooks/useHoverHelper"; +import { cn } from "@/lib/utils"; import { - DebugQueryToggle, - ResetDefaultsButton, - StreamToggle, ToggleThemeButton, + StreamToggle, + ResetDefaultsButton, + DebugQueryToggle, } from "./helpers"; import { DeviceConfig } from "./helpers/DeviceConfig"; import { UserConfig } from "./helpers/UserConfig"; -import { useHoverHelper } from "@/hooks/useHoverHelper"; -import { cn } from "@/lib/utils"; -import { MaterialIcons } from "@expo/vector-icons"; +import { DrawerScreenWrapper } from "../DrawerScreenWrapper"; enum SideMenu { General = "General", @@ -35,13 +27,7 @@ const configView = { [SideMenu.Debug]: , }; -export default function SettingsDialog({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { +export function SettingsView() { const [activeTab, setActiveTab] = useState(SideMenu.General); const renderNavButtons = () => { return Object.values(SideMenu).map((menu, i) => ( @@ -56,37 +42,24 @@ export default function SettingsDialog({ }; return ( - - - {children} - - - - - - - {renderNavButtons()} - - - {configView[activeTab]} - - - - + + + + + {renderNavButtons()} + + + {configView[activeTab]} + + + ); } function SettingsHeader() { return ( - - Settings - - - + + Settings ); } diff --git a/client/src/views/tools/ToolCard.tsx b/client/src/views/tools/ToolCard.tsx index d7cb38f..3768dac 100644 --- a/client/src/views/tools/ToolCard.tsx +++ b/client/src/views/tools/ToolCard.tsx @@ -1,31 +1,23 @@ import { View } from "react-native"; import Toast from "react-native-toast-message"; -import { Agent, AgentTool, AgentUpdateSchema, ToolName } from "@/types"; import { Text } from "@/components/ui/Text"; import { Switch } from "@/components/ui/Switch"; -import { useAgentPatch } from "@/hooks/fetchers/Agent/useAgentPatch"; +import { useAgentToolQuery } from "@/hooks/fetchers/AgentTool/useAgentToolQuery"; +import { useAgentToolPatch } from "@/hooks/fetchers/AgentTool/useAgentToolPatch"; -export function ToolCard({ - agent, - toolName, - tool, -}: { - agent: Agent; - toolName: ToolName; - tool?: AgentTool; -}) { - const agentEditMut = useAgentPatch(); +export function ToolCard({ agentId, toolId }: { agentId: string; toolId: string }) { + const agentToolQuery = useAgentToolQuery(agentId, toolId); + const agentToolEditMut = useAgentToolPatch(); + + const agentTool = agentToolQuery.data; const onCheckedChange = async (checked: boolean) => { try { - const value = checked - ? [...(agent.tools ? agent.tools : []), { toolName }] - : agent.tools?.filter((t) => t.toolName !== toolName); - - await agentEditMut.mutateAsync({ - agentId: agent.id, - agentConfig: { type: "tools", value } as AgentUpdateSchema, + await agentToolEditMut.mutateAsync({ + agentId, + toolId, + agentToolConfig: { type: "enabled", value: !agentTool?.enabled }, }); } catch (error: any) { console.error(error); @@ -40,10 +32,12 @@ export function ToolCard({ return ( - {toolName} + + {typeof agentTool?.name === "string" ? agentTool.name : "Tool Name"} + diff --git a/client/src/views/tools/ToolList.tsx b/client/src/views/tools/ToolList.tsx index e8b0067..3127349 100644 --- a/client/src/views/tools/ToolList.tsx +++ b/client/src/views/tools/ToolList.tsx @@ -1,6 +1,6 @@ import { ScrollView, Pressable, View } from "react-native"; -import { useToolsQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; +import { useToolsQuery } from "@/hooks/fetchers/AgentTool/useAgentToolQuery"; import { Agent, ToolName } from "@/types"; import { Text } from "@/components/ui/Text"; import { cn } from "@/lib/utils"; diff --git a/client/src/views/tools/ToolView.tsx b/client/src/views/tools/ToolView.tsx new file mode 100644 index 0000000..2f7241c --- /dev/null +++ b/client/src/views/tools/ToolView.tsx @@ -0,0 +1,28 @@ +import { View } from "react-native"; + +import { Text } from "@/components/ui/Text"; +import { ToggleToolsSwitch } from "../agent/helpers/ToggleTools"; +import { ToolsOverview } from "./ToolsOverview"; +import { useUserQuery } from "@/hooks/fetchers/User/useUserQuery"; +import { DrawerScreenWrapper } from "../DrawerScreenWrapper"; + +export function ToolView() { + const userQuery = useUserQuery(); + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const agent = userQuery.data?.defaultAgent!; + return ( + + + + Tools + + + + + + + + + + ); +} diff --git a/client/src/views/tools/ToolsOverview.tsx b/client/src/views/tools/ToolsOverview.tsx index 664b2c5..9c1d87e 100644 --- a/client/src/views/tools/ToolsOverview.tsx +++ b/client/src/views/tools/ToolsOverview.tsx @@ -7,18 +7,15 @@ import { ToolList } from "./ToolList"; export function ToolsOverview({ agent }: { agent: Agent }) { const [activeTool, setTool] = useState(null); + const tool = agent.tools?.find((t) => t.toolName === activeTool); return ( <> - {activeTool && ( - t.toolName === activeTool)} - toolName={activeTool} - /> + {tool && ( + )} diff --git a/client/tsconfig.json b/client/tsconfig.json index 1aa611b..84160d4 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,21 +1,22 @@ { - "extends": "expo/tsconfig.base", - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "paths": { - "@/*": ["./src/*"], - "@db/*": ["../server/src/modules/*"] - }, + "extends": "expo/tsconfig.base", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "paths": { + "@/*": ["./src/*"], + "@db/*": ["../server/src/modules/*"] + }, - "experimentalDecorators": true - }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts", - "../server/src/modules/Models/data.ts" - ] + "experimentalDecorators": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts", + "../server/src/modules/**/*.ts", + "../server/src/types/**/*.ts" + ] } diff --git a/server/bun.lockb b/server/bun.lockb index c48c639..2dcc324 100755 Binary files a/server/bun.lockb and b/server/bun.lockb differ diff --git a/server/package.json b/server/package.json index 0559ec6..4d813d2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,54 +1,56 @@ { - "name": "mychat", - "module": "src/index.ts", - "type": "module", - "scripts": { - "start": "bun --hot src/index.ts", - "dev": "bun --watch src/index.ts", - "dev:clean": "DEBUG_RESET_DB=true bun --watch src/index.ts", - "test": "export TEST_ENV=true jest", - "lint": "eslint ." - }, - "dependencies": { - "@expo/server": "^0.3.1", - "@fastify/cors": "^9.0.1", - "@fastify/multipart": "^8.2.0", - "@fastify/static": "^7.0.3", - "@fastify/swagger": "^8.14.0", - "@fastify/swagger-ui": "^3.0.0", - "@readme/openapi-parser": "^2.5.1", - "fastify": "^4.26.2", - "fastify-type-provider-zod": "^1.1.9", - "gpt4-tokenizer": "^1.3.0", - "install": "^0.13.0", - "json-stringify-safe": "^5.0.1", - "langchain": "^0.1.34", - "openai": "^4.38.2", - "pg": "^8.11.5", - "playwright": "^1.43.1", - "typeorm": "^0.3.20", - "typeorm-fastify-plugin": "^1.0.5", - "winston": "^3.13.0", - "zod": "^3.23.0" - }, - "devDependencies": { - "@babel/preset-typescript": "^7.24.1", - "@types/bun": "latest", - "@types/jest": "^29.5.12", - "@types/json-stringify-safe": "^5.0.3", - "@types/morgan": "^1.9.9", - "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", - "eslint": "8.57.0", - "jest": "^29.7.0", - "openapi-types": "^12.1.3", - "supertest": "^6.3.4", - "ts-jest": "^29.1.2", - "ts-mockito": "^2.6.1", - "typescript": "next" - }, - "peerDependencies": { - "typescript": "^5.0.0" - } + "name": "mychat", + "module": "src/index.ts", + "type": "module", + "scripts": { + "start": "bun --hot src/index.ts", + "dev": "bun --watch src/index.ts", + "dev:clean": "DEBUG_RESET_DB=true bun --watch src/index.ts", + "test": "export TEST_ENV=true jest", + "lint": "eslint ." + }, + "dependencies": { + "@expo/server": "^0.3.1", + "@fastify/cors": "^9.0.1", + "@fastify/multipart": "^8.2.0", + "@fastify/rate-limit": "^9.1.0", + "@fastify/static": "^7.0.3", + "@fastify/swagger": "^8.14.0", + "@fastify/swagger-ui": "^3.0.0", + "@readme/openapi-parser": "^2.5.1", + "fastify": "^4.26.2", + "fastify-plugin": "^4.5.1", + "fastify-type-provider-zod": "^1.1.9", + "gpt4-tokenizer": "^1.3.0", + "install": "^0.13.0", + "json-stringify-safe": "^5.0.1", + "langchain": "^0.1.34", + "openai": "^4.38.2", + "pg": "^8.11.5", + "playwright": "^1.43.1", + "typeorm": "^0.3.20", + "typeorm-fastify-plugin": "^1.0.5", + "winston": "^3.13.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@babel/preset-typescript": "^7.24.1", + "@types/bun": "latest", + "@types/jest": "^29.5.12", + "@types/json-stringify-safe": "^5.0.3", + "@types/morgan": "^1.9.9", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", + "eslint": "8.57.0", + "jest": "^29.7.0", + "openapi-types": "^12.1.3", + "supertest": "^6.3.4", + "ts-jest": "^29.1.2", + "ts-mockito": "^2.6.1", + "typescript": "next" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } } diff --git a/server/src/app.ts b/server/src/app.ts index 3075318..ec747a8 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -2,9 +2,9 @@ import Fastify from "fastify"; import fastifyCors from "@fastify/cors"; import fastifyMultipart from "@fastify/multipart"; import fastifyStatic from "@fastify/static"; -import fastifyORM from "typeorm-fastify-plugin"; import fastifySwagger from "@fastify/swagger"; import fastifySwaggerUI from "@fastify/swagger-ui"; +import fastifyRateLimit from "@fastify/rate-limit"; import { jsonSchemaTransform, serializerCompiler, @@ -13,11 +13,9 @@ import { } from "fastify-type-provider-zod"; import { Config } from "./config"; -import { initDb, resetDatabase, AppDataSource } from "./lib/pg"; -import { errorHandler } from "./errors"; - import { getUser } from "./hooks/getUser"; -import { accessErrorLogger, accessLogger } from "./hooks/accessLogger"; +import { setupLogger } from "./hooks/setupLogger"; +import { setupDatabase } from "./hooks/setupDatabase"; import { setupUserRoute } from "./routes/user"; import { setupAgentsRoute } from "./routes/agents"; @@ -37,26 +35,24 @@ export type BuildAppParams = { staticClientFilesDir: string; }; -export async function buildApp( - { resetDbOnInit, staticClientFilesDir }: BuildAppParams | undefined = { - resetDbOnInit: Config.resetDbOnInit, - staticClientFilesDir: Config.staticClientFilesDir, - } -) { - // Connect to Postgres and initialize TypeORM - if (resetDbOnInit) { - await initDb(); - await resetDatabase(); - } - await app.register(fastifyORM, { connection: AppDataSource }); +export async function buildApp({ + resetDbOnInit, + staticClientFilesDir, +}: BuildAppParams | undefined = Config) { + // Initialize Database + await app.register(setupDatabase, { resetDbOnInit }); + + // Access Logger + await app.register(setupLogger); + // Type Providers app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler); app.withTypeProvider(); // Hooks await app.register(fastifyMultipart); - app.register(fastifySwagger, { + await app.register(fastifySwagger, { openapi: { openapi: "3.0.0", info: { @@ -68,18 +64,13 @@ export async function buildApp( }, transform: jsonSchemaTransform, }); - - app.register(fastifySwaggerUI, { - routePrefix: "/docs", + await app.register(fastifyRateLimit, { + max: 100, + timeWindow: "1 minute", }); + await app.register(fastifySwaggerUI, { routePrefix: "/docs" }); await app.register(fastifyCors, { origin: "*" }); - // Access Logger - app.addHook("onRequest", accessLogger); - app.addHook("onSend", accessErrorLogger); - - app.setErrorHandler(errorHandler); - // Api Routes await app.register( async (app) => { @@ -87,6 +78,7 @@ export async function buildApp( await app.register(setupUserRoute); await app.register(setupModelsRoute); + // authenticated routes await app.register(async (app) => { app.addHook("preHandler", getUser); diff --git a/server/src/errors.ts b/server/src/errors.ts deleted file mode 100644 index 9f7bd81..0000000 --- a/server/src/errors.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { FastifyError, FastifyReply, FastifyRequest } from "fastify"; -import logger from "./lib/logs/logger"; -import { ResponseValidationError } from "fastify-type-provider-zod"; -import type { ZodError } from "zod"; - -type ErrorType = FastifyError & T; - -export async function errorHandler( - error: FastifyError, - request: FastifyRequest, - reply: FastifyReply -) { - switch (error.name) { - case "RequestValidationError": { - const err = error as ErrorType; - logger.error("Request Validation error", { - details: err.details, - }); - return reply.status(400).send(err.details); - } - case "ResponseValidationError": { - const err = error as ErrorType; - logger.error("Response Validation error", { - details: err.details, - }); - return reply.status(400).send(err.details); - } - case "ZodError": { - const err = error as ErrorType; - logger.error("Zod error", { errors: err.issues }); - return reply.status(400).send(err.issues); - } - } - logger.error("Fastify error", { - err: error, - params: request.params, - method: request.method, - url: request.url, - body: request.body, - headers: request.headers, - }); - return reply.status(409).send({ ok: false }); -} diff --git a/server/src/hooks/accessLogger.ts b/server/src/hooks/accessLogger.ts deleted file mode 100644 index c0d79c4..0000000 --- a/server/src/hooks/accessLogger.ts +++ /dev/null @@ -1,13 +0,0 @@ -import logger, { accessLogger as aLogger } from "@/lib/logs/logger"; -import type { FastifyReply, FastifyRequest } from "fastify"; - -export async function accessLogger(request: FastifyRequest) { - aLogger.info(`${request.headers.referer} ${request.method} ${request.url}`); - logger.info(`Req: ${request.headers.referer} ${request.method} ${request.url}`); -} - -export async function accessErrorLogger(request: FastifyRequest, reply: FastifyReply) { - if (reply.statusCode >= 400) { - logger.error(`Error: ${reply.statusCode} ${request.method} ${request.url}`); - } -} diff --git a/server/src/hooks/getAgentTool.ts b/server/src/hooks/getAgentTool.ts new file mode 100644 index 0000000..6060c15 --- /dev/null +++ b/server/src/hooks/getAgentTool.ts @@ -0,0 +1,34 @@ +import type { FindOneOptions } from "typeorm"; +import type { FastifyRequest, FastifyReply } from "fastify"; + +import logger from "@/lib/logs/logger"; +import { AgentTool } from "@/modules/AgentTool/AgentToolModel"; + +export function getAgentTool(relations?: FindOneOptions["relations"]) { + return async function getAgent(request: FastifyRequest, reply: FastifyReply) { + try { + const { agent } = request; + const { agentToolId } = request.params as { agentToolId: string }; + const agentTool = await request.server.orm.getRepository(AgentTool).findOne({ + where: { id: agentToolId, agent: { id: agent.id } }, + relations, + }); + if (!agentTool) { + return reply.status(404).send({ + error: "(AgentRepo.getAgentById) Agent not found.", + }); + } + + request.agentTool = agentTool; + } catch (error) { + // error for missing item in db + logger.error("Error in getAgent", { + error, + functionName: "getAgent", + }); + reply.status(500).send({ + error: "An error occurred while processing your request.", + }); + } + }; +} diff --git a/server/src/hooks/getUser.ts b/server/src/hooks/getUser.ts index 9f07b08..c99606e 100644 --- a/server/src/hooks/getUser.ts +++ b/server/src/hooks/getUser.ts @@ -1,16 +1,22 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import { User } from "@/modules/User/UserModel"; +import logger from "@/lib/logs/logger"; export async function getUser(request: FastifyRequest, reply: FastifyReply) { - const token = request.headers.authorization; - if (!token) return reply.code(401).send({ error: "Unauthorized" }); + try { + const token = request.headers.authorization; + if (!token) return reply.code(401).send({ error: "Unauthorized" }); - const user = await request.server.orm.getRepository(User).findOne({ - where: { apiKey: token }, - relations: ["threads", "agents"], - }); - if (!user) return reply.code(401).send({ error: "User not found" }); + const user = await request.server.orm.getRepository(User).findOne({ + where: { apiKey: token }, + relations: ["threads", "agents", "tools"], + }); + if (!user) return reply.code(401).send({ error: "User not found" }); - request.user = user; + request.user = user; + } catch (error) { + logger.error("Error getting user", error); + return reply.code(401).send({ error: "Unauthorized" }); + } } diff --git a/server/src/hooks/setupDatabase.ts b/server/src/hooks/setupDatabase.ts new file mode 100644 index 0000000..da6d058 --- /dev/null +++ b/server/src/hooks/setupDatabase.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from "fastify"; +import fastifyORM from "typeorm-fastify-plugin"; +import fastifyPlugin from "fastify-plugin"; + +import { AppDataSource, initDb, resetDatabase } from "@/lib/pg"; + +export const setupDatabase = fastifyPlugin( + async (app: FastifyInstance, opts: { resetDbOnInit: boolean }) => { + // Connect to Postgres and initialize TypeORM + if (opts.resetDbOnInit) { + await initDb(); + await resetDatabase(); + } + await app.register(fastifyORM, { connection: AppDataSource }); + + app.addHook("onReady", async () => { + // seed database + }); + } +); diff --git a/server/src/hooks/setupLogger.ts b/server/src/hooks/setupLogger.ts new file mode 100644 index 0000000..00c5d2c --- /dev/null +++ b/server/src/hooks/setupLogger.ts @@ -0,0 +1,62 @@ +import type { FastifyError, FastifyReply, FastifyRequest } from "fastify"; +import type { FastifyInstance } from "fastify"; +import fastifyPlugin from "fastify-plugin"; +import type { ResponseValidationError } from "fastify-type-provider-zod"; +import type { ZodError } from "zod"; + +import logger, { accessLogger as aLogger } from "@/lib/logs/logger"; + +type ErrorType = FastifyError & T; + +export const setupLogger = fastifyPlugin(async (app: FastifyInstance) => { + app.addHook("onRequest", async (request: FastifyRequest) => { + aLogger.info(`${request.headers.referer} ${request.method} ${request.url}`); + logger.info(`Req: ${request.headers.referer} ${request.method} ${request.url}`); + }); + + app.addHook("onSend", async (request: FastifyRequest, reply: FastifyReply) => { + if (reply.statusCode >= 400) { + logger.error(`Error: ${reply.statusCode} ${request.method} ${request.url}`); + } + }); + + app.setErrorHandler( + async (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => { + logger.warn(`Error handler Status Code: ${error.statusCode || "undefined"}`); + if (error.statusCode === 429) { + reply.code(429); + error.message = "You hit the rate limit! Slow down please!"; + } + + switch (error.name) { + case "RequestValidationError": { + const err = error as ErrorType; + logger.error("Request Validation error", { + details: err.details, + }); + return reply.status(400).send(err.details); + } + case "ResponseValidationError": { + const err = error as ErrorType; + logger.error("Response Validation error", { + details: err.details, + }); + return reply.status(400).send(err.details); + } + case "ZodError": { + const err = error as ErrorType; + logger.error("Zod error", { errors: err.issues }); + return reply.status(400).send(err.issues); + } + } + logger.error("Fastify error", { + error, + ...error, + params: request.params, + method: request.method, + url: request.url, + }); + return reply.status(409).send(error); + } + ); +}); diff --git a/server/src/lib/pg.ts b/server/src/lib/pg.ts index 4f656ef..3787acf 100644 --- a/server/src/lib/pg.ts +++ b/server/src/lib/pg.ts @@ -12,7 +12,7 @@ import { FileData, MessageFile } from "@/modules/MessageFile/MessageFileModel"; import { AgentRun } from "@/modules/AgentRun/AgentRunModel"; import { ToolCall } from "@/modules/Message/ToolCallModel"; import { UserSession } from "@/modules/User/SessionModel"; -import { AgentTool } from "@/modules/Agent/AgentToolModel"; +import { AgentTool } from "@/modules/AgentTool/AgentToolModel"; export const AppDataSource = new DataSource({ ...Config.database, @@ -30,6 +30,8 @@ export const AppDataSource = new DataSource({ ], synchronize: true, logger: new DBLogger(), + migrations: ["migrations/*.ts"], + migrationsTableName: "migrations", }); /** Initialize Database Connection */ diff --git a/server/src/modules/Agent/AgentController.ts b/server/src/modules/Agent/AgentController.ts index eec1712..5eee8a7 100644 --- a/server/src/modules/Agent/AgentController.ts +++ b/server/src/modules/Agent/AgentController.ts @@ -3,14 +3,14 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import type { AgentCreateSchema, AgentUpdateSchema } from "./AgentSchema"; import { Agent } from "./AgentModel"; import { Tools } from "../LLMNexus/Tools"; -import { AgentTool } from "./AgentToolModel"; +import { AgentTool } from "../AgentTool/AgentToolModel"; export class AgentController { static async createAgent(request: FastifyRequest, reply: FastifyReply) { const user = request.user; const agent = request.body as AgentCreateSchema; const savedAgent = await request.server.orm.getRepository(Agent).save({ - ...(agent as Agent), + ...agent, owner: user, }); reply.send(savedAgent); @@ -56,7 +56,11 @@ export class AgentController { } } const updatedAgent = await agent.save(); - reply.send(updatedAgent); + reply.send({ + ...updatedAgent, + threads: updatedAgent.threads.map((thread) => thread.id), + owner: updatedAgent.owner.id, + }); } static async deleteAgent(request: FastifyRequest, reply: FastifyReply) { diff --git a/server/src/modules/Agent/AgentModel.ts b/server/src/modules/Agent/AgentModel.ts index 4921e61..60ffc3c 100644 --- a/server/src/modules/Agent/AgentModel.ts +++ b/server/src/modules/Agent/AgentModel.ts @@ -8,13 +8,15 @@ import { OneToMany, CreateDateColumn, VersionColumn, + ManyToMany, + JoinTable, } from "typeorm"; import { User } from "../User/UserModel"; import { Thread } from "../Thread/ThreadModel"; import { ToolsMap } from "../LLMNexus/Tools"; import { modelMap } from "../Models/data"; -import { AgentTool } from "./AgentToolModel"; +import { AgentTool } from "../AgentTool/AgentToolModel"; const defaultAgent: Partial = { name: "myChat Agent", @@ -36,7 +38,8 @@ export class Agent extends BaseEntity { name: string = "myChat Agent"; /** Tools available to the Agent */ - @OneToMany(() => AgentTool, (tool) => tool.agent, { eager: true }) + @ManyToMany(() => AgentTool, (tool) => tool.agents, { eager: true }) + @JoinTable() tools: Relation; /** Model API for the Agent */ diff --git a/server/src/modules/Agent/AgentSchema.ts b/server/src/modules/Agent/AgentSchema.ts index d117703..8279ac2 100644 --- a/server/src/modules/Agent/AgentSchema.ts +++ b/server/src/modules/Agent/AgentSchema.ts @@ -1,25 +1,7 @@ import z from "zod"; -import { ToolNames } from "../LLMNexus/Tools"; -import { constructZodLiteralUnionType } from "@/lib/zod"; -import { ModelInfoSchema } from "../Models/ModelsSchema"; - -export const AgentToolsNameSchema = constructZodLiteralUnionType( - ToolNames.map((t) => z.literal(t)) -); -export type AgentToolsNameSchema = z.infer; -export const AgentToolSchema = z.object({ - id: z.string(), - createdAt: z.date(), - name: z.string(), - enabled: z.boolean(), - description: z.string(), - parameters: z.object({}), - toolName: AgentToolsNameSchema, - parse: z.string(), - version: z.number(), -}); -export type AgentToolSchema = z.infer; +import { ModelInfoSchema } from "../Models/ModelsSchema"; +import { AgentToolSchema } from "../AgentTool/AgentToolSchema"; export const AgentObjectSchema = z.object({ id: z.string(), diff --git a/server/src/modules/AgentTool/AgentToolController.ts b/server/src/modules/AgentTool/AgentToolController.ts new file mode 100644 index 0000000..9217c46 --- /dev/null +++ b/server/src/modules/AgentTool/AgentToolController.ts @@ -0,0 +1,70 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; + +import { Tools } from "../LLMNexus/Tools"; +import { AgentTool } from "../AgentTool/AgentToolModel"; +import type { AgentToolCreateSchema, AgentToolUpdateSchema } from "./AgentToolSchema"; + +export class AgentToolController { + static async createAgentTool(request: FastifyRequest, reply: FastifyReply) { + const { agent } = request; + const agentTool = request.body as AgentToolCreateSchema; + const savedAgent = await request.server.orm.getRepository(AgentTool).save({ + ...agentTool, + agent, + }); + reply.send(savedAgent); + } + + static async getAgentTools(request: FastifyRequest, reply: FastifyReply) { + reply.send( + request.user.agents.reduce( + (acc, agent) => [...acc, ...agent.tools], + [] as AgentTool[] + ) + ); + } + + static async getAgentTool(request: FastifyRequest, reply: FastifyReply) { + const { agentToolId } = request.params as { agentToolId: string }; + const agentTool = await request.server.orm + .getRepository(AgentTool) + .findOne({ where: { id: agentToolId } }); + reply.send(agentTool); + } + + static async updateAgentTool(request: FastifyRequest, reply: FastifyReply) { + const agentUpdate = request.body as AgentToolUpdateSchema; + const agentTool = request.agentTool; + + switch (agentUpdate.type) { + case "enabled": { + agentTool[agentUpdate.type] = agentUpdate.value; + break; + } + case "parameters": { + agentTool[agentUpdate.type] = agentUpdate.value; + break; + } + case "toolName": { + agentTool[agentUpdate.type] = agentUpdate.value; + break; + } + case "name": + case "description": { + agentTool[agentUpdate.type] = agentUpdate.value; + break; + } + } + const updatedAgentTool = await agentTool.save(); + reply.send(updatedAgentTool); + } + + static async deleteAgentTool(request: FastifyRequest, reply: FastifyReply) { + reply.send("TODO"); + } + + /** Return list of all tools available on the server */ + static async getTools(request: FastifyRequest, reply: FastifyReply) { + reply.send(Tools.map((tool) => tool.name)); + } +} diff --git a/server/src/modules/Agent/AgentToolModel.ts b/server/src/modules/AgentTool/AgentToolModel.ts similarity index 51% rename from server/src/modules/Agent/AgentToolModel.ts rename to server/src/modules/AgentTool/AgentToolModel.ts index e982804..08d5c0f 100644 --- a/server/src/modules/Agent/AgentToolModel.ts +++ b/server/src/modules/AgentTool/AgentToolModel.ts @@ -7,10 +7,12 @@ import { ManyToOne, type Relation, Entity, + ManyToMany, } from "typeorm"; -import type { ToolName } from "../LLMNexus/Tools"; -import { Agent } from "./AgentModel"; +import type { ToolConfigUnion, ToolName } from "../LLMNexus/Tools"; +import { Agent } from "../Agent/AgentModel"; +import { User } from "../User/UserModel"; @Entity("AgentTool") export class AgentTool extends BaseEntity { @@ -20,6 +22,7 @@ export class AgentTool extends BaseEntity { @CreateDateColumn() createdAt: Date; + /** User friendly name */ @Column({ type: "text" }) name: string; @@ -32,15 +35,26 @@ export class AgentTool extends BaseEntity { @Column({ type: "jsonb" }) parameters: object; + /** Tool name for backend */ @Column({ type: "text" }) toolName: ToolName; - @Column({ type: "text" }) - parse: string; - @VersionColumn() version: number; - @ManyToOne(() => Agent, (agent) => agent.tools) - agent: Relation; + @ManyToMany(() => Agent, (agent) => agent.tools) + agents: Relation; + + @ManyToOne(() => User, (user) => user.tools) + owner: Relation; +} + +export function fromToolConfig(tool: ToolConfigUnion): Partial { + return { + name: tool.name, + description: tool.description, + parameters: {}, + toolName: tool.name, + enabled: false, + }; } diff --git a/server/src/modules/AgentTool/AgentToolSchema.ts b/server/src/modules/AgentTool/AgentToolSchema.ts new file mode 100644 index 0000000..7a5d7b9 --- /dev/null +++ b/server/src/modules/AgentTool/AgentToolSchema.ts @@ -0,0 +1,37 @@ +import z from "zod"; +import { ToolNames } from "../LLMNexus/Tools"; +import { constructZodLiteralUnionType } from "@/lib/zod"; + +export const AgentToolsNameSchema = constructZodLiteralUnionType( + ToolNames.map((t) => z.literal(t)) +); +export type AgentToolsNameSchema = z.infer; + +export const AgentToolSchema = z.object({ + id: z.string(), + createdAt: z.date(), + name: z.string(), + enabled: z.boolean(), + description: z.string(), + parameters: z.object({}), + toolName: AgentToolsNameSchema, + parse: z.string(), + version: z.number(), +}); +export type AgentToolSchema = z.infer; + +export const AgentToolCreateSchema = AgentToolSchema.omit({ + id: true, + createdAt: true, + version: true, +}); +export type AgentToolCreateSchema = z.infer; + +export const AgentToolUpdateSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("name"), value: z.string() }), + z.object({ type: z.literal("enabled"), value: z.boolean() }), + z.object({ type: z.literal("description"), value: z.string() }), + z.object({ type: z.literal("parameters"), value: z.any() }), + z.object({ type: z.literal("toolName"), value: AgentToolsNameSchema }), +]); +export type AgentToolUpdateSchema = z.infer; diff --git a/server/src/modules/LLMNexus/Tools/index.ts b/server/src/modules/LLMNexus/Tools/index.ts index 139a6a2..0452fd3 100644 --- a/server/src/modules/LLMNexus/Tools/index.ts +++ b/server/src/modules/LLMNexus/Tools/index.ts @@ -6,6 +6,13 @@ export * from "./types"; export const Tools = [Browser, Fetcher] as const satisfies ToolConfig[]; export type Tools = typeof Tools; +export type ToolConfigs = { + [K in (typeof Tools)[number]["name"]]: (typeof Tools)[number]; +}; + +// union of all toolconfigs +export type ToolConfigUnion = ToolConfigs[keyof ToolConfigs]; + export type ToolName = Tools[number]["name"]; export const ToolNames = Tools.map((t) => t.name) as [ToolName, ...ToolName[]]; diff --git a/server/src/modules/User/UserModel.ts b/server/src/modules/User/UserModel.ts index 9dd6c93..73d3f95 100644 --- a/server/src/modules/User/UserModel.ts +++ b/server/src/modules/User/UserModel.ts @@ -1,61 +1,71 @@ import { - BaseEntity, - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - type Relation, + BaseEntity, + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + type Relation, } from "typeorm"; + import { Thread } from "../Thread/ThreadModel"; import { Agent } from "../Agent/AgentModel"; import { UserSession } from "./SessionModel"; +import { AgentTool } from "../AgentTool/AgentToolModel"; @Entity("User") export class User extends BaseEntity { - @PrimaryGeneratedColumn("uuid") - id!: string; - - @Column({ type: "varchar", length: 255 }) - apiKey!: string; - - /** User name. - * Default is "New User" - */ - @Column({ type: "text", default: "New User" }) - name: string; - - /** Email */ - @Column({ type: "text", unique: true }) - email: string; - - /** Password */ - @Column({ type: "text", default: "" }) - password: string; - - /** Threads owned by the User. */ - @OneToMany(() => Thread, (thread) => thread.user) - threads: Relation; - - /** Agents owned by the User. - * Cascaded. - */ - @OneToMany(() => Agent, (agent) => agent.owner, { - cascade: true, - }) - agents: Relation; - - /** Default Agent when starting new chat. */ - @ManyToOne(() => Agent, { - eager: true, - }) - @JoinColumn() - defaultAgent: Relation; - - /** User sessions. */ - @OneToMany(() => UserSession, (session) => session.user, { - cascade: true, - }) - sessions: Relation; + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 255 }) + apiKey!: string; + + /** User name. + * Default is "New User" + */ + @Column({ type: "text", default: "New User" }) + name: string; + + /** Email */ + @Column({ type: "text", unique: true }) + email: string; + + /** Password */ + @Column({ type: "text", default: "" }) + password: string; + + /** Threads owned by the User. */ + @OneToMany(() => Thread, (thread) => thread.user) + threads: Relation; + + /** Agents owned by the User. + * Cascaded + */ + @OneToMany(() => Agent, (agent) => agent.owner, { + cascade: true, + }) + agents: Relation; + + /** Agent Tools owned by the User. + * Cascaded. + */ + @OneToMany(() => AgentTool, (tool) => tool.owner, { + cascade: true, + }) + tools: Relation; + + /** Default Agent when starting new chat. */ + @ManyToOne(() => Agent, { + eager: true, + }) + @JoinColumn() + defaultAgent: Relation; + + /** User sessions. */ + @OneToMany(() => UserSession, (session) => session.user, { + cascade: true, + }) + sessions: Relation; } diff --git a/server/src/modules/User/UserSchema.ts b/server/src/modules/User/UserSchema.ts index 4101b17..815c1c9 100644 --- a/server/src/modules/User/UserSchema.ts +++ b/server/src/modules/User/UserSchema.ts @@ -4,18 +4,19 @@ import { ThreadSchema } from "../Thread/ThreadSchema"; import { AgentObjectSchema } from "../Agent/AgentSchema"; export const AuthInputSchema = z.object({ - email: z.string().email(), - password: z.string().min(8, { message: "Password must be at least 8 characters" }), + email: z.string().email(), + password: z.string().min(8, { message: "Password must be at least 8 characters" }), }); export type AuthInputSchema = z.infer; export const UserSchema = z.object({ - id: z.string(), - apiKey: z.string(), - name: z.string(), - threads: z.optional(z.array(ThreadSchema)), - agents: z.optional(z.array(AgentObjectSchema)), - defaultAgent: AgentObjectSchema, - sessions: z.optional(z.array(z.string())), + id: z.string(), + apiKey: z.string(), + name: z.string(), + threads: z.optional(z.array(ThreadSchema)), + agents: z.optional(z.array(AgentObjectSchema)), + tools: z.optional(z.array(z.any())), + defaultAgent: AgentObjectSchema, + sessions: z.optional(z.array(z.string())), }); export type UserSchema = z.infer; diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index e985ee7..90bdafa 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -8,7 +8,8 @@ import { } from "@/modules/Agent/AgentSchema"; import { AgentController } from "@/modules/Agent/AgentController"; import { getAgent } from "@/hooks/getAgent"; -import { getUser } from "@/hooks/getUser"; +import { AgentToolController } from "@/modules/AgentTool/AgentToolController"; +import { AgentToolSchema } from "@/modules/AgentTool/AgentToolSchema"; export async function setupAgentsRoute(app: FastifyInstance) { // POST Create a new agent @@ -32,34 +33,66 @@ export async function setupAgentsRoute(app: FastifyInstance) { handler: AgentController.getAgents, }); - // GET Agent by ID - app.get("/:agentId", { - schema: { - description: "Get Agent by ID.", - tags: ["Agent"], - response: { 200: AgentObjectSchema }, - }, - preHandler: [getUser, getAgent(["threads", "owner"])], - handler: AgentController.getAgent, - }); + await app.register(async (app) => { + app.addHook("preHandler", getAgent(["threads", "owner"])); - // PATCH Update an agent by ID - app.patch("/:agentId", { - schema: { - description: "Update Agent by ID.", - tags: ["Agent"], - body: AgentUpdateSchema, - response: { 200: AgentObjectSchema }, - }, - preHandler: [getAgent()], - handler: AgentController.updateAgent, - }); + // GET Agent by ID + app.get("/:agentId", { + schema: { + description: "Get Agent by ID.", + tags: ["Agent"], + response: { 200: AgentObjectSchema }, + }, + handler: AgentController.getAgent, + }); + + // PATCH Update an agent by ID + app.patch("/:agentId", { + schema: { + description: "Update Agent by ID.", + tags: ["Agent"], + body: AgentUpdateSchema, + response: { 200: AgentObjectSchema }, + }, + handler: AgentController.updateAgent, + }); + + // DELETE Agent by ID + app.delete("/:agentId", { + schema: { description: "Delete Agent by ID.", tags: ["Agent"] }, + handler: AgentController.deleteAgent, + }); + + await app.register(async (app) => { + app.addHook("preHandler", getAgent(["threads", "owner"])); + + // GET Agent by ID + app.get("/:agentId/tool/:toolId", { + schema: { + description: "Get Agent Tool by ID.", + tags: ["Agent Tool"], + response: { 200: AgentToolSchema }, + }, + handler: AgentToolController.getAgentTool, + }); + + // PATCH Update an agent by ID + app.patch("/:agentId/tool/:toolId", { + schema: { + description: "Update Agent Tool by ID.", + tags: ["Agent Tool"], + body: AgentUpdateSchema, + response: { 200: AgentToolSchema }, + }, + handler: AgentToolController.updateAgentTool, + }); - // DELETE Agent by ID - app.delete("/:agentId", { - schema: { description: "Delete Agent by ID.", tags: ["Agent"] }, - preHandler: [getAgent()], - handler: AgentController.deleteAgent, + // DELETE Agent by ID + app.delete("/:agentId/tool/:toolId", { + schema: { description: "Delete Agent Tool by ID.", tags: ["Agent Tool"] }, + handler: AgentToolController.deleteAgentTool, + }); + }); }); // GET list of available tools diff --git a/server/src/routes/server.ts b/server/src/routes/server.ts index 76c1390..baeddfd 100644 --- a/server/src/routes/server.ts +++ b/server/src/routes/server.ts @@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { resetDatabase, initDb } from "@/lib/pg"; +import { getUser } from "@/hooks/getUser"; export async function setupServerRoute(app: FastifyInstance) { app.get("/ping", { @@ -13,13 +14,17 @@ export async function setupServerRoute(app: FastifyInstance) { handler: async (_, reply) => reply.send("pong"), }); - app.get( - "/reset", - { schema: { description: "Reset the database", tags: ["Admin"] } }, - async (_, res) => { - await resetDatabase(); - await initDb(); - res.send({ ok: true }); - } - ); + await app.register(async (app) => { + app.addHook("preHandler", getUser); + + app.get( + "/reset", + { schema: { description: "Reset the database", tags: ["Admin"] } }, + async (_, res) => { + await resetDatabase(); + await initDb(); + res.send({ ok: true }); + } + ); + }); } diff --git a/server/src/routes/threads/messages.ts b/server/src/routes/threads/messages.ts index 704fcdb..463e0e0 100644 --- a/server/src/routes/threads/messages.ts +++ b/server/src/routes/threads/messages.ts @@ -13,6 +13,14 @@ import { MessageController } from "@/modules/Message/MessageController"; import { MessageFileController } from "@/modules/MessageFile/MessageFileController"; export async function setupMessagesRoute(app: FastifyInstance) { + app.addHook( + "preHandler", + getThread({ + activeMessage: true, + messages: { files: true }, + }) + ); + // POST Create Message in Thread app.post("/", { schema: { @@ -21,7 +29,6 @@ export async function setupMessagesRoute(app: FastifyInstance) { body: MessageCreateSchema, response: { 200: MessageObjectSchema }, }, - preHandler: [getThread(["activeMessage"])], handler: MessageController.createMessage, }); @@ -32,12 +39,6 @@ export async function setupMessagesRoute(app: FastifyInstance) { tags: ["Message"], response: { 200: MessageListSchema }, }, - preHandler: [ - getThread({ - activeMessage: true, - messages: { files: true }, - }), - ], handler: MessageController.getMessageList, }); @@ -52,50 +53,58 @@ export async function setupMessagesRoute(app: FastifyInstance) { handler: async (req, res) => res.send(req.message), }); - // PATCH Modify Message - app.patch("/:messageId", { - schema: { - description: "Modify Message.", - tags: ["Message"], - response: { 200: MessageObjectSchema }, - }, - preHandler: [getMessage()], - handler: MessageController.modifyMessage, - }); + await app.register(async (app) => { + app.addHook( + "preHandler", + getMessage({ + parent: true, + children: true, + files: { + fileData: true, + }, + }) + ); - // DELETE Message - app.delete("/:messageId", { - schema: { - description: "Delete Message.", - tags: ["Message"], - response: { 200: MessageSchemaWithoutId }, - }, - preHandler: [getMessage(["parent", "children"])], - handler: MessageController.deleteMessage, - }); + // PATCH Modify Message + app.patch("/:messageId", { + schema: { + description: "Modify Message.", + tags: ["Message"], + response: { 200: MessageObjectSchema }, + }, + handler: MessageController.modifyMessage, + }); - // POST Create a Message File - app.post("/:messageId/files", { - schema: { - description: "Create a Message File.", - tags: ["MessageFile"], - response: { 200: MessageObjectSchema }, - }, - preHandler: [getMessage()], - handler: MessageFileController.createMessageFile, - }); + // DELETE Message + app.delete("/:messageId", { + schema: { + description: "Delete Message.", + tags: ["Message"], + response: { 200: MessageSchemaWithoutId }, + }, + handler: MessageController.deleteMessage, + }); - // GET list of files for a message - app.get("/:messageId/files", { - schema: { description: "List Files for a Message.", tags: ["MessageFile"] }, - preHandler: [getMessage(["files"])], - handler: MessageFileController.getMessageFiles, - }); + // POST Create a Message File + app.post("/:messageId/files", { + schema: { + description: "Create a Message File.", + tags: ["MessageFile"], + response: { 200: MessageObjectSchema }, + }, + handler: MessageFileController.createMessageFile, + }); + + // GET list of files for a message + app.get("/:messageId/files", { + schema: { description: "List Files for a Message.", tags: ["MessageFile"] }, + handler: MessageFileController.getMessageFiles, + }); - // GET file by message ID - app.get("/:messageId/files/:fileId", { - schema: { description: "Get File by Message ID.", tags: ["MessageFile"] }, - preHandler: [getMessage({ files: { fileData: true } })], - handler: MessageFileController.getMessageFile, + // GET file by message ID + app.get("/:messageId/files/:fileId", { + schema: { description: "Get File by Message ID.", tags: ["MessageFile"] }, + handler: MessageFileController.getMessageFile, + }); }); } diff --git a/server/src/routes/threads/runs.ts b/server/src/routes/threads/runs.ts index 31fd369..ab4db47 100644 --- a/server/src/routes/threads/runs.ts +++ b/server/src/routes/threads/runs.ts @@ -5,67 +5,51 @@ import { CreateRunBody } from "@/modules/AgentRun/AgentRunSchema"; import { AgentRunController } from "@/modules/AgentRun/AgentRunController"; export async function setupAgentRunsRoute(app: FastifyInstance) { - // POST Create Thread and Run - app.post("/runs", { - schema: { - description: "Create Thread and Run.", - tags: ["Run"], - body: CreateRunBody, - }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: (req, rep) => rep.send("TODO"), - }); + app.addHook( + "preHandler", + getThread({ + activeMessage: true, + messages: { files: { fileData: true } }, + }) + ); - // POST Create Run for Thread - app.post("/:threadId/runs", { - schema: { description: "Create a run.", tags: ["Run"], body: CreateRunBody }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: AgentRunController.createAndRunHandler, - }); + // POST Create Thread and Run + app.post("/runs", { + schema: { + description: "Create Thread and Run.", + tags: ["Run"], + body: CreateRunBody, + }, + handler: (req, rep) => rep.send("TODO"), + }); - // GET List of Runs for Thread - app.get("/:threadId/runs", { - schema: { description: "List of Runs for a Thread.", tags: ["Run"] }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: (req, rep) => rep.send("TODO"), - }); + // POST Create Run for Thread + app.post("/:threadId/runs", { + schema: { description: "Create a run.", tags: ["Run"], body: CreateRunBody }, + handler: AgentRunController.createAndRunHandler, + }); - // GET Get Run for Thread - app.get("/:threadId/runs/:runId", { - schema: { description: "Get Run for Thread.", tags: ["Run"] }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: (req, rep) => rep.send("TODO"), - }); + // GET List of Runs for Thread + app.get("/:threadId/runs", { + schema: { description: "List of Runs for a Thread.", tags: ["Run"] }, + handler: (req, rep) => rep.send("TODO"), + }); - // POST Modify a Run - app.post("/:threadId/runs/:runId", { - schema: { description: "Modify Run for Thread.", tags: ["Run"] }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: (req, rep) => rep.send("TODO"), - }); + // GET Get Run for Thread + app.get("/:threadId/runs/:runId", { + schema: { description: "Get Run for Thread.", tags: ["Run"] }, + handler: (req, rep) => rep.send("TODO"), + }); - // POST Cancel a Run - app.post("/:threadId/runs/:runId/cancel", { - schema: { description: "Cancel a Run for Thread.", tags: ["Run"] }, - preHandler: getThread({ - activeMessage: true, - messages: { files: { fileData: true } }, - }), - handler: (req, rep) => rep.send("TODO"), - }); + // POST Modify a Run + app.post("/:threadId/runs/:runId", { + schema: { description: "Modify Run for Thread.", tags: ["Run"] }, + handler: (req, rep) => rep.send("TODO"), + }); + + // POST Cancel a Run + app.post("/:threadId/runs/:runId/cancel", { + schema: { description: "Cancel a Run for Thread.", tags: ["Run"] }, + handler: (req, rep) => rep.send("TODO"), + }); } diff --git a/server/src/routes/threads/threads.ts b/server/src/routes/threads/threads.ts index 26e6ca6..b175b66 100644 --- a/server/src/routes/threads/threads.ts +++ b/server/src/routes/threads/threads.ts @@ -5,52 +5,53 @@ import { ThreadSchema, ThreadListSchema } from "@/modules/Thread/ThreadSchema"; import { ThreadController } from "@/modules/Thread/ThreadController"; export async function setupThreadsRoute(app: FastifyInstance) { - // GET Thread History for user - app.get("/", { - schema: { - description: "List Threads for User.", - tags: ["Thread"], - response: { 200: ThreadListSchema }, - }, - handler: async (request, reply) => reply.send(request.user.threads), - }); + // GET Thread History for user + app.get("/", { + schema: { + description: "List Threads for User.", + tags: ["Thread"], + response: { 200: ThreadListSchema }, + }, + handler: async (request, reply) => reply.send(request.user.threads), + }); - // POST Create a new thread - app.post("/", { - schema: { - description: "Create new Thread.", - tags: ["Thread"], - response: { 200: ThreadSchema }, - }, - handler: ThreadController.createThread, - }); + // POST Create a new thread + app.post("/", { + schema: { + description: "Create new Thread.", + tags: ["Thread"], + response: { 200: ThreadSchema }, + }, + handler: ThreadController.createThread, + }); - // GET Thread by ID - app.get("/:threadId", { - schema: { - description: "Get Thread by ID.", - tags: ["Thread"], - response: { 200: ThreadSchema }, - }, - preHandler: [getThread()], - handler: async (req, res) => res.send(req.thread), - }); + await app.register(async (app) => { + app.addHook("preHandler", getThread()); - // POST Update a thread by ID - app.post("/:threadId", { - schema: { - description: "Update Thread by ID.", - tags: ["Thread"], - response: { 200: ThreadSchema }, - }, - preHandler: [getThread()], - handler: ThreadController.updateThread, - }); + // GET Thread by ID + app.get("/:threadId", { + schema: { + description: "Get Thread by ID.", + tags: ["Thread"], + response: { 200: ThreadSchema }, + }, + handler: async (req, res) => res.send(req.thread), + }); - // DELETE Thread by ID - app.delete("/:threadId", { - schema: { description: "Delete Thread by ID.", tags: ["Thread"] }, - preHandler: [getThread()], - handler: ThreadController.deleteThread, - }); + // POST Update a thread by ID + app.post("/:threadId", { + schema: { + description: "Update Thread by ID.", + tags: ["Thread"], + response: { 200: ThreadSchema }, + }, + handler: ThreadController.updateThread, + }); + + // DELETE Thread by ID + app.delete("/:threadId", { + schema: { description: "Delete Thread by ID.", tags: ["Thread"] }, + handler: ThreadController.deleteThread, + }); + }); } diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts index c587a62..0e4c492 100644 --- a/server/src/routes/user.ts +++ b/server/src/routes/user.ts @@ -9,18 +9,6 @@ import { Agent } from "@/modules/Agent/AgentModel"; import { User } from "@/modules/User/UserModel"; export async function setupUserRoute(app: FastifyInstance) { - app.get("/user", { - schema: { - description: "Get the current user", - tags: ["User"], - response: { 200: UserSchema }, - }, - handler: async (request, reply) => { - await getUser(request, reply); - reply.send(request.user); - }, - }); - app.post("/user", { schema: { description: "Create user", @@ -61,28 +49,35 @@ export async function setupUserRoute(app: FastifyInstance) { }, }); - app.get("/user/:userId", { - schema: { - description: "Get user by ID", - tags: ["User"], - response: { 200: UserSchema }, - }, - handler: async (request, reply) => { - await getUser(request, reply); - return reply.send(request.user); - }, - }); - - app.get("/user/session", { - schema: { - description: "Get user session by ID", - tags: ["User"], - response: { 200: UserSchema }, - }, - handler: async (request, reply) => { - await getUser(request, reply); - return reply.send(request.user); - }, + await app.register(async (app) => { + app.addHook("preHandler", getUser); + + app.get("/user", { + schema: { + description: "Get the current user", + tags: ["User"], + response: { 200: UserSchema }, + }, + handler: async (request, reply) => reply.send(request.user), + }); + + app.get("/user/:userId", { + schema: { + description: "Get user by ID", + tags: ["User"], + response: { 200: UserSchema }, + }, + handler: async (request, reply) => reply.send(request.user), + }); + + app.get("/user/session", { + schema: { + description: "Get user session by ID", + tags: ["User"], + response: { 200: UserSchema }, + }, + handler: async (request, reply) => reply.send(request.user), + }); }); app.post("/user/session", { diff --git a/server/src/types/patches.ts b/server/src/types/patches.ts index fb55923..98f1c0a 100644 --- a/server/src/types/patches.ts +++ b/server/src/types/patches.ts @@ -2,13 +2,15 @@ import type { User } from "@/modules/User/UserModel"; import type { Thread } from "@/modules/Thread/ThreadModel"; import type { Agent } from "@/modules/Agent/AgentModel"; import type { Message } from "@/modules/Message/MessageModel"; +import type { AgentTool } from "@/modules/AgentTool/AgentToolModel"; declare module "fastify" { - export interface FastifyRequest { - /** The user ID of the authenticated user */ - user: User; - thread: Thread; - message: Message; - agent: Agent; - } + export interface FastifyRequest { + /** The user ID of the authenticated user */ + user: User; + thread: Thread; + message: Message; + agent: Agent; + agentTool: AgentTool; + } }