This commit is contained in:
Mikan
2026-06-19 16:31:45 +03:00
parent d0d1f003ae
commit 5a78def096
21 changed files with 1250 additions and 548 deletions

View File

@@ -6,7 +6,7 @@ import type { LlmLog, SettingsOut } from "@/types";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Card, CardBody, CardHeader } from "@/components/ui/Card";
import { Save, ArrowLeft, Activity, Users, Zap } from "lucide-react";
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2 } from "lucide-react";
export function AdminPanelPage() {
const { t } = useTranslation();
@@ -22,6 +22,7 @@ export function AdminPanelPage() {
const [error, setError] = useState("");
const [embeddingTest, setEmbeddingTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingEmbeddings, setTestingEmbeddings] = useState(false);
const [userActionError, setUserActionError] = useState("");
useEffect(() => {
(async () => {
@@ -110,6 +111,16 @@ export function AdminPanelPage() {
}
};
const toggleUserActive = async (userId: string, currentActive: boolean) => {
setUserActionError("");
try {
const updated = await adminApi.setUserActive(userId, !currentActive);
setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, is_active: updated.is_active } : u)));
} catch (err: any) {
setUserActionError(err.response?.data?.detail || t("errors.unknown"));
}
};
if (loading) {
return <div className="p-8 text-center text-ink-400">{t("common.loading")}</div>;
}
@@ -302,23 +313,17 @@ export function AdminPanelPage() {
</Card>
<Card>
<CardHeader title={t("admin.trigger_settings")} />
<CardHeader title={t("admin.trigger_settings")} subtitle={t("admin.trigger_settings_desc")} />
<CardBody>
<div className="grid grid-cols-2 gap-3 items-end">
<label className="flex items-center gap-2 text-xs text-ink-300">
<input
type="checkbox"
checked={!!values["triggers.enabled"]}
onChange={(e) => setValues({ ...values, "triggers.enabled": e.target.checked })}
/>
{t("admin.triggers_enabled")}
</label>
<NumberInput
label={t("admin.triggers_check_interval")}
value={values["triggers.check_interval"]}
onChange={(v) => setValues({ ...values, "triggers.check_interval": v })}
<label className="flex items-center gap-2 text-xs text-ink-300">
<input
type="checkbox"
checked={!!values["triggers.enabled"]}
onChange={(e) => setValues({ ...values, "triggers.enabled": e.target.checked })}
/>
</div>
{t("admin.triggers_enabled")}
</label>
<p className="text-xs text-ink-500 mt-2">{t("admin.triggers_enabled_desc")}</p>
</CardBody>
</Card>
@@ -382,6 +387,9 @@ export function AdminPanelPage() {
<Card>
<CardHeader title={t("admin.users")} subtitle={`${users.length} users`} />
<CardBody>
{userActionError && (
<p className="text-sm text-red-400 mb-3">{userActionError}</p>
)}
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
@@ -390,7 +398,8 @@ export function AdminPanelPage() {
<th className="py-2 pr-3">Username</th>
<th className="py-2 pr-3">Role</th>
<th className="py-2 pr-3">Active</th>
<th className="py-2">Created</th>
<th className="py-2 pr-3">Created</th>
<th className="py-2 text-right">{t("admin.users_actions")}</th>
</tr>
</thead>
<tbody>
@@ -412,9 +421,32 @@ export function AdminPanelPage() {
<span className="text-red-400"></span>
)}
</td>
<td className="py-2 text-ink-400">
<td className="py-2 pr-3 text-ink-400">
{new Date(u.created_at).toLocaleDateString()}
</td>
<td className="py-2 text-right">
{u.is_admin ? (
<span className="text-ink-500 text-xs"></span>
) : u.is_active ? (
<Button
size="sm"
variant="ghost"
onClick={() => toggleUserActive(u.id, u.is_active)}
>
<Ban size={12} className="mr-1" />
{t("admin.users_ban")}
</Button>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => toggleUserActive(u.id, u.is_active)}
>
<CheckCircle2 size={12} className="mr-1" />
{t("admin.users_unban")}
</Button>
)}
</td>
</tr>
))}
</tbody>

View File

@@ -55,12 +55,6 @@ export function HomePage() {
desc="RU / EN"
/>
</div>
<div className="mt-12 text-center text-sm text-ink-500">
<Link to="/admin/setup" className="hover:text-accent-400 underline">
{t("auth.admin_setup_title")}
</Link>
</div>
</div>
);
}

View File

@@ -11,7 +11,7 @@ export function LoginPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { setAuth } = useAuthStore();
const [email, setEmail] = useState("");
const [login, setLogin] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
@@ -21,7 +21,8 @@ export function LoginPage() {
setError("");
setLoading(true);
try {
const { access_token, user } = await authApi.login(email, password);
// `login` accepts either email or username.
const { access_token, user } = await authApi.login(login, password);
setAuth(access_token, user);
navigate("/dashboard");
} catch (err: any) {
@@ -38,13 +39,14 @@ export function LoginPage() {
<CardBody>
<form onSubmit={onSubmit} className="space-y-4">
<Input
label={t("auth.email")}
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
label={t("auth.login_or_email")}
type="text"
name="login"
value={login}
onChange={(e) => setLogin(e.target.value)}
required
autoComplete="email"
autoComplete="username"
placeholder="alice / alice@example.com"
/>
<Input
label={t("auth.password")}

View File

@@ -24,7 +24,7 @@ export function WorldBuilderPage() {
const fromScratch = location.state?.fromScratch as boolean | undefined;
const [worldName, setWorldName] = useState("");
const [language, setLanguage] = useState(i18n.language === "en" ? "en" : "ru");
const [language, setLanguage] = useState(i18n.language === "ru" ? "ru" : "en");
const [settingBrief, setSettingBrief] = useState("");
const [characterBrief, setCharacterBrief] = useState("");
const [rulesBrief, setRulesBrief] = useState("");