mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
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:
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -48,13 +48,12 @@ async function openChat(userData: UserProfile | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Menu items.
|
||||
const items = [
|
||||
{
|
||||
title: "Home",
|
||||
url: "/",
|
||||
icon: HouseSimple
|
||||
icon: HouseSimple,
|
||||
},
|
||||
{
|
||||
title: "Agents",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user