Shubham Sattigeri commited on
Commit
8552e78
·
1 Parent(s): 295e745

remove SSO, revert to original model

Browse files
.env.example DELETED
@@ -1,6 +0,0 @@
1
- # Backend SSO configuration
2
- SSO_DISCOVERY_URL=https://YOUR_SSO_PROVIDER/.well-known/openid-configuration
3
- SSO_CLIENT_ID=your-client-id
4
- SSO_CLIENT_SECRET=your-client-secret
5
- SESSION_SECRET=replace-with-a-secure-secret
6
- FRONTEND_URLS=http://localhost:5173,http://127.0.0.1:5173
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -1,3 +1,2 @@
1
  node_modules/
2
  *.db
3
- .env
 
1
  node_modules/
2
  *.db
 
Dockerfile CHANGED
@@ -1,4 +1,3 @@
1
- # Force rebuild - 2026-07-12 v2
2
  # Use an official lightweight Python image
3
  FROM python:3.9-slim
4
 
 
 
1
  # Use an official lightweight Python image
2
  FROM python:3.9-slim
3
 
README.md CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  # Artwork Bandit
2
 
3
  Project implementing a contextual bandit system for dynamic artwork thumbnail selection.
 
1
+ ---
2
+ title: Artwork Bandit
3
+ emoji: 🎨
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
  # Artwork Bandit
11
 
12
  Project implementing a contextual bandit system for dynamic artwork thumbnail selection.
api/auth.py DELETED
@@ -1,12 +0,0 @@
1
- from fastapi import HTTPException, status
2
- from starlette.requests import Request
3
-
4
-
5
- def require_authenticated_user(request: Request):
6
- """Require a logged-in SSO user when SSO is enabled."""
7
- if getattr(request.app.state, 'sso_enabled', False) and not request.session.get('user'):
8
- raise HTTPException(
9
- status_code=status.HTTP_401_UNAUTHORIZED,
10
- detail='Authentication required. Please sign in using SSO.',
11
- )
12
- return request.session.get('user')
 
 
 
 
 
 
 
 
 
 
 
 
 
api/main.py CHANGED
@@ -4,13 +4,8 @@ FastAPI application entry point.
4
  import os
5
  import json
6
  import time
7
- from dotenv import load_dotenv
8
  from fastapi import FastAPI
9
-
10
- load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))
11
  from fastapi.middleware.cors import CORSMiddleware
12
- from starlette.middleware.sessions import SessionMiddleware
13
- from authlib.integrations.starlette_client import OAuth
14
  from .routes import router
15
  from artwork_bandit.db import database
16
  from artwork_bandit.features.nlp_encoder import NLPEncoder
@@ -21,43 +16,7 @@ from artwork_bandit.bandit.thompson import ThompsonBandit
21
 
22
  APP = FastAPI()
23
 
24
- FRONTEND_URLS = os.getenv('FRONTEND_URLS', 'http://localhost:5173,http://127.0.0.1:5173').split(',')
25
- FRONTEND_URL = FRONTEND_URLS[0].strip()
26
-
27
- APP.add_middleware(
28
- SessionMiddleware,
29
- secret_key=os.getenv('SESSION_SECRET', 'replace-with-a-secure-secret'),
30
- same_site='lax',
31
- )
32
- APP.add_middleware(
33
- CORSMiddleware,
34
- allow_origins=[url.strip() for url in FRONTEND_URLS if url.strip()],
35
- allow_methods=['*'],
36
- allow_headers=['*'],
37
- allow_credentials=True,
38
- )
39
-
40
- SSO_DISCOVERY_URL = os.getenv('SSO_DISCOVERY_URL')
41
- SSO_CLIENT_ID = os.getenv('SSO_CLIENT_ID')
42
- SSO_CLIENT_SECRET = os.getenv('SSO_CLIENT_SECRET')
43
- SSO_SCOPES = os.getenv('SSO_SCOPES', 'openid profile email')
44
-
45
- APP.state.frontend_url = FRONTEND_URL
46
- APP.state.sso_enabled = bool(SSO_DISCOVERY_URL and SSO_CLIENT_ID and SSO_CLIENT_SECRET)
47
- print(f"[STARTUP] SSO enabled: {APP.state.sso_enabled}")
48
- APP.state.oauth = None
49
-
50
- if APP.state.sso_enabled:
51
- oauth = OAuth()
52
- oauth.register(
53
- name='sso',
54
- client_id=SSO_CLIENT_ID,
55
- client_secret=SSO_CLIENT_SECRET,
56
- server_metadata_url=SSO_DISCOVERY_URL,
57
- client_kwargs={'scope': SSO_SCOPES},
58
- )
59
- APP.state.oauth = oauth
60
-
61
  APP.include_router(router)
62
 
63
  DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
 
4
  import os
5
  import json
6
  import time
 
7
  from fastapi import FastAPI
 
 
8
  from fastapi.middleware.cors import CORSMiddleware
 
 
9
  from .routes import router
10
  from artwork_bandit.db import database
11
  from artwork_bandit.features.nlp_encoder import NLPEncoder
 
16
 
17
  APP = FastAPI()
18
 
19
+ APP.add_middleware(CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  APP.include_router(router)
21
 
22
  DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
api/routes.py CHANGED
@@ -1,5 +1,4 @@
1
  from fastapi import APIRouter, HTTPException, Request
2
- from fastapi.responses import RedirectResponse
3
  from .schemas import RecommendRequest, RecommendResponse, FeedbackRequest, FeedbackResponse, StatsResponse
4
  import time
5
  from artwork_bandit.db import database
@@ -8,53 +7,8 @@ import numpy as np
8
  router = APIRouter()
9
 
10
 
11
- def require_auth(request: Request):
12
- if getattr(request.app.state, 'sso_enabled', False) and not request.session.get('user'):
13
- raise HTTPException(status_code=401, detail='Authentication required')
14
-
15
-
16
- @router.get('/auth/user')
17
- async def auth_user(request: Request):
18
- return {
19
- 'is_authenticated': bool(request.session.get('user')),
20
- 'user': request.session.get('user'),
21
- 'sso_enabled': getattr(request.app.state, 'sso_enabled', False),
22
- }
23
-
24
-
25
- @router.get('/auth/login')
26
- async def auth_login(request: Request):
27
- if not getattr(request.app.state, 'sso_enabled', False):
28
- raise HTTPException(status_code=400, detail='SSO is not configured')
29
- redirect_uri = request.url_for('auth_callback')
30
- return await request.app.state.oauth.sso.authorize_redirect(request, redirect_uri)
31
-
32
-
33
- @router.get('/auth/callback')
34
- async def auth_callback(request: Request):
35
- if not getattr(request.app.state, 'sso_enabled', False):
36
- raise HTTPException(status_code=400, detail='SSO is not configured')
37
- token = await request.app.state.oauth.sso.authorize_access_token(request)
38
- user = token.get('userinfo')
39
- if not user:
40
- user = await request.app.state.oauth.sso.parse_id_token(request, token)
41
- request.session['user'] = {
42
- 'name': user.get('name') or user.get('email') or user.get('preferred_username'),
43
- 'email': user.get('email'),
44
- 'sub': user.get('sub'),
45
- }
46
- return RedirectResponse(request.app.state.frontend_url)
47
-
48
-
49
- @router.get('/auth/logout')
50
- async def auth_logout(request: Request):
51
- request.session.clear()
52
- return RedirectResponse(request.app.state.frontend_url)
53
-
54
-
55
  @router.get('/users')
56
  async def list_users(request: Request):
57
- require_auth(request)
58
  app = request.app
59
  users = getattr(app.state, 'users', None)
60
  if users is None:
@@ -65,7 +19,6 @@ async def list_users(request: Request):
65
 
66
  @router.get('/contents')
67
  async def list_contents(request: Request):
68
- require_auth(request)
69
  app = request.app
70
  contents = getattr(app.state, 'contents', None)
71
  if contents is None:
@@ -74,7 +27,6 @@ async def list_contents(request: Request):
74
 
75
  @router.post('/recommend', response_model=RecommendResponse)
76
  async def recommend(req: RecommendRequest, request: Request):
77
- require_auth(request)
78
  app = request.app
79
  start = time.time()
80
  fs = app.state.feature_store
@@ -128,7 +80,6 @@ async def recommend(req: RecommendRequest, request: Request):
128
 
129
  @router.post('/feedback', response_model=FeedbackResponse)
130
  async def feedback(req: FeedbackRequest, request: Request):
131
- require_auth(request)
132
  app = request.app
133
  imp = database.get_impression(req.impression_id)
134
  if imp is None:
@@ -149,7 +100,6 @@ async def feedback(req: FeedbackRequest, request: Request):
149
 
150
  @router.get('/stats', response_model=StatsResponse)
151
  async def stats(request: Request):
152
- require_auth(request)
153
  rows = database.impressions_with_rewards()
154
  total = len(rows)
155
  clicks = sum(1 for r in rows if r.reward and r.reward > 0)
 
1
  from fastapi import APIRouter, HTTPException, Request
 
2
  from .schemas import RecommendRequest, RecommendResponse, FeedbackRequest, FeedbackResponse, StatsResponse
3
  import time
4
  from artwork_bandit.db import database
 
7
  router = APIRouter()
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  @router.get('/users')
11
  async def list_users(request: Request):
 
12
  app = request.app
13
  users = getattr(app.state, 'users', None)
14
  if users is None:
 
19
 
20
  @router.get('/contents')
21
  async def list_contents(request: Request):
 
22
  app = request.app
23
  contents = getattr(app.state, 'contents', None)
24
  if contents is None:
 
27
 
28
  @router.post('/recommend', response_model=RecommendResponse)
29
  async def recommend(req: RecommendRequest, request: Request):
 
30
  app = request.app
31
  start = time.time()
32
  fs = app.state.feature_store
 
80
 
81
  @router.post('/feedback', response_model=FeedbackResponse)
82
  async def feedback(req: FeedbackRequest, request: Request):
 
83
  app = request.app
84
  imp = database.get_impression(req.impression_id)
85
  if imp is None:
 
100
 
101
  @router.get('/stats', response_model=StatsResponse)
102
  async def stats(request: Request):
 
103
  rows = database.impressions_with_rewards()
104
  total = len(rows)
105
  clicks = sum(1 for r in rows if r.reward and r.reward > 0)
frontend/.env.example DELETED
@@ -1,2 +0,0 @@
1
- # Frontend API base URL for development
2
- VITE_API_BASE_URL=http://localhost:8000
 
 
 
frontend/src/App.jsx CHANGED
@@ -1,66 +1,22 @@
1
  import React, { useState, useEffect } from 'react'
2
- import {
3
- initiateLogin,
4
- initiateLogout,
5
- getCurrentUser,
6
- recommend,
7
- feedback,
8
- stats,
9
- getUsers,
10
- getContents,
11
- } from './api'
12
 
13
  export default function App() {
14
- const [authUser, setAuthUser] = useState(null)
15
- const [authChecked, setAuthChecked] = useState(false)
16
- const [ssoEnabled, setSsoEnabled] = useState(false)
17
- const [userId, setUserId] = useState('')
18
- const [contentId, setContentId] = useState('')
19
- const [algorithm, setAlgorithm] = useState('linucb')
20
  const [users, setUsers] = useState([])
21
  const [contents, setContents] = useState([])
22
  const [result, setResult] = useState(null)
23
  const [impressionId, setImpressionId] = useState(null)
24
- const [statsData, setStatsData] = useState(null)
25
  const [statusMsg, setStatusMsg] = useState('')
26
 
27
- async function refreshSession() {
28
- setStatusMsg('Checking authentication...')
29
- try {
30
- const auth = await getCurrentUser()
31
- setAuthUser(auth.user)
32
- setSsoEnabled(auth.sso_enabled)
33
- if (auth.user) {
34
- await loadLists()
35
- }
36
- setStatusMsg('')
37
- } catch (e) {
38
- setStatusMsg('Unable to verify authentication: ' + (e.message || e))
39
- } finally {
40
- setAuthChecked(true)
41
- }
42
- }
43
-
44
- async function loadLists() {
45
- try {
46
- const [u, c] = await Promise.all([getUsers(), getContents()])
47
- setUsers(u || [])
48
- setContents(c || [])
49
- if (u?.length && !userId) setUserId(u[0].user_id)
50
- if (c?.length && !contentId) setContentId(c[0].content_id)
51
- } catch (e) {
52
- setStatusMsg('Failed to load users or contents: ' + (e.message || e))
53
- }
54
- }
55
-
56
  async function handleRecommend() {
57
  setStatusMsg('Requesting recommendation...')
58
  try {
59
- const res = await recommend(userId, contentId, algorithm)
60
  setResult(res)
61
  setImpressionId(res.impression_id)
62
- setStatsData(null)
63
- setStatusMsg(`Recommendation delivered in ${Math.round(res.latency_ms)} ms`)
64
  } catch (e) {
65
  setStatusMsg('Error: ' + (e.message || e))
66
  }
@@ -68,165 +24,97 @@ export default function App() {
68
 
69
  async function handleFeedback(reward = 1.0) {
70
  if (!impressionId) {
71
- setStatusMsg('No recommendation available for feedback')
72
  return
73
  }
74
  setStatusMsg('Sending feedback...')
75
  try {
76
- await feedback(impressionId, reward)
77
- setStatusMsg('Feedback recorded successfully')
78
  } catch (e) {
79
  setStatusMsg('Error: ' + (e.message || e))
80
  }
81
  }
82
 
83
  async function handleStats() {
84
- setStatusMsg('Loading production stats...')
85
  try {
86
  const s = await stats()
87
- setStatsData(s)
88
- setStatusMsg('Stats loaded')
89
  } catch (e) {
90
  setStatusMsg('Error: ' + (e.message || e))
91
  }
92
  }
93
 
94
  useEffect(() => {
95
- refreshSession()
 
 
 
 
 
 
 
 
 
 
 
96
  }, [])
97
 
98
- const isReady = authChecked && (!!authUser || !authUser)
99
-
100
  return (
101
  <div className="container">
102
- <header className="topbar">
103
- <div>
104
- <p className="eyebrow">Artwork Bandit</p>
105
- <h1>Recommendation Studio</h1>
106
- <p className="subtitle">A modern SSO dashboard for artwork recommendation, feedback, and performance insights.</p>
107
- </div>
108
- <div className="profile-bar">
109
- {authChecked ? (
110
- authUser ? (
111
- <>
112
- <div className="profile-info">
113
- <span>{authUser.name}</span>
114
- <small>{authUser.email}</small>
115
- </div>
116
- <button className="secondary" onClick={initiateLogout}>Sign out</button>
117
- </>
118
- ) : ssoEnabled ? (
119
- <button className="primary" onClick={initiateLogin}>Sign in with SSO</button>
120
- ) : (
121
- <div className="profile-info">
122
- <span>SSO is not configured</span>
123
- <small>Please set SSO environment variables to enable login.</small>
124
- </div>
125
- )
126
- ) : (
127
- <span>Checking login...</span>
128
- )}
129
- </div>
130
  </header>
131
 
132
- {!authUser ? (
133
- <main className="hero-card">
134
- <div>
135
- <h2>Secure access with SSO</h2>
136
- <p>Sign in to access the artwork recommendation engine, track model performance, and share feedback securely.</p>
137
- <button className="primary" onClick={initiateLogin} disabled={!ssoEnabled}>
138
- {ssoEnabled ? 'Start with SSO' : 'SSO not configured'}
139
- </button>
140
- </div>
141
- <div className="hero-aside">
142
- <p>Configured backend SSO keeps your recommendation experiments protected while preserving the same fast feedback loop.</p>
143
- {!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>}
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
- </main>
146
- ) : (
147
- <main>
148
- <section className="dashboard-grid">
149
- <section className="panel controls-panel">
150
- <h2>Recommendation controls</h2>
151
- <label>
152
- Dataset user
153
- <select value={userId} onChange={(e) => setUserId(e.target.value)}>
154
- {users.map((u) => (
155
- <option key={u.user_id} value={u.user_id}>{u.user_id}{u.mood ? ` — ${u.mood}` : ''}</option>
156
- ))}
157
- </select>
158
- </label>
159
-
160
- <label>
161
- Content topic
162
- <select value={contentId} onChange={(e) => setContentId(e.target.value)}>
163
- {contents.map((c) => (
164
- <option key={c.content_id} value={c.content_id}>{c.title || c.content_id}</option>
165
- ))}
166
- </select>
167
- </label>
168
-
169
- <label>
170
- Algorithm
171
- <select value={algorithm} onChange={(e) => setAlgorithm(e.target.value)}>
172
- <option value="linucb">LinUCB</option>
173
- <option value="thompson">Thompson Sampling</option>
174
- </select>
175
- </label>
176
 
177
- <div className="actions">
178
- <button className="primary" onClick={handleRecommend}>Recommend artwork</button>
179
- <button onClick={() => handleFeedback(1.0)}>Submit click</button>
180
- <button onClick={() => handleFeedback(0.0)}>Submit no click</button>
181
- <button className="secondary" onClick={handleStats}>View stats</button>
 
 
 
 
182
  </div>
183
- </section>
184
-
185
- <section className="panel result-panel">
186
- <h2>Latest recommendation</h2>
187
- {result ? (
188
- <div className="card result-card">
189
- <img src={result.artwork_image || ''} alt={result.artwork_id} />
190
- <div className="card-body">
191
- <h3>{result.artwork_id}</h3>
192
- <p>Algorithm: {result.algorithm}</p>
193
- <p>Impression ID: {result.impression_id}</p>
194
- <p>Latency: {Math.round(result.latency_ms)} ms</p>
195
- </div>
196
- </div>
197
- ) : (
198
- <p className="empty-state">No recommendation requested yet. Use the controls to generate your first prediction.</p>
199
- )}
200
- </section>
201
- </section>
202
 
203
- <section className="panel stats-panel">
204
- <h2>Live KPI dashboard</h2>
205
- {statsData ? (
206
- <div className="stats-grid">
207
- <div className="stat-card">
208
- <span>Total impressions</span>
209
- <strong>{statsData.total_impressions}</strong>
210
- </div>
211
- <div className="stat-card">
212
- <span>Total clicks</span>
213
- <strong>{statsData.total_clicks}</strong>
214
- </div>
215
- <div className="stat-card">
216
- <span>Overall CTR</span>
217
- <strong>{(statsData.overall_ctr * 100).toFixed(1)}%</strong>
218
- </div>
219
- </div>
220
- ) : (
221
- <p className="empty-state">Run the stats query to inspect production performance.</p>
222
- )}
223
- </section>
224
- </main>
225
- )}
226
 
227
  <footer>
228
- <p>{statusMsg || 'Ready'}</p>
229
- <small>Backend session auth is enabled when SSO credentials are configured.</small>
230
  </footer>
231
  </div>
232
  )
 
1
  import React, { useState, useEffect } from 'react'
2
+ import { recommend, feedback, stats, getUsers, getContents } from './api'
 
 
 
 
 
 
 
 
 
3
 
4
  export default function App() {
5
+ const [userId, setUserId] = useState('user_001')
6
+ const [contentId, setContentId] = useState('content_001')
 
 
 
 
7
  const [users, setUsers] = useState([])
8
  const [contents, setContents] = useState([])
9
  const [result, setResult] = useState(null)
10
  const [impressionId, setImpressionId] = useState(null)
 
11
  const [statusMsg, setStatusMsg] = useState('')
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  async function handleRecommend() {
14
  setStatusMsg('Requesting recommendation...')
15
  try {
16
+ const res = await recommend(userId, contentId)
17
  setResult(res)
18
  setImpressionId(res.impression_id)
19
+ setStatusMsg(`Got recommendation (latency ${Math.round(res.latency_ms)} ms)`)
 
20
  } catch (e) {
21
  setStatusMsg('Error: ' + (e.message || e))
22
  }
 
24
 
25
  async function handleFeedback(reward = 1.0) {
26
  if (!impressionId) {
27
+ setStatusMsg('No impression to give feedback for')
28
  return
29
  }
30
  setStatusMsg('Sending feedback...')
31
  try {
32
+ const res = await feedback(impressionId, reward)
33
+ setStatusMsg('Feedback recorded')
34
  } catch (e) {
35
  setStatusMsg('Error: ' + (e.message || e))
36
  }
37
  }
38
 
39
  async function handleStats() {
40
+ setStatusMsg('Fetching stats...')
41
  try {
42
  const s = await stats()
43
+ setStatusMsg(`CTR: ${s.overall_ctr} (${s.total_clicks}/${s.total_impressions})`)
 
44
  } catch (e) {
45
  setStatusMsg('Error: ' + (e.message || e))
46
  }
47
  }
48
 
49
  useEffect(() => {
50
+ async function loadLists() {
51
+ try {
52
+ const [u, c] = await Promise.all([getUsers(), getContents()])
53
+ setUsers(u || [])
54
+ setContents(c || [])
55
+ if (u && u.length && !userId) setUserId(u[0].user_id)
56
+ if (c && c.length && !contentId) setContentId(c[0].content_id)
57
+ } catch (e) {
58
+ setStatusMsg('Failed to load users/contents: ' + (e.message || e))
59
+ }
60
+ }
61
+ loadLists()
62
  }, [])
63
 
 
 
64
  return (
65
  <div className="container">
66
+ <header>
67
+ <h1>Artwork Bandit — Demo UI</h1>
68
+ <p className="subtitle">Recommend artworks, send feedback, and view stats</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </header>
70
 
71
+ <main>
72
+ <div className="controls">
73
+ <label>
74
+ User
75
+ <select value={userId} onChange={(e) => setUserId(e.target.value)}>
76
+ {users.map((u) => (
77
+ <option key={u.user_id} value={u.user_id}>{u.user_id} {u.mood ? `— ${u.mood}` : ''}</option>
78
+ ))}
79
+ </select>
80
+ </label>
81
+ <label>
82
+ Content
83
+ <select value={contentId} onChange={(e) => setContentId(e.target.value)}>
84
+ {contents.map((c) => (
85
+ <option key={c.content_id} value={c.content_id}>{c.content_id} {c.title ? `— ${c.title}` : ''}</option>
86
+ ))}
87
+ </select>
88
+ </label>
89
+ <div className="actions">
90
+ <button onClick={handleRecommend}>Recommend</button>
91
+ <button onClick={() => handleFeedback(1.0)}>Click (reward=1)</button>
92
+ <button onClick={() => handleFeedback(0.0)}>No Click (reward=0)</button>
93
+ <button onClick={handleStats}>Stats</button>
94
  </div>
95
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ <section className="result">
98
+ {result ? (
99
+ <div className="card">
100
+ <img src={result.artwork_image || ''} alt={result.artwork_id} />
101
+ <div className="card-body">
102
+ <h3>{result.artwork_id}</h3>
103
+ <p>Algorithm: {result.algorithm}</p>
104
+ <p>Latency: {Math.round(result.latency_ms)} ms</p>
105
+ <p>Impression: {result.impression_id}</p>
106
  </div>
107
+ </div>
108
+ ) : (
109
+ <p>No recommendation yet.</p>
110
+ )}
111
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ <div className="status">{statusMsg}</div>
114
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  <footer>
117
+ <small>Runs against API at <code>http://127.0.0.1:8000</code></small>
 
118
  </footer>
119
  </div>
120
  )
frontend/src/api.js CHANGED
@@ -1,42 +1,28 @@
1
  import axios from 'axios'
2
 
3
- const BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
4
- const client = axios.create({ baseURL: BASE, withCredentials: true })
5
 
6
- export const initiateLogin = () => {
7
- window.location.href = `${BASE}/auth/login`
8
- }
9
-
10
- export const initiateLogout = () => {
11
- window.location.href = `${BASE}/auth/logout`
12
- }
13
-
14
- export async function getCurrentUser() {
15
- const r = await client.get('/auth/user')
16
- return r.data
17
- }
18
-
19
- export async function recommend(user_id, content_id, algorithm = 'linucb') {
20
- const r = await client.post('/recommend', { user_id, content_id, algorithm })
21
  return r.data
22
  }
23
 
24
  export async function feedback(impression_id, reward) {
25
- const r = await client.post('/feedback', { impression_id, reward })
26
  return r.data
27
  }
28
 
29
  export async function stats() {
30
- const r = await client.get('/stats')
31
  return r.data
32
  }
33
 
34
  export async function getUsers() {
35
- const r = await client.get('/users')
36
  return r.data
37
  }
38
 
39
  export async function getContents() {
40
- const r = await client.get('/contents')
41
  return r.data
42
  }
 
1
  import axios from 'axios'
2
 
3
+ const BASE = 'http://127.0.0.1:8000'
 
4
 
5
+ export async function recommend(user_id, content_id) {
6
+ const r = await axios.post(`${BASE}/recommend`, { user_id, content_id })
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  return r.data
8
  }
9
 
10
  export async function feedback(impression_id, reward) {
11
+ const r = await axios.post(`${BASE}/feedback`, { impression_id, reward })
12
  return r.data
13
  }
14
 
15
  export async function stats() {
16
+ const r = await axios.get(`${BASE}/stats`)
17
  return r.data
18
  }
19
 
20
  export async function getUsers() {
21
+ const r = await axios.get(`${BASE}/users`)
22
  return r.data
23
  }
24
 
25
  export async function getContents() {
26
+ const r = await axios.get(`${BASE}/contents`)
27
  return r.data
28
  }
frontend/src/styles.css CHANGED
@@ -1,282 +1,18 @@
1
- :root {
2
- --bg: #0f1724;
3
- --surface: #111827;
4
- --surface-soft: #131f33;
5
- --muted: #94a3b8;
6
- --text: #e6eef6;
7
- --accent: #22d3ee;
8
- --accent-dark: #0e7490;
9
- --border: rgba(148, 163, 184, 0.14);
10
- }
11
-
12
- * {
13
- box-sizing: border-box;
14
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, 'Helvetica Neue', Arial, sans-serif;
15
- }
16
-
17
- html,
18
- body {
19
- margin: 0;
20
- min-height: 100%;
21
- background: radial-gradient(circle at top left, rgba(56, 189, 248, 0.16), transparent 35%),
22
- radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.15), transparent 28%),
23
- linear-gradient(180deg, #050811 0%, #08101c 100%);
24
- color: var(--text);
25
- }
26
-
27
- button,
28
- select {
29
- font: inherit;
30
- }
31
-
32
- .container {
33
- max-width: 1180px;
34
- margin: 0 auto;
35
- padding: 28px 24px 42px;
36
- }
37
-
38
- header h1 {
39
- margin: 0;
40
- font-size: clamp(2rem, 2.8vw, 3.4rem);
41
- line-height: 1.05;
42
- }
43
-
44
- .subtitle {
45
- margin: 12px 0 0;
46
- color: var(--muted);
47
- max-width: 720px;
48
- }
49
-
50
- .topbar {
51
- display: flex;
52
- flex-wrap: wrap;
53
- justify-content: space-between;
54
- gap: 20px;
55
- align-items: center;
56
- margin-bottom: 28px;
57
- }
58
-
59
- .eyebrow {
60
- text-transform: uppercase;
61
- letter-spacing: 0.24em;
62
- color: var(--accent);
63
- margin: 0 0 12px;
64
- font-size: 0.82rem;
65
- }
66
-
67
- .profile-bar {
68
- display: flex;
69
- gap: 12px;
70
- align-items: center;
71
- }
72
-
73
- .profile-info {
74
- display: grid;
75
- gap: 2px;
76
- text-align: right;
77
- }
78
-
79
- .profile-info span {
80
- font-weight: 700;
81
- }
82
-
83
- .profile-info small {
84
- color: var(--muted);
85
- }
86
-
87
- button {
88
- border: none;
89
- border-radius: 999px;
90
- padding: 12px 18px;
91
- cursor: pointer;
92
- transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
93
- }
94
-
95
- button.primary {
96
- background: linear-gradient(135deg, var(--accent), #38bdf8);
97
- color: #031827;
98
- box-shadow: 0 18px 50px rgba(34, 211, 238, 0.18);
99
- }
100
-
101
- button.secondary {
102
- background: rgba(255, 255, 255, 0.08);
103
- color: var(--text);
104
- border: 1px solid rgba(255, 255, 255, 0.08);
105
- }
106
-
107
- button:hover {
108
- transform: translateY(-1px);
109
- }
110
-
111
- .hero-card,
112
- .dashboard-grid,
113
- .panel,
114
- .stats-grid {
115
- display: grid;
116
- }
117
-
118
- .hero-card {
119
- gap: 20px;
120
- padding: 36px;
121
- border-radius: 24px;
122
- background: rgba(255, 255, 255, 0.04);
123
- border: 1px solid rgba(255, 255, 255, 0.06);
124
- box-shadow: 0 30px 80px rgba(0, 0, 0, 0.22);
125
- }
126
-
127
- .hero-card h2,
128
- .panel h2 {
129
- margin: 0 0 14px;
130
- font-size: 1.4rem;
131
- }
132
-
133
- .hero-card p,
134
- .panel p,
135
- .empty-state {
136
- color: var(--muted);
137
- line-height: 1.7;
138
- }
139
-
140
- .hero-aside {
141
- color: var(--text);
142
- font-size: 0.95rem;
143
- line-height: 1.8;
144
- }
145
-
146
- .dashboard-grid {
147
- gap: 24px;
148
- grid-template-columns: minmax(300px, 360px) minmax(400px, 1fr);
149
- }
150
-
151
- .panel {
152
- gap: 16px;
153
- padding: 26px;
154
- border-radius: 24px;
155
- background: rgba(255, 255, 255, 0.04);
156
- border: 1px solid rgba(255, 255, 255, 0.06);
157
- }
158
-
159
- .controls-panel label,
160
- .panel label {
161
- display: flex;
162
- flex-direction: column;
163
- gap: 8px;
164
- color: var(--muted);
165
- }
166
-
167
- label select {
168
- min-width: 0;
169
- border-radius: 16px;
170
- border: 1px solid rgba(148, 163, 184, 0.18);
171
- background: rgba(15, 23, 42, 0.9);
172
- color: var(--text);
173
- padding: 14px 16px;
174
- }
175
-
176
- .actions {
177
- display: grid;
178
- gap: 12px;
179
- margin-top: 12px;
180
- }
181
-
182
- .actions button {
183
- width: 100%;
184
- }
185
-
186
- .result-panel {
187
- min-height: 316px;
188
- }
189
-
190
- .card {
191
- display: grid;
192
- grid-template-columns: 180px 1fr;
193
- gap: 20px;
194
- align-items: center;
195
- padding: 24px;
196
- border-radius: 24px;
197
- background: rgba(255, 255, 255, 0.04);
198
- border: 1px solid rgba(255, 255, 255, 0.06);
199
- }
200
-
201
- .result-card img {
202
- width: 100%;
203
- max-width: 180px;
204
- aspect-ratio: 1 / 1;
205
- object-fit: cover;
206
- border-radius: 20px;
207
- background: #0d172a;
208
- }
209
-
210
- .card-body h3 {
211
- margin: 0 0 12px;
212
- }
213
-
214
- .card-body p {
215
- margin: 4px 0;
216
- color: var(--muted);
217
- }
218
-
219
- .stats-panel {
220
- grid-column: 1 / -1;
221
- }
222
-
223
- .stats-grid {
224
- grid-template-columns: repeat(3, minmax(180px, 1fr));
225
- gap: 16px;
226
- }
227
-
228
- .stat-card {
229
- padding: 20px;
230
- border-radius: 20px;
231
- background: rgba(15, 23, 42, 0.9);
232
- border: 1px solid rgba(148, 163, 184, 0.12);
233
- }
234
-
235
- .stat-card span {
236
- display: block;
237
- color: var(--muted);
238
- margin-bottom: 8px;
239
- }
240
-
241
- .stat-card strong {
242
- display: block;
243
- font-size: 1.8rem;
244
- color: var(--text);
245
- }
246
-
247
- footer {
248
- margin-top: 24px;
249
- color: var(--muted);
250
- display: flex;
251
- justify-content: space-between;
252
- align-items: center;
253
- flex-wrap: wrap;
254
- gap: 12px;
255
- }
256
-
257
- .empty-state {
258
- padding: 28px 0;
259
- color: var(--muted);
260
- }
261
-
262
- @media (max-width: 900px) {
263
- .dashboard-grid,
264
- .stats-grid {
265
- grid-template-columns: 1fr;
266
- }
267
- }
268
-
269
- @media (max-width: 640px) {
270
- .container {
271
- padding: 20px 16px 28px;
272
- }
273
-
274
- .topbar {
275
- align-items: flex-start;
276
- }
277
-
278
- .profile-bar {
279
- width: 100%;
280
- justify-content: flex-start;
281
- }
282
- }
 
1
+ :root{--bg:#0f1724;--card:#0b1220;--muted:#94a3b8;--accent:#06b6d4}
2
+ *{box-sizing:border-box;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,'Helvetica Neue',Arial}
3
+ body{margin:0;background:linear-gradient(180deg,#041022 0%,#071029 100%);color:#e6eef6}
4
+ .container{max-width:980px;margin:32px auto;padding:24px}
5
+ header h1{margin:0;font-size:1.6rem}
6
+ .subtitle{margin:4px 0 18px;color:var(--muted)}
7
+ .controls{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
8
+ label{display:flex;flex-direction:column;font-size:0.9rem;color:var(--muted)}
9
+ input{padding:8px 10px;border-radius:8px;border:1px solid #123;background:#071226;color:inherit}
10
+ .actions{grid-column:1/-1;display:flex;gap:10px;margin-top:8px}
11
+ button{background:var(--accent);border:none;padding:8px 12px;border-radius:8px;color:#052;cursor:pointer}
12
+ .result{margin-top:18px}
13
+ .card{display:flex;gap:12px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);padding:12px;border-radius:10px;align-items:center}
14
+ .card img{width:130px;height:130px;object-fit:cover;border-radius:8px;background:#022}
15
+ .card-body h3{margin:0 0 6px}
16
+ .status{margin-top:12px;color:var(--muted)}
17
+ footer{margin-top:22px;color:var(--muted)}
18
+ code{background:#022;padding:2px 6px;border-radius:6px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -14,6 +14,3 @@ httpx==0.27.0
14
  matplotlib==3.9.0
15
  seaborn==0.13.2
16
  tqdm==4.66.4
17
- authlib==1.2.0
18
- python-dotenv==1.0.1
19
- itsdangerous==2.2.0
 
14
  matplotlib==3.9.0
15
  seaborn==0.13.2
16
  tqdm==4.66.4