mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 13:19:16 +00:00
522 lines
24 KiB
TypeScript
522 lines
24 KiB
TypeScript
"use client";
|
|
import "./globals.css";
|
|
import styles from "./page.module.css";
|
|
import "katex/dist/katex.min.css";
|
|
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
import useSWR from "swr";
|
|
import { ArrowsVertical } from "@phosphor-icons/react";
|
|
|
|
import { Card, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
StepOneSuggestionCard,
|
|
StepOneSuggestionRevertCard,
|
|
StepTwoSuggestionCard,
|
|
} from "@/app/components/suggestions/suggestionCard";
|
|
import Loading from "@/app/components/loading/loading";
|
|
import {
|
|
AttachedFileText,
|
|
ChatInputArea,
|
|
ChatInputFocus,
|
|
ChatOptions,
|
|
} from "@/app/components/chatInputArea/chatInputArea";
|
|
import {
|
|
StepOneSuggestion,
|
|
stepOneSuggestions,
|
|
StepTwoSuggestion,
|
|
getStepTwoSuggestions,
|
|
SuggestionType,
|
|
} from "@/app/components/suggestions/suggestionsData";
|
|
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
|
|
|
|
import {
|
|
isUserSubscribed,
|
|
useAuthenticatedData,
|
|
UserConfig,
|
|
useUserConfig,
|
|
} from "@/app/common/auth";
|
|
import { convertColorToBorderClass } from "@/app/common/colorUtils";
|
|
import { getIconFromIconName } from "@/app/common/iconUtils";
|
|
import { AgentData } from "@/app/agents/page";
|
|
import { createNewConversation } from "./common/chatFunctions";
|
|
import { useDebounce, useIsMobileWidth } from "./common/utils";
|
|
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";
|
|
import LoginPopup from "./components/loginPrompt/loginPopup";
|
|
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";
|
|
|
|
interface ChatBodyDataProps {
|
|
chatOptionsData: ChatOptions | null;
|
|
onConversationIdChange?: (conversationId: string) => void;
|
|
setUploadedFiles: (files: AttachedFileText[]) => void;
|
|
isMobileWidth?: boolean;
|
|
isLoggedIn: boolean;
|
|
userConfig: UserConfig | null;
|
|
isLoadingUserConfig: boolean;
|
|
}
|
|
|
|
function ChatBodyData(props: ChatBodyDataProps) {
|
|
const [message, setMessage] = useState("");
|
|
const [prefillMessage, setPrefillMessage] = useState("");
|
|
const [chatInputFocus, setChatInputFocus] = useState<ChatInputFocus>(ChatInputFocus.MESSAGE);
|
|
const [images, setImages] = useState<string[]>([]);
|
|
const [processingMessage, setProcessingMessage] = useState(false);
|
|
const [greeting, setGreeting] = useState("");
|
|
const [stepOneSuggestionOptions, setStepOneSuggestionOptions] = useState<StepOneSuggestion[]>(
|
|
stepOneSuggestions.slice(0, 3),
|
|
);
|
|
const [stepTwoSuggestionOptions, setStepTwoSuggestionOptions] = useState<StepTwoSuggestion[]>(
|
|
[],
|
|
);
|
|
const [selectedStepOneSuggestion, setSelectedStepOneSuggestion] =
|
|
useState<StepOneSuggestion | null>(null);
|
|
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 [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
|
|
const [agents, setAgents] = useState<AgentData[]>([]);
|
|
const chatInputRef = useRef<HTMLTextAreaElement>(null);
|
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const queryParam = searchParams.get("q");
|
|
|
|
useEffect(() => {
|
|
if (queryParam) {
|
|
setMessage(decodeURIComponent(queryParam));
|
|
}
|
|
}, [queryParam]);
|
|
|
|
useEffect(() => {
|
|
if (debouncedHoveredAgent) {
|
|
setIsPopoverOpen(true);
|
|
}
|
|
}, [debouncedHoveredAgent]);
|
|
|
|
const onConversationIdChange = props.onConversationIdChange;
|
|
|
|
const agentsFetcher = () =>
|
|
window
|
|
.fetch("/api/agents")
|
|
.then((res) => res.json())
|
|
.catch((err) => console.log(err));
|
|
const { data: agentsData, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
|
|
revalidateOnFocus: false,
|
|
});
|
|
|
|
const openAgentEditCard = (agentSlug: string) => {
|
|
router.push(`/agents?agent=${agentSlug}`);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (props.isLoadingUserConfig) return;
|
|
|
|
// Get today's day
|
|
const today = new Date();
|
|
const day = today.getDay();
|
|
const timeOfDay =
|
|
today.getHours() >= 17 || today.getHours() < 4
|
|
? "evening"
|
|
: today.getHours() >= 12
|
|
? "afternoon"
|
|
: "morning";
|
|
const nameSuffix = props.userConfig?.given_name ? `, ${props.userConfig?.given_name}` : "";
|
|
const greetings = [
|
|
`What would you like to get done${nameSuffix}?`,
|
|
`Hey${nameSuffix}! How can I help?`,
|
|
`Good ${timeOfDay}${nameSuffix}! What's on your mind?`,
|
|
`Ready to breeze through ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]}?`,
|
|
`Let's navigate your ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]} workload`,
|
|
];
|
|
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
|
setGreeting(greeting);
|
|
}, [props.isLoadingUserConfig, props.userConfig]);
|
|
|
|
useEffect(() => {
|
|
const agents = (agentsData || []).filter((agent) => agent !== null && agent !== undefined);
|
|
setAgents(agents);
|
|
// set the first agent, which is always the default agent, as the default for chat
|
|
setSelectedAgent(agents.length > 1 ? agents[0].slug : "khoj");
|
|
|
|
// generate colored icons for the available agents
|
|
const agentIcons = agents.map((agent) => getIconFromIconName(agent.icon, agent.color)!);
|
|
setAgentIcons(agentIcons);
|
|
}, [agentsData]);
|
|
|
|
function showAllSuggestionsCards() {
|
|
setStepOneSuggestionOptions(stepOneSuggestions);
|
|
}
|
|
|
|
useEffect(() => {
|
|
const processMessage = async () => {
|
|
if (message && !processingMessage) {
|
|
setProcessingMessage(true);
|
|
try {
|
|
const newConversationId = await createNewConversation(selectedAgent || "khoj");
|
|
onConversationIdChange?.(newConversationId);
|
|
localStorage.setItem("message", message);
|
|
if (images.length > 0) {
|
|
localStorage.setItem("images", JSON.stringify(images));
|
|
}
|
|
|
|
window.location.href = `/chat?conversationId=${newConversationId}`;
|
|
} catch (error) {
|
|
console.error("Error creating new conversation:", error);
|
|
setProcessingMessage(false);
|
|
}
|
|
setMessage("");
|
|
setImages([]);
|
|
}
|
|
};
|
|
processMessage();
|
|
if (message || images.length > 0) {
|
|
setProcessingMessage(true);
|
|
}
|
|
}, [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 clickStepOneSuggestion(suggestion: StepOneSuggestion) {
|
|
setPrefillMessage(suggestion.intent);
|
|
const stepTwoSuggestions = getStepTwoSuggestions(suggestion.type);
|
|
setSelectedStepOneSuggestion(suggestion);
|
|
setStepTwoSuggestionOptions(stepTwoSuggestions);
|
|
setChatInputFocus(suggestion.focus);
|
|
}
|
|
|
|
return (
|
|
<div className={`${styles.homeGreetings} w-full md:w-auto`}>
|
|
{showLoginPrompt && (
|
|
<LoginPrompt
|
|
onOpenChange={setShowLoginPrompt}
|
|
isMobileWidth={props.isMobileWidth}
|
|
/>
|
|
)}
|
|
{!props.isLoggedIn && (
|
|
<LoginPopup
|
|
isMobileWidth={props.isMobileWidth}
|
|
setShowLoginPrompt={setShowLoginPrompt}
|
|
/>
|
|
)}
|
|
<div className={`w-full text-center justify-end content-end`}>
|
|
<div className="items-center">
|
|
<h1 className="text-2xl md:text-3xl text-center w-fit pb-6 px-4 mx-auto">
|
|
{greeting}
|
|
</h1>
|
|
</div>
|
|
{!props.isMobileWidth && (
|
|
<ScrollArea className="w-full max-w-[600px] mx-auto">
|
|
<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
|
|
className={`${
|
|
selectedAgent === agent.slug
|
|
? convertColorToBorderClass(agent.color)
|
|
: "border-stone-100 dark:border-neutral-700 text-muted-foreground"
|
|
}
|
|
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 className="text-center text-md font-medium flex justify-center items-center whitespace-nowrap">
|
|
{agentIcons[index]} {agent.name}
|
|
</CardTitle>
|
|
</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>
|
|
))}
|
|
</div>
|
|
<ScrollBar orientation="horizontal" />
|
|
</ScrollArea>
|
|
)}
|
|
</div>
|
|
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-full max-w-screen-md"}`}>
|
|
{!props.isMobileWidth && (
|
|
<div
|
|
className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}
|
|
>
|
|
<ChatInputArea
|
|
isLoggedIn={props.isLoggedIn}
|
|
prefillMessage={prefillMessage}
|
|
focus={chatInputFocus}
|
|
sendMessage={(message) => setMessage(message)}
|
|
sendImage={(image) => setImages((prevImages) => [...prevImages, image])}
|
|
sendDisabled={processingMessage}
|
|
chatOptionsData={props.chatOptionsData}
|
|
conversationId={null}
|
|
isMobileWidth={props.isMobileWidth}
|
|
setUploadedFiles={props.setUploadedFiles}
|
|
agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color}
|
|
ref={chatInputRef}
|
|
setTriggeredAbort={() => {}}
|
|
/>
|
|
</div>
|
|
)}
|
|
{stepTwoSuggestionOptions.length == 0 && (
|
|
<div
|
|
className={`${styles.suggestions} w-full ${props.isMobileWidth ? (stepOneSuggestions.length > 3 ? "grid grid-cols-2" : "grid grid-cols-3") : "grid grid-cols-3"} "justify-center items-center"`}
|
|
>
|
|
{stepOneSuggestionOptions.map((suggestion, index) => (
|
|
<div
|
|
key={`${suggestion.type} ${suggestion.actionTagline}`}
|
|
onClick={(event) => {
|
|
if (props.isLoggedIn) {
|
|
clickStepOneSuggestion(suggestion);
|
|
} else {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setShowLoginPrompt(true);
|
|
}
|
|
}}
|
|
>
|
|
<StepOneSuggestionCard
|
|
key={suggestion.type + Math.random()}
|
|
title={suggestion.type}
|
|
body={suggestion.actionTagline}
|
|
color={suggestion.color}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{stepTwoSuggestionOptions.length == 0 &&
|
|
stepOneSuggestionOptions.length < stepOneSuggestions.length && (
|
|
<div className="flex items-center justify-center margin-auto">
|
|
<button
|
|
onClick={showAllSuggestionsCards}
|
|
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700"
|
|
>
|
|
Show All <ArrowsVertical className="h-4 w-4 inline" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{selectedStepOneSuggestion && (
|
|
<StepOneSuggestionRevertCard
|
|
title={selectedStepOneSuggestion.type}
|
|
body={selectedStepOneSuggestion.actionTagline}
|
|
color={selectedStepOneSuggestion.color}
|
|
onClick={() => {
|
|
setPrefillMessage("");
|
|
setSelectedStepOneSuggestion(null);
|
|
setStepTwoSuggestionOptions([]);
|
|
setChatInputFocus(ChatInputFocus.MESSAGE);
|
|
}}
|
|
/>
|
|
)}
|
|
{stepTwoSuggestionOptions.length > 0 && (
|
|
<div
|
|
className={`w-full ${props.isMobileWidth ? "grid" : "grid grid-cols-1"} justify-center items-center gap-2 p-2`}
|
|
>
|
|
{stepTwoSuggestionOptions.map((suggestion, index) => (
|
|
<div
|
|
key={`${suggestion.prompt} ${index}`}
|
|
className={`w-full cursor-pointer animate-fade-in-up`}
|
|
onClick={(event) => {
|
|
setMessage(suggestion.prompt);
|
|
}}
|
|
>
|
|
<StepTwoSuggestionCard
|
|
key={suggestion.prompt}
|
|
prompt={suggestion.prompt}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{props.isMobileWidth && (
|
|
<>
|
|
<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-2xl mb-2`}
|
|
>
|
|
<ScrollArea className="w-full max-w-[85vw]">
|
|
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
|
|
{agentIcons.map((icon, index) => (
|
|
<Card
|
|
key={`${index}-${agents[index].slug}`}
|
|
className={`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : "border-muted text-muted-foreground"} hover:cursor-pointer`}
|
|
>
|
|
<CardTitle
|
|
className="text-center text-xs font-medium flex justify-center items-center whitespace-nowrap px-1.5 py-1"
|
|
onDoubleClick={() =>
|
|
openAgentEditCard(agents[index].slug)
|
|
}
|
|
onClick={() => {
|
|
setSelectedAgent(agents[index].slug);
|
|
chatInputRef.current?.focus();
|
|
}}
|
|
>
|
|
{icon} {agents[index].name}
|
|
</CardTitle>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
<ScrollBar orientation="horizontal" />
|
|
</ScrollArea>
|
|
<ChatInputArea
|
|
isLoggedIn={props.isLoggedIn}
|
|
sendMessage={(message) => setMessage(message)}
|
|
sendImage={(image) => setImages((prevImages) => [...prevImages, image])}
|
|
sendDisabled={processingMessage}
|
|
chatOptionsData={props.chatOptionsData}
|
|
conversationId={null}
|
|
isMobileWidth={props.isMobileWidth}
|
|
setUploadedFiles={props.setUploadedFiles}
|
|
agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color}
|
|
ref={chatInputRef}
|
|
setTriggeredAbort={() => {}}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Home() {
|
|
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
|
const [isLoading, setLoading] = useState(true);
|
|
const [conversationId, setConversationID] = useState<string | null>(null);
|
|
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | null>(null);
|
|
const isMobileWidth = useIsMobileWidth();
|
|
|
|
const { userConfig: initialUserConfig, isLoadingUserConfig } = useUserConfig(true);
|
|
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
|
|
|
|
const {
|
|
data: authenticatedData,
|
|
error: authenticationError,
|
|
isLoading: authenticationLoading,
|
|
} = useAuthenticatedData();
|
|
|
|
const handleConversationIdChange = (newConversationId: string) => {
|
|
setConversationID(newConversationId);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setUserConfig(initialUserConfig);
|
|
}, [initialUserConfig]);
|
|
|
|
useEffect(() => {
|
|
if (uploadedFiles) {
|
|
localStorage.setItem("uploadedFiles", JSON.stringify(uploadedFiles));
|
|
}
|
|
}, [uploadedFiles]);
|
|
|
|
useEffect(() => {
|
|
fetch("/api/chat/options")
|
|
.then((response) => response.json())
|
|
.then((data: ChatOptions) => {
|
|
setLoading(false);
|
|
if (data) {
|
|
setChatOptionsData(data);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
return;
|
|
});
|
|
}, []);
|
|
|
|
if (isLoading) {
|
|
return <Loading />;
|
|
}
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<AppSidebar conversationId={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 ? (
|
|
<KhojLogoType className="h-auto w-16" />
|
|
) : (
|
|
<h2 className="text-lg">Ask Anything</h2>
|
|
)}
|
|
</header>
|
|
<div className={`${styles.main} ${styles.chatLayout}`}>
|
|
<title>Khoj AI - Your Second Brain</title>
|
|
<div className={`${styles.chatBox}`}>
|
|
<div className={`${styles.chatBoxBody}`}>
|
|
{!authenticationLoading && (
|
|
<ChatBodyData
|
|
isLoggedIn={authenticatedData ? true : false}
|
|
chatOptionsData={chatOptionsData}
|
|
setUploadedFiles={setUploadedFiles}
|
|
isMobileWidth={isMobileWidth}
|
|
onConversationIdChange={handleConversationIdChange}
|
|
userConfig={userConfig}
|
|
isLoadingUserConfig={isLoadingUserConfig}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
);
|
|
}
|