Live AI provider/model switching without restart
CI / Lint & Format (push) Failing after 8s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 8s
CI / Docker Build (push) Has been skipped

Backend:
- RuntimeAIConfig singleton: mutable in-memory overrides for provider/model
- PATCH /admin/api/ai/config: change active provider/model on the fly
- GET /admin/api/ai/config: read current runtime state
- ai_vision.py: uses runtime_ai_config before falling back to settings
- health/status endpoints reflect runtime overrides

Frontend:
- 'Définir comme actif' button in AI config section
- Live feedback message on save
This commit is contained in:
2026-06-25 11:07:17 -04:00
parent 7dd26ea636
commit 92cf23dbb3
6 changed files with 216 additions and 14 deletions
+22 -1
View File
@@ -1,4 +1,4 @@
import { apiGet, apiPost } from './client';
import { apiGet, apiPost, apiPatch } from './client';
import type { AIStatus, TestModelRequest, TestModelResponse } from '@/types/ai.types';
export async function fetchAIStatus(): Promise<AIStatus> {
@@ -8,3 +8,24 @@ export async function fetchAIStatus(): Promise<AIStatus> {
export async function testModel(body: TestModelRequest): Promise<TestModelResponse> {
return apiPost<TestModelResponse>('/admin/api/ai/test', body);
}
export interface UpdateAIConfigRequest {
provider?: string;
model?: string;
reset?: boolean;
}
export interface UpdateAIConfigResponse {
provider: string;
model: string;
is_overridden: boolean;
message: string;
}
export async function updateAIConfig(body: UpdateAIConfigRequest): Promise<UpdateAIConfigResponse> {
return apiPatch<UpdateAIConfigResponse>('/admin/api/ai/config', body);
}
export async function fetchRuntimeConfig(): Promise<UpdateAIConfigResponse> {
return apiGet<UpdateAIConfigResponse>('/admin/api/ai/config');
}
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchAIStatus, testModel } from '@/api/ai';
import { fetchAIStatus, testModel, updateAIConfig } from '@/api/ai';
import type { TestModelResponse } from '@/types/ai.types';
import { Bot, Zap, AlertTriangle, CheckCircle, Loader2, RefreshCw, Cpu, ChevronDown } from 'lucide-react';
import { clsx } from 'clsx';
@@ -18,6 +18,8 @@ export function AIConfigSection() {
const [testResult, setTestResult] = useState<TestModelResponse | null>(null);
const [isTesting, setIsTesting] = useState(false);
const [testError, setTestError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState<string | null>(null);
// Sync dropdowns with current config when data loads
useEffect(() => {
@@ -64,6 +66,26 @@ export function AIConfigSection() {
}
}, [selectedProvider, selectedModel]);
const handleSetActive = useCallback(async () => {
if (!selectedProvider || !selectedModel) return;
setIsSaving(true);
setSaveMessage(null);
try {
const result = await updateAIConfig({
provider: selectedProvider,
model: selectedModel,
});
setSaveMessage(result.message);
// Auto-clear message after 4s
setTimeout(() => setSaveMessage(null), 4000);
} catch (err: any) {
setSaveMessage(err?.message || 'Erreur lors de la mise à jour');
} finally {
setIsSaving(false);
}
}, [selectedProvider, selectedModel]);
if (isLoading) {
return (
<section className="card mb-6">
@@ -240,6 +262,30 @@ export function AIConfigSection() {
)}
</button>
<button
onClick={handleSetActive}
disabled={isSaving || !selectedProvider || !selectedModel}
className="btn-secondary flex items-center gap-2 text-sm disabled:opacity-50 ml-3"
>
{isSaving ? (
<>
<Loader2 size={14} className="animate-spin" />
Application...
</>
) : (
<>
<CheckCircle size={14} />
Définir comme actif
</>
)}
</button>
{saveMessage && (
<span className="ml-3 text-sm text-status-ok animate-fade-in">
{saveMessage}
</span>
)}
{/* Test result — live, no refresh needed */}
{(testResult || testError || isTesting) && (
<div className={clsx(