mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
- Update API to allow free tier users to switch between free models - Update web app to allow model switching on agent creation, settings chat page (via right side pane), even for free tier users. Previously the model switching APIs and UX fields on web app were completely disabled for free tier users
223 lines
9.8 KiB
TypeScript
223 lines
9.8 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { useState, useEffect } from "react";
|
|
import { PopoverProps } from "@radix-ui/react-popover"
|
|
|
|
import { Check, CaretUpDown } from "@phosphor-icons/react";
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { useIsMobileWidth, useMutationObserver } from "@/app/common/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
|
|
import { ModelOptions, useUserConfig } from "./auth";
|
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
interface ModelSelectorProps extends PopoverProps {
|
|
onSelect: (model: ModelOptions) => void;
|
|
disabled?: boolean;
|
|
isActive?: boolean;
|
|
initialModel?: string;
|
|
}
|
|
|
|
export function ModelSelector({ ...props }: ModelSelectorProps) {
|
|
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);
|
|
const [models, setModels] = useState<ModelOptions[]>([]);
|
|
const isMobileWidth = useIsMobileWidth();
|
|
|
|
useEffect(() => {
|
|
if (isLoadingUserConfig) return;
|
|
|
|
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);
|
|
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);
|
|
setSelectedModel(model);
|
|
}
|
|
}
|
|
}, [userConfig, props.initialModel, isLoadingUserConfig]);
|
|
|
|
useEffect(() => {
|
|
if (selectedModel && userConfig) {
|
|
props.onSelect(selectedModel);
|
|
}
|
|
}, [selectedModel, userConfig, props.onSelect]);
|
|
|
|
if (isLoadingUserConfig) {
|
|
return (
|
|
<Skeleton className="w-full h-10" />
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="text-sm text-error">{error.message}</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid gap-2 w-[250px]">
|
|
<Popover open={open} onOpenChange={setOpen} {...props}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
aria-label="Select a model"
|
|
className="w-full justify-between text-left"
|
|
disabled={props.disabled ?? false}
|
|
>
|
|
<p className="truncate">
|
|
{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 ?
|
|
<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) => (
|
|
<ModelItem
|
|
key={model.id}
|
|
model={model}
|
|
isSelected={selectedModel?.id === model.id}
|
|
onPeek={(model) => setPeekedModel(model)}
|
|
onSelect={() => {
|
|
setSelectedModel(model)
|
|
setOpen(false)
|
|
}}
|
|
isActive={props.isActive}
|
|
/>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</div>
|
|
</HoverCard>
|
|
}
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ModelItemProps {
|
|
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)
|
|
|
|
useMutationObserver(ref, (mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (
|
|
mutation.type === "attributes" &&
|
|
mutation.attributeName === "aria-selected" &&
|
|
ref.current?.getAttribute("aria-selected") === "true"
|
|
) {
|
|
onPeek(model)
|
|
}
|
|
})
|
|
})
|
|
|
|
return (
|
|
<CommandItem
|
|
key={model.id}
|
|
onSelect={onSelect}
|
|
ref={ref}
|
|
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")}
|
|
/>
|
|
</CommandItem>
|
|
)
|
|
}
|