mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
Add a ride hand side bar for chat controls
This commit is contained in:
@@ -33,6 +33,8 @@ export function useAuthenticatedData() {
|
||||
export interface ModelOptions {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
strengths: string;
|
||||
}
|
||||
export interface SyncedContent {
|
||||
computer: boolean;
|
||||
@@ -99,6 +101,14 @@ export function useUserConfig(detailed: boolean = false) {
|
||||
return { userConfig, isLoadingUserConfig };
|
||||
}
|
||||
|
||||
export function useChatModelOptions() {
|
||||
const { data, error, isLoading } = useSWR<ModelOptions[]>(`/api/model/chat/options`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
return { models: data, error, isLoading };
|
||||
}
|
||||
|
||||
export function isUserSubscribed(userConfig: UserConfig | null): boolean {
|
||||
return (
|
||||
(userConfig?.subscription_state &&
|
||||
|
||||
168
src/interface/web/app/common/modelSelector.tsx
Normal file
168
src/interface/web/app/common/modelSelector.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"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 { useMutationObserver } from "@/app/common/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { ModelOptions, useChatModelOptions } from "./auth";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function ModelSelector({ ...props }: PopoverProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { models, isLoading, error } = useChatModelOptions();
|
||||
const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined);
|
||||
const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (models && models.length > 0) {
|
||||
setSelectedModel(models[0])
|
||||
}
|
||||
|
||||
if (models && models.length > 0 && !selectedModel) {
|
||||
setSelectedModel(models[0])
|
||||
}
|
||||
|
||||
}, [models]);
|
||||
|
||||
if (isLoading) {
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
{peekedModel.strengths}
|
||||
</ul>
|
||||
</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)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelItemProps {
|
||||
model: ModelOptions,
|
||||
isSelected: boolean,
|
||||
onSelect: () => void,
|
||||
onPeek: (model: ModelOptions) => void
|
||||
}
|
||||
|
||||
function ModelItem({ model, isSelected, onSelect, onPeek }: 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-primary data-[selected=true]:text-primary-foreground"
|
||||
>
|
||||
{model.name}
|
||||
<Check
|
||||
className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import * as React from "react"
|
||||
|
||||
export interface LocationData {
|
||||
city?: string;
|
||||
@@ -69,6 +70,25 @@ export function useIsMobileWidth() {
|
||||
return isMobileWidth;
|
||||
}
|
||||
|
||||
export const useMutationObserver = (
|
||||
ref: React.MutableRefObject<HTMLElement | null>,
|
||||
callback: MutationCallback,
|
||||
options = {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
const observer = new MutationObserver(callback)
|
||||
observer.observe(ref.current, options)
|
||||
return () => observer.disconnect()
|
||||
}
|
||||
}, [ref, callback, options])
|
||||
}
|
||||
|
||||
export const convertBytesToText = (fileSize: number) => {
|
||||
if (fileSize < 1024) {
|
||||
return `${fileSize} B`;
|
||||
|
||||
Reference in New Issue
Block a user