const { useState, useEffect, useRef } = React; // Dynamically detect server origin to support both localhost and Cloud Run production deployments const API_BASE = "/api"; function App() { const [token, setToken] = useState(localStorage.getItem("token")); const [user, setUser] = useState(null); useEffect(() => { if (token) { axios.get(`${API_BASE}/users/me`, { headers: { Authorization: `Bearer ${token}` } }) .then(res => setUser(res.data)) .catch(() => { setToken(null); localStorage.removeItem("token"); }); } }, [token]); const handleLogin = (newToken) => { setToken(newToken); localStorage.setItem("token", newToken); }; const handleLogout = () => { setToken(null); setUser(null); localStorage.removeItem("token"); }; return (
R

ReLAC AI-Assistant

{user && (
{user.email}
)}
{!token ? ( ) : ( )}
); } function AuthForm({ onLogin }) { const [isLogin, setIsLogin] = useState(true); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [msg, setMsg] = useState(""); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setError(""); setMsg(""); setLoading(true); try { if (isLogin) { const params = new URLSearchParams(); params.append('username', email); params.append('password', password); const res = await axios.post(`${API_BASE}/auth/login`, params); onLogin(res.data.access_token); } else { const res = await axios.post(`${API_BASE}/auth/register`, { email, password }); setMsg("Registro exitoso. Se ha enviado un enlace de verificación a tu correo. Por favor valida tu cuenta antes de ingresar."); setIsLogin(true); } } catch (err) { setError(err.response?.data?.detail || "Ha ocurrido un error"); } finally { setLoading(false); } }; return (

{isLogin ? "Iniciar Sesión" : "Crear Cuenta"}

{error &&
{error}
} {msg &&
{msg}
}
setEmail(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-relac-green focus:border-transparent outline-none transition" placeholder="ejemplo@correo.com" required />
setPassword(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-relac-green focus:border-transparent outline-none transition" placeholder="••••••••" required />

{isLogin ? "¿No tienes cuenta? " : "¿Ya tienes cuenta? "}

); } const renderMessageContent = (content) => { if (!content) return null; const lines = content.split('\n'); return lines.map((line, i) => { // Parse bold **text** const parts = line.split(/(\*\*.*?\*\*)/g); const formattedParts = parts.map((part, j) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; } return part; }); const trimmed = line.trim(); // Unordered lists if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) { const listText = trimmed.slice(2); // Re-parse the rest for bold const listParts = listText.split(/(\*\*.*?\*\*)/g).map((part, j) => { if (part.startsWith('**') && part.endsWith('**')) return {part.slice(2, -2)}; return part; }); return (
{listParts}
); } // Ordered lists const orderedMatch = trimmed.match(/^(\d+)\.\s(.*)/); if (orderedMatch) { const listPrefix = orderedMatch[1]; const listText = orderedMatch[2]; const listParts = listText.split(/(\*\*.*?\*\*)/g).map((part, j) => { if (part.startsWith('**') && part.endsWith('**')) return {part.slice(2, -2)}; return part; }); return (
{listPrefix}. {listParts}
); } return

{formattedParts}

; }); }; function ChatInterface({ token }) { const [messages, setMessages] = useState([ { role: "assistant", content: "¡Hola! Soy el asistente de la biblioteca ReLAC. ¿En qué puedo ayudarte hoy? Por ejemplo, puedes pedirme: 'Encuentra evaluaciones sobre género en Ecuador'." } ]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [loadingTime, setLoadingTime] = useState(0); const messagesEndRef = useRef(null); const inputRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { const lastMessage = messages[messages.length - 1]; if (lastMessage && lastMessage.role === "user") { scrollToBottom(); } }, [messages]); useEffect(() => { if (loading) { scrollToBottom(); } }, [loading]); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, []); useEffect(() => { let interval; if (loading) { setLoadingTime(0); interval = setInterval(() => { setLoadingTime((prev) => prev + 1); }, 1000); } else { setLoadingTime(0); } return () => clearInterval(interval); }, [loading]); const handleSend = async (e) => { e.preventDefault(); if (!input.trim() || loading) return; const userMsg = input.trim(); setInput(""); setMessages(prev => [...prev, { role: "user", content: userMsg }]); setLoading(true); try { const res = await axios.post(`${API_BASE}/chat`, { prompt: userMsg }, { headers: { Authorization: `Bearer ${token}` } } ); const botResp = res.data; setMessages(prev => [...prev, { role: "assistant", content: botResp.answer, citations: botResp.citations, prompt: userMsg }]); } catch (err) { let errorMsg = "Lo siento, ocurrió un error."; if (err.response?.status === 429) { errorMsg = "Has alcanzado el límite de 20 consultas en 24 horas."; } else if (err.response?.status === 503) { errorMsg = "El sistema está ocupado. Por favor intenta de nuevo en unos minutos."; } setMessages(prev => [...prev, { role: "assistant", content: errorMsg, isError: true }]); } finally { setLoading(false); // Re-focus the input after submission completes setTimeout(() => { inputRef.current?.focus(); }, 50); } }; const handleFeedback = async (msgIndex, isPositive) => { const msg = messages[msgIndex]; if (msg.feedbackSubmitted) return; try { await axios.post(`${API_BASE}/feedback`, { query_prompt: msg.prompt || "Desconocido", bot_response: msg.content, is_positive: isPositive }, { headers: { Authorization: `Bearer ${token}` } }); setMessages(prev => prev.map((m, i) => i === msgIndex ? { ...m, feedbackSubmitted: true } : m)); } catch (err) { console.error("Feedback error", err); } }; return (
{/* Header */}

Bibliografía AI-Assistant

Respuestas basadas en el catálogo de 1000+ recursos

ReLAC Logo
{/* Chat Area */}
{messages.map((msg, idx) => (
{renderMessageContent(msg.content)}
{/* Citations */} {msg.citations && msg.citations.length > 0 && (

Fuentes y Documentos Citados:

{msg.citations.map((cit, cIdx) => (

{cit.title}

{cit.author}

{cit.link && ( Acceder )}
))}
)} {/* Feedback Mechanism */} {msg.role === "assistant" && !msg.isError && msg.prompt && (
{msg.feedbackSubmitted && ¡Gracias por tu retroalimentación!}
)}
))} {loading && (
Analizando catálogo... ({loadingTime}s)
)}
{/* Input Area */}
setInput(e.target.value)} placeholder="Ej. Encuentra evaluaciones sobre educación en Perú..." className="w-full bg-gray-50 border border-gray-200 text-gray-800 rounded-full py-3.5 pl-6 pr-14 focus:outline-none focus:ring-2 focus:ring-relac-green/50 focus:border-relac-green transition shadow-inner disabled:bg-gray-100" disabled={loading} />

ReLAC AI MVP • Límite de 20 consultas por día.

); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();