kioai / artifacts /image-gen /src /components /SetupWizard.tsx
kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Shield, CheckCircle, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { useAuth } from "@/contexts/AuthContext";
import { useLang } from "@/contexts/LanguageContext";
import { useToast } from "@/hooks/use-toast";
const BASE = import.meta.env.BASE_URL.replace(/\/$/, "");
const API_BASE = `${BASE}/api`;
const BASE_URL = import.meta.env.BASE_URL.replace(/\/$/, "");
async function fetchSetupStatus(): Promise<{ refreshTokenConfigured: boolean }> {
const r = await fetch(`${API_BASE}/admin/setup-status`, { credentials: "include" });
if (!r.ok) return { refreshTokenConfigured: true };
return r.json();
}
export function SetupWizard() {
const { isAdmin, isLoaded } = useAuth();
const { t } = useLang();
const { toast } = useToast();
const qc = useQueryClient();
const [token, setToken] = useState("");
const [saving, setSaving] = useState(false);
const [stepsOpen, setStepsOpen] = useState(true);
const { data, isLoading } = useQuery({
queryKey: ["setupStatus"],
queryFn: fetchSetupStatus,
enabled: isLoaded && isAdmin,
staleTime: 0,
});
if (!isLoaded || !isAdmin || isLoading || data?.refreshTokenConfigured) return null;
const handleSave = async () => {
if (!token.trim()) return;
setSaving(true);
try {
const r = await fetch(`${API_BASE}/admin/setup`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: token.trim() }),
});
const data = await r.json();
if (!r.ok) throw new Error(data.error || "Failed");
await qc.invalidateQueries({ queryKey: ["setupStatus"] });
toast({ description: t.setupSuccess });
} catch (e: any) {
toast({ variant: "destructive", description: e.message || t.setupError });
}
setSaving(false);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="w-full max-w-xl bg-card border border-border/80 rounded-2xl shadow-2xl overflow-hidden">
<div className="bg-gradient-to-r from-primary/20 to-accent/10 border-b border-border/50 px-6 py-5 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary/20 border border-primary/30 flex items-center justify-center">
<Shield className="w-5 h-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-bold text-foreground">{t.setupTitle}</h2>
<p className="text-xs text-muted-foreground">{t.setupSubtitle}</p>
</div>
</div>
<div className="px-6 py-5 space-y-5 max-h-[75vh] overflow-y-auto">
<div className="rounded-xl border border-border/50 overflow-hidden">
<button
onClick={() => setStepsOpen(!stepsOpen)}
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-foreground hover:bg-secondary/50 transition-colors"
>
<span>📖 {t.adminGuideTitle}</span>
{stepsOpen ? <ChevronUp className="w-4 h-4 text-muted-foreground" /> : <ChevronDown className="w-4 h-4 text-muted-foreground" />}
</button>
{stepsOpen && (
<div className="border-t border-border/40 px-4 py-4 space-y-4 bg-black/10">
<ol className="space-y-2.5">
{(t.adminGuideSteps as string[]).map((step, i) => (
<li key={i} className="flex gap-3 text-sm text-muted-foreground">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 border border-primary/30 text-primary text-xs flex items-center justify-center font-bold mt-0.5">
{i + 1}
</span>
<span dangerouslySetInnerHTML={{ __html: step }} />
</li>
))}
</ol>
<div className="rounded-lg overflow-hidden border border-border/40">
<img
src={`${BASE_URL}/token-guide.png`}
alt="Token guide screenshot"
className="w-full h-auto"
/>
</div>
</div>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">{t.setupPasteLabel}</label>
<Textarea
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder={t.setupPastePlaceholder}
className="font-mono text-xs bg-background/50 border-border/60 resize-none h-24"
spellCheck={false}
/>
<p className="text-xs text-muted-foreground">{t.setupPasteHint}</p>
</div>
</div>
<div className="border-t border-border/50 px-6 py-4 flex justify-end gap-3 bg-black/10">
<Button
onClick={handleSave}
disabled={!token.trim() || saving}
className="gap-2 min-w-[120px]"
>
{saving ? (
<><Loader2 className="w-4 h-4 animate-spin" />{t.setupSaving}</>
) : (
<><CheckCircle className="w-4 h-4" />{t.setupConfirm}</>
)}
</Button>
</div>
</div>
</div>
);
}