Files changed (1) hide show
  1. page.tsx +377 -0
page.tsx ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { useSession } from "next-auth/react";
5
+ import { SignInButton } from "@/components/auth-button";
6
+ import { createMetaMaskManager, MetaMaskWalletState } from "@/lib/metamask";
7
+
8
+ export default function Home() {
9
+ const { data: session, status } = useSession();
10
+ const [permissions, setPermissions] = useState<string[]>([]);
11
+ const [models, setModels] = useState<any[]>([]);
12
+ const [loadingModels, setLoadingModels] = useState(false);
13
+ const walletManager = useMemo(
14
+ () =>
15
+ createMetaMaskManager({
16
+ autoReconnect: true,
17
+ enforceChainId: "0x1",
18
+ }),
19
+ [],
20
+ );
21
+ const [walletState, setWalletState] = useState<MetaMaskWalletState>(() =>
22
+ walletManager.getState(),
23
+ );
24
+ const [isSwitchingChain, setIsSwitchingChain] = useState(false);
25
+
26
+ useEffect(() => walletManager.watch(setWalletState), [walletManager]);
27
+
28
+ useEffect(() => {
29
+ if (session?.user?.id) {
30
+ // Only fetch if we haven't loaded models yet
31
+ if (models.length === 0 && !loadingModels) {
32
+ setLoadingModels(true);
33
+ fetch("/api/models")
34
+ .then((r) => r.json())
35
+ .then((d) => {
36
+ console.log('Models API response:', d);
37
+ setModels(d.models || []);
38
+ })
39
+ .catch((error) => {
40
+ console.error('Error fetching models:', error);
41
+ setModels([]);
42
+ })
43
+ .finally(() => setLoadingModels(false));
44
+ }
45
+
46
+ // Fetch permissions only once
47
+ if (permissions.length === 0) {
48
+ fetch("/api/permissions")
49
+ .then((r) => r.json())
50
+ .then((d) => setPermissions(d.permissions || []))
51
+ .catch(() => setPermissions([]));
52
+ }
53
+ } else if (!session) {
54
+ // For logged-out users, fetch public models
55
+ if (models.length === 0 && !loadingModels) {
56
+ setLoadingModels(true);
57
+ fetch("/api/models")
58
+ .then((r) => r.json())
59
+ .then((d) => setModels(d.models || []))
60
+ .catch(() => setModels([]))
61
+ .finally(() => setLoadingModels(false));
62
+ }
63
+ }
64
+ }, [session?.user?.id, models.length, loadingModels, permissions.length]);
65
+
66
+ const primaryAccount = walletState.accounts[0];
67
+ const shortenedAccount = primaryAccount
68
+ ? `${primaryAccount.slice(0, 6)}...${primaryAccount.slice(-4)}`
69
+ : undefined;
70
+ const walletStatusText =
71
+ walletState.status === "connected" && shortenedAccount
72
+ ? `Connected: ${shortenedAccount}`
73
+ : walletState.status === "connecting"
74
+ ? "Connecting to MetaMask..."
75
+ : walletState.status === "unavailable"
76
+ ? "MetaMask extension not detected"
77
+ : "Wallet disconnected";
78
+ const walletButtonLabel =
79
+ walletState.status === "connected"
80
+ ? "Disconnect MetaMask"
81
+ : walletState.status === "connecting"
82
+ ? "Connecting..."
83
+ : "Connect MetaMask";
84
+ const walletButtonDisabled =
85
+ walletState.status === "connecting" || walletState.status === "unavailable";
86
+ const needsChainSwitch =
87
+ walletState.status === "connected" &&
88
+ walletState.chainId?.toLowerCase() !== "0x1";
89
+
90
+ const handleWalletClick = () => {
91
+ if (walletState.status === "connected") {
92
+ walletManager.disconnect();
93
+ return;
94
+ }
95
+ walletManager.connect();
96
+ };
97
+
98
+ const handleSwitchToMainnet = async () => {
99
+ setIsSwitchingChain(true);
100
+ try {
101
+ await walletManager.switchChain("0x1");
102
+ } catch (error) {
103
+ console.error("Unable to switch MetaMask network", error);
104
+ } finally {
105
+ setIsSwitchingChain(false);
106
+ }
107
+ };
108
+
109
+ const WalletControls = () => (
110
+ <section className="mb-6 border border-black/10 dark:border-white/15 rounded-lg">
111
+ <div className="px-4 py-3 border-b border-black/10 dark:border-white/15 flex items-center justify-between gap-4">
112
+ <div>
113
+ <h3 className="font-medium">MetaMask Wallet</h3>
114
+ <p className="text-xs text-gray-500 dark:text-gray-400">
115
+ {walletStatusText}
116
+ </p>
117
+ {walletState.chainId && (
118
+ <p className="text-[11px] text-gray-400 dark:text-gray-500">
119
+ Chain ID: {walletState.chainId}
120
+ </p>
121
+ )}
122
+ </div>
123
+ <button
124
+ type="button"
125
+ onClick={handleWalletClick}
126
+ disabled={walletButtonDisabled}
127
+ className="px-4 py-2 rounded-md text-sm font-medium border border-blue-500 text-blue-600 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed"
128
+ >
129
+ {walletButtonLabel}
130
+ </button>
131
+ </div>
132
+ <div className="p-4 space-y-3 text-sm text-gray-600 dark:text-gray-300">
133
+ {walletState.status === "unavailable" && (
134
+ <p>
135
+ Install the{" "}
136
+ <a
137
+ className="text-blue-600 underline"
138
+ href="https://metamask.io/download/"
139
+ target="_blank"
140
+ rel="noreferrer"
141
+ >
142
+ MetaMask extension
143
+ </a>{" "}
144
+ to enable wallet actions.
145
+ </p>
146
+ )}
147
+ {walletState.error && (
148
+ <p className="text-red-600 text-xs">{walletState.error}</p>
149
+ )}
150
+ {walletState.status === "connected" && (
151
+ <div className="flex flex-wrap items-center gap-3">
152
+ <span className="text-xs bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-md">
153
+ {primaryAccount}
154
+ </span>
155
+ {needsChainSwitch && (
156
+ <button
157
+ type="button"
158
+ onClick={handleSwitchToMainnet}
159
+ disabled={isSwitchingChain}
160
+ className="px-3 py-1 rounded-md text-xs border border-amber-500 text-amber-600 hover:bg-amber-50 disabled:opacity-60"
161
+ >
162
+ {isSwitchingChain ? "Switching..." : "Switch to Ethereum Mainnet"}
163
+ </button>
164
+ )}
165
+ </div>
166
+ )}
167
+ </div>
168
+ </section>
169
+ );
170
+
171
+ if (status === "loading") {
172
+ return (
173
+ <div className="min-h-screen grid place-items-center font-poppins">
174
+ <p className="text-sm text-gray-500">Loading…</p>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ if (!session) {
180
+ return (
181
+ <div className="min-h-screen font-poppins">
182
+ <main className="p-6 max-w-6xl mx-auto">
183
+ <div className="mb-6 text-center">
184
+ <h1 className="text-4xl font-semibold mb-2">GarbinAI</h1>
185
+ <h1 className="text-xl font- mb-2">Discover public models on GarbinAI</h1>
186
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">Sign in to create and manage your own.</p>
187
+ <div className="flex justify-center"><SignInButton /></div>
188
+ </div>
189
+ <WalletControls />
190
+ <section className="border border-black/10 dark:border-white/15 rounded-lg">
191
+ <div className="px-4 py-3 border-b border-black/10 dark:border-white/15 flex items-center justify-between">
192
+ <h3 className="font-medium">Public Models</h3>
193
+ <span className="text-xs text-gray-500">{loadingModels ? "Loading..." : `${models.length} models`}</span>
194
+ </div>
195
+ <div className="p-4">
196
+ {loadingModels ? (
197
+ <div className="text-center py-8">
198
+ <div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
199
+ <p className="text-sm text-gray-500 mt-2">Loading public models...</p>
200
+ </div>
201
+ ) : (
202
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
203
+ {models.map((model) => (
204
+ <div key={model.id} className="group border border-gray-200 dark:border-gray-700 rounded-xl p-5 hover:shadow-xl transition-all duration-300 cursor-pointer bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-800 hover:border-blue-400 dark:hover:border-blue-500 hover:-translate-y-1" onClick={() => window.location.href = `/models/${encodeURIComponent(model.id)}`}>
205
+ <div className="flex items-start justify-between mb-3">
206
+ <h4 className="font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors line-clamp-1 flex-1">{model.id}</h4>
207
+ {model.pipeline_tag && (
208
+ <span className="ml-2 px-2 py-1 text-xs font-medium rounded-md bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 whitespace-nowrap">
209
+ {model.pipeline_tag}
210
+ </span>
211
+ )}
212
+ </div>
213
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2 leading-relaxed">{model.cardData?.description || 'No description available'}</p>
214
+ <div className="flex items-center justify-between text-xs text-gray-500 pt-3 border-t border-gray-100 dark:border-gray-700">
215
+ <div className="flex items-center gap-1">
216
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
217
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
218
+ </svg>
219
+ <span>{model.downloads?.toLocaleString() || 0}</span>
220
+ </div>
221
+ <div className="flex items-center gap-1 text-blue-600 dark:text-blue-400 group-hover:gap-2 transition-all">
222
+ <span className="font-medium">View</span>
223
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
224
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
225
+ </svg>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ ))}
230
+ </div>
231
+ )}
232
+ </div>
233
+ </section>
234
+ </main>
235
+ </div>
236
+ );
237
+ }
238
+
239
+ return (
240
+ <div className="min-h-screen font-poppins">
241
+ <div className="grid grid-cols-1 md:grid-cols-[240px_1fr] min-h-0">
242
+ {/* Sidebar */}
243
+ <aside className="border-r border-black/10 dark:border-white/15 p-4 hidden md:block">
244
+ <div className="text-xs uppercase tracking-wide text-gray-500 mb-3">AI Platform</div>
245
+ <nav className="space-y-2">
246
+ <a href="#" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
247
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
248
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
249
+ </svg>
250
+ Models
251
+ </a>
252
+ <a href="/upload" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
253
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
255
+ </svg>
256
+ Create Model
257
+ </a>
258
+ <a href="/models/import" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
259
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
260
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
261
+ </svg>
262
+ Import Models
263
+ </a>
264
+ <a href="#" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
265
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
266
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
267
+ </svg>
268
+ Inference
269
+ </a>
270
+ <a href="#" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
271
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
272
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
273
+ </svg>
274
+ Docs
275
+ </a>
276
+ </nav>
277
+
278
+ <div className="text-xs uppercase tracking-wide text-gray-500 mb-3 mt-8">Account</div>
279
+ <nav className="space-y-2">
280
+ <a href="#" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
281
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
282
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
283
+ </svg>
284
+ Profile
285
+ </a>
286
+ <a href="#" className="flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
287
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
288
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
289
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
290
+ </svg>
291
+ Settings
292
+ </a>
293
+ </nav>
294
+ </aside>
295
+
296
+ {/* Main Content */}
297
+ <main className="p-6 overflow-auto">
298
+ {/* Welcome Section */}
299
+ <div className="mb-6">
300
+ <h1 className="text-2xl mb-2">Welcome back, {session.user?.name}!</h1>
301
+ <p className="text-gray-600 dark:text-gray-400">Your AI-powered platform is ready!</p>
302
+ </div>
303
+ <WalletControls />
304
+
305
+ {/* User's Models */}
306
+ <section className="border border-black/10 dark:border-white/15 rounded-lg">
307
+ <div className="px-4 py-3 border-b border-black/10 dark:border-white/15 flex items-center justify-between">
308
+ <div className="flex items-center gap-3">
309
+ <div className="w-8 h-8 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center">
310
+ <svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
311
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
312
+ </svg>
313
+ </div>
314
+ <h3 className="font-medium">Your Models</h3>
315
+ </div>
316
+ <span className="text-xs text-gray-500">
317
+ {loadingModels ? "Loading..." : `${models.length} models`}
318
+ </span>
319
+ </div>
320
+ <div className="p-4">
321
+ {loadingModels ? (
322
+ <div className="text-center py-8">
323
+ <div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
324
+ <p className="text-sm text-gray-500 mt-2">Loading your models...</p>
325
+ </div>
326
+ ) : models.length > 0 ? (
327
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
328
+ {models.map((model) => (
329
+ <div
330
+ key={model.id}
331
+ className="group border border-gray-200 dark:border-gray-700 rounded-xl p-5 hover:shadow-xl transition-all duration-300 cursor-pointer bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-800 hover:border-blue-400 dark:hover:border-blue-500 hover:-translate-y-1"
332
+ onClick={() => window.location.href = `/models/${encodeURIComponent(model.id)}`}
333
+ >
334
+ <div className="flex items-start justify-between mb-3">
335
+ <h4 className="font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors line-clamp-1 flex-1">{model.id}</h4>
336
+ {model.pipeline_tag && (
337
+ <span className="ml-2 px-2 py-1 text-xs font-medium rounded-md bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 whitespace-nowrap">
338
+ {model.pipeline_tag}
339
+ </span>
340
+ )}
341
+ </div>
342
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2 leading-relaxed">
343
+ {model.cardData?.description || 'No description available'}
344
+ </p>
345
+ <div className="flex items-center justify-between text-xs text-gray-500 pt-3 border-t border-gray-100 dark:border-gray-700">
346
+ <div className="flex items-center gap-1">
347
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
348
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
349
+ </svg>
350
+ <span>{model.downloads?.toLocaleString() || 0}</span>
351
+ </div>
352
+ <div className="flex items-center gap-1 text-blue-600 dark:text-blue-400 group-hover:gap-2 transition-all">
353
+ <span className="font-medium">View</span>
354
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
355
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
356
+ </svg>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ ))}
361
+ </div>
362
+ ) : (
363
+ <div className="text-center py-8">
364
+ <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
365
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
366
+ </svg>
367
+ <h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">No models found</h3>
368
+ <p className="mt-1 text-sm text-gray-500">Get started by creating your first model on Hugging Face.</p>
369
+ </div>
370
+ )}
371
+ </div>
372
+ </section>
373
+ </main>
374
+ </div>
375
+ </div>
376
+ );
377
+ }