Spaces:
Build error
Build error
| import React, { useState, useEffect } from 'react' | |
| import { | |
| initiateLogin, | |
| initiateLogout, | |
| getCurrentUser, | |
| recommend, | |
| feedback, | |
| stats, | |
| getUsers, | |
| getContents, | |
| } from './api' | |
| export default function App() { | |
| const [authUser, setAuthUser] = useState(null) | |
| const [authChecked, setAuthChecked] = useState(false) | |
| const [ssoEnabled, setSsoEnabled] = useState(false) | |
| const [userId, setUserId] = useState('') | |
| const [contentId, setContentId] = useState('') | |
| const [algorithm, setAlgorithm] = useState('linucb') | |
| const [users, setUsers] = useState([]) | |
| const [contents, setContents] = useState([]) | |
| const [result, setResult] = useState(null) | |
| const [impressionId, setImpressionId] = useState(null) | |
| const [statsData, setStatsData] = useState(null) | |
| const [statusMsg, setStatusMsg] = useState('') | |
| async function refreshSession() { | |
| setStatusMsg('Checking authentication...') | |
| try { | |
| const auth = await getCurrentUser() | |
| setAuthUser(auth.user) | |
| setSsoEnabled(auth.sso_enabled) | |
| if (auth.user) { | |
| await loadLists() | |
| } | |
| setStatusMsg('') | |
| } catch (e) { | |
| setStatusMsg('Unable to verify authentication: ' + (e.message || e)) | |
| } finally { | |
| setAuthChecked(true) | |
| } | |
| } | |
| async function loadLists() { | |
| try { | |
| const [u, c] = await Promise.all([getUsers(), getContents()]) | |
| setUsers(u || []) | |
| setContents(c || []) | |
| if (u?.length && !userId) setUserId(u[0].user_id) | |
| if (c?.length && !contentId) setContentId(c[0].content_id) | |
| } catch (e) { | |
| setStatusMsg('Failed to load users or contents: ' + (e.message || e)) | |
| } | |
| } | |
| async function handleRecommend() { | |
| setStatusMsg('Requesting recommendation...') | |
| try { | |
| const res = await recommend(userId, contentId, algorithm) | |
| setResult(res) | |
| setImpressionId(res.impression_id) | |
| setStatsData(null) | |
| setStatusMsg(`Recommendation delivered in ${Math.round(res.latency_ms)} ms`) | |
| } catch (e) { | |
| setStatusMsg('Error: ' + (e.message || e)) | |
| } | |
| } | |
| async function handleFeedback(reward = 1.0) { | |
| if (!impressionId) { | |
| setStatusMsg('No recommendation available for feedback') | |
| return | |
| } | |
| setStatusMsg('Sending feedback...') | |
| try { | |
| await feedback(impressionId, reward) | |
| setStatusMsg('Feedback recorded successfully') | |
| } catch (e) { | |
| setStatusMsg('Error: ' + (e.message || e)) | |
| } | |
| } | |
| async function handleStats() { | |
| setStatusMsg('Loading production stats...') | |
| try { | |
| const s = await stats() | |
| setStatsData(s) | |
| setStatusMsg('Stats loaded') | |
| } catch (e) { | |
| setStatusMsg('Error: ' + (e.message || e)) | |
| } | |
| } | |
| useEffect(() => { | |
| refreshSession() | |
| }, []) | |
| const isReady = authChecked && (!!authUser || !authUser) | |
| return ( | |
| <div className="container"> | |
| <header className="topbar"> | |
| <div> | |
| <p className="eyebrow">Artwork Bandit</p> | |
| <h1>Recommendation Studio</h1> | |
| <p className="subtitle">A modern SSO dashboard for artwork recommendation, feedback, and performance insights.</p> | |
| </div> | |
| <div className="profile-bar"> | |
| {authChecked ? ( | |
| authUser ? ( | |
| <> | |
| <div className="profile-info"> | |
| <span>{authUser.name}</span> | |
| <small>{authUser.email}</small> | |
| </div> | |
| <button className="secondary" onClick={initiateLogout}>Sign out</button> | |
| </> | |
| ) : ssoEnabled ? ( | |
| <button className="primary" onClick={initiateLogin}>Sign in with SSO</button> | |
| ) : ( | |
| <div className="profile-info"> | |
| <span>SSO is not configured</span> | |
| <small>Please set SSO environment variables to enable login.</small> | |
| </div> | |
| ) | |
| ) : ( | |
| <span>Checking login...</span> | |
| )} | |
| </div> | |
| </header> | |
| {!authUser ? ( | |
| <main className="hero-card"> | |
| <div> | |
| <h2>Secure access with SSO</h2> | |
| <p>Sign in to access the artwork recommendation engine, track model performance, and share feedback securely.</p> | |
| <button className="primary" onClick={initiateLogin} disabled={!ssoEnabled}> | |
| {ssoEnabled ? 'Start with SSO' : 'SSO not configured'} | |
| </button> | |
| </div> | |
| <div className="hero-aside"> | |
| <p>Configured backend SSO keeps your recommendation experiments protected while preserving the same fast feedback loop.</p> | |
| {!ssoEnabled && <p className="empty-state">Set <code>SSO_DISCOVERY_URL</code>, <code>SSO_CLIENT_ID</code>, and <code>SSO_CLIENT_SECRET</code> to enable login.</p>} | |
| </div> | |
| </main> | |
| ) : ( | |
| <main> | |
| <section className="dashboard-grid"> | |
| <section className="panel controls-panel"> | |
| <h2>Recommendation controls</h2> | |
| <label> | |
| Dataset user | |
| <select value={userId} onChange={(e) => setUserId(e.target.value)}> | |
| {users.map((u) => ( | |
| <option key={u.user_id} value={u.user_id}>{u.user_id}{u.mood ? ` — ${u.mood}` : ''}</option> | |
| ))} | |
| </select> | |
| </label> | |
| <label> | |
| Content topic | |
| <select value={contentId} onChange={(e) => setContentId(e.target.value)}> | |
| {contents.map((c) => ( | |
| <option key={c.content_id} value={c.content_id}>{c.title || c.content_id}</option> | |
| ))} | |
| </select> | |
| </label> | |
| <label> | |
| Algorithm | |
| <select value={algorithm} onChange={(e) => setAlgorithm(e.target.value)}> | |
| <option value="linucb">LinUCB</option> | |
| <option value="thompson">Thompson Sampling</option> | |
| </select> | |
| </label> | |
| <div className="actions"> | |
| <button className="primary" onClick={handleRecommend}>Recommend artwork</button> | |
| <button onClick={() => handleFeedback(1.0)}>Submit click</button> | |
| <button onClick={() => handleFeedback(0.0)}>Submit no click</button> | |
| <button className="secondary" onClick={handleStats}>View stats</button> | |
| </div> | |
| </section> | |
| <section className="panel result-panel"> | |
| <h2>Latest recommendation</h2> | |
| {result ? ( | |
| <div className="card result-card"> | |
| <img src={result.artwork_image || ''} alt={result.artwork_id} /> | |
| <div className="card-body"> | |
| <h3>{result.artwork_id}</h3> | |
| <p>Algorithm: {result.algorithm}</p> | |
| <p>Impression ID: {result.impression_id}</p> | |
| <p>Latency: {Math.round(result.latency_ms)} ms</p> | |
| </div> | |
| </div> | |
| ) : ( | |
| <p className="empty-state">No recommendation requested yet. Use the controls to generate your first prediction.</p> | |
| )} | |
| </section> | |
| </section> | |
| <section className="panel stats-panel"> | |
| <h2>Live KPI dashboard</h2> | |
| {statsData ? ( | |
| <div className="stats-grid"> | |
| <div className="stat-card"> | |
| <span>Total impressions</span> | |
| <strong>{statsData.total_impressions}</strong> | |
| </div> | |
| <div className="stat-card"> | |
| <span>Total clicks</span> | |
| <strong>{statsData.total_clicks}</strong> | |
| </div> | |
| <div className="stat-card"> | |
| <span>Overall CTR</span> | |
| <strong>{(statsData.overall_ctr * 100).toFixed(1)}%</strong> | |
| </div> | |
| </div> | |
| ) : ( | |
| <p className="empty-state">Run the stats query to inspect production performance.</p> | |
| )} | |
| </section> | |
| </main> | |
| )} | |
| <footer> | |
| <p>{statusMsg || 'Ready'}</p> | |
| <small>Backend session auth is enabled when SSO credentials are configured.</small> | |
| </footer> | |
| </div> | |
| ) | |
| } | |