Initial MVP skeleton with auth, chat persistence, UI and text LLM integration

This commit is contained in:
2026-03-10 16:58:02 +00:00
commit 105b8b3db4
40 changed files with 1984 additions and 0 deletions

268
frontend/app/login/page.tsx Normal file
View File

@@ -0,0 +1,268 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:18000";
export default function LoginPage() {
const [login, setLogin] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setIsSubmitting(true);
try {
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ login, password }),
});
if (!res.ok) {
let message = "Неверный логин или пароль";
try {
const data = await res.json();
if (data?.detail && typeof data.detail === "string") {
message = data.detail;
}
} catch {
// ignore malformed/non-json body
}
throw new Error(message);
}
router.push("/");
router.refresh();
} catch (err) {
setError(err?.message || "Ошибка входа");
} finally {
setIsSubmitting(false);
}
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
minHeight: "100vh",
backgroundColor: "var(--bg-color)",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 24,
left: 32,
display: "flex",
alignItems: "center",
gap: 8,
fontSize: 20,
fontWeight: 600,
color: "var(--text-main)",
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2ZM12 4C16.418 4 20 7.582 20 12C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"
fill="var(--primary)"
/>
<path
d="M15 13H9C8.448 13 8 12.552 8 12C8 11.448 8.448 11 9 11H15C15.552 11 16 11.448 16 12C16 12.552 15.552 13 15 13Z"
fill="var(--primary)"
/>
</svg>
AI Chat MVP
</div>
<div
style={{
background: "#fff",
padding: "40px",
borderRadius: "16px",
boxShadow: "0 4px 20px rgba(0,0,0,0.05)",
width: "100%",
maxWidth: "420px",
textAlign: "center",
}}
>
<h1
style={{
fontSize: "24px",
fontWeight: 500,
marginBottom: "32px",
color: "var(--text-main)",
}}
>
Вход в систему
</h1>
<form
onSubmit={handleSubmit}
style={{
display: "flex",
flexDirection: "column",
gap: "20px",
textAlign: "left",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "14px",
marginBottom: "8px",
color: "var(--text-main)",
}}
>
Логин
</label>
<input
value={login}
onChange={(e) => setLogin(e.target.value)}
type="text"
placeholder="admin"
required
style={{
width: "100%",
padding: "12px 16px",
borderRadius: "8px",
border: "1px solid var(--border-color)",
fontSize: "15px",
outline: "none",
transition: "border-color 0.2s",
}}
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
onBlur={(e) =>
(e.target.style.borderColor = "var(--border-color)")
}
/>
</div>
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "8px",
}}
>
<label style={{ fontSize: "14px", color: "var(--text-main)" }}>
Пароль
</label>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
style={{
background: "none",
border: "none",
fontSize: "13px",
color: "var(--text-muted)",
display: "flex",
alignItems: "center",
gap: "4px",
padding: 0,
cursor: "pointer",
}}
>
{showPassword ? "Скрыть" : "Показать"}
</button>
</div>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? "text" : "password"}
placeholder="••••••••"
required
style={{
width: "100%",
padding: "12px 16px",
borderRadius: "8px",
border: "1px solid var(--border-color)",
fontSize: "15px",
outline: "none",
transition: "border-color 0.2s",
}}
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
onBlur={(e) =>
(e.target.style.borderColor = "var(--border-color)")
}
/>
</div>
{error ? (
<div
style={{
color: "#d32f2f",
fontSize: "14px",
textAlign: "center",
}}
>
{error}
</div>
) : null}
<button
type="submit"
disabled={isSubmitting}
style={{
padding: "14px",
marginTop: "8px",
backgroundColor: isSubmitting
? "var(--border-color)"
: "var(--primary)",
color: "#fff",
border: "none",
borderRadius: "8px",
fontSize: "15px",
fontWeight: 500,
transition: "background-color 0.2s",
cursor: isSubmitting ? "not-allowed" : "pointer",
}}
onMouseOver={(e) => {
if (!isSubmitting) {
e.currentTarget.style.backgroundColor = "var(--primary-hover)";
}
}}
onMouseOut={(e) => {
if (!isSubmitting) {
e.currentTarget.style.backgroundColor = "var(--primary)";
}
}}
>
{isSubmitting ? "Вход..." : "Войти"}
</button>
</form>
<div
style={{
marginTop: "24px",
fontSize: "14px",
color: "var(--text-muted)",
}}
>
Вход доступен только для пользователей, созданных администратором.
</div>
</div>
</div>
);
}