satheeshbhukya
Per-user API key support
56a9143
import { useState, useRef, useEffect } from "react";
const API_URL = import.meta.env.VITE_API_URL || "https://happy4040-mock-technical-interviewer.hf.space";
function Markdown({ text }) {
const html = (text || "")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/`{3}[\w]*\n?([\s\S]*?)`{3}/g, "<pre><code>$1</code></pre>")
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/^---$/gm, "<hr/>")
.replace(/^\* (.+)$/gm, "<li>$1</li>")
.replace(/\n\n/g, "</p><p>")
.replace(/\n/g, "<br/>");
return <div className="md-body" dangerouslySetInnerHTML={{ __html: `<p>${html}</p>` }} />;
}
function Whiteboard({ onCapture }) {
const canvasRef = useRef(null);
const drawing = useRef(false);
const lastPos = useRef(null);
const [color, setColor] = useState("#f0f4ff");
const [lineWidth, setLineWidth] = useState(3);
const [eraser, setEraser] = useState(false);
const getPos = (e, canvas) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
if (e.touches) return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY };
return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY };
};
const startDraw = (e) => { e.preventDefault(); drawing.current = true; lastPos.current = getPos(e, canvasRef.current); };
const draw = (e) => {
e.preventDefault();
if (!drawing.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const pos = getPos(e, canvas);
ctx.beginPath(); ctx.moveTo(lastPos.current.x, lastPos.current.y); ctx.lineTo(pos.x, pos.y);
ctx.strokeStyle = eraser ? "#0e1117" : color; ctx.lineWidth = eraser ? 20 : lineWidth;
ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.stroke();
lastPos.current = pos;
};
const stopDraw = () => { drawing.current = false; };
const clear = () => {
const canvas = canvasRef.current; const ctx = canvas.getContext("2d");
ctx.fillStyle = "#0e1117"; ctx.fillRect(0, 0, canvas.width, canvas.height);
};
useEffect(() => { clear(); }, []);
const capture = () => { const b64 = canvasRef.current.toDataURL("image/png").split(",")[1]; onCapture(b64); };
return (
<div className="whiteboard-wrap">
<div className="wb-toolbar">
<span className="wb-label">Whiteboard</span>
<div className="color-swatches">
{["#f0f4ff","#7ee8a2","#ffd166","#ef476f","#06d6a0","#ffffff"].map(c => (
<button key={c} className={`swatch${color === c && !eraser ? " active" : ""}`}
style={{ background: c }} onClick={() => { setColor(c); setEraser(false); }} />
))}
</div>
<input type="range" min="1" max="12" value={lineWidth} onChange={e => setLineWidth(+e.target.value)} className="size-slider" title="Brush size" />
<button className={`wb-btn${eraser ? " active" : ""}`} onClick={() => setEraser(!eraser)}>Erase</button>
<button className="wb-btn" onClick={clear}>Clear</button>
<button className="wb-btn send-wb" onClick={capture}>Send to AI</button>
</div>
<canvas ref={canvasRef} width={900} height={340} className="wb-canvas"
onMouseDown={startDraw} onMouseMove={draw} onMouseUp={stopDraw} onMouseLeave={stopDraw}
onTouchStart={startDraw} onTouchMove={draw} onTouchEnd={stopDraw} />
</div>
);
}
function Message({ role, text }) {
return (
<div className={`msg ${role}`}>
<div className="msg-avatar">{role === "ai" ? "AI" : "You"}</div>
<div className="msg-bubble">{role === "ai" ? <Markdown text={text} /> : <p>{text}</p>}</div>
</div>
);
}
function CodeEditor({ value, onChange }) {
return (
<div className="code-editor-wrap">
<div className="code-editor-header">
<span className="dot red"/><span className="dot yellow"/><span className="dot green"/>
<span className="code-lang">Python</span>
</div>
<textarea className="code-editor" value={value} onChange={e => onChange(e.target.value)}
spellCheck={false} placeholder="# Write your solution here..." />
</div>
);
}
export default function App() {
const [screen, setScreen] = useState("home");
const [apiKey, setApiKey] = useState("");
const [sessionId, setSessionId] = useState(null);
const [messages, setMessages] = useState([]);
const [problem, setProblem] = useState("");
const [code, setCode] = useState("# Your solution here\n");
const [codeChanged, setCodeChanged] = useState(false);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [starting, setStarting] = useState(false);
const [finished, setFinished] = useState(false);
const [report, setReport] = useState("");
const [showWb, setShowWb] = useState(false);
const [error, setError] = useState("");
const chatEndRef = useRef(null);
useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);
const apiFetch = (path, opts = {}) =>
fetch(`${API_URL}${path}`, {
...opts,
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
});
const startSession = async () => {
if (!apiKey.trim()) { setError("Please enter your Google Gemini API key."); return; }
setError(""); setStarting(true);
try {
const res = await apiFetch("/api/session/start", { method: "POST", body: JSON.stringify({ api_key: apiKey.trim() }) });
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
setSessionId(data.session_id);
setMessages([{ role: "ai", text: data.message }]);
setScreen("interview");
} catch (e) {
setError("Could not connect to the server. Please try again.");
} finally { setStarting(false); }
};
const sendMessage = async (extraImageBase64 = null) => {
if (!input.trim() && !codeChanged && !extraImageBase64) return;
const userText = input.trim();
setInput("");
const userMsg = userText || (extraImageBase64 ? "[Whiteboard sent]" : "[Code updated]");
setMessages(m => [...m, { role: "user", text: userMsg }]);
setLoading(true);
try {
const body = { session_id: sessionId, message: userText, code, code_changed: codeChanged, image_base64: extraImageBase64 || null, api_key: apiKey.trim() };
setCodeChanged(false);
const res = await apiFetch("/api/chat", { method: "POST", body: JSON.stringify(body) });
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
setMessages(m => [...m, { role: "ai", text: data.message }]);
if (data.problem && !data.problem.includes("not been selected")) setProblem(data.problem);
if (data.code && data.code !== "# Your code here") setCode(data.code);
if (data.finished) { setFinished(true); if (data.report) setReport(data.report); }
} catch (e) {
setMessages(m => [...m, { role: "ai", text: `Something went wrong. Please try again in a moment.` }]);
} finally { setLoading(false); }
};
const handleWbCapture = (b64) => { setShowWb(false); sendMessage(b64); };
const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } };
const downloadReport = () => {
const blob = new Blob([report], { type: "text/markdown" });
const a = document.createElement("a"); a.href = URL.createObjectURL(blob);
a.download = "interview_report.md"; a.click();
};
if (screen === "home") return (
<div className="home-screen">
<div className="home-card">
<div className="logo-mark"></div>
<h1 className="home-title">Mock Technologie<br/><span>Interview Suite</span></h1>
<p className="home-sub">Practice real technical interviews with an AI interviewer powered by Google Gemini. Get hints, use the whiteboard, write code, and receive a full evaluation report.</p>
<div className="features-row">
<div className="feat"><span>🧠</span><p>Gemini AI</p></div>
<div className="feat"><span>🖊</span><p>Whiteboard</p></div>
<div className="feat"><span>💻</span><p>Code Editor</p></div>
<div className="feat"><span>📊</span><p>Report</p></div>
</div>
<div className="config-section">
<label className="input-label">Google Gemini API Key</label>
<input type="password" className="config-input" placeholder="AIza..."
value={apiKey} onChange={e => setApiKey(e.target.value)}
onKeyDown={e => e.key === "Enter" && startSession()} />
<p className="key-hint">Free key at <a href="https://aistudio.google.com/apikey" target="_blank" rel="noreferrer">aistudio.google.com/apikey</a> — your quota, your usage</p>
</div>
{error && <p className="err-msg">{error}</p>}
<button className="start-btn" onClick={startSession} disabled={starting || !apiKey.trim()}>
{starting ? <span className="spinner"/> : "Start Interview →"}
</button>
</div>
<div className="home-bg">
{Array.from({length:24}).map((_,i)=>(
<div key={i} className="bg-orb" style={{animationDelay:`${i*0.3}s`, left:`${(i*41)%100}%`, top:`${(i*31)%100}%`}}/>
))}
</div>
</div>
);
return (
<div className="app">
<header className="app-header">
<div className="header-brand"><span className="header-logo"></span><span>Mock Technologie Inc.</span></div>
<div className="header-status">
{finished
? <span className="badge done">✓ Complete</span>
: <span className="badge live"><span className="pulse"/>Live</span>}
</div>
{finished && (
<div className="header-actions">
<button className="hdr-btn" onClick={downloadReport}>⬇ Download Report</button>
<button className="hdr-btn accent" onClick={() => setScreen("report")}>View Report</button>
</div>
)}
</header>
<div className="layout">
<div className="left-panel">
<div className="problem-box">
<div className="problem-header"><span>📋</span><span>Problem Statement</span></div>
<div className="problem-body">
{!problem || problem === "No problem selected yet." || problem === "Problem has not been selected yet"
? <p className="no-problem">A question will appear here once selected.</p>
: <Markdown text={problem} />}
</div>
</div>
<div className="chat-box">
<div className="chat-messages">
{messages.map((m, i) => <Message key={i} role={m.role} text={m.text} />)}
{loading && (
<div className="msg ai">
<div className="msg-avatar">AI</div>
<div className="msg-bubble typing"><span/><span/><span/></div>
</div>
)}
<div ref={chatEndRef}/>
</div>
<div className="chat-input-row">
<textarea className="chat-input" rows={3}
placeholder="Type your message… (Enter to send, Shift+Enter for newline)"
value={input} onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown} disabled={loading || finished} />
<div className="chat-actions">
<button className="icon-btn" title="Whiteboard" onClick={() => setShowWb(v => !v)} disabled={finished}>🖊</button>
<button className="send-btn" onClick={() => sendMessage()}
disabled={loading || finished || (!input.trim() && !codeChanged)}>
{loading ? <span className="spinner sm"/> : "Send →"}
</button>
</div>
</div>
</div>
</div>
<div className="right-panel">
<div className="panel-header">
<span>Code Editor</span>
{codeChanged && <span className="changed-badge">● unsent changes</span>}
</div>
<CodeEditor value={code} onChange={v => { setCode(v); setCodeChanged(true); }}/>
<button className="submit-code-btn" onClick={() => sendMessage()} disabled={loading || finished || !codeChanged}>
Submit Code Update
</button>
</div>
</div>
{showWb && (
<div className="wb-overlay">
<div className="wb-modal">
<div className="wb-modal-header">
<span>Whiteboard — Draw your approach</span>
<button className="close-btn" onClick={() => setShowWb(false)}>✕</button>
</div>
<Whiteboard onCapture={handleWbCapture}/>
</div>
</div>
)}
{screen === "report" && (
<div className="report-overlay">
<div className="report-modal">
<div className="report-modal-header">
<span>📊 Interview Evaluation Report</span>
<div style={{display:"flex",gap:"8px"}}>
<button className="hdr-btn" onClick={downloadReport}>⬇ Download</button>
<button className="close-btn" onClick={() => setScreen("interview")}>✕</button>
</div>
</div>
<div className="report-body"><Markdown text={report || "Generating report…"} /></div>
</div>
</div>
)}
</div>
);
}