Checkpoint: Ajout de la section "Moteur IA" dans les paramètres : sélecteur Mistral/Manus, champs clé API avec affichage masqué, persistance en DB, migration VPS appliquée

This commit is contained in:
Manus
2026-04-15 08:04:54 -04:00
parent 7fbb3d5f2c
commit 62b7a61bf5
10 changed files with 3807 additions and 13 deletions

View File

@@ -23,7 +23,11 @@ import {
Plus,
Trash2,
Upload,
User
User,
Eye,
EyeOff,
Zap,
Bot
} from "lucide-react";
// useRef, useCallback already imported above
import { toast } from "sonner";
@@ -673,6 +677,12 @@ export default function Settings() {
const [sftpAutoExport, setSftpAutoExport] = useState(false);
const [sftpRecipientFilter, setSftpRecipientFilter] = useState("");
const [llmLogsRetentionMonths, setLlmLogsRetentionMonths] = useState(3);
const [aiProvider, setAiProvider] = useState<"mistral" | "manus">("mistral");
const [mistralApiKey, setMistralApiKey] = useState("");
const [manusForgeApiKey, setManusForgeApiKey] = useState("");
const [manusForgeApiUrl, setManusForgeApiUrl] = useState("");
const [showMistralKey, setShowMistralKey] = useState(false);
const [showManusKey, setShowManusKey] = useState(false);
useEffect(() => {
if (settings) {
@@ -692,6 +702,10 @@ export default function Settings() {
setSftpAutoExport(settings.sftpAutoExport === 1);
setSftpRecipientFilter((settings as any).sftpRecipientFilter || "");
setLlmLogsRetentionMonths(settings.llmLogsRetentionMonths || 3);
setAiProvider((settings as any).aiProvider || "mistral");
setMistralApiKey((settings as any).mistralApiKey || "");
setManusForgeApiKey((settings as any).manusForgeApiKey || "");
setManusForgeApiUrl((settings as any).manusForgeApiUrl || "");
}
}, [settings]);
@@ -736,6 +750,10 @@ export default function Settings() {
sftpAutoExport: sftpAutoExport ? 1 : 0,
sftpRecipientFilter,
llmLogsRetentionMonths,
aiProvider,
mistralApiKey,
manusForgeApiKey,
manusForgeApiUrl,
});
};
@@ -816,6 +834,156 @@ export default function Settings() {
{/* LLM Tab */}
<TabsContent value="llm" className="space-y-6 animate-in fade-in-50 duration-300">
{/* AI Engine Selector */}
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-500 rounded-lg">
<Zap className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Moteur IA</CardTitle>
<CardDescription className="mt-1">
Choisissez le fournisseur d'intelligence artificielle pour l'extraction des factures
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6 pt-6">
{/* Provider selector */}
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setAiProvider("mistral")}
className={`relative flex flex-col items-center gap-3 p-5 rounded-xl border-2 transition-all cursor-pointer ${
aiProvider === "mistral"
? "border-orange-500 bg-orange-50 dark:bg-orange-950/20 shadow-md"
: "border-muted hover:border-orange-300 bg-background"
}`}
>
{aiProvider === "mistral" && (
<div className="absolute top-2 right-2">
<CheckCircle className="w-5 h-5 text-orange-500" />
</div>
)}
<div className="p-3 bg-orange-100 dark:bg-orange-900/30 rounded-xl">
<Bot className="w-8 h-8 text-orange-600" />
</div>
<div className="text-center">
<p className="font-bold text-base">Mistral AI</p>
<p className="text-xs text-muted-foreground mt-1">API Mistral directe<br/>Clé API requise</p>
</div>
{aiProvider === "mistral" && (
<Badge className="bg-orange-500 text-white text-xs">Actif</Badge>
)}
</button>
<button
type="button"
onClick={() => setAiProvider("manus")}
className={`relative flex flex-col items-center gap-3 p-5 rounded-xl border-2 transition-all cursor-pointer ${
aiProvider === "manus"
? "border-blue-500 bg-blue-50 dark:bg-blue-950/20 shadow-md"
: "border-muted hover:border-blue-300 bg-background"
}`}
>
{aiProvider === "manus" && (
<div className="absolute top-2 right-2">
<CheckCircle className="w-5 h-5 text-blue-500" />
</div>
)}
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-xl">
<Sparkles className="w-8 h-8 text-blue-600" />
</div>
<div className="text-center">
<p className="font-bold text-base">Manus AI</p>
<p className="text-xs text-muted-foreground mt-1">API Manus Forge<br/>Clé Forge requise</p>
</div>
{aiProvider === "manus" && (
<Badge className="bg-blue-500 text-white text-xs">Actif</Badge>
)}
</button>
</div>
{/* Mistral config */}
{aiProvider === "mistral" && (
<div className="space-y-4 p-4 bg-orange-50 dark:bg-orange-950/10 rounded-xl border border-orange-200 dark:border-orange-800">
<p className="text-sm font-semibold text-orange-700 dark:text-orange-400 uppercase tracking-wide">Configuration Mistral AI</p>
<div className="space-y-2">
<Label htmlFor="mistralApiKey" className="font-medium">Clé API Mistral</Label>
<div className="relative">
<Input
id="mistralApiKey"
type={showMistralKey ? "text" : "password"}
value={mistralApiKey}
onChange={(e) => setMistralApiKey(e.target.value)}
placeholder="Votre clé API Mistral (ex: 3rCjWwA2...)"
className="h-11 pr-10"
/>
<button
type="button"
onClick={() => setShowMistralKey(!showMistralKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showMistralKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Obtenez votre clé sur <a href="https://console.mistral.ai" target="_blank" rel="noopener noreferrer" className="text-orange-600 hover:underline">console.mistral.ai</a>. Laissez vide pour utiliser la variable d'environnement MISTRAL_API_KEY du serveur.
</p>
</div>
</div>
)}
{/* Manus config */}
{aiProvider === "manus" && (
<div className="space-y-4 p-4 bg-blue-50 dark:bg-blue-950/10 rounded-xl border border-blue-200 dark:border-blue-800">
<p className="text-sm font-semibold text-blue-700 dark:text-blue-400 uppercase tracking-wide">Configuration Manus Forge</p>
<div className="space-y-2">
<Label htmlFor="manusForgeApiUrl" className="font-medium">URL de l'API Forge</Label>
<Input
id="manusForgeApiUrl"
type="text"
value={manusForgeApiUrl}
onChange={(e) => setManusForgeApiUrl(e.target.value)}
placeholder="https://forge.manus.im"
className="h-11"
/>
<p className="text-xs text-muted-foreground">Laissez vide pour utiliser la valeur par défaut (https://forge.manus.im)</p>
</div>
<div className="space-y-2">
<Label htmlFor="manusForgeApiKey" className="font-medium">Clé API Forge</Label>
<div className="relative">
<Input
id="manusForgeApiKey"
type={showManusKey ? "text" : "password"}
value={manusForgeApiKey}
onChange={(e) => setManusForgeApiKey(e.target.value)}
placeholder="Votre clé API Manus Forge"
className="h-11 pr-10"
/>
<button
type="button"
onClick={() => setShowManusKey(!showManusKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showManusKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Laissez vide pour utiliser la variable d'environnement BUILT_IN_FORGE_API_KEY du serveur.
</p>
</div>
</div>
)}
</CardContent>
</Card>
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-b">
<div className="flex items-center gap-3">