import { useRef, useState, useEffect } from "react"; import { supabase } from "./supabaseClient"; import ReactMarkdown from "react-markdown"; const getEnv = (key) => { if (typeof window !== "undefined" && window.__ENV__ && window.__ENV__[key]) { return window.__ENV__[key]; } return import.meta.env[key]; }; const API_BASE_URL = getEnv("VITE_API_BASE_URL") || "/api"; const Panel = ({ title, subtitle, children }) => (
{subtitle}
: null}
{statusText || "Waiting for run..."}
);
export default function App() {
const [view, setView] = useState("login"); // login | signup | otp | app
const [emailDisplay, setEmailDisplay] = useState("");
const [session, setSession] = useState(null);
const [status, setStatus] = useState("");
const [forceRefresh, setForceRefresh] = useState(false);
const [urlValue, setUrlValue] = useState("");
const [jobResult, setJobResult] = useState(null);
const [systemPrompt, setSystemPrompt] = useState("");
const [siteName, setSiteName] = useState("Bot");
const [progressValue, setProgressValue] = useState(0);
const [progressText, setProgressText] = useState("Idle");
const [otpEmail, setOtpEmail] = useState("");
const [firstNameDisplay, setFirstNameDisplay] = useState("");
const [resetStatus, setResetStatus] = useState("");
const [resetEmail, setResetEmail] = useState("");
const [resetSent, setResetSent] = useState(false);
const [resetOtpEntered, setResetOtpEntered] = useState(false);
const [resetOtpValue, setResetOtpValue] = useState("");
const [isRunning, setIsRunning] = useState(false);
const [isAuthLoading, setIsAuthLoading] = useState(false);
const resetEmailRef = useRef(null);
const resetOtpRef = useRef(null);
const resetNewPassRef = useRef(null);
const resetNewPassConfirmRef = useRef(null);
const [signupPassword, setSignupPassword] = useState("");
const [summaryVisible, setSummaryVisible] = useState(false);
const [summaryData, setSummaryData] = useState({ pages: 0, searches: 0 });
const [isSessionChecking, setIsSessionChecking] = useState(true); // [NEW] Loading state for initial session check
// [NEW] Check for existing session on mount
useEffect(() => {
// 1. Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
if (session) {
setSession(session);
setEmailDisplay(session.user?.email || "");
const fn = session.user?.user_metadata?.first_name;
setFirstNameDisplay(fn || (session.user?.email ? session.user.email.split("@")[0] : ""));
setView("app");
setStatus("Restored session.");
}
setIsSessionChecking(false);
});
// 2. Listen for changes (login, logout, auto-refresh)
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
setEmailDisplay(session.user?.email || "");
const fn = session.user?.user_metadata?.first_name;
setFirstNameDisplay(fn || (session.user?.email ? session.user.email.split("@")[0] : ""));
// Only switch to app if we were in a login/signup flow to avoid disrupting user
setView((prev) => (prev === "login" || prev === "signup" || prev === "otp" || prev === "reset" ? "app" : prev));
} else {
// If logged out
setView("login");
setEmailDisplay("");
setFirstNameDisplay("");
}
});
return () => subscription.unsubscribe();
}, []);
// Refs to avoid re-rendering while typing (prevents cursor jump/blur)
const loginEmailRef = useRef(null);
const loginPassRef = useRef(null);
const signupFirstRef = useRef(null);
const signupLastRef = useRef(null);
const signupEmailRef = useRef(null);
const signupPassRef = useRef(null);
const otpEmailRef = useRef(null);
const otpPassRef = useRef(null);
const otpCodeRef = useRef(null);
const urlInputRef = useRef(null);
const handleSignup = async () => {
const email = signupEmailRef.current?.value?.trim() || "";
const password = signupPassRef.current?.value || "";
const first = signupFirstRef.current?.value?.trim() || "";
const last = signupLastRef.current?.value?.trim() || "";
setStatus("Signing up...");
setSignupPassword(password);
const { error } = await supabase.auth.signUp({
email,
password,
options: { data: { first_name: first, last_name: last } },
});
if (error) {
setStatus(`Signup failed: ${error.message}`);
} else {
setFirstNameDisplay(first || "");
setStatus("Signup initiated. Check your email for OTP.");
setOtpEmail(email);
setView("otp");
}
};
const handleVerifyOtp = async () => {
const otp = otpCodeRef.current?.value?.trim() || "";
setStatus("Verifying OTP...");
const { error } = await supabase.auth.verifyOtp({
email: otpEmail,
token: otp,
type: "signup",
});
if (error) {
setStatus(`OTP failed: ${error.message}`);
} else {
const { data: loginData, error: loginError } =
await supabase.auth.signInWithPassword({
email: otpEmail,
password: signupPassRef.current?.value || signupPassword || "",
});
if (loginError) {
setStatus(`Verified; now log in. ${loginError.message}`);
setView("login");
} else {
setSession(loginData.session);
setEmailDisplay(loginData.session?.user?.email || otpEmail);
const fn = loginData.session?.user?.user_metadata?.first_name;
setFirstNameDisplay(fn || firstNameDisplay || "");
setStatus("Account confirmed and logged in.");
setView("app");
}
}
};
const handleLogin = async () => {
if (isAuthLoading) return;
setIsAuthLoading(true);
const email = loginEmailRef.current?.value?.trim() || "";
const password = loginPassRef.current?.value || "";
setStatus("Logging in...");
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setStatus(`Login failed: ${error.message}`);
setIsAuthLoading(false);
} else {
setView("app"); // jump to app immediately on success
setSession(data.session);
setEmailDisplay(data.session?.user?.email || email);
const fn = data.session?.user?.user_metadata?.first_name;
setFirstNameDisplay(fn || firstNameDisplay || (email ? email.split("@")[0] : ""));
setStatus("Logged in.");
setIsAuthLoading(false);
}
};
const handleLogout = async () => {
await supabase.auth.signOut();
setSession(null);
setJobResult(null);
setSystemPrompt("");
setEmailDisplay("");
setChatMessages([]);
if (chatInputRef.current) chatInputRef.current.value = "";
setProgressValue(0);
setProgressText("Idle");
setStatus("Logged out.");
setView("login");
};
const handleSendReset = async () => {
const email =
resetEmailRef.current?.value?.trim() ||
resetEmail ||
loginEmailRef.current?.value?.trim() ||
"";
if (!email) {
setResetStatus("Enter an email to reset.");
return;
}
setResetEmail(email);
setResetOtpEntered(false);
setResetOtpValue("");
setResetStatus("Sending reset OTP...");
const { error } = await supabase.auth.resetPasswordForEmail(email);
if (error) setResetStatus(`Failed: ${error.message}`);
else {
setResetStatus("Reset OTP sent. Check your email.");
setResetSent(true);
}
};
const handleVerifyResetOtp = () => {
const otp = resetOtpRef.current?.value?.trim() || "";
if (!otp) {
setResetStatus("Enter the OTP you received.");
return;
}
setResetOtpValue(otp);
setResetOtpEntered(true);
setResetStatus("OTP captured. Enter new password.");
};
const handleConfirmReset = async () => {
const email =
resetEmailRef.current?.value?.trim() ||
resetEmail ||
loginEmailRef.current?.value?.trim() ||
"";
const otp = resetOtpValue;
const newPass = resetNewPassRef.current?.value || "";
const newPassConfirm = resetNewPassConfirmRef.current?.value || "";
if (!email || !otp || !newPass) {
setResetStatus("Enter OTP and new password.");
return;
}
if (newPass !== newPassConfirm) {
setResetStatus("New password and confirm password do not match.");
return;
}
setResetEmail(email);
setResetStatus("Resetting password...");
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
email,
token: otp,
type: "recovery",
});
if (verifyError) {
setResetStatus(`Failed: ${verifyError.message}`);
return;
}
// After OTP verification, update the password
const { error: updateError } = await supabase.auth.updateUser({
password: newPass,
});
if (updateError) {
setResetStatus(`Failed: ${updateError.message}`);
return;
}
setResetStatus("Password reset. You can log in now.");
setResetSent(false);
setResetOtpEntered(false);
setResetOtpValue("");
if (resetNewPassRef.current) resetNewPassRef.current.value = "";
if (resetNewPassConfirmRef.current) resetNewPassConfirmRef.current.value = "";
setView("login");
};
const runJob = async () => {
const targetUrl = (urlInputRef.current?.value || "").trim();
if (!targetUrl) {
setStatus("Please enter a URL.");
return;
}
setIsRunning(true);
setStatus("Submitting job...");
setJobResult(null);
setSystemPrompt("");
setChatMessages([]);
if (chatInputRef.current) chatInputRef.current.value = "";
setProgressValue(10);
setProgressText("Starting...");
setSummaryVisible(false);
try {
const resp = await fetch(`${API_BASE_URL}/jobs/run`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: targetUrl,
force_refresh: forceRefresh,
user_id: session?.user?.id
}),
});
if (!resp.ok) {
const msg = await resp.text();
throw new Error(msg || `HTTP ${resp.status}`);
}
const json = await resp.json();
setJobResult(json);
const statusText = json?.stats?.status_text || "Completed.";
setStatus("Job completed.");
setSystemPrompt(json?.stats?.system_prompt || "");
setSiteName(json?.stats?.name || "Bot");
setJobResult((prev) => ({ ...prev, status_text: statusText }));
setProgressText(statusText);
const match = statusText.match(/Progress:\s*([0-9]{1,3})%/i);
setProgressValue(match ? Math.min(100, Math.max(0, parseInt(match[1], 10))) : 100);
setSummaryData({
pages: json?.stats?.pages_scraped ?? 0,
searches: json?.stats?.searches_run ?? 0,
});
setSummaryVisible(true);
setTimeout(() => setSummaryVisible(false), 5000);
} catch (err) {
console.error("Job failed", err);
setStatus(`Job failed: ${err.message} | API: ${API_BASE_URL}`);
setProgressText("Failed");
setProgressValue(0);
} finally {
setIsRunning(false);
}
};
const renderHeader = () => (
{firstNameDisplay ? `Welcome, ${firstNameDisplay}` : emailDisplay ? `Welcome, ${emailDisplay.split("@")[0]}` : "Welcome"}
AI-powered chatbot generator for any website.
setView("signup")}> Don’t have an account? Sign up
setView("login")}> Back to login
setView("login")}> Back to login
setView("login")}> Back to login
Paste a URL and generate a chatbot instantly. Scrape → gap detection → targeted search → knowledge base.
Pages scraped: {summaryData.pages}
Web searches: {summaryData.searches}