Files
khoj/src/interface/web/app/page.tsx
2024-12-24 10:52:42 -08:00

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>
);
}