diff --git a/client/bun.lockb b/client/bun.lockb index b6d60da..fe78908 100755 Binary files a/client/bun.lockb and b/client/bun.lockb differ diff --git a/client/package.json b/client/package.json index 2c770e7..ffc444e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,131 +1,132 @@ { - "name": "mychat", - "main": "expo-router/entry", - "version": "1.0.0", - "scripts": { - "start": "APP_VARIANT=development expo start", - "start:clean": "APP_VARIANT=development expo start -c", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web", - "test": "jest --watchAll", - "export": "expo export -p web", - "build": "expo run:ios --no-build-cache", - "build:dev": "APP_VARIANT=development expo run:ios --device --no-build-cache", - "build:prev": "APP_VARIANT=preview expo run:ios --device", - "build:sign": "APP_VARIANT=preview eas build -e preview -p ios", - "analyze:web": "source-map-explorer 'dist/_expo/static/js/web/*.js' 'dist/_expo/static/js/web/*.js.map'", - "analyze:ios": "source-map-explorer 'dist/_expo/static/js/ios/*.js' 'dist/_expo/static/js/ios/*.js.map'", - "postinstall": "patch-package", - "tsc": "tsc", - "lint": "eslint ." - }, - "jest": { - "preset": "jest-expo", - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "dependencies": { - "@expo/metro-runtime": "^3.1.3", - "@expo/vector-icons": "^14.0.0", - "@hookform/resolvers": "^3.3.4", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-switch": "^1.0.3", - "@react-native-async-storage/async-storage": "1.23.1", - "@react-native-community/netinfo": "^11.3.1", - "@react-navigation/drawer": "^6.6.15", - "@react-navigation/elements": "^1.3.30", - "@react-navigation/native": "^6.1.17", - "@react-navigation/native-stack": "^6.9.26", - "@shopify/flash-list": "^1.6.4", - "@tanstack/query-async-storage-persister": "^5.29.1", - "@tanstack/react-query": "^5.29.2", - "@tanstack/react-query-persist-client": "^5.29.2", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "cmdk": "^1.0.0", - "expo": "~50.0.17", - "expo-clipboard": "~5.0.1", - "expo-dev-client": "~3.3.11", - "expo-document-picker": "^11.10.1", - "expo-font": "~11.10.3", - "expo-haptics": "~12.8.1", - "expo-image": "~1.10.6", - "expo-linking": "~6.2.2", - "expo-network": "^5.8.0", - "expo-router": "~3.4.8", - "expo-splash-screen": "~0.26.4", - "expo-status-bar": "~1.11.1", - "expo-system-ui": "~2.9.4", - "expo-updates": "^0.24.12", - "expo-web-browser": "~12.8.2", - "gpt4-tokenizer": "^1.3.0", - "nativewind": "^4.0.1", - "openai": "^4.38.1", - "patch-package": "^8.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-hook-form": "^7.51.3", - "react-native": "0.73.7", - "react-native-animation-library": "^0.0.8", - "react-native-drawer-layout": "^3.3.0", - "react-native-fetch-api": "^3.0.0", - "react-native-gesture-handler": "~2.16.0", - "react-native-ios-context-menu": "^2.5.1", - "react-native-ios-utilities": "^4.4.5", - "react-native-markdown-display": "^7.0.2", - "react-native-react-query-devtools": "^1.1.0", - "react-native-reanimated": "~3.8.1", - "react-native-root-siblings": "^5.0.1", - "react-native-safe-area-context": "4.9.0", - "react-native-screens": "~3.30.1", - "react-native-svg": "^15.1.0", - "react-native-toast-message": "^2.2.0", - "react-native-url-polyfill": "^2.0.0", - "react-native-vector-icons": "^10.0.3", - "react-native-web": "~0.19.10", - "react-syntax-highlighter": "^15.5.0", - "react-textarea-autosize": "^8.5.3", - "tailwind-merge": "^2.3.0", - "tailwindcss": "^3.4.3", - "tailwindcss-animate": "^1.0.7", - "text-encoding": "^0.7.0", - "web-streams-polyfill": "^4.0.0", - "zod": "^3.22.5", - "zustand": "^4.5.2" - }, - "devDependencies": { - "@babel/core": "^7.24.4", - "@tanstack/eslint-plugin-query": "^5.28.11", - "@types/bun": "^1.1.0", - "@types/react": "~18.2.79", - "@types/react-dom": "^18.2.25", - "@types/react-syntax-highlighter": "^15.5.11", - "@types/text-encoding": "^0.0.39", - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", - "ajv": "^8.12.0", - "eslint": "8.57.0", - "eslint-config-react-app": "^7.0.1", - "jest": "^29.7.0", - "jest-expo": "~50.0.4", - "react-test-renderer": "18.2.0", - "source-map-explorer": "^2.5.3", - "typescript": "next" - }, - "resolutions": { - "@typescript-eslint/eslint-plugin": "^7.7.0", - "react": "^18.2", - "react-native": "^0.73", - "react-native-svg": "^15.1.0", - "typescript": "next" - }, - "private": true + "name": "mychat", + "main": "expo-router/entry", + "version": "1.0.0", + "scripts": { + "start": "APP_VARIANT=development expo start", + "start:clean": "APP_VARIANT=development expo start -c", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "test": "jest --watchAll", + "export": "expo export -p web", + "build": "expo run:ios --no-build-cache", + "build:dev": "APP_VARIANT=development expo run:ios --device --no-build-cache", + "build:prev": "APP_VARIANT=preview expo run:ios --device", + "build:sign": "APP_VARIANT=preview eas build -e preview -p ios", + "analyze:web": "source-map-explorer 'dist/_expo/static/js/web/*.js' 'dist/_expo/static/js/web/*.js.map'", + "analyze:ios": "source-map-explorer 'dist/_expo/static/js/ios/*.js' 'dist/_expo/static/js/ios/*.js.map'", + "postinstall": "patch-package", + "tsc": "tsc", + "lint": "eslint ." + }, + "jest": { + "preset": "jest-expo", + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "dependencies": { + "@expo/metro-runtime": "^3.1.3", + "@expo/vector-icons": "^14.0.0", + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-switch": "^1.0.3", + "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-community/netinfo": "^11.3.1", + "@react-navigation/drawer": "^6.6.15", + "@react-navigation/elements": "^1.3.30", + "@react-navigation/native": "^6.1.17", + "@react-navigation/native-stack": "^6.9.26", + "@shopify/flash-list": "^1.6.4", + "@tanstack/query-async-storage-persister": "^5.29.1", + "@tanstack/react-query": "^5.29.2", + "@tanstack/react-query-persist-client": "^5.29.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "cmdk": "^1.0.0", + "expo": "~50.0.17", + "expo-clipboard": "~5.0.1", + "expo-dev-client": "~3.3.11", + "expo-document-picker": "^11.10.1", + "expo-font": "~11.10.3", + "expo-haptics": "~12.8.1", + "expo-image": "~1.10.6", + "expo-linking": "~6.2.2", + "expo-network": "^5.8.0", + "expo-router": "~3.4.8", + "expo-splash-screen": "~0.26.4", + "expo-status-bar": "~1.11.1", + "expo-system-ui": "~2.9.4", + "expo-updates": "^0.24.12", + "expo-web-browser": "~12.8.2", + "gpt4-tokenizer": "^1.3.0", + "nativewind": "^4.0.1", + "openai": "^4.38.1", + "patch-package": "^8.0.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "^7.51.3", + "react-native": "0.73.7", + "react-native-animation-library": "^0.0.8", + "react-native-drawer-layout": "^3.3.0", + "react-native-fetch-api": "^3.0.0", + "react-native-gesture-handler": "~2.16.0", + "react-native-ios-context-menu": "^2.5.1", + "react-native-ios-utilities": "^4.4.5", + "react-native-markdown-display": "^7.0.2", + "react-native-react-query-devtools": "^1.1.0", + "react-native-reanimated": "~3.8.1", + "react-native-root-siblings": "^5.0.1", + "react-native-safe-area-context": "4.9.0", + "react-native-screens": "~3.30.1", + "react-native-svg": "^15.1.0", + "react-native-toast-message": "^2.2.0", + "react-native-url-polyfill": "^2.0.0", + "react-native-vector-icons": "^10.0.3", + "react-native-web": "~0.19.10", + "react-syntax-highlighter": "^15.5.0", + "react-textarea-autosize": "^8.5.3", + "tailwind-merge": "^2.3.0", + "tailwindcss": "^3.4.3", + "tailwindcss-animate": "^1.0.7", + "text-encoding": "^0.7.0", + "web-streams-polyfill": "^4.0.0", + "zod": "^3.22.5", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@babel/core": "^7.24.4", + "@tanstack/eslint-plugin-query": "^5.28.11", + "@types/bun": "^1.1.0", + "@types/react": "~18.2.79", + "@types/react-dom": "^18.2.25", + "@types/react-syntax-highlighter": "^15.5.11", + "@types/text-encoding": "^0.0.39", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", + "ajv": "^8.12.0", + "eslint": "8.57.0", + "eslint-config-react-app": "^7.0.1", + "jest": "^29.7.0", + "jest-expo": "~50.0.4", + "react-test-renderer": "18.2.0", + "source-map-explorer": "^2.5.3", + "typescript": "next" + }, + "resolutions": { + "@typescript-eslint/eslint-plugin": "^7.7.0", + "react": "^18.2", + "react-native": "^0.73", + "react-native-svg": "^15.1.0", + "typescript": "next" + }, + "private": true } diff --git a/client/src/components/primitives/checkbox/checkbox.tsx b/client/src/components/primitives/checkbox/checkbox.tsx new file mode 100644 index 0000000..2c5f1eb --- /dev/null +++ b/client/src/components/primitives/checkbox/checkbox.tsx @@ -0,0 +1,109 @@ +/* eslint-disable jsx-a11y/role-supports-aria-props */ +import * as React from "react"; +import { GestureResponderEvent, Pressable, View } from "react-native"; +import * as Slot from "@/components/primitives/slot"; +import type { + ComponentPropsWithAsChild, + PressableRef, + SlottablePressableProps, +} from "@/components/primitives/types"; +import type { CheckboxIndicator, CheckboxRootProps } from "./types"; + +interface RootContext extends CheckboxRootProps { + nativeID?: string; +} + +const CheckboxContext = React.createContext(null); + +const Root = React.forwardRef( + ( + { asChild, disabled = false, checked, onCheckedChange, nativeID, ...props }, + ref + ) => { + return ( + + + + ); + } +); + +Root.displayName = "RootNativeCheckbox"; + +function useCheckboxContext() { + const context = React.useContext(CheckboxContext); + if (!context) { + throw new Error( + "Checkbox compound components cannot be rendered outside the Checkbox component" + ); + } + return context; +} + +const Trigger = React.forwardRef( + ({ asChild, onPress: onPressProp, ...props }, ref) => { + const { disabled, checked, onCheckedChange, nativeID } = useCheckboxContext(); + + function onPress(ev: GestureResponderEvent) { + if (disabled) return; + const newValue = !checked; + onCheckedChange(newValue); + onPressProp?.(ev); + } + + const Component = asChild ? Slot.Pressable : Pressable; + return ( + + ); + } +); + +Trigger.displayName = "TriggerNativeCheckbox"; + +const Indicator = React.forwardRef< + React.ElementRef, + ComponentPropsWithAsChild & CheckboxIndicator +>(({ asChild, forceMount, ...props }, ref) => { + const { checked, disabled } = useCheckboxContext(); + + if (!forceMount) { + if (!checked) { + return null; + } + } + + const Component = asChild ? Slot.View : View; + return ( + + ); +}); + +Indicator.displayName = "IndicatorNativeCheckbox"; + +export { Indicator, Root }; diff --git a/client/src/components/primitives/checkbox/checkbox.web.tsx b/client/src/components/primitives/checkbox/checkbox.web.tsx new file mode 100644 index 0000000..5135cf8 --- /dev/null +++ b/client/src/components/primitives/checkbox/checkbox.web.tsx @@ -0,0 +1,126 @@ +import * as Checkbox from "@radix-ui/react-checkbox"; +import * as React from "react"; +import { GestureResponderEvent, Pressable, View } from "react-native"; +import { useAugmentedRef } from "@/components/primitives/hooks"; +import * as Slot from "@/components/primitives/slot"; +import type { + ComponentPropsWithAsChild, + PressableRef, + SlottablePressableProps, +} from "@/components/primitives/types"; +import type { CheckboxIndicator, CheckboxRootProps } from "./types"; + +const CheckboxContext = React.createContext(null); + +const Root = React.forwardRef( + ( + { + asChild, + disabled, + checked, + onCheckedChange, + onPress: onPressProp, + role: _role, + ...props + }, + ref + ) => { + const augmentedRef = useAugmentedRef({ ref }); + + function onPress(ev: GestureResponderEvent) { + onPressProp?.(ev); + onCheckedChange(!checked); + } + + React.useLayoutEffect(() => { + if (augmentedRef.current) { + const augRef = augmentedRef.current as unknown as HTMLButtonElement; + augRef.dataset.state = checked ? "checked" : "unchecked"; + augRef.value = checked ? "on" : "off"; + } + }, [checked]); + + React.useLayoutEffect(() => { + if (augmentedRef.current) { + const augRef = augmentedRef.current as unknown as HTMLButtonElement; + augRef.type = "button"; + augRef.role = "checkbox"; + + if (disabled) { + augRef.dataset.disabled = "true"; + } else { + augRef.dataset.disabled = undefined; + } + } + }, [disabled]); + + const Component = asChild ? Slot.Pressable : Pressable; + return ( + + + + + + ); + } +); + +Root.displayName = "RootWebCheckbox"; + +function useCheckboxContext() { + const context = React.useContext(CheckboxContext); + if (context === null) { + throw new Error( + "Checkbox compound components cannot be rendered outside the Checkbox component" + ); + } + return context; +} + +const Indicator = React.forwardRef< + React.ElementRef, + ComponentPropsWithAsChild & CheckboxIndicator +>(({ asChild, forceMount, ...props }, ref) => { + const { checked, disabled } = useCheckboxContext(); + const augmentedRef = useAugmentedRef({ ref }); + + React.useLayoutEffect(() => { + if (augmentedRef.current) { + const augRef = augmentedRef.current as unknown as HTMLDivElement; + augRef.dataset.state = checked ? "checked" : "unchecked"; + } + }, [checked]); + + React.useLayoutEffect(() => { + if (augmentedRef.current) { + const augRef = augmentedRef.current as unknown as HTMLDivElement; + if (disabled) { + augRef.dataset.disabled = "true"; + } else { + augRef.dataset.disabled = undefined; + } + } + }, [disabled]); + + const Component = asChild ? Slot.View : View; + return ( + + + + ); +}); + +Indicator.displayName = "IndicatorWebCheckbox"; + +export { Indicator, Root }; diff --git a/client/src/components/primitives/checkbox/index.tsx b/client/src/components/primitives/checkbox/index.tsx new file mode 100644 index 0000000..057f167 --- /dev/null +++ b/client/src/components/primitives/checkbox/index.tsx @@ -0,0 +1 @@ +export * from "./checkbox"; diff --git a/client/src/components/primitives/checkbox/types.ts b/client/src/components/primitives/checkbox/types.ts new file mode 100644 index 0000000..3d33288 --- /dev/null +++ b/client/src/components/primitives/checkbox/types.ts @@ -0,0 +1,11 @@ +import type { ForceMountable } from "@/components/primitives/types"; + +interface CheckboxRootProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + disabled?: boolean; +} + +type CheckboxIndicator = ForceMountable; + +export type { CheckboxRootProps, CheckboxIndicator }; diff --git a/client/src/components/ui/Checkboz.tsx b/client/src/components/ui/Checkboz.tsx new file mode 100644 index 0000000..da79577 --- /dev/null +++ b/client/src/components/ui/Checkboz.tsx @@ -0,0 +1,36 @@ +import { Icon } from "@/components/ui/Icon"; +import * as React from "react"; +import * as CheckboxPrimitive from "@/components/primitives/checkbox"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/client/src/types/index.ts b/client/src/types/index.ts index 92bfa8a..8d1c999 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -2,8 +2,8 @@ export type { MessageFileObjectSchema as MessageFile } from "@db/MessageFile/Mes export type { MessageObjectSchema as Message } from "@db/Message/MessageSchema"; export type { - ThreadSchema as Thread, - ThreadSchemaWithoutId as ThreadDelete, + ThreadSchema as Thread, + ThreadSchemaWithoutId as ThreadDelete, } from "@db/Thread/ThreadSchema"; export type { UserSchema as User } from "@db/User/UserSchema"; @@ -11,13 +11,14 @@ export type { UserSchema as User } from "@db/User/UserSchema"; export type { UserSessionSchema as UserSession } from "@db/User/SessionSchema"; export type { - AgentObjectSchema as Agent, - AgentToolsSchema as Tool, - AgentCreateSchema, - AgentUpdateSchema, + AgentObjectSchema as Agent, + AgentCreateSchema, + AgentUpdateSchema, } from "@db/Agent/AgentSchema"; +export type { ToolName as Tool } from "@db/LLMNexus/Tools"; + export type { - ModelLiteral as Model, - ModelInfoSchema as ModelInformation, + ModelLiteral as Model, + ModelInfoSchema as ModelInformation, } from "@db/Models/ModelsSchema"; diff --git a/client/src/views/agent/AgentView.web.tsx b/client/src/views/agent/AgentView.web.tsx index fc7f9e9..8c98e22 100644 --- a/client/src/views/agent/AgentView.web.tsx +++ b/client/src/views/agent/AgentView.web.tsx @@ -6,17 +6,17 @@ import { SystemMessage } from "./helpers/SystemMessage"; import { ModelSection, ModelStats, ToolSection } from "./helpers"; export function AgentView({ agent }: { agent: Agent }) { - const ViewRef = useRef(null); + const ViewRef = useRef(null); - return ( - } - className="flex w-full gap-4 p-2" - > - - - - - - ); + return ( + } + className="flex w-full gap-4 p-2" + > + + + + + + ); } diff --git a/client/src/views/agent/helpers/SystemMessage.tsx b/client/src/views/agent/helpers/SystemMessage.tsx index 2f4956f..fd760ea 100644 --- a/client/src/views/agent/helpers/SystemMessage.tsx +++ b/client/src/views/agent/helpers/SystemMessage.tsx @@ -43,7 +43,10 @@ export function SystemMessage({ agent }: { agent: Agent }) { ) : ( - setEditMode(true)} className="text-xs"> + setEditMode(true)} + className="text-xs hover:text-foreground text-foreground/50" + > Edit ) diff --git a/client/src/views/agent/helpers/ToggleTools.tsx b/client/src/views/agent/helpers/ToggleTools.tsx new file mode 100644 index 0000000..5a1eff6 --- /dev/null +++ b/client/src/views/agent/helpers/ToggleTools.tsx @@ -0,0 +1,33 @@ +import Toast from "react-native-toast-message"; + +import { useAgentPatch } from "@/hooks/fetchers/Agent/useAgentPatch"; +import { Agent } from "@/types"; +import { Switch } from "@/components/ui/Switch"; + +export function ToggleToolsSwitch({ agent }: { agent: Agent }) { + const agentEditMut = useAgentPatch(); + + const onCheckedChange = async (checked: boolean) => { + try { + await agentEditMut.mutateAsync({ + agentId: agent.id, + agentConfig: { type: "toolsEnabled", value: checked }, + }); + } catch (error: any) { + console.error(error); + Toast.show({ + type: "error", + text1: "Error", + text2: "message" in error ? error.message : "An error occurred", + }); + } + }; + + return ( + + ); +} diff --git a/client/src/views/agent/helpers/ToolOption.tsx b/client/src/views/agent/helpers/ToolOption.tsx deleted file mode 100644 index 11f9762..0000000 --- a/client/src/views/agent/helpers/ToolOption.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Pressable } from "react-native"; - -import { Agent, Tool } from "@/types"; -import { Text } from "@/components/ui/Text"; -import { useState } from "react"; - -export function ToolOption({ agent, tool }: { agent: Agent; tool: Tool }) { - const [open, setOpen] = useState(false); - return ( - setOpen(!open)} - > - {tool} - - ); -} diff --git a/client/src/views/agent/helpers/ToolSection.tsx b/client/src/views/agent/helpers/ToolSection.tsx index acec91e..a156028 100644 --- a/client/src/views/agent/helpers/ToolSection.tsx +++ b/client/src/views/agent/helpers/ToolSection.tsx @@ -1,51 +1,61 @@ +import { useState } from "react"; +import { Pressable, View } from "react-native"; + import { Agent, Tool } from "@/types"; -import { useAgentPatch } from "@/hooks/fetchers/Agent/useAgentPatch"; import { useToolsQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; -import { Section, RowItem } from "@/components/ui/Section"; -import { Switch } from "@/components/ui/Switch"; +import { Section } from "@/components/ui/Section"; import { Text } from "@/components/ui/Text"; -import { ToolOption } from "./ToolOption"; +import { Checkbox } from "@/components/ui/Checkboz"; +import { ToolDialog } from "@/views/tools/ToolDialog.web"; +import { ToggleToolsSwitch } from "./ToggleTools"; export function ToolSection({ agent }: { agent: Agent }) { - const { data } = useToolsQuery(); - const agentEditMut = useAgentPatch(); - - const onCheckedChange = async (checked: boolean) => { - try { - await agentEditMut.mutateAsync({ - agentId: agent.id, - agentConfig: { type: "toolsEnabled", value: checked }, - }); - } catch (error) { - console.error(error); - } - }; - return ( -
- - Enabled - - - {agent.toolsEnabled && - (data && data.length ? ( - data.map((tool) => ( - - )) - ) : ( - No tools Found - ))} +
+ + + + + + + Configure + + + + + } + > + {agent.toolsEnabled ? ( + + ) : ( + Tools are disabled + )}
); } -function ToolFilter({ agent, tool }: { agent: Agent; tool: Tool }) { - switch (tool) { - case "Browser": - return ; - } +function ToolList({ agent }: { agent: Agent }) { + const { data } = useToolsQuery(); + + return data && data.length ? ( + data.map((tool) => ) + ) : ( + No tools Found + ); +} + +export function ToolOption({ agent, tool }: { agent: Agent; tool: Tool }) { + const [open, setOpen] = useState(false); + return ( + setOpen(!open)} + > + null} /> + {tool} + + ); } diff --git a/client/src/views/agents/AgentsView.web.tsx b/client/src/views/agents/AgentsView.web.tsx index d3d068f..18100e7 100644 --- a/client/src/views/agents/AgentsView.web.tsx +++ b/client/src/views/agents/AgentsView.web.tsx @@ -12,102 +12,102 @@ import { Button } from "@/components/ui/Button"; import { AgentDialog } from "@/views/agent/AgentDialog.web"; export function AgentsView() { - const { data, isSuccess, error } = useAgentsQuery(); + const { data, isSuccess, error } = useAgentsQuery(); - if (!isSuccess) { - if (error) console.error(error); - } - const agents = data || []; - return ( - - - - - Agents - - - {agents.length > 0 ? ( - agents.map((a, i) => ) - ) : ( - No agents found - )} - - - - - - - ); + if (!isSuccess) { + if (error) console.error(error); + } + const agents = data || []; + return ( + + + + + Agents + + + {agents.length > 0 ? ( + agents.map((a, i) => ) + ) : ( + No agents found + )} + + + + + + + ); } function NewAgentButton() { - return ( - - - - ); + return ( + + + + ); } function AgentButton({ agent }: { agent: Agent }) { - return ( - - - - {agent.name} - Description - - - - - - - + return ( + + + + {agent.name} + Description + + + + + + + - - - - - - - - - ); + + + + + + + + + ); } function CollapseDrawer() { - const navigation = useNavigation>(); - const isDrawerOpen = useDrawerStatus() === "open"; + const navigation = useNavigation>(); + const isDrawerOpen = useDrawerStatus() === "open"; - return ( - - { - navigation.dispatch(DrawerActions.toggleDrawer()); - Keyboard.dismiss(); - }} - hitSlop={{ top: 16, right: 16, bottom: 16, left: 16 }} - > - - - - ); + return ( + + { + navigation.dispatch(DrawerActions.toggleDrawer()); + Keyboard.dismiss(); + }} + hitSlop={{ top: 16, right: 16, bottom: 16, left: 16 }} + > + + + + ); } diff --git a/client/src/views/chat/ChatHeader/Dropdown.tsx b/client/src/views/chat/ChatHeader/Dropdown.tsx index 584edc3..11f99a5 100644 --- a/client/src/views/chat/ChatHeader/Dropdown.tsx +++ b/client/src/views/chat/ChatHeader/Dropdown.tsx @@ -7,106 +7,104 @@ import { useAgentQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; import { useMessagesQuery } from "@/hooks/fetchers/Message/useMessagesQuery"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuShortcut, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuShortcut, + DropdownMenuTrigger, } from "@/components/ui/DropdownMenu"; import { Text } from "@/components/ui/Text"; import { Entypo } from "@/components/ui/Icon"; import { AgentDialog } from "@/views/agent/AgentDialog.web"; export function Dropdown({ - children, - className, - threadId, + children, + className, + threadId, }: { - children: React.ReactNode; - className?: string; - threadId: string | null; + children: React.ReactNode; + className?: string; + threadId: string | null; }) { - const threadQuery = useThreadQuery(threadId); - const { - data: { user }, - } = useUser(); - const currentAgent = threadId ? threadQuery.data?.agent : user?.defaultAgent; - const agentQuery = useAgentQuery(currentAgent?.id || ""); + const threadQuery = useThreadQuery(threadId); + const { data } = useUser(); + const currentAgent = threadId ? threadQuery.data?.agent : data?.user?.defaultAgent; + const agentQuery = useAgentQuery(currentAgent?.id || ""); - const [open, setOpen] = useState(false); - const [agentOpen, setAgentOpen] = useState(false); + const [open, setOpen] = useState(false); + const [agentOpen, setAgentOpen] = useState(false); - const deleteThread = useAction("deleteThread")(); - const { data: messages } = useMessagesQuery(threadId); + const deleteThread = useAction("deleteThread")(); + const { data: messages } = useMessagesQuery(threadId); - const tokens = messages?.reduce((acc, m) => acc + m.tokenCount || 0, 0) || 0; + const tokens = messages?.reduce((acc, m) => acc + m.tokenCount || 0, 0) || 0; - const openAgentMenu = () => setAgentOpen(true); + const openAgentMenu = () => setAgentOpen(true); - const actions = [ - { - label: `Tokens: ${tokens} / ${agentQuery.data?.model?.params.maxTokens}`, - hidden: !agentQuery.data?.model, - }, - { - label: "View Agent", - onPress: openAgentMenu, - icon: Entypo, - iconLabel: "chevron-right", - }, - { - label: "Delete Thread", - onPress: () => deleteThread.action(threadId!), - }, - { - label: "Share Thread", - disabled: true, - }, - ]; + const actions = [ + { + label: `Tokens: ${tokens} / ${agentQuery.data?.model?.params.maxTokens}`, + hidden: !agentQuery.data?.model || !threadId, + }, + { + label: "View Agent", + onPress: openAgentMenu, + icon: Entypo, + iconLabel: "chevron-right", + }, + { + label: "Delete Thread", + onPress: () => deleteThread.action(threadId!), + }, + { + label: "Share Thread", + disabled: true, + }, + ]; - if (agentQuery.isError) { - console.error(agentQuery.error); - } - return ( - <> - setOpen(newVal)}> - - {children} - - - - {actions.map((action, index) => - !action.hidden ? ( - action.onPress() - : undefined - } - > - {action.label} - {action.icon && ( - - - - )} - - ) : null - )} - - - - {agentQuery.data && ( - setAgentOpen(false)} - /> - )} - - ); + if (agentQuery.isError) { + console.error(agentQuery.error); + } + return ( + <> + setOpen(newVal)}> + + {children} + + + + {actions.map((action, index) => + !action.hidden ? ( + action.onPress() + : undefined + } + > + {action.label} + {action.icon && ( + + + + )} + + ) : null + )} + + + + {agentQuery.data && ( + setAgentOpen(false)} + /> + )} + + ); } diff --git a/client/src/views/tools/ToolCard.tsx b/client/src/views/tools/ToolCard.tsx new file mode 100644 index 0000000..7550a00 --- /dev/null +++ b/client/src/views/tools/ToolCard.tsx @@ -0,0 +1,49 @@ +import { View } from "react-native"; + +import { Agent, Tool } from "@/types"; +import { Text } from "@/components/ui/Text"; +import { Switch } from "@/components/ui/Switch"; +import { useAgentPatch } from "@/hooks/fetchers/Agent/useAgentPatch"; +import Toast from "react-native-toast-message"; + +export function ToolCard({ agent, tool }: { agent: Agent; tool: Tool }) { + const agentEditMut = useAgentPatch(); + + const onCheckedChange = async (checked: boolean) => { + try { + const value = checked + ? [...agent.tools, tool] + : agent.tools.filter((t) => t !== tool); + + await agentEditMut.mutateAsync({ + agentId: agent.id, + agentConfig: { type: "tools", value }, + }); + } catch (error: any) { + console.error(error); + Toast.show({ + type: "error", + text1: "Error", + text2: "message" in error ? error.message : "An error occurred", + }); + } + }; + + return ( + + + {tool} + + + + + ); +} + +export function ToolOverview() { + return Tool Overview; +} diff --git a/client/src/views/tools/ToolDialog.web.tsx b/client/src/views/tools/ToolDialog.web.tsx new file mode 100644 index 0000000..553df48 --- /dev/null +++ b/client/src/views/tools/ToolDialog.web.tsx @@ -0,0 +1,50 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from "@/components/ui/Dialog"; +import { ToolsOverview } from "./ToolsOverview"; +import { Text } from "@/components/ui/Text"; +import { Agent } from "@/types"; +import { ToggleToolsSwitch } from "@/views/agent/helpers/ToggleTools"; +import { View } from "react-native"; + +export function ToolDialog({ + agent, + children, + className, + open, + onClose, +}: { + agent: Agent; + children?: React.ReactNode; + className?: string; + open?: boolean; + onClose?: () => void; +}) { + return ( + + {children && ( + + {children} + + )} + + + + + Tools + + + + + + + + + + + ); +} diff --git a/client/src/views/tools/ToolList.tsx b/client/src/views/tools/ToolList.tsx new file mode 100644 index 0000000..a215aaa --- /dev/null +++ b/client/src/views/tools/ToolList.tsx @@ -0,0 +1,75 @@ +import { ScrollView, Pressable, View } from "react-native"; + +import { useToolsQuery } from "@/hooks/fetchers/Agent/useAgentQuery"; +import { Agent, Tool } from "@/types"; +import { Text } from "@/components/ui/Text"; +import { cn } from "@/lib/utils"; + +export function ToolList({ + agent, + activeTool, + setTool, +}: { + agent: Agent; + activeTool: Tool | null; + setTool: (tool: Tool) => void; +}) { + const { data, isPending, isError } = useToolsQuery(); + + if (isError) return Error loading tools; + if (isPending) return Loading...; + return ( + + Available Tools + + {data.map((t) => ( + setTool(t)} + active={activeTool === t} + agent={agent} + /> + ))} + + + ); +} + +export function ToolListItem({ + tool, + active, + onPress, + agent, +}: { + tool: Tool; + active: boolean; + onPress: () => void; + agent: Agent; +}) { + return ( + + + {tool} + + + + ); +} diff --git a/client/src/views/tools/ToolsOverview.tsx b/client/src/views/tools/ToolsOverview.tsx new file mode 100644 index 0000000..dd33a4a --- /dev/null +++ b/client/src/views/tools/ToolsOverview.tsx @@ -0,0 +1,22 @@ +import { useState } from "react"; +import { View } from "react-native"; + +import { Agent, Tool } from "@/types"; +import { ToolCard } from "./ToolCard"; +import { ToolList } from "./ToolList"; + +export function ToolsOverview({ agent }: { agent: Agent }) { + const [activeTool, setTool] = useState(null); + return ( + <> + + + + + {activeTool && } + + + + + ); +} diff --git a/server/src/modules/Agent/AgentController.ts b/server/src/modules/Agent/AgentController.ts index 303333d..0c71c9a 100644 --- a/server/src/modules/Agent/AgentController.ts +++ b/server/src/modules/Agent/AgentController.ts @@ -5,56 +5,57 @@ import { Agent } from "./AgentModel"; import { Tools } from "../LLMNexus/Tools"; 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), - owner: user, - }); - reply.send(savedAgent); - } - - static async getAgents(request: FastifyRequest, reply: FastifyReply) { - reply.send(request.user.agents); - } - - static async getAgent(request: FastifyRequest, reply: FastifyReply) { - reply.send({ - ...request.agent, - threads: request.agent.threads.map((thread) => thread.id), - owner: request.agent.owner.id, - }); - } - - static async updateAgent(request: FastifyRequest, reply: FastifyReply) { - const agentUpdate = request.body as AgentUpdateSchema; - const agent = request.agent; - - switch (agentUpdate.type) { - case "tools": - //agent.tools = agentUpdate.value; - break; - case "toolsEnabled": - agent[agentUpdate.type] = agentUpdate.value; - break; - case "model": - agent[agentUpdate.type] = agentUpdate.value; - break; - case "name": - case "systemMessage": - agent[agentUpdate.type] = agentUpdate.value; - break; - } - const updatedAgent = await agent.save(); - reply.send(updatedAgent); - } - - static async deleteAgent(request: FastifyRequest, reply: FastifyReply) { - reply.send("TODO"); - } - - static async getTools(request: FastifyRequest, reply: FastifyReply) { - reply.send(Tools.map((tool) => tool.name)); - } + 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), + owner: user, + }); + reply.send(savedAgent); + } + + static async getAgents(request: FastifyRequest, reply: FastifyReply) { + reply.send(request.user.agents); + } + + static async getAgent(request: FastifyRequest, reply: FastifyReply) { + reply.send({ + ...request.agent, + threads: request.agent.threads.map((thread) => thread.id), + owner: request.agent.owner.id, + }); + } + + static async updateAgent(request: FastifyRequest, reply: FastifyReply) { + const agentUpdate = request.body as AgentUpdateSchema; + const agent = request.agent; + + switch (agentUpdate.type) { + case "tools": + agent.tools = agentUpdate.value; + break; + case "toolsEnabled": + agent[agentUpdate.type] = agentUpdate.value; + break; + case "model": + agent[agentUpdate.type] = agentUpdate.value; + break; + case "name": + case "systemMessage": + agent[agentUpdate.type] = agentUpdate.value; + break; + } + const updatedAgent = await agent.save(); + reply.send(updatedAgent); + } + + static async deleteAgent(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/AgentModel.ts b/server/src/modules/Agent/AgentModel.ts index a5fb104..b56b7fc 100644 --- a/server/src/modules/Agent/AgentModel.ts +++ b/server/src/modules/Agent/AgentModel.ts @@ -1,13 +1,13 @@ import { - BaseEntity, - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - type Relation, - OneToMany, - CreateDateColumn, - VersionColumn, + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + type Relation, + OneToMany, + CreateDateColumn, + VersionColumn, } from "typeorm"; import { User } from "../User/UserModel"; import { Thread } from "../Thread/ThreadModel"; @@ -15,54 +15,54 @@ import { ToolsMap, type ToolName } from "../LLMNexus/Tools"; import { modelMap } from "../Models/data"; const defaultAgent: Partial = { - name: "myChat Agent", - tools: [ToolsMap.Browser.name, ToolsMap.Fetcher.name], - toolsEnabled: true, - systemMessage: "You are a personal assistant.", - model: modelMap["gpt-4-turbo"], + name: "myChat Agent", + tools: [], + toolsEnabled: true, + systemMessage: "You are a personal assistant.", + model: modelMap["gpt-4-turbo"], }; @Entity("Agent") export class Agent extends BaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; + @PrimaryGeneratedColumn("uuid") + id: string; - @CreateDateColumn() - createdAt: Date; + @CreateDateColumn() + createdAt: Date; - /** Agent name for frontend only */ - @Column({ type: "text", default: defaultAgent.name }) - name: string = "myChat Agent"; + /** Agent name for frontend only */ + @Column({ type: "text", default: defaultAgent.name }) + name: string = "myChat Agent"; - /** Tools available to the Agent */ - @Column({ type: "text", array: true, default: defaultAgent.tools }) - tools: ToolName[] = []; + /** Tools available to the Agent */ + @Column({ type: "text", array: true, default: defaultAgent.tools }) + tools: ToolName[] = []; - /** Model API for the Agent */ - @Column({ type: "jsonb", default: defaultAgent.model }) - model: ModelApi; + /** Model API for the Agent */ + @Column({ type: "jsonb", default: defaultAgent.model }) + model: ModelApi; - /** Whether tools are enabled */ - @Column({ type: "boolean", default: defaultAgent.toolsEnabled }) - toolsEnabled: boolean = false; + /** Whether tools are enabled */ + @Column({ type: "boolean", default: defaultAgent.toolsEnabled }) + toolsEnabled: boolean = false; - /** System message for the Agent */ - @Column({ type: "text", default: defaultAgent.systemMessage }) - systemMessage: string; + /** System message for the Agent */ + @Column({ type: "text", default: defaultAgent.systemMessage }) + systemMessage: string; - /** Threads that the Agent is a part of */ - @OneToMany(() => Thread, (thread) => thread.agent) - threads: Relation; + /** Threads that the Agent is a part of */ + @OneToMany(() => Thread, (thread) => thread.agent) + threads: Relation; - /** User that owns the Agent */ - @ManyToOne(() => User, (user) => user.agents) - owner: Relation; + /** User that owns the Agent */ + @ManyToOne(() => User, (user) => user.agents) + owner: Relation; - @VersionColumn() - version: number; + @VersionColumn() + version: number; - /** Get List of Agent Tools */ - getTools() { - return this.tools.map((tool) => ToolsMap[tool]); - } + /** Get List of Agent Tools */ + getTools() { + return this.tools.map((tool) => ToolsMap[tool]); + } } diff --git a/server/src/modules/Agent/AgentSchema.ts b/server/src/modules/Agent/AgentSchema.ts index 7fdf326..5503af1 100644 --- a/server/src/modules/Agent/AgentSchema.ts +++ b/server/src/modules/Agent/AgentSchema.ts @@ -4,21 +4,21 @@ import { constructZodLiteralUnionType } from "@/lib/zod"; import { ModelInfoSchema } from "../Models/ModelsSchema"; export const AgentToolsSchema = constructZodLiteralUnionType( - ToolNames.map((t) => z.literal(t)) + ToolNames.map((t) => z.literal(t)) ); export type AgentToolsSchema = z.infer; export const AgentObjectSchema = z.object({ - id: z.string(), - createdAt: z.date(), - name: z.string(), - model: ModelInfoSchema, - tools: z.array(AgentToolsSchema), - toolsEnabled: z.boolean(), - systemMessage: z.string(), - threads: z.optional(z.array(z.string())), - owner: z.optional(z.string()), - version: z.optional(z.number()), + id: z.string(), + createdAt: z.date(), + name: z.string(), + model: ModelInfoSchema, + tools: z.array(AgentToolsSchema), + toolsEnabled: z.boolean(), + systemMessage: z.string(), + threads: z.optional(z.array(z.string())), + owner: z.optional(z.string()), + version: z.optional(z.number()), }); export type AgentObjectSchema = z.infer; @@ -26,20 +26,20 @@ export const AgentObjectListSchema = z.array(AgentObjectSchema); export type AgentObjectListSchema = z.infer; export const AgentCreateSchema = AgentObjectSchema.omit({ - id: true, - createdAt: true, - threads: true, - owner: true, - version: true, + id: true, + createdAt: true, + threads: true, + owner: true, + version: true, }); export type AgentCreateSchema = z.infer; // Discriminated union schema options export const AgentUpdateSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal("name"), value: z.string() }), - z.object({ type: z.literal("model"), value: ModelInfoSchema }), - z.object({ type: z.literal("tools"), value: z.array(z.string()) }), - z.object({ type: z.literal("toolsEnabled"), value: z.boolean() }), - z.object({ type: z.literal("systemMessage"), value: z.string() }), + z.object({ type: z.literal("name"), value: z.string() }), + z.object({ type: z.literal("model"), value: ModelInfoSchema }), + z.object({ type: z.literal("tools"), value: z.array(AgentToolsSchema) }), + z.object({ type: z.literal("toolsEnabled"), value: z.boolean() }), + z.object({ type: z.literal("systemMessage"), value: z.string() }), ]); export type AgentUpdateSchema = z.infer;