Add a ride hand side bar for chat controls

This commit is contained in:
sabaimran
2025-01-17 16:45:50 -08:00
parent 00843f4f24
commit 2fa212061d
15 changed files with 465 additions and 51 deletions

View File

@@ -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 &&

View 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>
)
}

View File

@@ -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`;