Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -3,7 +3,7 @@ import os, re, json
|
|
3 |
|
4 |
app = Flask(__name__)
|
5 |
|
6 |
-
# 1
|
7 |
CATEGORIES = {
|
8 |
"Productivity": [
|
9 |
"https://huggingface.co/spaces/ginigen/perflexity-clone",
|
@@ -61,83 +61,119 @@ CATEGORIES = {
|
|
61 |
],
|
62 |
}
|
63 |
|
64 |
-
# 2
|
65 |
-
def
|
66 |
-
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)",
|
67 |
if not m:
|
68 |
-
return
|
69 |
-
owner,
|
70 |
owner = owner.lower()
|
71 |
-
|
72 |
-
return f"https://{owner}-{
|
73 |
|
74 |
-
def
|
75 |
-
return f"https://image.thum.io/get/fullpage/{
|
76 |
|
77 |
-
# 3
|
78 |
@app.route('/api/category')
|
79 |
-
def
|
80 |
-
|
81 |
-
urls = CATEGORIES.get(
|
82 |
-
|
83 |
-
|
84 |
-
"
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
|
88 |
-
# 4
|
89 |
@app.route('/')
|
90 |
def home():
|
91 |
return render_template('index.html', cats=list(CATEGORIES.keys()))
|
92 |
|
93 |
-
# 5
|
94 |
os.makedirs('templates', exist_ok=True)
|
95 |
with open('templates/index.html', 'w', encoding='utf-8') as fp:
|
96 |
fp.write(r'''<!DOCTYPE html>
|
97 |
<html><head><meta charset="utf-8"><meta name="viewport"content="width=device-width,initial-scale=1">
|
98 |
-
<title>
|
99 |
<style>
|
|
|
100 |
body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb}
|
101 |
.tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px}
|
102 |
.tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer}
|
103 |
.tab.active{background:#a78bfa;color:#1a202c}
|
104 |
-
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
105 |
-
.card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:420px;display:flex;flex-direction:column}
|
106 |
.frame{flex:1;position:relative;overflow:hidden}
|
107 |
-
.frame img{width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left}
|
|
|
108 |
.foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee}
|
109 |
-
.foot a{font-size:.
|
110 |
</style></head>
|
111 |
<body>
|
112 |
<div class="tabs" id="tabs"></div>
|
113 |
<div class="grid" id="grid"></div>
|
|
|
114 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
const cats={{cats|tojson}};
|
116 |
const tabs=document.getElementById('tabs');
|
117 |
const grid=document.getElementById('grid');
|
118 |
let active="";
|
119 |
function load(cat){
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
c.innerHTML=`<div class="frame"><img src="${sp.img}" loading="lazy"></div>
|
129 |
-
<div class="foot"><a href="${sp.link}" target="_blank">${sp.title}</a></div>`;
|
130 |
-
grid.appendChild(c);
|
131 |
-
});
|
132 |
-
});
|
133 |
}
|
|
|
134 |
cats.forEach((c,i)=>{
|
135 |
-
|
136 |
-
|
|
|
|
|
137 |
});
|
138 |
</script>
|
139 |
</body></html>''')
|
140 |
|
141 |
-
# 6
|
142 |
if __name__ == '__main__':
|
|
|
143 |
app.run(host='0.0.0.0', port=7860)
|
|
|
3 |
|
4 |
app = Flask(__name__)
|
5 |
|
6 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโ 1. CURATED CATEGORIES โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
7 |
CATEGORIES = {
|
8 |
"Productivity": [
|
9 |
"https://huggingface.co/spaces/ginigen/perflexity-clone",
|
|
|
61 |
],
|
62 |
}
|
63 |
|
64 |
+
# โโโโโโโโโโโโโโโ 2. URL HELPERS (iframe + screenshot) โโโโโโโโโโโโโโโโ
|
65 |
+
def direct_url(hf_url: str) -> str:
|
66 |
+
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
|
67 |
if not m:
|
68 |
+
return hf_url
|
69 |
+
owner, name = m.groups()
|
70 |
owner = owner.lower()
|
71 |
+
name = name.replace('.', '-').replace('_', '-').lower()
|
72 |
+
return f"https://{owner}-{name}.hf.space"
|
73 |
|
74 |
+
def screenshot_url(hf_url: str) -> str:
|
75 |
+
return f"https://image.thum.io/get/fullpage/{direct_url(hf_url)}"
|
76 |
|
77 |
+
# โโโโโโโโโโโโโโโโ 3. CATEGORY API (used by tabs) โโโโโโโโโโโโโโโโ
|
78 |
@app.route('/api/category')
|
79 |
+
def api_category():
|
80 |
+
cat = request.args.get('name', '')
|
81 |
+
urls = CATEGORIES.get(cat, [])
|
82 |
+
data = []
|
83 |
+
for url in urls:
|
84 |
+
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", url)
|
85 |
+
owner, name = m.groups() if m else ('', url.split('/')[-1])
|
86 |
+
data.append({
|
87 |
+
"title": name,
|
88 |
+
"owner": owner,
|
89 |
+
"name": name,
|
90 |
+
"iframe": direct_url(url),
|
91 |
+
"shot": screenshot_url(url),
|
92 |
+
"hf": url
|
93 |
+
})
|
94 |
+
return jsonify(data)
|
95 |
|
96 |
+
# โโโโโโโโโโโโโโโโโโโโโ 4. FRONT PAGE ROUTE โโโโโโโโโโโโโโโโโโโโโ
|
97 |
@app.route('/')
|
98 |
def home():
|
99 |
return render_template('index.html', cats=list(CATEGORIES.keys()))
|
100 |
|
101 |
+
# โโโโโโโโโโโโโ 5. BUILD index.html (once) โโโโโโโโโโโโโโ
|
102 |
os.makedirs('templates', exist_ok=True)
|
103 |
with open('templates/index.html', 'w', encoding='utf-8') as fp:
|
104 |
fp.write(r'''<!DOCTYPE html>
|
105 |
<html><head><meta charset="utf-8"><meta name="viewport"content="width=device-width,initial-scale=1">
|
106 |
+
<title>Dynamic HF Spaces Gallery</title>
|
107 |
<style>
|
108 |
+
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
|
109 |
body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb}
|
110 |
.tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px}
|
111 |
.tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer}
|
112 |
.tab.active{background:#a78bfa;color:#1a202c}
|
113 |
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:14px;padding:0 16px 60px}
|
114 |
+
.card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:420px;display:flex;flex-direction:column;position:relative}
|
115 |
.frame{flex:1;position:relative;overflow:hidden}
|
116 |
+
.frame iframe,.frame img{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0}
|
117 |
+
.err{display:none;align-items:center;justify-content:center;width:100%;height:100%;background:#fafafa;text-align:center;padding:10px;color:#666;font-size:.9rem}
|
118 |
.foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee}
|
119 |
+
.foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none}
|
120 |
</style></head>
|
121 |
<body>
|
122 |
<div class="tabs" id="tabs"></div>
|
123 |
<div class="grid" id="grid"></div>
|
124 |
+
|
125 |
<script>
|
126 |
+
// ----- helpers --------------------------------------------------
|
127 |
+
function createCard(sp){
|
128 |
+
const c=document.createElement('div');c.className='card';
|
129 |
+
const f=document.createElement('div');f.className='frame';
|
130 |
+
const ifr=document.createElement('iframe');
|
131 |
+
ifr.src=sp.iframe; ifr.loading='lazy'; f.appendChild(ifr);
|
132 |
+
const err=document.createElement('div');err.className='err';
|
133 |
+
err.innerHTML=`Preview blocked.<br><a href="${sp.hf}" target="_blank">Open on HF โ</a>`;
|
134 |
+
f.appendChild(err);
|
135 |
+
ifr.onerror=()=>toSnapshot();
|
136 |
+
// after 10 s fallback if still blank
|
137 |
+
setTimeout(()=>{try{
|
138 |
+
const ok=ifr.contentWindow && ifr.contentWindow.document.body.innerHTML.length>30;
|
139 |
+
if(!ok) toSnapshot();}catch(e){toSnapshot();}},10000);
|
140 |
+
function toSnapshot(){
|
141 |
+
ifr.remove(); const img=new Image();
|
142 |
+
img.src=sp.shot; img.loading='lazy';
|
143 |
+
img.onerror=()=>{err.style.display='flex';};
|
144 |
+
f.prepend(img);
|
145 |
+
}
|
146 |
+
const foot=document.createElement('div');foot.className='foot';
|
147 |
+
foot.innerHTML=`<a href="${sp.iframe}" target="_blank">${sp.title}</a>`;
|
148 |
+
c.appendChild(f); c.appendChild(foot);
|
149 |
+
return c;
|
150 |
+
}
|
151 |
+
// ----- load tab -------------------------------------------------
|
152 |
const cats={{cats|tojson}};
|
153 |
const tabs=document.getElementById('tabs');
|
154 |
const grid=document.getElementById('grid');
|
155 |
let active="";
|
156 |
function load(cat){
|
157 |
+
if(cat===active)return; active=cat;
|
158 |
+
[...tabs.children].forEach(b=>b.classList.toggle('active',b.dataset.c===cat));
|
159 |
+
grid.innerHTML='<p style="grid-column:1/-1;text-align:center;padding:40px">Loadingโฆ</p>';
|
160 |
+
fetch('/api/category?name='+encodeURIComponent(cat))
|
161 |
+
.then(r=>r.json()).then(arr=>{
|
162 |
+
grid.innerHTML='';
|
163 |
+
arr.forEach(sp=>grid.appendChild(createCard(sp)));
|
164 |
+
});
|
|
|
|
|
|
|
|
|
|
|
165 |
}
|
166 |
+
// tabs init
|
167 |
cats.forEach((c,i)=>{
|
168 |
+
const b=document.createElement('button');
|
169 |
+
b.className='tab';b.textContent=c;b.dataset.c=c;
|
170 |
+
b.onclick=()=>load(c);tabs.appendChild(b);
|
171 |
+
if(i===0)load(c);
|
172 |
});
|
173 |
</script>
|
174 |
</body></html>''')
|
175 |
|
176 |
+
# โโโโโโโโโโโโโโโโโโ 6. RUN APP โโโโโโโโโโโโโโโโโโ
|
177 |
if __name__ == '__main__':
|
178 |
+
# hugggingface spaces use 7860 by convention
|
179 |
app.run(host='0.0.0.0', port=7860)
|