Merge pull request #53 from R0m1k3/dev

Dev
This commit is contained in:
LogiFlow authored and GitHub committed 2025-10-10 06:53:46 +02:00
commit e5baacc9d4
8 files changed
+123 -14

No files matched your search

+4
View File
@@ -34,6 +34,10 @@ externalPort = 5000
localPort = 37659
externalPort = 4200
[[ports]]
localPort = 38631
externalPort = 8080
[[ports]]
localPort = 40537
externalPort = 3000
+56 -4
View File
@@ -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<Message[]>([
{
@@ -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 (
<div className="bg-card rounded-2xl border border-border/50 overflow-hidden shadow-lg h-full flex flex-col">
<div className="border-b border-border/50 p-6 bg-gradient-to-r from-primary/5 to-secondary/5">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg">
<Sparkles className="text-white w-6 h-6" />
@@ -89,6 +111,36 @@ export default function AiChat() {
Nouveau chat
</Button>
</div>
<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>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-6 bg-muted/20">
+1
View File
@@ -417,6 +417,7 @@ export default function CalendarView() {
mediaList={previewData.allMedia}
onPublish={() => {}}
isPublishing={false}
readOnly={true}
/>
</div>
);
+14 -1
View File
@@ -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<PreviewFormat>('facebook-feed');
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
@@ -311,6 +313,15 @@ export function PreviewModal({
{/* Actions */}
<div className="flex justify-end gap-3">
{readOnly ? (
<Button
onClick={() => onOpenChange(false)}
data-testid="button-close-preview"
>
Fermer
</Button>
) : (
<>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
@@ -327,6 +338,8 @@ export function PreviewModal({
<Send className="w-4 h-4 mr-2" />
{isPublishing ? 'Publication...' : 'Publier'}
</Button>
</>
)}
</div>
</div>
</DialogContent>
@@ -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() {
</span>
<span>•</span>
<span>
{format(new Date(scheduledPost.publishedAt!), "d MMM yyyy 'à' HH:mm", { locale: fr })}
{format(new Date(scheduledPost.scheduledAt!), "d MMM yyyy 'à' HH:mm", { locale: fr })}
</span>
{scheduledPost.error && (
<>
@@ -165,6 +166,7 @@ export default function RecentPublications() {
mediaList={previewData.allMedia}
onPublish={() => {}}
isPublishing={false}
readOnly={true}
/>
</Card>
);
+2
View File
@@ -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
+13 -2
View File
@@ -423,14 +423,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// 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({
+27 -3
View File
@@ -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<GeneratedText[]> {
async generatePostText(productInfo: ProductInfo, userId: string, modelOverride?: string): Promise<GeneratedText[]> {
// Get user's OpenRouter configuration
const config = await storage.getOpenrouterConfig(userId);
@@ -27,6 +27,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, {
method: "POST",
@@ -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<any[]> {
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();