diff --git a/.replit b/.replit index 7bce39b..8bbc693 100644 --- a/.replit +++ b/.replit @@ -34,6 +34,10 @@ externalPort = 5000 localPort = 37659 externalPort = 4200 +[[ports]] +localPort = 38631 +externalPort = 8080 + [[ports]] localPort = 40537 externalPort = 3000 diff --git a/client/src/components/ai-chat.tsx b/client/src/components/ai-chat.tsx index 212eb35..bb2d530 100644 --- a/client/src/components/ai-chat.tsx +++ b/client/src/components/ai-chat.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { useMutation, useQuery } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { Bot, User, Lightbulb, History, Zap, Sparkles } from "lucide-react"; +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"; interface Message { role: "user" | "assistant"; @@ -13,6 +14,16 @@ interface Message { variants?: Array<{ variant: string; text: string; characterCount: number }>; } +interface OpenRouterModel { + id: string; + name: string; + context_length?: number; + pricing?: { + prompt: string; + completion: string; + }; +} + export default function AiChat() { const [messages, setMessages] = useState([ { @@ -21,11 +32,22 @@ export default function AiChat() { }, ]); const [input, setInput] = useState(""); + const [selectedModel, setSelectedModel] = useState("anthropic/claude-3.5-sonnet"); const { toast } = useToast(); + // Fetch available models from OpenRouter + const { data: modelsData, isLoading: modelsLoading } = useQuery<{ models: OpenRouterModel[] }>({ + queryKey: ['/api/ai/models'], + }); + + const availableModels = modelsData?.models || []; + const generateMutation = useMutation({ mutationFn: async (productInfo: any) => { - const response = await apiRequest("POST", "/api/ai/generate", productInfo); + const response = await apiRequest("POST", "/api/ai/generate", { + ...productInfo, + model: selectedModel, + }); return response.json(); }, onSuccess: (data) => { @@ -68,7 +90,7 @@ export default function AiChat() { return (
-
+
@@ -89,6 +111,36 @@ export default function AiChat() { Nouveau chat
+ +
+ + +
diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 1466388..5d9fdfe 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -417,6 +417,7 @@ export default function CalendarView() { mediaList={previewData.allMedia} onPublish={() => {}} isPublishing={false} + readOnly={true} />
); diff --git a/client/src/components/preview-modal.tsx b/client/src/components/preview-modal.tsx index 5e1f55a..9b796f9 100644 --- a/client/src/components/preview-modal.tsx +++ b/client/src/components/preview-modal.tsx @@ -13,6 +13,7 @@ interface PreviewModalProps { mediaList: Media[]; onPublish: () => void; isPublishing: boolean; + readOnly?: boolean; } type PreviewFormat = 'facebook-feed' | 'instagram-feed' | 'instagram-story'; @@ -24,7 +25,8 @@ export function PreviewModal({ selectedMedia, mediaList, onPublish, - isPublishing + isPublishing, + readOnly = false }: PreviewModalProps) { const [previewFormat, setPreviewFormat] = useState('facebook-feed'); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); @@ -311,22 +313,33 @@ export function PreviewModal({ {/* Actions */}
- - + {readOnly ? ( + + ) : ( + <> + + + + )}
diff --git a/client/src/components/recent-publications.tsx b/client/src/components/recent-publications.tsx index 86808b4..550d0ab 100644 --- a/client/src/components/recent-publications.tsx +++ b/client/src/components/recent-publications.tsx @@ -23,10 +23,11 @@ export default function RecentPublications() { queryKey: ['/api/scheduled-posts'], }); - // Filter and sort published posts + // Filter and sort attempted posts (scheduled in the past) + const now = new Date(); const recentPublished = scheduledPosts - .filter(sp => sp.publishedAt) - .sort((a, b) => new Date(b.publishedAt!).getTime() - new Date(a.publishedAt!).getTime()) + .filter(sp => sp.scheduledAt && new Date(sp.scheduledAt) <= now) + .sort((a, b) => new Date(b.scheduledAt!).getTime() - new Date(a.scheduledAt!).getTime()) .slice(0, 10); const handlePreviewPost = async (scheduledPost: ScheduledPostWithRelations) => { @@ -141,7 +142,7 @@ export default function RecentPublications() { • - {format(new Date(scheduledPost.publishedAt!), "d MMM yyyy 'à' HH:mm", { locale: fr })} + {format(new Date(scheduledPost.scheduledAt!), "d MMM yyyy 'à' HH:mm", { locale: fr })} {scheduledPost.error && ( <> @@ -165,6 +166,7 @@ export default function RecentPublications() { mediaList={previewData.allMedia} onPublish={() => {}} isPublishing={false} + readOnly={true} /> ); diff --git a/replit.md b/replit.md index cdadfc8..22b6fd7 100644 --- a/replit.md +++ b/replit.md @@ -15,6 +15,8 @@ Preferred communication style: Simple, everyday language. - **Calendar Auto-Refresh**: Fixed calendar not updating after creating a new scheduled post. Added `queryClient.invalidateQueries` with `refetchType: 'all'` in `createPostMutation.onSuccess` to force cache invalidation and immediate refetch - **Dashboard Scheduled Posts Count**: Fixed scheduled posts count showing 0 in dashboard. Problem was that posts with `scheduledFor` were created with status "draft" instead of "scheduled". Solution: POST /api/posts now sets `status = "scheduled"` when `scheduledFor` is provided. Legacy posts updated with SQL: `UPDATE posts SET status = 'scheduled' WHERE scheduled_for IS NOT NULL AND status = 'draft'` - **Preview in Calendar & History**: Added eye icon (👁️) buttons to preview posts across calendar (desktop/mobile) and history sections. Clicking the eye icon fetches full post data via GET /api/posts/:id and opens PreviewModal with post text and all associated media. Desktop calendar shows icon on hover; mobile and history show icon always. Preview available in calendar grid, popover ("voir plus"), list view, and recent publications history. +- **Dynamic AI Model Selector**: Replaced hardcoded list of 13 models with dynamic loading from OpenRouter API. Added GET /api/ai/models endpoint that fetches complete catalog (326+ models) from https://openrouter.ai/api/v1/models. AI Assistant page now displays all available models with pricing info in scrollable dropdown. Backend accepts optional model parameter to override config model. Loading state shows spinner while fetching models. +- **Read-Only Preview Mode**: Enhanced PreviewModal with optional `readOnly` prop that hides the "Publier" button when viewing posts from calendar or history. Calendar and history now pass `readOnly={true}` to PreviewModal, showing only "Fermer" button. History section updated to display all attempted posts (where `scheduledAt` is in the past) instead of only successfully published posts, and displays `scheduledAt` date instead of `publishedAt`. ## System Architecture diff --git a/server/routes.ts b/server/routes.ts index a176d7a..7582533 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -423,14 +423,25 @@ export async function registerRoutes(app: Express): Promise { } }); + // Get available AI models from OpenRouter + app.get("/api/ai/models", requireAuth, async (req, res) => { + try { + const models = await openRouterService.getAvailableModels(); + res.json({ models }); + } catch (error) { + console.error("Error fetching models:", error); + res.status(500).json({ error: "Failed to fetch models" }); + } + }); + // AI text generation app.post("/api/ai/generate", requireAuth, async (req, res) => { try { const user = req.user as User; const userId = user.id; - const productInfo = req.body; + const { model, ...productInfo } = req.body; - const generatedTexts = await openRouterService.generatePostText(productInfo, userId); + const generatedTexts = await openRouterService.generatePostText(productInfo, userId, model); // Save to database await storage.createAiGeneration({ diff --git a/server/services/openrouter.ts b/server/services/openrouter.ts index 1653903..e1747a2 100644 --- a/server/services/openrouter.ts +++ b/server/services/openrouter.ts @@ -17,7 +17,7 @@ import { storage } from '../storage'; export class OpenRouterService { private baseUrl = "https://openrouter.ai/api/v1/chat/completions"; - async generatePostText(productInfo: ProductInfo, userId: string): Promise { + async generatePostText(productInfo: ProductInfo, userId: string, modelOverride?: string): Promise { // Get user's OpenRouter configuration const config = await storage.getOpenrouterConfig(userId); @@ -26,6 +26,9 @@ export class OpenRouterService { } const prompt = this.buildPrompt(productInfo, config.systemPrompt); + + // Use provided model or fall back to config model + const modelToUse = modelOverride || config.model; try { const response = await fetch(this.baseUrl, { @@ -37,7 +40,7 @@ export class OpenRouterService { "X-Title": "Social Flow" }, body: JSON.stringify({ - model: config.model, + model: modelToUse, messages: [ { role: "user", @@ -58,7 +61,7 @@ export class OpenRouterService { const data = await response.json(); if (!data.choices || !data.choices[0]) { - console.error('Invalid OpenRouter response - no choices:', { hasChoices: !!data.choices, model: config.model }); + console.error('Invalid OpenRouter response - no choices:', { hasChoices: !!data.choices, model: modelToUse }); throw new Error('Réponse invalide de l\'API OpenRouter. Vérifiez votre clé API et votre modèle.'); } @@ -202,6 +205,27 @@ VERSION 3 - ÉMOTIONNELLE: private capitalizeFirst(text: string): string { return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); } + + async getAvailableModels(): Promise { + try { + const response = await fetch("https://openrouter.ai/api/v1/models", { + method: "GET", + headers: { + "Content-Type": "application/json", + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return data.data || []; + } catch (error) { + console.error("Error fetching OpenRouter models:", error); + throw error; + } + } } export const openRouterService = new OpenRouterService();