mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
4 files changed
+159
-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>
|
||||
|
||||
@@ -10,6 +10,9 @@ Preferred communication style: Simple, everyday language.
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### October 11, 2025
|
||||
- **Model Selector Improvement**: Replaced basic Select dropdowns with advanced ModelCombobox component featuring search and scroll functionality. The new combobox allows users to search models by name across 326+ OpenRouter models, displays pricing information (prompt/completion costs), and provides smooth scrolling through a max-height limited list. Implemented in both Settings page (OpenRouter configuration) and AI Assistant page. The combobox prevents accidental deselection and includes loading states with spinner for better UX.
|
||||
|
||||
### October 10, 2025
|
||||
- **Post Type Visual Indicators**: Added visual icons throughout the app to distinguish post types at a glance. Feed posts display Image icon (🖼️), Story posts display Smartphone icon (📱), and posts configured for both show both icons side-by-side. Icons appear consistently across calendar view (desktop grid and mobile list), dashboard "Historique des publications" section, and history page for improved visual clarity.
|
||||
- **Calendar Auto-Refresh**: Implemented automatic calendar data refresh every 30 seconds using TanStack Query's `refetchInterval` option. Calendar continuously fetches updated scheduled posts without requiring page reload or manual refresh, ensuring users see the latest schedule data in real-time.
|
||||
|
||||
Reference in new issue
Block a user