+ {/* Save Button — Floating/Sticky style at bottom */}
+
diff --git a/src/app/api/ai/analyze/route.ts b/src/app/api/ai/analyze/route.ts
index d2f1507..cb1557d 100644
--- a/src/app/api/ai/analyze/route.ts
+++ b/src/app/api/ai/analyze/route.ts
@@ -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 });
}
diff --git a/src/app/globals.css b/src/app/globals.css
index a265178..fe8d23b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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;
diff --git a/src/components/shared/confirm-modal.tsx b/src/components/shared/confirm-modal.tsx
index c5d54b0..7fbafd8 100644
--- a/src/components/shared/confirm-modal.tsx
+++ b/src/components/shared/confirm-modal.tsx
@@ -115,8 +115,7 @@ export function ConfirmModal({