mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
- Also, when in not subscribed state, fallback to the default model when chatting with an agent - With conversion, create a brand new agent from inside the chat view that can be managed separately
421 lines
18 KiB
TypeScript
421 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import styles from "./agents.module.css";
|
|
|
|
import useSWR from "swr";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import {
|
|
useAuthenticatedData,
|
|
UserProfile,
|
|
ModelOptions,
|
|
useUserConfig,
|
|
isUserSubscribed,
|
|
} from "../common/auth";
|
|
|
|
import { Lightning, Plus } from "@phosphor-icons/react";
|
|
import { z } from "zod";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "@/components/ui/dialog";
|
|
import LoginPrompt from "../components/loginPrompt/loginPrompt";
|
|
import { InlineLoading } from "../components/loading/loading";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { useIsDarkMode, useIsMobileWidth } from "../common/utils";
|
|
import {
|
|
AgentCard,
|
|
EditAgentSchema,
|
|
AgentModificationForm,
|
|
AgentData,
|
|
} from "@/app/components/agentCard/agentCard";
|
|
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
import { AppSidebar } from "../components/appSidebar/appSidebar";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { KhojLogoType } from "../components/logo/khojLogo";
|
|
import { DialogTitle } from "@radix-ui/react-dialog";
|
|
import Link from "next/link";
|
|
|
|
const agentsFetcher = () =>
|
|
window
|
|
.fetch("/api/agents")
|
|
.then((res) => res.json())
|
|
.catch((err) => console.log(err));
|
|
|
|
// A generic fetcher function that uses the fetch API to make a request to a given URL and returns the response as JSON.
|
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
|
|
|
interface CreateAgentCardProps {
|
|
data: AgentData;
|
|
userProfile: UserProfile | null;
|
|
isMobileWidth: boolean;
|
|
filesOptions: string[];
|
|
modelOptions: ModelOptions[];
|
|
selectedChatModelOption: string;
|
|
isSubscribed: boolean;
|
|
setAgentChangeTriggered: (value: boolean) => void;
|
|
inputToolOptions: { [key: string]: string };
|
|
outputModeOptions: { [key: string]: string };
|
|
}
|
|
|
|
function CreateAgentCard(props: CreateAgentCardProps) {
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [errors, setErrors] = useState<string | null>(null);
|
|
const [showLoginPrompt, setShowLoginPrompt] = useState(true);
|
|
|
|
const form = useForm<z.infer<typeof EditAgentSchema>>({
|
|
resolver: zodResolver(EditAgentSchema),
|
|
defaultValues: {
|
|
name: props.data.name,
|
|
persona: props.data.persona,
|
|
color: props.data.color,
|
|
icon: props.data.icon,
|
|
privacy_level: props.data.privacy_level,
|
|
chat_model: props.selectedChatModelOption,
|
|
files: [],
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
form.reset({
|
|
name: props.data.name,
|
|
persona: props.data.persona,
|
|
color: props.data.color,
|
|
icon: props.data.icon,
|
|
privacy_level: props.data.privacy_level,
|
|
chat_model: props.selectedChatModelOption,
|
|
files: [],
|
|
});
|
|
}, [props.selectedChatModelOption, props.data]);
|
|
|
|
const onSubmit = (values: z.infer<typeof EditAgentSchema>) => {
|
|
let agentsApiUrl = `/api/agents`;
|
|
|
|
fetch(agentsApiUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(values),
|
|
})
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
form.reset();
|
|
setShowModal(false);
|
|
setErrors(null);
|
|
props.setAgentChangeTriggered(true);
|
|
} else {
|
|
response.json().then((data) => {
|
|
console.error(data);
|
|
if (data.error) {
|
|
setErrors(data.error);
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
setErrors(error);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
|
<DialogTrigger>
|
|
<div className="flex items-center text-md gap-2">
|
|
<Plus />
|
|
Create Agent
|
|
</div>
|
|
</DialogTrigger>
|
|
<DialogContent
|
|
className={
|
|
"lg:max-w-screen-lg py-4 overflow-y-scroll h-full md:h-4/6 rounded-lg flex flex-col"
|
|
}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>Create Agent</DialogTitle>
|
|
</DialogHeader>
|
|
{!props.userProfile && showLoginPrompt && (
|
|
<LoginPrompt
|
|
onOpenChange={setShowLoginPrompt}
|
|
isMobileWidth={props.isMobileWidth}
|
|
/>
|
|
)}
|
|
<AgentModificationForm
|
|
form={form}
|
|
onSubmit={onSubmit}
|
|
create={true}
|
|
errors={errors}
|
|
filesOptions={props.filesOptions}
|
|
modelOptions={props.modelOptions}
|
|
inputToolOptions={props.inputToolOptions}
|
|
outputModeOptions={props.outputModeOptions}
|
|
isSubscribed={props.isSubscribed}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
export interface AgentConfigurationOptions {
|
|
input_tools: { [key: string]: string };
|
|
output_modes: { [key: string]: string };
|
|
}
|
|
|
|
export default function Agents() {
|
|
const { data, error, mutate } = useSWR<AgentData[]>("agents", agentsFetcher, {
|
|
revalidateOnFocus: false,
|
|
});
|
|
const {
|
|
data: authenticatedData,
|
|
error: authenticationError,
|
|
isLoading: authenticationLoading,
|
|
} = useAuthenticatedData();
|
|
const { data: userConfig } = useUserConfig(true);
|
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
|
const isMobileWidth = useIsMobileWidth();
|
|
|
|
const [personalAgents, setPersonalAgents] = useState<AgentData[]>([]);
|
|
const [publicAgents, setPublicAgents] = useState<AgentData[]>([]);
|
|
|
|
const [agentSlug, setAgentSlug] = useState<string | null>(null);
|
|
|
|
const { data: filesData, error: fileError } = useSWR<string[]>(
|
|
userConfig ? "/api/content/computer" : null,
|
|
fetcher,
|
|
);
|
|
|
|
const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
|
|
useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher);
|
|
|
|
const [agentChangeTriggered, setAgentChangeTriggered] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (agentChangeTriggered) {
|
|
mutate();
|
|
setAgentChangeTriggered(false);
|
|
}
|
|
}, [agentChangeTriggered]);
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
const personalAgents = data.filter(
|
|
(agent) => agent.creator === authenticatedData?.username,
|
|
);
|
|
setPersonalAgents(personalAgents);
|
|
|
|
// Public agents are agents that are not private and not created by the user
|
|
const publicAgents = data.filter(
|
|
(agent) =>
|
|
agent.privacy_level !== "private" &&
|
|
agent.creator !== authenticatedData?.username,
|
|
);
|
|
setPublicAgents(publicAgents);
|
|
|
|
if (typeof window !== "undefined") {
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
const agentSlug = searchParams.get("agent");
|
|
|
|
// Search for the agent with the slug in the URL
|
|
if (agentSlug) {
|
|
setAgentSlug(agentSlug);
|
|
let selectedAgent = data.find((agent) => agent.slug === agentSlug);
|
|
|
|
// If the agent is not found in all the returned agents, check in the public agents. The code may be running 2x after either agent data or authenticated data is retrieved.
|
|
if (!selectedAgent) {
|
|
selectedAgent = publicAgents.find((agent) => agent.slug === agentSlug);
|
|
}
|
|
|
|
if (!selectedAgent) {
|
|
// See if the agent is accessible as a protected agent.
|
|
fetch(`/api/agents/${agentSlug}`)
|
|
.then((res) => {
|
|
if (res.status === 404) {
|
|
throw new Error("Agent not found");
|
|
}
|
|
return res.json();
|
|
})
|
|
.then((agent: AgentData) => {
|
|
if (agent.privacy_level === "protected") {
|
|
setPublicAgents((prev) => [...prev, agent]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [data, authenticatedData]);
|
|
|
|
if (error) {
|
|
return (
|
|
<main className={styles.main}>
|
|
<div className={`${styles.titleBar} text-5xl`}>Agents</div>
|
|
<div className={styles.agentList}>Error loading agents</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<main className={styles.main}>
|
|
<div className={styles.agentList}>
|
|
<InlineLoading /> booting up your agents
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const modelOptions: ModelOptions[] = userConfig?.chat_model_options || [];
|
|
const selectedChatModelOption: number = userConfig?.selected_chat_model_config || 0;
|
|
const isSubscribed: boolean = userConfig?.is_active || false;
|
|
|
|
// The default model option should map to the item in the modelOptions array that has the same id as the selectedChatModelOption
|
|
const defaultModelOption = modelOptions.find(
|
|
(modelOption) => modelOption.id === selectedChatModelOption,
|
|
);
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<AppSidebar conversationId={""} />
|
|
<SidebarInset>
|
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
|
<SidebarTrigger className="-ml-1" />
|
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
{isMobileWidth ? (
|
|
<a className="p-0 no-underline" href="/">
|
|
<KhojLogoType className="h-auto w-16" />
|
|
</a>
|
|
) : (
|
|
<h2 className="text-lg">Agents</h2>
|
|
)}
|
|
</header>
|
|
<main className={`w-full mx-auto`}>
|
|
<div className={`grid w-full mx-auto`}>
|
|
<div className={`${styles.pageLayout} w-full`}>
|
|
<div className={`pt-6 md:pt-8 flex justify-between`}>
|
|
<h1 className="text-3xl flex items-center">Agents</h1>
|
|
<div className="ml-auto float-right border p-2 pt-3 rounded-xl font-bold hover:bg-stone-100 dark:hover:bg-neutral-900">
|
|
<CreateAgentCard
|
|
data={{
|
|
slug: "",
|
|
name: "",
|
|
persona: "",
|
|
color: "",
|
|
icon: "",
|
|
privacy_level: "private",
|
|
managed_by_admin: false,
|
|
chat_model: "",
|
|
input_tools: [],
|
|
output_modes: [],
|
|
is_hidden: false,
|
|
}}
|
|
userProfile={
|
|
authenticationLoading
|
|
? null
|
|
: (authenticatedData ?? null)
|
|
}
|
|
isMobileWidth={isMobileWidth}
|
|
filesOptions={filesData || []}
|
|
modelOptions={userConfig?.chat_model_options || []}
|
|
selectedChatModelOption={defaultModelOption?.name || ""}
|
|
isSubscribed={isSubscribed}
|
|
setAgentChangeTriggered={setAgentChangeTriggered}
|
|
inputToolOptions={
|
|
agentConfigurationOptions?.input_tools || {}
|
|
}
|
|
outputModeOptions={
|
|
agentConfigurationOptions?.output_modes || {}
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{showLoginPrompt && (
|
|
<LoginPrompt
|
|
onOpenChange={setShowLoginPrompt}
|
|
isMobileWidth={isMobileWidth}
|
|
/>
|
|
)}
|
|
<Alert className="bg-secondary border-none my-4">
|
|
<AlertDescription>
|
|
<Lightning
|
|
weight={"fill"}
|
|
className="h-4 w-4 text-purple-400 inline"
|
|
/>
|
|
<span className="font-bold">How it works</span> Use any of these
|
|
specialized personas to tune your conversation to your needs.
|
|
{
|
|
!isSubscribed && (
|
|
<span>
|
|
{" "}
|
|
<Link href="/settings" className="font-bold">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting.
|
|
</span>
|
|
)
|
|
}
|
|
</AlertDescription>
|
|
</Alert>
|
|
<div className="pt-6 md:pt-8">
|
|
<div className={`${styles.agentList}`}>
|
|
{authenticatedData &&
|
|
personalAgents.map((agent) => (
|
|
<AgentCard
|
|
key={agent.slug}
|
|
data={agent}
|
|
userProfile={authenticatedData}
|
|
isMobileWidth={isMobileWidth}
|
|
filesOptions={filesData ?? []}
|
|
selectedChatModelOption={
|
|
defaultModelOption?.name || ""
|
|
}
|
|
isSubscribed={isSubscribed}
|
|
setAgentChangeTriggered={setAgentChangeTriggered}
|
|
modelOptions={userConfig?.chat_model_options || []}
|
|
editCard={true}
|
|
agentSlug={agentSlug || ""}
|
|
inputToolOptions={
|
|
agentConfigurationOptions?.input_tools || {}
|
|
}
|
|
outputModeOptions={
|
|
agentConfigurationOptions?.output_modes || {}
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="pt-6 md:pt-8">
|
|
<h2 className="text-2xl">Explore</h2>
|
|
<div className={`${styles.agentList}`}>
|
|
{!authenticationLoading &&
|
|
publicAgents.map((agent) => (
|
|
<AgentCard
|
|
key={agent.slug}
|
|
data={agent}
|
|
userProfile={authenticatedData || null}
|
|
isMobileWidth={isMobileWidth}
|
|
editCard={false}
|
|
filesOptions={filesData ?? []}
|
|
selectedChatModelOption={
|
|
defaultModelOption?.name || ""
|
|
}
|
|
isSubscribed={isSubscribed}
|
|
setAgentChangeTriggered={setAgentChangeTriggered}
|
|
modelOptions={userConfig?.chat_model_options || []}
|
|
agentSlug={agentSlug || ""}
|
|
inputToolOptions={
|
|
agentConfigurationOptions?.input_tools || {}
|
|
}
|
|
outputModeOptions={
|
|
agentConfigurationOptions?.output_modes || {}
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
);
|
|
}
|