feat: Add new settings page with Apple-style UI, OpenRouter AI integration, and Mary v4 analysis.

This commit is contained in:
Michael committed 2026-02-27 10:02:23 +01:00
1 parent f352594b19
commit c95eb0ecb6
7 files changed
+221 -48

No files matched your search

+24 -12
View File
@@ -214,7 +214,7 @@ export default function SettingsPage() {
<label htmlFor="ssl" className="text-[12px] cursor-pointer text-[var(--text-secondary)]">Connexion SSL</label>
</div>
<div className="flex items-center gap-3">
<button onClick={testDb} disabled={dbStatus === "testing"} className="apple-btn-secondary">
<button onClick={testDb} disabled={dbStatus === "testing"} className="apple-btn-secondary h-9 px-4">
{dbStatus === "testing" && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
{dbStatus === "ok" && <CheckCircle className="w-3.5 h-3.5 text-teal-500" />}
{dbStatus === "error" && <AlertCircle className="w-3.5 h-3.5 text-red-400" />}
@@ -233,9 +233,9 @@ export default function SettingsPage() {
<div className="pt-2">
<button
onClick={reloadFromServer}
className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-[var(--accent)] text-[var(--text-secondary)]"
className="apple-btn-secondary h-8 px-3 text-[11px] opacity-80 hover:opacity-100"
>
<RotateCcw className="w-3.5 h-3.5" />
<RotateCcw className="w-3 h-3" />
Recharger la configuration enregistrée sur le serveur
</button>
</div>
@@ -263,7 +263,7 @@ export default function SettingsPage() {
<button
onClick={() => fetchModels(apiKey)}
disabled={!apiKey || modelsStatus === "loading"}
className="apple-btn-secondary whitespace-nowrap"
className="apple-btn-secondary h-9 px-4 whitespace-nowrap"
>
{modelsStatus === "loading" ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RefreshCw className="w-3.5 h-3.5" />}
Charger les modèles
@@ -390,9 +390,9 @@ export default function SettingsPage() {
<div className="pt-2">
<button
onClick={resetScoreDefaults}
className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-[var(--accent)] text-[var(--text-secondary)]"
className="apple-btn-secondary h-8 px-3 text-[11px] opacity-80 hover:opacity-100"
>
<RotateCcw className="w-3.5 h-3.5" />
<RotateCcw className="w-3 h-3" />
Rétablir les valeurs par défaut
</button>
</div>
@@ -406,16 +406,28 @@ export default function SettingsPage() {
<UserManagement />
</Section>
{/* Save */}
<div className="flex items-center gap-3">
{/* Save Button — Floating/Sticky style at bottom */}
<div className="sticky bottom-6 flex justify-end pt-4 pb-2">
<button
onClick={handleSave}
disabled={saveStatus === "saving"}
className="apple-btn-primary"
style={{ background: "var(--accent)" }}
className={`apple-btn-primary min-w-[240px] shadow-lg ${saveStatus === "saving" ? "animate-pulse" : saveStatus === "saved" ? "animate-apple-save" : ""}`}
style={{ background: saveStatus === "saved" ? "var(--accent-success)" : "var(--brand-solid)" }}
>
{saveStatus === "saving" ? <Loader2 className="w-4 h-4 animate-spin" /> : saveStatus === "saved" ? <CheckCircle className="w-4 h-4" /> : <Save className="w-4 h-4" />}
{saveStatus === "saving" ? "Enregistrement..." : saveStatus === "saved" ? "Sauvegardé !" : "Sauvegarder la configuration"}
{saveStatus === "saving" ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : saveStatus === "saved" ? (
<CheckCircle className="w-4 h-4" />
) : (
<Save className="w-4 h-4" />
)}
<span className="ml-1">
{saveStatus === "saving"
? "Enregistrement en cours..."
: saveStatus === "saved"
? "Configuration enregistrée !"
: "Sauvegarder la configuration"}
</span>
</button>
</div>
</div>
+9
View File
@@ -6,6 +6,10 @@ import { db } from "@/db";
import { aiSupplierContext } from "@/db/schema";
import { eq } from "drizzle-orm";
// Limite max acceptable pour la route (Node.js self-hosted).
// Evite que le process tourne indéfiniment en cas de deadlock.
export const maxDuration = 55;
export async function POST(req: NextRequest) {
const config = await getSavedDatabaseConfig();
const apiKey = process.env.OPENROUTER_API_KEY || config?.openRouterKey;
@@ -42,6 +46,11 @@ export async function POST(req: NextRequest) {
if (err instanceof Error && err.message === "rate_limited") {
return NextResponse.json({ error: "rate_limited", retryAfter: 30 }, { status: 429 });
}
// Timeout explicite déclenché par l'AbortController du client
if (err instanceof Error && err.message === "timeout") {
console.warn(`[AI] Timeout for product analysis — OpenRouter too slow.`);
return NextResponse.json({ error: "timeout", retryAfter: 5 }, { status: 504 });
}
const msg = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ error: msg }, { status: 500 });
}
+123
View File
@@ -274,6 +274,129 @@
}
/* ── Apple-style Action Buttons ── */
/* ── Apple-style Action Buttons (Glossy & Premium) ── */
.apple-btn-primary,
.apple-btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 40px;
padding: 0 20px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
}
.apple-btn-primary {
background: linear-gradient(180deg, var(--brand-solid) 0%, var(--brand-hover) 100%);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 4px 12px rgba(16, 185, 129, 0.25),
inset 0 1px 1px rgba(255, 255, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.apple-btn-primary:hover {
filter: brightness(1.1);
transform: translateY(-1px);
box-shadow:
0 6px 16px rgba(16, 185, 129, 0.35),
inset 0 1px 1px rgba(255, 255, 255, 0.3);
}
.apple-btn-primary:active {
transform: scale(0.97) translateY(0);
filter: brightness(0.95);
}
.apple-btn-secondary {
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border-strong);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.apple-btn-secondary:hover {
background: var(--bg-elevated);
border-color: var(--text-muted);
transform: translateY(-1px);
}
.apple-btn-secondary:active {
transform: scale(0.97) translateY(0);
}
.apple-btn-danger {
background: linear-gradient(180deg, var(--accent-error) 0%, #be123c 100%);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 4px 12px rgba(244, 63, 94, 0.25),
inset 0 1px 1px rgba(255, 255, 255, 0.2);
}
.apple-btn-danger:hover {
filter: brightness(1.1);
transform: translateY(-1px);
box-shadow:
0 6px 16px rgba(244, 63, 94, 0.35);
}
.apple-btn-ai {
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 4px 15px rgba(168, 85, 247, 0.3),
inset 0 1px 1px rgba(255, 255, 255, 0.3);
}
.apple-btn-ai:hover {
filter: brightness(1.1);
transform: translateY(-1px) scale(1.02);
box-shadow: 0 6px 20px rgba(168, 85, 247, 0.4);
}
/* Glass effect overlay for loading/success */
.btn-glass-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
}
@keyframes apple-pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
}
70% {
transform: scale(1.02);
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
.animate-apple-save {
animation: apple-pulse 2s infinite ease-in-out;
}
.btn-action {
display: inline-flex;
align-items: center;