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">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting. <Link href="/settings" className="font-bold">
Upgrade your plan
</Link>{" "}
to leverage custom models. You will fallback to the
default model when chatting.
</span> </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,32 @@ 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> <div>
<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,7 +122,7 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
</CommandList> </CommandList>
</Command> </Command>
</div> </div>
: ) : (
<HoverCard> <HoverCard>
<HoverCardContent <HoverCardContent
side="left" side="left"
@@ -133,7 +131,9 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
className="min-h-[280px]" className="min-h-[280px]"
> >
<div className="grid gap-2"> <div className="grid gap-2">
<h4 className="font-medium leading-none">{peekedModel?.name}</h4> <h4 className="font-medium leading-none">
{peekedModel?.name}
</h4>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{peekedModel?.description} {peekedModel?.description}
</div> </div>
@@ -156,16 +156,17 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
<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}
/> />
@@ -175,23 +176,23 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
</Command> </Command>
</div> </div>
</HoverCard> </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,18 +223,27 @@ 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" &&
group.frames &&
group.frames.length > 0 && (
<TrainOfThoughtVideoPlayer <TrainOfThoughtVideoPlayer
frames={group.frames} frames={group.frames}
autoPlay={false} autoPlay={false}
playbackSpeed={1500} playbackSpeed={1500}
/> />
)} )}
{group.type === 'text' && group.textEntries && group.textEntries.map((entry, entryIndex) => { {group.type === "text" &&
group.textEntries &&
group.textEntries.map((entry, entryIndex) => {
const lastIndex = trainOfThoughtGroups.length - 1; const lastIndex = trainOfThoughtGroups.length - 1;
const isLastGroup = groupIndex === lastIndex; const isLastGroup = groupIndex === lastIndex;
const isLastEntry = entryIndex === group.textEntries!.length - 1; const isLastEntry =
const isPrimaryEntry = isLastGroup && isLastEntry && props.lastMessage && !props.completed; entryIndex === group.textEntries!.length - 1;
const isPrimaryEntry =
isLastGroup &&
isLastEntry &&
props.lastMessage &&
!props.completed;
return ( return (
<TrainOfThought <TrainOfThought
@@ -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,12 +647,15 @@ export default function ChatHistory(props: ChatHistoryProps) {
conversationId={props.conversationId} conversationId={props.conversationId}
turnId={messageTurnId} turnId={messageTurnId}
/> />
{message.trainOfThought && message.trainOfThought.length > 0 && ( {message.trainOfThought &&
message.trainOfThought.length > 0 && (
<TrainOfThoughtComponent <TrainOfThoughtComponent
trainOfThought={message.trainOfThought} trainOfThought={message.trainOfThought}
lastMessage={index === incompleteIncomingMessageIndex} lastMessage={
index === incompleteIncomingMessageIndex
}
agentColor={data?.agent?.color || "orange"} agentColor={data?.agent?.color || "orange"}
key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map(t => t.length).join('-')}`} key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map((t) => t.length).join("-")}`}
keyId={`${index}trainOfThought`} keyId={`${index}trainOfThought`}
completed={message.completed} completed={message.completed}
/> />

View File

@@ -817,18 +817,22 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
/> />
</button> </button>
)} )}
{props.chatMessage.by === "khoj" && props.onRetryMessage && props.isLastMessage && ( {props.chatMessage.by === "khoj" &&
props.onRetryMessage &&
props.isLastMessage && (
<button <button
title="Retry" title="Retry"
className={`${styles.retryButton}`} className={`${styles.retryButton}`}
onClick={() => { onClick={() => {
const turnId = props.chatMessage.turnId || props.turnId; const turnId = props.chatMessage.turnId || props.turnId;
const query = props.chatMessage.rawQuery || props.chatMessage.intent?.query; const query =
props.chatMessage.rawQuery ||
props.chatMessage.intent?.query;
console.log("Retry button clicked for turnId:", turnId); console.log("Retry button clicked for turnId:", turnId);
console.log("ChatMessage data:", { console.log("ChatMessage data:", {
rawQuery: props.chatMessage.rawQuery, rawQuery: props.chatMessage.rawQuery,
intent: props.chatMessage.intent, intent: props.chatMessage.intent,
message: props.chatMessage.message message: props.chatMessage.message,
}); });
console.log("Extracted query:", query); console.log("Extracted query:", query);
if (query) { if (query) {
@@ -836,7 +840,9 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
} else { } else {
console.error("No original query found for retry"); console.error("No original query found for retry");
// Fallback: try to get from a previous user message or show an input dialog // Fallback: try to get from a previous user message or show an input dialog
const fallbackQuery = prompt("Enter the original query to retry:"); const fallbackQuery = prompt(
"Enter the original query to retry:",
);
if (fallbackQuery) { if (fallbackQuery) {
props.onRetryMessage?.(fallbackQuery, turnId); props.onRetryMessage?.(fallbackQuery, turnId);
} }

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,37 +174,27 @@ 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>
) : ( ) : (
<DialogTitle> <DialogTitle>Create a New Agent</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 }}
@@ -180,13 +202,10 @@ function AgentCreationForm(props: IAgentCreationProps) {
transition={{ transition={{
type: "spring", type: "spring",
stiffness: 260, stiffness: 260,
damping: 20 damping: 20,
}} }}
> >
<CheckCircle <CheckCircle className="w-16 h-16 text-green-500" weight="fill" />
className="w-16 h-16 text-green-500"
weight="fill"
/>
</motion.div> </motion.div>
<motion.p <motion.p
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
@@ -208,7 +227,7 @@ function AgentCreationForm(props: IAgentCreationProps) {
</Link> </Link>
</motion.div> </motion.div>
</div> </div>
) : ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<Label htmlFor="agent_name">Name</Label> <Label htmlFor="agent_name">Name</Label>
@@ -222,8 +241,14 @@ function AgentCreationForm(props: IAgentCreationProps) {
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<Select onValueChange={setCustomAgentColor} defaultValue={customAgentColor}> <Select
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}> onValueChange={setCustomAgentColor}
defaultValue={customAgentColor}
>
<SelectTrigger
className="w-full dark:bg-muted"
disabled={isCreating}
>
<SelectValue placeholder="Color" /> <SelectValue placeholder="Color" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="items-center space-y-1 inline-flex flex-col"> <SelectContent className="items-center space-y-1 inline-flex flex-col">
@@ -242,8 +267,14 @@ function AgentCreationForm(props: IAgentCreationProps) {
</Select> </Select>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Select onValueChange={setCustomAgentIcon} defaultValue={customAgentIcon}> <Select
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}> onValueChange={setCustomAgentIcon}
defaultValue={customAgentIcon}
>
<SelectTrigger
className="w-full dark:bg-muted"
disabled={isCreating}
>
<SelectValue placeholder="Icon" /> <SelectValue placeholder="Icon" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="items-center space-y-1 inline-flex flex-col"> <SelectContent className="items-center space-y-1 inline-flex flex-col">
@@ -265,39 +296,29 @@ function AgentCreationForm(props: IAgentCreationProps) {
</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">
{error}
</div>
)
}
{
!doneCreating && (
<Button <Button
type="submit" type="submit"
onClick={() => createAgent()} onClick={() => createAgent()}
disabled={isCreating || !isValid} disabled={isCreating || !isValid}
> >
{ {isCreating ? (
isCreating ?
<CircleNotch className="animate-spin" /> <CircleNotch className="animate-spin" />
: ) : (
<PersonSimpleTaiChi /> <PersonSimpleTaiChi />
} )}
Create Create
</Button> </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 ${
props.isOpen
? "translate-x-0 opacity-100 w-[300px] relative" ? "translate-x-0 opacity-100 w-[300px] relative"
: "translate-x-full opacity-100 w-0 p-0 m-0"} : "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 className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}> <a
className="text-lg font-bold flex flex-row items-center"
href={`/agents?agent=${agentData.slug}`}
>
{getIconFromIconName(agentData.icon, agentData.color)} {getIconFromIconName(agentData.icon, agentData.color)}
{agentData.name} {agentData.name}
</a> </a>
</div> </div>
) : ( ) : (
<div className="flex items-center relative text-sm justify-between"> <div className="flex items-center relative text-sm justify-between">
<p> <p>Chat Options</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"><Sparkle /></div> <div className="text-muted-foreground">
<div className="text-muted-foreground text-sm">Using custom knowledge base</div> <Sparkle />
</div>
<div className="text-muted-foreground text-sm">
Using custom knowledge base
</div>
</div> </div>
</SidebarMenuItem> </SidebarMenuItem>
) : null ) : 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 && (
!isSubscribed && ( <a
<a href="/settings" className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"> href="/settings"
className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"
>
Upgrade Upgrade
</a> </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={
isDefaultAgent ? undefined : agentData?.chat_model
}
isActive={props.isActive} isActive={props.isActive}
/> />
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
) )}
}
<Popover defaultOpen={false}> <Popover defaultOpen={false}>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel asChild> <SidebarGroupLabel asChild>
@@ -550,14 +602,18 @@ 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 ?? {},
).map(([key, value]) => {
return ( return (
<SidebarMenuItem key={key} className="list-none"> <SidebarMenuItem key={key} className="list-none">
<Tooltip> <Tooltip>
<TooltipTrigger key={key} asChild> <TooltipTrigger key={key} asChild>
<div className="flex items-center space-x-2 py-1 justify-between"> <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"> <Label
htmlFor={key}
className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"
>
{getIconForSlashCommand(key)} {getIconForSlashCommand(key)}
<p className="text-sm my-auto flex items-center"> <p className="text-sm my-auto flex items-center">
{key} {key}
@@ -566,11 +622,22 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Checkbox <Checkbox
id={key} id={key}
className={`${isEditable ? "cursor-pointer" : ""}`} className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, displayInputTools ?? [])} checked={isValueChecked(
key,
displayInputTools ?? [],
)}
onCheckedChange={() => { onCheckedChange={() => {
let updatedInputTools = handleCheckToggle(key, displayInputTools ?? []) let updatedInputTools =
setInputTools(updatedInputTools); handleCheckToggle(
setDisplayInputTools(updatedInputTools); key,
displayInputTools ?? [],
);
setInputTools(
updatedInputTools,
);
setDisplayInputTools(
updatedInputTools,
);
}} }}
disabled={!isEditable} disabled={!isEditable}
> >
@@ -578,23 +645,30 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
</Checkbox> </Checkbox>
</div> </div>
</TooltipTrigger> </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"> <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} {value}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</SidebarMenuItem> </SidebarMenuItem>
); );
} })}
) {Object.entries(
} agentConfigurationOptions?.output_modes ?? {},
{ ).map(([key, value]) => {
Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => {
return ( return (
<SidebarMenuItem key={key} className="list-none"> <SidebarMenuItem key={key} className="list-none">
<Tooltip> <Tooltip>
<TooltipTrigger key={key} asChild> <TooltipTrigger key={key} asChild>
<div className="flex items-center space-x-2 py-1 justify-between"> <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"> <Label
htmlFor={key}
className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"
>
{getIconForSlashCommand(key)} {getIconForSlashCommand(key)}
<p className="text-sm my-auto flex items-center"> <p className="text-sm my-auto flex items-center">
{key} {key}
@@ -603,11 +677,23 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Checkbox <Checkbox
id={key} id={key}
className={`${isEditable ? "cursor-pointer" : ""}`} className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, displayOutputModes ?? [])} checked={isValueChecked(
key,
displayOutputModes ?? [],
)}
onCheckedChange={() => { onCheckedChange={() => {
let updatedOutputModes = handleCheckToggle(key, displayOutputModes ?? []) let updatedOutputModes =
setOutputModes(updatedOutputModes); handleCheckToggle(
setDisplayOutputModes(updatedOutputModes); key,
displayOutputModes ??
[],
);
setOutputModes(
updatedOutputModes,
);
setDisplayOutputModes(
updatedOutputModes,
);
}} }}
disabled={!isEditable} disabled={!isEditable}
> >
@@ -615,17 +701,19 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
</Checkbox> </Checkbox>
</div> </div>
</TooltipTrigger> </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"> <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} {value}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</SidebarMenuItem> </SidebarMenuItem>
); );
} })}
)
}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</PopoverContent> </PopoverContent>
</SidebarGroup> </SidebarGroup>
@@ -645,28 +733,30 @@ 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 ? (
{
(agentData && !isEditable && agentData.is_creator) ? (
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<Button <Button
className="w-full" className="w-full"
variant={"ghost"} variant={"ghost"}
onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`} onClick={() =>
(window.location.href = `/agents?agent=${agentData?.slug}`)
}
> >
Manage Manage
</Button> </Button>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
) : ) : (
<> <>
{ {!hasModified &&
!hasModified && isEditable && customPrompt && !isDefaultAgent && selectedModel && ( isEditable &&
customPrompt &&
!isDefaultAgent &&
selectedModel && (
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<AgentCreationForm <AgentCreationForm
@@ -677,8 +767,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
/> />
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
) )}
}
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<Button <Button
@@ -699,25 +788,20 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
onClick={() => handleSave()} onClick={() => handleSave()}
disabled={!isEditable || !hasModified || isSaving} disabled={!isEditable || !hasModified || isSaving}
> >
{ {isSaving ? (
isSaving ?
<CircleNotch className="animate-spin" /> <CircleNotch className="animate-spin" />
: ) : (
<ArrowsDownUp /> <ArrowsDownUp />
} )}
{ {isSaving ? "Saving" : "Save"}
isSaving ? "Saving" : "Save"
}
</Button> </Button>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</> </>
} )}
</SidebarMenu> </SidebarMenu>
</SidebarFooter> </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,8 +139,7 @@ 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">
@@ -141,8 +148,7 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
</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"
@@ -528,7 +528,11 @@ function FileFilterComboBox(props: FileFilterComboBoxProps) {
</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>
@@ -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 =
modelType === "chat"
? userConfig?.chat_model_options ? userConfig?.chat_model_options
: modelType === "paint" : modelType === "paint"
? userConfig?.paint_model_options ? userConfig?.paint_model_options
: userConfig?.voice_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);
@@ -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,10 +1,10 @@
"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>,
@@ -14,7 +14,7 @@ const Checkbox = React.forwardRef<
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}
> >
@@ -24,7 +24,7 @@ const Checkbox = React.forwardRef<
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

View File

@@ -1,13 +1,13 @@
"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>,
@@ -19,11 +19,11 @@ const HoverCardContent = React.forwardRef<
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,8 +1,8 @@
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
@@ -11,40 +11,27 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
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,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
className={cn( className={cn(
@@ -53,12 +40,12 @@ const PaginationLink = ({
size, size,
}), }),
"no-underline", "no-underline",
className className,
)} )}
{...props} {...props}
/> />
) );
PaginationLink.displayName = "PaginationLink" PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ const PaginationPrevious = ({
className, className,
@@ -73,13 +60,10 @@ const PaginationPrevious = ({
<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,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" size="default"
@@ -89,13 +73,10 @@ const PaginationNext = ({
<span>Next</span> <span>Next</span>
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</PaginationLink> </PaginationLink>
) );
PaginationNext.displayName = "PaginationNext" PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
className,
...props
}: React.ComponentProps<"span">) => (
<span <span
aria-hidden aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)} className={cn("flex h-9 w-9 items-center justify-center", className)}
@@ -104,8 +85,8 @@ const PaginationEllipsis = ({
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span> <span className="sr-only">More pages</span>
</span> </span>
) );
PaginationEllipsis.displayName = "PaginationEllipsis" PaginationEllipsis.displayName = "PaginationEllipsis";
export { export {
Pagination, Pagination,
@@ -115,4 +96,4 @@ export {
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} };

View File

@@ -1,15 +1,15 @@
"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>,
@@ -20,11 +20,11 @@ const TooltipContent = React.forwardRef<
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 };