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
specialized personas to tune your conversation to your needs.
{
!isSubscribed && (
<span>
{" "}
<Link href="/settings" className="font-bold">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting.
</span>
)
}
{!isSubscribed && (
<span>
{" "}
<Link href="/settings" className="font-bold">
Upgrade your plan
</Link>{" "}
to leverage custom models. You will fallback to the
default model when chatting.
</span>
)}
</AlertDescription>
</Alert>
<div className="pt-6 md:pt-8">

View File

@@ -90,11 +90,9 @@ export interface UserConfig {
export function useUserConfig(detailed: boolean = false) {
const url = `/api/settings?detailed=${detailed}`;
const {
data,
error,
isLoading,
} = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
const { data, error, isLoading } = useSWR<UserConfig>(url, fetcher, {
revalidateOnFocus: false,
});
if (error || !data || data?.detail === "Forbidden") {
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 { PopoverProps } from "@radix-ui/react-popover"
import { PopoverProps } from "@radix-ui/react-popover";
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 { Button } from "@/components/ui/button";
import {
@@ -17,11 +17,7 @@ import {
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ModelOptions, useUserConfig } from "./auth";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
@@ -35,7 +31,7 @@ interface ModelSelectorProps extends PopoverProps {
}
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 [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined);
const { data: userConfig, error, isLoading: isLoadingUserConfig } = useUserConfig(true);
@@ -48,14 +44,18 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
if (userConfig) {
setModels(userConfig.chat_model_options);
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) {
setSelectedModel(userConfig.chat_model_options[0]);
} else {
setSelectedModel(selectedChatModelOption);
}
} 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);
}
}
@@ -68,15 +68,11 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
}, [selectedModel, userConfig, props.onSelect]);
if (isLoadingUserConfig) {
return (
<Skeleton className="w-full h-10" />
);
return <Skeleton className="w-full h-10" />;
}
if (error) {
return (
<div className="text-sm text-error">{error.message}</div>
);
return <div className="text-sm text-error">{error.message}</div>;
}
return (
@@ -92,30 +88,85 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
disabled={props.disabled ?? false}
>
<p className="truncate">
{selectedModel ? selectedModel.name?.substring(0, 20) : "Select a model..."}
{selectedModel
? selectedModel.name?.substring(0, 20)
: "Select a model..."}
</p>
<CaretUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<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>
<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) => (
{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)
setSelectedModel(model);
setOpen(false);
}}
isActive={props.isActive}
/>
@@ -124,74 +175,24 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
</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>
<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>
}
</HoverCard>
)}
</PopoverContent>
</Popover>
</div>
)
);
}
interface ModelItemProps {
model: ModelOptions,
isSelected: boolean,
onSelect: () => void,
onPeek: (model: ModelOptions) => void
isActive?: boolean
model: ModelOptions;
isSelected: boolean;
onSelect: () => void;
onPeek: (model: ModelOptions) => void;
isActive?: boolean;
}
function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemProps) {
const ref = React.useRef<HTMLDivElement>(null)
const ref = React.useRef<HTMLDivElement>(null);
useMutationObserver(ref, (mutations) => {
mutations.forEach((mutation) => {
@@ -200,10 +201,10 @@ function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemP
mutation.attributeName === "aria-selected" &&
ref.current?.getAttribute("aria-selected") === "true"
) {
onPeek(model)
onPeek(model);
}
})
})
});
});
return (
<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"
disabled={!isActive && model.tier !== "free"}
>
{model.name} {model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
<Check
className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")}
/>
{model.name}{" "}
{model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
<Check className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")} />
</CommandItem>
)
);
}

View File

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

View File

@@ -1061,12 +1061,27 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
className="h-6 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
const filteredFiles = allFileOptions.filter(file =>
file.toLowerCase().includes(fileSearchValue.toLowerCase())
const filteredFiles =
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
@@ -1078,12 +1093,28 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
className="h-6 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
const filteredFiles = allFileOptions.filter(file =>
file.toLowerCase().includes(fileSearchValue.toLowerCase())
const filteredFiles =
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

View File

@@ -127,7 +127,7 @@ function renameConversation(conversationId: string, newTitle: string) {
},
})
.then((response) => response.json())
.then((data) => { })
.then((data) => {})
.catch((err) => {
console.error(err);
return;
@@ -171,7 +171,7 @@ function deleteConversation(conversationId: string) {
mutate("/api/chat/sessions");
}
})
.then((data) => { })
.then((data) => {})
.catch((err) => {
console.error(err);
return;
@@ -245,9 +245,7 @@ export function FilesMenu(props: FilesMenuProps) {
Context
<p>
<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>
</p>
</h4>
@@ -257,7 +255,7 @@ export function FilesMenu(props: FilesMenuProps) {
</Button>
</div>
</div>
)
);
}
if (!files) return <InlineLoading />;
@@ -443,10 +441,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
<div>
{props.sideBarOpen && (
<ScrollArea>
<ScrollAreaScrollbar
orientation="vertical"
className="h-full w-2.5"
/>
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5" />
<div className="p-0 m-0">
{props.subsetOrganizedData != null &&
Object.keys(props.subsetOrganizedData)
@@ -471,7 +466,9 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
agent_name={chatHistory.agent_name}
agent_color={chatHistory.agent_color}
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"
>
<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}
</p>

View File

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

View File

@@ -52,7 +52,7 @@ interface TrainOfThoughtFrame {
}
interface TrainOfThoughtGroup {
type: 'video' | 'text';
type: "video" | "text";
frames?: TrainOfThoughtFrame[];
textEntries?: TrainOfThoughtObject[];
}
@@ -65,7 +65,9 @@ interface TrainOfThoughtComponentProps {
completed?: boolean;
}
function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): TrainOfThoughtGroup[] {
function extractTrainOfThoughtGroups(
trainOfThought?: TrainOfThoughtObject[],
): TrainOfThoughtGroup[] {
if (!trainOfThought) return [];
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 (currentTextEntries.length > 0) {
groups.push({
type: 'text',
textEntries: [...currentTextEntries]
type: "text",
textEntries: [...currentTextEntries],
});
currentTextEntries = [];
}
@@ -116,8 +118,8 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
// If we have accumulated video frames, add them as a video group
if (currentVideoFrames.length > 0) {
groups.push({
type: 'video',
frames: [...currentVideoFrames]
type: "video",
frames: [...currentVideoFrames],
});
currentVideoFrames = [];
}
@@ -130,14 +132,14 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
// Add any remaining frames/entries
if (currentVideoFrames.length > 0) {
groups.push({
type: 'video',
frames: currentVideoFrames
type: "video",
frames: currentVideoFrames,
});
}
if (currentTextEntries.length > 0) {
groups.push({
type: 'text',
textEntries: currentTextEntries
type: "text",
textEntries: currentTextEntries,
});
}
@@ -177,10 +179,10 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
// Convert string array to TrainOfThoughtObject array if needed
let trainOfThoughtObjects: TrainOfThoughtObject[];
if (typeof props.trainOfThought[0] === 'string') {
if (typeof props.trainOfThought[0] === "string") {
trainOfThoughtObjects = (props.trainOfThought as string[]).map((data, index) => ({
type: 'text',
data: data
type: "text",
data: data,
}));
} else {
trainOfThoughtObjects = props.trainOfThought as TrainOfThoughtObject[];
@@ -221,28 +223,37 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
<motion.div initial="closed" animate="open" exit="closed" variants={variants}>
{trainOfThoughtGroups.map((group, groupIndex) => (
<div key={`train-group-${groupIndex}`}>
{group.type === 'video' && group.frames && group.frames.length > 0 && (
<TrainOfThoughtVideoPlayer
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 === "video" &&
group.frames &&
group.frames.length > 0 && (
<TrainOfThoughtVideoPlayer
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}
/>
);
})}
</div>
))}
</motion.div>
@@ -300,7 +311,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
// ResizeObserver to handle content height changes (e.g., images loading)
useEffect(() => {
const contentWrapper = scrollableContentWrapperRef.current;
const scrollViewport = scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector);
const scrollViewport =
scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector);
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
const { scrollTop, scrollHeight, clientHeight } = scrollViewport;
const bottomThreshold = 50;
const currentlyNearBottom = (scrollHeight - (scrollTop + clientHeight)) <= bottomThreshold;
const currentlyNearBottom =
scrollHeight - (scrollTop + clientHeight) <= bottomThreshold;
if (currentlyNearBottom) {
// Only auto-scroll if there are incoming messages being processed
if (props.incomingMessages && props.incomingMessages.length > 0) {
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 (!lastMessage.completed || (lastMessage.completed && incompleteIncomingMessageIndex !== null)) {
if (
!lastMessage.completed ||
(lastMessage.completed && incompleteIncomingMessageIndex !== null)
) {
scrollToBottom(true); // Use instant scroll
}
}
@@ -463,7 +479,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
});
});
// 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);
}
};
@@ -626,16 +647,19 @@ export default function ChatHistory(props: ChatHistoryProps) {
conversationId={props.conversationId}
turnId={messageTurnId}
/>
{message.trainOfThought && message.trainOfThought.length > 0 && (
<TrainOfThoughtComponent
trainOfThought={message.trainOfThought}
lastMessage={index === incompleteIncomingMessageIndex}
agentColor={data?.agent?.color || "orange"}
key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map(t => t.length).join('-')}`}
keyId={`${index}trainOfThought`}
completed={message.completed}
/>
)}
{message.trainOfThought &&
message.trainOfThought.length > 0 && (
<TrainOfThoughtComponent
trainOfThought={message.trainOfThought}
lastMessage={
index === incompleteIncomingMessageIndex
}
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
key={`${index}incoming`}
isMobileWidth={isMobileWidth}

View File

@@ -817,38 +817,44 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
/>
</button>
)}
{props.chatMessage.by === "khoj" && props.onRetryMessage && props.isLastMessage && (
<button
title="Retry"
className={`${styles.retryButton}`}
onClick={() => {
const turnId = props.chatMessage.turnId || props.turnId;
const query = props.chatMessage.rawQuery || props.chatMessage.intent?.query;
console.log("Retry button clicked for turnId:", turnId);
console.log("ChatMessage data:", {
rawQuery: props.chatMessage.rawQuery,
intent: props.chatMessage.intent,
message: props.chatMessage.message
});
console.log("Extracted query:", query);
if (query) {
props.onRetryMessage?.(query, turnId);
} else {
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);
{props.chatMessage.by === "khoj" &&
props.onRetryMessage &&
props.isLastMessage && (
<button
title="Retry"
className={`${styles.retryButton}`}
onClick={() => {
const turnId = props.chatMessage.turnId || props.turnId;
const query =
props.chatMessage.rawQuery ||
props.chatMessage.intent?.query;
console.log("Retry button clicked for turnId:", turnId);
console.log("ChatMessage data:", {
rawQuery: props.chatMessage.rawQuery,
intent: props.chatMessage.intent,
message: props.chatMessage.message,
});
console.log("Extracted query:", query);
if (query) {
props.onRetryMessage?.(query, turnId);
} else {
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
alt="Retry Message"
className="hsl(var(--muted-foreground)) hover:text-blue-500"
/>
</button>
)}
}}
>
<ArrowClockwise
alt="Retry Message"
className="hsl(var(--muted-foreground)) hover:text-blue-500"
/>
</button>
)}
<button
title="Copy"
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 { 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 { ModelSelector } from "@/app/common/modelSelector";
import { FilesMenu } from "../allConversations/allConversations";
@@ -14,21 +33,39 @@ import { mutate } from "swr";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { AgentData } from "../agentCard/agentCard";
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 { Checkbox } from "@/components/ui/checkbox";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { TooltipContent } from "@radix-ui/react-tooltip";
import { useAuthenticatedData } from "@/app/common/auth";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
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 { Input } from "@/components/ui/input";
import Link from "next/link";
import { motion } from "framer-motion";
interface ChatSideBarProps {
conversationId: string;
isOpen: boolean;
@@ -40,15 +77,10 @@ interface ChatSideBarProps {
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function ChatSidebar({ ...props }: ChatSideBarProps) {
if (props.isMobileWidth) {
return (
<Sheet
open={props.isOpen}
onOpenChange={props.onOpenChange}>
<SheetContent
className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
>
<Sheet open={props.isOpen} onOpenChange={props.onOpenChange}>
<SheetContent className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden">
<ChatSidebarInternal {...props} />
</SheetContent>
</Sheet>
@@ -110,14 +142,14 @@ function AgentCreationForm(props: IAgentCreationProps) {
fetch(createAgentUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(data)
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((data: AgentData | AgentError) => {
console.log("Success:", data);
if ('detail' in data) {
if ("detail" in data) {
setError(`Error creating agent: ${data.detail}`);
setIsCreating(false);
return;
@@ -142,162 +174,151 @@ function AgentCreationForm(props: IAgentCreationProps) {
}, [customAgentName, customAgentIcon, customAgentColor]);
return (
<Dialog>
<DialogTrigger asChild>
<Button
className="w-full"
variant="secondary"
>
<Button className="w-full" variant="secondary">
Create Agent
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
{
doneCreating && createdSlug ? (
<DialogTitle>
Created {customAgentName}
</DialogTitle>
) : (
<DialogTitle>
Create a New Agent
</DialogTitle>
)
}
{doneCreating && createdSlug ? (
<DialogTitle>Created {customAgentName}</DialogTitle>
) : (
<DialogTitle>Create a New Agent</DialogTitle>
)}
<DialogClose />
<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>
</DialogHeader>
<div className="py-4">
{
doneCreating && createdSlug ? (
<div className="flex flex-col items-center justify-center gap-4 py-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20
}}
>
<CheckCircle
className="w-16 h-16 text-green-500"
weight="fill"
/>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center text-lg font-medium text-accent-foreground"
>
Created successfully!
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Link href={`/agents?agent=${createdSlug}`}>
<Button variant="secondary" className="mt-2">
Manage Agent
</Button>
</Link>
</motion.div>
{doneCreating && createdSlug ? (
<div className="flex flex-col items-center justify-center gap-4 py-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20,
}}
>
<CheckCircle className="w-16 h-16 text-green-500" weight="fill" />
</motion.div>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center text-lg font-medium text-accent-foreground"
>
Created successfully!
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Link href={`/agents?agent=${createdSlug}`}>
<Button variant="secondary" className="mt-2">
Manage Agent
</Button>
</Link>
</motion.div>
</div>
) : (
<div className="flex flex-col gap-4">
<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 className="flex flex-col gap-4">
<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 className="flex gap-4">
<div className="flex-1">
<Select
onValueChange={setCustomAgentColor}
defaultValue={customAgentColor}
>
<SelectTrigger
className="w-full dark:bg-muted"
disabled={isCreating}
>
<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 className="flex gap-4">
<div className="flex-1">
<Select onValueChange={setCustomAgentColor} defaultValue={customAgentColor}>
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
<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 className="flex-1">
<Select onValueChange={setCustomAgentIcon} defaultValue={customAgentIcon}>
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
<SelectValue placeholder="Icon" />
</SelectTrigger>
<SelectContent className="items-center space-y-1 inline-flex flex-col">
{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 className="flex-1">
<Select
onValueChange={setCustomAgentIcon}
defaultValue={customAgentIcon}
>
<SelectTrigger
className="w-full dark:bg-muted"
disabled={isCreating}
>
<SelectValue placeholder="Icon" />
</SelectTrigger>
<SelectContent className="items-center space-y-1 inline-flex flex-col">
{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>
<DialogFooter>
{
error && (
<div className="text-red-500 text-sm">
{error}
</div>
)
}
{
!doneCreating && (
<Button
type="submit"
onClick={() => createAgent()}
disabled={isCreating || !isValid}
>
{
isCreating ?
<CircleNotch className="animate-spin" />
:
<PersonSimpleTaiChi />
}
Create
</Button>
)
}
{error && <div className="text-red-500 text-sm">{error}</div>}
{!doneCreating && (
<Button
type="submit"
onClick={() => createAgent()}
disabled={isCreating || !isValid}
>
{isCreating ? (
<CircleNotch className="animate-spin" />
) : (
<PersonSimpleTaiChi />
)}
Create
</Button>
)}
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog >
)
</Dialog>
);
}
function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
@@ -305,7 +326,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
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 {
data: authenticatedData,
error: authenticationError,
@@ -317,7 +345,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const [inputTools, setInputTools] = useState<string[] | undefined>();
const [outputModes, setOutputModes] = useState<string[] | undefined>();
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 [displayOutputModes, setDisplayOutputModes] = useState<string[] | undefined>();
@@ -330,12 +360,20 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
setInputTools(agentData.input_tools);
setDisplayInputTools(agentData.input_tools);
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);
setDisplayOutputModes(agentData.output_modes);
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) {
@@ -367,8 +405,12 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
const promptChanged = !!customPrompt && customPrompt !== agentData.persona;
// 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 modesChanged = JSON.stringify(outputModes?.sort() || []) !== JSON.stringify(agentData.output_modes?.sort());
const toolsChanged =
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);
@@ -394,7 +436,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
function handleSave() {
if (hasModified) {
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;
}
@@ -409,12 +453,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
chat_model: selectedModel,
input_tools: inputTools,
output_modes: outputModes,
...(isDefaultAgent ? {} : { slug: agentData?.slug })
...(isDefaultAgent ? {} : { slug: agentData?.slug }),
};
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.
// 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, {
method: mode,
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(data)
body: JSON.stringify(data),
})
.then((res) => {
setIsSaving(false);
res.json()
res.json();
})
.then((data) => {
mutate(`/api/agents/conversation?conversation_id=${props.conversationId}`);
@@ -456,43 +502,47 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Sidebar
collapsible="none"
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"
: "translate-x-full opacity-100 w-0 p-0 m-0"}
${
props.isOpen
? "translate-x-0 opacity-100 w-[300px] relative"
: "translate-x-full opacity-100 w-0 p-0 m-0"
}
`}
variant="floating">
variant="floating"
>
<SidebarContent>
<SidebarHeader>
{
agentData && !isEditable ? (
<div className="flex items-center relative text-sm">
<a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}>
{getIconFromIconName(agentData.icon, agentData.color)}
{agentData.name}
</a>
</div>
) : (
<div className="flex items-center relative text-sm justify-between">
<p>
Chat Options
</p>
</div>
)
}
{agentData && !isEditable ? (
<div className="flex items-center relative text-sm">
<a
className="text-lg font-bold flex flex-row items-center"
href={`/agents?agent=${agentData.slug}`}
>
{getIconFromIconName(agentData.icon, agentData.color)}
{agentData.name}
</a>
</div>
) : (
<div className="flex items-center relative text-sm justify-between">
<p>Chat Options</p>
</div>
)}
</SidebarHeader>
<SidebarGroup key={"knowledge"} className="border-b last:border-none">
<SidebarGroupContent className="gap-0">
<SidebarMenu className="p-0 m-0">
{
agentData && agentData.has_files ? (
<SidebarMenuItem key={"agent_knowledge"} className="list-none">
<div className="flex items-center space-x-2 rounded-full">
<div className="text-muted-foreground"><Sparkle /></div>
<div className="text-muted-foreground text-sm">Using custom knowledge base</div>
{agentData && agentData.has_files ? (
<SidebarMenuItem key={"agent_knowledge"} className="list-none">
<div className="flex items-center space-x-2 rounded-full">
<div className="text-muted-foreground">
<Sparkle />
</div>
</SidebarMenuItem>
) : null
}
<div className="text-muted-foreground text-sm">
Using custom knowledge base
</div>
</div>
</SidebarMenuItem>
) : null}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
@@ -506,39 +556,41 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
value={customPrompt || ""}
onChange={(e) => handleCustomPromptChange(e.target.value)}
readOnly={!isEditable}
disabled={!isEditable} />
disabled={!isEditable}
/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{
!agentDataLoading && agentData && (
<SidebarGroup key={"model"}>
<SidebarGroupContent>
<SidebarGroupLabel>
Model
{
!isSubscribed && (
<a href="/settings" className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg">
Upgrade
</a>
)
}
</SidebarGroupLabel>
<SidebarMenu className="p-0 m-0">
<SidebarMenuItem key={"model"} className="list-none">
<ModelSelector
disabled={!isEditable}
onSelect={(model) => handleModelSelect(model.name)}
initialModel={isDefaultAgent ? undefined : agentData?.chat_model}
isActive={props.isActive}
/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
{!agentDataLoading && agentData && (
<SidebarGroup key={"model"}>
<SidebarGroupContent>
<SidebarGroupLabel>
Model
{!isSubscribed && (
<a
href="/settings"
className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"
>
Upgrade
</a>
)}
</SidebarGroupLabel>
<SidebarMenu className="p-0 m-0">
<SidebarMenuItem key={"model"} className="list-none">
<ModelSelector
disabled={!isEditable}
onSelect={(model) => handleModelSelect(model.name)}
initialModel={
isDefaultAgent ? undefined : agentData?.chat_model
}
isActive={props.isActive}
/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)}
<Popover defaultOpen={false}>
<SidebarGroup>
<SidebarGroupLabel asChild>
@@ -550,82 +602,118 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<PopoverContent>
<SidebarGroupContent>
<SidebarMenu className="p-1 m-0">
{
Object.entries(agentConfigurationOptions?.input_tools ?? {}).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 text-accent-foreground p-1 cursor-pointer">
{getIconForSlashCommand(key)}
<p className="text-sm my-auto flex items-center">
{key}
</p>
</Label>
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, displayInputTools ?? [])}
onCheckedChange={() => {
let updatedInputTools = handleCheckToggle(key, displayInputTools ?? [])
setInputTools(updatedInputTools);
setDisplayInputTools(updatedInputTools);
}}
disabled={!isEditable}
>
{Object.entries(
agentConfigurationOptions?.input_tools ?? {},
).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 text-accent-foreground p-1 cursor-pointer"
>
{getIconForSlashCommand(key)}
<p className="text-sm my-auto flex items-center">
{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>
);
}
)
}
{
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}
</p>
</Label>
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, displayOutputModes ?? [])}
onCheckedChange={() => {
let updatedOutputModes = handleCheckToggle(key, displayOutputModes ?? [])
setOutputModes(updatedOutputModes);
setDisplayOutputModes(updatedOutputModes);
}}
disabled={!isEditable}
>
</p>
</Label>
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(
key,
displayInputTools ?? [],
)}
onCheckedChange={() => {
let updatedInputTools =
handleCheckToggle(
key,
displayInputTools ?? [],
);
setInputTools(
updatedInputTools,
);
setDisplayInputTools(
updatedInputTools,
);
}}
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>
);
})}
{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}
</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>
);
}
)
}
</p>
</Label>
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(
key,
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>
</SidebarGroupContent>
</PopoverContent>
</SidebarGroup>
@@ -645,79 +733,75 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{
props.isOpen && (
<SidebarFooter key={"actions"}>
<SidebarMenu className="p-0 m-0">
{
(agentData && !isEditable && agentData.is_creator) ? (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className="w-full"
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>
)
{props.isOpen && (
<SidebarFooter key={"actions"}>
<SidebarMenu className="p-0 m-0">
{agentData && !isEditable && agentData.is_creator ? (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className="w-full"
variant={"ghost"}
onClick={() =>
(window.location.href = `/agents?agent=${agentData?.slug}`)
}
>
Manage
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
) : (
<>
{!hasModified &&
isEditable &&
customPrompt &&
!isDefaultAgent &&
selectedModel && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className="w-full"
onClick={() => handleReset()}
variant={"ghost"}
disabled={!isEditable || !hasModified}
>
Reset
</Button>
<AgentCreationForm
customPrompt={customPrompt}
selectedModel={selectedModel}
inputTools={displayInputTools ?? []}
outputModes={displayOutputModes ?? []}
/>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`}
variant={"secondary"}
onClick={() => handleSave()}
disabled={!isEditable || !hasModified || isSaving}
>
{
isSaving ?
<CircleNotch className="animate-spin" />
:
<ArrowsDownUp />
}
{
isSaving ? "Saving" : "Save"
}
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
</>
}
</SidebarMenu>
</SidebarFooter>
)
}
)}
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className="w-full"
onClick={() => handleReset()}
variant={"ghost"}
disabled={!isEditable || !hasModified}
>
Reset
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`}
variant={"secondary"}
onClick={() => handleSave()}
disabled={!isEditable || !hasModified || isSaving}
>
{isSaving ? (
<CircleNotch className="animate-spin" />
) : (
<ArrowsDownUp />
)}
{isSaving ? "Saving" : "Save"}
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
</>
)}
</SidebarMenu>
</SidebarFooter>
)}
</Sidebar>
)
);
}

View File

@@ -147,9 +147,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
<span>{mermaidError}</span>
</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">
{
chart
}
{chart}
</code>
</>
) : (

View File

@@ -12,7 +12,15 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} 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 LoginPrompt from "../loginPrompt/loginPrompt";
import { Button } from "@/components/ui/button";
@@ -69,7 +77,7 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
icon: <BuildingOffice className="w-6 h-6" />,
link: "https://khoj.dev/teams",
},
]
];
return (
<SidebarMenu className="border-none p-0 m-0">
@@ -131,18 +139,16 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
</p>
</div>
</DropdownMenuItem>
{
menuItems.map((menuItem, index) => (
<DropdownMenuItem key={index}>
<Link href={menuItem.link} className="no-underline w-full">
<div className="flex flex-rows">
{menuItem.icon}
<p className="ml-3 font-semibold">{menuItem.title}</p>
</div>
</Link>
</DropdownMenuItem>
))
}
{menuItems.map((menuItem, index) => (
<DropdownMenuItem key={index}>
<Link href={menuItem.link} className="no-underline w-full">
<div className="flex flex-rows">
{menuItem.icon}
<p className="ml-3 font-semibold">{menuItem.title}</p>
</div>
</Link>
</DropdownMenuItem>
))}
{!userData ? (
<DropdownMenuItem>
<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 }) {
const [darkMode, setDarkMode] = useIsDarkMode();

View File

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

View File

@@ -23,8 +23,15 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
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 JSZip from "jszip";
import { saveAs } from 'file-saver';
import { saveAs } from "file-saver";
interface DropdownComponentProps {
items: ModelOptions[];
@@ -81,7 +88,12 @@ interface DropdownComponentProps {
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");
return (
@@ -114,7 +126,10 @@ const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected,
value={item.id.toString()}
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>
))}
</DropdownMenuRadioGroup>
@@ -327,8 +342,8 @@ export default function SettingsView() {
initialUserConfig?.is_phone_number_verified
? PhoneNumberValidationState.Verified
: initialUserConfig?.phone_number
? PhoneNumberValidationState.SendOTP
: PhoneNumberValidationState.Setup,
? PhoneNumberValidationState.SendOTP
: PhoneNumberValidationState.Setup,
);
setName(initialUserConfig?.given_name);
setNotionToken(initialUserConfig?.notion_token ?? null);
@@ -524,13 +539,14 @@ export default function SettingsView() {
const updateModel = (modelType: string) => async (id: string) => {
// Get the selected model from the options
const modelOptions = modelType === "chat"
? userConfig?.chat_model_options
: modelType === "paint"
? userConfig?.paint_model_options
: userConfig?.voice_model_options;
const modelOptions =
modelType === "chat"
? userConfig?.chat_model_options
: modelType === "paint"
? 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;
// 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({
title: `✅ Switched ${modelType} model to ${modelName}`,
@@ -570,7 +587,7 @@ export default function SettingsView() {
setIsExporting(true);
// Get total conversation count
const statsResponse = await fetch('/api/chat/stats');
const statsResponse = await fetch("/api/chat/stats");
const stats = await statsResponse.json();
const total = stats.num_conversations;
setTotalConversations(total);
@@ -586,7 +603,7 @@ export default function SettingsView() {
conversations.push(...data);
setExportedConversations((page + 1) * 10);
setExportProgress(((page + 1) * 10 / total) * 100);
setExportProgress((((page + 1) * 10) / total) * 100);
}
// Add conversations to zip
@@ -605,7 +622,7 @@ export default function SettingsView() {
toast({
title: "Export Failed",
description: "Failed to export chats. Please try again.",
variant: "destructive"
variant: "destructive",
});
} finally {
setIsExporting(false);
@@ -808,93 +825,93 @@ export default function SettingsView() {
)) ||
(userConfig.subscription_state ===
"subscribed" && (
<>
<p className="text-xl text-primary/80">
Futurist
</p>
<p className="text-gray-400">
Subscription <b>renews</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
</>
)) ||
<>
<p className="text-xl text-primary/80">
Futurist
</p>
<p className="text-gray-400">
Subscription <b>renews</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
</>
)) ||
(userConfig.subscription_state ===
"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">
Subscription <b>ends</b> on{" "}
Subscription <b>expired</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">
Subscription <b>expired</b>{" "}
on{" "}
<b>
{
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>
)}
</>
))}
)) || (
<p className="text-gray-400">
Check{" "}
<a
href="https://khoj.dev/#pricing"
target="_blank"
>
pricing page
</a>{" "}
to compare plans.
</p>
)}
</>
))}
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{(userConfig.subscription_state ==
"subscribed" && (
<Button
variant="outline"
className="hover:text-red-400"
onClick={() =>
setSubscription("cancel")
}
>
<ArrowCircleDown className="h-5 w-5 mr-2" />
Unsubscribe
</Button>
)) ||
<Button
variant="outline"
className="hover:text-red-400"
onClick={() =>
setSubscription("cancel")
}
>
<ArrowCircleDown className="h-5 w-5 mr-2" />
Unsubscribe
</Button>
)) ||
(userConfig.subscription_state ==
"unsubscribed" && (
<Button
variant="outline"
className="text-primary/80 hover:text-primary"
onClick={() =>
setSubscription("resubscribe")
}
>
<ArrowCircleUp
weight="bold"
className="h-5 w-5 mr-2"
/>
Resubscribe
</Button>
)) ||
<Button
variant="outline"
className="text-primary/80 hover:text-primary"
onClick={() =>
setSubscription("resubscribe")
}
>
<ArrowCircleUp
weight="bold"
className="h-5 w-5 mr-2"
/>
Resubscribe
</Button>
)) ||
(userConfig.subscription_enabled_trial_at && (
<Button
variant="outline"
@@ -984,16 +1001,16 @@ export default function SettingsView() {
<Button variant="outline" size="sm">
{(userConfig.enabled_content_source
.github && (
<>
<Files className="h-5 w-5 inline mr-1" />
Manage
</>
)) || (
<>
<Plugs className="h-5 w-5 inline mr-1" />
Connect
</>
)}
<>
<Files className="h-5 w-5 inline mr-1" />
Manage
</>
)) || (
<>
<Plugs className="h-5 w-5 inline mr-1" />
Connect
</>
)}
</Button>
<Button
variant="outline"
@@ -1035,8 +1052,8 @@ export default function SettingsView() {
{
/* Show connect to notion button if notion oauth url setup and user disconnected*/
userConfig.notion_oauth_url &&
!userConfig.enabled_content_source
.notion ? (
!userConfig.enabled_content_source
.notion ? (
<Button
variant="outline"
size="sm"
@@ -1050,39 +1067,39 @@ export default function SettingsView() {
Connect
</Button>
) : /* Show sync button if user connected to notion and API key unchanged */
userConfig.enabled_content_source.notion &&
notionToken ===
userConfig.notion_token ? (
<Button
variant="outline"
size="sm"
onClick={() =>
syncContent("notion")
}
>
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
Sync
</Button>
) : /* Show set API key button notion oauth url not set setup */
!userConfig.notion_oauth_url ? (
<Button
variant="outline"
size="sm"
onClick={saveNotionToken}
disabled={
notionToken ===
userConfig.notion_token
}
>
<FloppyDisk className="h-5 w-5 inline mr-1" />
{(userConfig.enabled_content_source
.notion &&
"Update API Key") ||
"Set API Key"}
</Button>
) : (
<></>
)
userConfig.enabled_content_source.notion &&
notionToken ===
userConfig.notion_token ? (
<Button
variant="outline"
size="sm"
onClick={() =>
syncContent("notion")
}
>
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
Sync
</Button>
) : /* Show set API key button notion oauth url not set setup */
!userConfig.notion_oauth_url ? (
<Button
variant="outline"
size="sm"
onClick={saveNotionToken}
disabled={
notionToken ===
userConfig.notion_token
}
>
<FloppyDisk className="h-5 w-5 inline mr-1" />
{(userConfig.enabled_content_source
.notion &&
"Update API Key") ||
"Set API Key"}
</Button>
) : (
<></>
)
}
<Button
variant="outline"
@@ -1123,7 +1140,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<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"
: "Subscribe to switch model"}
</p>
@@ -1154,7 +1174,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<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"
: "Subscribe to switch model"}
</p>
@@ -1185,7 +1208,10 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<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"
: "Subscribe to switch model"}
</p>
@@ -1219,9 +1245,13 @@ export default function SettingsView() {
</p>
{exportProgress > 0 && (
<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">
Exported {exportedConversations} of {totalConversations} conversations
Exported {exportedConversations} of{" "}
{totalConversations} conversations
</p>
</div>
)}
@@ -1233,7 +1263,9 @@ export default function SettingsView() {
disabled={isExporting}
>
<Download className="h-5 w-5 mr-2" />
{isExporting ? "Exporting..." : "Export Chats"}
{isExporting
? "Exporting..."
: "Export Chats"}
</Button>
</CardFooter>
</Card>
@@ -1245,7 +1277,11 @@ export default function SettingsView() {
</CardHeader>
<CardContent className="overflow-hidden">
<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>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
@@ -1261,36 +1297,56 @@ export default function SettingsView() {
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription>
This action is irreversible. This will permanently delete your account
and remove all your data from our servers.
This action is irreversible.
This will permanently delete
your account and remove all your
data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-500 hover:bg-red-600"
onClick={async () => {
try {
const response = await fetch('/api/self', {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete account');
const response =
await fetch(
"/api/self",
{
method: "DELETE",
},
);
if (!response.ok)
throw new Error(
"Failed to delete account",
);
toast({
title: "Account Deleted",
description: "Your account has been successfully deleted.",
description:
"Your account has been successfully deleted.",
});
// Redirect to home page after successful deletion
window.location.href = "/";
window.location.href =
"/";
} catch (error) {
console.error('Error deleting account:', error);
console.error(
"Error deleting account:",
error,
);
toast({
title: "Error",
description: "Failed to delete account. Please try again or contact support.",
variant: "destructive"
description:
"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 CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
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",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
<CheckboxPrimitive.Root
ref={ref}
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",
className,
)}
{...props}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</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 HoverCardPrimitive from "@radix-ui/react-hover-card"
import * as React from "react";
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<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
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",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
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",
className,
)}
{...props}
/>
));
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 { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
);
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
React.ComponentProps<"a">;
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
"no-underline",
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
"no-underline",
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
className,
...props
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@@ -1,30 +1,30 @@
"use client"
"use client";
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
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<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
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",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
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",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };