Enable free tier users to switch between free tier AI models

- 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
This commit is contained in:
Debanjum
2025-04-01 11:54:09 +05:30
parent 30570e3e06
commit 79fc911633
10 changed files with 165 additions and 47 deletions

View File

@@ -24,9 +24,7 @@ import {
ChatOptions,
} from "../components/chatInputArea/chatInputArea";
import { useAuthenticatedData } from "../common/auth";
import {
AgentData,
} from "@/app/components/agentCard/agentCard";
import { AgentData } from "@/app/components/agentCard/agentCard";
import { ChatSessionActionMenu } from "../components/allConversations/allConversations";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "../components/appSidebar/appSidebar";
@@ -50,6 +48,7 @@ interface ChatBodyDataProps {
setTriggeredAbort: (triggeredAbort: boolean) => void;
isChatSideBarOpen: boolean;
setIsChatSideBarOpen: (open: boolean) => void;
isActive?: boolean;
}
function ChatBodyData(props: ChatBodyDataProps) {
@@ -180,9 +179,11 @@ function ChatBodyData(props: ChatBodyDataProps) {
</div>
<ChatSidebar
conversationId={conversationId}
isActive={props.isActive}
isOpen={props.isChatSideBarOpen}
onOpenChange={props.setIsChatSideBarOpen}
isMobileWidth={props.isMobileWidth} />
isMobileWidth={props.isMobileWidth}
/>
</div>
);
}
@@ -480,12 +481,13 @@ export default function Chat() {
setTriggeredAbort={setTriggeredAbort}
isChatSideBarOpen={isChatSideBarOpen}
setIsChatSideBarOpen={setIsChatSideBarOpen}
isActive={authenticatedData?.is_active}
/>
</Suspense>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider >
</SidebarProvider>
);
}

View File

@@ -33,6 +33,7 @@ export function useAuthenticatedData() {
export interface ModelOptions {
id: number;
name: string;
tier: string;
description: string;
strengths: string;
}

View File

@@ -30,6 +30,7 @@ import { Skeleton } from "@/components/ui/skeleton";
interface ModelSelectorProps extends PopoverProps {
onSelect: (model: ModelOptions) => void;
disabled?: boolean;
isActive?: boolean;
initialModel?: string;
}
@@ -116,6 +117,7 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
setSelectedModel(model)
setOpen(false)
}}
isActive={props.isActive}
/>
))}
</CommandGroup>
@@ -165,6 +167,7 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
setSelectedModel(model)
setOpen(false)
}}
isActive={props.isActive}
/>
))}
</CommandGroup>
@@ -184,9 +187,10 @@ interface ModelItemProps {
isSelected: boolean,
onSelect: () => void,
onPeek: (model: ModelOptions) => void
isActive?: boolean
}
function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemProps) {
const ref = React.useRef<HTMLDivElement>(null)
useMutationObserver(ref, (mutations) => {
@@ -207,8 +211,9 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
onSelect={onSelect}
ref={ref}
className="data-[selected=true]:bg-muted data-[selected=true]:text-secondary-foreground"
disabled={!isActive && model.tier !== "free"}
>
{model.name}
{model.name} {model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
<Check
className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")}
/>

View File

@@ -773,11 +773,7 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
<p>Which chat model would you like to use?</p>
)}
</FormDescription>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
disabled={!props.isSubscribed}
>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="text-left dark:bg-muted">
<SelectValue />
@@ -788,9 +784,18 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
<SelectItem
key={modelOption.id}
value={modelOption.name}
disabled={
!props.isSubscribed &&
modelOption.tier !== "free"
}
>
<div className="flex items-center space-x-2">
{modelOption.name}
{modelOption.name}{" "}
{modelOption.tier === "standard" && (
<span className="text-green-500 ml-2">
(Futurist)
</span>
)}
</div>
</SelectItem>
))}

View File

@@ -34,6 +34,7 @@ interface ChatSideBarProps {
isOpen: boolean;
isMobileWidth?: boolean;
onOpenChange: (open: boolean) => void;
isActive?: boolean;
}
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -527,9 +528,10 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<SidebarMenu className="p-0 m-0">
<SidebarMenuItem key={"model"} className="list-none">
<ModelSelector
disabled={!isEditable || !isSubscribed}
disabled={!isEditable}
onSelect={(model) => handleModelSelect(model.name)}
initialModel={isDefaultAgent ? undefined : agentData?.chat_model}
isActive={props.isActive}
/>
</SidebarMenuItem>
</SidebarMenu>

View File

@@ -77,10 +77,11 @@ import { saveAs } from 'file-saver';
interface DropdownComponentProps {
items: ModelOptions[];
selected: number;
isActive?: boolean;
callbackFunc: (value: string) => Promise<void>;
}
const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected, callbackFunc }) => {
const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected, isActive, callbackFunc }) => {
const [position, setPosition] = useState(selected?.toString() ?? "0");
return (
@@ -111,8 +112,9 @@ const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected,
<DropdownMenuRadioItem
key={item.id.toString()}
value={item.id.toString()}
disabled={!isActive && item.tier !== "free"}
>
{item.name}
{item.name} {item.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
@@ -520,33 +522,44 @@ export default function SettingsView() {
}
};
const updateModel = (name: string) => async (id: string) => {
if (!userConfig?.is_active) {
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 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
if (!userConfig?.is_active && selectedModel?.tier !== "free") {
toast({
title: `Model Update`,
description: `You need to be subscribed to update ${name} models`,
description: `Subscribe to switch ${modelType} model to ${modelName}.`,
variant: "destructive",
});
return;
}
try {
const response = await fetch(`/api/model/${name}?id=` + id, {
const response = await fetch(`/api/model/${modelType}?id=` + id, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error("Failed to update model");
if (!response.ok) throw new Error(`Failed to switch ${modelType} model to ${modelName}`);
toast({
title: `Updated ${toTitleCase(name)} Model`,
title: `Switched ${modelType} model to ${modelName}`,
});
} catch (error) {
console.error(`Failed to update ${name} model:`, error);
console.error(`Failed to update ${modelType} model to ${modelName}:`, error);
toast({
description: `❌ Failed to update ${toTitleCase(name)} model. Try again.`,
description: `❌ Failed to switch ${modelType} model to ${modelName}. Try again.`,
variant: "destructive",
});
}
@@ -1103,13 +1116,16 @@ export default function SettingsView() {
selected={
userConfig.selected_chat_model_config
}
isActive={userConfig.is_active}
callbackFunc={updateModel("chat")}
/>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<p className="text-gray-400">
Subscribe to switch model
{userConfig.chat_model_options.some(model => model.tier === "free")
? "Free models available"
: "Subscribe to switch model"}
</p>
)}
</CardFooter>
@@ -1131,13 +1147,16 @@ export default function SettingsView() {
selected={
userConfig.selected_paint_model_config
}
isActive={userConfig.is_active}
callbackFunc={updateModel("paint")}
/>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<p className="text-gray-400">
Subscribe to switch model
{userConfig.paint_model_options.some(model => model.tier === "free")
? "Free models available"
: "Subscribe to switch model"}
</p>
)}
</CardFooter>
@@ -1159,13 +1178,16 @@ export default function SettingsView() {
selected={
userConfig.selected_voice_model_config
}
isActive={userConfig.is_active}
callbackFunc={updateModel("voice")}
/>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{!userConfig.is_active && (
<p className="text-gray-400">
Subscribe to switch model
{userConfig.voice_model_options.some(model => model.tier === "free")
? "Free models available"
: "Subscribe to switch model"}
</p>
)}
</CardFooter>