umer6016
Add markdown rendering support with react-markdown
a9aabde
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 }) => (
<div className="card">
<div className="card-head">
<div>
<h2>{title}</h2>
{subtitle ? <p className="card-subtitle">{subtitle}</p> : null}
</div>
</div>
{children}
</div>
);
const ProgressStrip = ({ statusText }) => (
<pre className="result" style={{ whiteSpace: "pre-wrap" }}>
{statusText || "Waiting for run..."}
</pre>
);
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 = () => (
<header className="hero">
<div>
<h1>ChatSmith</h1>
<p className="muted">
{firstNameDisplay
? `Welcome, ${firstNameDisplay}`
: emailDisplay
? `Welcome, ${emailDisplay.split("@")[0]}`
: "Welcome"}
</p>
<p className="hero-subtitle">AI-powered chatbot generator for any website.</p>
</div>
<div className="status">{status}</div>
</header>
);
const renderLoginCard = () => (
<Panel title="Login" subtitle="AI-powered chatbot generator for any website.">
<input
placeholder="Email"
ref={loginEmailRef}
defaultValue=""
/>
<input
placeholder="Password"
type="password"
ref={loginPassRef}
defaultValue=""
/>
<button onClick={handleLogin} disabled={isAuthLoading} className={isAuthLoading ? "loading" : ""}>
{isAuthLoading ? "Logging in..." : "Log In"}
</button>
<p className="link" onClick={() => setView("signup")}>
Don’t have an account? Sign up
</p>
<div className="muted small" style={{ marginTop: 8, cursor: "pointer" }} onClick={() => setView("reset")}>
Forgot password?
</div>
</Panel>
);
const renderSignupCard = () => (
<Panel title="Sign Up" subtitle="Create your account and start building.">
<input
placeholder="First name"
ref={signupFirstRef}
defaultValue=""
/>
<input placeholder="Last name" ref={signupLastRef} defaultValue="" />
<input placeholder="Email" ref={signupEmailRef} defaultValue="" />
<input placeholder="Password" type="password" ref={signupPassRef} defaultValue="" />
<button onClick={handleSignup}>Sign Up</button>
<p className="link" onClick={() => setView("login")}>
Back to login
</p>
</Panel>
);
const renderResetCard = () => (
<Panel title="Reset Password" subtitle="Securely recover access with OTP.">
{!resetSent && (
<>
<input
placeholder="Email for reset"
ref={resetEmailRef}
defaultValue=""
/>
<button onClick={handleSendReset}>Send reset OTP</button>
</>
)}
{resetSent && !resetOtpEntered && (
<>
<input
placeholder="Reset OTP"
ref={resetOtpRef}
defaultValue=""
/>
<button onClick={handleVerifyResetOtp}>Verify OTP</button>
</>
)}
{resetSent && resetOtpEntered && (
<>
<div className="muted small">OTP captured. Enter new password.</div>
<input
placeholder="New password"
type="password"
ref={resetNewPassRef}
defaultValue=""
/>
<input
placeholder="Confirm new password"
type="password"
ref={resetNewPassConfirmRef}
defaultValue=""
/>
<button onClick={handleConfirmReset}>Confirm reset</button>
</>
)}
<div className="status">{resetStatus}</div>
<p className="link" onClick={() => setView("login")}>
Back to login
</p>
</Panel>
);
const renderOtpCard = () => (
<Panel title="Enter OTP" subtitle="Check your inbox for the 6-digit code.">
<div className="muted small">OTP sent to: {otpEmail || "your email"}</div>
<input placeholder="OTP code" ref={otpCodeRef} defaultValue="" />
<button onClick={handleVerifyOtp}>Verify OTP & Login</button>
<p className="link" onClick={() => setView("login")}>
Back to login
</p>
</Panel>
);
const renderAppCards = () => (
<div className="grid single-column">
<Panel title="Generate Chatbot" subtitle="Paste a URL and generate a chatbot instantly.">
<label className="label">Website URL</label>
<input
placeholder="https://example.com"
defaultValue={urlValue}
ref={urlInputRef}
autoCorrect="off"
autoCapitalize="none"
spellCheck={false}
onChange={(e) => setUrlValue(e.target.value)}
/>
<label className="checkbox">
<input type="checkbox" checked={forceRefresh} onChange={(e) => setForceRefresh(e.target.checked)} />
Force refresh
</label>
<button className={isRunning ? "loading" : ""} onClick={runJob} disabled={isRunning}>
{isRunning ? "Running..." : "Run"}
</button>
<p className="muted small generate-desc">Paste a URL and generate a chatbot instantly. Scrape → gap detection → targeted search → knowledge base.</p>
<div className="progress-container">
<div className="progress-bar" style={{ width: `${progressValue}%` }} />
</div>
{systemPrompt && (
<>
<hr style={{ border: "1px solid rgba(255,255,255,0.06)" }} />
{summaryVisible ? (
<div className="card summary-card">
<h3>Summary</h3>
<p className="muted small">Pages scraped: {summaryData.pages}</p>
<p className="muted small">Web searches: {summaryData.searches}</p>
</div>
) : (
<>
<div className="muted small">Chatbot: {siteName}</div>
<div className="chat-box">
{chatMessages.length === 0 && <div className="muted">Ask anything about the scraped site.</div>}
{chatMessages.map((m, idx) => (
<div key={idx} className={`chat-msg ${m.role}`}>
<strong>{m.role === "user" ? "You" : siteName}:</strong>
<div className="md-content">
<ReactMarkdown>{m.content}</ReactMarkdown>
</div>
</div>
))}
</div>
<textarea
rows={4}
placeholder="Type your question..."
ref={chatInputRef}
defaultValue=""
/>
<button onClick={sendChat}>Send</button>
<div className="status">{chatStatus}</div>
</>
)}
</>
)}
</Panel>
<div className="logout-row">
<button className="link logout-small" onClick={handleLogout}>Log out</button>
</div>
</div>
);
const [chatMessages, setChatMessages] = useState([]);
const chatInputRef = useRef(null);
const [chatStatus, setChatStatus] = useState("");
const sendChat = async () => {
const text = chatInputRef.current?.value || "";
if (!text.trim()) return;
const newMessages = [...chatMessages, { role: "user", content: text }];
setChatMessages(newMessages);
if (chatInputRef.current) chatInputRef.current.value = "";
setChatStatus("Thinking...");
try {
const resp = await fetch(`${API_BASE_URL}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
system_prompt: systemPrompt,
messages: newMessages,
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();
const assistantMsg = json?.message;
setChatMessages([...newMessages, assistantMsg]);
setChatStatus("Ready");
} catch (err) {
console.error("Chat failed", err);
setChatStatus(`Chat failed: ${err.message}`);
}
};
return (
<div className="app-shell">
{renderHeader()}
{isSessionChecking ? (
<div className="auth-page">
<div className="auth-stage">
<div className="auth-card-wrap">
<Panel title="Loading..." subtitle="Checking authentication status...">
<div style={{ textAlign: "center", padding: "20px" }}>
<div className="loading" style={{ margin: "0 auto" }}></div>
</div>
</Panel>
</div>
</div>
</div>
) : (
<>
{view === "login" && (
<div className="auth-page">
<div className="auth-stage">
<div className="auth-card-wrap">
{renderLoginCard()}
</div>
</div>
</div>
)}
{view === "signup" && (
<div className="auth-page">
<div className="auth-stage">
<div className="auth-card-wrap">
{renderSignupCard()}
</div>
</div>
</div>
)}
{view === "reset" && (
<div className="auth-page">
<div className="auth-stage">
<div className="auth-card-wrap">
{renderResetCard()}
</div>
</div>
</div>
)}
{view === "otp" && (
<div className="auth-page">
<div className="auth-stage">
<div className="auth-card-wrap">
{renderOtpCard()}
</div>
</div>
</div>
)}
{view === "app" && (
<div className="main-page">
<div className="main-card-wrap">
{renderAppCards()}
</div>
</div>
)}
</>
)}
</div>
);
}