From 2fa212061d1284ad9e797f679f2d23a40b026a08 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Fri, 17 Jan 2025 16:45:50 -0800 Subject: [PATCH 01/43] Add a ride hand side bar for chat controls --- src/interface/web/app/agents/page.tsx | 2 +- src/interface/web/app/chat/chat.module.css | 4 +- src/interface/web/app/chat/page.tsx | 87 +++++---- src/interface/web/app/common/auth.ts | 10 ++ .../web/app/common/modelSelector.tsx | 168 ++++++++++++++++++ src/interface/web/app/common/utils.ts | 20 +++ .../allConversations/allConversations.tsx | 9 +- .../app/components/appSidebar/appSidebar.tsx | 2 + .../components/chatHistory/chatHistory.tsx | 20 ++- .../components/chatSidebar/chatSidebar.tsx | 148 +++++++++++++++ .../web/app/share/chat/sharedChat.module.css | 3 +- .../web/components/ui/hover-card.tsx | 29 +++ src/interface/web/package.json | 1 + src/interface/web/tailwind.config.ts | 11 ++ src/interface/web/yarn.lock | 2 +- 15 files changed, 465 insertions(+), 51 deletions(-) create mode 100644 src/interface/web/app/common/modelSelector.tsx create mode 100644 src/interface/web/app/components/chatSidebar/chatSidebar.tsx create mode 100644 src/interface/web/components/ui/hover-card.tsx diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index 8ea080415..b03d248c7 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -171,7 +171,7 @@ function CreateAgentCard(props: CreateAgentCardProps) { ); } -interface AgentConfigurationOptions { +export interface AgentConfigurationOptions { input_tools: { [key: string]: string }; output_modes: { [key: string]: string }; } diff --git a/src/interface/web/app/chat/chat.module.css b/src/interface/web/app/chat/chat.module.css index c98684296..58a2a9bff 100644 --- a/src/interface/web/app/chat/chat.module.css +++ b/src/interface/web/app/chat/chat.module.css @@ -39,7 +39,7 @@ div.inputBox:focus { div.chatBodyFull { display: grid; grid-template-columns: 1fr; - height: 100%; + height: auto; } button.inputBox { @@ -83,7 +83,7 @@ div.titleBar { div.chatBoxBody { display: grid; height: 100%; - width: 95%; + width: 100%; margin: auto; } diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 824a7e4c9..7c96e2add 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -30,6 +30,9 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s import { AppSidebar } from "../components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; +import { Button } from "@/components/ui/button"; +import { Joystick } from "@phosphor-icons/react"; +import { ChatSidebar } from "../components/chatSidebar/chatSidebar"; interface ChatBodyDataProps { chatOptionsData: ChatOptions | null; @@ -43,6 +46,8 @@ interface ChatBodyDataProps { isLoggedIn: boolean; setImages: (images: string[]) => void; setTriggeredAbort: (triggeredAbort: boolean) => void; + isChatSideBarOpen: boolean; + onChatSideBarOpenChange: (open: boolean) => void; } function ChatBodyData(props: ChatBodyDataProps) { @@ -138,37 +143,44 @@ function ChatBodyData(props: ChatBodyDataProps) { } return ( - <> -
- -
-
- setMessage(message)} - sendImage={(image) => setImages((prevImages) => [...prevImages, image])} - sendDisabled={processingMessage} - chatOptionsData={props.chatOptionsData} - conversationId={conversationId} - isMobileWidth={props.isMobileWidth} - setUploadedFiles={props.setUploadedFiles} - ref={chatInputRef} - isResearchModeEnabled={isInResearchMode} - setTriggeredAbort={props.setTriggeredAbort} - /> +
+
+
+ +
+
+ setMessage(message)} + sendImage={(image) => setImages((prevImages) => [...prevImages, image])} + sendDisabled={processingMessage} + chatOptionsData={props.chatOptionsData} + conversationId={conversationId} + isMobileWidth={props.isMobileWidth} + setUploadedFiles={props.setUploadedFiles} + ref={chatInputRef} + isResearchModeEnabled={isInResearchMode} + setTriggeredAbort={props.setTriggeredAbort} + /> +
- + +
); } @@ -199,6 +211,7 @@ export default function Chat() { isLoading: authenticationLoading, } = useAuthenticatedData(); const isMobileWidth = useIsMobileWidth(); + const [isChatSideBarOpen, setIsChatSideBarOpen] = useState(false); useEffect(() => { fetch("/api/chat/options") @@ -432,6 +445,16 @@ export default function Chat() { )}
)} +
+ +
@@ -452,12 +475,14 @@ export default function Chat() { onConversationIdChange={handleConversationIdChange} setImages={setImages} setTriggeredAbort={setTriggeredAbort} + isChatSideBarOpen={isChatSideBarOpen} + onChatSideBarOpenChange={setIsChatSideBarOpen} /> </Suspense> </div> </div> </div> </SidebarInset> - </SidebarProvider> + </SidebarProvider > ); } diff --git a/src/interface/web/app/common/auth.ts b/src/interface/web/app/common/auth.ts index 738d273f4..e00caa25f 100644 --- a/src/interface/web/app/common/auth.ts +++ b/src/interface/web/app/common/auth.ts @@ -33,6 +33,8 @@ export function useAuthenticatedData() { export interface ModelOptions { id: number; name: string; + description: string; + strengths: string; } export interface SyncedContent { computer: boolean; @@ -99,6 +101,14 @@ export function useUserConfig(detailed: boolean = false) { return { userConfig, isLoadingUserConfig }; } +export function useChatModelOptions() { + const { data, error, isLoading } = useSWR<ModelOptions[]>(`/api/model/chat/options`, fetcher, { + revalidateOnFocus: false, + }); + + return { models: data, error, isLoading }; +} + export function isUserSubscribed(userConfig: UserConfig | null): boolean { return ( (userConfig?.subscription_state && diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx new file mode 100644 index 000000000..ae0e6d4f2 --- /dev/null +++ b/src/interface/web/app/common/modelSelector.tsx @@ -0,0 +1,168 @@ +"use client" + +import * as React from "react" +import { useState, useEffect } from "react"; +import { PopoverProps } from "@radix-ui/react-popover" + +import { Check, CaretUpDown } from "@phosphor-icons/react"; + +import { cn } from "@/lib/utils" +import { useMutationObserver } from "@/app/common/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +import { ModelOptions, useChatModelOptions } from "./auth"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function ModelSelector({ ...props }: PopoverProps) { + const [open, setOpen] = React.useState(false) + const { models, isLoading, error } = useChatModelOptions(); + const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined); + const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined); + + useEffect(() => { + if (models && models.length > 0) { + setSelectedModel(models[0]) + } + + if (models && models.length > 0 && !selectedModel) { + setSelectedModel(models[0]) + } + + }, [models]); + + if (isLoading) { + return ( + <Skeleton className="w-full h-10" /> + ); + } + + if (error) { + return ( + <div className="text-sm text-error">{error.message}</div> + ); + } + + return ( + <div className="grid gap-2 w-[250px]"> + <Popover open={open} onOpenChange={setOpen} {...props}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + aria-label="Select a model" + className="w-full justify-between text-left" + > + <p className="truncate"> + {selectedModel ? selectedModel.name.substring(0,20) : "Select a model..."} + </p> + <CaretUpDown className="opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent align="end" className="w-[250px] p-0"> + <HoverCard> + <HoverCardContent + side="left" + align="start" + forceMount + className="min-h-[280px]" + > + <div className="grid gap-2"> + <h4 className="font-medium leading-none">{peekedModel?.name}</h4> + <div className="text-sm text-muted-foreground"> + {peekedModel?.description} + </div> + {peekedModel?.strengths ? ( + <div className="mt-4 grid gap-2"> + <h5 className="text-sm font-medium leading-none"> + Strengths + </h5> + <ul className="text-sm text-muted-foreground"> + {peekedModel.strengths} + </ul> + </div> + ) : null} + </div> + </HoverCardContent> + <div> + <HoverCardTrigger /> + <Command loop> + <CommandList className="h-[var(--cmdk-list-height)]"> + <CommandInput placeholder="Search Models..." /> + <CommandEmpty>No Models found.</CommandEmpty> + <CommandGroup key={"models"} heading={"Models"}> + {models && models.length > 0 && models + .map((model) => ( + <ModelItem + key={model.id} + model={model} + isSelected={selectedModel?.id === model.id} + onPeek={(model) => setPeekedModel(model)} + onSelect={() => { + setSelectedModel(model) + setOpen(false) + }} + /> + ))} + </CommandGroup> + </CommandList> + </Command> + </div> + </HoverCard> + </PopoverContent> + </Popover> + </div> + ) +} + +interface ModelItemProps { + model: ModelOptions, + isSelected: boolean, + onSelect: () => void, + onPeek: (model: ModelOptions) => void +} + +function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) { + const ref = React.useRef<HTMLDivElement>(null) + + useMutationObserver(ref, (mutations) => { + mutations.forEach((mutation) => { + if ( + mutation.type === "attributes" && + mutation.attributeName === "aria-selected" && + ref.current?.getAttribute("aria-selected") === "true" + ) { + onPeek(model) + } + }) + }) + + return ( + <CommandItem + key={model.id} + onSelect={onSelect} + ref={ref} + className="data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground" + > + {model.name} + <Check + className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")} + /> + </CommandItem> + ) +} diff --git a/src/interface/web/app/common/utils.ts b/src/interface/web/app/common/utils.ts index 8bf6db84e..fe5043a24 100644 --- a/src/interface/web/app/common/utils.ts +++ b/src/interface/web/app/common/utils.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import useSWR from "swr"; +import * as React from "react" export interface LocationData { city?: string; @@ -69,6 +70,25 @@ export function useIsMobileWidth() { return isMobileWidth; } +export const useMutationObserver = ( + ref: React.MutableRefObject<HTMLElement | null>, + callback: MutationCallback, + options = { + attributes: true, + characterData: true, + childList: true, + subtree: true, + } +) => { + React.useEffect(() => { + if (ref.current) { + const observer = new MutationObserver(callback) + observer.observe(ref.current, options) + return () => observer.disconnect() + } + }, [ref, callback, options]) +} + export const convertBytesToText = (fileSize: number) => { if (fileSize < 1024) { return `${fileSize} B`; diff --git a/src/interface/web/app/components/allConversations/allConversations.tsx b/src/interface/web/app/components/allConversations/allConversations.tsx index 53ad20bf5..94d05eba1 100644 --- a/src/interface/web/app/components/allConversations/allConversations.tsx +++ b/src/interface/web/app/components/allConversations/allConversations.tsx @@ -184,7 +184,7 @@ interface FilesMenuProps { isMobileWidth: boolean; } -function FilesMenu(props: FilesMenuProps) { +export function FilesMenu(props: FilesMenuProps) { // Use SWR to fetch files const { data: files, error } = useSWR<string[]>("/api/content/computer", fetcher); const { data: selectedFiles, error: selectedFilesError } = useSWR( @@ -981,13 +981,6 @@ export default function AllConversations(props: SidePanelProps) { sideBarOpen={props.sideBarOpen} /> </div> - {props.sideBarOpen && ( - <FilesMenu - conversationId={props.conversationId} - uploadedFiles={props.uploadedFiles} - isMobileWidth={props.isMobileWidth} - /> - )} </> )} </div> diff --git a/src/interface/web/app/components/appSidebar/appSidebar.tsx b/src/interface/web/app/components/appSidebar/appSidebar.tsx index d80314b9b..97b7664af 100644 --- a/src/interface/web/app/components/appSidebar/appSidebar.tsx +++ b/src/interface/web/app/components/appSidebar/appSidebar.tsx @@ -8,6 +8,7 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarRail, } from "@/components/ui/sidebar"; import { KhojAgentLogo, @@ -150,6 +151,7 @@ export function AppSidebar(props: AppSidebarProps) { <SidebarFooter> <FooterMenu sideBarIsOpen={open} /> </SidebarFooter> + <SidebarRail /> </Sidebar> ); } diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 829211bec..f0b46e1ef 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -314,7 +314,15 @@ export default function ChatHistory(props: ChatHistoryProps) { } return ( - <ScrollArea className={`h-[73vh] relative`} ref={scrollAreaRef}> + <ScrollArea + className={` + h-[calc(100svh-theme(spacing.44))] + sm:h-[calc(100svh-theme(spacing.44))] + md:h-[calc(100svh-theme(spacing.44))] + lg:h-[calc(100svh-theme(spacing.72))] + `} + ref={scrollAreaRef}> + <div> <div className={`${styles.chatHistory} ${props.customClassName}`}> <div ref={sentinelRef} style={{ height: "1px" }}> @@ -343,12 +351,12 @@ export default function ChatHistory(props: ChatHistoryProps) { index === data.chat.length - 2 ? latestUserMessageRef : // attach ref to the newest fetched message to handle scroll on fetch - // note: stabilize index selection against last page having less messages than fetchMessageCount - index === + // note: stabilize index selection against last page having less messages than fetchMessageCount + index === data.chat.length - - (currentPage - 1) * fetchMessageCount - ? latestFetchedMessageRef - : null + (currentPage - 1) * fetchMessageCount + ? latestFetchedMessageRef + : null } isMobileWidth={isMobileWidth} chatMessage={chatMessage} diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx new file mode 100644 index 000000000..9d3588e02 --- /dev/null +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -0,0 +1,148 @@ +"use client" + +import * as React from "react" + +import { Bell } from "@phosphor-icons/react"; + +import { Button } from "@/components/ui/button"; + +import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Textarea } from "@/components/ui/textarea"; +import { ModelSelector } from "@/app/common/modelSelector"; +import { FilesMenu } from "../allConversations/allConversations"; +import { AgentConfigurationOptions } from "@/app/agents/page"; +import useSWR from "swr"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; + +interface ChatSideBarProps { + conversationId: string; + isOpen: boolean; + isMobileWidth?: boolean; + onOpenChange: (open: boolean) => void; +} + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export function ChatSidebar({ ...props }: ChatSideBarProps) { + const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = + useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); + + if (props.isMobileWidth) { + return ( + <Sheet + open={props.isOpen} + onOpenChange={props.onOpenChange}> + <SheetContent + className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" + > + <ChatSidebarInternal {...props} /> + </SheetContent> + </Sheet> + ); + } + + return ( + <ChatSidebarInternal {...props} /> + ); +} + + +function ChatSidebarInternal({ ...props }: ChatSideBarProps) { + const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = + useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); + + return ( + <Sidebar + collapsible="none" + className={`ml-auto rounded-lg p-2 transition-all transform duration-300 ease-in-out + ${props.isOpen + ? "translate-x-0 opacity-100 w-[300px]" + : "translate-x-full opacity-0 w-0"} + `} + variant="floating"> + <SidebarContent> + <SidebarHeader> + Chat Options + </SidebarHeader> + <SidebarGroup key={"test"} className="border-b last:border-none"> + <SidebarGroupContent className="gap-0"> + <SidebarMenu className="p-0 m-0"> + <SidebarMenuItem key={"item4"} className="list-none"> + <span>Custom Instructions</span> + <Textarea className="w-full h-32" /> + </SidebarMenuItem> + <SidebarMenuItem key={"item"} className="list-none"> + <SidebarMenuButton> + <Bell /> <span>Model</span> + </SidebarMenuButton> + <ModelSelector /> + </SidebarMenuItem> + <SidebarMenuItem key={"item1"} className="list-none"> + <SidebarMenuButton> + <Bell /> <span>Input Tools</span> + </SidebarMenuButton> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline">Input Tools</Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-56"> + <DropdownMenuLabel>Input Tool Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + { + Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { + return ( + <DropdownMenuCheckboxItem + checked={true} + onCheckedChange={() => { }} + > + {key} + </DropdownMenuCheckboxItem> + ); + } + ) + } + </DropdownMenuContent> + </DropdownMenu> + </SidebarMenuItem> + <SidebarMenuItem key={"item2"} className="list-none"> + <SidebarMenuButton> + <Bell /> <span>Output Tools</span> + </SidebarMenuButton> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline">Output Tools</Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-56"> + <DropdownMenuLabel>Output Tool Options</DropdownMenuLabel> + <DropdownMenuSeparator /> + { + Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { + return ( + <DropdownMenuCheckboxItem + checked={true} + onCheckedChange={() => { }} + > + {key} + </DropdownMenuCheckboxItem> + ); + } + ) + } + </DropdownMenuContent> + </DropdownMenu> + </SidebarMenuItem> + <SidebarMenuItem key={"item3"} className="list-none"> + <FilesMenu + conversationId={props.conversationId} + uploadedFiles={[]} + isMobileWidth={props.isMobileWidth ?? false} + /> + </SidebarMenuItem> + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + </SidebarContent> + </Sidebar> + ) +} diff --git a/src/interface/web/app/share/chat/sharedChat.module.css b/src/interface/web/app/share/chat/sharedChat.module.css index 286e5ba45..844834f3f 100644 --- a/src/interface/web/app/share/chat/sharedChat.module.css +++ b/src/interface/web/app/share/chat/sharedChat.module.css @@ -35,7 +35,6 @@ div.inputBox:focus { div.chatBodyFull { display: grid; grid-template-columns: 1fr; - height: 100%; } button.inputBox { @@ -78,7 +77,7 @@ div.titleBar { div.chatBoxBody { display: grid; height: 100%; - width: 95%; + width: 100%; margin: auto; } diff --git a/src/interface/web/components/ui/hover-card.tsx b/src/interface/web/components/ui/hover-card.tsx new file mode 100644 index 000000000..e54d91cf8 --- /dev/null +++ b/src/interface/web/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + <HoverCardPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index 5aa04ff49..f899a0c27 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-menubar": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.0", diff --git a/src/interface/web/tailwind.config.ts b/src/interface/web/tailwind.config.ts index 07bd07543..15391ae0f 100644 --- a/src/interface/web/tailwind.config.ts +++ b/src/interface/web/tailwind.config.ts @@ -137,12 +137,23 @@ const config = { "0%": { opacity: "0", transform: "translateY(20px)" }, "100%": { opacity: "1", transform: "translateY(0)" }, }, + fadeInRight: { + "0%": { opacity: "0", transform: "translateX(20px)" }, + "100%": { opacity: "1", transform: "translateX(0)" }, + }, + fadeInLeft: { + "0%": { opacity: "0", transform: "translateX(-20px)" }, + "100%": { opacity: "1", transform: "translateX(0)" }, + }, + }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "caret-blink": "caret-blink 1.25s ease-out infinite", "fade-in-up": "fadeInUp 0.3s ease-out", + "fade-in-right": "fadeInRight 0.3s ease-out", + "fade-in-left": "fadeInLeft 0.3s ease-out", }, }, }, diff --git a/src/interface/web/yarn.lock b/src/interface/web/yarn.lock index 14aec059c..22cac901b 100644 --- a/src/interface/web/yarn.lock +++ b/src/interface/web/yarn.lock @@ -716,7 +716,7 @@ "@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-hover-card@^1.1.2": +"@radix-ui/react-hover-card@^1.1.2", "@radix-ui/react-hover-card@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.4.tgz#f334fdc1814e14a81ecb4e88a72b92326c1f89a6" integrity sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ== From 5aadba20a6a454558a5464646abadfe1c42654cb Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Fri, 17 Jan 2025 16:46:37 -0800 Subject: [PATCH 02/43] Add backend support for hidden agents (not yet enabled) --- src/khoj/database/adapters/__init__.py | 7 ++++- ...s_hidden_chatmodel_description_and_more.py | 27 +++++++++++++++++++ src/khoj/database/models/__init__.py | 4 +++ src/khoj/routers/api_agents.py | 3 +++ src/khoj/routers/api_chat.py | 3 ++- src/khoj/routers/api_model.py | 21 ++++++++++----- src/khoj/routers/helpers.py | 9 ++++++- 7 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 src/khoj/database/migrations/0081_agent_is_hidden_chatmodel_description_and_more.py diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 8e595dae5..31ccd2f2e 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -714,9 +714,12 @@ def get_all_accessible_agents(user: KhojUser = None): public_query = Q(privacy_level=Agent.PrivacyLevel.PUBLIC) # TODO Update this to allow any public agent that's officially approved once that experience is launched public_query &= Q(managed_by_admin=True) + + user_query = Q(creator=user) + user_query &= Q(is_hidden=False) if user: return ( - Agent.objects.filter(public_query | Q(creator=user)) + Agent.objects.filter(public_query | user_query) .distinct() .order_by("created_at") .prefetch_related("creator", "chat_model", "fileobject_set") @@ -808,6 +811,7 @@ async def aupdate_agent( input_tools: List[str], output_modes: List[str], slug: Optional[str] = None, + is_hidden: Optional[bool] = False, ): chat_model_option = await ChatModel.objects.filter(name=chat_model).afirst() @@ -823,6 +827,7 @@ async def aupdate_agent( "chat_model": chat_model_option, "input_tools": input_tools, "output_modes": output_modes, + "is_hidden": is_hidden, } ) diff --git a/src/khoj/database/migrations/0081_agent_is_hidden_chatmodel_description_and_more.py b/src/khoj/database/migrations/0081_agent_is_hidden_chatmodel_description_and_more.py new file mode 100644 index 000000000..adbbe7fa4 --- /dev/null +++ b/src/khoj/database/migrations/0081_agent_is_hidden_chatmodel_description_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.10 on 2025-01-16 23:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0080_speechtotextmodeloptions_ai_model_api"), + ] + + operations = [ + migrations.AddField( + model_name="agent", + name="is_hidden", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="chatmodel", + name="description", + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name="chatmodel", + name="strengths", + field=models.TextField(blank=True, default=None, null=True), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index f46f107b5..97b245103 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -209,6 +209,8 @@ class ModelType(models.TextChoices): model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE) vision_enabled = models.BooleanField(default=False) ai_model_api = models.ForeignKey(AiModelApi, on_delete=models.CASCADE, default=None, null=True, blank=True) + description = models.TextField(default=None, null=True, blank=True) + strengths = models.TextField(default=None, null=True, blank=True) def __str__(self): return self.name @@ -286,6 +288,7 @@ class OutputModeOptions(models.TextChoices): TEXT = "text" IMAGE = "image" AUTOMATION = "automation" + DIAGRAM = "diagram" creator = models.ForeignKey( KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True @@ -304,6 +307,7 @@ class OutputModeOptions(models.TextChoices): style_color = models.CharField(max_length=200, choices=StyleColorTypes.choices, default=StyleColorTypes.BLUE) style_icon = models.CharField(max_length=200, choices=StyleIconTypes.choices, default=StyleIconTypes.LIGHTBULB) privacy_level = models.CharField(max_length=30, choices=PrivacyLevel.choices, default=PrivacyLevel.PRIVATE) + is_hidden = models.BooleanField(default=False) def save(self, *args, **kwargs): is_new = self._state.adding diff --git a/src/khoj/routers/api_agents.py b/src/khoj/routers/api_agents.py index e14a666f8..87693f56d 100644 --- a/src/khoj/routers/api_agents.py +++ b/src/khoj/routers/api_agents.py @@ -38,6 +38,7 @@ class ModifyAgentBody(BaseModel): input_tools: Optional[List[str]] = [] output_modes: Optional[List[str]] = [] slug: Optional[str] = None + is_hidden: Optional[bool] = False @api_agents.get("", response_class=Response) @@ -214,6 +215,7 @@ async def create_agent( body.input_tools, body.output_modes, body.slug, + body.is_hidden, ) agents_packet = { @@ -229,6 +231,7 @@ async def create_agent( "files": body.files, "input_tools": agent.input_tools, "output_modes": agent.output_modes, + "is_hidden": agent.is_hidden, } return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index d3b933ee3..8d9bd7ccb 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -474,6 +474,7 @@ async def create_chat_session( request: Request, common: CommonQueryParams, agent_slug: Optional[str] = None, + # Add parameters here to create a custom hidden agent on the fly ): user = request.user.object @@ -865,7 +866,7 @@ def collect_telemetry(): # and not triggered via slash command and not used_slash_summarize # but we can't actually summarize - and len(file_filters) != 1 + and len(file_filters) == 0 ): conversation_commands.remove(ConversationCommand.Summarize) elif ConversationCommand.Summarize in conversation_commands: diff --git a/src/khoj/routers/api_model.py b/src/khoj/routers/api_model.py index 88cb72ecd..26404c3f3 100644 --- a/src/khoj/routers/api_model.py +++ b/src/khoj/routers/api_model.py @@ -20,13 +20,20 @@ def get_chat_model_options( request: Request, client: Optional[str] = None, ): - conversation_options = ConversationAdapters.get_conversation_processor_options().all() - - all_conversation_options = list() - for conversation_option in conversation_options: - all_conversation_options.append({"chat_model": conversation_option.name, "id": conversation_option.id}) - - return Response(content=json.dumps(all_conversation_options), media_type="application/json", status_code=200) + chat_models = ConversationAdapters.get_conversation_processor_options().all() + + chat_model_options = list() + for chat_model in chat_models: + chat_model_options.append( + { + "name": chat_model.name, + "id": chat_model.id, + "strengths": chat_model.strengths, + "description": chat_model.description, + } + ) + + return Response(content=json.dumps(chat_model_options), media_type="application/json", status_code=200) @api_model.get("/chat") diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 4d545bc85..9cfc492a5 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -2231,7 +2231,14 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False) chat_models = ConversationAdapters.get_conversation_processor_options().all() chat_model_options = list() for chat_model in chat_models: - chat_model_options.append({"name": chat_model.name, "id": chat_model.id}) + chat_model_options.append( + { + "name": chat_model.name, + "id": chat_model.id, + "strengths": chat_model.strengths, + "description": chat_model.description, + } + ) selected_paint_model_config = ConversationAdapters.get_user_text_to_image_model_config(user) paint_model_options = ConversationAdapters.get_text_to_image_model_options().all() From 7481f78f22e05f05a6118e6bee3ef04c50e13dce Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Fri, 17 Jan 2025 17:18:47 -0800 Subject: [PATCH 03/43] Remove unused API request --- src/interface/web/app/components/chatSidebar/chatSidebar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 9d3588e02..8ebd32434 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -25,8 +25,6 @@ interface ChatSideBarProps { const fetcher = (url: string) => fetch(url).then((res) => res.json()); export function ChatSidebar({ ...props }: ChatSideBarProps) { - const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = - useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); if (props.isMobileWidth) { return ( From c80e0883ee77ec3e62f1ed939310adbd85362bc0 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Fri, 17 Jan 2025 17:36:46 -0800 Subject: [PATCH 04/43] Use python3 instead of python for install pip commands in GH action --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f5dfc6f7..c68dbf969 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,8 +72,8 @@ jobs: - name: ⬇️ Install pip run: | apt install -y python3-pip - python -m ensurepip --upgrade - python -m pip install --upgrade pip + python3 -m ensurepip --upgrade + python3 -m pip install --upgrade pip - name: ⬇️ Install Application env: From 00370c70ede8ae2531894a4f9e06648c4e37d25d Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:06:54 -0800 Subject: [PATCH 05/43] Consolidate the AgentData Type into the agentCard --- src/interface/web/app/agents/page.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index b03d248c7..0d8e4b722 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -25,6 +25,7 @@ import { AgentCard, EditAgentSchema, AgentModificationForm, + AgentData, } from "@/app/components/agentCard/agentCard"; import { useForm } from "react-hook-form"; @@ -35,21 +36,6 @@ import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; import { DialogTitle } from "@radix-ui/react-dialog"; -export interface AgentData { - slug: string; - name: string; - persona: string; - color: string; - icon: string; - privacy_level: string; - files?: string[]; - creator?: string; - managed_by_admin: boolean; - chat_model: string; - input_tools: string[]; - output_modes: string[]; -} - const agentsFetcher = () => window .fetch("/api/agents") @@ -321,6 +307,7 @@ export default function Agents() { chat_model: "", input_tools: [], output_modes: [], + is_hidden: false, }} userProfile={ authenticationLoading From 7998a258b6dbbca950259f28bdbc2ce4b20b7ff3 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:08:02 -0800 Subject: [PATCH 06/43] Add additional ui components for tooltip, checkbox --- src/interface/web/components/ui/checkbox.tsx | 30 ++++++++++++++ src/interface/web/components/ui/tooltip.tsx | 42 ++++++++++---------- src/interface/web/package.json | 1 + src/interface/web/yarn.lock | 4 +- 4 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 src/interface/web/components/ui/checkbox.tsx diff --git a/src/interface/web/components/ui/checkbox.tsx b/src/interface/web/components/ui/checkbox.tsx new file mode 100644 index 000000000..18b9a486f --- /dev/null +++ b/src/interface/web/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> +>(({ className, ...props }, ref) => ( + <CheckboxPrimitive.Root + ref={ref} + className={cn( + "peer h-4 w-4 shrink-0 rounded-sm border border-muted ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground", + className + )} + {...props} + > + <CheckboxPrimitive.Indicator + className={cn("flex items-center justify-center text-current")} + > + <Check className="h-4 w-4" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/interface/web/components/ui/tooltip.tsx b/src/interface/web/components/ui/tooltip.tsx index 7001de6a6..30fc44d90 100644 --- a/src/interface/web/components/ui/tooltip.tsx +++ b/src/interface/web/components/ui/tooltip.tsx @@ -1,30 +1,30 @@ -"use client"; +"use client" -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" -const TooltipProvider = TooltipPrimitive.Provider; +const TooltipProvider = TooltipPrimitive.Provider -const Tooltip = TooltipPrimitive.Root; +const Tooltip = TooltipPrimitive.Root -const TooltipTrigger = TooltipPrimitive.Trigger; +const TooltipTrigger = TooltipPrimitive.Trigger const TooltipContent = React.forwardRef< - React.ElementRef<typeof TooltipPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> + React.ElementRef<typeof TooltipPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> >(({ className, sideOffset = 4, ...props }, ref) => ( - <TooltipPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className, - )} - {...props} - /> -)); -TooltipContent.displayName = TooltipPrimitive.Content.displayName; + <TooltipPrimitive.Content + ref={ref} + sideOffset={sideOffset} + className={cn( + "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index f899a0c27..fad451444 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -24,6 +24,7 @@ "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.1", diff --git a/src/interface/web/yarn.lock b/src/interface/web/yarn.lock index 22cac901b..de43429c9 100644 --- a/src/interface/web/yarn.lock +++ b/src/interface/web/yarn.lock @@ -593,7 +593,7 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-checkbox@^1.1.2": +"@radix-ui/react-checkbox@^1.1.2", "@radix-ui/react-checkbox@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32" integrity sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw== @@ -977,7 +977,7 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.1" -"@radix-ui/react-switch@^1.1.1": +"@radix-ui/react-switch@^1.1.1", "@radix-ui/react-switch@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec" integrity sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g== From 7837628bb3447f7eb51e46700c6af6278150fc69 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:12:23 -0800 Subject: [PATCH 07/43] Update existing agentData imports --- src/interface/web/app/chat/page.tsx | 5 ++++- src/interface/web/app/page.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 7c96e2add..7507e24dc 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -24,7 +24,9 @@ import { ChatOptions, } from "../components/chatInputArea/chatInputArea"; import { useAuthenticatedData } from "../common/auth"; -import { AgentData } from "../agents/page"; +import { + AgentData, +} from "@/app/components/agentCard/agentCard"; import { ChatSessionActionMenu } from "../components/allConversations/allConversations"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "../components/appSidebar/appSidebar"; @@ -179,6 +181,7 @@ function ChatBodyData(props: ChatBodyDataProps) { conversationId={conversationId} isOpen={props.isChatSideBarOpen} onOpenChange={props.onChatSideBarOpenChange} + preexistingAgent={agentMetadata} isMobileWidth={props.isMobileWidth} /> </div> ); diff --git a/src/interface/web/app/page.tsx b/src/interface/web/app/page.tsx index 40e24a821..e2d185695 100644 --- a/src/interface/web/app/page.tsx +++ b/src/interface/web/app/page.tsx @@ -37,7 +37,7 @@ import { } from "@/app/common/auth"; import { convertColorToBorderClass } from "@/app/common/colorUtils"; import { getIconFromIconName } from "@/app/common/iconUtils"; -import { AgentData } from "@/app/agents/page"; +import { AgentData } from"@/app/components/agentCard/agentCard"; import { createNewConversation } from "./common/chatFunctions"; import { useDebounce, useIsMobileWidth } from "./common/utils"; import { useRouter, useSearchParams } from "next/navigation"; From f10b072634efde377420bcd1736706921779eebd Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:13:37 -0800 Subject: [PATCH 08/43] update looks & feel of chat side bar with model selector, checkboxes for tools, and actions (not yet implemented) --- .../web/app/common/modelSelector.tsx | 26 +- .../app/components/agentCard/agentCard.tsx | 3 + .../allConversations/allConversations.tsx | 4 +- .../components/chatSidebar/chatSidebar.tsx | 282 +++++++++++++----- 4 files changed, 233 insertions(+), 82 deletions(-) diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx index ae0e6d4f2..d5716c1da 100644 --- a/src/interface/web/app/common/modelSelector.tsx +++ b/src/interface/web/app/common/modelSelector.tsx @@ -28,22 +28,29 @@ import { ModelOptions, useChatModelOptions } from "./auth"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Skeleton } from "@/components/ui/skeleton"; -export function ModelSelector({ ...props }: PopoverProps) { +interface ModelSelectorProps extends PopoverProps { + onSelect: (model: ModelOptions) => void; + selectedModel?: string; + disabled?: boolean; +} + +export function ModelSelector({ ...props }: ModelSelectorProps) { const [open, setOpen] = React.useState(false) const { models, isLoading, error } = useChatModelOptions(); const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined); const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined); useEffect(() => { - if (models && models.length > 0) { - setSelectedModel(models[0]) - } + if (!models?.length) return; - if (models && models.length > 0 && !selectedModel) { - setSelectedModel(models[0]) + if (props.selectedModel) { + const model = models.find(model => model.name === props.selectedModel); + setSelectedModel(model || models[0]); + return; } - }, [models]); + setSelectedModel(models[0]); + }, [models, props.selectedModel]); if (isLoading) { return ( @@ -67,9 +74,10 @@ export function ModelSelector({ ...props }: PopoverProps) { aria-expanded={open} aria-label="Select a model" className="w-full justify-between text-left" + disabled={props.disabled ?? false} > <p className="truncate"> - {selectedModel ? selectedModel.name.substring(0,20) : "Select a model..."} + {selectedModel ? selectedModel.name.substring(0, 20) : "Select a model..."} </p> <CaretUpDown className="opacity-50" /> </Button> @@ -157,7 +165,7 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) { key={model.id} onSelect={onSelect} ref={ref} - className="data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground" + className="data-[selected=true]:bg-muted data-[selected=true]:text-secondary-foreground" > {model.name} <Check diff --git a/src/interface/web/app/components/agentCard/agentCard.tsx b/src/interface/web/app/components/agentCard/agentCard.tsx index 87ce30d26..ccb601e5e 100644 --- a/src/interface/web/app/components/agentCard/agentCard.tsx +++ b/src/interface/web/app/components/agentCard/agentCard.tsx @@ -103,10 +103,13 @@ export interface AgentData { privacy_level: string; files?: string[]; creator?: string; + is_creator?: boolean; managed_by_admin: boolean; chat_model: string; input_tools: string[]; output_modes: string[]; + is_hidden: boolean; + has_files?: boolean; } async function openChat(slug: string, userData: UserProfile | null) { diff --git a/src/interface/web/app/components/allConversations/allConversations.tsx b/src/interface/web/app/components/allConversations/allConversations.tsx index 94d05eba1..0bab81658 100644 --- a/src/interface/web/app/components/allConversations/allConversations.tsx +++ b/src/interface/web/app/components/allConversations/allConversations.tsx @@ -363,7 +363,7 @@ export function FilesMenu(props: FilesMenuProps) { <> <Popover open={isOpen} onOpenChange={setIsOpen}> <PopoverTrigger asChild> - <div className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl my-8"> + <div className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl"> <div className="flex items-center justify-between space-x-4"> <h4 className="text-sm font-semibold"> {usingConversationContext ? "Manage Context" : "Files"} @@ -424,7 +424,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) { <ScrollArea> <ScrollAreaScrollbar orientation="vertical" - className="h-full w-2.5 border-l border-l-transparent p-[1px]" + className="h-full w-2.5" /> <div className="p-0 m-0"> {props.subsetOrganizedData != null && diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 8ebd32434..8047a97b5 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -1,21 +1,28 @@ "use client" -import * as React from "react" - -import { Bell } from "@phosphor-icons/react"; +import { ArrowsDownUp, Bell, CaretCircleDown, Sparkle } from "@phosphor-icons/react"; import { Button } from "@/components/ui/button"; -import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; -import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Textarea } from "@/components/ui/textarea"; import { ModelSelector } from "@/app/common/modelSelector"; import { FilesMenu } from "../allConversations/allConversations"; import { AgentConfigurationOptions } from "@/app/agents/page"; import useSWR from "swr"; import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { AgentData } from "../agentCard/agentCard"; +import { useState } from "react"; +import { getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; +import { TooltipContent } from "@radix-ui/react-tooltip"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; interface ChatSideBarProps { + preexistingAgent?: AgentData | null; conversationId: string; isOpen: boolean; isMobileWidth?: boolean; @@ -47,13 +54,28 @@ export function ChatSidebar({ ...props }: ChatSideBarProps) { function ChatSidebarInternal({ ...props }: ChatSideBarProps) { + const isEditable = props.preexistingAgent?.name.toLowerCase() === "khoj" || props.preexistingAgent?.is_hidden === true; + const isDefaultAgent = props.preexistingAgent?.name.toLowerCase() === "khoj"; const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); + const [customPrompt, setCustomPrompt] = useState<string | undefined>(!isDefaultAgent && props.preexistingAgent ? props.preexistingAgent.persona : "always respond in spanish"); + const [selectedModel, setSelectedModel] = useState<string | undefined>(props.preexistingAgent?.chat_model); + const [inputTools, setInputTools] = useState<string[] | undefined>(props.preexistingAgent?.input_tools); + + + function isValueChecked(value: string, existingSelections: string[] | undefined): boolean { + if (existingSelections === undefined || existingSelections === null || existingSelections.length === 0) { + return true; + } + + return existingSelections.includes(value); + } + return ( <Sidebar collapsible="none" - className={`ml-auto rounded-lg p-2 transition-all transform duration-300 ease-in-out + className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out ${props.isOpen ? "translate-x-0 opacity-100 w-[300px]" : "translate-x-full opacity-0 w-0"} @@ -61,76 +83,148 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { variant="floating"> <SidebarContent> <SidebarHeader> - Chat Options + { + props.preexistingAgent && !isEditable ? ( + <div className="flex items-center relative top-2"> + <a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${props.preexistingAgent.slug}`}> + {getIconFromIconName(props.preexistingAgent.icon, props.preexistingAgent.color)} + {props.preexistingAgent.name} + </a> + </div> + ) : ( + <div className="flex items-center relative top-2"> + {getIconFromIconName("lightbulb", "orange")} + Chat Options + </div> + ) + } </SidebarHeader> - <SidebarGroup key={"test"} className="border-b last:border-none"> + <SidebarGroup key={"knowledge"} className="border-b last:border-none"> <SidebarGroupContent className="gap-0"> <SidebarMenu className="p-0 m-0"> - <SidebarMenuItem key={"item4"} className="list-none"> - <span>Custom Instructions</span> - <Textarea className="w-full h-32" /> - </SidebarMenuItem> - <SidebarMenuItem key={"item"} className="list-none"> - <SidebarMenuButton> - <Bell /> <span>Model</span> - </SidebarMenuButton> - <ModelSelector /> - </SidebarMenuItem> - <SidebarMenuItem key={"item1"} className="list-none"> - <SidebarMenuButton> - <Bell /> <span>Input Tools</span> - </SidebarMenuButton> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="outline">Input Tools</Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-56"> - <DropdownMenuLabel>Input Tool Options</DropdownMenuLabel> - <DropdownMenuSeparator /> - { - Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { - return ( - <DropdownMenuCheckboxItem - checked={true} - onCheckedChange={() => { }} - > - {key} - </DropdownMenuCheckboxItem> - ); - } - ) - } - </DropdownMenuContent> - </DropdownMenu> + { + props.preexistingAgent && props.preexistingAgent.has_files ? ( + <SidebarMenuItem key={"agent_knowledge"} className="list-none"> + <div className="flex items-center space-x-2 rounded-full"> + <div className="text-muted-foreground"><Sparkle /></div> + <div className="text-muted-foreground text-sm">Using custom knowledge base</div> + </div> + </SidebarMenuItem> + ) : null + } + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + <SidebarGroup key={"instructions"}> + <SidebarGroupContent> + <SidebarGroupLabel>Custom Instructions</SidebarGroupLabel> + <SidebarMenu className="p-0 m-0"> + <SidebarMenuItem className="list-none"> + <Textarea + className="w-full h-32" + value={customPrompt} + onChange={(e) => setCustomPrompt(e.target.value)} + readOnly={!isEditable} + disabled={!isEditable} /> </SidebarMenuItem> - <SidebarMenuItem key={"item2"} className="list-none"> - <SidebarMenuButton> - <Bell /> <span>Output Tools</span> - </SidebarMenuButton> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="outline">Output Tools</Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-56"> - <DropdownMenuLabel>Output Tool Options</DropdownMenuLabel> - <DropdownMenuSeparator /> - { - Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { - return ( - <DropdownMenuCheckboxItem - checked={true} - onCheckedChange={() => { }} - > - {key} - </DropdownMenuCheckboxItem> - ); - } - ) - } - </DropdownMenuContent> - </DropdownMenu> + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + <SidebarGroup key={"model"}> + <SidebarGroupContent> + <SidebarGroupLabel>Model</SidebarGroupLabel> + <SidebarMenu className="p-0 m-0"> + <SidebarMenuItem key={"model"} className="list-none"> + <ModelSelector + disabled={!isEditable} + onSelect={(model) => setSelectedModel(model.name)} + selectedModel={selectedModel} + /> </SidebarMenuItem> - <SidebarMenuItem key={"item3"} className="list-none"> + </SidebarMenu> + </SidebarGroupContent> + </SidebarGroup> + <Collapsible defaultOpen className="group/collapsible"> + <SidebarGroup> + <SidebarGroupLabel asChild> + <CollapsibleTrigger> + Tools + <CaretCircleDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" /> + </CollapsibleTrigger> + </SidebarGroupLabel> + <CollapsibleContent> + <SidebarGroupContent> + { + Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { + return ( + <Tooltip> + <TooltipTrigger key={key} asChild> + <div className="flex items-center space-x-2 py-1 justify-between"> + <Label htmlFor={key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"> + {getIconForSlashCommand(key)} + <p className="text-sm my-auto flex items-center"> + {key} + </p> + </Label> + <Checkbox + id={key} + className={`${isEditable ? "cursor-pointer" : ""}`} + checked={isValueChecked(key, props.preexistingAgent?.input_tools)} + onCheckedChange={() => { }} + disabled={!isEditable} + > + {key} + </Checkbox> + </div> + </TooltipTrigger> + <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> + {value} + </TooltipContent> + </Tooltip> + ); + } + ) + } + { + Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { + return ( + <Tooltip> + <TooltipTrigger key={key} asChild> + <div className="flex items-center space-x-2 py-1 justify-between"> + <Label htmlFor={key} className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"> + {getIconForSlashCommand(key)} + <p className="text-sm my-auto flex items-center"> + {key} + </p> + </Label> + <Checkbox + id={key} + className={`${isEditable ? "cursor-pointer" : ""}`} + checked={isValueChecked(key, props.preexistingAgent?.output_modes)} + onCheckedChange={() => { }} + disabled={!isEditable} + > + {key} + </Checkbox> + </div> + </TooltipTrigger> + <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> + {value} + </TooltipContent> + </Tooltip> + ); + } + ) + } + </SidebarGroupContent> + </CollapsibleContent> + </SidebarGroup> + </Collapsible> + <SidebarGroup key={"files"}> + <SidebarGroupContent> + <SidebarGroupLabel>Files</SidebarGroupLabel> + <SidebarMenu className="p-0 m-0"> + <SidebarMenuItem key={"files-conversation"} className="list-none"> <FilesMenu conversationId={props.conversationId} uploadedFiles={[]} @@ -141,6 +235,52 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarGroupContent> </SidebarGroup> </SidebarContent> + <SidebarFooter key={"actions"}> + <SidebarMenu className="p-0 m-0"> + + { + (props.preexistingAgent && props.preexistingAgent.is_creator) ? ( + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + variant={"ghost"} + onClick={() => window.location.href = `/agents?agent=${props.preexistingAgent?.slug}`} + > + Manage + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + ) : + <> + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + onClick={() => { }} + variant={"ghost"} + disabled={!isEditable} + > + Reset + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + variant={"secondary"} + onClick={() => { }} + disabled={!isEditable} + > + Save + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + </> + } + </SidebarMenu> + </SidebarFooter> </Sidebar> ) } From 0a0f30c53b4af9d4de72541a00f0bd1e5ef273e3 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:14:28 -0800 Subject: [PATCH 09/43] Update relevant agent tool descriptions Remove text (as by default, must output text), and improve the Notes description for clarity --- src/khoj/utils/helpers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index e3cab3c73..3dfbc8767 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -354,7 +354,7 @@ class ConversationCommand(str, Enum): } command_descriptions_for_agent = { - ConversationCommand.General: "Agent can use the agents knowledge base and general knowledge.", + ConversationCommand.General: "Agent can use its own knowledge base and general knowledge.", ConversationCommand.Notes: "Agent can search the personal knowledge base for information, as well as its own.", ConversationCommand.Online: "Agent can search the internet for information.", ConversationCommand.Webpage: "Agent can read suggested web pages for information.", @@ -388,7 +388,6 @@ class ConversationCommand(str, Enum): mode_descriptions_for_agent = { ConversationCommand.Image: "Agent can generate images in response. It cannot not use this to generate charts and graphs.", ConversationCommand.Automation: "Agent can schedule a task to run at a scheduled date, time and frequency in response.", - ConversationCommand.Text: "Agent can generate text in response.", ConversationCommand.Diagram: "Agent can generate a visual representation that requires primitives like lines, rectangles, and text.", } @@ -433,6 +432,18 @@ def generate_random_name(): return name +def generate_random_internal_agent_name(): + random_name = generate_random_name() + + random_name.replace(" ", "_") + + random_number = random.randint(1000, 9999) + + name = f"{random_name}{random_number}" + + return name + + def batcher(iterable, max_n): "Split an iterable into chunks of size max_n" it = iter(iterable) From be11f666e4bd3b30235a80ca57a44e22c527a0ba Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 12:16:37 -0800 Subject: [PATCH 10/43] Initialize the concept in the backend of hidden agents A hidden agent basically allows each individual conversation to maintain custom settings, via an agent that's not exposed to the traditional functionalities allotted for manually created agents (e.g., browsing, maintenance in agents page). This will be hooked up to the front-end such that any conversation that's initiated with the default agent can then be given custom settings, which in the background creates a hidden agent. This allows us to repurpose all of our existing agents infrastructure for chat-level customization. --- src/khoj/database/adapters/__init__.py | 34 +++++++- src/khoj/database/models/__init__.py | 1 - src/khoj/routers/api_agents.py | 107 ++++++++++++++++++++++++- src/khoj/routers/api_chat.py | 12 ++- 4 files changed, 147 insertions(+), 7 deletions(-) diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 31ccd2f2e..0390ce23d 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -69,6 +69,7 @@ from khoj.utils import state from khoj.utils.config import OfflineChatProcessorModel from khoj.utils.helpers import ( + generate_random_internal_agent_name, generate_random_name, in_debug_mode, is_none_or_empty, @@ -806,13 +807,15 @@ async def aupdate_agent( privacy_level: str, icon: str, color: str, - chat_model: str, + chat_model: Optional[str], files: List[str], input_tools: List[str], output_modes: List[str], slug: Optional[str] = None, is_hidden: Optional[bool] = False, ): + if not chat_model: + chat_model = ConversationAdapters.get_default_chat_model(user) chat_model_option = await ChatModel.objects.filter(name=chat_model).afirst() # Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug @@ -864,6 +867,35 @@ async def aupdate_agent( return agent + @staticmethod + @arequire_valid_user + async def aupdate_hidden_agent( + user: KhojUser, + slug: Optional[str] = None, + persona: Optional[str] = None, + chat_model: Optional[str] = None, + input_tools: Optional[List[str]] = None, + output_modes: Optional[List[str]] = None, + ): + random_name = generate_random_internal_agent_name() + + agent = await AgentAdapters.aupdate_agent( + user=user, + name=random_name, + personality=persona, + privacy_level=Agent.PrivacyLevel.PRIVATE, + icon=Agent.StyleIconTypes.LIGHTBULB, + color=Agent.StyleColorTypes.BLUE, + chat_model=chat_model, + files=[], + input_tools=input_tools, + output_modes=output_modes, + slug=slug, + is_hidden=True, + ) + + return agent + class PublicConversationAdapters: @staticmethod diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 97b245103..b88ae3f36 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -285,7 +285,6 @@ class InputToolOptions(models.TextChoices): class OutputModeOptions(models.TextChoices): # These map to various ConversationCommand types - TEXT = "text" IMAGE = "image" AUTOMATION = "automation" DIAGRAM = "diagram" diff --git a/src/khoj/routers/api_agents.py b/src/khoj/routers/api_agents.py index 87693f56d..432117e11 100644 --- a/src/khoj/routers/api_agents.py +++ b/src/khoj/routers/api_agents.py @@ -9,7 +9,7 @@ from fastapi.requests import Request from fastapi.responses import Response from pydantic import BaseModel -from starlette.authentication import requires +from starlette.authentication import has_required_scope, requires from khoj.database.adapters import AgentAdapters, ConversationAdapters from khoj.database.models import Agent, Conversation, KhojUser @@ -41,6 +41,14 @@ class ModifyAgentBody(BaseModel): is_hidden: Optional[bool] = False +class ModifyHiddenAgentBody(BaseModel): + slug: str + persona: Optional[str] = None + chat_model: Optional[str] = None + input_tools: Optional[List[str]] = [] + output_modes: Optional[List[str]] = [] + + @api_agents.get("", response_class=Response) async def all_agents( request: Request, @@ -183,6 +191,93 @@ async def delete_agent( return Response(content=json.dumps({"message": "Agent deleted."}), media_type="application/json", status_code=200) +@api_agents.patch("/hidden", response_class=Response) +@requires(["authenticated"]) +async def update_hidden_agent( + request: Request, + common: CommonQueryParams, + body: ModifyHiddenAgentBody, +) -> Response: + user: KhojUser = request.user.object + + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + + selected_agent = await AgentAdapters.aget_agent_by_slug(body.slug, user) + + if not selected_agent: + return Response( + content=json.dumps({"error": f"Agent with name {body.slug} not found."}), + media_type="application/json", + status_code=404, + ) + + agent = await AgentAdapters.aupdate_hidden_agent( + user, + body.slug, + body.persona, + chat_model, + body.input_tools, + body.output_modes, + ) + + agents_packet = { + "slug": agent.slug, + "name": agent.name, + "persona": agent.personality, + "creator": agent.creator.username if agent.creator else None, + "managed_by_admin": agent.managed_by_admin, + "color": agent.style_color, + "icon": agent.style_icon, + "privacy_level": agent.privacy_level, + "chat_model": agent.chat_model.name, + "files": body.files, + "input_tools": agent.input_tools, + "output_modes": agent.output_modes, + } + + return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) + + +@api_agents.post("/hidden", response_class=Response) +@requires(["authenticated"]) +async def create_hidden_agent( + request: Request, + common: CommonQueryParams, + body: ModifyHiddenAgentBody, +) -> Response: + user: KhojUser = request.user.object + + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + + agent = await AgentAdapters.aupdate_hidden_agent( + user, + body.slug, + body.persona, + chat_model, + body.input_tools, + body.output_modes, + ) + + agents_packet = { + "slug": agent.slug, + "name": agent.name, + "persona": agent.personality, + "creator": agent.creator.username if agent.creator else None, + "managed_by_admin": agent.managed_by_admin, + "color": agent.style_color, + "icon": agent.style_icon, + "privacy_level": agent.privacy_level, + "chat_model": agent.chat_model.name, + "files": body.files, + "input_tools": agent.input_tools, + "output_modes": agent.output_modes, + } + + return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) + + @api_agents.post("", response_class=Response) @requires(["authenticated"]) async def create_agent( @@ -203,6 +298,9 @@ async def create_agent( status_code=400, ) + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + agent = await AgentAdapters.aupdate_agent( user, body.name, @@ -210,7 +308,7 @@ async def create_agent( body.privacy_level, body.icon, body.color, - body.chat_model, + chat_model, body.files, body.input_tools, body.output_modes, @@ -266,6 +364,9 @@ async def update_agent( status_code=404, ) + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + agent = await AgentAdapters.aupdate_agent( user, body.name, @@ -273,7 +374,7 @@ async def update_agent( body.privacy_level, body.icon, body.color, - body.chat_model, + chat_model, body.files, body.input_tools, body.output_modes, diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 8d9bd7ccb..99f585d02 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -223,13 +223,17 @@ def chat_history( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user: conversation.agent = None else: + agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, - "isCreator": conversation.agent.creator == user, + "is_creator": conversation.agent.creator == user, "color": conversation.agent.style_color, "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, + "is_hidden": conversation.agent.is_hidden, + "chat_model": conversation.agent.chat_model.name, + "has_files": agent_has_files, } meta_log = conversation.conversation_log @@ -282,13 +286,17 @@ def get_shared_chat( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE: conversation.agent = None else: + agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, - "isCreator": conversation.agent.creator == user, + "is_creator": conversation.agent.creator == user, "color": conversation.agent.style_color, "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, + "is_hidden": conversation.agent.is_hidden, + "chat_model": conversation.agent.chat_model.name, + "has_files": agent_has_files, } meta_log = conversation.conversation_log From b248123135ac7fdec89dab04d6cf3f38b38a03b5 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 17:36:30 -0800 Subject: [PATCH 11/43] Hook up hidden agent creation and update APIs to the UI - This allows users to initiate hidden agent creation from the side bar directly. Any updates can easily be applied to the conversation agent. --- src/interface/web/app/chat/page.tsx | 1 - .../components/chatHistory/chatHistory.tsx | 12 +- .../components/chatSidebar/chatSidebar.tsx | 311 +++++++++++++----- src/interface/web/components/ui/checkbox.tsx | 2 +- src/khoj/database/adapters/__init__.py | 5 +- src/khoj/database/models/__init__.py | 2 +- src/khoj/routers/api_agents.py | 104 ++++-- src/khoj/routers/api_chat.py | 6 - src/khoj/utils/helpers.py | 2 +- 9 files changed, 316 insertions(+), 129 deletions(-) diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 7507e24dc..0831891fb 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -181,7 +181,6 @@ function ChatBodyData(props: ChatBodyDataProps) { conversationId={conversationId} isOpen={props.isChatSideBarOpen} onOpenChange={props.onChatSideBarOpenChange} - preexistingAgent={agentMetadata} isMobileWidth={props.isMobileWidth} /> </div> ); diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index f0b46e1ef..ec4fe062f 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -17,7 +17,7 @@ import { Lightbulb, ArrowDown, XCircle } from "@phosphor-icons/react"; import AgentProfileCard from "../profileCard/profileCard"; import { getIconFromIconName } from "@/app/common/iconUtils"; -import { AgentData } from "@/app/agents/page"; +import { AgentData } from "../agentCard/agentCard"; import React from "react"; import { useIsMobileWidth } from "@/app/common/utils"; import { Button } from "@/components/ui/button"; @@ -281,12 +281,20 @@ export default function ChatHistory(props: ChatHistoryProps) { function constructAgentName() { if (!data || !data.agent || !data.agent?.name) return `Agent`; + if (data.agent.is_hidden) return 'Khoj'; + console.log(data.agent); return data.agent?.name; } function constructAgentPersona() { - if (!data || !data.agent || !data.agent?.persona) + if (!data || !data.agent) { return `Your agent is no longer available. You will be reset to the default agent.`; + } + + if (!data.agent?.persona) { + return `You can set a persona for your agent in the Chat Options side panel.`; + } + return data.agent?.persona; } diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 8047a97b5..8c2ca249c 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -1,19 +1,19 @@ "use client" -import { ArrowsDownUp, Bell, CaretCircleDown, Sparkle } from "@phosphor-icons/react"; +import { ArrowsDownUp, CaretCircleDown, CircleNotch, Sparkle } from "@phosphor-icons/react"; import { Button } from "@/components/ui/button"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; -import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Textarea } from "@/components/ui/textarea"; import { ModelSelector } from "@/app/common/modelSelector"; import { FilesMenu } from "../allConversations/allConversations"; import { AgentConfigurationOptions } from "@/app/agents/page"; import useSWR from "swr"; +import { mutate } from "swr"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { AgentData } from "../agentCard/agentCard"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; @@ -22,7 +22,6 @@ import { TooltipContent } from "@radix-ui/react-tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; interface ChatSideBarProps { - preexistingAgent?: AgentData | null; conversationId: string; isOpen: boolean; isMobileWidth?: boolean; @@ -48,47 +47,162 @@ export function ChatSidebar({ ...props }: ChatSideBarProps) { } return ( - <ChatSidebarInternal {...props} /> + <div className="relative"> + <ChatSidebarInternal {...props} /> + </div> ); } function ChatSidebarInternal({ ...props }: ChatSideBarProps) { - const isEditable = props.preexistingAgent?.name.toLowerCase() === "khoj" || props.preexistingAgent?.is_hidden === true; - const isDefaultAgent = props.preexistingAgent?.name.toLowerCase() === "khoj"; + const [isEditable, setIsEditable] = useState<boolean>(false); + const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(false); const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); - const [customPrompt, setCustomPrompt] = useState<string | undefined>(!isDefaultAgent && props.preexistingAgent ? props.preexistingAgent.persona : "always respond in spanish"); - const [selectedModel, setSelectedModel] = useState<string | undefined>(props.preexistingAgent?.chat_model); - const [inputTools, setInputTools] = useState<string[] | undefined>(props.preexistingAgent?.input_tools); + const { data: agentData, error: agentDataError } = useSWR<AgentData>(`/api/agents/conversation?conversation_id=${props.conversationId}`, fetcher); + const [customPrompt, setCustomPrompt] = useState<string | undefined>(""); + const [selectedModel, setSelectedModel] = useState<string | undefined>(); + const [inputTools, setInputTools] = useState<string[] | undefined>(); + const [outputModes, setOutputModes] = useState<string[] | undefined>(); + const [hasModified, setHasModified] = useState<boolean>(false); - function isValueChecked(value: string, existingSelections: string[] | undefined): boolean { - if (existingSelections === undefined || existingSelections === null || existingSelections.length === 0) { - return true; + const [isSaving, setIsSaving] = useState<boolean>(false); + + function setupAgentData() { + if (agentData) { + setSelectedModel(agentData.chat_model); + setInputTools(agentData.input_tools); + if (agentData.input_tools === undefined || agentData.input_tools.length === 0) { + setInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []); + } + setOutputModes(agentData.output_modes); + if (agentData.output_modes === undefined || agentData.output_modes.length === 0) { + setOutputModes(agentConfigurationOptions?.output_modes ? Object.keys(agentConfigurationOptions.output_modes) : []); + } + + if (agentData.name.toLowerCase() === "khoj" || agentData.is_hidden === true) { + setIsEditable(true); + } + + if (agentData.slug.toLowerCase() === "khoj") { + setIsDefaultAgent(true); + } else { + setCustomPrompt(agentData.persona); + } } + } + + useEffect(() => { + setupAgentData(); + }, [agentData]); + + + function isValueChecked(value: string, existingSelections: string[]): boolean { + console.log("isValueChecked", value, existingSelections); return existingSelections.includes(value); } + function handleCheckToggle(value: string, existingSelections: string[]): string[] { + console.log("handleCheckToggle", value, existingSelections); + + setHasModified(true); + + if (existingSelections.includes(value)) { + return existingSelections.filter((v) => v !== value); + } + + return [...existingSelections, value]; + } + + function handleCustomPromptChange(value: string) { + setCustomPrompt(value); + setHasModified(true); + } + + function handleSave() { + if (hasModified) { + + if (agentData?.is_hidden === false) { + alert("This agent is not a hidden agent. It cannot be modified from this interface."); + return; + } + + let mode = "PATCH"; + + if (isDefaultAgent) { + mode = "POST"; + } + + const data = { + persona: customPrompt, + chat_model: selectedModel, + input_tools: inputTools, + output_modes: outputModes, + ...(isDefaultAgent ? {} : { slug: agentData?.slug }) + }; + + console.log("outgoing data payload", data); + setIsSaving(true); + + const url = !isDefaultAgent ? `/api/agents/hidden` : `/api/agents/hidden?conversation_id=${props.conversationId}`; + + // There are four scenarios here. + // 1. If the agent is a default agent, then we need to create a new agent just to associate with this conversation. + // 2. If the agent is not a default agent, then we need to update the existing hidden agent. This will be associated using the `slug` field. + // 3. If the agent is a "proper" agent and not a hidden agent, then it cannot be updated from this API. + // 4. The API is being called before the new conversation has been provisioned. If this happens, then create the agent and associate it with the conversation. Reroute the user to the new conversation page. + fetch(url, { + method: mode, + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then((res) => { + setIsSaving(false); + res.json() + }) + .then((data) => { + mutate(`/api/agents/conversation?conversation_id=${props.conversationId}`); + setHasModified(false); + }) + .catch((error) => { + console.error("Error:", error); + setIsSaving(false); + }); + } + } + + function handleReset() { + setupAgentData(); + setHasModified(false); + } + + function handleModelSelect(model: string) { + setSelectedModel(model); + setHasModified(true); + } + return ( <Sidebar collapsible="none" className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out - ${props.isOpen - ? "translate-x-0 opacity-100 w-[300px]" - : "translate-x-full opacity-0 w-0"} - `} + ${props.isOpen + ? "translate-x-0 opacity-100 w-[300px] relative" + : "translate-x-full opacity-0 w-0 p-0 m-0 fixed"} + `} variant="floating"> <SidebarContent> <SidebarHeader> { - props.preexistingAgent && !isEditable ? ( + agentData && !isEditable ? ( <div className="flex items-center relative top-2"> - <a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${props.preexistingAgent.slug}`}> - {getIconFromIconName(props.preexistingAgent.icon, props.preexistingAgent.color)} - {props.preexistingAgent.name} + <a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}> + {getIconFromIconName(agentData.icon, agentData.color)} + {agentData.name} </a> </div> ) : ( @@ -103,7 +217,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarGroupContent className="gap-0"> <SidebarMenu className="p-0 m-0"> { - props.preexistingAgent && props.preexistingAgent.has_files ? ( + agentData && agentData.has_files ? ( <SidebarMenuItem key={"agent_knowledge"} className="list-none"> <div className="flex items-center space-x-2 rounded-full"> <div className="text-muted-foreground"><Sparkle /></div> @@ -123,7 +237,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <Textarea className="w-full h-32" value={customPrompt} - onChange={(e) => setCustomPrompt(e.target.value)} + onChange={(e) => handleCustomPromptChange(e.target.value)} readOnly={!isEditable} disabled={!isEditable} /> </SidebarMenuItem> @@ -137,7 +251,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenuItem key={"model"} className="list-none"> <ModelSelector disabled={!isEditable} - onSelect={(model) => setSelectedModel(model.name)} + onSelect={(model) => handleModelSelect(model.name)} selectedModel={selectedModel} /> </SidebarMenuItem> @@ -154,68 +268,75 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarGroupLabel> <CollapsibleContent> <SidebarGroupContent> - { - Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { - return ( - <Tooltip> - <TooltipTrigger key={key} asChild> - <div className="flex items-center space-x-2 py-1 justify-between"> - <Label htmlFor={key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"> - {getIconForSlashCommand(key)} - <p className="text-sm my-auto flex items-center"> - {key} - </p> - </Label> - <Checkbox - id={key} - className={`${isEditable ? "cursor-pointer" : ""}`} - checked={isValueChecked(key, props.preexistingAgent?.input_tools)} - onCheckedChange={() => { }} - disabled={!isEditable} - > - {key} - </Checkbox> - </div> - </TooltipTrigger> - <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> - {value} - </TooltipContent> - </Tooltip> - ); + <SidebarMenu className="p-1 m-0"> + { + Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { + return ( + <SidebarMenuItem key={key} className="list-none"> + <Tooltip> + <TooltipTrigger key={key} asChild> + <div className="flex items-center space-x-2 py-1 justify-between"> + <Label htmlFor={key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"> + {getIconForSlashCommand(key)} + <p className="text-sm my-auto flex items-center"> + {key} + </p> + </Label> + <Checkbox + id={key} + className={`${isEditable ? "cursor-pointer" : ""}`} + checked={isValueChecked(key, inputTools ?? [])} + onCheckedChange={() => setInputTools(handleCheckToggle(key, inputTools ?? []))} + disabled={!isEditable} + > + {key} + </Checkbox> + </div> + </TooltipTrigger> + <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> + {value} + </TooltipContent> + </Tooltip> + </SidebarMenuItem> + ); + } + ) } - ) - } - { - Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { - return ( - <Tooltip> - <TooltipTrigger key={key} asChild> - <div className="flex items-center space-x-2 py-1 justify-between"> - <Label htmlFor={key} className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"> - {getIconForSlashCommand(key)} - <p className="text-sm my-auto flex items-center"> - {key} - </p> - </Label> - <Checkbox - id={key} - className={`${isEditable ? "cursor-pointer" : ""}`} - checked={isValueChecked(key, props.preexistingAgent?.output_modes)} - onCheckedChange={() => { }} - disabled={!isEditable} - > - {key} - </Checkbox> - </div> - </TooltipTrigger> - <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> - {value} - </TooltipContent> - </Tooltip> - ); + { + Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { + return ( + <SidebarMenuItem key={key} className="list-none"> + <Tooltip> + <TooltipTrigger key={key} asChild> + <div className="flex items-center space-x-2 py-1 justify-between"> + <Label htmlFor={key} className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"> + {getIconForSlashCommand(key)} + <p className="text-sm my-auto flex items-center"> + {key} + </p> + </Label> + <Checkbox + id={key} + className={`${isEditable ? "cursor-pointer" : ""}`} + checked={isValueChecked(key, outputModes ?? [])} + onCheckedChange={() => setOutputModes(handleCheckToggle(key, outputModes ?? []))} + disabled={!isEditable} + > + {key} + </Checkbox> + </div> + </TooltipTrigger> + <TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> + {value} + </TooltipContent> + </Tooltip> + </SidebarMenuItem> + ); + } + ) } - ) - } + </SidebarMenu> + </SidebarGroupContent> </CollapsibleContent> </SidebarGroup> @@ -239,13 +360,13 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenu className="p-0 m-0"> { - (props.preexistingAgent && props.preexistingAgent.is_creator) ? ( + (agentData && !isEditable && agentData.is_creator) ? ( <SidebarMenuItem> <SidebarMenuButton asChild> <Button className="w-full" variant={"ghost"} - onClick={() => window.location.href = `/agents?agent=${props.preexistingAgent?.slug}`} + onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`} > Manage </Button> @@ -257,9 +378,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenuButton asChild> <Button className="w-full" - onClick={() => { }} + onClick={() => handleReset()} variant={"ghost"} - disabled={!isEditable} + disabled={!isEditable || !hasModified} > Reset </Button> @@ -270,10 +391,18 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <Button className="w-full" variant={"secondary"} - onClick={() => { }} - disabled={!isEditable} + onClick={() => handleSave()} + disabled={!isEditable || !hasModified || isSaving} > - Save + { + isSaving ? + <CircleNotch className="animate-spin" /> + : + <ArrowsDownUp /> + } + { + isSaving ? "Saving" : "Save" + } </Button> </SidebarMenuButton> </SidebarMenuItem> diff --git a/src/interface/web/components/ui/checkbox.tsx b/src/interface/web/components/ui/checkbox.tsx index 18b9a486f..ea65ec4d1 100644 --- a/src/interface/web/components/ui/checkbox.tsx +++ b/src/interface/web/components/ui/checkbox.tsx @@ -13,7 +13,7 @@ const Checkbox = React.forwardRef< <CheckboxPrimitive.Root ref={ref} className={cn( - "peer h-4 w-4 shrink-0 rounded-sm border border-muted ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground", + "peer h-4 w-4 shrink-0 rounded-sm border border-secondary-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground", className )} {...props} diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 0390ce23d..316d22f4a 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -876,12 +876,13 @@ async def aupdate_hidden_agent( chat_model: Optional[str] = None, input_tools: Optional[List[str]] = None, output_modes: Optional[List[str]] = None, + existing_agent: Optional[Agent] = None, ): - random_name = generate_random_internal_agent_name() + name = generate_random_internal_agent_name() if not existing_agent else existing_agent.name agent = await AgentAdapters.aupdate_agent( user=user, - name=random_name, + name=name, personality=persona, privacy_level=Agent.PrivacyLevel.PRIVATE, icon=Agent.StyleIconTypes.LIGHTBULB, diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index b88ae3f36..d9e965a6f 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -303,7 +303,7 @@ class OutputModeOptions(models.TextChoices): managed_by_admin = models.BooleanField(default=False) chat_model = models.ForeignKey(ChatModel, on_delete=models.CASCADE) slug = models.CharField(max_length=200, unique=True) - style_color = models.CharField(max_length=200, choices=StyleColorTypes.choices, default=StyleColorTypes.BLUE) + style_color = models.CharField(max_length=200, choices=StyleColorTypes.choices, default=StyleColorTypes.ORANGE) style_icon = models.CharField(max_length=200, choices=StyleIconTypes.choices, default=StyleIconTypes.LIGHTBULB) privacy_level = models.CharField(max_length=30, choices=PrivacyLevel.choices, default=PrivacyLevel.PRIVATE) is_hidden = models.BooleanField(default=False) diff --git a/src/khoj/routers/api_agents.py b/src/khoj/routers/api_agents.py index 432117e11..05fe27fc5 100644 --- a/src/khoj/routers/api_agents.py +++ b/src/khoj/routers/api_agents.py @@ -11,7 +11,7 @@ from pydantic import BaseModel from starlette.authentication import has_required_scope, requires -from khoj.database.adapters import AgentAdapters, ConversationAdapters +from khoj.database.adapters import AgentAdapters, ConversationAdapters, EntryAdapters from khoj.database.models import Agent, Conversation, KhojUser from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt from khoj.utils.helpers import ( @@ -42,7 +42,7 @@ class ModifyAgentBody(BaseModel): class ModifyHiddenAgentBody(BaseModel): - slug: str + slug: Optional[str] = None persona: Optional[str] = None chat_model: Optional[str] = None input_tools: Optional[List[str]] = [] @@ -102,6 +102,47 @@ async def all_agents( return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) +@api_agents.get("/conversation", response_class=Response) +@requires(["authenticated"]) +async def get_agent_by_conversation( + request: Request, + common: CommonQueryParams, + conversation_id: str, +) -> Response: + user: KhojUser = request.user.object if request.user.is_authenticated else None + conversation = await ConversationAdapters.aget_conversation_by_user(user=user, conversation_id=conversation_id) + + if not conversation: + return Response( + content=json.dumps({"error": f"Conversation with id {conversation_id} not found for user {user}."}), + media_type="application/json", + status_code=404, + ) + + agent = await AgentAdapters.aget_agent_by_slug(conversation.agent.slug, user) + + has_files = agent.fileobject_set.exists() + + agents_packet = { + "slug": agent.slug, + "name": agent.name, + "persona": agent.personality, + "creator": agent.creator.username if agent.creator else None, + "managed_by_admin": agent.managed_by_admin, + "color": agent.style_color, + "icon": agent.style_icon, + "privacy_level": agent.privacy_level, + "chat_model": agent.chat_model.name, + "has_files": has_files, + "input_tools": agent.input_tools, + "output_modes": agent.output_modes, + "is_creator": agent.creator == user, + "is_hidden": agent.is_hidden, + } + + return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) + + @api_agents.get("/options", response_class=Response) async def get_agent_configuration_options( request: Request, @@ -213,12 +254,13 @@ async def update_hidden_agent( ) agent = await AgentAdapters.aupdate_hidden_agent( - user, - body.slug, - body.persona, - chat_model, - body.input_tools, - body.output_modes, + user=user, + slug=body.slug, + persona=body.persona, + chat_model=chat_model, + input_tools=body.input_tools, + output_modes=body.output_modes, + existing_agent=selected_agent, ) agents_packet = { @@ -226,12 +268,7 @@ async def update_hidden_agent( "name": agent.name, "persona": agent.personality, "creator": agent.creator.username if agent.creator else None, - "managed_by_admin": agent.managed_by_admin, - "color": agent.style_color, - "icon": agent.style_icon, - "privacy_level": agent.privacy_level, "chat_model": agent.chat_model.name, - "files": body.files, "input_tools": agent.input_tools, "output_modes": agent.output_modes, } @@ -244,6 +281,7 @@ async def update_hidden_agent( async def create_hidden_agent( request: Request, common: CommonQueryParams, + conversation_id: str, body: ModifyHiddenAgentBody, ) -> Response: user: KhojUser = request.user.object @@ -251,26 +289,44 @@ async def create_hidden_agent( subscribed = has_required_scope(request, ["premium"]) chat_model = body.chat_model if subscribed else None + conversation = await ConversationAdapters.aget_conversation_by_user(user=user, conversation_id=conversation_id) + if not conversation: + return Response( + content=json.dumps({"error": f"Conversation with id {conversation_id} not found for user {user}."}), + media_type="application/json", + status_code=404, + ) + + if conversation.agent: + # If the conversation is not already associated with an agent (i.e., it's using the default agent ), we can create a new one + if conversation.agent.slug != AgentAdapters.DEFAULT_AGENT_SLUG: + return Response( + content=json.dumps( + {"error": f"Conversation with id {conversation_id} already has an agent. Use the PATCH method."} + ), + media_type="application/json", + status_code=400, + ) + agent = await AgentAdapters.aupdate_hidden_agent( - user, - body.slug, - body.persona, - chat_model, - body.input_tools, - body.output_modes, + user=user, + slug=body.slug, + persona=body.persona, + chat_model=chat_model, + input_tools=body.input_tools, + output_modes=body.output_modes, + existing_agent=None, ) + conversation.agent = agent + await conversation.asave() + agents_packet = { "slug": agent.slug, "name": agent.name, "persona": agent.personality, "creator": agent.creator.username if agent.creator else None, - "managed_by_admin": agent.managed_by_admin, - "color": agent.style_color, - "icon": agent.style_icon, - "privacy_level": agent.privacy_level, "chat_model": agent.chat_model.name, - "files": body.files, "input_tools": agent.input_tools, "output_modes": agent.output_modes, } diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 99f585d02..5f4d1821e 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -223,7 +223,6 @@ def chat_history( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user: conversation.agent = None else: - agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, @@ -232,8 +231,6 @@ def chat_history( "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, "is_hidden": conversation.agent.is_hidden, - "chat_model": conversation.agent.chat_model.name, - "has_files": agent_has_files, } meta_log = conversation.conversation_log @@ -286,7 +283,6 @@ def get_shared_chat( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE: conversation.agent = None else: - agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, @@ -295,8 +291,6 @@ def get_shared_chat( "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, "is_hidden": conversation.agent.is_hidden, - "chat_model": conversation.agent.chat_model.name, - "has_files": agent_has_files, } meta_log = conversation.conversation_log diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 3dfbc8767..1682b166d 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -435,7 +435,7 @@ def generate_random_name(): def generate_random_internal_agent_name(): random_name = generate_random_name() - random_name.replace(" ", "_") + random_name = random_name.replace(" ", "_") random_number = random.randint(1000, 9999) From 0d38cc9753a5edd3fdb037e5b436e2f6a9815390 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 17:59:37 -0800 Subject: [PATCH 12/43] Handle further edge cases when setting chat agent data and fix alignment of chat input / side panel --- src/interface/web/app/chat/page.tsx | 2 +- src/interface/web/app/common/modelSelector.tsx | 6 ++++++ .../app/components/chatSidebar/chatSidebar.tsx | 16 ++++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 0831891fb..e7b8c81aa 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -159,7 +159,7 @@ function ChatBodyData(props: ChatBodyDataProps) { /> </div> <div - className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto`} + className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto`} > <ChatInputArea agentColor={agentMetadata?.color} diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx index d5716c1da..6fde77b19 100644 --- a/src/interface/web/app/common/modelSelector.tsx +++ b/src/interface/web/app/common/modelSelector.tsx @@ -52,6 +52,12 @@ export function ModelSelector({ ...props }: ModelSelectorProps) { setSelectedModel(models[0]); }, [models, props.selectedModel]); + useEffect(() => { + if (selectedModel) { + props.onSelect(selectedModel); + } + }, [selectedModel]); + if (isLoading) { return ( <Skeleton className="w-full h-10" /> diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 8c2ca249c..6519833c2 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -20,6 +20,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; import { TooltipContent } from "@radix-ui/react-tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { useAuthenticatedData } from "@/app/common/auth"; interface ChatSideBarProps { conversationId: string; @@ -61,6 +62,11 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); const { data: agentData, error: agentDataError } = useSWR<AgentData>(`/api/agents/conversation?conversation_id=${props.conversationId}`, fetcher); + const { + data: authenticatedData, + error: authenticationError, + isLoading: authenticationLoading, + } = useAuthenticatedData(); const [customPrompt, setCustomPrompt] = useState<string | undefined>(""); const [selectedModel, setSelectedModel] = useState<string | undefined>(); @@ -100,14 +106,10 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { function isValueChecked(value: string, existingSelections: string[]): boolean { - console.log("isValueChecked", value, existingSelections); - return existingSelections.includes(value); } function handleCheckToggle(value: string, existingSelections: string[]): string[] { - console.log("handleCheckToggle", value, existingSelections); - setHasModified(true); if (existingSelections.includes(value)) { @@ -124,8 +126,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { function handleSave() { if (hasModified) { - - if (agentData?.is_hidden === false) { + if (!isDefaultAgent && agentData?.is_hidden === false) { alert("This agent is not a hidden agent. It cannot be modified from this interface."); return; } @@ -144,7 +145,6 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { ...(isDefaultAgent ? {} : { slug: agentData?.slug }) }; - console.log("outgoing data payload", data); setIsSaving(true); const url = !isDefaultAgent ? `/api/agents/hidden` : `/api/agents/hidden?conversation_id=${props.conversationId}`; @@ -250,7 +250,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenu className="p-0 m-0"> <SidebarMenuItem key={"model"} className="list-none"> <ModelSelector - disabled={!isEditable} + disabled={!isEditable || !authenticatedData?.is_active} onSelect={(model) => handleModelSelect(model.name)} selectedModel={selectedModel} /> From dbce039033707435b7bf44242237de4684a35522 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 18:11:53 -0800 Subject: [PATCH 13/43] Revert the `fixed` hack to hide horizontal spacing issue with sidebar because it breaks the animation on closed. Sigh. --- src/interface/web/app/components/chatSidebar/chatSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 6519833c2..c5f7b131f 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -192,7 +192,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out ${props.isOpen ? "translate-x-0 opacity-100 w-[300px] relative" - : "translate-x-full opacity-0 w-0 p-0 m-0 fixed"} + : "translate-x-full opacity-0 w-0 p-0 m-0"} `} variant="floating"> <SidebarContent> From e982398c2c7473dfbf69e8724bcf20a6314463f5 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 18:16:43 -0800 Subject: [PATCH 14/43] Weird spacing issue resolve (it was because of the footer in the collapsed state still having some width) --- .../components/chatSidebar/chatSidebar.tsx | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index c5f7b131f..4a399c42a 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -192,7 +192,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out ${props.isOpen ? "translate-x-0 opacity-100 w-[300px] relative" - : "translate-x-full opacity-0 w-0 p-0 m-0"} + : "translate-x-full opacity-100 w-0 p-0 m-0"} `} variant="floating"> <SidebarContent> @@ -356,60 +356,64 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarGroupContent> </SidebarGroup> </SidebarContent> - <SidebarFooter key={"actions"}> - <SidebarMenu className="p-0 m-0"> + { + props.isOpen && ( + <SidebarFooter key={"actions"}> + <SidebarMenu className="p-0 m-0"> - { - (agentData && !isEditable && agentData.is_creator) ? ( - <SidebarMenuItem> - <SidebarMenuButton asChild> - <Button - className="w-full" - variant={"ghost"} - onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`} - > - Manage - </Button> - </SidebarMenuButton> - </SidebarMenuItem> - ) : - <> - <SidebarMenuItem> - <SidebarMenuButton asChild> - <Button - className="w-full" - onClick={() => handleReset()} - variant={"ghost"} - disabled={!isEditable || !hasModified} - > - Reset - </Button> - </SidebarMenuButton> - </SidebarMenuItem> - <SidebarMenuItem> - <SidebarMenuButton asChild> - <Button - className="w-full" - variant={"secondary"} - onClick={() => handleSave()} - disabled={!isEditable || !hasModified || isSaving} - > - { - isSaving ? - <CircleNotch className="animate-spin" /> - : - <ArrowsDownUp /> - } - { - isSaving ? "Saving" : "Save" - } - </Button> - </SidebarMenuButton> - </SidebarMenuItem> - </> - } - </SidebarMenu> - </SidebarFooter> + { + (agentData && !isEditable && agentData.is_creator) ? ( + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + variant={"ghost"} + onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`} + > + Manage + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + ) : + <> + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + onClick={() => handleReset()} + variant={"ghost"} + disabled={!isEditable || !hasModified} + > + Reset + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + <SidebarMenuItem> + <SidebarMenuButton asChild> + <Button + className="w-full" + variant={"secondary"} + onClick={() => handleSave()} + disabled={!isEditable || !hasModified || isSaving} + > + { + isSaving ? + <CircleNotch className="animate-spin" /> + : + <ArrowsDownUp /> + } + { + isSaving ? "Saving" : "Save" + } + </Button> + </SidebarMenuButton> + </SidebarMenuItem> + </> + } + </SidebarMenu> + </SidebarFooter> + ) + } </Sidebar> ) } From 59ee6e961aeefed3f397bf148b670226f26b1f0f Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sun, 19 Jan 2025 18:19:48 -0800 Subject: [PATCH 15/43] Only set hasmodified to true during model select if different from original model --- src/interface/web/app/components/chatSidebar/chatSidebar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 4a399c42a..c394d8c29 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -183,7 +183,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { function handleModelSelect(model: string) { setSelectedModel(model); - setHasModified(true); + if (model !== agentData?.chat_model) { + setHasModified(true); + } } return ( From d7800812adf9b54c0d4b1d9ea437395d4299856a Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 10:18:09 -0800 Subject: [PATCH 16/43] Fix default states for the model selector --- src/interface/web/app/common/modelSelector.tsx | 9 +++++++-- .../web/app/components/chatHistory/chatHistory.tsx | 1 - .../web/app/components/chatSidebar/chatSidebar.tsx | 4 +++- src/interface/web/yarn.lock | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx index 6fde77b19..ece4709f4 100644 --- a/src/interface/web/app/common/modelSelector.tsx +++ b/src/interface/web/app/common/modelSelector.tsx @@ -24,7 +24,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; -import { ModelOptions, useChatModelOptions } from "./auth"; +import { ModelOptions, useChatModelOptions, useUserConfig } from "./auth"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -39,6 +39,7 @@ export function ModelSelector({ ...props }: ModelSelectorProps) { const { models, isLoading, error } = useChatModelOptions(); const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined); const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined); + const { userConfig } = useUserConfig(); useEffect(() => { if (!models?.length) return; @@ -47,10 +48,14 @@ export function ModelSelector({ ...props }: ModelSelectorProps) { const model = models.find(model => model.name === props.selectedModel); setSelectedModel(model || models[0]); return; + } else if (userConfig) { + const model = models.find(model => model.id === userConfig.selected_chat_model_config); + setSelectedModel(model || models[0]); + return; } setSelectedModel(models[0]); - }, [models, props.selectedModel]); + }, [models, props.selectedModel, userConfig]); useEffect(() => { if (selectedModel) { diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index ec4fe062f..3cf312c05 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -282,7 +282,6 @@ export default function ChatHistory(props: ChatHistoryProps) { function constructAgentName() { if (!data || !data.agent || !data.agent?.name) return `Agent`; if (data.agent.is_hidden) return 'Khoj'; - console.log(data.agent); return data.agent?.name; } diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index c394d8c29..7e89f3e0f 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -78,7 +78,6 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { function setupAgentData() { if (agentData) { - setSelectedModel(agentData.chat_model); setInputTools(agentData.input_tools); if (agentData.input_tools === undefined || agentData.input_tools.length === 0) { setInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []); @@ -94,8 +93,11 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { if (agentData.slug.toLowerCase() === "khoj") { setIsDefaultAgent(true); + setSelectedModel(undefined); + setCustomPrompt(undefined); } else { setCustomPrompt(agentData.persona); + setSelectedModel(agentData.chat_model); } } } diff --git a/src/interface/web/yarn.lock b/src/interface/web/yarn.lock index 84c56b275..0c8b8c2f0 100644 --- a/src/interface/web/yarn.lock +++ b/src/interface/web/yarn.lock @@ -977,7 +977,7 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.1" -"@radix-ui/react-switch@^1.1.1", "@radix-ui/react-switch@^1.1.2": +"@radix-ui/react-switch@^1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec" integrity sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g== From a3fcd6f06eb3df3804f61c783edeb9bdde49b41b Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 10:36:11 -0800 Subject: [PATCH 17/43] Fix import of AgentData from agentcard --- src/interface/web/app/components/chatMessage/chatMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx index c3227f593..4b25346ec 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.tsx +++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx @@ -36,7 +36,7 @@ import { import DOMPurify from "dompurify"; import { InlineLoading } from "../loading/loading"; import { convertColorToTextClass } from "@/app/common/colorUtils"; -import { AgentData } from "@/app/agents/page"; +import { AgentData } from "@/app/components/agentCard/agentCard"; import renderMathInElement from "katex/contrib/auto-render"; import "katex/dist/katex.min.css"; From d681a2080a527242c046ee436fd13dc9db148e6c Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 10:59:02 -0800 Subject: [PATCH 18/43] Centralize use of useUserConfig and use that to retrieve default model and chat model options --- src/interface/web/app/agents/page.tsx | 2 +- src/interface/web/app/common/auth.ts | 11 ++++--- .../web/app/common/modelSelector.tsx | 32 +++++++++++-------- src/interface/web/app/page.tsx | 2 +- src/interface/web/app/settings/page.tsx | 2 +- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index 0d8e4b722..9a854d4da 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -171,7 +171,7 @@ export default function Agents() { error: authenticationError, isLoading: authenticationLoading, } = useAuthenticatedData(); - const { userConfig } = useUserConfig(true); + const { data: userConfig } = useUserConfig(true); const [showLoginPrompt, setShowLoginPrompt] = useState(false); const isMobileWidth = useIsMobileWidth(); diff --git a/src/interface/web/app/common/auth.ts b/src/interface/web/app/common/auth.ts index e00caa25f..917fa7dab 100644 --- a/src/interface/web/app/common/auth.ts +++ b/src/interface/web/app/common/auth.ts @@ -90,15 +90,16 @@ export interface UserConfig { export function useUserConfig(detailed: boolean = false) { const url = `/api/settings?detailed=${detailed}`; const { - data: userConfig, + data, error, - isLoading: isLoadingUserConfig, + isLoading, } = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false }); - if (error || !userConfig || userConfig?.detail === "Forbidden") - return { userConfig: null, isLoadingUserConfig }; + if (error || !data || data?.detail === "Forbidden") { + return { data: null, error, isLoading }; + } - return { userConfig, isLoadingUserConfig }; + return { data, error, isLoading }; } export function useChatModelOptions() { diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx index ece4709f4..bf837ea1b 100644 --- a/src/interface/web/app/common/modelSelector.tsx +++ b/src/interface/web/app/common/modelSelector.tsx @@ -17,14 +17,13 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { ModelOptions, useChatModelOptions, useUserConfig } from "./auth"; +import { ModelOptions, useUserConfig } from "./auth"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -36,34 +35,39 @@ interface ModelSelectorProps extends PopoverProps { export function ModelSelector({ ...props }: ModelSelectorProps) { const [open, setOpen] = React.useState(false) - const { models, isLoading, error } = useChatModelOptions(); const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined); const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined); - const { userConfig } = useUserConfig(); + const { data: userConfig, error, isLoading: isLoadingUserConfig } = useUserConfig(true); + const [models, setModels] = useState<ModelOptions[]>([]); useEffect(() => { - if (!models?.length) return; + if (isLoadingUserConfig) return; - if (props.selectedModel) { - const model = models.find(model => model.name === props.selectedModel); - setSelectedModel(model || models[0]); - return; - } else if (userConfig) { - const model = models.find(model => model.id === userConfig.selected_chat_model_config); - setSelectedModel(model || models[0]); + if (userConfig) { + setModels(userConfig.chat_model_options); + const selectedChatModelOption = userConfig.chat_model_options.find(model => model.id === userConfig.selected_chat_model_config); + setSelectedModel(selectedChatModelOption); return; + } else { + setSelectedModel(models[0]); } - setSelectedModel(models[0]); }, [models, props.selectedModel, userConfig]); + useEffect(() => { + if (props.selectedModel) { + const model = models.find(model => model.name === props.selectedModel); + setSelectedModel(model); + } + }, [props.selectedModel]); + useEffect(() => { if (selectedModel) { props.onSelect(selectedModel); } }, [selectedModel]); - if (isLoading) { + if (isLoadingUserConfig) { return ( <Skeleton className="w-full h-10" /> ); diff --git a/src/interface/web/app/page.tsx b/src/interface/web/app/page.tsx index e2d185695..f0a3ee1e9 100644 --- a/src/interface/web/app/page.tsx +++ b/src/interface/web/app/page.tsx @@ -486,7 +486,7 @@ export default function Home() { const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | null>(null); const isMobileWidth = useIsMobileWidth(); - const { userConfig: initialUserConfig, isLoadingUserConfig } = useUserConfig(true); + const { data: initialUserConfig, isLoading: isLoadingUserConfig } = useUserConfig(true); const [userConfig, setUserConfig] = useState<UserConfig | null>(null); const { diff --git a/src/interface/web/app/settings/page.tsx b/src/interface/web/app/settings/page.tsx index 38481ad53..f19c53ddd 100644 --- a/src/interface/web/app/settings/page.tsx +++ b/src/interface/web/app/settings/page.tsx @@ -595,7 +595,7 @@ enum PhoneNumberValidationState { } export default function SettingsView() { - const { userConfig: initialUserConfig } = useUserConfig(true); + const { data: initialUserConfig } = useUserConfig(true); const [userConfig, setUserConfig] = useState<UserConfig | null>(null); const [name, setName] = useState<string | undefined>(undefined); const [notionToken, setNotionToken] = useState<string | null>(null); From 235114b43271d35f9d448c64aaf79851f29a4dbd Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 11:04:48 -0800 Subject: [PATCH 19/43] Fix agent data import across chat page + --- src/interface/web/app/components/chatHistory/chatHistory.tsx | 2 +- src/interface/web/app/share/chat/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 3cf312c05..33c2c00ef 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -17,7 +17,7 @@ import { Lightbulb, ArrowDown, XCircle } from "@phosphor-icons/react"; import AgentProfileCard from "../profileCard/profileCard"; import { getIconFromIconName } from "@/app/common/iconUtils"; -import { AgentData } from "../agentCard/agentCard"; +import { AgentData } from "@/app/components/agentCard/agentCard"; import React from "react"; import { useIsMobileWidth } from "@/app/common/utils"; import { Button } from "@/components/ui/button"; diff --git a/src/interface/web/app/share/chat/page.tsx b/src/interface/web/app/share/chat/page.tsx index b7966310c..3e00afb83 100644 --- a/src/interface/web/app/share/chat/page.tsx +++ b/src/interface/web/app/share/chat/page.tsx @@ -17,7 +17,7 @@ import { ChatOptions, } from "@/app/components/chatInputArea/chatInputArea"; import { StreamMessage } from "@/app/components/chatMessage/chatMessage"; -import { AgentData } from "@/app/agents/page"; +import { AgentData } from "@/app/components/agentCard/agentCard"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "@/app/components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; From 000580cb8aff947d962e996eb616719ef85ffa18 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 11:47:42 -0800 Subject: [PATCH 20/43] Improve loading state when files not found and fix default state interpretation for model selector --- .../web/app/common/modelSelector.tsx | 6 +-- .../allConversations/allConversations.tsx | 37 +++++++++++++------ .../components/chatSidebar/chatSidebar.tsx | 8 ++-- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/interface/web/app/common/modelSelector.tsx b/src/interface/web/app/common/modelSelector.tsx index bf837ea1b..c9313d587 100644 --- a/src/interface/web/app/common/modelSelector.tsx +++ b/src/interface/web/app/common/modelSelector.tsx @@ -28,7 +28,7 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/h import { Skeleton } from "@/components/ui/skeleton"; interface ModelSelectorProps extends PopoverProps { - onSelect: (model: ModelOptions) => void; + onSelect: (model: ModelOptions, userModification: boolean) => void; selectedModel?: string; disabled?: boolean; } @@ -51,7 +51,6 @@ export function ModelSelector({ ...props }: ModelSelectorProps) { } else { setSelectedModel(models[0]); } - }, [models, props.selectedModel, userConfig]); useEffect(() => { @@ -63,7 +62,8 @@ export function ModelSelector({ ...props }: ModelSelectorProps) { useEffect(() => { if (selectedModel) { - props.onSelect(selectedModel); + const userModification = selectedModel.id !== userConfig?.selected_chat_model_config; + props.onSelect(selectedModel, userModification); } }, [selectedModel]); diff --git a/src/interface/web/app/components/allConversations/allConversations.tsx b/src/interface/web/app/components/allConversations/allConversations.tsx index 0bab81658..29d34ddac 100644 --- a/src/interface/web/app/components/allConversations/allConversations.tsx +++ b/src/interface/web/app/components/allConversations/allConversations.tsx @@ -46,20 +46,14 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { ArrowRight, - ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, - House, - StackPlus, - UserCirclePlus, - Sidebar, - NotePencil, FunnelSimple, - MagnifyingGlass, ChatsCircle, + Info, } from "@phosphor-icons/react"; interface ChatHistory { @@ -132,7 +126,7 @@ function renameConversation(conversationId: string, newTitle: string) { }, }) .then((response) => response.json()) - .then((data) => {}) + .then((data) => { }) .catch((err) => { console.error(err); return; @@ -171,7 +165,7 @@ function deleteConversation(conversationId: string) { response.json(); mutate("/api/chat/sessions"); }) - .then((data) => {}) + .then((data) => { }) .catch((err) => { console.error(err); return; @@ -237,8 +231,29 @@ export function FilesMenu(props: FilesMenuProps) { ); }; - if (error) return <div>Failed to load files</div>; - if (selectedFilesError) return <div>Failed to load selected files</div>; + if (error || selectedFilesError) { + return ( + <div className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl"> + <div className="flex items-center justify-between space-x-4"> + <h4 className="text-sm font-semibold"> + Context + <p> + <span className="text-muted-foreground text-xs"> + { + error ? "Failed to load files" : "Failed to load selected files" + } + </span> + </p> + </h4> + <Button variant="ghost" size="sm" className="w-9 p-0"> + <Info className="h-4 w-4" /> + <span className="sr-only">Error Info</span> + </Button> + </div> + </div> + ) + } + if (!files) return <InlineLoading />; if (!selectedFiles && props.conversationId) return <InlineLoading />; diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 7e89f3e0f..3b0288e5e 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -183,9 +183,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { setHasModified(false); } - function handleModelSelect(model: string) { + function handleModelSelect(model: string, userModification: boolean = true) { setSelectedModel(model); - if (model !== agentData?.chat_model) { + if (userModification) { setHasModified(true); } } @@ -255,7 +255,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenuItem key={"model"} className="list-none"> <ModelSelector disabled={!isEditable || !authenticatedData?.is_active} - onSelect={(model) => handleModelSelect(model.name)} + onSelect={(model, userModification) => handleModelSelect(model.name, userModification)} selectedModel={selectedModel} /> </SidebarMenuItem> @@ -395,7 +395,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarMenuItem> <SidebarMenuButton asChild> <Button - className="w-full" + className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`} variant={"secondary"} onClick={() => handleSave()} disabled={!isEditable || !hasModified || isSaving} From 0c29c7a5bf6466207ededa3ab00e78f2a590c7f5 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Mon, 20 Jan 2025 12:02:07 -0800 Subject: [PATCH 21/43] Update layout and rendering of share page for hidden agent --- src/interface/web/app/share/chat/page.tsx | 60 ++++++++++--------- .../web/app/share/chat/sharedChat.module.css | 1 + src/khoj/routers/api_chat.py | 16 ++++- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/interface/web/app/share/chat/page.tsx b/src/interface/web/app/share/chat/page.tsx index 3e00afb83..b9b51966d 100644 --- a/src/interface/web/app/share/chat/page.tsx +++ b/src/interface/web/app/share/chat/page.tsx @@ -79,36 +79,38 @@ function ChatBodyData(props: ChatBodyDataProps) { } return ( - <> - <div className={false ? styles.chatBody : styles.chatBodyFull}> - <ChatHistory - publicConversationSlug={props.publicConversationSlug} - conversationId={props.conversationId || ""} - setAgent={setAgentMetadata} - setTitle={props.setTitle} - pendingMessage={processingMessage ? message : ""} - incomingMessages={props.streamedMessages} - customClassName={chatHistoryCustomClassName} - /> - </div> - <div - className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto`} - > - <ChatInputArea - isLoggedIn={props.isLoggedIn} - sendMessage={(message) => setMessage(message)} - sendImage={(image) => setImages((prevImages) => [...prevImages, image])} - sendDisabled={processingMessage} - chatOptionsData={props.chatOptionsData} - conversationId={props.conversationId} - agentColor={agentMetadata?.color} - isMobileWidth={props.isMobileWidth} - setUploadedFiles={props.setUploadedFiles} - setTriggeredAbort={() => {}} - ref={chatInputRef} - /> + <div className="flex flex-row h-full w-full"> + <div className="flex flex-col h-full w-full"> + <div className={false ? styles.chatBody : styles.chatBodyFull}> + <ChatHistory + publicConversationSlug={props.publicConversationSlug} + conversationId={props.conversationId || ""} + setAgent={setAgentMetadata} + setTitle={props.setTitle} + pendingMessage={processingMessage ? message : ""} + incomingMessages={props.streamedMessages} + customClassName={chatHistoryCustomClassName} + /> + </div> + <div + className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto`} + > + <ChatInputArea + isLoggedIn={props.isLoggedIn} + sendMessage={(message) => setMessage(message)} + sendImage={(image) => setImages((prevImages) => [...prevImages, image])} + sendDisabled={processingMessage} + chatOptionsData={props.chatOptionsData} + conversationId={props.conversationId} + agentColor={agentMetadata?.color} + isMobileWidth={props.isMobileWidth} + setUploadedFiles={props.setUploadedFiles} + setTriggeredAbort={() => { }} + ref={chatInputRef} + /> + </div> </div> - </> + </div> ); } diff --git a/src/interface/web/app/share/chat/sharedChat.module.css b/src/interface/web/app/share/chat/sharedChat.module.css index 844834f3f..d147502bb 100644 --- a/src/interface/web/app/share/chat/sharedChat.module.css +++ b/src/interface/web/app/share/chat/sharedChat.module.css @@ -35,6 +35,7 @@ div.inputBox:focus { div.chatBodyFull { display: grid; grid-template-columns: 1fr; + height: auto; } button.inputBox { diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 5f4d1821e..0bd9cca04 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -280,8 +280,20 @@ def get_shared_chat( agent_metadata = None if conversation.agent: - if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE: - conversation.agent = None + if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user: + if conversation.agent.is_hidden: + default_agent = AgentAdapters.get_default_agent() + agent_metadata = { + "slug": default_agent.slug, + "name": default_agent.name, + "is_creator": False, + "color": default_agent.style_color, + "icon": default_agent.style_icon, + "persona": default_agent.personality, + "is_hidden": default_agent.is_hidden, + } + else: + conversation.agent = None else: agent_metadata = { "slug": conversation.agent.slug, From c43079cb21ec429e5428ed261a72b61a3ab08a24 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 21 Jan 2025 11:01:08 -0800 Subject: [PATCH 22/43] Add merge migration and add a new button for new convo in sidebar --- .../app/components/appSidebar/appSidebar.tsx | 45 ++++++++++++++++--- .../migrations/0082_merge_20250121_1842.py | 12 +++++ 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/khoj/database/migrations/0082_merge_20250121_1842.py diff --git a/src/interface/web/app/components/appSidebar/appSidebar.tsx b/src/interface/web/app/components/appSidebar/appSidebar.tsx index 97b7664af..6916a6219 100644 --- a/src/interface/web/app/components/appSidebar/appSidebar.tsx +++ b/src/interface/web/app/components/appSidebar/appSidebar.tsx @@ -17,23 +17,44 @@ import { KhojLogoType, KhojSearchLogo, } from "../logo/khojLogo"; -import { Gear } from "@phosphor-icons/react/dist/ssr"; -import { Plus } from "@phosphor-icons/react"; +import { Plus, Gear, HouseSimple } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; import AllConversations from "../allConversations/allConversations"; import FooterMenu from "../navMenu/navMenu"; import { useSidebar } from "@/components/ui/sidebar"; import { useIsMobileWidth } from "@/app/common/utils"; import { UserPlusIcon } from "lucide-react"; -import { useAuthenticatedData } from "@/app/common/auth"; +import { useAuthenticatedData, UserProfile } from "@/app/common/auth"; import LoginPrompt from "../loginPrompt/loginPrompt"; +async function openChat(userData: UserProfile | null | undefined) { + const unauthenticatedRedirectUrl = `/login?redirect=${encodeURIComponent(window.location.pathname)}`; + if (!userData) { + window.location.href = unauthenticatedRedirectUrl; + return; + } + + const response = await fetch(`/api/chat/sessions`, { + method: "POST", + }); + + const data = await response.json(); + if (response.status == 200) { + window.location.href = `/chat?conversationId=${data.conversation_id}`; + } else if (response.status == 403 || response.status == 401) { + window.location.href = unauthenticatedRedirectUrl; + } else { + alert("Failed to start chat session"); + } +} + + // Menu items. const items = [ { - title: "New", + title: "Home", url: "/", - icon: Plus, + icon: HouseSimple }, { title: "Agents", @@ -125,6 +146,20 @@ export function AppSidebar(props: AppSidebarProps) { </SidebarMenuButton> </SidebarMenuItem> )} + { + <SidebarMenuItem className="p-0 m-0 list-none"> + <SidebarMenuButton + asChild + variant={"default"} + onClick={() => openChat(data)} + > + <div> + <Plus /> + <span>New</span> + </div> + </SidebarMenuButton> + </SidebarMenuItem> + } {items.map((item) => ( <SidebarMenuItem key={item.title} className="p-0 list-none m-0"> <SidebarMenuButton asChild> diff --git a/src/khoj/database/migrations/0082_merge_20250121_1842.py b/src/khoj/database/migrations/0082_merge_20250121_1842.py new file mode 100644 index 000000000..2f1dbaab5 --- /dev/null +++ b/src/khoj/database/migrations/0082_merge_20250121_1842.py @@ -0,0 +1,12 @@ +# Generated by Django 5.0.10 on 2025-01-21 18:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0081_agent_is_hidden_chatmodel_description_and_more"), + ("database", "0081_merge_20250120_1633"), + ] + + operations = [] From e3e93e091d8897edd74722fdfc0f18dd8b3acd64 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 21 Jan 2025 11:56:06 -0800 Subject: [PATCH 23/43] automatically open the side bar when a new chat is created with the default agent. --- src/interface/web/app/chat/page.tsx | 7 ++++--- .../web/app/components/chatHistory/chatHistory.tsx | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index e7b8c81aa..2428bb1b5 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -49,7 +49,7 @@ interface ChatBodyDataProps { setImages: (images: string[]) => void; setTriggeredAbort: (triggeredAbort: boolean) => void; isChatSideBarOpen: boolean; - onChatSideBarOpenChange: (open: boolean) => void; + setIsChatSideBarOpen: (open: boolean) => void; } function ChatBodyData(props: ChatBodyDataProps) { @@ -156,6 +156,7 @@ function ChatBodyData(props: ChatBodyDataProps) { incomingMessages={props.streamedMessages} setIncomingMessages={props.setStreamedMessages} customClassName={chatHistoryCustomClassName} + setIsChatSideBarOpen={props.setIsChatSideBarOpen} /> </div> <div @@ -180,7 +181,7 @@ function ChatBodyData(props: ChatBodyDataProps) { <ChatSidebar conversationId={conversationId} isOpen={props.isChatSideBarOpen} - onOpenChange={props.onChatSideBarOpenChange} + onOpenChange={props.setIsChatSideBarOpen} isMobileWidth={props.isMobileWidth} /> </div> ); @@ -478,7 +479,7 @@ export default function Chat() { setImages={setImages} setTriggeredAbort={setTriggeredAbort} isChatSideBarOpen={isChatSideBarOpen} - onChatSideBarOpenChange={setIsChatSideBarOpen} + setIsChatSideBarOpen={setIsChatSideBarOpen} /> </Suspense> </div> diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 33c2c00ef..e85b60b93 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -40,6 +40,7 @@ interface ChatHistoryProps { publicConversationSlug?: string; setAgent: (agent: AgentData) => void; customClassName?: string; + setIsChatSideBarOpen?: (isOpen: boolean) => void; } interface TrainOfThoughtComponentProps { @@ -149,6 +150,7 @@ export default function ChatHistory(props: ChatHistoryProps) { latestUserMessageRef.current?.scrollIntoView({ behavior: "auto", block: "start" }); }); } + }, [data, currentPage]); useEffect(() => { @@ -251,6 +253,9 @@ export default function ChatHistory(props: ChatHistoryProps) { }; props.setAgent(chatData.response.agent); setData(chatMetadata); + if (props.setIsChatSideBarOpen) { + props.setIsChatSideBarOpen(true); + } } setHasMoreMessages(false); From e518626027e422bb7a80fe90e09c15f50a556bd9 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 21 Jan 2025 11:56:52 -0800 Subject: [PATCH 24/43] Add typing to empty list of operations --- src/khoj/database/migrations/0082_merge_20250121_1842.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/khoj/database/migrations/0082_merge_20250121_1842.py b/src/khoj/database/migrations/0082_merge_20250121_1842.py index 2f1dbaab5..9264028ac 100644 --- a/src/khoj/database/migrations/0082_merge_20250121_1842.py +++ b/src/khoj/database/migrations/0082_merge_20250121_1842.py @@ -1,4 +1,5 @@ # Generated by Django 5.0.10 on 2025-01-21 18:42 +from typing import List from django.db import migrations @@ -9,4 +10,4 @@ class Migration(migrations.Migration): ("database", "0081_merge_20250120_1633"), ] - operations = [] + operations: List[str] = [] From c1b0a9f8d4745002c444f31036df2f9d539292ed Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 21 Jan 2025 12:06:09 -0800 Subject: [PATCH 25/43] Fix sync to async issue when getting default chat model in hidden agent configuration API --- src/khoj/database/adapters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 344af2a5b..e50ea29ab 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -815,7 +815,7 @@ async def aupdate_agent( is_hidden: Optional[bool] = False, ): if not chat_model: - chat_model = ConversationAdapters.get_default_chat_model(user) + chat_model = await ConversationAdapters.aget_default_chat_model(user) chat_model_option = await ChatModel.objects.filter(name=chat_model).afirst() # Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug From dc6e9e86677ae1d7d88a42f27a1fbcf3283aa86e Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 21 Jan 2025 12:43:34 -0800 Subject: [PATCH 26/43] Skip showing hidden agents in the all conversations agent filter --- .../allConversations/allConversations.tsx | 18 ++++++++++++++---- .../allConversations/sidePanel.module.css | 2 +- src/khoj/routers/api_chat.py | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/interface/web/app/components/allConversations/allConversations.tsx b/src/interface/web/app/components/allConversations/allConversations.tsx index 29d34ddac..41361bc5a 100644 --- a/src/interface/web/app/components/allConversations/allConversations.tsx +++ b/src/interface/web/app/components/allConversations/allConversations.tsx @@ -62,6 +62,7 @@ interface ChatHistory { agent_name: string; agent_icon: string; agent_color: string; + agent_is_hidden: boolean; compressed: boolean; created: string; updated: string; @@ -465,6 +466,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) { agent_name={chatHistory.agent_name} agent_color={chatHistory.agent_color} agent_icon={chatHistory.agent_icon} + agent_is_hidden={chatHistory.agent_is_hidden} /> ), )} @@ -694,7 +696,7 @@ function ChatSession(props: ChatHistory) { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} key={props.conversation_id} - className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""}`} + className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""} m-1`} > <SidebarMenuButton asChild> <Link @@ -740,14 +742,21 @@ function ChatSessionsModal({ data, sideBarOpen }: ChatSessionsModalProps) { let agentNameToStyleMapLocal: Record<string, AgentStyle> = {}; Object.keys(data).forEach((timeGrouping) => { data[timeGrouping].forEach((chatHistory) => { - if (!agents.includes(chatHistory.agent_name) && chatHistory.agent_name) { + if (chatHistory.agent_is_hidden) return; + if (!chatHistory.agent_color) return; + if (!chatHistory.agent_name) return; + if (!chatHistory.agent_icon) return; + + const agentName = chatHistory.agent_name; + + if (agentName && !agents.includes(agentName)) { agents.push(chatHistory.agent_name); agentNameToStyleMapLocal = { ...agentNameToStyleMapLocal, [chatHistory.agent_name]: { - color: chatHistory.agent_color, - icon: chatHistory.agent_icon, + color: chatHistory.agent_color ?? "orange", + icon: chatHistory.agent_icon ?? "Lightbulb", }, }; } @@ -875,6 +884,7 @@ function ChatSessionsModal({ data, sideBarOpen }: ChatSessionsModalProps) { agent_name={chatHistory.agent_name} agent_color={chatHistory.agent_color} agent_icon={chatHistory.agent_icon} + agent_is_hidden={chatHistory.agent_is_hidden} /> ))} </div> diff --git a/src/interface/web/app/components/allConversations/sidePanel.module.css b/src/interface/web/app/components/allConversations/sidePanel.module.css index 5a8a1bd68..baebda6f9 100644 --- a/src/interface/web/app/components/allConversations/sidePanel.module.css +++ b/src/interface/web/app/components/allConversations/sidePanel.module.css @@ -75,7 +75,7 @@ p.session { } p.compressed { - width: 12rem; + width: 11rem; } p.expanded { diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 0bd9cca04..4d346d403 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -457,6 +457,7 @@ def chat_sessions( "updated_at", "agent__style_icon", "agent__style_color", + "agent__is_hidden", ) session_values = [ @@ -468,6 +469,7 @@ def chat_sessions( "updated": session[6].strftime("%Y-%m-%d %H:%M:%S"), "agent_icon": session[7], "agent_color": session[8], + "agent_is_hidden": session[9], } for session in sessions ] From 43c9ec260d5ebf3bf54ba9607a8bc2b28ffd1150 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Fri, 24 Jan 2025 08:24:53 -0800 Subject: [PATCH 27/43] Allow agent personality to be nullable, in which case the default prompt will be used. --- ..._modes_alter_agent_personality_and_more.py | 71 +++++++++++++++++++ src/khoj/database/models/__init__.py | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/khoj/database/migrations/0083_alter_agent_output_modes_alter_agent_personality_and_more.py diff --git a/src/khoj/database/migrations/0083_alter_agent_output_modes_alter_agent_personality_and_more.py b/src/khoj/database/migrations/0083_alter_agent_output_modes_alter_agent_personality_and_more.py new file mode 100644 index 000000000..7a2b45d06 --- /dev/null +++ b/src/khoj/database/migrations/0083_alter_agent_output_modes_alter_agent_personality_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.0.10 on 2025-01-24 16:23 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0082_merge_20250121_1842"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="output_modes", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[("image", "Image"), ("automation", "Automation"), ("diagram", "Diagram")], max_length=200 + ), + blank=True, + default=list, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="agent", + name="personality", + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AlterField( + model_name="agent", + name="style_color", + field=models.CharField( + choices=[ + ("blue", "Blue"), + ("green", "Green"), + ("red", "Red"), + ("yellow", "Yellow"), + ("orange", "Orange"), + ("purple", "Purple"), + ("pink", "Pink"), + ("teal", "Teal"), + ("cyan", "Cyan"), + ("lime", "Lime"), + ("indigo", "Indigo"), + ("fuchsia", "Fuchsia"), + ("rose", "Rose"), + ("sky", "Sky"), + ("amber", "Amber"), + ("emerald", "Emerald"), + ], + default="orange", + max_length=200, + ), + ), + migrations.AlterField( + model_name="processlock", + name="name", + field=models.CharField( + choices=[ + ("index_content", "Index Content"), + ("scheduled_job", "Scheduled Job"), + ("schedule_leader", "Schedule Leader"), + ("apply_migrations", "Apply Migrations"), + ], + max_length=200, + unique=True, + ), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index ea7fbfe89..f377c9f79 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -293,7 +293,7 @@ class OutputModeOptions(models.TextChoices): KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True ) # Creator will only be null when the agents are managed by admin name = models.CharField(max_length=200) - personality = models.TextField() + personality = models.TextField(default=None, null=True, blank=True) input_tools = ArrayField( models.CharField(max_length=200, choices=InputToolOptions.choices), default=list, null=True, blank=True ) From ee3ae18f5556b7d1ba35ab4e54c201c2582ac593 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 18:11:25 -0800 Subject: [PATCH 28/43] add code, remove summarize from agent tools --- .../0084_alter_agent_input_tools.py | 33 +++++++++++++++++++ src/khoj/database/models/__init__.py | 4 +-- src/khoj/utils/helpers.py | 3 +- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/khoj/database/migrations/0084_alter_agent_input_tools.py diff --git a/src/khoj/database/migrations/0084_alter_agent_input_tools.py b/src/khoj/database/migrations/0084_alter_agent_input_tools.py new file mode 100644 index 000000000..8133d5795 --- /dev/null +++ b/src/khoj/database/migrations/0084_alter_agent_input_tools.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.10 on 2025-01-28 23:35 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0083_alter_agent_output_modes_alter_agent_personality_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="input_tools", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("general", "General"), + ("online", "Online"), + ("notes", "Notes"), + ("webpage", "Webpage"), + ("code", "Code"), + ], + max_length=200, + ), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 8e76d3ec8..51bc8e27d 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -280,8 +280,8 @@ class InputToolOptions(models.TextChoices): GENERAL = "general" ONLINE = "online" NOTES = "notes" - SUMMARIZE = "summarize" - WEBPAGE = "webpage" + WEBPAGE = ("webpage",) + CODE = "code" class OutputModeOptions(models.TextChoices): # These map to various ConversationCommand types diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 1682b166d..322906572 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -358,8 +358,8 @@ class ConversationCommand(str, Enum): ConversationCommand.Notes: "Agent can search the personal knowledge base for information, as well as its own.", ConversationCommand.Online: "Agent can search the internet for information.", ConversationCommand.Webpage: "Agent can read suggested web pages for information.", - ConversationCommand.Summarize: "Agent can read an entire document. Agents knowledge base must be a single document.", ConversationCommand.Research: "Agent can do deep research on a topic.", + ConversationCommand.Code: "Agent can run Python code to parse information, run complex calculations, create documents and charts.", } tool_descriptions_for_llm = { @@ -369,7 +369,6 @@ class ConversationCommand(str, Enum): ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**", ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.", ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse complex information, run complex calculations, create plaintext documents, and create charts with quantitative data. Only matplotlib, panda, numpy, scipy, bs4 and sympy external packages are available.", - ConversationCommand.Summarize: "To retrieve an answer that depends on the entire document or a large text.", } function_calling_description_for_llm = { From e076ebd133ecc3f8947560db8c70453a572da021 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 18:12:13 -0800 Subject: [PATCH 29/43] Make tools section of sidebar a popover to prevent increasing height --- .../components/chatSidebar/chatSidebar.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 3b0288e5e..397a5d493 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -19,8 +19,8 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; import { TooltipContent } from "@radix-ui/react-tooltip"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { useAuthenticatedData } from "@/app/common/auth"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; interface ChatSideBarProps { conversationId: string; @@ -155,7 +155,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { // 1. If the agent is a default agent, then we need to create a new agent just to associate with this conversation. // 2. If the agent is not a default agent, then we need to update the existing hidden agent. This will be associated using the `slug` field. // 3. If the agent is a "proper" agent and not a hidden agent, then it cannot be updated from this API. - // 4. The API is being called before the new conversation has been provisioned. If this happens, then create the agent and associate it with the conversation. Reroute the user to the new conversation page. + // 4. The API is being called before the new conversation has been provisioned. This is currently not supported. fetch(url, { method: mode, headers: { @@ -250,9 +250,19 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarGroup> <SidebarGroup key={"model"}> <SidebarGroupContent> - <SidebarGroupLabel>Model</SidebarGroupLabel> + <SidebarGroupLabel> + Model + { + !authenticatedData?.is_active && ( + <a href="/settings" className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"> + Upgrade + </a> + ) + } + </SidebarGroupLabel> <SidebarMenu className="p-0 m-0"> <SidebarMenuItem key={"model"} className="list-none"> + <ModelSelector disabled={!isEditable || !authenticatedData?.is_active} onSelect={(model, userModification) => handleModelSelect(model.name, userModification)} @@ -262,15 +272,15 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarMenu> </SidebarGroupContent> </SidebarGroup> - <Collapsible defaultOpen className="group/collapsible"> + <Popover defaultOpen={false}> <SidebarGroup> <SidebarGroupLabel asChild> - <CollapsibleTrigger> + <PopoverTrigger> Tools <CaretCircleDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" /> - </CollapsibleTrigger> + </PopoverTrigger> </SidebarGroupLabel> - <CollapsibleContent> + <PopoverContent> <SidebarGroupContent> <SidebarMenu className="p-1 m-0"> { @@ -342,9 +352,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarMenu> </SidebarGroupContent> - </CollapsibleContent> + </PopoverContent> </SidebarGroup> - </Collapsible> + </Popover> <SidebarGroup key={"files"}> <SidebarGroupContent> <SidebarGroupLabel>Files</SidebarGroupLabel> From 58879693f39259a205071f548297c52c8b12f367 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 18:12:50 -0800 Subject: [PATCH 30/43] Simplify nav menu and add a teams section --- .../web/app/components/navMenu/navMenu.tsx | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/interface/web/app/components/navMenu/navMenu.tsx b/src/interface/web/app/components/navMenu/navMenu.tsx index f45fe3c24..7f4c2c1e6 100644 --- a/src/interface/web/app/components/navMenu/navMenu.tsx +++ b/src/interface/web/app/components/navMenu/navMenu.tsx @@ -12,7 +12,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Moon, Sun, UserCircle, Question, ArrowRight, Code } from "@phosphor-icons/react"; +import { Moon, Sun, UserCircle, Question, ArrowRight, Code, BuildingOffice } from "@phosphor-icons/react"; import { useIsMobileWidth } from "@/app/common/utils"; import LoginPrompt from "../loginPrompt/loginPrompt"; import { Button } from "@/components/ui/button"; @@ -87,6 +87,24 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) { localStorage.setItem("theme", darkMode ? "dark" : "light"); } + const menuItems = [ + { + title: "Help", + icon: <Question className="w-6 h-6" />, + link: "https://docs.khoj.dev", + }, + { + title: "Releases", + icon: <Code className="w-6 h-6" />, + link: "/~https://github.com/khoj-ai/khoj/releases", + }, + { + title: "Organization", + icon: <BuildingOffice className="w-6 h-6" />, + link: "https://khoj.dev/teams", + }, + ] + return ( <SidebarMenu className="border-none p-0 m-0"> <SidebarMenuItem className="p-0 m-0"> @@ -147,26 +165,18 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) { </p> </div> </DropdownMenuItem> - <DropdownMenuItem> - <Link href="https://docs.khoj.dev" className="no-underline w-full"> - <div className="flex flex-rows"> - <Question className="w-6 h-6" /> - <p className="ml-3 font-semibold">Help</p> - </div> - </Link> - </DropdownMenuItem> - - <DropdownMenuItem> - <Link - href="/~https://github.com/khoj-ai/khoj/releases" - className="no-underline w-full" - > - <div className="flex flex-rows"> - <Code className="w-6 h-6" /> - <p className="ml-3 font-semibold">Releases</p> - </div> - </Link> - </DropdownMenuItem> + { + menuItems.map((menuItem, index) => ( + <DropdownMenuItem key={index}> + <Link href={menuItem.link} className="no-underline w-full"> + <div className="flex flex-rows"> + {menuItem.icon} + <p className="ml-3 font-semibold">{menuItem.title}</p> + </div> + </Link> + </DropdownMenuItem> + )) + } {!userData ? ( <DropdownMenuItem> <Button From b61226779e4c7e17dc11ff37f5aabab2574ad5e7 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 18:13:26 -0800 Subject: [PATCH 31/43] Simplify references section with icons in chat message --- .../referencePanel/referencePanel.tsx | 175 +++++++++++------- 1 file changed, 110 insertions(+), 65 deletions(-) diff --git a/src/interface/web/app/components/referencePanel/referencePanel.tsx b/src/interface/web/app/components/referencePanel/referencePanel.tsx index a907a2a9d..4419b2897 100644 --- a/src/interface/web/app/components/referencePanel/referencePanel.tsx +++ b/src/interface/web/app/components/referencePanel/referencePanel.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; -import { ArrowCircleDown, ArrowRight } from "@phosphor-icons/react"; +import { ArrowCircleDown, ArrowRight, Code, Note } from "@phosphor-icons/react"; import markdownIt from "markdown-it"; const md = new markdownIt({ @@ -239,10 +239,10 @@ function CodeContextReferenceCard(props: CodeContextReferenceCardProps) { </div> {(props.output_files?.length > 0 && renderOutputFiles(props.output_files?.slice(0, 1), true)) || ( - <pre className="text-xs border-t mt-1 pt-1 verflow-hidden line-clamp-10"> - {sanitizedCodeSnippet} - </pre> - )} + <pre className="text-xs border-t mt-1 pt-1 overflow-hidden line-clamp-10"> + {sanitizedCodeSnippet} + </pre> + )} </Card> </PopoverContent> </Popover> @@ -475,6 +475,63 @@ export function constructAllReferences( }; } +interface SimpleIconProps { + type: string; + link?: string; +} + +function SimpleIcon(props: SimpleIconProps) { + + let favicon = ``; + let domain = "unknown"; + + if (props.link) { + try { + domain = new URL(props.link).hostname; + favicon = `https://www.google.com/s2/favicons?domain=${domain}`; + } catch (error) { + console.warn(`Error parsing domain from link: ${props.link}`); + return null; + } + } + + let symbol = null; + + const itemClasses = "!w-4 !h-4 text-muted-foreground inline-flex mr-2 rounded-lg"; + + switch (props.type) { + case "code": + symbol = <Code className={`${itemClasses}`} />; + break; + case "online": + symbol = + <img + src={favicon} + alt="" + className={`${itemClasses}`} + />; + break; + case "notes": + symbol = + <Note className={`${itemClasses}`} />; + break; + default: + symbol = null; + } + + console.log("symbol", symbol); + + if (!symbol) { + return null; + } + + return ( + <div className="flex items-center gap-2"> + {symbol} + </div> + ); +} + export interface TeaserReferenceSectionProps { notesReferenceCardData: NotesContextReferenceData[]; onlineReferenceCardData: OnlineReferenceData[]; @@ -483,25 +540,6 @@ export interface TeaserReferenceSectionProps { } export function TeaserReferencesSection(props: TeaserReferenceSectionProps) { - const [numTeaserSlots, setNumTeaserSlots] = useState(3); - - useEffect(() => { - setNumTeaserSlots(props.isMobileWidth ? 1 : 3); - }, [props.isMobileWidth]); - - const codeDataToShow = props.codeReferenceCardData.slice(0, numTeaserSlots); - const notesDataToShow = props.notesReferenceCardData.slice( - 0, - numTeaserSlots - codeDataToShow.length, - ); - const onlineDataToShow = - notesDataToShow.length + codeDataToShow.length < numTeaserSlots - ? props.onlineReferenceCardData.slice( - 0, - numTeaserSlots - codeDataToShow.length - notesDataToShow.length, - ) - : []; - const shouldShowShowMoreButton = props.notesReferenceCardData.length > 0 || props.codeReferenceCardData.length > 0 || @@ -517,47 +555,20 @@ export function TeaserReferencesSection(props: TeaserReferenceSectionProps) { } return ( - <div className="pt-0 px-4 pb-4 md:px-6"> + <div className="pt-0 px-4 pb-4"> <h3 className="inline-flex items-center"> - References <p className="text-gray-400 m-2">{numReferences} sources</p> - </h3> - <div className={`flex flex-wrap gap-2 w-auto mt-2`}> - {codeDataToShow.map((code, index) => { - return ( - <CodeContextReferenceCard - showFullContent={false} - {...code} - key={`code-${index}`} + <div className={`flex flex-wrap gap-2 w-auto m-2`}> + {shouldShowShowMoreButton && ( + <ReferencePanel + notesReferenceCardData={props.notesReferenceCardData} + onlineReferenceCardData={props.onlineReferenceCardData} + codeReferenceCardData={props.codeReferenceCardData} + isMobileWidth={props.isMobileWidth} /> - ); - })} - {notesDataToShow.map((note, index) => { - return ( - <NotesContextReferenceCard - showFullContent={false} - {...note} - key={`${note.title}-${index}`} - /> - ); - })} - {onlineDataToShow.map((online, index) => { - return ( - <GenericOnlineReferenceCard - showFullContent={false} - {...online} - key={`${online.title}-${index}`} - /> - ); - })} - {shouldShowShowMoreButton && ( - <ReferencePanel - notesReferenceCardData={props.notesReferenceCardData} - onlineReferenceCardData={props.onlineReferenceCardData} - codeReferenceCardData={props.codeReferenceCardData} - /> - )} - </div> + )} + </div> + </h3> </div> ); } @@ -566,18 +577,52 @@ interface ReferencePanelDataProps { notesReferenceCardData: NotesContextReferenceData[]; onlineReferenceCardData: OnlineReferenceData[]; codeReferenceCardData: CodeReferenceData[]; + isMobileWidth: boolean; } export default function ReferencePanel(props: ReferencePanelDataProps) { + const [numTeaserSlots, setNumTeaserSlots] = useState(3); + + useEffect(() => { + setNumTeaserSlots(props.isMobileWidth ? 1 : 3); + }, [props.isMobileWidth]); + if (!props.notesReferenceCardData && !props.onlineReferenceCardData) { return null; } + const codeDataToShow = props.codeReferenceCardData.slice(0, numTeaserSlots); + const notesDataToShow = props.notesReferenceCardData.slice( + 0, + numTeaserSlots - codeDataToShow.length, + ); + const onlineDataToShow = + notesDataToShow.length + codeDataToShow.length < numTeaserSlots + ? props.onlineReferenceCardData.filter((online) => online.link).slice( + 0, + numTeaserSlots - codeDataToShow.length - notesDataToShow.length, + ) + : []; + return ( <Sheet> - <SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center !m-2 inline-flex"> - View references - <ArrowRight className="m-1" /> + <SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center m-0 inline-flex"> + {codeDataToShow.map((code, index) => { + return ( + <SimpleIcon type="code" key={`code-${index}`} /> + ); + })} + {notesDataToShow.map((note, index) => { + return ( + <SimpleIcon type="notes" key={`${note.title}-${index}`} /> + ); + })} + {onlineDataToShow.map((online, index) => { + return ( + <SimpleIcon type="online" key={`${online.title}-${index}`} link={online.link} /> + ); + })} + <ArrowRight className="m-0" /> </SheetTrigger> <SheetContent className="overflow-y-scroll"> <SheetHeader> From 272764d734789b8a87194167d635d320335465f6 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 21:17:56 -0800 Subject: [PATCH 32/43] Simplify the chat response / user message bubbles --- .../web/app/components/chatMessage/chatMessage.module.css | 1 - src/interface/web/app/components/chatMessage/chatMessage.tsx | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/components/chatMessage/chatMessage.module.css b/src/interface/web/app/components/chatMessage/chatMessage.module.css index 2abfdda78..475a9c095 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.module.css +++ b/src/interface/web/app/components/chatMessage/chatMessage.module.css @@ -25,7 +25,6 @@ div.chatMessageWrapper a span { } div.khojfullHistory { - border-width: 1px; padding-left: 4px; } diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx index 4b25346ec..8c76c5510 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.tsx +++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx @@ -559,7 +559,10 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) => } function constructClasses(chatMessage: SingleChatMessage) { - let classes = [styles.chatMessageContainer, "shadow-md"]; + let classes = [styles.chatMessageContainer]; + if (chatMessage.by === "khoj") { + classes.push("shadow-md"); + } classes.push(styles[chatMessage.by]); if (!chatMessage.message) { classes.push(styles.emptyChatMessage); From 59f0873232c0ce8914ce007bbc58872b1f629ba0 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 21:32:21 -0800 Subject: [PATCH 33/43] Rename train of thought button --- src/interface/web/app/components/chatHistory/chatHistory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index e85b60b93..14c796594 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -13,7 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { InlineLoading } from "../loading/loading"; -import { Lightbulb, ArrowDown, XCircle } from "@phosphor-icons/react"; +import { Lightbulb, ArrowDown, XCircle, CaretDown } from "@phosphor-icons/react"; import AgentProfileCard from "../profileCard/profileCard"; import { getIconFromIconName } from "@/app/common/iconUtils"; @@ -75,7 +75,7 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { variant="ghost" size="sm" > - What was my train of thought? + Thought Process <CaretDown size={16} className="ml-1" /> </Button> ) : ( <Button From 01faae0299529c0243b20ec9eaa05e87fcb354ab Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Tue, 28 Jan 2025 22:00:09 -0800 Subject: [PATCH 34/43] Simplify the train of thought UI --- .../web/app/components/chatHistory/chatHistory.module.css | 2 -- .../web/app/components/chatHistory/chatHistory.tsx | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.module.css b/src/interface/web/app/components/chatHistory/chatHistory.module.css index 417b7f6ea..d20d6be34 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.module.css +++ b/src/interface/web/app/components/chatHistory/chatHistory.module.css @@ -13,8 +13,6 @@ div.agentIndicator a { } div.trainOfThought { - border: 1px var(--border-color) solid; - border-radius: 16px; padding: 8px 16px; margin: 12px; } diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 14c796594..dfdffef8b 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -13,7 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { InlineLoading } from "../loading/loading"; -import { Lightbulb, ArrowDown, XCircle, CaretDown } from "@phosphor-icons/react"; +import { Lightbulb, ArrowDown, CaretDown, CaretUp } from "@phosphor-icons/react"; import AgentProfileCard from "../profileCard/profileCard"; import { getIconFromIconName } from "@/app/common/iconUtils"; @@ -63,7 +63,7 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { return ( <div - className={`${!collapsed ? styles.trainOfThought + " shadow-sm" : ""}`} + className={`${!collapsed ? styles.trainOfThought + " border" : ""} rounded-lg`} key={props.keyId} > {!props.completed && <InlineLoading className="float-right" />} @@ -84,8 +84,7 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { variant="ghost" size="sm" > - <XCircle size={16} className="mr-1" /> - Close + Close <CaretUp size={16} className="ml-1" /> </Button> ))} {!collapsed && From 0b2305d8f24dfdd831640b95a976a0dd5a484a70 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Wed, 29 Jan 2025 12:47:14 -0800 Subject: [PATCH 35/43] Add an animation to opening and closing the thought process --- .../components/chatHistory/chatHistory.tsx | 42 +++++++++++++++---- src/interface/web/package.json | 1 + src/interface/web/yarn.lock | 21 ++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index dfdffef8b..70e72f644 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -2,6 +2,7 @@ import styles from "./chatHistory.module.css"; import { useRef, useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import ChatMessage, { ChatHistoryData, @@ -55,6 +56,19 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { const lastIndex = props.trainOfThought.length - 1; const [collapsed, setCollapsed] = useState(props.completed); + const variants = { + open: { + height: "auto", + opacity: 1, + transition: { duration: 0.3, ease: "easeOut" } + }, + closed: { + height: 0, + opacity: 0, + transition: { duration: 0.3, ease: "easeIn" } + } + }; + useEffect(() => { if (props.completed) { setCollapsed(true); @@ -87,15 +101,25 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { Close <CaretUp size={16} className="ml-1" /> </Button> ))} - {!collapsed && - props.trainOfThought.map((train, index) => ( - <TrainOfThought - key={`train-${index}`} - message={train} - primary={index === lastIndex && props.lastMessage && !props.completed} - agentColor={props.agentColor} - /> - ))} + <AnimatePresence initial={false}> + {!collapsed && ( + <motion.div + initial="closed" + animate="open" + exit="closed" + variants={variants} + > + {props.trainOfThought.map((train, index) => ( + <TrainOfThought + key={`train-${index}`} + message={train} + primary={index === lastIndex && props.lastMessage && !props.completed} + agentColor={props.agentColor} + /> + ))} + </motion.div> + )} + </AnimatePresence> </div> ); } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index f0aac4af5..0b43c8ed5 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -53,6 +53,7 @@ "embla-carousel-react": "^8.5.1", "eslint": "^8", "eslint-config-next": "14.2.3", + "framer-motion": "^12.0.6", "input-otp": "^1.2.4", "intl-tel-input": "^23.8.1", "katex": "^0.16.21", diff --git a/src/interface/web/yarn.lock b/src/interface/web/yarn.lock index 0c8b8c2f0..73bbe077a 100644 --- a/src/interface/web/yarn.lock +++ b/src/interface/web/yarn.lock @@ -3121,6 +3121,15 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +framer-motion@^12.0.6: + version "12.0.6" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.0.6.tgz#4ec8b9225ead4d0d7d4ff00ade6e76a83e6d0c99" + integrity sha512-LmrXbXF6Vv5WCNmb+O/zn891VPZrH7XbsZgRLBROw6kFiP+iTK49gxTv2Ur3F0Tbw6+sy9BVtSqnWfMUpH+6nA== + dependencies: + motion-dom "^12.0.0" + motion-utils "^12.0.0" + tslib "^2.4.0" + fs-extra@^11.1.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -4167,6 +4176,18 @@ mlly@^1.7.3: pkg-types "^1.2.1" ufo "^1.5.4" +motion-dom@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.0.0.tgz#7045c63642eecbcc04c40b4457ebb07b3c2b3d0c" + integrity sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg== + dependencies: + motion-utils "^12.0.0" + +motion-utils@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.0.0.tgz#fabf79f4f1c818720a1b70f615e2a1768f396ac0" + integrity sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" From 58f77edcad7a0dcafb60bc366f262b8962d8ca0d Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Wed, 29 Jan 2025 12:47:36 -0800 Subject: [PATCH 36/43] Make conversation sessions rounded-lg --- .../web/app/components/allConversations/allConversations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface/web/app/components/allConversations/allConversations.tsx b/src/interface/web/app/components/allConversations/allConversations.tsx index 41361bc5a..fcfa45fe0 100644 --- a/src/interface/web/app/components/allConversations/allConversations.tsx +++ b/src/interface/web/app/components/allConversations/allConversations.tsx @@ -696,7 +696,7 @@ function ChatSession(props: ChatHistory) { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} key={props.conversation_id} - className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""} m-1`} + className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""} m-1 rounded-lg`} > <SidebarMenuButton asChild> <Link From e2bfd4ac0f9f95cf91e6aa2cea1b34a512f52ee9 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Wed, 29 Jan 2025 12:52:10 -0800 Subject: [PATCH 37/43] Change name of teams section to teams --- src/interface/web/app/components/navMenu/navMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface/web/app/components/navMenu/navMenu.tsx b/src/interface/web/app/components/navMenu/navMenu.tsx index 7f4c2c1e6..583ffdadc 100644 --- a/src/interface/web/app/components/navMenu/navMenu.tsx +++ b/src/interface/web/app/components/navMenu/navMenu.tsx @@ -99,7 +99,7 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) { link: "/~https://github.com/khoj-ai/khoj/releases", }, { - title: "Organization", + title: "Teams", icon: <BuildingOffice className="w-6 h-6" />, link: "https://khoj.dev/teams", }, From b5f99dd103b14622e9ffb5cca027ea8f629abf0f Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Wed, 29 Jan 2025 15:06:22 -0800 Subject: [PATCH 38/43] Rename custom instructions -> instructions --- src/interface/web/app/components/chatSidebar/chatSidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index 397a5d493..d9c2fcab4 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -235,11 +235,11 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { </SidebarGroup> <SidebarGroup key={"instructions"}> <SidebarGroupContent> - <SidebarGroupLabel>Custom Instructions</SidebarGroupLabel> + <SidebarGroupLabel>Instructions</SidebarGroupLabel> <SidebarMenu className="p-0 m-0"> <SidebarMenuItem className="list-none"> <Textarea - className="w-full h-32" + className="w-full h-32 resize-none hover:resize-y" value={customPrompt} onChange={(e) => handleCustomPromptChange(e.target.value)} readOnly={!isEditable} From 98e3f5162a361f68d9b5212b5fe14edc235eda51 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Wed, 29 Jan 2025 21:49:33 -0800 Subject: [PATCH 39/43] Show generated diagram raw code if fails rendering --- .../web/app/components/mermaid/mermaid.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/interface/web/app/components/mermaid/mermaid.tsx b/src/interface/web/app/components/mermaid/mermaid.tsx index d73a44476..bdbbe6a3e 100644 --- a/src/interface/web/app/components/mermaid/mermaid.tsx +++ b/src/interface/web/app/components/mermaid/mermaid.tsx @@ -141,10 +141,17 @@ const Mermaid: React.FC<MermaidProps> = ({ chart }) => { return ( <div> {mermaidError ? ( - <div className="flex items-center gap-2 bg-red-100 border border-red-500 rounded-md p-3 mt-3 text-red-900 text-sm"> - <Info className="w-12 h-12" /> - <span>Error rendering diagram: {mermaidError}</span> - </div> + <> + <div className="flex items-center gap-2 bg-red-100 border border-red-500 rounded-md p-1 mt-3 text-red-900 text-sm"> + <Info className="w-12 h-12" /> + <span>{mermaidError}</span> + </div> + <code className="block bg-secondary text-secondary-foreground p-4 mt-3 rounded-lg font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-[400px] border border-gray-200"> + { + chart + } + </code> + </> ) : ( <div id={mermaidId} From 641f1bcd9167c2e8b497551cafe1d5b1c1f3faf7 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Thu, 30 Jan 2025 16:07:27 -0800 Subject: [PATCH 40/43] Only open the side bar automatically when there is no chat history && no pending messages. --- .../web/app/components/chatHistory/chatHistory.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 70e72f644..67af2ddb9 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -142,6 +142,7 @@ export default function ChatHistory(props: ChatHistoryProps) { const isMobileWidth = useIsMobileWidth(); const scrollAreaSelector = "[data-radix-scroll-area-viewport]"; const fetchMessageCount = 10; + const hasStartingMessage = localStorage.getItem("message"); useEffect(() => { const scrollAreaEl = scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector); @@ -277,7 +278,9 @@ export default function ChatHistory(props: ChatHistoryProps) { props.setAgent(chatData.response.agent); setData(chatMetadata); if (props.setIsChatSideBarOpen) { - props.setIsChatSideBarOpen(true); + if (!hasStartingMessage) { + props.setIsChatSideBarOpen(true); + } } } @@ -469,7 +472,7 @@ export default function ChatHistory(props: ChatHistoryProps) { onDeleteMessage={handleDeleteMessage} customClassName="fullHistory" borderLeftColor={`${data?.agent?.color}-500`} - isLastMessage={true} + isLastMessage={index === (props.incomingMessages!.length - 1)} /> </React.Fragment> ); From 0645af9b168ed5a0461beac43add257b7ded9e79 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sat, 1 Feb 2025 13:06:42 -0800 Subject: [PATCH 41/43] Use the agent CM as a backup chat model when available / necessary. Remove automation as agent option. --- src/khoj/database/adapters/__init__.py | 14 ++++-- .../0085_alter_agent_output_modes.py | 24 +++++++++++ src/khoj/database/models/__init__.py | 1 - src/khoj/routers/helpers.py | 43 +++++++++++++++++-- src/khoj/utils/helpers.py | 1 - 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/khoj/database/migrations/0085_alter_agent_output_modes.py diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index e50ea29ab..953171db4 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -1008,7 +1008,9 @@ async def aget_conversation_by_user( if create_new: return await ConversationAdapters.acreate_conversation_session(user, client_application) - query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent") + query = Conversation.objects.filter(user=user, client=client_application).prefetch_related( + "agent", "agent__chat_model" + ) if conversation_id: return await query.filter(id=conversation_id).afirst() @@ -1017,7 +1019,7 @@ async def aget_conversation_by_user( conversation = await query.order_by("-updated_at").afirst() - return conversation or await Conversation.objects.prefetch_related("agent").acreate( + return conversation or await Conversation.objects.prefetch_related("agent", "agent__chat_model").acreate( user=user, client=client_application ) @@ -1147,7 +1149,7 @@ def get_default_chat_model(user: KhojUser = None): return ChatModel.objects.filter().first() @staticmethod - async def aget_default_chat_model(user: KhojUser = None): + async def aget_default_chat_model(user: KhojUser = None, fallback_chat_model: Optional[ChatModel] = None): """Get default conversation config. Prefer chat model by server admin > user > first created chat model""" # Get the server chat settings server_chat_settings: ServerChatSettings = ( @@ -1167,12 +1169,18 @@ async def aget_default_chat_model(user: KhojUser = None): if server_chat_settings.chat_default: return server_chat_settings.chat_default + # Revert to an explicit fallback model if the server chat settings are not set + if fallback_chat_model: + # The chat model may not be full loaded from the db, so explicitly load it here + return await ChatModel.objects.filter(id=fallback_chat_model.id).prefetch_related("ai_model_api").afirst() + # Get the user's chat settings, if the server chat settings are not set user_chat_settings = ( (await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__ai_model_api").afirst()) if user else None ) + if user_chat_settings is not None and user_chat_settings.setting is not None: return user_chat_settings.setting diff --git a/src/khoj/database/migrations/0085_alter_agent_output_modes.py b/src/khoj/database/migrations/0085_alter_agent_output_modes.py new file mode 100644 index 000000000..ccf88c286 --- /dev/null +++ b/src/khoj/database/migrations/0085_alter_agent_output_modes.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.10 on 2025-02-01 20:10 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0084_alter_agent_input_tools"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="output_modes", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(choices=[("image", "Image"), ("diagram", "Diagram")], max_length=200), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 51bc8e27d..f366c15a4 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -286,7 +286,6 @@ class InputToolOptions(models.TextChoices): class OutputModeOptions(models.TextChoices): # These map to various ConversationCommand types IMAGE = "image" - AUTOMATION = "automation" DIAGRAM = "diagram" creator = models.ForeignKey( diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 8339323ee..3c65bce3a 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -393,12 +393,15 @@ async def aget_data_sources_and_output_format( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Infer information sources to refer", logger): response = await send_message_to_model_wrapper( relevant_tools_prompt, response_type="json_object", user=user, query_files=query_files, + agent_chat_model=agent_chat_model, tracer=tracer, ) @@ -472,6 +475,8 @@ async def infer_webpage_urls( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Infer webpage urls to read", logger): response = await send_message_to_model_wrapper( online_queries_prompt, @@ -479,6 +484,7 @@ async def infer_webpage_urls( response_type="json_object", user=user, query_files=query_files, + agent_chat_model=agent_chat_model, tracer=tracer, ) @@ -528,6 +534,8 @@ async def generate_online_subqueries( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate online search subqueries", logger): response = await send_message_to_model_wrapper( online_queries_prompt, @@ -535,6 +543,7 @@ async def generate_online_subqueries( response_type="json_object", user=user, query_files=query_files, + agent_chat_model=agent_chat_model, tracer=tracer, ) @@ -628,10 +637,13 @@ async def extract_relevant_info( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + response = await send_message_to_model_wrapper( extract_relevant_information, prompts.system_prompt_extract_relevant_information, user=user, + agent_chat_model=agent_chat_model, tracer=tracer, ) return response.strip() @@ -666,12 +678,15 @@ async def extract_relevant_summary( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Extract relevant information from data", logger): response = await send_message_to_model_wrapper( extract_relevant_information, prompts.system_prompt_extract_relevant_summary, user=user, query_images=query_images, + agent_chat_model=agent_chat_model, tracer=tracer, ) return response.strip() @@ -834,12 +849,15 @@ async def generate_better_diagram_description( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate better diagram description", logger): response = await send_message_to_model_wrapper( improve_diagram_description_prompt, query_images=query_images, user=user, query_files=query_files, + agent_chat_model=agent_chat_model, tracer=tracer, ) response = response.strip() @@ -864,9 +882,11 @@ async def generate_excalidraw_diagram_from_description( query=q, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate excalidraw diagram", logger): raw_response = await send_message_to_model_wrapper( - query=excalidraw_diagram_generation, user=user, tracer=tracer + query=excalidraw_diagram_generation, user=user, agent_chat_model=agent_chat_model, tracer=tracer ) raw_response = clean_json(raw_response) try: @@ -980,12 +1000,15 @@ async def generate_better_mermaidjs_diagram_description( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate better Mermaid.js diagram description", logger): response = await send_message_to_model_wrapper( improve_diagram_description_prompt, query_images=query_images, user=user, query_files=query_files, + agent_chat_model=agent_chat_model, tracer=tracer, ) response = response.strip() @@ -1010,8 +1033,12 @@ async def generate_mermaidjs_diagram_from_description( query=q, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate Mermaid.js diagram", logger): - raw_response = await send_message_to_model_wrapper(query=mermaidjs_diagram_generation, user=user, tracer=tracer) + raw_response = await send_message_to_model_wrapper( + query=mermaidjs_diagram_generation, user=user, agent_chat_model=agent_chat_model, tracer=tracer + ) return clean_mermaidjs(raw_response.strip()) @@ -1072,9 +1099,16 @@ async def generate_better_image_prompt( personality_context=personality_context, ) + agent_chat_model = agent.chat_model if agent else None + with timer("Chat actor: Generate contextual image prompt", logger): response = await send_message_to_model_wrapper( - image_prompt, query_images=query_images, user=user, query_files=query_files, tracer=tracer + image_prompt, + query_images=query_images, + user=user, + query_files=query_files, + agent_chat_model=agent_chat_model, + tracer=tracer, ) response = response.strip() if response.startswith(('"', "'")) and response.endswith(('"', "'")): @@ -1091,9 +1125,10 @@ async def send_message_to_model_wrapper( query_images: List[str] = None, context: str = "", query_files: str = None, + agent_chat_model: ChatModel = None, tracer: dict = {}, ): - chat_model: ChatModel = await ConversationAdapters.aget_default_chat_model(user) + chat_model: ChatModel = await ConversationAdapters.aget_default_chat_model(user, agent_chat_model) vision_available = chat_model.vision_enabled if not vision_available and query_images: logger.warning(f"Vision is not enabled for default model: {chat_model.name}.") diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 322906572..b48436c64 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -386,7 +386,6 @@ class ConversationCommand(str, Enum): mode_descriptions_for_agent = { ConversationCommand.Image: "Agent can generate images in response. It cannot not use this to generate charts and graphs.", - ConversationCommand.Automation: "Agent can schedule a task to run at a scheduled date, time and frequency in response.", ConversationCommand.Diagram: "Agent can generate a visual representation that requires primitives like lines, rectangles, and text.", } From f3b2580649634a1982f33e6e0372ba8981a07bc4 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sat, 1 Feb 2025 14:03:17 -0800 Subject: [PATCH 42/43] Add back hover state with collapsed references --- .../referencePanel/referencePanel.tsx | 139 +++++++++++------- 1 file changed, 83 insertions(+), 56 deletions(-) diff --git a/src/interface/web/app/components/referencePanel/referencePanel.tsx b/src/interface/web/app/components/referencePanel/referencePanel.tsx index 4419b2897..1dd9c92b2 100644 --- a/src/interface/web/app/components/referencePanel/referencePanel.tsx +++ b/src/interface/web/app/components/referencePanel/referencePanel.tsx @@ -31,6 +31,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import DOMPurify from "dompurify"; import { getIconFromFilename } from "@/app/common/iconUtils"; +import Link from "next/link"; interface NotesContextReferenceData { title: string; @@ -68,18 +69,25 @@ function NotesContextReferenceCard(props: NotesContextReferenceCardProps) { <Card onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} - className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words text-balance rounded-lg border-none p-2 bg-muted`} + className={`${props.showFullContent ? "w-auto bg-muted" : "w-auto"} overflow-hidden break-words text-balance rounded-lg border-none p-2`} > - <h3 - className={`${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground}`} - > - {fileIcon} - {props.showFullContent ? props.title : fileName} - </h3> - <p - className={`text-sm ${props.showFullContent ? "overflow-x-auto block" : "overflow-hidden line-clamp-2"}`} - dangerouslySetInnerHTML={{ __html: snippet }} - ></p> + { + !props.showFullContent ? + <SimpleIcon type="notes" key={`${props.title}`} /> + : + <> + <h3 + className={`${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground}`} + > + {fileIcon} + {props.showFullContent ? props.title : fileName} + </h3> + <p + className={`text-sm overflow-x-auto block`} + dangerouslySetInnerHTML={{ __html: snippet }} + ></p> + </> + } </Card> </PopoverTrigger> <PopoverContent className="w-[400px] mx-2"> @@ -204,25 +212,30 @@ function CodeContextReferenceCard(props: CodeContextReferenceCardProps) { <Card onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} - className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words text-balance rounded-lg border-none p-2 bg-muted`} + className={`${props.showFullContent ? "w-auto bg-muted" : "w-auto"} overflow-hidden break-words text-balance rounded-lg border-none p-2`} > - <div className="flex flex-col px-1"> - <div className="flex items-center gap-2"> - {fileIcon} - <h3 - className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground flex-grow`} - > - code {props.output_files?.length > 0 ? "artifacts" : ""} - </h3> - </div> - <pre - className={`text-xs pb-2 ${props.showFullContent ? "block overflow-x-auto" : props.output_files?.length > 0 ? "hidden" : "overflow-hidden line-clamp-3"}`} - > - {sanitizedCodeSnippet} - </pre> - {props.output_files?.length > 0 && - renderOutputFiles(props.output_files, false)} - </div> + { + !props.showFullContent ? + <SimpleIcon type="code" key={`code-${props.code}`} /> + : + <div className="flex flex-col px-1"> + <div className="flex items-center gap-2"> + {fileIcon} + <h3 + className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground flex-grow`} + > + code {props.output_files?.length > 0 ? "artifacts" : ""} + </h3> + </div> + <pre + className={`text-xs pb-2 ${props.showFullContent ? "block overflow-x-auto" : props.output_files?.length > 0 ? "hidden" : "overflow-hidden line-clamp-3"}`} + > + {sanitizedCodeSnippet} + </pre> + {props.output_files?.length > 0 && + renderOutputFiles(props.output_files, false)} + </div> + } </Card> </PopoverTrigger> <PopoverContent className="w-[400px] mx-2"> @@ -299,35 +312,37 @@ function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) { <Card onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words text-balance rounded-lg border-none p-2 bg-muted`} + className={`${props.showFullContent ? "w-auto bg-muted" : "w-auto"} overflow-hidden break-words text-balance rounded-lg border-none p-2`} > - <div className="flex flex-col"> - <a - href={props.link} - target="_blank" - rel="noreferrer" - className="!no-underline px-1" - > - <div className="flex items-center gap-2"> - <img src={favicon} alt="" className="!w-4 h-4 flex-shrink-0" /> + { + !props.showFullContent ? + <SimpleIcon type="online" key={props.title} link={props.link} /> + : + <div className="flex flex-col"> + { + <Link href={props.link}> + <div className="flex items-center gap-2"> + <img src={favicon} alt="" className="!w-4 h-4 flex-shrink-0" /> + <h3 + className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground flex-grow`} + > + {domain} + </h3> + </div> + </Link> + } <h3 - className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground flex-grow`} + className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} font-bold`} > - {domain} + {props.title} </h3> + <p + className={`overflow-hidden text-sm ${props.showFullContent ? "block" : "line-clamp-2"}`} + > + {props.description} + </p> </div> - <h3 - className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} font-bold`} - > - {props.title} - </h3> - <p - className={`overflow-hidden text-sm ${props.showFullContent ? "block" : "line-clamp-2"}`} - > - {props.description} - </p> - </a> - </div> + } </Card> </PopoverTrigger> <PopoverContent className="w-[400px] mx-2"> @@ -609,17 +624,29 @@ export default function ReferencePanel(props: ReferencePanelDataProps) { <SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center m-0 inline-flex"> {codeDataToShow.map((code, index) => { return ( - <SimpleIcon type="code" key={`code-${index}`} /> + <CodeContextReferenceCard + showFullContent={false} + {...code} + key={`code-${index}`} + /> ); })} {notesDataToShow.map((note, index) => { return ( - <SimpleIcon type="notes" key={`${note.title}-${index}`} /> + <NotesContextReferenceCard + showFullContent={false} + {...note} + key={`${note.title}-${index}`} + /> ); })} {onlineDataToShow.map((online, index) => { return ( - <SimpleIcon type="online" key={`${online.title}-${index}`} link={online.link} /> + <GenericOnlineReferenceCard + showFullContent={false} + {...online} + key={`${online.title}-${index}`} + /> ); })} <ArrowRight className="m-0" /> From 08bc1d3bcb499abaf9590b77a61760680f73d118 Mon Sep 17 00:00:00 2001 From: sabaimran <narmiabas@gmail.com> Date: Sat, 1 Feb 2025 14:25:45 -0800 Subject: [PATCH 43/43] Improve sizing / spacing of side bar --- src/interface/web/app/components/chatSidebar/chatSidebar.tsx | 4 ++-- .../web/app/components/referencePanel/referencePanel.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx index d9c2fcab4..e08a1c0c7 100644 --- a/src/interface/web/app/components/chatSidebar/chatSidebar.tsx +++ b/src/interface/web/app/components/chatSidebar/chatSidebar.tsx @@ -203,14 +203,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) { <SidebarHeader> { agentData && !isEditable ? ( - <div className="flex items-center relative top-2"> + <div className="flex items-center relative text-sm"> <a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}> {getIconFromIconName(agentData.icon, agentData.color)} {agentData.name} </a> </div> ) : ( - <div className="flex items-center relative top-2"> + <div className="flex items-center relative text-sm"> {getIconFromIconName("lightbulb", "orange")} Chat Options </div> diff --git a/src/interface/web/app/components/referencePanel/referencePanel.tsx b/src/interface/web/app/components/referencePanel/referencePanel.tsx index 1dd9c92b2..9f07191f3 100644 --- a/src/interface/web/app/components/referencePanel/referencePanel.tsx +++ b/src/interface/web/app/components/referencePanel/referencePanel.tsx @@ -572,7 +572,7 @@ export function TeaserReferencesSection(props: TeaserReferenceSectionProps) { return ( <div className="pt-0 px-4 pb-4"> <h3 className="inline-flex items-center"> - <p className="text-gray-400 m-2">{numReferences} sources</p> + <div className="text-gray-400 m-2">{numReferences} sources</div> <div className={`flex flex-wrap gap-2 w-auto m-2`}> {shouldShowShowMoreButton && ( <ReferencePanel @@ -621,7 +621,7 @@ export default function ReferencePanel(props: ReferencePanelDataProps) { return ( <Sheet> - <SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center m-0 inline-flex"> + <SheetTrigger className="text-balance w-auto justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center m-0 inline-flex"> {codeDataToShow.map((code, index) => { return ( <CodeContextReferenceCard