Spaces:
Running
Running
File size: 7,193 Bytes
b2c1c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import { useState } from "react";
import { BrainCircuit, FileSearch, Gauge, Github, Sparkles, Wrench } from "lucide-react";
import { api, setTokens } from "../lib/api.js";
export default function AuthPage({ onAuthed, appInfo }) {
const [mode, setMode] = useState("login");
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const [demoBusy, setDemoBusy] = useState(false);
const isDemo = Boolean(appInfo?.demo_mode);
async function startDemo() {
setError("");
setDemoBusy(true);
try {
const response = await fetch("/api/auth/demo", { method: "POST" });
const data = await response.json();
if (!response.ok) {
throw new Error(
typeof data.detail === "string"
? data.detail
: "Could not start the demo. Please try again."
);
}
setTokens(data);
onAuthed(await api("/api/auth/me"));
} catch (err) {
setError(err.message);
} finally {
setDemoBusy(false);
}
}
async function submit(event) {
event.preventDefault();
setError("");
setBusy(true);
try {
const path = mode === "login" ? "/api/auth/login" : "/api/auth/register";
const body =
mode === "login" ? { email, password } : { email, username, password };
const response = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) {
throw new Error(
typeof data.detail === "string"
? data.detail
: "Please check your details and try again."
);
}
setTokens(data);
onAuthed(await api("/api/auth/me"));
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
return (
<div className="auth-page">
<div className="auth-hero">
<div className="brand" style={{ padding: 0 }}>
<div className="brand-logo">
<BrainCircuit size={18} />
</div>
Synapse
</div>
<h1>
Your agentic AI assistant,
<br />
with real superpowers.
</h1>
<p>
Streaming chat backed by an autonomous tool-calling agent, hybrid
retrieval over your documents, and full cost observability.
</p>
<div className="hero-feature">
<div className="bullet"><Wrench size={16} /></div>
<div>
<b>Agentic tool use</b>
<span>Web search, weather, calculator and document retrieval, chosen autonomously.</span>
</div>
</div>
<div className="hero-feature">
<div className="bullet"><FileSearch size={16} /></div>
<div>
<b>Hybrid RAG with citations</b>
<span>BM25 + dense vectors fused with RRF, reranked, and cited inline.</span>
</div>
</div>
<div className="hero-feature">
<div className="bullet"><Gauge size={16} /></div>
<div>
<b>Built-in observability</b>
<span>Token, cost and latency analytics for every conversation.</span>
</div>
</div>
</div>
<div className="auth-form-side">
<form className="auth-card fade-in" onSubmit={submit}>
{isDemo ? (
<>
<h2>Try the live demo</h2>
<p className="sub">
One click, no signup. You get a private sandbox preloaded with a
sample report so document retrieval works immediately.
</p>
{error && <div className="auth-error">{error}</div>}
<button
type="button"
className="btn primary demo-cta"
onClick={startDemo}
disabled={demoBusy}
>
<Sparkles size={17} />
{demoBusy ? "Preparing your sandbox..." : "Launch demo"}
</button>
<ul className="demo-points">
<li>Ask about the weather or search the web, the agent picks its own tools</li>
<li>Ask about the preloaded report to see hybrid RAG with citations</li>
<li>Upload your own PDF, or open Analytics for live token and cost metrics</li>
</ul>
{appInfo?.repo_url && (
<a className="repo-link" href={appInfo.repo_url} target="_blank" rel="noreferrer">
<Github size={15} /> View the source on GitHub
</a>
)}
<div className="auth-divider"><span>or use an account</span></div>
</>
) : (
<>
<h2>{mode === "login" ? "Welcome back" : "Create your account"}</h2>
<p className="sub">
{mode === "login"
? "Sign in to continue your conversations."
: "A minute from now you will be chatting with Synapse."}
</p>
{error && <div className="auth-error">{error}</div>}
</>
)}
{mode === "register" && (
<div className="field">
<label htmlFor="username">Name</label>
<input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="What should we call you?"
required
minLength={2}
/>
</div>
)}
<div className="field">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
<div className="field">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={mode === "register" ? "At least 8 characters" : "Your password"}
required
minLength={8}
/>
</div>
<button className="btn primary" style={{ width: "100%" }} disabled={busy}>
{busy ? "Please wait..." : mode === "login" ? "Sign in" : "Create account"}
</button>
<div className="auth-switch">
{mode === "login" ? "New to Synapse?" : "Already have an account?"}{" "}
<button
type="button"
onClick={() => {
setMode(mode === "login" ? "register" : "login");
setError("");
}}
>
{mode === "login" ? "Create an account" : "Sign in"}
</button>
</div>
</form>
</div>
</div>
);
}
|