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 (
-
- );
+ return (
+
+ );
};
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 (
-
+
+
+
+
+ {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;
+ }
}