Merge pull request #61 from R0m1k3/dev

Dev
This commit is contained in:
LogiFlow authored and GitHub committed 2025-10-11 09:21:01 +02:00
commit b92732069e
4 files changed
+159 -41

No files matched your search

+11 -27
View File
@@ -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>
+134
View File
@@ -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>
);
}
+11 -14
View File
@@ -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>
+3
View File
@@ -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.