mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Enhance AI model selection with search and scroll functionality
Replace standard select components with a new ModelCombobox component in both the AI chat and settings pages. This component supports searching and vertical scrolling for a better user experience when selecting AI models. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/cesUZ4w
This commit is contained in:
1 parent
c6819edd10
commit
99d4296ee1
3 files changed
+156
-41
No files matched your search
@@ -6,7 +6,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { Bot, User, Lightbulb, History, Zap, Sparkles, Loader2 } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ModelCombobox } from "@/components/model-combobox";
|
||||
|
||||
interface Message {
|
||||
role: "user" | "assistant";
|
||||
@@ -114,32 +114,16 @@ export default function AiChat() {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-sm font-medium text-foreground">Modèle IA:</label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel} disabled={modelsLoading}>
|
||||
<SelectTrigger className="w-[300px] rounded-xl" data-testid="select-ai-model">
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Chargement des modèles...</span>
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Sélectionner un modèle" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
||||
{availableModels.map((model: OpenRouterModel) => (
|
||||
<SelectItem key={model.id} value={model.id} data-testid={`model-option-${model.id}`}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{model.name}</span>
|
||||
{model.pricing && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
${model.pricing.prompt} / ${model.pricing.completion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ModelCombobox
|
||||
models={availableModels}
|
||||
value={selectedModel}
|
||||
onValueChange={setSelectedModel}
|
||||
placeholder="Sélectionner un modèle"
|
||||
isLoading={modelsLoading}
|
||||
disabled={modelsLoading}
|
||||
className="w-[300px] rounded-xl"
|
||||
testId="select-ai-model"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState } from "react";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/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";
|
||||
|
||||
interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
pricing?: {
|
||||
prompt: string;
|
||||
completion: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ModelComboboxProps {
|
||||
models: Model[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function ModelCombobox({
|
||||
models,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = "Sélectionner un modèle",
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
className,
|
||||
testId,
|
||||
}: ModelComboboxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const selectedModel = models.find((model) => model.id === value);
|
||||
|
||||
const filteredModels = models.filter((model) =>
|
||||
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
model.id.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("justify-between", className)}
|
||||
disabled={disabled || isLoading}
|
||||
data-testid={testId}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Chargement des modèles...</span>
|
||||
</div>
|
||||
) : selectedModel ? (
|
||||
<div className="flex flex-col items-start w-full overflow-hidden">
|
||||
<span className="font-medium truncate w-full">{selectedModel.name}</span>
|
||||
{selectedModel.pricing && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
${selectedModel.pricing.prompt} / ${selectedModel.pricing.completion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
placeholder
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Rechercher un modèle..."
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
data-testid={`${testId}-search`}
|
||||
/>
|
||||
<CommandList className="max-h-[300px] overflow-y-auto">
|
||||
<CommandEmpty>Aucun modèle trouvé.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredModels.map((model) => (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
onSelect={(currentValue) => {
|
||||
onValueChange(currentValue);
|
||||
setOpen(false);
|
||||
setSearchQuery("");
|
||||
}}
|
||||
data-testid={`${testId}-option-${model.id}`}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === model.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<span className="font-medium truncate">{model.name}</span>
|
||||
{model.pricing && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Prompt: ${model.pricing.prompt} | Completion: ${model.pricing.completion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { ModelCombobox } from "@/components/model-combobox";
|
||||
|
||||
export default function Settings() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -33,7 +33,7 @@ export default function Settings() {
|
||||
queryKey: ['/api/openrouter/config'],
|
||||
});
|
||||
|
||||
const { data: openrouterModels } = useQuery({
|
||||
const { data: openrouterModels, isLoading: modelsLoading } = useQuery({
|
||||
queryKey: ['/api/openrouter/models'],
|
||||
});
|
||||
|
||||
@@ -227,18 +227,15 @@ export default function Settings() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="openrouterModel">Modèle d'IA</Label>
|
||||
<Select value={openrouterModel} onValueChange={setOpenrouterModel}>
|
||||
<SelectTrigger id="openrouterModel" data-testid="select-openrouter-model">
|
||||
<SelectValue placeholder="Sélectionner un modèle" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(openrouterModels as any)?.data?.map((model: any) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ModelCombobox
|
||||
models={(openrouterModels as any)?.data || []}
|
||||
value={openrouterModel}
|
||||
onValueChange={setOpenrouterModel}
|
||||
placeholder="Sélectionner un modèle"
|
||||
isLoading={modelsLoading}
|
||||
className="w-full"
|
||||
testId="select-openrouter-model"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choisissez le modèle d'IA pour générer vos publications
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user