mrfakename commited on
Commit
c7bcb60
·
1 Parent(s): a2bc252

discussions

Browse files
Files changed (2) hide show
  1. app.py +157 -11
  2. templates/dashboard.html +183 -51
app.py CHANGED
@@ -1,7 +1,10 @@
1
  import os
2
  import secrets
3
  import urllib.parse
4
- from flask import Flask, redirect, request, session, render_template, url_for
 
 
 
5
  import requests
6
 
7
  app = Flask(__name__)
@@ -17,28 +20,158 @@ OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET")
17
  OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
18
  SPACE_HOST = os.environ.get("SPACE_HOST", "localhost:7860")
19
 
20
- # Determine the base URL for redirects
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def get_base_url():
22
  if "localhost" in SPACE_HOST or "127.0.0.1" in SPACE_HOST:
23
  return f"http://{SPACE_HOST}"
24
  return f"https://{SPACE_HOST}"
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @app.route("/")
27
  def index():
28
  if "user" in session:
29
  return redirect(url_for("dashboard"))
30
  return render_template("index.html")
31
 
 
32
  @app.route("/login")
33
  def login():
34
  if not OAUTH_CLIENT_ID:
35
  return "OAuth not configured. Make sure hf_oauth: true is set in your Space's README.md", 500
36
 
37
- # Generate state for CSRF protection
38
  state = secrets.token_urlsafe(32)
39
  session["oauth_state"] = state
40
 
41
- # Build authorization URL
42
  redirect_uri = f"{get_base_url()}/login/callback"
43
  params = {
44
  "client_id": OAUTH_CLIENT_ID,
@@ -51,20 +184,18 @@ def login():
51
 
52
  return redirect(auth_url)
53
 
 
54
  @app.route("/login/callback")
55
  def callback():
56
- # Verify state
57
  state = request.args.get("state")
58
  if state != session.get("oauth_state"):
59
  return "Invalid state parameter", 400
60
 
61
- # Get authorization code
62
  code = request.args.get("code")
63
  if not code:
64
  error = request.args.get("error", "Unknown error")
65
  return f"Authorization failed: {error}", 400
66
 
67
- # Exchange code for tokens
68
  redirect_uri = f"{get_base_url()}/login/callback"
69
  token_url = f"{OPENID_PROVIDER_URL}/oauth/token"
70
 
@@ -88,7 +219,6 @@ def callback():
88
  tokens = token_response.json()
89
  access_token = tokens.get("access_token")
90
 
91
- # Get user info
92
  userinfo_url = f"{OPENID_PROVIDER_URL}/oauth/userinfo"
93
  userinfo_response = requests.get(
94
  userinfo_url,
@@ -100,7 +230,6 @@ def callback():
100
 
101
  user_info = userinfo_response.json()
102
 
103
- # Store user info in session
104
  session["user"] = {
105
  "sub": user_info.get("sub"),
106
  "username": user_info.get("preferred_username"),
@@ -110,21 +239,38 @@ def callback():
110
  }
111
  session["access_token"] = access_token
112
 
113
- # Clean up state
114
  session.pop("oauth_state", None)
115
 
116
  return redirect(url_for("dashboard"))
117
 
 
118
  @app.route("/dashboard")
119
  def dashboard():
120
  if "user" not in session:
121
  return redirect(url_for("index"))
122
- return render_template("dashboard.html", user=session["user"])
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  @app.route("/logout")
125
  def logout():
126
  session.clear()
127
  return redirect(url_for("index"))
128
 
 
 
 
 
129
  if __name__ == "__main__":
130
  app.run(host="0.0.0.0", port=7860, debug=True)
 
1
  import os
2
  import secrets
3
  import urllib.parse
4
+ import sqlite3
5
+ import json
6
+ import time
7
+ from flask import Flask, redirect, request, session, render_template, url_for, g
8
  import requests
9
 
10
  app = Flask(__name__)
 
20
  OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
21
  SPACE_HOST = os.environ.get("SPACE_HOST", "localhost:7860")
22
 
23
+ # Cache settings
24
+ CACHE_TTL = 300 # 5 minutes
25
+ DB_PATH = "cache.db"
26
+
27
+
28
+ def get_db():
29
+ if "db" not in g:
30
+ g.db = sqlite3.connect(DB_PATH)
31
+ g.db.row_factory = sqlite3.Row
32
+ return g.db
33
+
34
+
35
+ @app.teardown_appcontext
36
+ def close_db(exception):
37
+ db = g.pop("db", None)
38
+ if db is not None:
39
+ db.close()
40
+
41
+
42
+ def init_db():
43
+ with sqlite3.connect(DB_PATH) as conn:
44
+ conn.execute("""
45
+ CREATE TABLE IF NOT EXISTS cache (
46
+ key TEXT PRIMARY KEY,
47
+ value TEXT,
48
+ expires_at REAL
49
+ )
50
+ """)
51
+ conn.commit()
52
+
53
+
54
+ def cache_get(key):
55
+ db = get_db()
56
+ row = db.execute(
57
+ "SELECT value, expires_at FROM cache WHERE key = ?", (key,)
58
+ ).fetchone()
59
+ if row and row["expires_at"] > time.time():
60
+ return json.loads(row["value"])
61
+ return None
62
+
63
+
64
+ def cache_set(key, value, ttl=CACHE_TTL):
65
+ db = get_db()
66
+ expires_at = time.time() + ttl
67
+ db.execute(
68
+ "INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)",
69
+ (key, json.dumps(value), expires_at),
70
+ )
71
+ db.commit()
72
+
73
+
74
  def get_base_url():
75
  if "localhost" in SPACE_HOST or "127.0.0.1" in SPACE_HOST:
76
  return f"http://{SPACE_HOST}"
77
  return f"https://{SPACE_HOST}"
78
 
79
+
80
+ def fetch_user_spaces(username, token=None):
81
+ cache_key = f"spaces:{username}"
82
+ cached = cache_get(cache_key)
83
+ if cached is not None:
84
+ return cached
85
+
86
+ headers = {}
87
+ if token:
88
+ headers["Authorization"] = f"Bearer {token}"
89
+
90
+ spaces = []
91
+ url = f"https://huggingface.co/api/spaces?author={username}"
92
+
93
+ try:
94
+ resp = requests.get(url, headers=headers, timeout=10)
95
+ if resp.status_code == 200:
96
+ spaces = resp.json()
97
+ except Exception:
98
+ pass
99
+
100
+ cache_set(cache_key, spaces)
101
+ return spaces
102
+
103
+
104
+ def fetch_space_discussions(space_id, token=None):
105
+ cache_key = f"discussions:{space_id}"
106
+ cached = cache_get(cache_key)
107
+ if cached is not None:
108
+ return cached
109
+
110
+ headers = {}
111
+ if token:
112
+ headers["Authorization"] = f"Bearer {token}"
113
+
114
+ discussions = []
115
+ url = f"https://huggingface.co/api/spaces/{space_id}/discussions"
116
+
117
+ try:
118
+ resp = requests.get(url, headers=headers, timeout=10)
119
+ if resp.status_code == 200:
120
+ data = resp.json()
121
+ discussions = data.get("discussions", [])
122
+ except Exception:
123
+ pass
124
+
125
+ cache_set(cache_key, discussions)
126
+ return discussions
127
+
128
+
129
+ def get_discussions_feed(username, token=None):
130
+ spaces = fetch_user_spaces(username, token)
131
+
132
+ all_discussions = []
133
+ for space in spaces:
134
+ space_id = space.get("id", "")
135
+ discussions = fetch_space_discussions(space_id, token)
136
+
137
+ for d in discussions:
138
+ score = d.get("numComments", 0) + d.get("numReactionUsers", 0) * 2
139
+ all_discussions.append({
140
+ "space_id": space_id,
141
+ "space_name": space_id.split("/")[-1] if "/" in space_id else space_id,
142
+ "num": d.get("num"),
143
+ "title": d.get("title"),
144
+ "status": d.get("status"),
145
+ "is_pr": d.get("isPullRequest", False),
146
+ "author": d.get("author", {}).get("name", "unknown"),
147
+ "author_avatar": d.get("author", {}).get("avatarUrl", ""),
148
+ "created_at": d.get("createdAt", ""),
149
+ "num_comments": d.get("numComments", 0),
150
+ "num_reactions": d.get("numReactionUsers", 0),
151
+ "top_reactions": d.get("topReactions", []),
152
+ "score": score,
153
+ "url": f"https://huggingface.co/spaces/{space_id}/discussions/{d.get('num')}",
154
+ })
155
+
156
+ all_discussions.sort(key=lambda x: x["score"], reverse=True)
157
+ return all_discussions
158
+
159
+
160
  @app.route("/")
161
  def index():
162
  if "user" in session:
163
  return redirect(url_for("dashboard"))
164
  return render_template("index.html")
165
 
166
+
167
  @app.route("/login")
168
  def login():
169
  if not OAUTH_CLIENT_ID:
170
  return "OAuth not configured. Make sure hf_oauth: true is set in your Space's README.md", 500
171
 
 
172
  state = secrets.token_urlsafe(32)
173
  session["oauth_state"] = state
174
 
 
175
  redirect_uri = f"{get_base_url()}/login/callback"
176
  params = {
177
  "client_id": OAUTH_CLIENT_ID,
 
184
 
185
  return redirect(auth_url)
186
 
187
+
188
  @app.route("/login/callback")
189
  def callback():
 
190
  state = request.args.get("state")
191
  if state != session.get("oauth_state"):
192
  return "Invalid state parameter", 400
193
 
 
194
  code = request.args.get("code")
195
  if not code:
196
  error = request.args.get("error", "Unknown error")
197
  return f"Authorization failed: {error}", 400
198
 
 
199
  redirect_uri = f"{get_base_url()}/login/callback"
200
  token_url = f"{OPENID_PROVIDER_URL}/oauth/token"
201
 
 
219
  tokens = token_response.json()
220
  access_token = tokens.get("access_token")
221
 
 
222
  userinfo_url = f"{OPENID_PROVIDER_URL}/oauth/userinfo"
223
  userinfo_response = requests.get(
224
  userinfo_url,
 
230
 
231
  user_info = userinfo_response.json()
232
 
 
233
  session["user"] = {
234
  "sub": user_info.get("sub"),
235
  "username": user_info.get("preferred_username"),
 
239
  }
240
  session["access_token"] = access_token
241
 
 
242
  session.pop("oauth_state", None)
243
 
244
  return redirect(url_for("dashboard"))
245
 
246
+
247
  @app.route("/dashboard")
248
  def dashboard():
249
  if "user" not in session:
250
  return redirect(url_for("index"))
251
+
252
+ username = session["user"]["username"]
253
+ token = session.get("access_token")
254
+
255
+ spaces = fetch_user_spaces(username, token)
256
+ discussions = get_discussions_feed(username, token)
257
+
258
+ return render_template(
259
+ "dashboard.html",
260
+ user=session["user"],
261
+ spaces=spaces,
262
+ discussions=discussions,
263
+ )
264
+
265
 
266
  @app.route("/logout")
267
  def logout():
268
  session.clear()
269
  return redirect(url_for("index"))
270
 
271
+
272
+ # Initialize database on startup
273
+ init_db()
274
+
275
  if __name__ == "__main__":
276
  app.run(host="0.0.0.0", port=7860, debug=True)
templates/dashboard.html CHANGED
@@ -16,12 +16,20 @@
16
  color: #e5e5e5;
17
  min-height: 100vh;
18
  }
 
 
 
 
19
  .navbar {
20
  border-bottom: 1px solid #1a1a1a;
21
  padding: 1rem 2rem;
22
  display: flex;
23
  justify-content: space-between;
24
  align-items: center;
 
 
 
 
25
  }
26
  .navbar h1 {
27
  font-size: 0.875rem;
@@ -46,58 +54,148 @@
46
  .logout-btn {
47
  color: #666;
48
  font-size: 0.75rem;
49
- text-decoration: none;
50
  transition: color 0.2s;
51
  }
52
  .logout-btn:hover {
53
  color: #e5e5e5;
54
  }
55
  .main {
56
- max-width: 600px;
57
  margin: 0 auto;
58
- padding: 4rem 2rem;
59
  }
60
- .welcome {
61
  margin-bottom: 3rem;
62
  }
63
- .welcome h2 {
64
- font-size: 1.25rem;
65
- font-weight: 500;
66
- letter-spacing: -0.02em;
67
- margin-bottom: 0.5rem;
 
 
68
  }
69
- .welcome p {
 
 
70
  color: #666;
71
- font-size: 0.875rem;
 
 
 
 
 
72
  }
73
- .info-list {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  display: flex;
75
  flex-direction: column;
76
- gap: 1px;
77
- background: #1a1a1a;
78
- border-radius: 8px;
79
- overflow: hidden;
80
  }
81
- .info-item {
82
- background: #0a0a0a;
83
- padding: 1rem 1.25rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  display: flex;
85
- justify-content: space-between;
86
  align-items: center;
 
 
87
  }
88
- .info-item:first-child {
89
- padding-top: 1.25rem;
 
90
  }
91
- .info-item:last-child {
92
- padding-bottom: 1.25rem;
 
 
 
 
93
  }
94
- .info-label {
95
- color: #666;
96
- font-size: 0.8125rem;
97
  }
98
- .info-value {
99
- font-size: 0.875rem;
 
 
 
 
 
 
 
 
 
 
 
 
100
  color: #e5e5e5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
  </style>
103
  </head>
@@ -114,33 +212,67 @@
114
  </nav>
115
 
116
  <main class="main">
117
- <div class="welcome">
118
- <h2>{{ user.name or user.username }}</h2>
119
- <p>Signed in with Hugging Face</p>
120
- </div>
121
-
122
- <div class="info-list">
123
- <div class="info-item">
124
- <span class="info-label">Username</span>
125
- <span class="info-value">{{ user.username }}</span>
126
  </div>
127
- {% if user.name %}
128
- <div class="info-item">
129
- <span class="info-label">Name</span>
130
- <span class="info-value">{{ user.name }}</span>
 
 
 
131
  </div>
 
 
132
  {% endif %}
133
- {% if user.email %}
134
- <div class="info-item">
135
- <span class="info-label">Email</span>
136
- <span class="info-value">{{ user.email }}</span>
 
 
137
  </div>
138
- {% endif %}
139
- <div class="info-item">
140
- <span class="info-label">ID</span>
141
- <span class="info-value">{{ user.sub }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  </div>
143
- </div>
 
 
 
144
  </main>
145
  </body>
146
  </html>
 
16
  color: #e5e5e5;
17
  min-height: 100vh;
18
  }
19
+ a {
20
+ color: inherit;
21
+ text-decoration: none;
22
+ }
23
  .navbar {
24
  border-bottom: 1px solid #1a1a1a;
25
  padding: 1rem 2rem;
26
  display: flex;
27
  justify-content: space-between;
28
  align-items: center;
29
+ position: sticky;
30
+ top: 0;
31
+ background: #0a0a0a;
32
+ z-index: 100;
33
  }
34
  .navbar h1 {
35
  font-size: 0.875rem;
 
54
  .logout-btn {
55
  color: #666;
56
  font-size: 0.75rem;
 
57
  transition: color 0.2s;
58
  }
59
  .logout-btn:hover {
60
  color: #e5e5e5;
61
  }
62
  .main {
63
+ max-width: 700px;
64
  margin: 0 auto;
65
+ padding: 2rem 1.5rem 4rem;
66
  }
67
+ .section {
68
  margin-bottom: 3rem;
69
  }
70
+ .section-header {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: center;
74
+ margin-bottom: 1rem;
75
+ padding-bottom: 0.75rem;
76
+ border-bottom: 1px solid #1a1a1a;
77
  }
78
+ .section-title {
79
+ font-size: 0.75rem;
80
+ font-weight: 500;
81
  color: #666;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.05em;
84
+ }
85
+ .section-count {
86
+ font-size: 0.75rem;
87
+ color: #444;
88
  }
89
+ .spaces-grid {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: 0.5rem;
93
+ }
94
+ .space-chip {
95
+ font-size: 0.8125rem;
96
+ padding: 0.375rem 0.75rem;
97
+ background: #141414;
98
+ border: 1px solid #222;
99
+ border-radius: 4px;
100
+ color: #999;
101
+ transition: border-color 0.2s, color 0.2s;
102
+ }
103
+ .space-chip:hover {
104
+ border-color: #333;
105
+ color: #e5e5e5;
106
+ }
107
+ .feed {
108
  display: flex;
109
  flex-direction: column;
 
 
 
 
110
  }
111
+ .feed-item {
112
+ padding: 1rem 0;
113
+ border-bottom: 1px solid #141414;
114
+ display: block;
115
+ transition: background 0.2s;
116
+ }
117
+ .feed-item:first-child {
118
+ padding-top: 0;
119
+ }
120
+ .feed-item:last-child {
121
+ border-bottom: none;
122
+ }
123
+ .feed-item:hover {
124
+ background: #0f0f0f;
125
+ margin: 0 -1rem;
126
+ padding-left: 1rem;
127
+ padding-right: 1rem;
128
+ }
129
+ .feed-meta {
130
  display: flex;
 
131
  align-items: center;
132
+ gap: 0.5rem;
133
+ margin-bottom: 0.375rem;
134
  }
135
+ .feed-space {
136
+ font-size: 0.75rem;
137
+ color: #555;
138
  }
139
+ .feed-type {
140
+ font-size: 0.625rem;
141
+ padding: 0.125rem 0.375rem;
142
+ border-radius: 3px;
143
+ text-transform: uppercase;
144
+ letter-spacing: 0.03em;
145
  }
146
+ .feed-type.pr {
147
+ background: #1a1a2e;
148
+ color: #6366f1;
149
  }
150
+ .feed-type.discussion {
151
+ background: #1a2e1a;
152
+ color: #22c55e;
153
+ }
154
+ .feed-status {
155
+ font-size: 0.625rem;
156
+ color: #444;
157
+ text-transform: uppercase;
158
+ }
159
+ .feed-status.open { color: #22c55e; }
160
+ .feed-status.merged { color: #a855f7; }
161
+ .feed-status.closed { color: #666; }
162
+ .feed-title {
163
+ font-size: 0.9375rem;
164
  color: #e5e5e5;
165
+ margin-bottom: 0.5rem;
166
+ line-height: 1.4;
167
+ }
168
+ .feed-stats {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 1rem;
172
+ font-size: 0.75rem;
173
+ color: #555;
174
+ }
175
+ .feed-stat {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 0.25rem;
179
+ }
180
+ .feed-reactions {
181
+ display: flex;
182
+ gap: 0.25rem;
183
+ }
184
+ .feed-author {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 0.375rem;
188
+ }
189
+ .feed-author-avatar {
190
+ width: 16px;
191
+ height: 16px;
192
+ border-radius: 50%;
193
+ }
194
+ .empty {
195
+ text-align: center;
196
+ padding: 3rem 1rem;
197
+ color: #444;
198
+ font-size: 0.875rem;
199
  }
200
  </style>
201
  </head>
 
212
  </nav>
213
 
214
  <main class="main">
215
+ <section class="section">
216
+ <div class="section-header">
217
+ <h2 class="section-title">Your Spaces</h2>
218
+ <span class="section-count">{{ spaces|length }}</span>
 
 
 
 
 
219
  </div>
220
+ {% if spaces %}
221
+ <div class="spaces-grid">
222
+ {% for space in spaces %}
223
+ <a href="https://huggingface.co/spaces/{{ space.id }}" target="_blank" class="space-chip">
224
+ {{ space.id.split('/')[-1] }}
225
+ </a>
226
+ {% endfor %}
227
  </div>
228
+ {% else %}
229
+ <div class="empty">No spaces found</div>
230
  {% endif %}
231
+ </section>
232
+
233
+ <section class="section">
234
+ <div class="section-header">
235
+ <h2 class="section-title">Discussions Feed</h2>
236
+ <span class="section-count">{{ discussions|length }}</span>
237
  </div>
238
+ {% if discussions %}
239
+ <div class="feed">
240
+ {% for d in discussions %}
241
+ <a href="{{ d.url }}" target="_blank" class="feed-item">
242
+ <div class="feed-meta">
243
+ <span class="feed-space">{{ d.space_name }}</span>
244
+ <span class="feed-type {{ 'pr' if d.is_pr else 'discussion' }}">
245
+ {{ 'PR' if d.is_pr else 'Discussion' }}
246
+ </span>
247
+ <span class="feed-status {{ d.status }}">{{ d.status }}</span>
248
+ </div>
249
+ <div class="feed-title">{{ d.title }}</div>
250
+ <div class="feed-stats">
251
+ <span class="feed-author">
252
+ {% if d.author_avatar %}
253
+ <img src="{{ d.author_avatar if d.author_avatar.startswith('http') else 'https://huggingface.co' + d.author_avatar }}" alt="" class="feed-author-avatar">
254
+ {% endif %}
255
+ {{ d.author }}
256
+ </span>
257
+ <span class="feed-stat">{{ d.num_comments }} comment{{ 's' if d.num_comments != 1 else '' }}</span>
258
+ {% if d.num_reactions > 0 %}
259
+ <span class="feed-stat">
260
+ <span class="feed-reactions">
261
+ {% for r in d.top_reactions[:3] %}
262
+ {{ r.reaction }}
263
+ {% endfor %}
264
+ </span>
265
+ {{ d.num_reactions }}
266
+ </span>
267
+ {% endif %}
268
+ </div>
269
+ </a>
270
+ {% endfor %}
271
  </div>
272
+ {% else %}
273
+ <div class="empty">No discussions yet</div>
274
+ {% endif %}
275
+ </section>
276
  </main>
277
  </body>
278
  </html>