"use client"; import styles from "./agents.module.css"; import useSWR from "swr"; import { useEffect, useRef, useState } from "react"; import { useAuthenticatedData, UserProfile, ModelOptions, useUserConfig, UserConfig, SubscriptionStates, } from "../common/auth"; import { Button } from "@/components/ui/button"; import { PaperPlaneTilt, Lightning, Plus, Circle, Info, Check, ShieldWarning, Lock, Book, Brain, Waveform, CaretUpDown, Globe, LockOpen, FloppyDisk, DotsThreeCircleVertical, DotsThreeVertical, Pencil, Trash, } from "@phosphor-icons/react"; import { set, z } from "zod"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTrigger, } from "@/components/ui/dialog"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import LoginPrompt from "../components/loginPrompt/loginPrompt"; import { InlineLoading } from "../components/loading/loading"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; import { getAvailableIcons, getIconForSlashCommand, getIconFromIconName, } from "../common/iconUtils"; import { convertColorToTextClass, tailwindColors } from "../common/colorUtils"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useIsMobileWidth } from "../common/utils"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { useForm, UseFormReturn } from "react-hook-form"; import { Input } from "@/components/ui/input"; import { zodResolver } from "@hookform/resolvers/zod"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DialogTitle } from "@radix-ui/react-dialog"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { uploadDataForIndexing } from "../common/chatFunctions"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Progress } from "@/components/ui/progress"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import ShareLink from "../components/shareLink/shareLink"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 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[]; } async function openChat(slug: string, userData: UserProfile | null) { const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`; if (!userData) { window.location.href = unauthenticatedRedirectUrl; return; } const response = await fetch(`/api/chat/sessions?agent_slug=${encodeURIComponent(slug)}`, { 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"); } } function Badge(props: { icon: JSX.Element; text?: string; hoverText?: string }) { // Always convert text to proper case (e.g., "public" -> "Public") const displayBadgeText = props.text?.replace(/^\w/, (c) => c.toUpperCase()) || ""; return (
{props.hoverText || displayBadgeText}
{props.icon}
{displayBadgeText && displayBadgeText.length > 0 && (
{displayBadgeText}
)}
); } 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 AgentCardProps { data: AgentData; userProfile: UserProfile | null; isMobileWidth: boolean; editCard: boolean; filesOptions: string[]; modelOptions: ModelOptions[]; selectedChatModelOption: string; isSubscribed: boolean; setAgentChangeTriggered: (value: boolean) => void; agentSlug: string; inputToolOptions: { [key: string]: string }; outputModeOptions: { [key: string]: string }; } function AgentCard(props: AgentCardProps) { const [showModal, setShowModal] = useState(props.agentSlug === props.data.slug); const [showLoginPrompt, setShowLoginPrompt] = useState(false); const [errors, setErrors] = useState(null); let lockIcon = ; let privacyHoverText = "Private agents are only visible to you."; if (props.data.privacy_level === "public") { lockIcon = ; privacyHoverText = "Public agents are visible to everyone."; } else if (props.data.privacy_level === "protected") { lockIcon = ; privacyHoverText = "Protected agents are visible to anyone with a direct link."; } const userData = props.userProfile; const form = useForm>({ 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.data.chat_model, files: props.data.files, input_tools: props.data.input_tools, output_modes: props.data.output_modes, }, }); 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.data.chat_model, files: props.data.files, input_tools: props.data.input_tools, output_modes: props.data.output_modes, }); }, [props.data]); if (showModal) { window.history.pushState( {}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`, ); } const onSubmit = (values: z.infer) => { let agentsApiUrl = `/api/agents`; let method = props.editCard ? "PATCH" : "POST"; fetch(agentsApiUrl, { method: method, 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); form.clearErrors(); if (data.error) { setErrors(data.error); } }); } }) .catch((error) => { console.error("Error:", error); setErrors(error); form.clearErrors(); }); }; const stylingString = convertColorToTextClass(props.data.color); function makeBadgeFooter() { return (
{props.editCard && ( )} {props.data.files && props.data.files.length > 0 && ( } text={`knowledge`} hoverText={ "The agent has a custom knowledge base it can use to give you answers." } /> )} } text={props.data.chat_model} hoverText={`The agent uses the ${props.data.chat_model} model to chat with you.`} /> {props.data.output_modes.map((outputMode) => ( ))} {props.data.input_tools.map((inputTool) => ( ))}
); } return ( {showLoginPrompt && ( )} {!props.isMobileWidth ? ( { setShowModal(!showModal); window.history.pushState({}, `Khoj AI - Agents`, `/agents`); }} >
{getIconFromIconName(props.data.icon, props.data.color)} {props.data.name}
{props.editCard && (
{props.editCard && props.data.privacy_level !== "private" && ( )} {props.data.creator === userData?.username && ( )}
)}
{props.userProfile ? ( ) : ( )}
{props.editCard ? ( Edit {props.data.name} ) : (
{getIconFromIconName(props.data.icon, props.data.color)}

{props.data.name}

{props.data.persona}
{makeBadgeFooter()}
)}
) : ( { setShowModal(open); window.history.pushState({}, `Khoj AI - Agents`, `/agents`); }} >
{getIconFromIconName(props.data.icon, props.data.color)} {props.data.name}
{props.editCard && (
{props.editCard && props.data.privacy_level !== "private" && ( )} {props.data.creator === userData?.username && ( )}
)}
{props.userProfile ? ( ) : ( )}
{props.editCard ? ( ) : ( {props.data.name} Persona {props.data.persona}
{makeBadgeFooter()}
Done
)}
)}
{makeBadgeFooter()}
); } const EditAgentSchema = z.object({ name: z.string({ required_error: "Name is required" }).min(1, "Name is required"), persona: z .string({ required_error: "Personality is required" }) .min(1, "Personality is required"), color: z.string({ required_error: "Color is required" }).min(1, "Color is required"), icon: z.string({ required_error: "Icon is required" }).min(1, "Icon is required"), privacy_level: z .string({ required_error: "Privacy level is required" }) .min(1, "Privacy level is required"), chat_model: z .string({ required_error: "Chat model is required" }) .min(1, "Chat model is required"), files: z.array(z.string()).default([]).optional(), input_tools: z.array(z.string()).default([]).optional(), output_modes: z.array(z.string()).default([]).optional(), }); interface AgentModificationFormProps { form: UseFormReturn>; onSubmit: (values: z.infer) => void; userConfig?: UserConfig; create?: boolean; errors?: string | null; modelOptions: ModelOptions[]; filesOptions: string[]; inputToolOptions: { [key: string]: string }; outputModeOptions: { [key: string]: string }; slug?: string; isSubscribed: boolean; } function AgentModificationForm(props: AgentModificationFormProps) { const [isSaving, setIsSaving] = useState(false); const iconOptions = getAvailableIcons(); const colorOptions = tailwindColors; const colorOptionClassName = convertColorToTextClass(props.form.getValues("color")); const [isDragAndDropping, setIsDragAndDropping] = useState(false); const [warning, setWarning] = useState(null); const [error, setError] = useState(null); const [uploading, setUploading] = useState(false); const [progressValue, setProgressValue] = useState(0); const [uploadedFiles, setUploadedFiles] = useState([]); const [allFileOptions, setAllFileOptions] = useState([]); const [showSubscribeDialog, setShowSubscribeDialog] = useState(true); const fileInputRef = useRef(null); useEffect(() => { if (!uploading) { setProgressValue(0); } if (uploading) { const interval = setInterval(() => { setProgressValue((prev) => { const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5 const nextValue = prev + increment; return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100 }); }, 800); return () => clearInterval(interval); } }, [uploading]); useEffect(() => { const currentFiles = props.form.getValues("files") || []; const fileOptions = props.filesOptions || []; const concatenatedFiles = [...currentFiles, ...fileOptions]; setAllFileOptions((prev) => [...prev, ...concatenatedFiles]); }, []); useEffect(() => { if (uploadedFiles.length > 0) { handleAgentFileChange(uploadedFiles); setAllFileOptions((prev) => [...prev, ...uploadedFiles]); } }, [uploadedFiles]); useEffect(() => { if (props.errors) { setIsSaving(false); } }, [props.errors]); function handleDragOver(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(true); } function handleDragLeave(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(false); } function handleDragAndDropFiles(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(false); if (!event.dataTransfer.files) return; uploadFiles(event.dataTransfer.files); } function uploadFiles(files: FileList) { uploadDataForIndexing(files, setWarning, setUploading, setError, setUploadedFiles); } function openFileInput() { if (fileInputRef && fileInputRef.current) { fileInputRef.current.click(); } } function handleFileChange(event: React.ChangeEvent) { if (!event.target.files) return; uploadFiles(event.target.files); } const handleAgentFileChange = (files: string[]) => { for (const file of files) { const currentFiles = props.form.getValues("files") || []; const newFiles = currentFiles.includes(file) ? currentFiles.filter((item) => item !== file) : [...currentFiles, file]; props.form.setValue("files", newFiles); } }; const privacyOptions = ["public", "private", "protected"]; if (!props.isSubscribed && showSubscribeDialog) { return ( Upgrade to Futurist You need to be a Futurist subscriber to create more agents.{" "} Upgrade now. { setShowSubscribeDialog(false); }} > Cancel { window.location.href = "/settings"; }} > Continue ); } return (
{ props.onSubmit(values); setIsSaving(true); })} className="space-y-6" > ( Name What should this agent be called? Pick something descriptive & memorable. )} /> ( Personality What is the personality, thought process, or tuning of this agent? Get creative; this is how you can influence the agent constitution.