Format web app code with prettier recommendations

Too many of these had accumulated earlier from being ignored.
Changed to make build logs less noisy
This commit is contained in:
Debanjum
2025-08-01 00:25:53 -07:00
parent c8e07e86e4
commit 791ebe3a97
19 changed files with 1109 additions and 924 deletions

View File

@@ -344,14 +344,16 @@ export default function Agents() {
/> />
<span className="font-bold">How it works</span> Use any of these <span className="font-bold">How it works</span> Use any of these
specialized personas to tune your conversation to your needs. specialized personas to tune your conversation to your needs.
{ {!isSubscribed && (
!isSubscribed && ( <span>
<span> {" "}
{" "} <Link href="/settings" className="font-bold">
<Link href="/settings" className="font-bold">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting. Upgrade your plan
</span> </Link>{" "}
) to leverage custom models. You will fallback to the
} default model when chatting.
</span>
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="pt-6 md:pt-8"> <div className="pt-6 md:pt-8">

View File

@@ -90,11 +90,9 @@ export interface UserConfig {
export function useUserConfig(detailed: boolean = false) { export function useUserConfig(detailed: boolean = false) {
const url = `/api/settings?detailed=${detailed}`; const url = `/api/settings?detailed=${detailed}`;
const { const { data, error, isLoading } = useSWR<UserConfig>(url, fetcher, {
data, revalidateOnFocus: false,
error, });
isLoading,
} = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
if (error || !data || data?.detail === "Forbidden") { if (error || !data || data?.detail === "Forbidden") {
return { data: null, error, isLoading }; return { data: null, error, isLoading };

View File

@@ -1,12 +1,12 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { PopoverProps } from "@radix-ui/react-popover" import { PopoverProps } from "@radix-ui/react-popover";
import { Check, CaretUpDown } from "@phosphor-icons/react"; import { Check, CaretUpDown } from "@phosphor-icons/react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { useIsMobileWidth, useMutationObserver } from "@/app/common/utils"; import { useIsMobileWidth, useMutationObserver } from "@/app/common/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -17,11 +17,7 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ModelOptions, useUserConfig } from "./auth"; import { ModelOptions, useUserConfig } from "./auth";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
@@ -35,7 +31,7 @@ interface ModelSelectorProps extends PopoverProps {
} }
export function ModelSelector({ ...props }: ModelSelectorProps) { export function ModelSelector({ ...props }: ModelSelectorProps) {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false);
const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined); const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined);
const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined); const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined);
const { data: userConfig, error, isLoading: isLoadingUserConfig } = useUserConfig(true); const { data: userConfig, error, isLoading: isLoadingUserConfig } = useUserConfig(true);
@@ -48,14 +44,18 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
if (userConfig) { if (userConfig) {
setModels(userConfig.chat_model_options); setModels(userConfig.chat_model_options);
if (!props.initialModel) { if (!props.initialModel) {
const selectedChatModelOption = userConfig.chat_model_options.find(model => model.id === userConfig.selected_chat_model_config); const selectedChatModelOption = userConfig.chat_model_options.find(
(model) => model.id === userConfig.selected_chat_model_config,
);
if (!selectedChatModelOption && userConfig.chat_model_options.length > 0) { if (!selectedChatModelOption && userConfig.chat_model_options.length > 0) {
setSelectedModel(userConfig.chat_model_options[0]); setSelectedModel(userConfig.chat_model_options[0]);
} else { } else {
setSelectedModel(selectedChatModelOption); setSelectedModel(selectedChatModelOption);
} }
} else { } else {
const model = userConfig.chat_model_options.find(model => model.name === props.initialModel); const model = userConfig.chat_model_options.find(
(model) => model.name === props.initialModel,
);
setSelectedModel(model); setSelectedModel(model);
} }
} }
@@ -68,15 +68,11 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
}, [selectedModel, userConfig, props.onSelect]); }, [selectedModel, userConfig, props.onSelect]);
if (isLoadingUserConfig) { if (isLoadingUserConfig) {
return ( return <Skeleton className="w-full h-10" />;
<Skeleton className="w-full h-10" />
);
} }
if (error) { if (error) {
return ( return <div className="text-sm text-error">{error.message}</div>;
<div className="text-sm text-error">{error.message}</div>
);
} }
return ( return (
@@ -92,30 +88,85 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
disabled={props.disabled ?? false} disabled={props.disabled ?? false}
> >
<p className="truncate"> <p className="truncate">
{selectedModel ? selectedModel.name?.substring(0, 20) : "Select a model..."} {selectedModel
? selectedModel.name?.substring(0, 20)
: "Select a model..."}
</p> </p>
<CaretUpDown className="opacity-50" /> <CaretUpDown className="opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="end" className="w-[250px] p-0"> <PopoverContent align="end" className="w-[250px] p-0">
{ {isMobileWidth ? (
isMobileWidth ? <div>
<Command loop>
<CommandList className="h-[var(--cmdk-list-height)]">
<CommandInput placeholder="Search Models..." />
<CommandEmpty>No Models found.</CommandEmpty>
<CommandGroup key={"models"} heading={"Models"}>
{models &&
models.length > 0 &&
models.map((model) => (
<ModelItem
key={model.id}
model={model}
isSelected={selectedModel?.id === model.id}
onPeek={(model) => setPeekedModel(model)}
onSelect={() => {
setSelectedModel(model);
setOpen(false);
}}
isActive={props.isActive}
/>
))}
</CommandGroup>
</CommandList>
</Command>
</div>
) : (
<HoverCard>
<HoverCardContent
side="left"
align="start"
forceMount
className="min-h-[280px]"
>
<div className="grid gap-2">
<h4 className="font-medium leading-none">
{peekedModel?.name}
</h4>
<div className="text-sm text-muted-foreground">
{peekedModel?.description}
</div>
{peekedModel?.strengths ? (
<div className="mt-4 grid gap-2">
<h5 className="text-sm font-medium leading-none">
Strengths
</h5>
<p className="text-sm text-muted-foreground">
{peekedModel.strengths}
</p>
</div>
) : null}
</div>
</HoverCardContent>
<div> <div>
<HoverCardTrigger />
<Command loop> <Command loop>
<CommandList className="h-[var(--cmdk-list-height)]"> <CommandList className="h-[var(--cmdk-list-height)]">
<CommandInput placeholder="Search Models..." /> <CommandInput placeholder="Search Models..." />
<CommandEmpty>No Models found.</CommandEmpty> <CommandEmpty>No Models found.</CommandEmpty>
<CommandGroup key={"models"} heading={"Models"}> <CommandGroup key={"models"} heading={"Models"}>
{models && models.length > 0 && models {models &&
.map((model) => ( models.length > 0 &&
models.map((model) => (
<ModelItem <ModelItem
key={model.id} key={model.id}
model={model} model={model}
isSelected={selectedModel?.id === model.id} isSelected={selectedModel?.id === model.id}
onPeek={(model) => setPeekedModel(model)} onPeek={(model) => setPeekedModel(model)}
onSelect={() => { onSelect={() => {
setSelectedModel(model) setSelectedModel(model);
setOpen(false) setOpen(false);
}} }}
isActive={props.isActive} isActive={props.isActive}
/> />
@@ -124,74 +175,24 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
</CommandList> </CommandList>
</Command> </Command>
</div> </div>
: </HoverCard>
<HoverCard> )}
<HoverCardContent
side="left"
align="start"
forceMount
className="min-h-[280px]"
>
<div className="grid gap-2">
<h4 className="font-medium leading-none">{peekedModel?.name}</h4>
<div className="text-sm text-muted-foreground">
{peekedModel?.description}
</div>
{peekedModel?.strengths ? (
<div className="mt-4 grid gap-2">
<h5 className="text-sm font-medium leading-none">
Strengths
</h5>
<p className="text-sm text-muted-foreground">
{peekedModel.strengths}
</p>
</div>
) : null}
</div>
</HoverCardContent>
<div>
<HoverCardTrigger />
<Command loop>
<CommandList className="h-[var(--cmdk-list-height)]">
<CommandInput placeholder="Search Models..." />
<CommandEmpty>No Models found.</CommandEmpty>
<CommandGroup key={"models"} heading={"Models"}>
{models && models.length > 0 && models
.map((model) => (
<ModelItem
key={model.id}
model={model}
isSelected={selectedModel?.id === model.id}
onPeek={(model) => setPeekedModel(model)}
onSelect={() => {
setSelectedModel(model)
setOpen(false)
}}
isActive={props.isActive}
/>
))}
</CommandGroup>
</CommandList>
</Command>
</div>
</HoverCard>
}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
) );
} }
interface ModelItemProps { interface ModelItemProps {
model: ModelOptions, model: ModelOptions;
isSelected: boolean, isSelected: boolean;
onSelect: () => void, onSelect: () => void;
onPeek: (model: ModelOptions) => void onPeek: (model: ModelOptions) => void;
isActive?: boolean isActive?: boolean;
} }
function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemProps) { function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemProps) {
const ref = React.useRef<HTMLDivElement>(null) const ref = React.useRef<HTMLDivElement>(null);
useMutationObserver(ref, (mutations) => { useMutationObserver(ref, (mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
@@ -200,10 +201,10 @@ function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemP
mutation.attributeName === "aria-selected" && mutation.attributeName === "aria-selected" &&
ref.current?.getAttribute("aria-selected") === "true" ref.current?.getAttribute("aria-selected") === "true"
) { ) {
onPeek(model) onPeek(model);
} }
}) });
}) });
return ( return (
<CommandItem <CommandItem
@@ -213,10 +214,9 @@ function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemP
className="data-[selected=true]:bg-muted data-[selected=true]:text-secondary-foreground" className="data-[selected=true]:bg-muted data-[selected=true]:text-secondary-foreground"
disabled={!isActive && model.tier !== "free"} disabled={!isActive && model.tier !== "free"}
> >
{model.name} {model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>} {model.name}{" "}
<Check {model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")} <Check className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")} />
/>
</CommandItem> </CommandItem>
) );
} }

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import * as React from "react" import * as React from "react";
export interface LocationData { export interface LocationData {
city?: string; city?: string;
@@ -78,16 +78,16 @@ export const useMutationObserver = (
characterData: true, characterData: true,
childList: true, childList: true,
subtree: true, subtree: true,
} },
) => { ) => {
React.useEffect(() => { React.useEffect(() => {
if (ref.current) { if (ref.current) {
const observer = new MutationObserver(callback) const observer = new MutationObserver(callback);
observer.observe(ref.current, options) observer.observe(ref.current, options);
return () => observer.disconnect() return () => observer.disconnect();
} }
}, [ref, callback, options]) }, [ref, callback, options]);
} };
export function useIsDarkMode() { export function useIsDarkMode() {
const [darkMode, setDarkMode] = useState(false); const [darkMode, setDarkMode] = useState(false);

View File

@@ -1061,12 +1061,27 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
className="h-6 px-2 text-xs" className="h-6 px-2 text-xs"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const filteredFiles = allFileOptions.filter(file => const filteredFiles =
file.toLowerCase().includes(fileSearchValue.toLowerCase()) allFileOptions.filter((file) =>
file
.toLowerCase()
.includes(
fileSearchValue.toLowerCase(),
),
);
const currentFiles =
props.form.getValues("files") ||
[];
const newFiles = [
...new Set([
...currentFiles,
...filteredFiles,
]),
];
props.form.setValue(
"files",
newFiles,
); );
const currentFiles = props.form.getValues("files") || [];
const newFiles = [...new Set([...currentFiles, ...filteredFiles])];
props.form.setValue("files", newFiles);
}} }}
> >
Select All Select All
@@ -1078,12 +1093,28 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
className="h-6 px-2 text-xs" className="h-6 px-2 text-xs"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const filteredFiles = allFileOptions.filter(file => const filteredFiles =
file.toLowerCase().includes(fileSearchValue.toLowerCase()) allFileOptions.filter((file) =>
file
.toLowerCase()
.includes(
fileSearchValue.toLowerCase(),
),
);
const currentFiles =
props.form.getValues("files") ||
[];
const newFiles =
currentFiles.filter(
(file) =>
!filteredFiles.includes(
file,
),
);
props.form.setValue(
"files",
newFiles,
); );
const currentFiles = props.form.getValues("files") || [];
const newFiles = currentFiles.filter(file => !filteredFiles.includes(file));
props.form.setValue("files", newFiles);
}} }}
> >
Deselect All Deselect All

View File

@@ -127,7 +127,7 @@ function renameConversation(conversationId: string, newTitle: string) {
}, },
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { }) .then((data) => {})
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
return; return;
@@ -171,7 +171,7 @@ function deleteConversation(conversationId: string) {
mutate("/api/chat/sessions"); mutate("/api/chat/sessions");
} }
}) })
.then((data) => { }) .then((data) => {})
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
return; return;
@@ -245,9 +245,7 @@ export function FilesMenu(props: FilesMenuProps) {
Context Context
<p> <p>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{ {error ? "Failed to load files" : "Failed to load selected files"}
error ? "Failed to load files" : "Failed to load selected files"
}
</span> </span>
</p> </p>
</h4> </h4>
@@ -257,7 +255,7 @@ export function FilesMenu(props: FilesMenuProps) {
</Button> </Button>
</div> </div>
</div> </div>
) );
} }
if (!files) return <InlineLoading />; if (!files) return <InlineLoading />;
@@ -443,10 +441,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
<div> <div>
{props.sideBarOpen && ( {props.sideBarOpen && (
<ScrollArea> <ScrollArea>
<ScrollAreaScrollbar <ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5" />
orientation="vertical"
className="h-full w-2.5"
/>
<div className="p-0 m-0"> <div className="p-0 m-0">
{props.subsetOrganizedData != null && {props.subsetOrganizedData != null &&
Object.keys(props.subsetOrganizedData) Object.keys(props.subsetOrganizedData)
@@ -471,7 +466,9 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
agent_name={chatHistory.agent_name} agent_name={chatHistory.agent_name}
agent_color={chatHistory.agent_color} agent_color={chatHistory.agent_color}
agent_icon={chatHistory.agent_icon} agent_icon={chatHistory.agent_icon}
agent_is_hidden={chatHistory.agent_is_hidden} agent_is_hidden={
chatHistory.agent_is_hidden
}
/> />
), ),
)} )}
@@ -709,7 +706,7 @@ function ChatSession(props: ChatHistory) {
className="flex items-center gap-2 no-underline" className="flex items-center gap-2 no-underline"
> >
<p <p
className={`${styles.session} ${props.compressed ? styles.compressed : 'max-w-[15rem] md:max-w-[22rem]'}`} className={`${styles.session} ${props.compressed ? styles.compressed : "max-w-[15rem] md:max-w-[22rem]"}`}
> >
{title} {title}
</p> </p>

View File

@@ -48,13 +48,12 @@ async function openChat(userData: UserProfile | null | undefined) {
} }
} }
// Menu items. // Menu items.
const items = [ const items = [
{ {
title: "Home", title: "Home",
url: "/", url: "/",
icon: HouseSimple icon: HouseSimple,
}, },
{ {
title: "Agents", title: "Agents",

View File

@@ -52,7 +52,7 @@ interface TrainOfThoughtFrame {
} }
interface TrainOfThoughtGroup { interface TrainOfThoughtGroup {
type: 'video' | 'text'; type: "video" | "text";
frames?: TrainOfThoughtFrame[]; frames?: TrainOfThoughtFrame[];
textEntries?: TrainOfThoughtObject[]; textEntries?: TrainOfThoughtObject[];
} }
@@ -65,7 +65,9 @@ interface TrainOfThoughtComponentProps {
completed?: boolean; completed?: boolean;
} }
function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): TrainOfThoughtGroup[] { function extractTrainOfThoughtGroups(
trainOfThought?: TrainOfThoughtObject[],
): TrainOfThoughtGroup[] {
if (!trainOfThought) return []; if (!trainOfThought) return [];
const groups: TrainOfThoughtGroup[] = []; const groups: TrainOfThoughtGroup[] = [];
@@ -94,8 +96,8 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
// If we have accumulated text entries, add them as a text group // If we have accumulated text entries, add them as a text group
if (currentTextEntries.length > 0) { if (currentTextEntries.length > 0) {
groups.push({ groups.push({
type: 'text', type: "text",
textEntries: [...currentTextEntries] textEntries: [...currentTextEntries],
}); });
currentTextEntries = []; currentTextEntries = [];
} }
@@ -116,8 +118,8 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
// If we have accumulated video frames, add them as a video group // If we have accumulated video frames, add them as a video group
if (currentVideoFrames.length > 0) { if (currentVideoFrames.length > 0) {
groups.push({ groups.push({
type: 'video', type: "video",
frames: [...currentVideoFrames] frames: [...currentVideoFrames],
}); });
currentVideoFrames = []; currentVideoFrames = [];
} }
@@ -130,14 +132,14 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
// Add any remaining frames/entries // Add any remaining frames/entries
if (currentVideoFrames.length > 0) { if (currentVideoFrames.length > 0) {
groups.push({ groups.push({
type: 'video', type: "video",
frames: currentVideoFrames frames: currentVideoFrames,
}); });
} }
if (currentTextEntries.length > 0) { if (currentTextEntries.length > 0) {
groups.push({ groups.push({
type: 'text', type: "text",
textEntries: currentTextEntries textEntries: currentTextEntries,
}); });
} }
@@ -177,10 +179,10 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
// Convert string array to TrainOfThoughtObject array if needed // Convert string array to TrainOfThoughtObject array if needed
let trainOfThoughtObjects: TrainOfThoughtObject[]; let trainOfThoughtObjects: TrainOfThoughtObject[];
if (typeof props.trainOfThought[0] === 'string') { if (typeof props.trainOfThought[0] === "string") {
trainOfThoughtObjects = (props.trainOfThought as string[]).map((data, index) => ({ trainOfThoughtObjects = (props.trainOfThought as string[]).map((data, index) => ({
type: 'text', type: "text",
data: data data: data,
})); }));
} else { } else {
trainOfThoughtObjects = props.trainOfThought as TrainOfThoughtObject[]; trainOfThoughtObjects = props.trainOfThought as TrainOfThoughtObject[];
@@ -221,28 +223,37 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
<motion.div initial="closed" animate="open" exit="closed" variants={variants}> <motion.div initial="closed" animate="open" exit="closed" variants={variants}>
{trainOfThoughtGroups.map((group, groupIndex) => ( {trainOfThoughtGroups.map((group, groupIndex) => (
<div key={`train-group-${groupIndex}`}> <div key={`train-group-${groupIndex}`}>
{group.type === 'video' && group.frames && group.frames.length > 0 && ( {group.type === "video" &&
<TrainOfThoughtVideoPlayer group.frames &&
frames={group.frames} group.frames.length > 0 && (
autoPlay={false} <TrainOfThoughtVideoPlayer
playbackSpeed={1500} frames={group.frames}
/> autoPlay={false}
)} playbackSpeed={1500}
{group.type === 'text' && group.textEntries && group.textEntries.map((entry, entryIndex) => {
const lastIndex = trainOfThoughtGroups.length - 1;
const isLastGroup = groupIndex === lastIndex;
const isLastEntry = entryIndex === group.textEntries!.length - 1;
const isPrimaryEntry = isLastGroup && isLastEntry && props.lastMessage && !props.completed;
return (
<TrainOfThought
key={`train-text-${groupIndex}-${entryIndex}-${entry.data.length}`}
message={entry.data}
primary={isPrimaryEntry}
agentColor={props.agentColor}
/> />
); )}
})} {group.type === "text" &&
group.textEntries &&
group.textEntries.map((entry, entryIndex) => {
const lastIndex = trainOfThoughtGroups.length - 1;
const isLastGroup = groupIndex === lastIndex;
const isLastEntry =
entryIndex === group.textEntries!.length - 1;
const isPrimaryEntry =
isLastGroup &&
isLastEntry &&
props.lastMessage &&
!props.completed;
return (
<TrainOfThought
key={`train-text-${groupIndex}-${entryIndex}-${entry.data.length}`}
message={entry.data}
primary={isPrimaryEntry}
agentColor={props.agentColor}
/>
);
})}
</div> </div>
))} ))}
</motion.div> </motion.div>
@@ -300,7 +311,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
// ResizeObserver to handle content height changes (e.g., images loading) // ResizeObserver to handle content height changes (e.g., images loading)
useEffect(() => { useEffect(() => {
const contentWrapper = scrollableContentWrapperRef.current; const contentWrapper = scrollableContentWrapperRef.current;
const scrollViewport = scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector); const scrollViewport =
scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector);
if (!contentWrapper || !scrollViewport) return; if (!contentWrapper || !scrollViewport) return;
@@ -308,14 +320,18 @@ export default function ChatHistory(props: ChatHistoryProps) {
// Check current scroll position to decide if auto-scroll is warranted // Check current scroll position to decide if auto-scroll is warranted
const { scrollTop, scrollHeight, clientHeight } = scrollViewport; const { scrollTop, scrollHeight, clientHeight } = scrollViewport;
const bottomThreshold = 50; const bottomThreshold = 50;
const currentlyNearBottom = (scrollHeight - (scrollTop + clientHeight)) <= bottomThreshold; const currentlyNearBottom =
scrollHeight - (scrollTop + clientHeight) <= bottomThreshold;
if (currentlyNearBottom) { if (currentlyNearBottom) {
// Only auto-scroll if there are incoming messages being processed // Only auto-scroll if there are incoming messages being processed
if (props.incomingMessages && props.incomingMessages.length > 0) { if (props.incomingMessages && props.incomingMessages.length > 0) {
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1]; const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
// If the last message is not completed, or it just completed (indicated by incompleteIncomingMessageIndex still being set) // If the last message is not completed, or it just completed (indicated by incompleteIncomingMessageIndex still being set)
if (!lastMessage.completed || (lastMessage.completed && incompleteIncomingMessageIndex !== null)) { if (
!lastMessage.completed ||
(lastMessage.completed && incompleteIncomingMessageIndex !== null)
) {
scrollToBottom(true); // Use instant scroll scrollToBottom(true); // Use instant scroll
} }
} }
@@ -463,7 +479,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
}); });
}); });
// Optimistically set, the scroll listener will verify // Optimistically set, the scroll listener will verify
if (instant || scrollAreaEl && (scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight)) < 5) { if (
instant ||
(scrollAreaEl &&
scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight) <
5)
) {
setIsNearBottom(true); setIsNearBottom(true);
} }
}; };
@@ -626,16 +647,19 @@ export default function ChatHistory(props: ChatHistoryProps) {
conversationId={props.conversationId} conversationId={props.conversationId}
turnId={messageTurnId} turnId={messageTurnId}
/> />
{message.trainOfThought && message.trainOfThought.length > 0 && ( {message.trainOfThought &&
<TrainOfThoughtComponent message.trainOfThought.length > 0 && (
trainOfThought={message.trainOfThought} <TrainOfThoughtComponent
lastMessage={index === incompleteIncomingMessageIndex} trainOfThought={message.trainOfThought}
agentColor={data?.agent?.color || "orange"} lastMessage={
key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map(t => t.length).join('-')}`} index === incompleteIncomingMessageIndex
keyId={`${index}trainOfThought`} }
completed={message.completed} agentColor={data?.agent?.color || "orange"}
/> key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map((t) => t.length).join("-")}`}
)} keyId={`${index}trainOfThought`}
completed={message.completed}
/>
)}
<ChatMessage <ChatMessage
key={`${index}incoming`} key={`${index}incoming`}
isMobileWidth={isMobileWidth} isMobileWidth={isMobileWidth}

View File

@@ -817,38 +817,44 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
/> />
</button> </button>
)} )}
{props.chatMessage.by === "khoj" && props.onRetryMessage && props.isLastMessage && ( {props.chatMessage.by === "khoj" &&
<button props.onRetryMessage &&
title="Retry" props.isLastMessage && (
className={`${styles.retryButton}`} <button
onClick={() => { title="Retry"
const turnId = props.chatMessage.turnId || props.turnId; className={`${styles.retryButton}`}
const query = props.chatMessage.rawQuery || props.chatMessage.intent?.query; onClick={() => {
console.log("Retry button clicked for turnId:", turnId); const turnId = props.chatMessage.turnId || props.turnId;
console.log("ChatMessage data:", { const query =
rawQuery: props.chatMessage.rawQuery, props.chatMessage.rawQuery ||
intent: props.chatMessage.intent, props.chatMessage.intent?.query;
message: props.chatMessage.message console.log("Retry button clicked for turnId:", turnId);
}); console.log("ChatMessage data:", {
console.log("Extracted query:", query); rawQuery: props.chatMessage.rawQuery,
if (query) { intent: props.chatMessage.intent,
props.onRetryMessage?.(query, turnId); message: props.chatMessage.message,
} else { });
console.error("No original query found for retry"); console.log("Extracted query:", query);
// Fallback: try to get from a previous user message or show an input dialog if (query) {
const fallbackQuery = prompt("Enter the original query to retry:"); props.onRetryMessage?.(query, turnId);
if (fallbackQuery) { } else {
props.onRetryMessage?.(fallbackQuery, turnId); console.error("No original query found for retry");
// Fallback: try to get from a previous user message or show an input dialog
const fallbackQuery = prompt(
"Enter the original query to retry:",
);
if (fallbackQuery) {
props.onRetryMessage?.(fallbackQuery, turnId);
}
} }
} }}
}} >
> <ArrowClockwise
<ArrowClockwise alt="Retry Message"
alt="Retry Message" className="hsl(var(--muted-foreground)) hover:text-blue-500"
className="hsl(var(--muted-foreground)) hover:text-blue-500" />
/> </button>
</button> )}
)}
<button <button
title="Copy" title="Copy"
className={`${styles.copyButton}`} className={`${styles.copyButton}`}

View File

@@ -1,10 +1,29 @@
"use client" "use client";
import { ArrowsDownUp, CaretCircleDown, CheckCircle, Circle, CircleNotch, PersonSimpleTaiChi, Sparkle } from "@phosphor-icons/react"; import {
ArrowsDownUp,
CaretCircleDown,
CheckCircle,
Circle,
CircleNotch,
PersonSimpleTaiChi,
Sparkle,
} from "@phosphor-icons/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { ModelSelector } from "@/app/common/modelSelector"; import { ModelSelector } from "@/app/common/modelSelector";
import { FilesMenu } from "../allConversations/allConversations"; import { FilesMenu } from "../allConversations/allConversations";
@@ -14,21 +33,39 @@ import { mutate } from "swr";
import { Sheet, SheetContent } from "@/components/ui/sheet"; import { Sheet, SheetContent } from "@/components/ui/sheet";
import { AgentData } from "../agentCard/agentCard"; import { AgentData } from "../agentCard/agentCard";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getAvailableIcons, getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils"; import {
getAvailableIcons,
getIconForSlashCommand,
getIconFromIconName,
} from "@/app/common/iconUtils";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { TooltipContent } from "@radix-ui/react-tooltip"; import { TooltipContent } from "@radix-ui/react-tooltip";
import { useAuthenticatedData } from "@/app/common/auth"; import { useAuthenticatedData } from "@/app/common/auth";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { convertColorToTextClass, tailwindColors } from "@/app/common/colorUtils"; import { convertColorToTextClass, tailwindColors } from "@/app/common/colorUtils";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
interface ChatSideBarProps { interface ChatSideBarProps {
conversationId: string; conversationId: string;
isOpen: boolean; isOpen: boolean;
@@ -40,15 +77,10 @@ interface ChatSideBarProps {
const fetcher = (url: string) => fetch(url).then((res) => res.json()); const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function ChatSidebar({ ...props }: ChatSideBarProps) { export function ChatSidebar({ ...props }: ChatSideBarProps) {
if (props.isMobileWidth) { if (props.isMobileWidth) {
return ( return (
<Sheet <Sheet open={props.isOpen} onOpenChange={props.onOpenChange}>
open={props.isOpen} <SheetContent className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden">
onOpenChange={props.onOpenChange}>
<SheetContent
className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
>
<ChatSidebarInternal {...props} /> <ChatSidebarInternal {...props} />
</SheetContent> </SheetContent>
</Sheet> </Sheet>
@@ -110,14 +142,14 @@ function AgentCreationForm(props: IAgentCreationProps) {
fetch(createAgentUrl, { fetch(createAgentUrl, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data: AgentData | AgentError) => { .then((data: AgentData | AgentError) => {
console.log("Success:", data); console.log("Success:", data);
if ('detail' in data) { if ("detail" in data) {
setError(`Error creating agent: ${data.detail}`); setError(`Error creating agent: ${data.detail}`);
setIsCreating(false); setIsCreating(false);
return; return;
@@ -142,162 +174,151 @@ function AgentCreationForm(props: IAgentCreationProps) {
}, [customAgentName, customAgentIcon, customAgentColor]); }, [customAgentName, customAgentIcon, customAgentColor]);
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button className="w-full" variant="secondary">
className="w-full"
variant="secondary"
>
Create Agent Create Agent
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
{ {doneCreating && createdSlug ? (
doneCreating && createdSlug ? ( <DialogTitle>Created {customAgentName}</DialogTitle>
<DialogTitle> ) : (
Created {customAgentName} <DialogTitle>Create a New Agent</DialogTitle>
</DialogTitle> )}
) : (
<DialogTitle>
Create a New Agent
</DialogTitle>
)
}
<DialogClose /> <DialogClose />
<DialogDescription> <DialogDescription>
If these settings have been helpful, create a dedicated agent you can re-use across conversations. If these settings have been helpful, create a dedicated agent you can re-use
across conversations.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">
{ {doneCreating && createdSlug ? (
doneCreating && createdSlug ? ( <div className="flex flex-col items-center justify-center gap-4 py-8">
<div className="flex flex-col items-center justify-center gap-4 py-8"> <motion.div
<motion.div initial={{ scale: 0 }}
initial={{ scale: 0 }} animate={{ scale: 1 }}
animate={{ scale: 1 }} transition={{
transition={{ type: "spring",
type: "spring", stiffness: 260,
stiffness: 260, damping: 20,
damping: 20 }}
}} >
> <CheckCircle className="w-16 h-16 text-green-500" weight="fill" />
<CheckCircle </motion.div>
className="w-16 h-16 text-green-500" <motion.p
weight="fill" initial={{ opacity: 0, y: 10 }}
/> animate={{ opacity: 1, y: 0 }}
</motion.div> transition={{ delay: 0.2 }}
<motion.p className="text-center text-lg font-medium text-accent-foreground"
initial={{ opacity: 0, y: 10 }} >
animate={{ opacity: 1, y: 0 }} Created successfully!
transition={{ delay: 0.2 }} </motion.p>
className="text-center text-lg font-medium text-accent-foreground" <motion.div
> initial={{ opacity: 0, y: 10 }}
Created successfully! animate={{ opacity: 1, y: 0 }}
</motion.p> transition={{ delay: 0.4 }}
<motion.div >
initial={{ opacity: 0, y: 10 }} <Link href={`/agents?agent=${createdSlug}`}>
animate={{ opacity: 1, y: 0 }} <Button variant="secondary" className="mt-2">
transition={{ delay: 0.4 }} Manage Agent
> </Button>
<Link href={`/agents?agent=${createdSlug}`}> </Link>
<Button variant="secondary" className="mt-2"> </motion.div>
Manage Agent </div>
</Button> ) : (
</Link> <div className="flex flex-col gap-4">
</motion.div> <div>
<Label htmlFor="agent_name">Name</Label>
<Input
id="agent_name"
className="w-full p-2 border mt-4 border-slate-500 rounded-lg"
disabled={isCreating}
value={customAgentName}
onChange={(e) => setCustomAgentName(e.target.value)}
/>
</div> </div>
) : <div className="flex gap-4">
<div className="flex flex-col gap-4"> <div className="flex-1">
<div> <Select
<Label htmlFor="agent_name">Name</Label> onValueChange={setCustomAgentColor}
<Input defaultValue={customAgentColor}
id="agent_name" >
className="w-full p-2 border mt-4 border-slate-500 rounded-lg" <SelectTrigger
disabled={isCreating} className="w-full dark:bg-muted"
value={customAgentName} disabled={isCreating}
onChange={(e) => setCustomAgentName(e.target.value)} >
/> <SelectValue placeholder="Color" />
</SelectTrigger>
<SelectContent className="items-center space-y-1 inline-flex flex-col">
{colorOptions.map((colorOption) => (
<SelectItem key={colorOption} value={colorOption}>
<div className="flex items-center space-x-2">
<Circle
className={`w-6 h-6 mr-2 ${convertColorToTextClass(colorOption)}`}
weight="fill"
/>
{colorOption}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="flex gap-4"> <div className="flex-1">
<div className="flex-1"> <Select
<Select onValueChange={setCustomAgentColor} defaultValue={customAgentColor}> onValueChange={setCustomAgentIcon}
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}> defaultValue={customAgentIcon}
<SelectValue placeholder="Color" /> >
</SelectTrigger> <SelectTrigger
<SelectContent className="items-center space-y-1 inline-flex flex-col"> className="w-full dark:bg-muted"
{colorOptions.map((colorOption) => ( disabled={isCreating}
<SelectItem key={colorOption} value={colorOption}> >
<div className="flex items-center space-x-2"> <SelectValue placeholder="Icon" />
<Circle </SelectTrigger>
className={`w-6 h-6 mr-2 ${convertColorToTextClass(colorOption)}`} <SelectContent className="items-center space-y-1 inline-flex flex-col">
weight="fill" {iconOptions.map((iconOption) => (
/> <SelectItem key={iconOption} value={iconOption}>
{colorOption} <div className="flex items-center space-x-2">
</div> {getIconFromIconName(
</SelectItem> iconOption,
))} customAgentColor ?? "gray",
</SelectContent> "w-6",
</Select> "h-6",
</div> )}
<div className="flex-1"> {iconOption}
<Select onValueChange={setCustomAgentIcon} defaultValue={customAgentIcon}> </div>
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}> </SelectItem>
<SelectValue placeholder="Icon" /> ))}
</SelectTrigger> </SelectContent>
<SelectContent className="items-center space-y-1 inline-flex flex-col"> </Select>
{iconOptions.map((iconOption) => (
<SelectItem key={iconOption} value={iconOption}>
<div className="flex items-center space-x-2">
{getIconFromIconName(
iconOption,
customAgentColor ?? "gray",
"w-6",
"h-6",
)}
{iconOption}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
</div> </div>
} </div>
)}
</div> </div>
<DialogFooter> <DialogFooter>
{ {error && <div className="text-red-500 text-sm">{error}</div>}
error && ( {!doneCreating && (
<div className="text-red-500 text-sm"> <Button
{error} type="submit"
</div> onClick={() => createAgent()}
) disabled={isCreating || !isValid}
} >
{ {isCreating ? (
!doneCreating && ( <CircleNotch className="animate-spin" />
<Button ) : (
type="submit" <PersonSimpleTaiChi />
onClick={() => createAgent()} )}
disabled={isCreating || !isValid} Create
> </Button>
{ )}
isCreating ?
<CircleNotch className="animate-spin" />
:
<PersonSimpleTaiChi />
}
Create
</Button>
)
}
<DialogClose /> <DialogClose />
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog > </Dialog>
);
)
} }
function ChatSidebarInternal({ ...props }: ChatSideBarProps) { function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
@@ -305,7 +326,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } = const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher); useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher);
const { data: agentData, isLoading: agentDataLoading, error: agentDataError } = useSWR<AgentData>(`/api/agents/conversation?conversation_id=${props.conversationId}`, fetcher); const {
data: agentData,
isLoading: agentDataLoading,
error: agentDataError,
} = useSWR<AgentData>(
`/api/agents/conversation?conversation_id=${props.conversationId}`,
fetcher,
);
const { const {
data: authenticatedData, data: authenticatedData,
error: authenticationError, error: authenticationError,
@@ -317,7 +345,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const [inputTools, setInputTools] = useState<string[] | undefined>(); const [inputTools, setInputTools] = useState<string[] | undefined>();
const [outputModes, setOutputModes] = useState<string[] | undefined>(); const [outputModes, setOutputModes] = useState<string[] | undefined>();
const [hasModified, setHasModified] = useState<boolean>(false); const [hasModified, setHasModified] = useState<boolean>(false);
const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(!agentData || agentData?.slug?.toLowerCase() === "khoj"); const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(
!agentData || agentData?.slug?.toLowerCase() === "khoj",
);
const [displayInputTools, setDisplayInputTools] = useState<string[] | undefined>(); const [displayInputTools, setDisplayInputTools] = useState<string[] | undefined>();
const [displayOutputModes, setDisplayOutputModes] = useState<string[] | undefined>(); const [displayOutputModes, setDisplayOutputModes] = useState<string[] | undefined>();
@@ -330,12 +360,20 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
setInputTools(agentData.input_tools); setInputTools(agentData.input_tools);
setDisplayInputTools(agentData.input_tools); setDisplayInputTools(agentData.input_tools);
if (agentData.input_tools === undefined || agentData.input_tools.length === 0) { if (agentData.input_tools === undefined || agentData.input_tools.length === 0) {
setDisplayInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []); setDisplayInputTools(
agentConfigurationOptions?.input_tools
? Object.keys(agentConfigurationOptions.input_tools)
: [],
);
} }
setOutputModes(agentData.output_modes); setOutputModes(agentData.output_modes);
setDisplayOutputModes(agentData.output_modes); setDisplayOutputModes(agentData.output_modes);
if (agentData.output_modes === undefined || agentData.output_modes.length === 0) { if (agentData.output_modes === undefined || agentData.output_modes.length === 0) {
setDisplayOutputModes(agentConfigurationOptions?.output_modes ? Object.keys(agentConfigurationOptions.output_modes) : []); setDisplayOutputModes(
agentConfigurationOptions?.output_modes
? Object.keys(agentConfigurationOptions.output_modes)
: [],
);
} }
if (agentData.name?.toLowerCase() === "khoj" || agentData.is_hidden === true) { if (agentData.name?.toLowerCase() === "khoj" || agentData.is_hidden === true) {
@@ -367,8 +405,12 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const promptChanged = !!customPrompt && customPrompt !== agentData.persona; const promptChanged = !!customPrompt && customPrompt !== agentData.persona;
// Order independent check to ensure input tools or output modes haven't been changed. // Order independent check to ensure input tools or output modes haven't been changed.
const toolsChanged = JSON.stringify(inputTools?.sort() || []) !== JSON.stringify(agentData.input_tools?.sort()); const toolsChanged =
const modesChanged = JSON.stringify(outputModes?.sort() || []) !== JSON.stringify(agentData.output_modes?.sort()); JSON.stringify(inputTools?.sort() || []) !==
JSON.stringify(agentData.input_tools?.sort());
const modesChanged =
JSON.stringify(outputModes?.sort() || []) !==
JSON.stringify(agentData.output_modes?.sort());
setHasModified(modelChanged || promptChanged || toolsChanged || modesChanged); setHasModified(modelChanged || promptChanged || toolsChanged || modesChanged);
@@ -394,7 +436,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
function handleSave() { function handleSave() {
if (hasModified) { if (hasModified) {
if (!isDefaultAgent && agentData?.is_hidden === false) { if (!isDefaultAgent && agentData?.is_hidden === false) {
alert("This agent is not a hidden agent. It cannot be modified from this interface."); alert(
"This agent is not a hidden agent. It cannot be modified from this interface.",
);
return; return;
} }
@@ -409,12 +453,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
chat_model: selectedModel, chat_model: selectedModel,
input_tools: inputTools, input_tools: inputTools,
output_modes: outputModes, output_modes: outputModes,
...(isDefaultAgent ? {} : { slug: agentData?.slug }) ...(isDefaultAgent ? {} : { slug: agentData?.slug }),
}; };
setIsSaving(true); setIsSaving(true);
const url = !isDefaultAgent ? `/api/agents/hidden` : `/api/agents/hidden?conversation_id=${props.conversationId}`; const url = !isDefaultAgent
? `/api/agents/hidden`
: `/api/agents/hidden?conversation_id=${props.conversationId}`;
// There are four scenarios here. // There are four scenarios here.
// 1. If the agent is a default agent, then we need to create a new agent just to associate with this conversation. // 1. If the agent is a default agent, then we need to create a new agent just to associate with this conversation.
@@ -424,13 +470,13 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
fetch(url, { fetch(url, {
method: mode, method: mode,
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
}) })
.then((res) => { .then((res) => {
setIsSaving(false); setIsSaving(false);
res.json() res.json();
}) })
.then((data) => { .then((data) => {
mutate(`/api/agents/conversation?conversation_id=${props.conversationId}`); mutate(`/api/agents/conversation?conversation_id=${props.conversationId}`);
@@ -456,43 +502,47 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Sidebar <Sidebar
collapsible="none" collapsible="none"
className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out
${props.isOpen ${
? "translate-x-0 opacity-100 w-[300px] relative" props.isOpen
: "translate-x-full opacity-100 w-0 p-0 m-0"} ? "translate-x-0 opacity-100 w-[300px] relative"
: "translate-x-full opacity-100 w-0 p-0 m-0"
}
`} `}
variant="floating"> variant="floating"
>
<SidebarContent> <SidebarContent>
<SidebarHeader> <SidebarHeader>
{ {agentData && !isEditable ? (
agentData && !isEditable ? ( <div className="flex items-center relative text-sm">
<div className="flex items-center relative text-sm"> <a
<a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}> className="text-lg font-bold flex flex-row items-center"
{getIconFromIconName(agentData.icon, agentData.color)} href={`/agents?agent=${agentData.slug}`}
{agentData.name} >
</a> {getIconFromIconName(agentData.icon, agentData.color)}
</div> {agentData.name}
) : ( </a>
<div className="flex items-center relative text-sm justify-between"> </div>
<p> ) : (
Chat Options <div className="flex items-center relative text-sm justify-between">
</p> <p>Chat Options</p>
</div> </div>
) )}
}
</SidebarHeader> </SidebarHeader>
<SidebarGroup key={"knowledge"} className="border-b last:border-none"> <SidebarGroup key={"knowledge"} className="border-b last:border-none">
<SidebarGroupContent className="gap-0"> <SidebarGroupContent className="gap-0">
<SidebarMenu className="p-0 m-0"> <SidebarMenu className="p-0 m-0">
{ {agentData && agentData.has_files ? (
agentData && agentData.has_files ? ( <SidebarMenuItem key={"agent_knowledge"} className="list-none">
<SidebarMenuItem key={"agent_knowledge"} className="list-none"> <div className="flex items-center space-x-2 rounded-full">
<div className="flex items-center space-x-2 rounded-full"> <div className="text-muted-foreground">
<div className="text-muted-foreground"><Sparkle /></div> <Sparkle />
<div className="text-muted-foreground text-sm">Using custom knowledge base</div>
</div> </div>
</SidebarMenuItem> <div className="text-muted-foreground text-sm">
) : null Using custom knowledge base
} </div>
</div>
</SidebarMenuItem>
) : null}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
@@ -506,39 +556,41 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
value={customPrompt || ""} value={customPrompt || ""}
onChange={(e) => handleCustomPromptChange(e.target.value)} onChange={(e) => handleCustomPromptChange(e.target.value)}
readOnly={!isEditable} readOnly={!isEditable}
disabled={!isEditable} /> disabled={!isEditable}
/>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
{ {!agentDataLoading && agentData && (
!agentDataLoading && agentData && ( <SidebarGroup key={"model"}>
<SidebarGroup key={"model"}> <SidebarGroupContent>
<SidebarGroupContent> <SidebarGroupLabel>
<SidebarGroupLabel> Model
Model {!isSubscribed && (
{ <a
!isSubscribed && ( href="/settings"
<a href="/settings" className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"> className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"
Upgrade >
</a> Upgrade
) </a>
} )}
</SidebarGroupLabel> </SidebarGroupLabel>
<SidebarMenu className="p-0 m-0"> <SidebarMenu className="p-0 m-0">
<SidebarMenuItem key={"model"} className="list-none"> <SidebarMenuItem key={"model"} className="list-none">
<ModelSelector <ModelSelector
disabled={!isEditable} disabled={!isEditable}
onSelect={(model) => handleModelSelect(model.name)} onSelect={(model) => handleModelSelect(model.name)}
initialModel={isDefaultAgent ? undefined : agentData?.chat_model} initialModel={
isActive={props.isActive} isDefaultAgent ? undefined : agentData?.chat_model
/> }
</SidebarMenuItem> isActive={props.isActive}
</SidebarMenu> />
</SidebarGroupContent> </SidebarMenuItem>
</SidebarGroup> </SidebarMenu>
) </SidebarGroupContent>
} </SidebarGroup>
)}
<Popover defaultOpen={false}> <Popover defaultOpen={false}>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel asChild> <SidebarGroupLabel asChild>
@@ -550,82 +602,118 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<PopoverContent> <PopoverContent>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu className="p-1 m-0"> <SidebarMenu className="p-1 m-0">
{ {Object.entries(
Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => { agentConfigurationOptions?.input_tools ?? {},
return ( ).map(([key, value]) => {
<SidebarMenuItem key={key} className="list-none"> return (
<Tooltip> <SidebarMenuItem key={key} className="list-none">
<TooltipTrigger key={key} asChild> <Tooltip>
<div className="flex items-center space-x-2 py-1 justify-between"> <TooltipTrigger key={key} asChild>
<Label htmlFor={key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"> <div className="flex items-center space-x-2 py-1 justify-between">
{getIconForSlashCommand(key)} <Label
<p className="text-sm my-auto flex items-center"> htmlFor={key}
{key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"
</p> >
</Label> {getIconForSlashCommand(key)}
<Checkbox <p className="text-sm my-auto flex items-center">
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, displayInputTools ?? [])}
onCheckedChange={() => {
let updatedInputTools = handleCheckToggle(key, displayInputTools ?? [])
setInputTools(updatedInputTools);
setDisplayInputTools(updatedInputTools);
}}
disabled={!isEditable}
>
{key} {key}
</Checkbox> </p>
</div> </Label>
</TooltipTrigger> <Checkbox
<TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> id={key}
{value} className={`${isEditable ? "cursor-pointer" : ""}`}
</TooltipContent> checked={isValueChecked(
</Tooltip> key,
</SidebarMenuItem> displayInputTools ?? [],
); )}
} onCheckedChange={() => {
) let updatedInputTools =
} handleCheckToggle(
{ key,
Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => { displayInputTools ?? [],
return ( );
<SidebarMenuItem key={key} className="list-none"> setInputTools(
<Tooltip> updatedInputTools,
<TooltipTrigger key={key} asChild> );
<div className="flex items-center space-x-2 py-1 justify-between"> setDisplayInputTools(
<Label htmlFor={key} className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"> updatedInputTools,
{getIconForSlashCommand(key)} );
<p className="text-sm my-auto flex items-center"> }}
{key} disabled={!isEditable}
</p> >
</Label> {key}
<Checkbox </Checkbox>
id={key} </div>
className={`${isEditable ? "cursor-pointer" : ""}`} </TooltipTrigger>
checked={isValueChecked(key, displayOutputModes ?? [])} <TooltipContent
onCheckedChange={() => { sideOffset={5}
let updatedOutputModes = handleCheckToggle(key, displayOutputModes ?? []) side="left"
setOutputModes(updatedOutputModes); align="start"
setDisplayOutputModes(updatedOutputModes); className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"
}} >
disabled={!isEditable} {value}
> </TooltipContent>
</Tooltip>
</SidebarMenuItem>
);
})}
{Object.entries(
agentConfigurationOptions?.output_modes ?? {},
).map(([key, value]) => {
return (
<SidebarMenuItem key={key} className="list-none">
<Tooltip>
<TooltipTrigger key={key} asChild>
<div className="flex items-center space-x-2 py-1 justify-between">
<Label
htmlFor={key}
className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"
>
{getIconForSlashCommand(key)}
<p className="text-sm my-auto flex items-center">
{key} {key}
</Checkbox> </p>
</div> </Label>
</TooltipTrigger> <Checkbox
<TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"> id={key}
{value} className={`${isEditable ? "cursor-pointer" : ""}`}
</TooltipContent> checked={isValueChecked(
</Tooltip> key,
</SidebarMenuItem> displayOutputModes ?? [],
); )}
} onCheckedChange={() => {
) let updatedOutputModes =
} handleCheckToggle(
key,
displayOutputModes ??
[],
);
setOutputModes(
updatedOutputModes,
);
setDisplayOutputModes(
updatedOutputModes,
);
}}
disabled={!isEditable}
>
{key}
</Checkbox>
</div>
</TooltipTrigger>
<TooltipContent
sideOffset={5}
side="left"
align="start"
className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"
>
{value}
</TooltipContent>
</Tooltip>
</SidebarMenuItem>
);
})}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</PopoverContent> </PopoverContent>
</SidebarGroup> </SidebarGroup>
@@ -645,79 +733,75 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
{ {props.isOpen && (
props.isOpen && ( <SidebarFooter key={"actions"}>
<SidebarFooter key={"actions"}> <SidebarMenu className="p-0 m-0">
<SidebarMenu className="p-0 m-0"> {agentData && !isEditable && agentData.is_creator ? (
<SidebarMenuItem>
{ <SidebarMenuButton asChild>
(agentData && !isEditable && agentData.is_creator) ? ( <Button
<SidebarMenuItem> className="w-full"
<SidebarMenuButton asChild> variant={"ghost"}
<Button onClick={() =>
className="w-full" (window.location.href = `/agents?agent=${agentData?.slug}`)
variant={"ghost"}
onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`}
>
Manage
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
) :
<>
{
!hasModified && isEditable && customPrompt && !isDefaultAgent && selectedModel && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<AgentCreationForm
customPrompt={customPrompt}
selectedModel={selectedModel}
inputTools={displayInputTools ?? []}
outputModes={displayOutputModes ?? []}
/>
</SidebarMenuButton>
</SidebarMenuItem>
)
} }
>
Manage
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
) : (
<>
{!hasModified &&
isEditable &&
customPrompt &&
!isDefaultAgent &&
selectedModel && (
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<Button <AgentCreationForm
className="w-full" customPrompt={customPrompt}
onClick={() => handleReset()} selectedModel={selectedModel}
variant={"ghost"} inputTools={displayInputTools ?? []}
disabled={!isEditable || !hasModified} outputModes={displayOutputModes ?? []}
> />
Reset
</Button>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
<SidebarMenuItem> )}
<SidebarMenuButton asChild> <SidebarMenuItem>
<Button <SidebarMenuButton asChild>
className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`} <Button
variant={"secondary"} className="w-full"
onClick={() => handleSave()} onClick={() => handleReset()}
disabled={!isEditable || !hasModified || isSaving} variant={"ghost"}
> disabled={!isEditable || !hasModified}
{ >
isSaving ? Reset
<CircleNotch className="animate-spin" /> </Button>
: </SidebarMenuButton>
<ArrowsDownUp /> </SidebarMenuItem>
} <SidebarMenuItem>
{ <SidebarMenuButton asChild>
isSaving ? "Saving" : "Save" <Button
} className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`}
</Button> variant={"secondary"}
</SidebarMenuButton> onClick={() => handleSave()}
</SidebarMenuItem> disabled={!isEditable || !hasModified || isSaving}
>
</> {isSaving ? (
} <CircleNotch className="animate-spin" />
</SidebarMenu> ) : (
</SidebarFooter> <ArrowsDownUp />
) )}
} {isSaving ? "Saving" : "Save"}
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
</>
)}
</SidebarMenu>
</SidebarFooter>
)}
</Sidebar> </Sidebar>
) );
} }

View File

@@ -147,9 +147,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
<span>{mermaidError}</span> <span>{mermaidError}</span>
</div> </div>
<code className="block bg-secondary text-secondary-foreground p-4 mt-3 rounded-lg font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-[400px] border border-gray-200"> <code className="block bg-secondary text-secondary-foreground p-4 mt-3 rounded-lg font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-[400px] border border-gray-200">
{ {chart}
chart
}
</code> </code>
</> </>
) : ( ) : (

View File

@@ -12,7 +12,15 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Moon, Sun, UserCircle, Question, ArrowRight, Code, BuildingOffice } from "@phosphor-icons/react"; import {
Moon,
Sun,
UserCircle,
Question,
ArrowRight,
Code,
BuildingOffice,
} from "@phosphor-icons/react";
import { useIsDarkMode, useIsMobileWidth } from "@/app/common/utils"; import { useIsDarkMode, useIsMobileWidth } from "@/app/common/utils";
import LoginPrompt from "../loginPrompt/loginPrompt"; import LoginPrompt from "../loginPrompt/loginPrompt";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -69,7 +77,7 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
icon: <BuildingOffice className="w-6 h-6" />, icon: <BuildingOffice className="w-6 h-6" />,
link: "https://khoj.dev/teams", link: "https://khoj.dev/teams",
}, },
] ];
return ( return (
<SidebarMenu className="border-none p-0 m-0"> <SidebarMenu className="border-none p-0 m-0">
@@ -131,18 +139,16 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
</p> </p>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
{ {menuItems.map((menuItem, index) => (
menuItems.map((menuItem, index) => ( <DropdownMenuItem key={index}>
<DropdownMenuItem key={index}> <Link href={menuItem.link} className="no-underline w-full">
<Link href={menuItem.link} className="no-underline w-full"> <div className="flex flex-rows">
<div className="flex flex-rows"> {menuItem.icon}
{menuItem.icon} <p className="ml-3 font-semibold">{menuItem.title}</p>
<p className="ml-3 font-semibold">{menuItem.title}</p> </div>
</div> </Link>
</Link> </DropdownMenuItem>
</DropdownMenuItem> ))}
))
}
{!userData ? ( {!userData ? (
<DropdownMenuItem> <DropdownMenuItem>
<Button <Button

View File

@@ -1,6 +1,6 @@
'use client' "use client";
import { useIsDarkMode } from '@/app/common/utils' import { useIsDarkMode } from "@/app/common/utils";
export function ThemeProvider({ children }: { children: React.ReactNode }) { export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [darkMode, setDarkMode] = useIsDarkMode(); const [darkMode, setDarkMode] = useIsDarkMode();

View File

@@ -508,7 +508,7 @@ function FileFilterComboBox(props: FileFilterComboBoxProps) {
}, [open]); }, [open]);
return ( return (
<Popover open={open || (noMatchingFiles && (!!inputText))} onOpenChange={setOpen}> <Popover open={open || (noMatchingFiles && !!inputText)} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -521,14 +521,18 @@ function FileFilterComboBox(props: FileFilterComboBoxProps) {
? "✔️" ? "✔️"
: "Selected" : "Selected"
: props.isMobileWidth : props.isMobileWidth
? " " ? " "
: "Select file"} : "Select file"}
<Funnel className="opacity-50" /> <Funnel className="opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[200px] p-0"> <PopoverContent className="w-[200px] p-0">
<Command> <Command>
<CommandInput placeholder="Search files..." value={inputText} onInput={(e) => setInputText(e.currentTarget.value)} /> <CommandInput
placeholder="Search files..."
value={inputText}
onInput={(e) => setInputText(e.currentTarget.value)}
/>
<CommandList> <CommandList>
<CommandEmpty>No files found.</CommandEmpty> <CommandEmpty>No files found.</CommandEmpty>
<CommandGroup> <CommandGroup>
@@ -614,7 +618,6 @@ export default function Search() {
setSelectedFileFilter("INITIALIZE"); setSelectedFileFilter("INITIALIZE");
} }
} }
}, [searchQuery]); }, [searchQuery]);
function handleSearchInputChange(value: string) { function handleSearchInputChange(value: string) {

View File

@@ -23,8 +23,15 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialog,
AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
@@ -72,7 +79,7 @@ import { KhojLogoType } from "../components/logo/khojLogo";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import JSZip from "jszip"; import JSZip from "jszip";
import { saveAs } from 'file-saver'; import { saveAs } from "file-saver";
interface DropdownComponentProps { interface DropdownComponentProps {
items: ModelOptions[]; items: ModelOptions[];
@@ -81,7 +88,12 @@ interface DropdownComponentProps {
callbackFunc: (value: string) => Promise<void>; callbackFunc: (value: string) => Promise<void>;
} }
const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected, isActive, callbackFunc }) => { const DropdownComponent: React.FC<DropdownComponentProps> = ({
items,
selected,
isActive,
callbackFunc,
}) => {
const [position, setPosition] = useState(selected?.toString() ?? "0"); const [position, setPosition] = useState(selected?.toString() ?? "0");
return ( return (
@@ -114,7 +126,10 @@ const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected,
value={item.id.toString()} value={item.id.toString()}
disabled={!isActive && item.tier !== "free"} disabled={!isActive && item.tier !== "free"}
> >
{item.name} {item.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>} {item.name}{" "}
{item.tier === "standard" && (
<span className="text-green-500 ml-2">(Futurist)</span>
)}
</DropdownMenuRadioItem> </DropdownMenuRadioItem>
))} ))}
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
@@ -327,8 +342,8 @@ export default function SettingsView() {
initialUserConfig?.is_phone_number_verified initialUserConfig?.is_phone_number_verified
? PhoneNumberValidationState.Verified ? PhoneNumberValidationState.Verified
: initialUserConfig?.phone_number : initialUserConfig?.phone_number
? PhoneNumberValidationState.SendOTP ? PhoneNumberValidationState.SendOTP
: PhoneNumberValidationState.Setup, : PhoneNumberValidationState.Setup,
); );
setName(initialUserConfig?.given_name); setName(initialUserConfig?.given_name);
setNotionToken(initialUserConfig?.notion_token ?? null); setNotionToken(initialUserConfig?.notion_token ?? null);
@@ -524,13 +539,14 @@ export default function SettingsView() {
const updateModel = (modelType: string) => async (id: string) => { const updateModel = (modelType: string) => async (id: string) => {
// Get the selected model from the options // Get the selected model from the options
const modelOptions = modelType === "chat" const modelOptions =
? userConfig?.chat_model_options modelType === "chat"
: modelType === "paint" ? userConfig?.chat_model_options
? userConfig?.paint_model_options : modelType === "paint"
: userConfig?.voice_model_options; ? userConfig?.paint_model_options
: userConfig?.voice_model_options;
const selectedModel = modelOptions?.find(model => model.id.toString() === id); const selectedModel = modelOptions?.find((model) => model.id.toString() === id);
const modelName = selectedModel?.name; const modelName = selectedModel?.name;
// Check if the model is free tier or if the user is active // Check if the model is free tier or if the user is active
@@ -551,7 +567,8 @@ export default function SettingsView() {
}, },
}); });
if (!response.ok) throw new Error(`Failed to switch ${modelType} model to ${modelName}`); if (!response.ok)
throw new Error(`Failed to switch ${modelType} model to ${modelName}`);
toast({ toast({
title: `✅ Switched ${modelType} model to ${modelName}`, title: `✅ Switched ${modelType} model to ${modelName}`,
@@ -570,7 +587,7 @@ export default function SettingsView() {
setIsExporting(true); setIsExporting(true);
// Get total conversation count // Get total conversation count
const statsResponse = await fetch('/api/chat/stats'); const statsResponse = await fetch("/api/chat/stats");
const stats = await statsResponse.json(); const stats = await statsResponse.json();
const total = stats.num_conversations; const total = stats.num_conversations;
setTotalConversations(total); setTotalConversations(total);
@@ -586,7 +603,7 @@ export default function SettingsView() {
conversations.push(...data); conversations.push(...data);
setExportedConversations((page + 1) * 10); setExportedConversations((page + 1) * 10);
setExportProgress(((page + 1) * 10 / total) * 100); setExportProgress((((page + 1) * 10) / total) * 100);
} }
// Add conversations to zip // Add conversations to zip
@@ -605,7 +622,7 @@ export default function SettingsView() {
toast({ toast({
title: "Export Failed", title: "Export Failed",
description: "Failed to export chats. Please try again.", description: "Failed to export chats. Please try again.",
variant: "destructive" variant: "destructive",
}); });
} finally { } finally {
setIsExporting(false); setIsExporting(false);
@@ -808,93 +825,93 @@ export default function SettingsView() {
)) || )) ||
(userConfig.subscription_state === (userConfig.subscription_state ===
"subscribed" && ( "subscribed" && (
<> <>
<p className="text-xl text-primary/80"> <p className="text-xl text-primary/80">
Futurist Futurist
</p> </p>
<p className="text-gray-400"> <p className="text-gray-400">
Subscription <b>renews</b> on{" "} Subscription <b>renews</b> on{" "}
<b> <b>
{ {
userConfig.subscription_renewal_date userConfig.subscription_renewal_date
} }
</b> </b>
</p> </p>
</> </>
)) || )) ||
(userConfig.subscription_state === (userConfig.subscription_state ===
"unsubscribed" && ( "unsubscribed" && (
<> <>
<p className="text-xl">Futurist</p> <p className="text-xl">Futurist</p>
<p className="text-gray-400">
Subscription <b>ends</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
</>
)) ||
(userConfig.subscription_state ===
"expired" && (
<>
<p className="text-xl">Humanist</p>
{(userConfig.subscription_renewal_date && (
<p className="text-gray-400"> <p className="text-gray-400">
Subscription <b>ends</b> on{" "} Subscription <b>expired</b>{" "}
on{" "}
<b> <b>
{ {
userConfig.subscription_renewal_date userConfig.subscription_renewal_date
} }
</b> </b>
</p> </p>
</> )) || (
)) || <p className="text-gray-400">
(userConfig.subscription_state === Check{" "}
"expired" && ( <a
<> href="https://khoj.dev/#pricing"
<p className="text-xl">Humanist</p> target="_blank"
{(userConfig.subscription_renewal_date && ( >
<p className="text-gray-400"> pricing page
Subscription <b>expired</b>{" "} </a>{" "}
on{" "} to compare plans.
<b> </p>
{ )}
userConfig.subscription_renewal_date </>
} ))}
</b>
</p>
)) || (
<p className="text-gray-400">
Check{" "}
<a
href="https://khoj.dev/#pricing"
target="_blank"
>
pricing page
</a>{" "}
to compare plans.
</p>
)}
</>
))}
</CardContent> </CardContent>
<CardFooter className="flex flex-wrap gap-4"> <CardFooter className="flex flex-wrap gap-4">
{(userConfig.subscription_state == {(userConfig.subscription_state ==
"subscribed" && ( "subscribed" && (
<Button <Button
variant="outline" variant="outline"
className="hover:text-red-400" className="hover:text-red-400"
onClick={() => onClick={() =>
setSubscription("cancel") setSubscription("cancel")
} }
> >
<ArrowCircleDown className="h-5 w-5 mr-2" /> <ArrowCircleDown className="h-5 w-5 mr-2" />
Unsubscribe Unsubscribe
</Button> </Button>
)) || )) ||
(userConfig.subscription_state == (userConfig.subscription_state ==
"unsubscribed" && ( "unsubscribed" && (
<Button <Button
variant="outline" variant="outline"
className="text-primary/80 hover:text-primary" className="text-primary/80 hover:text-primary"
onClick={() => onClick={() =>
setSubscription("resubscribe") setSubscription("resubscribe")
} }
> >
<ArrowCircleUp <ArrowCircleUp
weight="bold" weight="bold"
className="h-5 w-5 mr-2" className="h-5 w-5 mr-2"
/> />
Resubscribe Resubscribe
</Button> </Button>
)) || )) ||
(userConfig.subscription_enabled_trial_at && ( (userConfig.subscription_enabled_trial_at && (
<Button <Button
variant="outline" variant="outline"
@@ -984,16 +1001,16 @@ export default function SettingsView() {
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
{(userConfig.enabled_content_source {(userConfig.enabled_content_source
.github && ( .github && (
<> <>
<Files className="h-5 w-5 inline mr-1" /> <Files className="h-5 w-5 inline mr-1" />
Manage Manage
</> </>
)) || ( )) || (
<> <>
<Plugs className="h-5 w-5 inline mr-1" /> <Plugs className="h-5 w-5 inline mr-1" />
Connect Connect
</> </>
)} )}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -1035,8 +1052,8 @@ export default function SettingsView() {
{ {
/* Show connect to notion button if notion oauth url setup and user disconnected*/ /* Show connect to notion button if notion oauth url setup and user disconnected*/
userConfig.notion_oauth_url && userConfig.notion_oauth_url &&
!userConfig.enabled_content_source !userConfig.enabled_content_source
.notion ? ( .notion ? (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -1050,39 +1067,39 @@ export default function SettingsView() {
Connect Connect
</Button> </Button>
) : /* Show sync button if user connected to notion and API key unchanged */ ) : /* Show sync button if user connected to notion and API key unchanged */
userConfig.enabled_content_source.notion && userConfig.enabled_content_source.notion &&
notionToken === notionToken ===
userConfig.notion_token ? ( userConfig.notion_token ? (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => onClick={() =>
syncContent("notion") syncContent("notion")
} }
> >
<ArrowsClockwise className="h-5 w-5 inline mr-1" /> <ArrowsClockwise className="h-5 w-5 inline mr-1" />
Sync Sync
</Button> </Button>
) : /* Show set API key button notion oauth url not set setup */ ) : /* Show set API key button notion oauth url not set setup */
!userConfig.notion_oauth_url ? ( !userConfig.notion_oauth_url ? (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={saveNotionToken} onClick={saveNotionToken}
disabled={ disabled={
notionToken === notionToken ===
userConfig.notion_token userConfig.notion_token
} }
> >
<FloppyDisk className="h-5 w-5 inline mr-1" /> <FloppyDisk className="h-5 w-5 inline mr-1" />
{(userConfig.enabled_content_source {(userConfig.enabled_content_source
.notion && .notion &&
"Update API Key") || "Update API Key") ||
"Set API Key"} "Set API Key"}
</Button> </Button>
) : ( ) : (
<></> <></>
) )
} }
<Button <Button
variant="outline" variant="outline"
@@ -1123,7 +1140,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4"> <CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && ( {!userConfig.is_active && (
<p className="text-gray-400"> <p className="text-gray-400">
{userConfig.chat_model_options.some(model => model.tier === "free") {userConfig.chat_model_options.some(
(model) =>
model.tier === "free",
)
? "Free models available" ? "Free models available"
: "Subscribe to switch model"} : "Subscribe to switch model"}
</p> </p>
@@ -1154,7 +1174,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4"> <CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && ( {!userConfig.is_active && (
<p className="text-gray-400"> <p className="text-gray-400">
{userConfig.paint_model_options.some(model => model.tier === "free") {userConfig.paint_model_options.some(
(model) =>
model.tier === "free",
)
? "Free models available" ? "Free models available"
: "Subscribe to switch model"} : "Subscribe to switch model"}
</p> </p>
@@ -1185,7 +1208,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4"> <CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && ( {!userConfig.is_active && (
<p className="text-gray-400"> <p className="text-gray-400">
{userConfig.voice_model_options.some(model => model.tier === "free") {userConfig.voice_model_options.some(
(model) =>
model.tier === "free",
)
? "Free models available" ? "Free models available"
: "Subscribe to switch model"} : "Subscribe to switch model"}
</p> </p>
@@ -1219,9 +1245,13 @@ export default function SettingsView() {
</p> </p>
{exportProgress > 0 && ( {exportProgress > 0 && (
<div className="w-full mt-4"> <div className="w-full mt-4">
<Progress value={exportProgress} className="w-full" /> <Progress
value={exportProgress}
className="w-full"
/>
<p className="text-sm text-gray-500 mt-2"> <p className="text-sm text-gray-500 mt-2">
Exported {exportedConversations} of {totalConversations} conversations Exported {exportedConversations} of{" "}
{totalConversations} conversations
</p> </p>
</div> </div>
)} )}
@@ -1233,7 +1263,9 @@ export default function SettingsView() {
disabled={isExporting} disabled={isExporting}
> >
<Download className="h-5 w-5 mr-2" /> <Download className="h-5 w-5 mr-2" />
{isExporting ? "Exporting..." : "Export Chats"} {isExporting
? "Exporting..."
: "Export Chats"}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@@ -1245,7 +1277,11 @@ export default function SettingsView() {
</CardHeader> </CardHeader>
<CardContent className="overflow-hidden"> <CardContent className="overflow-hidden">
<p className="pb-4 text-gray-400"> <p className="pb-4 text-gray-400">
This will delete all your account data, including conversations, agents, and any assets you{"'"}ve generated. Be sure to export before you do this if you want to keep your information. This will delete all your account data,
including conversations, agents, and any
assets you{"'"}ve generated. Be sure to
export before you do this if you want to
keep your information.
</p> </p>
</CardContent> </CardContent>
<CardFooter className="flex flex-wrap gap-4"> <CardFooter className="flex flex-wrap gap-4">
@@ -1261,36 +1297,56 @@ export default function SettingsView() {
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action is irreversible. This will permanently delete your account This action is irreversible.
and remove all your data from our servers. This will permanently delete
your account and remove all your
data from our servers.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className="bg-red-500 hover:bg-red-600" className="bg-red-500 hover:bg-red-600"
onClick={async () => { onClick={async () => {
try { try {
const response = await fetch('/api/self', { const response =
method: 'DELETE' await fetch(
}); "/api/self",
if (!response.ok) throw new Error('Failed to delete account'); {
method: "DELETE",
},
);
if (!response.ok)
throw new Error(
"Failed to delete account",
);
toast({ toast({
title: "Account Deleted", title: "Account Deleted",
description: "Your account has been successfully deleted.", description:
"Your account has been successfully deleted.",
}); });
// Redirect to home page after successful deletion // Redirect to home page after successful deletion
window.location.href = "/"; window.location.href =
"/";
} catch (error) { } catch (error) {
console.error('Error deleting account:', error); console.error(
"Error deleting account:",
error,
);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to delete account. Please try again or contact support.", description:
variant: "destructive" "Failed to delete account. Please try again or contact support.",
variant:
"destructive",
}); });
} }
}} }}

View File

@@ -1,30 +1,30 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react" import { Check } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-secondary-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground", "peer h-4 w-4 shrink-0 rounded-sm border border-secondary-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground",
className className,
)} )}
{...props} {...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
> >
<Check className="h-4 w-4" /> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Indicator> className={cn("flex items-center justify-center text-current")}
</CheckboxPrimitive.Root> >
)) <Check className="h-4 w-4" />
Checkbox.displayName = CheckboxPrimitive.Root.displayName </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

View File

@@ -1,29 +1,29 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card" import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef< const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>, React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content <HoverCardPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
)) ));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent } export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -1,118 +1,99 @@
import * as React from "react" import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button" import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)} className={cn("mx-auto flex w-full justify-center", className)}
{...props} {...props}
/> />
) );
Pagination.displayName = "Pagination" Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef< const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
HTMLUListElement, ({ className, ...props }, ref) => (
React.ComponentProps<"ul"> <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<ul );
ref={ref} PaginationContent.displayName = "PaginationContent";
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef< const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
HTMLLIElement, ({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
React.ComponentProps<"li"> );
>(({ className, ...props }, ref) => ( PaginationItem.displayName = "PaginationItem";
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean;
} & Pick<ButtonProps, "size"> & } & Pick<ButtonProps, "size"> &
React.ComponentProps<"a"> React.ComponentProps<"a">;
const PaginationLink = ({ const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
className, <a
isActive, aria-current={isActive ? "page" : undefined}
size = "icon", className={cn(
...props buttonVariants({
}: PaginationLinkProps) => ( variant: isActive ? "outline" : "ghost",
<a size,
aria-current={isActive ? "page" : undefined} }),
className={cn( "no-underline",
buttonVariants({ className,
variant: isActive ? "outline" : "ghost", )}
size, {...props}
}), />
"no-underline", );
className PaginationLink.displayName = "PaginationLink";
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({ const PaginationPrevious = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" size="default"
className={cn("gap-1 pl-2.5", className)} className={cn("gap-1 pl-2.5", className)}
{...props} {...props}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
<span>Previous</span> <span>Previous</span>
</PaginationLink> </PaginationLink>
) );
PaginationPrevious.displayName = "PaginationPrevious" PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
className, <PaginationLink
...props aria-label="Go to next page"
}: React.ComponentProps<typeof PaginationLink>) => ( size="default"
<PaginationLink className={cn("gap-1 pr-2.5", className)}
aria-label="Go to next page" {...props}
size="default" >
className={cn("gap-1 pr-2.5", className)} <span>Next</span>
{...props} <ChevronRight className="h-4 w-4" />
> </PaginationLink>
<span>Next</span> );
<ChevronRight className="h-4 w-4" /> PaginationNext.displayName = "PaginationNext";
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
className, <span
...props aria-hidden
}: React.ComponentProps<"span">) => ( className={cn("flex h-9 w-9 items-center justify-center", className)}
<span {...props}
aria-hidden >
className={cn("flex h-9 w-9 items-center justify-center", className)} <MoreHorizontal className="h-4 w-4" />
{...props} <span className="sr-only">More pages</span>
> </span>
<MoreHorizontal className="h-4 w-4" /> );
<span className="sr-only">More pages</span> PaginationEllipsis.displayName = "PaginationEllipsis";
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export { export {
Pagination, Pagination,
PaginationContent, PaginationContent,
PaginationEllipsis, PaginationEllipsis,
PaginationItem, PaginationItem,
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} };

View File

@@ -1,30 +1,30 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };