mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-13 10:17:33 +02:00
feat: Add new settings page with Apple-style UI, OpenRouter AI integration, and Mary v4 analysis.
This commit is contained in:
1 parent
f352594b19
commit
c95eb0ecb6
7 files changed
+221
-48
No files matched your search
@@ -12,25 +12,42 @@ export class OpenRouterClient {
|
||||
constructor(private config: OpenRouterConfig) { }
|
||||
|
||||
async analyzeProduct(p: ProductAnalysisInput): Promise<AnalysisResult> {
|
||||
const response = await fetch(OPENROUTER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://collectflow.app",
|
||||
"X-Title": "CollectFlow AI Copilot",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.config.model,
|
||||
messages: [
|
||||
{ role: "system", content: AnalysisEngine.generateSystemPrompt() },
|
||||
{ role: "user", content: AnalysisEngine.generateUserMessage(p) },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
max_tokens: 200,
|
||||
temperature: 0.1,
|
||||
}),
|
||||
});
|
||||
// Timeout de 25 secondes : évite le blocage indéfini si OpenRouter est lent.
|
||||
// On préfère un échec propre avec message d'erreur plutôt qu'un timeout navigateur.
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 25_000);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(OPENROUTER_URL, {
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://collectflow.app",
|
||||
"X-Title": "CollectFlow AI Copilot",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.config.model,
|
||||
messages: [
|
||||
{ role: "system", content: AnalysisEngine.generateSystemPrompt() },
|
||||
{ role: "user", content: AnalysisEngine.generateUserMessage(p) },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
max_tokens: 200,
|
||||
temperature: 0.1,
|
||||
}),
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
throw new Error("timeout"); // Géré proprement plus haut
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
|
||||
if (response.status === 429) {
|
||||
throw new Error("rate_limited");
|
||||
|
||||
@@ -192,7 +192,8 @@ export function BulkAiAnalyzer() {
|
||||
|
||||
try {
|
||||
let res: Response | null = null;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Jusqu'à 3 tentatives par produit
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
if (isCancelledRef.current) break;
|
||||
try {
|
||||
res = await fetch("/api/ai/analyze", {
|
||||
@@ -200,17 +201,30 @@ export function BulkAiAnalyzer() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (res.ok) break;
|
||||
if (res.ok) break; // Succès → sortie de boucle
|
||||
|
||||
if (res.status === 429) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000 * (i + 1)));
|
||||
// Rate-limit : on attend avant de réessayer
|
||||
const retryData = await res.json().catch(() => ({}));
|
||||
const wait = (retryData.retryAfter ?? 30) * 1000;
|
||||
console.warn(`[BulkAI] Rate limited, retry in ${wait / 1000}s`);
|
||||
await new Promise((r) => setTimeout(r, Math.min(wait, 15_000)));
|
||||
} else if (res.status === 504) {
|
||||
// Timeout serveur : retry rapide (le modèle était peut-être surchargé)
|
||||
console.warn(`[BulkAI] Timeout (504) for ${payload.codein}, retry #${attempt + 1}`);
|
||||
await new Promise((r) => setTimeout(r, 2_000));
|
||||
} else {
|
||||
// Autre erreur (500, etc.) : ne pas réessayer inutilement
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Retry ${i} failed for ${payload.codein}`, e);
|
||||
} catch (fetchErr) {
|
||||
console.error(`[BulkAI] Fetch error (attempt ${attempt + 1}) for ${payload.codein}`, fetchErr);
|
||||
await new Promise((r) => setTimeout(r, 1_000)); // Petite pause avant retry réseau
|
||||
}
|
||||
}
|
||||
|
||||
if (!res || !res.ok) {
|
||||
setError(payload.codein, "Erreur API");
|
||||
setError(payload.codein, res?.status === 504 ? "Timeout IA" : "Erreur API");
|
||||
errorsCount++;
|
||||
} else {
|
||||
const data = await res.json();
|
||||
@@ -220,7 +234,7 @@ export function BulkAiAnalyzer() {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error processing ${payload.codein}`, err);
|
||||
console.error(`[BulkAI] Error processing ${payload.codein}`, err);
|
||||
setError(payload.codein, "Erreur");
|
||||
errorsCount++;
|
||||
} finally {
|
||||
@@ -284,7 +298,7 @@ export function BulkAiAnalyzer() {
|
||||
) : (
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
className="btn-action btn-action-ai"
|
||||
className="apple-btn-ai"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Analyse IA
|
||||
|
||||
Reference in new issue
Block a user