kioai / artifacts /image-gen /src /components /SettingsDialog.tsx
kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { useState } from "react";
import { Settings, CheckCircle, XCircle, Loader2, Eye, EyeOff, ExternalLink, Trash2, RefreshCw } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
import {
useGetConfigToken,
useSetConfigToken,
useDeleteConfigToken,
getGetConfigTokenQueryKey,
} from "@workspace/api-client-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { useLang } from "@/contexts/LanguageContext";
export function SettingsButton() {
const [open, setOpen] = useState(false);
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [showToken, setShowToken] = useState(false);
const queryClient = useQueryClient();
const { toast } = useToast();
const { t } = useLang();
const { data: tokenStatus, isLoading } = useGetConfigToken({
query: { queryKey: getGetConfigTokenQueryKey() },
});
const { mutate: saveToken, isPending: isSaving } = useSetConfigToken();
const { mutate: removeToken, isPending: isRemoving } = useDeleteConfigToken();
const handleSave = () => {
if (!accessToken.trim()) return;
saveToken(
{ data: { token: accessToken.trim(), refreshToken: refreshToken.trim() || undefined } as any },
{
onSuccess: () => {
toast({ title: t.settingsSavedTitle, description: t.settingsSavedDesc });
setAccessToken("");
setRefreshToken("");
queryClient.invalidateQueries({ queryKey: getGetConfigTokenQueryKey() });
},
onError: () => {
toast({ variant: "destructive", title: t.settingsSaveError, description: t.settingsSaveErrorDesc });
},
}
);
};
const handleRemove = () => {
removeToken(undefined, {
onSuccess: () => {
toast({ title: t.settingsRemovedTitle });
queryClient.invalidateQueries({ queryKey: getGetConfigTokenQueryKey() });
},
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="relative"
title={t.navSettings}
>
<Settings className="w-4 h-4" />
{tokenStatus && (
<span className={`absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full ${tokenStatus.configured ? "bg-emerald-400" : "bg-red-400"}`} />
)}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg bg-[#0e0a1a] border-violet-500/30">
<DialogHeader>
<DialogTitle className="text-violet-200">{t.settingsTitle}</DialogTitle>
</DialogHeader>
<div className="space-y-5 pt-2">
{/* Status */}
<div className="flex items-center gap-3 p-3 rounded-lg bg-black/30 border border-violet-500/20">
{isLoading ? (
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
) : tokenStatus?.configured ? (
<>
<CheckCircle className="w-4 h-4 text-emerald-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">{t.settingsStatus}</p>
<p className="text-xs text-muted-foreground font-mono truncate">{tokenStatus.token}</p>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-red-400 hover:text-red-300 hover:bg-red-400/10"
onClick={handleRemove}
disabled={isRemoving}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</>
) : (
<>
<XCircle className="w-4 h-4 text-red-400 shrink-0" />
<p className="text-sm text-red-400">{t.settingsNoToken}</p>
</>
)}
</div>
{/* Token Inputs */}
<div className="space-y-3">
<div className="space-y-1.5">
<Label className="text-violet-300 text-xs">{t.settingsAccessLabel}</Label>
<div className="relative">
<Input
type={showToken ? "text" : "password"}
placeholder={t.settingsAccessPlaceholder}
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
className="pr-10 font-mono text-xs bg-black/30 border-violet-500/30 focus-visible:ring-violet-400"
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowToken(!showToken)}
>
{showToken ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-violet-300 text-xs flex items-center gap-1.5">
<RefreshCw className="w-3 h-3" />
{t.settingsRefreshLabel}
</Label>
<Input
type={showToken ? "text" : "password"}
placeholder={t.settingsRefreshPlaceholder}
value={refreshToken}
onChange={(e) => setRefreshToken(e.target.value)}
className="font-mono text-xs bg-black/30 border-violet-500/30 focus-visible:ring-violet-400"
/>
</div>
<Button
className="w-full"
onClick={handleSave}
disabled={!accessToken.trim() || isSaving}
>
{isSaving ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
{isSaving ? t.settingsSaving : t.settingsSave}
</Button>
</div>
{/* How to get token */}
<div className="rounded-lg border border-violet-500/20 bg-violet-950/20 p-4 space-y-2">
<p className="text-xs font-semibold text-violet-300">{t.settingsHowTitle}</p>
<ol className="text-xs text-muted-foreground space-y-1.5 list-decimal list-inside">
<li>
{t.settingsStep1Pre}{" "}
<a
href="https://geminigen.ai"
target="_blank"
rel="noopener noreferrer"
className="text-violet-400 hover:underline inline-flex items-center gap-0.5"
>
geminigen.ai <ExternalLink className="w-3 h-3" />
</a>{" "}
{t.settingsStep1Post}
</li>
<li>{t.settingsStep2}</li>
<li>{t.settingsStep3}</li>
<li>
{t.settingsStep4Pre}{" "}
<code className="text-violet-300 bg-black/30 px-1 rounded">access_token</code>{" "}
{t.settingsStep4Mid}
</li>
<li>
{t.settingsStep5Pre}{" "}
<code className="text-violet-300 bg-black/30 px-1 rounded">refresh_token</code>{" "}
{t.settingsStep5Mid}
</li>
</ol>
<p className="text-xs text-amber-400/80 mt-2">
{t.settingsHint}
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}