Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
add user validation
Browse files- package-lock.json +28 -3
- package.json +3 -0
- prisma/dev.db +0 -0
- prisma/migrations/20240104145615_init/migration.sql +0 -25
- prisma/migrations/20240104153500_init/migration.sql +0 -2
- prisma/migrations/20240104154419_init/migration.sql +0 -25
- prisma/migrations/{20240104143521_init β 20240104191224_init}/migration.sql +1 -3
- prisma/schema.prisma +9 -8
- src/lib/components/Button.svelte +3 -1
- src/lib/components/GoTop.svelte +14 -5
- src/lib/components/community/Card.svelte +1 -2
- src/lib/components/community/reactions/Add.svelte +4 -2
- src/lib/components/models/Card.svelte +1 -1
- src/lib/components/models/Submit.svelte +91 -8
- src/lib/components/sidebar/Menu.svelte +0 -1
- src/lib/components/sidebar/Sidebar.svelte +30 -19
- src/lib/stores/use-user.ts +4 -0
- src/lib/utils/index.ts +27 -1
- src/routes/+layout.server.ts +10 -0
- src/routes/+layout.svelte +4 -0
- src/routes/api/@me/+server.ts +34 -0
- src/routes/api/models/+server.ts +9 -2
- src/routes/api/models/submit/+server.ts +69 -0
- src/routes/models/+page.svelte +2 -2
package-lock.json
CHANGED
@@ -11,6 +11,8 @@
|
|
11 |
"@iconify/svelte": "^3.1.4",
|
12 |
"@prisma/client": "^5.7.1",
|
13 |
"@sveltejs/adapter-node": "^1.3.1",
|
|
|
|
|
14 |
"svelte-infinite-scroll": "^2.0.1"
|
15 |
},
|
16 |
"devDependencies": {
|
@@ -20,6 +22,7 @@
|
|
20 |
"@sveltejs/enhanced-img": "^0.1.7",
|
21 |
"@sveltejs/kit": "^1.27.4",
|
22 |
"@types/cookie": "^0.5.1",
|
|
|
23 |
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
24 |
"@typescript-eslint/parser": "^6.0.0",
|
25 |
"autoprefixer": "^10.4.16",
|
@@ -1346,6 +1349,14 @@
|
|
1346 |
"vite": "^4.0.0"
|
1347 |
}
|
1348 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1349 |
"node_modules/@sveltejs/vite-plugin-svelte": {
|
1350 |
"version": "2.5.3",
|
1351 |
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz",
|
@@ -1393,6 +1404,12 @@
|
|
1393 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
1394 |
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
1395 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
1396 |
"node_modules/@types/json-schema": {
|
1397 |
"version": "7.0.15",
|
1398 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
@@ -2019,9 +2036,9 @@
|
|
2019 |
"dev": true
|
2020 |
},
|
2021 |
"node_modules/cookie": {
|
2022 |
-
"version": "0.
|
2023 |
-
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.
|
2024 |
-
"integrity": "sha512-
|
2025 |
"engines": {
|
2026 |
"node": ">= 0.6"
|
2027 |
}
|
@@ -2893,6 +2910,14 @@
|
|
2893 |
"jiti": "bin/jiti.js"
|
2894 |
}
|
2895 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2896 |
"node_modules/js-yaml": {
|
2897 |
"version": "4.1.0",
|
2898 |
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
|
|
11 |
"@iconify/svelte": "^3.1.4",
|
12 |
"@prisma/client": "^5.7.1",
|
13 |
"@sveltejs/adapter-node": "^1.3.1",
|
14 |
+
"cookie": "^0.6.0",
|
15 |
+
"js-cookie": "^3.0.5",
|
16 |
"svelte-infinite-scroll": "^2.0.1"
|
17 |
},
|
18 |
"devDependencies": {
|
|
|
22 |
"@sveltejs/enhanced-img": "^0.1.7",
|
23 |
"@sveltejs/kit": "^1.27.4",
|
24 |
"@types/cookie": "^0.5.1",
|
25 |
+
"@types/js-cookie": "^3.0.6",
|
26 |
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
27 |
"@typescript-eslint/parser": "^6.0.0",
|
28 |
"autoprefixer": "^10.4.16",
|
|
|
1349 |
"vite": "^4.0.0"
|
1350 |
}
|
1351 |
},
|
1352 |
+
"node_modules/@sveltejs/kit/node_modules/cookie": {
|
1353 |
+
"version": "0.5.0",
|
1354 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
1355 |
+
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
1356 |
+
"engines": {
|
1357 |
+
"node": ">= 0.6"
|
1358 |
+
}
|
1359 |
+
},
|
1360 |
"node_modules/@sveltejs/vite-plugin-svelte": {
|
1361 |
"version": "2.5.3",
|
1362 |
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz",
|
|
|
1404 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
1405 |
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
1406 |
},
|
1407 |
+
"node_modules/@types/js-cookie": {
|
1408 |
+
"version": "3.0.6",
|
1409 |
+
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
1410 |
+
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
1411 |
+
"dev": true
|
1412 |
+
},
|
1413 |
"node_modules/@types/json-schema": {
|
1414 |
"version": "7.0.15",
|
1415 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
|
2036 |
"dev": true
|
2037 |
},
|
2038 |
"node_modules/cookie": {
|
2039 |
+
"version": "0.6.0",
|
2040 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
2041 |
+
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
2042 |
"engines": {
|
2043 |
"node": ">= 0.6"
|
2044 |
}
|
|
|
2910 |
"jiti": "bin/jiti.js"
|
2911 |
}
|
2912 |
},
|
2913 |
+
"node_modules/js-cookie": {
|
2914 |
+
"version": "3.0.5",
|
2915 |
+
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
2916 |
+
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
2917 |
+
"engines": {
|
2918 |
+
"node": ">=14"
|
2919 |
+
}
|
2920 |
+
},
|
2921 |
"node_modules/js-yaml": {
|
2922 |
"version": "4.1.0",
|
2923 |
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
package.json
CHANGED
@@ -17,6 +17,7 @@
|
|
17 |
"@sveltejs/enhanced-img": "^0.1.7",
|
18 |
"@sveltejs/kit": "^1.27.4",
|
19 |
"@types/cookie": "^0.5.1",
|
|
|
20 |
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
21 |
"@typescript-eslint/parser": "^6.0.0",
|
22 |
"autoprefixer": "^10.4.16",
|
@@ -40,6 +41,8 @@
|
|
40 |
"@iconify/svelte": "^3.1.4",
|
41 |
"@prisma/client": "^5.7.1",
|
42 |
"@sveltejs/adapter-node": "^1.3.1",
|
|
|
|
|
43 |
"svelte-infinite-scroll": "^2.0.1"
|
44 |
}
|
45 |
}
|
|
|
17 |
"@sveltejs/enhanced-img": "^0.1.7",
|
18 |
"@sveltejs/kit": "^1.27.4",
|
19 |
"@types/cookie": "^0.5.1",
|
20 |
+
"@types/js-cookie": "^3.0.6",
|
21 |
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
22 |
"@typescript-eslint/parser": "^6.0.0",
|
23 |
"autoprefixer": "^10.4.16",
|
|
|
41 |
"@iconify/svelte": "^3.1.4",
|
42 |
"@prisma/client": "^5.7.1",
|
43 |
"@sveltejs/adapter-node": "^1.3.1",
|
44 |
+
"cookie": "^0.6.0",
|
45 |
+
"js-cookie": "^3.0.5",
|
46 |
"svelte-infinite-scroll": "^2.0.1"
|
47 |
}
|
48 |
}
|
prisma/dev.db
CHANGED
Binary files a/prisma/dev.db and b/prisma/dev.db differ
|
|
prisma/migrations/20240104145615_init/migration.sql
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
Warnings:
|
3 |
-
|
4 |
-
- You are about to drop the column `downloads` on the `Model` table. All the data in the column will be lost.
|
5 |
-
- You are about to drop the column `image` on the `Model` table. All the data in the column will be lost.
|
6 |
-
- You are about to drop the column `likes` on the `Model` table. All the data in the column will be lost.
|
7 |
-
- You are about to drop the column `title` on the `Model` table. All the data in the column will be lost.
|
8 |
-
- You are about to drop the column `trigger_word` on the `Model` table. All the data in the column will be lost.
|
9 |
-
- You are about to drop the column `weights` on the `Model` table. All the data in the column will be lost.
|
10 |
-
|
11 |
-
*/
|
12 |
-
-- RedefineTables
|
13 |
-
PRAGMA foreign_keys=OFF;
|
14 |
-
CREATE TABLE "new_Model" (
|
15 |
-
"id" TEXT NOT NULL PRIMARY KEY,
|
16 |
-
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
17 |
-
"repo" TEXT NOT NULL,
|
18 |
-
"isPublic" BOOLEAN NOT NULL DEFAULT false
|
19 |
-
);
|
20 |
-
INSERT INTO "new_Model" ("createdAt", "id", "isPublic", "repo") SELECT "createdAt", "id", "isPublic", "repo" FROM "Model";
|
21 |
-
DROP TABLE "Model";
|
22 |
-
ALTER TABLE "new_Model" RENAME TO "Model";
|
23 |
-
CREATE UNIQUE INDEX "Model_repo_key" ON "Model"("repo");
|
24 |
-
PRAGMA foreign_key_check;
|
25 |
-
PRAGMA foreign_keys=ON;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prisma/migrations/20240104153500_init/migration.sql
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
-- AlterTable
|
2 |
-
ALTER TABLE "Model" ADD COLUMN "image" TEXT;
|
|
|
|
|
|
prisma/migrations/20240104154419_init/migration.sql
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
Warnings:
|
3 |
-
|
4 |
-
- Added the required column `title` to the `Model` table without a default value. This is not possible if the table is not empty.
|
5 |
-
- Made the column `image` on table `Model` required. This step will fail if there are existing NULL values in that column.
|
6 |
-
|
7 |
-
*/
|
8 |
-
-- RedefineTables
|
9 |
-
PRAGMA foreign_keys=OFF;
|
10 |
-
CREATE TABLE "new_Model" (
|
11 |
-
"id" TEXT NOT NULL PRIMARY KEY,
|
12 |
-
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
13 |
-
"repo" TEXT NOT NULL,
|
14 |
-
"title" TEXT NOT NULL,
|
15 |
-
"image" TEXT NOT NULL,
|
16 |
-
"likes" INTEGER,
|
17 |
-
"downloads" INTEGER,
|
18 |
-
"isPublic" BOOLEAN NOT NULL DEFAULT false
|
19 |
-
);
|
20 |
-
INSERT INTO "new_Model" ("createdAt", "id", "image", "isPublic", "repo") SELECT "createdAt", "id", "image", "isPublic", "repo" FROM "Model";
|
21 |
-
DROP TABLE "Model";
|
22 |
-
ALTER TABLE "new_Model" RENAME TO "Model";
|
23 |
-
CREATE UNIQUE INDEX "Model_repo_key" ON "Model"("repo");
|
24 |
-
PRAGMA foreign_key_check;
|
25 |
-
PRAGMA foreign_keys=ON;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prisma/migrations/{20240104143521_init β 20240104191224_init}/migration.sql
RENAMED
@@ -3,10 +3,8 @@ CREATE TABLE "Model" (
|
|
3 |
"id" TEXT NOT NULL PRIMARY KEY,
|
4 |
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
5 |
"repo" TEXT NOT NULL,
|
6 |
-
"title" TEXT,
|
7 |
-
"trigger_word" TEXT,
|
8 |
"image" TEXT,
|
9 |
-
"weights" TEXT,
|
10 |
"likes" INTEGER,
|
11 |
"downloads" INTEGER,
|
12 |
"isPublic" BOOLEAN NOT NULL DEFAULT false
|
|
|
3 |
"id" TEXT NOT NULL PRIMARY KEY,
|
4 |
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
5 |
"repo" TEXT NOT NULL,
|
6 |
+
"title" TEXT NOT NULL,
|
|
|
7 |
"image" TEXT,
|
|
|
8 |
"likes" INTEGER,
|
9 |
"downloads" INTEGER,
|
10 |
"isPublic" BOOLEAN NOT NULL DEFAULT false
|
prisma/schema.prisma
CHANGED
@@ -11,14 +11,15 @@ datasource db {
|
|
11 |
}
|
12 |
|
13 |
model Model {
|
14 |
-
id
|
15 |
-
createdAt
|
16 |
-
repo
|
17 |
-
title
|
18 |
// trigger_word String?
|
19 |
-
image
|
20 |
// weights String?
|
21 |
-
likes
|
22 |
-
downloads
|
23 |
-
isPublic
|
|
|
24 |
}
|
|
|
11 |
}
|
12 |
|
13 |
model Model {
|
14 |
+
id String @id @default(uuid())
|
15 |
+
createdAt DateTime @default(now())
|
16 |
+
repo String @unique
|
17 |
+
title String
|
18 |
// trigger_word String?
|
19 |
+
image String
|
20 |
// weights String?
|
21 |
+
likes Int?
|
22 |
+
downloads Int?
|
23 |
+
isPublic Boolean @default(false)
|
24 |
+
hf_user_id String?
|
25 |
}
|
src/lib/components/Button.svelte
CHANGED
@@ -6,6 +6,7 @@ import { goto } from '$app/navigation';
|
|
6 |
export let size: "md" | "lg" = "md";
|
7 |
export let href: string | undefined = undefined;
|
8 |
export let icon: string | undefined = undefined;
|
|
|
9 |
export let iconPosition: "left" | "right" = "left";
|
10 |
export let disabled: boolean = false;
|
11 |
export let loading: boolean = false;
|
@@ -13,7 +14,8 @@ import { goto } from '$app/navigation';
|
|
13 |
|
14 |
const handleClick = async () => {
|
15 |
if (href) {
|
16 |
-
|
|
|
17 |
return
|
18 |
}
|
19 |
if (disabled || loading) return;
|
|
|
6 |
export let size: "md" | "lg" = "md";
|
7 |
export let href: string | undefined = undefined;
|
8 |
export let icon: string | undefined = undefined;
|
9 |
+
export let target: "_blank" | "_self" | undefined = undefined;
|
10 |
export let iconPosition: "left" | "right" = "left";
|
11 |
export let disabled: boolean = false;
|
12 |
export let loading: boolean = false;
|
|
|
14 |
|
15 |
const handleClick = async () => {
|
16 |
if (href) {
|
17 |
+
if (target) window.open(href, target);
|
18 |
+
else goto(href);
|
19 |
return
|
20 |
}
|
21 |
if (disabled || loading) return;
|
src/lib/components/GoTop.svelte
CHANGED
@@ -7,15 +7,24 @@
|
|
7 |
element?.scrollTo({ top: 0, behavior: 'smooth' });
|
8 |
}
|
9 |
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
</script>
|
13 |
|
14 |
<button
|
15 |
-
class="rounded-full
|
16 |
class:opacity-0={!visible}
|
17 |
class:pointer-events-none={!visible}
|
18 |
on:click={goTop}
|
19 |
-
>
|
20 |
-
<Icon icon="foundation:arrow-up" class="w-
|
|
|
21 |
</button>
|
|
|
7 |
element?.scrollTo({ top: 0, behavior: 'smooth' });
|
8 |
}
|
9 |
|
10 |
+
let visible = false;
|
11 |
+
|
12 |
+
if (browser) {
|
13 |
+
const element = document.getElementById('app');
|
14 |
+
element?.addEventListener('scroll', () => {
|
15 |
+
const scroll = element?.scrollTop ?? 0;
|
16 |
+
visible = scroll > 100;
|
17 |
+
});
|
18 |
+
}
|
19 |
+
|
20 |
</script>
|
21 |
|
22 |
<button
|
23 |
+
class="rounded-full text-sm text-neutral-950 font-semibold px-3 py-1 bg-white shadow-lg brightness-90 transition-all duration-200 hover:brightness-110 fixed bottom-8 right-8 flex items-center gap-1.5 justify-center z-10"
|
24 |
class:opacity-0={!visible}
|
25 |
class:pointer-events-none={!visible}
|
26 |
on:click={goTop}
|
27 |
+
>
|
28 |
+
<Icon icon="foundation:arrow-up" class="w-4 h-4" />
|
29 |
+
Go back to top
|
30 |
</button>
|
src/lib/components/community/Card.svelte
CHANGED
@@ -4,7 +4,6 @@
|
|
4 |
import type { CommunityCard } from "$lib/type";
|
5 |
|
6 |
export let card: CommunityCard;
|
7 |
-
|
8 |
</script>
|
9 |
|
10 |
<div
|
@@ -21,6 +20,6 @@
|
|
21 |
{#each card.reactions as reaction}
|
22 |
<Reaction emoji={reaction.emoji} count={reaction?.users?.length} />
|
23 |
{/each}
|
24 |
-
<Add />
|
25 |
</div>
|
26 |
</div>
|
|
|
4 |
import type { CommunityCard } from "$lib/type";
|
5 |
|
6 |
export let card: CommunityCard;
|
|
|
7 |
</script>
|
8 |
|
9 |
<div
|
|
|
20 |
{#each card.reactions as reaction}
|
21 |
<Reaction emoji={reaction.emoji} count={reaction?.users?.length} />
|
22 |
{/each}
|
23 |
+
<Add count={card?.reactions?.length} />
|
24 |
</div>
|
25 |
</div>
|
src/lib/components/community/reactions/Add.svelte
CHANGED
@@ -3,6 +3,8 @@
|
|
3 |
import Icon from "@iconify/svelte";
|
4 |
import { REACTION_EMOJIS } from "$lib/utils";
|
5 |
|
|
|
|
|
6 |
let isOpen: boolean = false;
|
7 |
$: uuid = Math.random().toString(36).substring(7);
|
8 |
|
@@ -30,12 +32,12 @@
|
|
30 |
<Icon icon="fluent:emoji-add-16-regular" class="w-5 h-5" />
|
31 |
</button>
|
32 |
<div
|
33 |
-
class=
|
34 |
class:opacity-100={isOpen}
|
35 |
class:pointer-events-auto={isOpen}
|
36 |
>
|
37 |
{#each REACTION_EMOJIS as emoji}
|
38 |
-
<div class="w-
|
39 |
{/each}
|
40 |
</div>
|
41 |
</div>
|
|
|
3 |
import Icon from "@iconify/svelte";
|
4 |
import { REACTION_EMOJIS } from "$lib/utils";
|
5 |
|
6 |
+
export let count: number;
|
7 |
+
|
8 |
let isOpen: boolean = false;
|
9 |
$: uuid = Math.random().toString(36).substring(7);
|
10 |
|
|
|
32 |
<Icon icon="fluent:emoji-add-16-regular" class="w-5 h-5" />
|
33 |
</button>
|
34 |
<div
|
35 |
+
class={`opacity-0 pointer-events-none absolute max-w-max flex items-center justify-center bg-white px-1 py-1 rounded-full gap-0 text-xl ${count > 0 ? "-translate-y-[calc(100%+8px)] -translate-x-1/2 left-0 top-0" : "right-0 translate-x-[calc(100%+8px)]"}`}
|
36 |
class:opacity-100={isOpen}
|
37 |
class:pointer-events-auto={isOpen}
|
38 |
>
|
39 |
{#each REACTION_EMOJIS as emoji}
|
40 |
+
<div class="w-8 h-8 hover:bg-neutral-200 rounded-full text-center flex items-center justify-center">{emoji}</div>
|
41 |
{/each}
|
42 |
</div>
|
43 |
</div>
|
src/lib/components/models/Card.svelte
CHANGED
@@ -14,7 +14,7 @@
|
|
14 |
<!-- <div class="w-full h-full bg-center bg-cover rounded-lg bg-neutral-800 relative" style=""> -->
|
15 |
<!-- <Loading /> -->
|
16 |
<!-- </div> -->
|
17 |
-
<img src="{card.image}" class="w-full h-full bg-center bg-cover rounded-lg object-cover object-center" alt="{card?.title}" />
|
18 |
<div class="group-hover:opacity-100 opacity-0 translate-x-full group-hover:translate-x-0 transition-all duration-200 absolute right-3 bottom-3">
|
19 |
<Button theme="light" size="md">
|
20 |
Try it now
|
|
|
14 |
<!-- <div class="w-full h-full bg-center bg-cover rounded-lg bg-neutral-800 relative" style=""> -->
|
15 |
<!-- <Loading /> -->
|
16 |
<!-- </div> -->
|
17 |
+
<img src="{card.image}" class="w-full h-full bg-center bg-cover rounded-lg object-cover object-center bg-neutral-800" alt="{card?.title}" />
|
18 |
<div class="group-hover:opacity-100 opacity-0 translate-x-full group-hover:translate-x-0 transition-all duration-200 absolute right-3 bottom-3">
|
19 |
<Button theme="light" size="md">
|
20 |
Try it now
|
src/lib/components/models/Submit.svelte
CHANGED
@@ -1,12 +1,59 @@
|
|
1 |
<script>
|
|
|
|
|
|
|
2 |
import Input from "$lib/components/fields/Input.svelte";
|
3 |
import Button from "$lib/components/Button.svelte";
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
</script>
|
6 |
<div class="grid grid-cols-1 gap-8">
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
<header>
|
11 |
<p class="text-white font-semibold text-lg">
|
12 |
Submit a Model
|
@@ -17,16 +64,52 @@
|
|
17 |
</header>
|
18 |
<main class="grid grid-cols-1 gap-6">
|
19 |
<div>
|
20 |
-
<p class="text-xs uppercase text-neutral-400 font-semibold mb-2">
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
</div>
|
23 |
<div>
|
24 |
-
<p class="text-xs uppercase text-neutral-400 font-semibold mb-2">
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</div>
|
27 |
</main>
|
28 |
<footer class="flex items-center justify-end gap-3">
|
29 |
<Button theme="dark" size="md">Cancel</Button>
|
30 |
-
<Button theme="blue" size="md">Submit</Button>
|
31 |
</footer>
|
32 |
</div>
|
|
|
1 |
<script>
|
2 |
+
import { get } from 'svelte/store';
|
3 |
+
import { userStore } from "$lib/stores/use-user";
|
4 |
+
|
5 |
import Input from "$lib/components/fields/Input.svelte";
|
6 |
import Button from "$lib/components/Button.svelte";
|
7 |
|
8 |
+
let user = get(userStore);
|
9 |
+
let model = {
|
10 |
+
repo: 'enzostvs/hair-colorrr',
|
11 |
+
title: 'testt',
|
12 |
+
image: 'dewdwedd',
|
13 |
+
}
|
14 |
+
let error = {
|
15 |
+
repo: '',
|
16 |
+
title: '',
|
17 |
+
image: ''
|
18 |
+
}
|
19 |
+
|
20 |
+
const handleSubmit = async () => {
|
21 |
+
fetch('/api/models/submit', {
|
22 |
+
method: 'POST',
|
23 |
+
headers: {
|
24 |
+
'Content-Type': 'application/json',
|
25 |
+
},
|
26 |
+
body: JSON.stringify(model),
|
27 |
+
})
|
28 |
+
.then(response => response.json())
|
29 |
+
.then(data => {
|
30 |
+
if (data.error) {
|
31 |
+
error = data.error;
|
32 |
+
} else {
|
33 |
+
console.log('Success:', data);
|
34 |
+
error = {
|
35 |
+
repo: '',
|
36 |
+
title: '',
|
37 |
+
image: ''
|
38 |
+
}
|
39 |
+
}
|
40 |
+
})
|
41 |
+
}
|
42 |
</script>
|
43 |
<div class="grid grid-cols-1 gap-8">
|
44 |
+
{#if user?.picture}
|
45 |
+
<div class="flex items-center justify-start gap-3">
|
46 |
+
<img src={user.picture} alt="User avatar" class="w-8 h-8 rounded-full border border-white inline-block" />
|
47 |
+
<div class="w-full text-left text-white">
|
48 |
+
<p class="text-base font-semibold">{user.name}</p>
|
49 |
+
<p class="text-xs leading-none text-neutral-400">{user.preferred_username}</p>
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
{:else}
|
53 |
+
<p class="bg-yellow-500/40 rounded-full text-xs text-yellow-400 px-3 py-1 font-semibold max-w-max">
|
54 |
+
You need to be logged in to submit a model.
|
55 |
+
</p>
|
56 |
+
{/if}
|
57 |
<header>
|
58 |
<p class="text-white font-semibold text-lg">
|
59 |
Submit a Model
|
|
|
64 |
</header>
|
65 |
<main class="grid grid-cols-1 gap-6">
|
66 |
<div>
|
67 |
+
<p class="text-xs uppercase text-neutral-400 font-semibold mb-2">
|
68 |
+
HuggingFace model URL
|
69 |
+
<span class="text-red-500">*</span>
|
70 |
+
</p>
|
71 |
+
<Input
|
72 |
+
value={model.repo}
|
73 |
+
placeholder="enzostvs/hair-color"
|
74 |
+
prefix="huggingface.co/"
|
75 |
+
onChange={(value) => model.repo = value}
|
76 |
+
/>
|
77 |
+
{#if error.repo}
|
78 |
+
<p class="text-xs text-red-500 mt-1">
|
79 |
+
{error.repo}
|
80 |
+
</p>
|
81 |
+
{/if}
|
82 |
+
</div>
|
83 |
+
<div>
|
84 |
+
<p class="text-xs uppercase text-neutral-400 font-semibold mb-2">
|
85 |
+
Title
|
86 |
+
<span class="text-red-500">*</span>
|
87 |
+
</p>
|
88 |
+
<Input
|
89 |
+
value={model.title}
|
90 |
+
placeholder="Simpson style"
|
91 |
+
onChange={(value) => model.title = value}
|
92 |
+
/>
|
93 |
</div>
|
94 |
<div>
|
95 |
+
<p class="text-xs uppercase text-neutral-400 font-semibold mb-2">
|
96 |
+
Thumbnail image
|
97 |
+
<span class="text-red-500">*</span>
|
98 |
+
</p>
|
99 |
+
<Input
|
100 |
+
value={model.image}
|
101 |
+
placeholder="https://"
|
102 |
+
onChange={(value) => model.image = value}
|
103 |
+
/>
|
104 |
+
{#if error.image}
|
105 |
+
<p class="text-xs text-red-500 mt-1">
|
106 |
+
{error.image}
|
107 |
+
</p>
|
108 |
+
{/if}
|
109 |
</div>
|
110 |
</main>
|
111 |
<footer class="flex items-center justify-end gap-3">
|
112 |
<Button theme="dark" size="md">Cancel</Button>
|
113 |
+
<Button theme="blue" size="md" onClick={handleSubmit}>Submit</Button>
|
114 |
</footer>
|
115 |
</div>
|
src/lib/components/sidebar/Menu.svelte
CHANGED
@@ -3,7 +3,6 @@
|
|
3 |
|
4 |
export let href: string;
|
5 |
|
6 |
-
|
7 |
$: active_class = $page.url.pathname === href ? 'bg-neutral-900 !border-neutral-800' : '';
|
8 |
</script>
|
9 |
|
|
|
3 |
|
4 |
export let href: string;
|
5 |
|
|
|
6 |
$: active_class = $page.url.pathname === href ? 'bg-neutral-900 !border-neutral-800' : '';
|
7 |
</script>
|
8 |
|
src/lib/components/sidebar/Sidebar.svelte
CHANGED
@@ -1,10 +1,15 @@
|
|
1 |
<script lang="ts">
|
|
|
2 |
import Icon from "@iconify/svelte"
|
|
|
|
|
|
|
|
|
3 |
|
4 |
import Menu from "./Menu.svelte";
|
5 |
-
import HFLogo from "$lib/assets/hf-logo.svg";
|
6 |
|
7 |
let isOpen = false;
|
|
|
8 |
|
9 |
const handleClick = () => {
|
10 |
const app = document.getElementById("app");
|
@@ -14,19 +19,10 @@
|
|
14 |
isOpen = !isOpen;
|
15 |
}
|
16 |
|
17 |
-
const
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
}, {
|
22 |
-
icon: "uim:cube",
|
23 |
-
label: "Models",
|
24 |
-
href: "/models",
|
25 |
-
}, {
|
26 |
-
icon: "fluent:glance-horizontal-sparkles-16-filled",
|
27 |
-
label: "Generate",
|
28 |
-
href: "/generate",
|
29 |
-
}]
|
30 |
</script>
|
31 |
|
32 |
<button class="bg-transparent absolute top-10 right-8 cursor-pointer xl:hidden" on:click="{handleClick}">
|
@@ -39,7 +35,7 @@
|
|
39 |
</header>
|
40 |
<div class="px-4">
|
41 |
<ul class="grid grid-cols-1 gap-2">
|
42 |
-
{#each
|
43 |
<Menu href={menu.href}>
|
44 |
<Icon icon={menu.icon} class="w-5 h-5" />
|
45 |
{menu.label}
|
@@ -54,8 +50,23 @@
|
|
54 |
</Menu>
|
55 |
</div>
|
56 |
</div>
|
57 |
-
|
58 |
-
<
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
</aside>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import cookies from 'js-cookie';
|
3 |
import Icon from "@iconify/svelte"
|
4 |
+
import { get } from 'svelte/store';
|
5 |
+
import { userStore } from "$lib/stores/use-user";
|
6 |
+
import { SIDEBAR_MENUS } from "$lib/utils";
|
7 |
+
import HFLogo from "$lib/assets/hf-logo.svg";
|
8 |
|
9 |
import Menu from "./Menu.svelte";
|
|
|
10 |
|
11 |
let isOpen = false;
|
12 |
+
let user = get(userStore);
|
13 |
|
14 |
const handleClick = () => {
|
15 |
const app = document.getElementById("app");
|
|
|
19 |
isOpen = !isOpen;
|
20 |
}
|
21 |
|
22 |
+
const logout = async () => {
|
23 |
+
cookies.remove("hf_access_token");
|
24 |
+
window.location.href = "/";
|
25 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</script>
|
27 |
|
28 |
<button class="bg-transparent absolute top-10 right-8 cursor-pointer xl:hidden" on:click="{handleClick}">
|
|
|
35 |
</header>
|
36 |
<div class="px-4">
|
37 |
<ul class="grid grid-cols-1 gap-2">
|
38 |
+
{#each SIDEBAR_MENUS as menu}
|
39 |
<Menu href={menu.href}>
|
40 |
<Icon icon={menu.icon} class="w-5 h-5" />
|
41 |
{menu.label}
|
|
|
50 |
</Menu>
|
51 |
</div>
|
52 |
</div>
|
53 |
+
{#if user?.picture}
|
54 |
+
<footer class="text-white text-center text-base pb-8 px-8 flex items-center justify-between gap-4">
|
55 |
+
<div class="flex items-center justify-start gap-4">
|
56 |
+
<img src={user.picture} alt="User avatar" class="w-10 h-10 rounded-full border-2 border-white inline-block" />
|
57 |
+
<div class="w-full text-left">
|
58 |
+
<p class="text-lg font-semibold">{user.name}</p>
|
59 |
+
<p class="text-sm leading-none text-neutral-400">{user.preferred_username}</p>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
<button on:click={logout}>
|
63 |
+
<Icon icon="solar:logout-2-bold" class="text-red-500 hover:text-red-400 w-7 h-7" />
|
64 |
+
</button>
|
65 |
+
</footer>
|
66 |
+
{:else}
|
67 |
+
<footer class="text-white text-center text-base pb-8 px-8 flex items-center justify-center gap-2 cursor-pointer">
|
68 |
+
<img src={HFLogo} alt="Hugging Face logo" class="w-8 h-8 inline-block" />
|
69 |
+
<u>Sign in with Hugging Face</u>
|
70 |
+
</footer>
|
71 |
+
{/if}
|
72 |
</aside>
|
src/lib/stores/use-user.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { writable } from "svelte/store";
|
2 |
+
|
3 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
4 |
+
export const userStore = writable<any>(null);
|
src/lib/utils/index.ts
CHANGED
@@ -28,4 +28,30 @@ export const MODELS_FILTER_OPTIONS = [
|
|
28 |
icon: "ph:fire-bold",
|
29 |
iconColor: "text-orange-500"
|
30 |
},
|
31 |
-
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
icon: "ph:fire-bold",
|
29 |
iconColor: "text-orange-500"
|
30 |
},
|
31 |
+
];
|
32 |
+
|
33 |
+
export const SIDEBAR_MENUS = [{
|
34 |
+
icon: "solar:gallery-bold-duotone",
|
35 |
+
label: "Gallery",
|
36 |
+
href: "/",
|
37 |
+
}, {
|
38 |
+
icon: "uim:cube",
|
39 |
+
label: "Models",
|
40 |
+
href: "/models",
|
41 |
+
}, {
|
42 |
+
icon: "fluent:glance-horizontal-sparkles-16-filled",
|
43 |
+
label: "Generate",
|
44 |
+
href: "/generate",
|
45 |
+
}]
|
46 |
+
|
47 |
+
export const tokenIsAvailable = async (token: string) => {
|
48 |
+
const userRequest = await fetch("https://huggingface.co/oauth/userinfo", {
|
49 |
+
method: "GET",
|
50 |
+
headers: {
|
51 |
+
Authorization: `Bearer ${token}`,
|
52 |
+
},
|
53 |
+
})
|
54 |
+
|
55 |
+
const user = await userRequest.clone().json().catch(() => ({}));
|
56 |
+
return !!user?.sub
|
57 |
+
}
|
src/routes/+layout.server.ts
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export async function load({ fetch }) {
|
2 |
+
const response = await fetch("/api/@me", {
|
3 |
+
method: "GET",
|
4 |
+
headers: {
|
5 |
+
"Content-Type": "application/json"
|
6 |
+
}
|
7 |
+
})
|
8 |
+
const user = await response.json()
|
9 |
+
return user
|
10 |
+
}
|
src/routes/+layout.svelte
CHANGED
@@ -1,6 +1,10 @@
|
|
1 |
<script>
|
2 |
import Sidebar from "$lib/components/sidebar/Sidebar.svelte";
|
3 |
import "$lib/styles/tailwind.css"
|
|
|
|
|
|
|
|
|
4 |
</script>
|
5 |
|
6 |
<div class="flex items-start">
|
|
|
1 |
<script>
|
2 |
import Sidebar from "$lib/components/sidebar/Sidebar.svelte";
|
3 |
import "$lib/styles/tailwind.css"
|
4 |
+
import { userStore } from "$lib/stores/use-user";
|
5 |
+
|
6 |
+
export let data;
|
7 |
+
userStore.set(data.user);
|
8 |
</script>
|
9 |
|
10 |
<div class="flex items-start">
|
src/routes/api/@me/+server.ts
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { json, type RequestEvent } from '@sveltejs/kit';
|
2 |
+
|
3 |
+
/** @type {import('./$types').RequestHandler} */
|
4 |
+
|
5 |
+
export async function GET(request : RequestEvent) {
|
6 |
+
if (!request.cookies.get('hf_access_token')) {
|
7 |
+
return json({
|
8 |
+
error: {
|
9 |
+
token: "You must be logged"
|
10 |
+
}
|
11 |
+
}, { status: 401 })
|
12 |
+
}
|
13 |
+
|
14 |
+
const response = await fetch("https://huggingface.co/oauth/userinfo", {
|
15 |
+
method: "GET",
|
16 |
+
headers: {
|
17 |
+
Authorization: `Bearer ${request.cookies.get('hf_access_token')}`,
|
18 |
+
},
|
19 |
+
})
|
20 |
+
|
21 |
+
const user = await response.clone().json().catch(() => ({}));
|
22 |
+
|
23 |
+
if (!user?.sub) {
|
24 |
+
return json({
|
25 |
+
error: {
|
26 |
+
token: "Token is invalid"
|
27 |
+
}
|
28 |
+
}, { status: 401 })
|
29 |
+
}
|
30 |
+
|
31 |
+
return json({
|
32 |
+
user
|
33 |
+
})
|
34 |
+
}
|
src/routes/api/models/+server.ts
CHANGED
@@ -8,7 +8,6 @@ import prisma from '$lib/prisma';
|
|
8 |
// refer to bulk-create-models +server.ts for example
|
9 |
|
10 |
export async function GET(request : RequestEvent) {
|
11 |
-
|
12 |
const page = parseInt(request.url.searchParams.get('page') || '0')
|
13 |
const filter = request.url.searchParams.get('filter') || 'hotest'
|
14 |
const search = request.url.searchParams.get('search') || ''
|
@@ -28,7 +27,15 @@ export async function GET(request : RequestEvent) {
|
|
28 |
take: 20,
|
29 |
})
|
30 |
|
31 |
-
const total_reposId = await prisma.model.count(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
const hasError = false
|
34 |
if (hasError) {
|
|
|
8 |
// refer to bulk-create-models +server.ts for example
|
9 |
|
10 |
export async function GET(request : RequestEvent) {
|
|
|
11 |
const page = parseInt(request.url.searchParams.get('page') || '0')
|
12 |
const filter = request.url.searchParams.get('filter') || 'hotest'
|
13 |
const search = request.url.searchParams.get('search') || ''
|
|
|
27 |
take: 20,
|
28 |
})
|
29 |
|
30 |
+
const total_reposId = await prisma.model.count({
|
31 |
+
where: {
|
32 |
+
isPublic: true,
|
33 |
+
OR: [
|
34 |
+
{ title: { contains: search } },
|
35 |
+
{ repo: { contains: search } },
|
36 |
+
]
|
37 |
+
},
|
38 |
+
})
|
39 |
|
40 |
const hasError = false
|
41 |
if (hasError) {
|
src/routes/api/models/submit/+server.ts
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { json } from '@sveltejs/kit';
|
2 |
+
import prisma from '$lib/prisma';
|
3 |
+
|
4 |
+
import { tokenIsAvailable } from '$lib/utils';
|
5 |
+
|
6 |
+
/** @type {import('./$types').RequestHandler} */
|
7 |
+
|
8 |
+
export async function POST({ request, fetch, cookies }) {
|
9 |
+
const model = await request.json();
|
10 |
+
|
11 |
+
const token = cookies.get('hf_access_token')
|
12 |
+
if (!token) {
|
13 |
+
return json({
|
14 |
+
error: {
|
15 |
+
token: "You must be logged"
|
16 |
+
}
|
17 |
+
}, { status: 401 })
|
18 |
+
}
|
19 |
+
|
20 |
+
const is_token_available = await tokenIsAvailable(token)
|
21 |
+
if (!is_token_available) {
|
22 |
+
return json({
|
23 |
+
error: {
|
24 |
+
token: "Invalid token"
|
25 |
+
}
|
26 |
+
}, { status: 401 })
|
27 |
+
}
|
28 |
+
|
29 |
+
// get model on hugging face
|
30 |
+
const res = await fetch(`https://huggingface.co/api/models/${model.repo}`)
|
31 |
+
const data = await res.json();
|
32 |
+
|
33 |
+
if (data?.error) {
|
34 |
+
return json({
|
35 |
+
error: {
|
36 |
+
repo: "Model not found on Hugging Face"
|
37 |
+
}
|
38 |
+
}, { status: 404 })
|
39 |
+
}
|
40 |
+
|
41 |
+
// check model.image is valid url and is an image
|
42 |
+
const imageRes = await fetch(model.image)
|
43 |
+
const imageBlob = await imageRes.blob()
|
44 |
+
const isImage = imageBlob.type.startsWith("image/")
|
45 |
+
const isValidUrl = imageRes.status === 200
|
46 |
+
|
47 |
+
if (!isImage || !isValidUrl) {
|
48 |
+
return json({
|
49 |
+
error: {
|
50 |
+
image: "Invalid image url"
|
51 |
+
}
|
52 |
+
}, { status: 400 })
|
53 |
+
}
|
54 |
+
|
55 |
+
await prisma.model.create({
|
56 |
+
data: {
|
57 |
+
repo: model.repo,
|
58 |
+
image: model.image,
|
59 |
+
title: model.title,
|
60 |
+
likes: data.likes,
|
61 |
+
downloads: data.downloads,
|
62 |
+
isPublic: false,
|
63 |
+
}
|
64 |
+
})
|
65 |
+
|
66 |
+
return json({
|
67 |
+
success: true
|
68 |
+
})
|
69 |
+
}
|
src/routes/models/+page.svelte
CHANGED
@@ -58,7 +58,7 @@
|
|
58 |
<div class="flex items-start sm:items-center justify-between mt-5 flex-col sm:flex-row gap-5 sm:justify-between">
|
59 |
<Radio options={MODELS_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
|
60 |
<div class="items-center justify-end gap-5 hidden lg:flex">
|
61 |
-
<Button icon="ic:round-plus" theme="dark" size="lg">Create</Button>
|
62 |
<Button
|
63 |
icon="octicon:upload-16"
|
64 |
theme="blue"
|
@@ -69,7 +69,7 @@
|
|
69 |
</Button>
|
70 |
</div>
|
71 |
<div class="items-center justify-end gap-3 flex lg:hidden">
|
72 |
-
<Button icon="ic:round-plus" theme="dark" size="md">Create</Button>
|
73 |
<Button
|
74 |
icon="octicon:upload-16"
|
75 |
theme="blue"
|
|
|
58 |
<div class="flex items-start sm:items-center justify-between mt-5 flex-col sm:flex-row gap-5 sm:justify-between">
|
59 |
<Radio options={MODELS_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
|
60 |
<div class="items-center justify-end gap-5 hidden lg:flex">
|
61 |
+
<Button href="https://huggingface.co/new" target="_blank" icon="ic:round-plus" theme="dark" size="lg">Create</Button>
|
62 |
<Button
|
63 |
icon="octicon:upload-16"
|
64 |
theme="blue"
|
|
|
69 |
</Button>
|
70 |
</div>
|
71 |
<div class="items-center justify-end gap-3 flex lg:hidden">
|
72 |
+
<Button href="https://huggingface.co/new" target="_blank" icon="ic:round-plus" theme="dark" size="md">Create</Button>
|
73 |
<Button
|
74 |
icon="octicon:upload-16"
|
75 |
theme="blue"
|