AUDIT / src /components /BackendSetupModal.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
13.8 kB
import { memo, useState, useRef, useEffect } from "react";
import { getLocalBackendStatus } from "@/lib/providerChain";
import { Z_INDEX } from "@/lib/zindex";
interface Props {
onClose: () => void;
onAvailable: () => void;
}
const STEPS = [
{
n: 1,
title: "Installa Ollama",
desc: "Scarica e installa il runtime AI locale (gratuito, open source)",
cmd: null as string | null,
link: { label: "Scarica Ollama →", url: "https://ollama.com/download" },
hint: "Disponibile per macOS, Linux, Windows",
},
{
n: 2,
title: "Avvia Ollama",
desc: "Apri il Terminale ed esegui",
cmd: "ollama serve",
link: null,
hint: "Lascia il terminale aperto",
},
{
n: 3,
title: "Scarica un modello AI",
desc: "Scegli un modello (la prima volta scarica il file)",
cmd: "ollama pull qwen2.5-coder:7b",
link: null,
hint: "~4GB — oppure phi4-mini (~2GB) per Mac con poca RAM",
},
{
n: 4,
title: "Avvia il backend",
desc: "Nella cartella backend/ del progetto esegui",
cmd: "bash start.sh",
link: null,
hint: "Installa dipendenze Python e avvia il server locale su porta 8000",
},
];
const BackendSetupModal = memo(function BackendSetupModal({ onClose, onAvailable }: Props) {
const [currentStep, setCurrentStep] = useState(0);
const [checking, setChecking] = useState(false);
const [detected, setDetected] = useState(false);
const [copied, setCopied] = useState<number | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
// [S47-bugfix] Stable ref for onAvailable — avoids useEffect re-running every render
// (inline arrow functions from parent create new references each render)
const onAvailableRef = useRef(onAvailable);
onAvailableRef.current = onAvailable;
// Poll for backend while modal is open — empty deps, stable via ref
useEffect(() => {
let mounted = true;
pollRef.current = setInterval(async () => {
try {
const s = await getLocalBackendStatus();
if (!mounted) return;
if (s.available) {
setDetected(true);
clearInterval(pollRef.current!);
setTimeout(() => onAvailableRef.current(), 1500);
}
} catch {
/* non-fatal — riprova al prossimo tick */
}
}, 4000);
return () => { mounted = false; clearInterval(pollRef.current!); };
}, []); // ← stable: onAvailable always current via ref
const checkNow = async () => {
setChecking(true);
const s = await getLocalBackendStatus();
setChecking(false);
if (s.available) {
setDetected(true);
setTimeout(() => onAvailableRef.current(), 1200);
}
};
const copyCmd = (cmd: string, idx: number) => {
navigator.clipboard.writeText(cmd).catch(() => {});
setCopied(idx);
setTimeout(() => setCopied(null), 2000);
};
if (detected) {
return (
<div data-modal style={{
position: "fixed", inset: 0, zIndex: Z_INDEX.CRITICAL_MODAL_OVERLAY,
display: "flex", alignItems: "center", justifyContent: "center",
background: "rgba(0,0,0,0.7)",
paddingTop: "var(--safe-top)", paddingBottom: "var(--safe-bottom)",
}}>
<div style={{
background: "rgba(8,8,22,0.96)",
border: "1px solid rgba(59,130,246,0.4)",
borderRadius: 20, padding: "40px 36px",
textAlign: "center", maxWidth: 380,
boxShadow: "0 0 40px rgba(59,130,246,0.2)",
}}>
<div style={{ width: 56, height: 56, borderRadius: "50%", background: "rgba(59,130,246,0.15)", border: "2px solid rgba(59,130,246,0.5)", display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 16px" }}>
<svg width={28} height={28} viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<h2 style={{ color: "#3b82f6", fontSize: "1.2rem", fontWeight: 700, marginBottom: 8 }}>
Backend locale rilevato!
</h2>
<p style={{ color: "rgba(255,255,255,0.6)", fontSize: "0.88rem" }}>
L'AI locale è attiva. Le prossime chat useranno Ollama invece del cloud.
</p>
</div>
</div>
);
}
return (
<div
data-modal
style={{
position: "fixed", inset: 0, zIndex: Z_INDEX.CRITICAL_MODAL_OVERLAY,
display: "flex", alignItems: "center", justifyContent: "center",
background: "rgba(0,0,0,0.75)",
paddingTop: "var(--safe-top)", paddingBottom: "var(--safe-bottom)",
padding: "calc(var(--safe-top) + 16px) 16px calc(var(--safe-bottom) + 16px)",
}}
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
>
<div style={{
background: "rgba(8,8,22,0.96)",
border: "1px solid rgba(59,130,246,0.25)",
borderRadius: 20,
width: "100%", maxWidth: 480,
maxHeight: "calc(100dvh - var(--safe-top) - var(--safe-bottom) - 32px)",
overflow: "auto",
boxShadow: "0 24px 80px rgba(0,0,0,0.8)",
}}>
{/* Header */}
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "20px 24px 16px",
borderBottom: "1px solid rgba(255,255,255,0.06)",
}}>
<div>
<h2 style={{ fontSize: "1.05rem", fontWeight: 700, color: "#fff" }}>
Attiva AI locale
</h2>
<p style={{ fontSize: "0.78rem", color: "rgba(255,255,255,0.45)", marginTop: 3 }}>
Ollama · privato · gratuito · nessun cloud
</p>
</div>
<button
onClick={onClose}
style={{
background: "rgba(255,255,255,0.07)", border: "none",
color: "rgba(255,255,255,0.6)", cursor: "pointer",
width: 32, height: 32, borderRadius: 8, fontSize: 16,
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0,
}}
></button>
</div>
{/* Steps */}
<div style={{ padding: "16px 24px" }}>
<p style={{ fontSize: "0.82rem", color: "rgba(255,255,255,0.5)", marginBottom: 16 }}>
Segui questi passaggi una sola volta — poi l'app lo rileva automaticamente.
</p>
{/* Step tabs */}
<div style={{ display: "flex", gap: 6, marginBottom: 20 }}>
{STEPS.map((s, i) => (
<button
key={i}
onClick={() => setCurrentStep(i)}
style={{
flex: 1, height: 4, borderRadius: 6, border: "none", cursor: "pointer",
background: i <= currentStep
? "linear-gradient(90deg, #3b82f6, #8b5cf6)"
: "rgba(255,255,255,0.1)",
transition: "all 0.2s",
}}
/>
))}
</div>
{/* Current step */}
{(() => {
const step = STEPS[currentStep];
return (
<div style={{
background: "rgba(59,130,246,0.06)",
border: "1px solid rgba(59,130,246,0.15)",
borderRadius: 14, padding: "18px 20px",
minHeight: 180,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
<div style={{
width: 28, height: 28, borderRadius: "50%",
background: "linear-gradient(135deg, #3b82f6, #8b5cf6)",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "0.78rem", fontWeight: 700, color: "#fff", flexShrink: 0,
}}>
{step.n}
</div>
<span style={{ fontWeight: 600, color: "#fff", fontSize: "0.95rem" }}>
{step.title}
</span>
</div>
<p style={{ color: "rgba(255,255,255,0.65)", fontSize: "0.85rem", marginBottom: 14 }}>
{step.desc}
</p>
{step.cmd && (
<div style={{
background: "#080814",
border: "1px solid rgba(59,130,246,0.2)",
borderRadius: 10, padding: "10px 14px",
display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 10, marginBottom: 10,
}}>
<code style={{ color: "#93c5fd", fontSize: "0.85rem", fontFamily: "ui-monospace,monospace", wordBreak: "break-all" }}>
{step.cmd}
</code>
<button
onClick={() => copyCmd(step.cmd!, currentStep)}
style={{
background: copied === currentStep ? "rgba(59,130,246,0.2)" : "rgba(59,130,246,0.2)",
border: "none", color: copied === currentStep ? "#3b82f6" : "#93c5fd",
cursor: "pointer", padding: "5px 10px", borderRadius: 8,
fontSize: "0.75rem", fontWeight: 600, flexShrink: 0, transition: "all 0.2s",
whiteSpace: "nowrap",
}}
>
{copied === currentStep ? "Copiato" : "Copia"}
</button>
</div>
)}
{step.link && (
<a
href={step.link.url}
target="_blank"
rel="noopener noreferrer"
style={{
display: "inline-flex", alignItems: "center", gap: 6,
background: "linear-gradient(135deg, #3b82f6, #8b5cf6)",
color: "#fff", textDecoration: "none",
padding: "8px 16px", borderRadius: 8,
fontSize: "0.85rem", fontWeight: 600,
boxShadow: "0 4px 16px rgba(59,130,246,0.35)",
}}
>
{step.link.label}
</a>
)}
<p style={{ color: "rgba(255,255,255,0.35)", fontSize: "0.76rem", marginTop: 10 }}>
{step.hint}
</p>
</div>
);
})()}
{/* Navigation */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 16 }}>
<button
onClick={() => setCurrentStep(s => Math.max(0, s - 1))}
disabled={currentStep === 0}
style={{
background: "rgba(255,255,255,0.07)", border: "1px solid rgba(255,255,255,0.1)",
color: currentStep === 0 ? "rgba(255,255,255,0.25)" : "rgba(255,255,255,0.7)",
cursor: currentStep === 0 ? "not-allowed" : "pointer",
padding: "8px 16px", borderRadius: 8, fontSize: "0.83rem", fontWeight: 500,
}}
>← Indietro</button>
<div style={{ display: "flex", gap: 8 }}>
<button
onClick={checkNow}
disabled={checking}
style={{
background: checking ? "rgba(59,130,246,0.1)" : "rgba(59,130,246,0.15)",
border: "1px solid rgba(59,130,246,0.3)",
color: "#3b82f6", cursor: checking ? "wait" : "pointer",
padding: "8px 14px", borderRadius: 8, fontSize: "0.83rem", fontWeight: 600,
transition: "all 0.2s",
}}
>
{checking ? "Verifica…" : "Verifica ora"}
</button>
{currentStep < STEPS.length - 1 && (
<button
onClick={() => setCurrentStep(s => Math.min(STEPS.length - 1, s + 1))}
style={{
background: "linear-gradient(135deg, #3b82f6, #8b5cf6)",
border: "none", color: "#fff", cursor: "pointer",
padding: "8px 16px", borderRadius: 8, fontSize: "0.83rem", fontWeight: 600,
boxShadow: "0 4px 12px rgba(59,130,246,0.3)",
}}
>Prossimo →</button>
)}
</div>
</div>
</div>
{/* Auto-detect notice */}
<div style={{
padding: "12px 24px 20px",
borderTop: "1px solid rgba(255,255,255,0.05)",
display: "flex", alignItems: "center", gap: 8,
}}>
<div style={{
width: 7, height: 7, borderRadius: "50%",
background: "#f59e0b",
animation: "pulse-dot 1.5s ease-in-out infinite",
}} />
<span style={{ fontSize: "0.75rem", color: "rgba(255,255,255,0.35)" }}>
Rilevamento automatico ogni 4 secondi — la finestra si chiuderà da sola
</span>
</div>
</div>
</div>
);
}
);
export default BackendSetupModal;