diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx
index 8ea08041..b03d248c 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 c9868429..58a2a9bf 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 824a7e4c..7c96e2ad 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}
/>
-
+
);
}
diff --git a/src/interface/web/app/common/auth.ts b/src/interface/web/app/common/auth.ts
index 738d273f..e00caa25 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(`/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 00000000..ae0e6d4f
--- /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(undefined);
+ const [selectedModel, setSelectedModel] = useState(undefined);
+
+ useEffect(() => {
+ if (models && models.length > 0) {
+ setSelectedModel(models[0])
+ }
+
+ if (models && models.length > 0 && !selectedModel) {
+ setSelectedModel(models[0])
+ }
+
+ }, [models]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+ {error.message}
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
{peekedModel?.name}
+
+ {peekedModel?.description}
+
+ {peekedModel?.strengths ? (
+
+
+ Strengths
+
+
+ {peekedModel.strengths}
+
+
+ ) : null}
+
+
+
+
+
+
+
+ No Models found.
+
+ {models && models.length > 0 && models
+ .map((model) => (
+ setPeekedModel(model)}
+ onSelect={() => {
+ setSelectedModel(model)
+ setOpen(false)
+ }}
+ />
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+interface ModelItemProps {
+ model: ModelOptions,
+ isSelected: boolean,
+ onSelect: () => void,
+ onPeek: (model: ModelOptions) => void
+}
+
+function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
+ const ref = React.useRef(null)
+
+ useMutationObserver(ref, (mutations) => {
+ mutations.forEach((mutation) => {
+ if (
+ mutation.type === "attributes" &&
+ mutation.attributeName === "aria-selected" &&
+ ref.current?.getAttribute("aria-selected") === "true"
+ ) {
+ onPeek(model)
+ }
+ })
+ })
+
+ return (
+
+ {model.name}
+
+
+ )
+}
diff --git a/src/interface/web/app/common/utils.ts b/src/interface/web/app/common/utils.ts
index 8bf6db84..fe5043a2 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,
+ 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 53ad20bf..94d05eba 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("/api/content/computer", fetcher);
const { data: selectedFiles, error: selectedFilesError } = useSWR(
@@ -981,13 +981,6 @@ export default function AllConversations(props: SidePanelProps) {
sideBarOpen={props.sideBarOpen}
/>
- {props.sideBarOpen && (
-
- )}
>
)}
diff --git a/src/interface/web/app/components/appSidebar/appSidebar.tsx b/src/interface/web/app/components/appSidebar/appSidebar.tsx
index d80314b9..97b7664a 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) {
+
);
}
diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx
index 829211be..f0b46e1e 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 (
-
+
+
@@ -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 00000000..9d3588e0
--- /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
("/api/agents/options", fetcher);
+
+ if (props.isMobileWidth) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+
+function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
+ const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
+ useSWR("/api/agents/options", fetcher);
+
+ return (
+
+
+
+ Chat Options
+
+
+
+
+
+ Custom Instructions
+
+
+
+
+ Model
+
+
+
+
+
+ Input Tools
+
+
+
+
+
+
+ Input Tool Options
+
+ {
+ Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => {
+ return (
+ { }}
+ >
+ {key}
+
+ );
+ }
+ )
+ }
+
+
+
+
+
+ Output Tools
+
+
+
+
+
+
+ Output Tool Options
+
+ {
+ Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => {
+ return (
+ { }}
+ >
+ {key}
+
+ );
+ }
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/interface/web/app/share/chat/sharedChat.module.css b/src/interface/web/app/share/chat/sharedChat.module.css
index 286e5ba4..844834f3 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 00000000..e54d91cf
--- /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,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+))
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/src/interface/web/package.json b/src/interface/web/package.json
index 5aa04ff4..f899a0c2 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 07bd0754..15391ae0 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 14aec059..22cac901 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==