mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 21:29:11 +00:00
Show all agents, smart sorted, in carousel on home screen of web app (#943)
## Overview Allow quickly selecting, switching agents from agents pane on home page of web app ## Details - Show all agents in carousel on home screen agent pane of web app - Smart Sort 1. Pin default agent as first for ease of access 2. Show used agents by MRU for ease of access 3. Shuffle unused agents for discoverability - Select most recently used agent to chat with by default - Push smart sort logic down to API - Common logic can be reused across clients - Agent sort was previously done in web app - Focus on chat input on agent select - Double click agent on home page to open edit agent card on agents page
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ import "katex/dist/katex.min.css";
|
|||||||
|
|
||||||
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
|
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
|
||||||
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/utils";
|
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/utils";
|
||||||
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
|
import { ChatInputArea, ChatOptions } from "../components/chatInputArea/chatInputArea";
|
||||||
import { useAuthenticatedData } from "../common/auth";
|
import { useAuthenticatedData } from "../common/auth";
|
||||||
import { AgentData } from "../agents/page";
|
import { AgentData } from "../agents/page";
|
||||||
|
|
||||||
|
|||||||
@@ -93,3 +93,15 @@ export function useUserConfig(detailed: boolean = false) {
|
|||||||
|
|
||||||
return { userConfig, isLoadingUserConfig };
|
return { userConfig, isLoadingUserConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUserSubscribed(userConfig: UserConfig | null): boolean {
|
||||||
|
return (
|
||||||
|
(userConfig?.subscription_state &&
|
||||||
|
[
|
||||||
|
SubscriptionStates.SUBSCRIBED.valueOf(),
|
||||||
|
SubscriptionStates.TRIAL.valueOf(),
|
||||||
|
SubscriptionStates.UNSUBSCRIBED.valueOf(),
|
||||||
|
].includes(userConfig.subscription_state)) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,19 @@ export function useIsMobileWidth() {
|
|||||||
|
|
||||||
return isMobileWidth;
|
return isMobileWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.agentPersonality p {
|
||||||
|
white-space: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 77px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentPersonality {
|
||||||
|
text-align: left;
|
||||||
|
grid-column: span 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.infoButton {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent !important;
|
||||||
|
text-align: left;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
1297
src/interface/web/app/components/agentCard/agentCard.tsx
Normal file
1297
src/interface/web/app/components/agentCard/agentCard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import styles from "./chatInputArea.module.css";
|
import styles from "./chatInputArea.module.css";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState, forwardRef } from "react";
|
||||||
|
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
@@ -52,7 +52,7 @@ interface ChatInputProps {
|
|||||||
agentColor?: string;
|
agentColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatInputArea(props: ChatInputProps) {
|
export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((props, ref) => {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -264,9 +264,9 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||||||
}
|
}
|
||||||
}, [recording, mediaRecorder]);
|
}, [recording, mediaRecorder]);
|
||||||
|
|
||||||
const chatInputRef = useRef<HTMLTextAreaElement>(null);
|
const chatInputRef = ref as React.MutableRefObject<HTMLTextAreaElement>;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatInputRef.current) return;
|
if (!chatInputRef?.current) return;
|
||||||
chatInputRef.current.style.height = "auto";
|
chatInputRef.current.style.height = "auto";
|
||||||
chatInputRef.current.style.height =
|
chatInputRef.current.style.height =
|
||||||
Math.max(chatInputRef.current.scrollHeight - 24, 64) + "px";
|
Math.max(chatInputRef.current.scrollHeight - 24, 64) + "px";
|
||||||
@@ -522,4 +522,6 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ChatInputArea.displayName = "ChatInputArea";
|
||||||
|
|||||||
@@ -3,26 +3,33 @@ import "./globals.css";
|
|||||||
import styles from "./page.module.css";
|
import styles from "./page.module.css";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import Image from "next/image";
|
|
||||||
import { ArrowCounterClockwise } from "@phosphor-icons/react";
|
import { ArrowCounterClockwise } from "@phosphor-icons/react";
|
||||||
|
|
||||||
import { Card, CardTitle } from "@/components/ui/card";
|
import { Card, CardTitle } from "@/components/ui/card";
|
||||||
import SuggestionCard from "@/app/components/suggestions/suggestionCard";
|
import SuggestionCard from "@/app/components/suggestions/suggestionCard";
|
||||||
import SidePanel from "@/app/components/sidePanel/chatHistorySidePanel";
|
import SidePanel from "@/app/components/sidePanel/chatHistorySidePanel";
|
||||||
import Loading from "@/app/components/loading/loading";
|
import Loading from "@/app/components/loading/loading";
|
||||||
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
import { ChatInputArea, ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
||||||
import { Suggestion, suggestionsData } from "@/app/components/suggestions/suggestionsData";
|
import { Suggestion, suggestionsData } from "@/app/components/suggestions/suggestionsData";
|
||||||
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
|
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
|
||||||
|
|
||||||
import { useAuthenticatedData, UserConfig, useUserConfig } from "@/app/common/auth";
|
import {
|
||||||
|
isUserSubscribed,
|
||||||
|
useAuthenticatedData,
|
||||||
|
UserConfig,
|
||||||
|
useUserConfig,
|
||||||
|
} from "@/app/common/auth";
|
||||||
import { convertColorToBorderClass } from "@/app/common/colorUtils";
|
import { convertColorToBorderClass } from "@/app/common/colorUtils";
|
||||||
import { getIconFromIconName } from "@/app/common/iconUtils";
|
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||||
import { AgentData } from "@/app/agents/page";
|
import { AgentData } from "@/app/agents/page";
|
||||||
import { createNewConversation } from "./common/chatFunctions";
|
import { createNewConversation } from "./common/chatFunctions";
|
||||||
import { useIsMobileWidth } from "./common/utils";
|
import { useDebounce, useIsMobileWidth } from "./common/utils";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
|
import { AgentCard } from "@/app/components/agentCard/agentCard";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
|
||||||
interface ChatBodyDataProps {
|
interface ChatBodyDataProps {
|
||||||
chatOptionsData: ChatOptions | null;
|
chatOptionsData: ChatOptions | null;
|
||||||
@@ -48,10 +55,15 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
const [processingMessage, setProcessingMessage] = useState(false);
|
const [processingMessage, setProcessingMessage] = useState(false);
|
||||||
const [greeting, setGreeting] = useState("");
|
const [greeting, setGreeting] = useState("");
|
||||||
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
|
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
|
||||||
|
const [hoveredAgent, setHoveredAgent] = useState<string | null>(null);
|
||||||
|
const debouncedHoveredAgent = useDebounce(hoveredAgent, 500);
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
|
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
|
||||||
const [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
|
const [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
|
||||||
const [agents, setAgents] = useState<AgentData[]>([]);
|
const [agents, setAgents] = useState<AgentData[]>([]);
|
||||||
|
const chatInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const queryParam = searchParams.get("q");
|
const queryParam = searchParams.get("q");
|
||||||
|
|
||||||
@@ -61,6 +73,12 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
}
|
}
|
||||||
}, [queryParam]);
|
}, [queryParam]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedHoveredAgent) {
|
||||||
|
setIsPopoverOpen(true);
|
||||||
|
}
|
||||||
|
}, [debouncedHoveredAgent]);
|
||||||
|
|
||||||
const onConversationIdChange = props.onConversationIdChange;
|
const onConversationIdChange = props.onConversationIdChange;
|
||||||
|
|
||||||
const agentsFetcher = () =>
|
const agentsFetcher = () =>
|
||||||
@@ -72,6 +90,10 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openAgentEditCard = (agentSlug: string) => {
|
||||||
|
router.push(`/agents?agent=${agentSlug}`);
|
||||||
|
};
|
||||||
|
|
||||||
function shuffleAndSetOptions() {
|
function shuffleAndSetOptions() {
|
||||||
const shuffled = FisherYatesShuffle(suggestionsData);
|
const shuffled = FisherYatesShuffle(suggestionsData);
|
||||||
setShuffledOptions(shuffled.slice(0, 3));
|
setShuffledOptions(shuffled.slice(0, 3));
|
||||||
@@ -108,22 +130,13 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
}, [props.chatOptionsData]);
|
}, [props.chatOptionsData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nSlice = props.isMobileWidth ? 2 : 4;
|
const agents = (agentsData || []).filter((agent) => agent !== null && agent !== undefined);
|
||||||
const shuffledAgents = agentsData ? [...agentsData].sort(() => 0.5 - Math.random()) : [];
|
|
||||||
const agents = agentsData ? [agentsData[0]] : []; // Always add the first/default agent.
|
|
||||||
|
|
||||||
shuffledAgents.slice(0, nSlice - 1).forEach((agent) => {
|
|
||||||
if (!agents.find((a) => a.slug === agent.slug)) {
|
|
||||||
agents.push(agent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setAgents(agents);
|
setAgents(agents);
|
||||||
|
// set the selected agent to the most recently used agent, first agent is always khoj
|
||||||
|
setSelectedAgent(agents.length > 1 ? agents[1].slug : "khoj");
|
||||||
|
|
||||||
//generate colored icons for the selected agents
|
// generate colored icons for the available agents
|
||||||
const agentIcons = agents
|
const agentIcons = agents.map((agent) => getIconFromIconName(agent.icon, agent.color)!);
|
||||||
.filter((agent) => agent !== null && agent !== undefined)
|
|
||||||
.map((agent) => getIconFromIconName(agent.icon, agent.color)!);
|
|
||||||
setAgentIcons(agentIcons);
|
setAgentIcons(agentIcons);
|
||||||
}, [agentsData, props.isMobileWidth]);
|
}, [agentsData, props.isMobileWidth]);
|
||||||
|
|
||||||
@@ -157,6 +170,20 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
}
|
}
|
||||||
}, [selectedAgent, message, processingMessage, onConversationIdChange]);
|
}, [selectedAgent, message, processingMessage, onConversationIdChange]);
|
||||||
|
|
||||||
|
// Close the agent detail hover card when scroll on agent pane
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollAreaSelector = "[data-radix-scroll-area-viewport]";
|
||||||
|
const scrollAreaEl = document.querySelector<HTMLElement>(scrollAreaSelector);
|
||||||
|
const handleScroll = () => {
|
||||||
|
setHoveredAgent(null);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollAreaEl?.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => scrollAreaEl?.removeEventListener("scroll", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
function fillArea(link: string, type: string, prompt: string) {
|
function fillArea(link: string, type: string, prompt: string) {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
let message_str = "";
|
let message_str = "";
|
||||||
@@ -195,34 +222,73 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{!props.isMobileWidth && (
|
{!props.isMobileWidth && (
|
||||||
<div className="flex pb-6 gap-2 items-center justify-center">
|
<ScrollArea className="w-full max-w-[600px] mx-auto">
|
||||||
{agentIcons.map((icon, index) => (
|
<div className="flex pb-2 gap-2 items-center justify-center">
|
||||||
|
{agents.map((agent, index) => (
|
||||||
|
<Popover
|
||||||
|
key={`${index}-${agent.slug}`}
|
||||||
|
open={isPopoverOpen && debouncedHoveredAgent === agent.slug}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setHoveredAgent(null);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
<Card
|
<Card
|
||||||
key={`${index}-${agents[index].slug}`}
|
|
||||||
className={`${
|
className={`${
|
||||||
selectedAgent === agents[index].slug
|
selectedAgent === agent.slug
|
||||||
? convertColorToBorderClass(agents[index].color)
|
? convertColorToBorderClass(agent.color)
|
||||||
: "border-stone-100 dark:border-neutral-700 text-muted-foreground"
|
: "border-stone-100 dark:border-neutral-700 text-muted-foreground"
|
||||||
}
|
}
|
||||||
hover:cursor-pointer rounded-lg px-2 py-2`}
|
hover:cursor-pointer rounded-lg px-2 py-2`}
|
||||||
|
onDoubleClick={() => openAgentEditCard(agent.slug)}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAgent(agent.slug);
|
||||||
|
chatInputRef.current?.focus();
|
||||||
|
setHoveredAgent(null);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHoveredAgent(agent.slug)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredAgent(null);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CardTitle
|
<CardTitle className="text-center text-md font-medium flex justify-center items-center whitespace-nowrap">
|
||||||
className="text-center text-md font-medium flex justify-center items-center"
|
{agentIcons[index]} {agent.name}
|
||||||
onClick={() => setSelectedAgent(agents[index].slug)}
|
|
||||||
>
|
|
||||||
{icon} {agents[index].name}
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</Card>
|
</Card>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-80 p-0 border-none bg-transparent shadow-none"
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredAgent(null);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AgentCard
|
||||||
|
data={agent}
|
||||||
|
userProfile={null}
|
||||||
|
isMobileWidth={props.isMobileWidth || false}
|
||||||
|
showChatButton={false}
|
||||||
|
editCard={false}
|
||||||
|
filesOptions={[]}
|
||||||
|
selectedChatModelOption=""
|
||||||
|
agentSlug=""
|
||||||
|
isSubscribed={isUserSubscribed(props.userConfig)}
|
||||||
|
setAgentChangeTriggered={() => {}}
|
||||||
|
modelOptions={[]}
|
||||||
|
inputToolOptions={{}}
|
||||||
|
outputModeOptions={{}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
))}
|
))}
|
||||||
<Card
|
|
||||||
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
|
|
||||||
onClick={() => (window.location.href = "/agents")}
|
|
||||||
>
|
|
||||||
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">
|
|
||||||
See All →
|
|
||||||
</CardTitle>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-fit max-w-screen-md"}`}>
|
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-fit max-w-screen-md"}`}>
|
||||||
@@ -239,6 +305,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
conversationId={null}
|
conversationId={null}
|
||||||
isMobileWidth={props.isMobileWidth}
|
isMobileWidth={props.isMobileWidth}
|
||||||
setUploadedFiles={props.setUploadedFiles}
|
setUploadedFiles={props.setUploadedFiles}
|
||||||
|
ref={chatInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -286,6 +353,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
<div
|
<div
|
||||||
className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}
|
className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}
|
||||||
>
|
>
|
||||||
|
<ScrollArea className="w-full max-w-[85vw]">
|
||||||
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
|
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
|
||||||
{agentIcons.map((icon, index) => (
|
{agentIcons.map((icon, index) => (
|
||||||
<Card
|
<Card
|
||||||
@@ -294,23 +362,21 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
>
|
>
|
||||||
<CardTitle
|
<CardTitle
|
||||||
className="text-center text-xs font-medium flex justify-center items-center px-1.5 py-1"
|
className="text-center text-xs font-medium flex justify-center items-center px-1.5 py-1"
|
||||||
onClick={() => setSelectedAgent(agents[index].slug)}
|
onDoubleClick={() =>
|
||||||
|
openAgentEditCard(agents[index].slug)
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAgent(agents[index].slug);
|
||||||
|
chatInputRef.current?.focus();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{icon} {agents[index].name}
|
{icon} {agents[index].name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
<Card
|
|
||||||
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
|
|
||||||
onClick={() => (window.location.href = "/agents")}
|
|
||||||
>
|
|
||||||
<CardTitle
|
|
||||||
className={`text-center ${props.isMobileWidth ? "text-xs" : "text-md"} font-normal flex justify-center items-center px-1.5 py-2`}
|
|
||||||
>
|
|
||||||
See All →
|
|
||||||
</CardTitle>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
<ChatInputArea
|
<ChatInputArea
|
||||||
isLoggedIn={props.isLoggedIn}
|
isLoggedIn={props.isLoggedIn}
|
||||||
sendMessage={(message) => setMessage(message)}
|
sendMessage={(message) => setMessage(message)}
|
||||||
@@ -320,6 +386,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
conversationId={null}
|
conversationId={null}
|
||||||
isMobileWidth={props.isMobileWidth}
|
isMobileWidth={props.isMobileWidth}
|
||||||
setUploadedFiles={props.setUploadedFiles}
|
setUploadedFiles={props.setUploadedFiles}
|
||||||
|
ref={chatInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import "katex/dist/katex.min.css";
|
|||||||
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../../common/utils";
|
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../../common/utils";
|
||||||
import { useAuthenticatedData } from "@/app/common/auth";
|
import { useAuthenticatedData } from "@/app/common/auth";
|
||||||
|
|
||||||
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
import { ChatInputArea, ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
||||||
import { StreamMessage } from "@/app/components/chatMessage/chatMessage";
|
import { StreamMessage } from "@/app/components/chatMessage/chatMessage";
|
||||||
import { processMessageChunk } from "@/app/common/chatFunctions";
|
import { processMessageChunk } from "@/app/common/chatFunctions";
|
||||||
import { AgentData } from "@/app/agents/page";
|
import { AgentData } from "@/app/agents/page";
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -9,8 +11,8 @@ from fastapi.responses import Response
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.authentication import requires
|
from starlette.authentication import requires
|
||||||
|
|
||||||
from khoj.database.adapters import AgentAdapters
|
from khoj.database.adapters import AgentAdapters, ConversationAdapters
|
||||||
from khoj.database.models import Agent, KhojUser
|
from khoj.database.models import Agent, Conversation, KhojUser
|
||||||
from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
|
from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
|
||||||
from khoj.utils.helpers import (
|
from khoj.utils.helpers import (
|
||||||
ConversationCommand,
|
ConversationCommand,
|
||||||
@@ -45,12 +47,13 @@ async def all_agents(
|
|||||||
) -> Response:
|
) -> Response:
|
||||||
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||||
agents = await AgentAdapters.aget_all_accessible_agents(user)
|
agents = await AgentAdapters.aget_all_accessible_agents(user)
|
||||||
|
default_agent = await AgentAdapters.aget_default_agent()
|
||||||
|
default_agent_packet = None
|
||||||
agents_packet = list()
|
agents_packet = list()
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
files = agent.fileobject_set.all()
|
files = agent.fileobject_set.all()
|
||||||
file_names = [file.file_name for file in files]
|
file_names = [file.file_name for file in files]
|
||||||
agents_packet.append(
|
agent_packet = {
|
||||||
{
|
|
||||||
"slug": agent.slug,
|
"slug": agent.slug,
|
||||||
"name": agent.name,
|
"name": agent.name,
|
||||||
"persona": agent.personality,
|
"persona": agent.personality,
|
||||||
@@ -64,11 +67,29 @@ async def all_agents(
|
|||||||
"input_tools": agent.input_tools,
|
"input_tools": agent.input_tools,
|
||||||
"output_modes": agent.output_modes,
|
"output_modes": agent.output_modes,
|
||||||
}
|
}
|
||||||
)
|
if agent.slug == default_agent.slug:
|
||||||
|
default_agent_packet = agent_packet
|
||||||
|
else:
|
||||||
|
agents_packet.append(agent_packet)
|
||||||
|
|
||||||
|
# Load recent conversation sessions
|
||||||
|
min_date = datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
two_weeks_ago = datetime.today() - timedelta(weeks=2)
|
||||||
|
conversations = []
|
||||||
|
if user:
|
||||||
|
conversations = await sync_to_async(list[Conversation])(
|
||||||
|
ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
|
||||||
|
.filter(updated_at__gte=two_weeks_ago)
|
||||||
|
.order_by("-updated_at")[:50]
|
||||||
|
)
|
||||||
|
conversation_times = {conv.agent.slug: conv.updated_at for conv in conversations if conv.agent}
|
||||||
|
|
||||||
|
# Put default agent first, then sort by mru and finally shuffle unused randomly
|
||||||
|
random.shuffle(agents_packet)
|
||||||
|
agents_packet.sort(key=lambda x: conversation_times.get(x["slug"]) or min_date, reverse=True)
|
||||||
|
if default_agent_packet:
|
||||||
|
agents_packet.insert(0, default_agent_packet)
|
||||||
|
|
||||||
# Make sure that the agent named 'khoj' is first in the list. Everything else is sorted by name.
|
|
||||||
agents_packet.sort(key=lambda x: x["name"])
|
|
||||||
agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
|
|
||||||
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user