Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
refacto models + community
Browse files- package-lock.json +17 -0
- package.json +1 -0
- prisma/dev.db +0 -0
- prisma/migrations/20240104191224_init/migration.sql +0 -14
- prisma/migrations/20240111171434_/migration.sql +31 -0
- prisma/schema.prisma +24 -6
- src/lib/assets/banner.webp +0 -0
- src/lib/assets/sparkles.svg +4 -0
- src/lib/components/Button.svelte +14 -7
- src/lib/components/Loading.svelte +2 -2
- src/lib/components/community/Card.svelte +8 -8
- src/lib/components/community/reactions/Reactions.svelte +22 -0
- src/lib/components/fields/Textarea.svelte +18 -0
- src/lib/components/generate/Banner.svelte +31 -0
- src/lib/components/generate/Response.svelte +93 -0
- src/lib/components/models/Card.svelte +3 -3
- src/lib/components/models/Submit.svelte +7 -7
- src/lib/components/models/autocomplete/Autocomplete.svelte +14 -4
- src/lib/components/models/autocomplete/Item.svelte +1 -1
- src/lib/type.ts +5 -3
- src/lib/utils/index.ts +6 -6
- src/lib/utils/uploader.ts +32 -0
- src/routes/+layout.server.ts +1 -1
- src/routes/+layout.svelte +1 -1
- src/routes/+page.svelte +74 -36
- src/routes/+page.ts +1 -1
- src/routes/api/bulk-create-models/+server.ts +7 -10
- src/routes/api/community/+server.ts +36 -11
- src/routes/api/generate/+server.ts +55 -0
- src/routes/api/generate/share/+server.ts +68 -0
- src/routes/api/models/+server.ts +3 -12
- src/routes/api/models/{[repo] β [id]}/+server.ts +2 -2
- src/routes/api/models/submit/+server.ts +3 -3
- src/routes/{models β gallery}/+page.svelte +37 -54
- src/routes/{models β gallery}/+page.ts +1 -1
- src/routes/generate/+page.svelte +74 -10
package-lock.json
CHANGED
@@ -8,6 +8,7 @@
|
|
8 |
"name": "loras-explorer",
|
9 |
"version": "0.0.1",
|
10 |
"dependencies": {
|
|
|
11 |
"@iconify/svelte": "^3.1.4",
|
12 |
"@prisma/client": "^5.7.1",
|
13 |
"@svelte-put/clickoutside": "^3.0.1",
|
@@ -486,6 +487,17 @@
|
|
486 |
"integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
|
487 |
"dev": true
|
488 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
489 |
"node_modules/@humanwhocodes/config-array": {
|
490 |
"version": "0.11.13",
|
491 |
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
@@ -2719,6 +2731,11 @@
|
|
2719 |
"node": ">=8"
|
2720 |
}
|
2721 |
},
|
|
|
|
|
|
|
|
|
|
|
2722 |
"node_modules/hasown": {
|
2723 |
"version": "2.0.0",
|
2724 |
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
|
|
8 |
"name": "loras-explorer",
|
9 |
"version": "0.0.1",
|
10 |
"dependencies": {
|
11 |
+
"@huggingface/hub": "^0.12.3-oauth",
|
12 |
"@iconify/svelte": "^3.1.4",
|
13 |
"@prisma/client": "^5.7.1",
|
14 |
"@svelte-put/clickoutside": "^3.0.1",
|
|
|
487 |
"integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
|
488 |
"dev": true
|
489 |
},
|
490 |
+
"node_modules/@huggingface/hub": {
|
491 |
+
"version": "0.12.3-oauth",
|
492 |
+
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.12.3-oauth.tgz",
|
493 |
+
"integrity": "sha512-9LE4Ded2VsjQTK/rxsIF/nvhkxs1UE+lcpNpuBxnTGNpaVzbO6HIlAtbThgNenKPplRt1iDkn82AQYBco1QHug==",
|
494 |
+
"dependencies": {
|
495 |
+
"hash-wasm": "^4.9.0"
|
496 |
+
},
|
497 |
+
"engines": {
|
498 |
+
"node": ">=18"
|
499 |
+
}
|
500 |
+
},
|
501 |
"node_modules/@humanwhocodes/config-array": {
|
502 |
"version": "0.11.13",
|
503 |
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
|
|
2731 |
"node": ">=8"
|
2732 |
}
|
2733 |
},
|
2734 |
+
"node_modules/hash-wasm": {
|
2735 |
+
"version": "4.11.0",
|
2736 |
+
"resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.11.0.tgz",
|
2737 |
+
"integrity": "sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ=="
|
2738 |
+
},
|
2739 |
"node_modules/hasown": {
|
2740 |
"version": "2.0.0",
|
2741 |
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
package.json
CHANGED
@@ -38,6 +38,7 @@
|
|
38 |
},
|
39 |
"type": "module",
|
40 |
"dependencies": {
|
|
|
41 |
"@iconify/svelte": "^3.1.4",
|
42 |
"@prisma/client": "^5.7.1",
|
43 |
"@svelte-put/clickoutside": "^3.0.1",
|
|
|
38 |
},
|
39 |
"type": "module",
|
40 |
"dependencies": {
|
41 |
+
"@huggingface/hub": "^0.12.3-oauth",
|
42 |
"@iconify/svelte": "^3.1.4",
|
43 |
"@prisma/client": "^5.7.1",
|
44 |
"@svelte-put/clickoutside": "^3.0.1",
|
prisma/dev.db
CHANGED
Binary files a/prisma/dev.db and b/prisma/dev.db differ
|
|
prisma/migrations/20240104191224_init/migration.sql
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
-- CreateTable
|
2 |
-
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 NOT NULL,
|
7 |
-
"image" TEXT,
|
8 |
-
"likes" INTEGER,
|
9 |
-
"downloads" INTEGER,
|
10 |
-
"isPublic" BOOLEAN NOT NULL DEFAULT false
|
11 |
-
);
|
12 |
-
|
13 |
-
-- CreateIndex
|
14 |
-
CREATE UNIQUE INDEX "Model_repo_key" ON "Model"("repo");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prisma/migrations/20240111171434_/migration.sql
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-- CreateTable
|
2 |
+
CREATE TABLE "Model" (
|
3 |
+
"id" TEXT NOT NULL PRIMARY KEY,
|
4 |
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
5 |
+
"title" TEXT NOT NULL,
|
6 |
+
"image" TEXT NOT NULL,
|
7 |
+
"likes" INTEGER,
|
8 |
+
"downloads" INTEGER,
|
9 |
+
"isPublic" BOOLEAN NOT NULL DEFAULT false,
|
10 |
+
"hf_user_id" TEXT
|
11 |
+
);
|
12 |
+
|
13 |
+
-- CreateTable
|
14 |
+
CREATE TABLE "Gallery" (
|
15 |
+
"id" TEXT NOT NULL PRIMARY KEY,
|
16 |
+
"hf_user_id" TEXT,
|
17 |
+
"prompt" TEXT NOT NULL,
|
18 |
+
"image" TEXT NOT NULL,
|
19 |
+
"modelId" TEXT NOT NULL,
|
20 |
+
CONSTRAINT "Gallery_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
21 |
+
);
|
22 |
+
|
23 |
+
-- CreateTable
|
24 |
+
CREATE TABLE "Reaction" (
|
25 |
+
"id" TEXT NOT NULL PRIMARY KEY,
|
26 |
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
27 |
+
"emoji" TEXT NOT NULL,
|
28 |
+
"hf_user_id" TEXT,
|
29 |
+
"galleryId" TEXT,
|
30 |
+
CONSTRAINT "Reaction_galleryId_fkey" FOREIGN KEY ("galleryId") REFERENCES "Gallery" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
31 |
+
);
|
prisma/schema.prisma
CHANGED
@@ -11,15 +11,33 @@ datasource db {
|
|
11 |
}
|
12 |
|
13 |
model Model {
|
14 |
-
id String
|
15 |
-
createdAt DateTime
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
hf_user_id String?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
|
|
11 |
}
|
12 |
|
13 |
model Model {
|
14 |
+
id String @id
|
15 |
+
createdAt DateTime @default(now())
|
|
|
16 |
title String
|
|
|
17 |
image String
|
|
|
18 |
likes Int?
|
19 |
downloads Int?
|
20 |
+
isPublic Boolean @default(false)
|
21 |
+
hf_user_id String?
|
22 |
+
Gallery Gallery[]
|
23 |
+
}
|
24 |
+
|
25 |
+
model Gallery {
|
26 |
+
id String @id @default(uuid())
|
27 |
hf_user_id String?
|
28 |
+
createdAt DateTime @default(now())
|
29 |
+
prompt String
|
30 |
+
image String
|
31 |
+
reactions Reaction[]
|
32 |
+
model Model @relation(fields: [modelId], references: [id])
|
33 |
+
modelId String
|
34 |
+
}
|
35 |
+
|
36 |
+
model Reaction {
|
37 |
+
id String @id @default(uuid())
|
38 |
+
createdAt DateTime @default(now())
|
39 |
+
emoji String
|
40 |
+
hf_user_id String
|
41 |
+
Gallery Gallery? @relation(fields: [galleryId], references: [id])
|
42 |
+
galleryId String?
|
43 |
}
|
src/lib/assets/banner.webp
ADDED
src/lib/assets/sparkles.svg
ADDED
src/lib/components/Button.svelte
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
<script lang="ts">
|
2 |
import { goto } from '$app/navigation';
|
3 |
import Icon from "@iconify/svelte";
|
|
|
4 |
|
5 |
export let theme: "light" | "dark" | "blue" | "pink" = "light";
|
6 |
export let size: "md" | "lg" = "md";
|
@@ -24,16 +25,22 @@ import { goto } from '$app/navigation';
|
|
24 |
|
25 |
</script>
|
26 |
|
27 |
-
<button on:click={handleClick} class="button {theme} {size}">
|
28 |
{#if icon && iconPosition === "left"}
|
29 |
-
<
|
|
|
|
|
30 |
{/if}
|
31 |
-
|
32 |
-
<
|
33 |
-
{/if}
|
34 |
-
<
|
|
|
|
|
35 |
{#if icon && iconPosition === "right"}
|
36 |
-
<
|
|
|
|
|
37 |
{/if}
|
38 |
</button>
|
39 |
|
|
|
1 |
<script lang="ts">
|
2 |
import { goto } from '$app/navigation';
|
3 |
import Icon from "@iconify/svelte";
|
4 |
+
import Loading from './Loading.svelte';
|
5 |
|
6 |
export let theme: "light" | "dark" | "blue" | "pink" = "light";
|
7 |
export let size: "md" | "lg" = "md";
|
|
|
25 |
|
26 |
</script>
|
27 |
|
28 |
+
<button on:click={handleClick} class="button {theme} {size} relative whitespace-nowrap" class:!bg-neutral-400={loading} class:!border-neutral-400={loading}>
|
29 |
{#if icon && iconPosition === "left"}
|
30 |
+
<p class:opacity-0={loading}>
|
31 |
+
<Icon icon={icon} class="w-[20px] h-[20px]" />
|
32 |
+
</p>
|
33 |
{/if}
|
34 |
+
{#if loading}
|
35 |
+
<Loading />
|
36 |
+
{/if}
|
37 |
+
<p class:opacity-0={loading}>
|
38 |
+
<slot />
|
39 |
+
</p>
|
40 |
{#if icon && iconPosition === "right"}
|
41 |
+
<p class:opacity-0={loading}>
|
42 |
+
<Icon icon={icon} class="w-[20px] h-[20px]" />
|
43 |
+
</p>
|
44 |
{/if}
|
45 |
</button>
|
46 |
|
src/lib/components/Loading.svelte
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
<div class="absolute left-0 top-0 w-full h-full flex items-center justify-center flex-col">
|
2 |
<svg
|
3 |
-
class="animate-spin
|
4 |
xmlns="http://www.w3.org/2000/svg"
|
5 |
fill="none"
|
6 |
viewBox="0 0 24 24"
|
|
|
1 |
+
<div class="absolute left-0 top-0 w-full h-full flex items-center justify-center flex-col gap-3">
|
2 |
<svg
|
3 |
+
class="animate-spin h-8 w-8 text-white"
|
4 |
xmlns="http://www.w3.org/2000/svg"
|
5 |
fill="none"
|
6 |
viewBox="0 0 24 24"
|
src/lib/components/community/Card.svelte
CHANGED
@@ -1,25 +1,25 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import Reaction from "$lib/components/community/reactions/Reaction.svelte";
|
3 |
import Add from "$lib/components/community/reactions/Add.svelte";
|
4 |
import type { CommunityCard } from "$lib/type";
|
|
|
5 |
|
6 |
export let card: CommunityCard;
|
7 |
-
</script>
|
8 |
|
9 |
<div
|
10 |
class="cursor-pointer group bg-neutral-700 rounded-xl h-[310px] relative flex items-start justify-between flex-col p-5 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
|
11 |
>
|
12 |
<div class="w-full h-full absolute top-0 left-0 -z-[1] rounded-xl overflow-hidden">
|
13 |
-
<div class="w-full h-full bg-center bg-cover transition-all duration-200 group-hover:scale-110 " style="background-image: url('{card.image}');"></div>
|
14 |
</div>
|
15 |
-
<div class="bg-black/40 backdrop-blur-sm border border-white/30 rounded-lg px-6 py-3 text-white transition-all duration-200 opacity-0 group-hover:opacity-100">
|
16 |
<p class="text-white font-semibold text-base">{card.prompt}</p>
|
17 |
-
<p class="text-white/75 font-regular text-sm">{card.
|
18 |
</div>
|
19 |
<div class="flex items-center justify-start gap-2">
|
20 |
-
{#
|
21 |
-
<
|
22 |
-
{/
|
23 |
<Add count={card?.reactions?.length} />
|
24 |
</div>
|
25 |
</div>
|
|
|
1 |
<script lang="ts">
|
|
|
2 |
import Add from "$lib/components/community/reactions/Add.svelte";
|
3 |
import type { CommunityCard } from "$lib/type";
|
4 |
+
import Reactions from "./reactions/Reactions.svelte";
|
5 |
|
6 |
export let card: CommunityCard;
|
7 |
+
</script>
|
8 |
|
9 |
<div
|
10 |
class="cursor-pointer group bg-neutral-700 rounded-xl h-[310px] relative flex items-start justify-between flex-col p-5 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
|
11 |
>
|
12 |
<div class="w-full h-full absolute top-0 left-0 -z-[1] rounded-xl overflow-hidden">
|
13 |
+
<div class="w-full h-full bg-center bg-cover transition-all duration-200 group-hover:scale-110 " style="background-image: url('https://huggingface.co/datasets/enzostvs/loras-studio/resolve/main/{card.image}?expose=true');"></div>
|
14 |
</div>
|
15 |
+
<div class="bg-black/40 backdrop-blur-sm border border-white/30 rounded-lg px-6 py-3 text-white transition-all duration-200 opacity-0 group-hover:opacity-100 w-full">
|
16 |
<p class="text-white font-semibold text-base">{card.prompt}</p>
|
17 |
+
<p class="text-white/75 font-regular text-sm">{card.model.id}</p>
|
18 |
</div>
|
19 |
<div class="flex items-center justify-start gap-2">
|
20 |
+
{#if card.reactions.length > 0}
|
21 |
+
<Reactions reactions={card.reactions} />
|
22 |
+
{/if}
|
23 |
<Add count={card?.reactions?.length} />
|
24 |
</div>
|
25 |
</div>
|
src/lib/components/community/reactions/Reactions.svelte
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { ReactionType } from "$lib/type";
|
3 |
+
import Reaction from "$lib/components/community/reactions/Reaction.svelte";
|
4 |
+
|
5 |
+
export let reactions: ReactionType[] = [];
|
6 |
+
|
7 |
+
const groupReactionsByEmoji = (reactions: ReactionType[]) => {
|
8 |
+
const grouped = new Set(reactions.map((reaction) => reaction.emoji));
|
9 |
+
return Array.from(grouped).map((emoji) => {
|
10 |
+
return {
|
11 |
+
emoji,
|
12 |
+
count: reactions.filter((reaction) => reaction.emoji === emoji).length,
|
13 |
+
};
|
14 |
+
});
|
15 |
+
};
|
16 |
+
|
17 |
+
const groupedReactions = groupReactionsByEmoji(reactions);
|
18 |
+
</script>
|
19 |
+
|
20 |
+
{#each groupedReactions as reaction}
|
21 |
+
<Reaction emoji={reaction.emoji} count={reaction?.count} />
|
22 |
+
{/each}
|
src/lib/components/fields/Textarea.svelte
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let placeholder: string = "Search";
|
3 |
+
export let value: string = "";
|
4 |
+
export let onChange: (value: string) => void = () => {};
|
5 |
+
|
6 |
+
const handleChange = (event: any) => {
|
7 |
+
const target = event.target as HTMLInputElement;
|
8 |
+
onChange(target.value as string);
|
9 |
+
}
|
10 |
+
</script>
|
11 |
+
|
12 |
+
<textarea
|
13 |
+
{value}
|
14 |
+
{placeholder}
|
15 |
+
rows="5"
|
16 |
+
class="bg-neutral-900 border border-neutral-800 rounded-lg text-neutral-200 text-base outline-none border-none placeholder:text-neutral-500 w-full px-4 py-3"
|
17 |
+
on:input={handleChange}
|
18 |
+
/>
|
src/lib/components/generate/Banner.svelte
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Banner from "$lib/assets/banner.webp";
|
3 |
+
import Sparkles from "$lib/assets/sparkles.svg";
|
4 |
+
import Button from "$lib/components/Button.svelte";
|
5 |
+
</script>
|
6 |
+
|
7 |
+
<div class="w-full rounded-xl mb-8 relative p-6 xl:p-8 flex flex-col xl:flex-row items-start xl:items-center justify-between gap-4 ring-4 ring-white/20">
|
8 |
+
<div
|
9 |
+
class="bg-cover bg-center brightness-75 absolute left-0 top-0 h-full w-full rounded-xl"
|
10 |
+
style={`
|
11 |
+
background-image: url(${Banner});
|
12 |
+
background-position: 0% 0%;
|
13 |
+
`}
|
14 |
+
/>
|
15 |
+
<div>
|
16 |
+
<p class="text-xl xl:text-2xl font-extrabold text-white drop-shadow">
|
17 |
+
Share with community!
|
18 |
+
</p>
|
19 |
+
<p class="text-base font-medium text-white drop-shadow mt-1">
|
20 |
+
Once you generate an image, you can share it with the community!
|
21 |
+
<span class="hidden xl:block">
|
22 |
+
You can also see what others have generated, and like their images.
|
23 |
+
</span>
|
24 |
+
</p>
|
25 |
+
</div>
|
26 |
+
<Button href="/gallery">
|
27 |
+
See gallery
|
28 |
+
</Button>
|
29 |
+
<img src={Sparkles} alt="" class="absolute -top-4 -left-2 w-8 object-contain brightness-200 grayscale" />
|
30 |
+
<img src={Sparkles} alt="" class="absolute -rotate-180 -right-4 -bottom-2 w-8 object-contain brightness-200 grayscale" />
|
31 |
+
</div>
|
src/lib/components/generate/Response.svelte
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Button from "../Button.svelte";
|
3 |
+
|
4 |
+
export let response: string | ArrayBuffer | null = '';
|
5 |
+
export let form: { model: any, inputs: string };
|
6 |
+
|
7 |
+
let loading: boolean = false;
|
8 |
+
let already_saved: boolean = false;
|
9 |
+
|
10 |
+
const saveImage = () => {
|
11 |
+
const link = document.createElement('a');
|
12 |
+
link.href = response as string;
|
13 |
+
link.download = `${form?.inputs?.slice(0, 20)}.png`;
|
14 |
+
document.body.appendChild(link);
|
15 |
+
link.click();
|
16 |
+
document.body.removeChild(link);
|
17 |
+
}
|
18 |
+
|
19 |
+
const share = () => {
|
20 |
+
if (loading) return;
|
21 |
+
loading = true;
|
22 |
+
fetch(`/api/generate/share`, {
|
23 |
+
method: "POST",
|
24 |
+
headers: {
|
25 |
+
"Content-Type": "application/json"
|
26 |
+
},
|
27 |
+
body: JSON.stringify({ image: response, generation: form })
|
28 |
+
}).then(() => {
|
29 |
+
loading = false;
|
30 |
+
already_saved = true;
|
31 |
+
})
|
32 |
+
}
|
33 |
+
|
34 |
+
</script>
|
35 |
+
|
36 |
+
<div class="w-full border-l border-neutral-800 h-full col-span-2" class:!border-black={!response}>
|
37 |
+
{#if response}
|
38 |
+
{#if typeof response === "string"}
|
39 |
+
<img src={response} alt="Generation" class="w-full mx-auto object-contain" />
|
40 |
+
<div class="p-8 w-full">
|
41 |
+
<div class="w-full flex items-center justify-end gap-4">
|
42 |
+
<Button size="lg" theme="light" icon="material-symbols:save" iconPosition="right" onClick={saveImage}>Save</Button>
|
43 |
+
<Button
|
44 |
+
size="lg"
|
45 |
+
theme="blue"
|
46 |
+
icon="bxs:share"
|
47 |
+
iconPosition="right"
|
48 |
+
loading={loading}
|
49 |
+
disabled={loading || already_saved}
|
50 |
+
onClick={share}
|
51 |
+
>
|
52 |
+
{#if already_saved}
|
53 |
+
Shared!
|
54 |
+
{:else}
|
55 |
+
Share with community
|
56 |
+
{/if}
|
57 |
+
</Button>
|
58 |
+
</div>
|
59 |
+
<p class="text-neutral-500 text-sm text-right mt-2.5">
|
60 |
+
All images not shared with the community are deleted right after generation.
|
61 |
+
<br>
|
62 |
+
Your informations are not shared with anyone.
|
63 |
+
</p>
|
64 |
+
{#if form}
|
65 |
+
<div class="mt-6 grid grid-cols-1 gap-4">
|
66 |
+
<div>
|
67 |
+
<p class="text-neutral-400 font-semibold text-xs uppercase">
|
68 |
+
Model selected
|
69 |
+
</p>
|
70 |
+
<div class="flex items-center justify-start gap-4 px-2 py-2.5 hover:bg-neutral-800/60 transition-all duration-200 rounded-lg cursor-pointer w-full text-left">
|
71 |
+
<img src={form?.model.image} alt={form?.model.title} class="w-14 h-14 rounded-lg object-cover" />
|
72 |
+
<div>
|
73 |
+
<p class="text-neutral-200 text-base font-medium">{form?.model.title}</p>
|
74 |
+
<p class="text-neutral-400 text-sm">{form?.model.id}</p>
|
75 |
+
</div>
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
<div>
|
79 |
+
<p class="text-neutral-400 font-semibold text-xs uppercase">
|
80 |
+
Prompt
|
81 |
+
</p>
|
82 |
+
<p class="text-neutral-200 text-base font-medium mt-2">"{form.inputs}"</p>
|
83 |
+
</div>
|
84 |
+
</div>
|
85 |
+
{/if}
|
86 |
+
</div>
|
87 |
+
{:else}
|
88 |
+
<div>
|
89 |
+
error displayed.
|
90 |
+
</div>
|
91 |
+
{/if}
|
92 |
+
{/if}
|
93 |
+
</div>
|
src/lib/components/models/Card.svelte
CHANGED
@@ -8,16 +8,16 @@
|
|
8 |
<div
|
9 |
class="w-full cursor-pointer group bg-neutral-900 rounded-xl relative flex items-start justify-between flex-col p-3 border border-neutral-800 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
|
10 |
>
|
11 |
-
<div class="w-full h-[
|
12 |
<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}" />
|
13 |
<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">
|
14 |
-
<Button theme="light" size="md" href={`/generate?model=${card.
|
15 |
Try it now
|
16 |
</Button>
|
17 |
</div>
|
18 |
</div>
|
19 |
<div class="flex items-center justify-between w-full gap-4 py-1">
|
20 |
-
<p class="text-white font-semibold text-base mb-1 truncate">{card?.title ?? card?.
|
21 |
<div class="flex items-center justify-end gap-3">
|
22 |
<div class="text-white text-sm flex items-center justify-end gap-1.5">
|
23 |
<Icon icon="solar:heart-bold" class="w-5 h-5 text-red-500" />
|
|
|
8 |
<div
|
9 |
class="w-full cursor-pointer group bg-neutral-900 rounded-xl relative flex items-start justify-between flex-col p-3 border border-neutral-800 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
|
10 |
>
|
11 |
+
<div class="w-full h-[350px] relative z-[1] mb-3 overflow-hidden">
|
12 |
<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}" />
|
13 |
<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">
|
14 |
+
<Button theme="light" size="md" href={`/generate?model=${card.id}`}>
|
15 |
Try it now
|
16 |
</Button>
|
17 |
</div>
|
18 |
</div>
|
19 |
<div class="flex items-center justify-between w-full gap-4 py-1">
|
20 |
+
<p class="text-white font-semibold text-base mb-1 truncate">{card?.title ?? card?.id}</p>
|
21 |
<div class="flex items-center justify-end gap-3">
|
22 |
<div class="text-white text-sm flex items-center justify-end gap-1.5">
|
23 |
<Icon icon="solar:heart-bold" class="w-5 h-5 text-red-500" />
|
src/lib/components/models/Submit.svelte
CHANGED
@@ -8,12 +8,12 @@
|
|
8 |
let user = get(userStore);
|
9 |
export let onClose: () => void;
|
10 |
let model = {
|
11 |
-
|
12 |
title: '',
|
13 |
image: '',
|
14 |
}
|
15 |
let error = {
|
16 |
-
|
17 |
title: '',
|
18 |
image: ''
|
19 |
}
|
@@ -33,7 +33,7 @@
|
|
33 |
} else {
|
34 |
console.log('Success:', data);
|
35 |
error = {
|
36 |
-
|
37 |
title: '',
|
38 |
image: ''
|
39 |
}
|
@@ -70,14 +70,14 @@
|
|
70 |
<span class="text-red-500">*</span>
|
71 |
</p>
|
72 |
<Input
|
73 |
-
value={model.
|
74 |
placeholder="{`${user?.preferred_username ?? 'enzostvs'}/`}"
|
75 |
prefix="huggingface.co/"
|
76 |
-
onChange={(value) => model.
|
77 |
/>
|
78 |
-
{#if error.
|
79 |
<p class="text-xs text-red-500 mt-1">
|
80 |
-
{error.
|
81 |
</p>
|
82 |
{/if}
|
83 |
</div>
|
|
|
8 |
let user = get(userStore);
|
9 |
export let onClose: () => void;
|
10 |
let model = {
|
11 |
+
id: '',
|
12 |
title: '',
|
13 |
image: '',
|
14 |
}
|
15 |
let error = {
|
16 |
+
id: '',
|
17 |
title: '',
|
18 |
image: ''
|
19 |
}
|
|
|
33 |
} else {
|
34 |
console.log('Success:', data);
|
35 |
error = {
|
36 |
+
id: '',
|
37 |
title: '',
|
38 |
image: ''
|
39 |
}
|
|
|
70 |
<span class="text-red-500">*</span>
|
71 |
</p>
|
72 |
<Input
|
73 |
+
value={model.id}
|
74 |
placeholder="{`${user?.preferred_username ?? 'enzostvs'}/`}"
|
75 |
prefix="huggingface.co/"
|
76 |
+
onChange={(value) => model.id = value}
|
77 |
/>
|
78 |
+
{#if error.id}
|
79 |
<p class="text-xs text-red-500 mt-1">
|
80 |
+
{error.id}
|
81 |
</p>
|
82 |
{/if}
|
83 |
</div>
|
src/lib/components/models/autocomplete/Autocomplete.svelte
CHANGED
@@ -8,8 +8,6 @@
|
|
8 |
export let onChange: (model: ModelCard |Β null) => void;
|
9 |
export let value: ModelCard | null = null;
|
10 |
|
11 |
-
console.log(value)
|
12 |
-
|
13 |
let models: ModelCard[] = [];
|
14 |
let search: string = "";
|
15 |
let open: boolean = false;
|
@@ -81,7 +79,13 @@
|
|
81 |
{#if search?.length >= 3}
|
82 |
{#if models?.length > 0}
|
83 |
{#each models as model}
|
84 |
-
<Item
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
{/each}
|
86 |
{:else}
|
87 |
<div class="flex items-center justify-center flex-col gap-2 p-3">
|
@@ -91,7 +95,13 @@
|
|
91 |
{/if}
|
92 |
{:else}
|
93 |
{#each defaultModels as model}
|
94 |
-
<Item
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
{/each}
|
96 |
{/if}
|
97 |
</div>
|
|
|
8 |
export let onChange: (model: ModelCard |Β null) => void;
|
9 |
export let value: ModelCard | null = null;
|
10 |
|
|
|
|
|
11 |
let models: ModelCard[] = [];
|
12 |
let search: string = "";
|
13 |
let open: boolean = false;
|
|
|
79 |
{#if search?.length >= 3}
|
80 |
{#if models?.length > 0}
|
81 |
{#each models as model}
|
82 |
+
<Item
|
83 |
+
model={model}
|
84 |
+
onClick={() => {
|
85 |
+
open = false;
|
86 |
+
onChange(model)
|
87 |
+
}}
|
88 |
+
/>
|
89 |
{/each}
|
90 |
{:else}
|
91 |
<div class="flex items-center justify-center flex-col gap-2 p-3">
|
|
|
95 |
{/if}
|
96 |
{:else}
|
97 |
{#each defaultModels as model}
|
98 |
+
<Item
|
99 |
+
model={model}
|
100 |
+
onClick={() => {
|
101 |
+
open = false;
|
102 |
+
onChange(model)
|
103 |
+
}}
|
104 |
+
/>
|
105 |
{/each}
|
106 |
{/if}
|
107 |
</div>
|
src/lib/components/models/autocomplete/Item.svelte
CHANGED
@@ -13,6 +13,6 @@
|
|
13 |
<img src={model.image} alt={model.title} class="w-14 h-14 rounded-lg object-cover" />
|
14 |
<div>
|
15 |
<p class="text-neutral-200 text-base font-medium">{model.title}</p>
|
16 |
-
<p class="text-neutral-400 text-sm">{model.
|
17 |
</div>
|
18 |
</button>
|
|
|
13 |
<img src={model.image} alt={model.title} class="w-14 h-14 rounded-lg object-cover" />
|
14 |
<div>
|
15 |
<p class="text-neutral-200 text-base font-medium">{model.title}</p>
|
16 |
+
<p class="text-neutral-400 text-sm">{model.id}</p>
|
17 |
</div>
|
18 |
</button>
|
src/lib/type.ts
CHANGED
@@ -8,20 +8,22 @@ export interface OptionRadio {
|
|
8 |
export interface CommunityCard {
|
9 |
reactions: ReactionType[],
|
10 |
id: string,
|
11 |
-
|
12 |
prompt: string,
|
13 |
image: string,
|
14 |
}
|
15 |
|
16 |
export interface ModelCard {
|
17 |
title: string,
|
18 |
-
|
19 |
likes: number,
|
20 |
downloads: number,
|
21 |
image: string,
|
22 |
}
|
23 |
|
24 |
export interface ReactionType {
|
|
|
|
|
25 |
emoji: string
|
26 |
-
|
27 |
}
|
|
|
8 |
export interface CommunityCard {
|
9 |
reactions: ReactionType[],
|
10 |
id: string,
|
11 |
+
model: ModelCard,
|
12 |
prompt: string,
|
13 |
image: string,
|
14 |
}
|
15 |
|
16 |
export interface ModelCard {
|
17 |
title: string,
|
18 |
+
id: string,
|
19 |
likes: number,
|
20 |
downloads: number,
|
21 |
image: string,
|
22 |
}
|
23 |
|
24 |
export interface ReactionType {
|
25 |
+
id: string
|
26 |
+
hf_user_id: string;
|
27 |
emoji: string
|
28 |
+
galleryId: string
|
29 |
}
|
src/lib/utils/index.ts
CHANGED
@@ -30,14 +30,14 @@ export const MODELS_FILTER_OPTIONS = [
|
|
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: "/
|
|
|
|
|
|
|
|
|
41 |
}, {
|
42 |
icon: "fluent:glance-horizontal-sparkles-16-filled",
|
43 |
label: "Generate",
|
|
|
30 |
},
|
31 |
];
|
32 |
|
33 |
+
export const SIDEBAR_MENUS = [ {
|
|
|
|
|
|
|
|
|
34 |
icon: "uim:cube",
|
35 |
label: "Models",
|
36 |
+
href: "/",
|
37 |
+
}, {
|
38 |
+
icon: "solar:gallery-bold-duotone",
|
39 |
+
label: "Gallery",
|
40 |
+
href: "/gallery",
|
41 |
}, {
|
42 |
icon: "fluent:glance-horizontal-sparkles-16-filled",
|
43 |
label: "Generate",
|
src/lib/utils/uploader.ts
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { uploadFile } from "@huggingface/hub";
|
2 |
+
import type { RepoDesignation, Credentials } from "@huggingface/hub";
|
3 |
+
import { env } from '$env/dynamic/private'
|
4 |
+
|
5 |
+
const TOTAL_FOLDERS = 10
|
6 |
+
// const MAX_IMAGES_PER_FOLDER = 10_000
|
7 |
+
|
8 |
+
export const UploaderDataset = async (blob: Blob, name: string) => {
|
9 |
+
const repo: RepoDesignation = { type: "dataset", name: "enzostvs/loras-studio" };
|
10 |
+
const credentials: Credentials = { accessToken: env.SECRET_HF_TOKEN as string };
|
11 |
+
|
12 |
+
const folder_key = Math.floor(Math.random() * TOTAL_FOLDERS)
|
13 |
+
const formatted_name = name.replace(/[^a-zA-Z0-9]/g, "-")
|
14 |
+
const path = `images-${folder_key}/${formatted_name}.png`
|
15 |
+
|
16 |
+
try {
|
17 |
+
await uploadFile({
|
18 |
+
repo,
|
19 |
+
credentials,
|
20 |
+
file:
|
21 |
+
{
|
22 |
+
path,
|
23 |
+
content: blob,
|
24 |
+
},
|
25 |
+
})
|
26 |
+
return { path, ok: true }
|
27 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
28 |
+
} catch (e: any) {
|
29 |
+
console.error(e)
|
30 |
+
return { ok: false }
|
31 |
+
}
|
32 |
+
}
|
src/routes/+layout.server.ts
CHANGED
@@ -6,5 +6,5 @@ export async function load({ fetch }) {
|
|
6 |
}
|
7 |
})
|
8 |
const user = await response.json()
|
9 |
-
return user
|
10 |
}
|
|
|
6 |
}
|
7 |
})
|
8 |
const user = await response.json()
|
9 |
+
return { user }
|
10 |
}
|
src/routes/+layout.svelte
CHANGED
@@ -9,7 +9,7 @@
|
|
9 |
|
10 |
<div class="flex items-start">
|
11 |
<Sidebar />
|
12 |
-
<main id="app" class="
|
13 |
<slot />
|
14 |
</main>
|
15 |
</div>
|
|
|
9 |
|
10 |
<div class="flex items-start">
|
11 |
<Sidebar />
|
12 |
+
<main id="app" class="flex-1 h-screen overflow-y-auto">
|
13 |
<slot />
|
14 |
</main>
|
15 |
</div>
|
src/routes/+page.svelte
CHANGED
@@ -1,62 +1,100 @@
|
|
1 |
<script lang="ts">
|
2 |
import { browser } from "$app/environment";
|
3 |
import InfiniteScroll from "svelte-infinite-scroll";
|
4 |
-
|
5 |
import Button from "$lib/components/Button.svelte";
|
6 |
-
import Card from "$lib/components/
|
7 |
import Input from "$lib/components/fields/Input.svelte";
|
8 |
import Radio from "$lib/components/fields/Radio.svelte";
|
9 |
-
import {
|
10 |
import GoTop from "$lib/components/GoTop.svelte";
|
|
|
|
|
11 |
|
12 |
-
export let data
|
13 |
|
14 |
let form = {
|
15 |
-
filter: "
|
|
|
16 |
page: "0",
|
17 |
}
|
|
|
18 |
|
19 |
$: elementScroll = browser ? document?.getElementById('app') : undefined;
|
20 |
|
21 |
-
const
|
22 |
form = {...form, page: (Number(form.page) + 1).toString()};
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
const response = await request.json();
|
25 |
-
data = {...data, cards: [...data.cards, ...response.cards ]};
|
|
|
26 |
}
|
27 |
</script>
|
28 |
|
29 |
<svelte:head>
|
30 |
-
<title>
|
31 |
<meta name="description" content="Svelte demo app" />
|
32 |
</svelte:head>
|
33 |
|
34 |
-
<
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
<
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
</div>
|
43 |
-
<div class="
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
</div>
|
47 |
-
|
48 |
-
|
49 |
-
<Input placeholder="Filter by prompt, model..." />
|
50 |
-
</div>
|
51 |
-
<div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
|
52 |
-
{#each data.cards as card}
|
53 |
-
<Card card={card} />
|
54 |
-
{/each}
|
55 |
-
<InfiniteScroll
|
56 |
-
elementScroll="{elementScroll ?? undefined}"
|
57 |
-
threshold={100}
|
58 |
-
hasMore={data.total_items > data.cards.length}
|
59 |
-
on:loadMore={fetchMore}
|
60 |
-
/>
|
61 |
-
<GoTop />
|
62 |
-
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import { browser } from "$app/environment";
|
3 |
import InfiniteScroll from "svelte-infinite-scroll";
|
|
|
4 |
import Button from "$lib/components/Button.svelte";
|
5 |
+
import Card from "$lib/components/models/Card.svelte";
|
6 |
import Input from "$lib/components/fields/Input.svelte";
|
7 |
import Radio from "$lib/components/fields/Radio.svelte";
|
8 |
+
import { MODELS_FILTER_OPTIONS } from "$lib/utils/index.js";
|
9 |
import GoTop from "$lib/components/GoTop.svelte";
|
10 |
+
import Dialog from "$lib/components/dialog/Dialog.svelte";
|
11 |
+
import SubmitModel from "$lib/components/models/Submit.svelte";
|
12 |
|
13 |
+
export let data
|
14 |
|
15 |
let form = {
|
16 |
+
filter: "hotest",
|
17 |
+
search: "",
|
18 |
page: "0",
|
19 |
}
|
20 |
+
let submitModelDialog = false;
|
21 |
|
22 |
$: elementScroll = browser ? document?.getElementById('app') : undefined;
|
23 |
|
24 |
+
const handleFetchMore = async () => {
|
25 |
form = {...form, page: (Number(form.page) + 1).toString()};
|
26 |
+
refetch(true);
|
27 |
+
}
|
28 |
+
const handleChangeFilter = async (filter: string) => {
|
29 |
+
form = { ...form, filter, page: (0).toString()};
|
30 |
+
refetch(false)
|
31 |
+
}
|
32 |
+
let timeout: any;
|
33 |
+
const handleChangeSearch = async (search: string) => {
|
34 |
+
clearTimeout(timeout);
|
35 |
+
form = { ...form, search, page: (0).toString()};
|
36 |
+
timeout = setTimeout(() => refetch(false), 500);
|
37 |
+
}
|
38 |
+
|
39 |
+
const refetch = async (add: boolean) => {
|
40 |
+
const request = await fetch(`/api/models?${new URLSearchParams(form)}`);
|
41 |
const response = await request.json();
|
42 |
+
if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
|
43 |
+
else data = response;
|
44 |
}
|
45 |
</script>
|
46 |
|
47 |
<svelte:head>
|
48 |
+
<title>Explore Models</title>
|
49 |
<meta name="description" content="Svelte demo app" />
|
50 |
</svelte:head>
|
51 |
|
52 |
+
<main class="px-6 py-10 lg:px-10 lg:py-12">
|
53 |
+
<Dialog open={submitModelDialog} onClose={() => submitModelDialog = false}>
|
54 |
+
<SubmitModel onClose={() => submitModelDialog = false} />
|
55 |
+
</Dialog>
|
56 |
+
<h1 class="text-white font-semibold text-2xl">
|
57 |
+
Explore Models ({data.total_items})
|
58 |
+
</h1>
|
59 |
+
<div class="flex items-start sm:items-center justify-between mt-5 flex-col sm:flex-row gap-5 sm:justify-between">
|
60 |
+
<Radio options={MODELS_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
|
61 |
+
<div class="items-center justify-end gap-5 hidden lg:flex">
|
62 |
+
<Button href="https://huggingface.co/new/stable-diffusion-lora" target="_blank" icon="ic:round-plus" theme="dark" size="lg">Create</Button>
|
63 |
+
<Button
|
64 |
+
icon="octicon:upload-16"
|
65 |
+
theme="blue"
|
66 |
+
size="lg"
|
67 |
+
onClick={() => submitModelDialog = true}
|
68 |
+
>
|
69 |
+
Upload model
|
70 |
+
</Button>
|
71 |
+
</div>
|
72 |
+
<div class="items-center justify-end gap-3 flex lg:hidden">
|
73 |
+
<Button href="https://huggingface.co/new/stable-diffusion-lora" target="_blank" icon="ic:round-plus" theme="dark" size="md">Create</Button>
|
74 |
+
<Button
|
75 |
+
icon="octicon:upload-16"
|
76 |
+
theme="blue"
|
77 |
+
size="md"
|
78 |
+
onClick={() => submitModelDialog = true}
|
79 |
+
>
|
80 |
+
Upload model
|
81 |
+
</Button>
|
82 |
+
</div>
|
83 |
+
</div>
|
84 |
+
<div class="mt-5 max-w-sm">
|
85 |
+
<Input value={form.search} placeholder="Search a model" onChange={handleChangeSearch} />
|
86 |
</div>
|
87 |
+
<div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
|
88 |
+
{#each data.cards as card}
|
89 |
+
<Card card={card} />
|
90 |
+
{/each}
|
91 |
+
<InfiniteScroll
|
92 |
+
elementScroll="{elementScroll ?? undefined}"
|
93 |
+
threshold={100}
|
94 |
+
hasMore={data.total_items > data.cards.length}
|
95 |
+
on:loadMore={handleFetchMore}
|
96 |
+
/>
|
97 |
+
<GoTop />
|
98 |
</div>
|
99 |
+
|
100 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/routes/+page.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
export async function load({ fetch }) {
|
2 |
-
const response = await fetch("/api/
|
3 |
method: "GET",
|
4 |
headers: {
|
5 |
"Content-Type": "application/json"
|
|
|
1 |
export async function load({ fetch }) {
|
2 |
+
const response = await fetch("/api/models?page=0&filter=hotest", {
|
3 |
method: "GET",
|
4 |
headers: {
|
5 |
"Content-Type": "application/json"
|
src/routes/api/bulk-create-models/+server.ts
CHANGED
@@ -1,16 +1,13 @@
|
|
|
|
|
|
1 |
import { json } from '@sveltejs/kit';
|
2 |
import prisma from '$lib/prisma';
|
3 |
-
|
4 |
-
|
5 |
-
// import jsonData from "$lib/utils/loras.json";
|
6 |
-
// import type { ModelCard } from '$lib/type';
|
7 |
-
|
8 |
-
/** @type {import('./$types').RequestHandler} */
|
9 |
|
10 |
export async function POST({ request }) {
|
11 |
const headers = Object.fromEntries(request.headers.entries());
|
12 |
|
13 |
-
if (headers["x-hf-token"] !==
|
14 |
return Response.json({
|
15 |
message: "Wrong castle fam :^)"
|
16 |
}, { status: 401 });
|
@@ -19,11 +16,11 @@ export async function POST({ request }) {
|
|
19 |
const { models } = await request.json();
|
20 |
|
21 |
const cards = await Promise.all(models.map(async (model: Record<string, string>) => {
|
22 |
-
const res = await fetch(`https://huggingface.co/api/models/${model.
|
23 |
const data = await res.json();
|
24 |
const mergedData = {
|
25 |
image: model.image,
|
26 |
-
|
27 |
title: model.title,
|
28 |
likes: data.likes,
|
29 |
downloads: data.downloads,
|
@@ -35,7 +32,7 @@ export async function POST({ request }) {
|
|
35 |
for (const model of cards) {
|
36 |
await prisma.model.create({
|
37 |
data: {
|
38 |
-
|
39 |
image: model.image,
|
40 |
title: model.title,
|
41 |
likes: model.likes,
|
|
|
1 |
+
/** @type {import('./$types').RequestHandler} */
|
2 |
+
|
3 |
import { json } from '@sveltejs/kit';
|
4 |
import prisma from '$lib/prisma';
|
5 |
+
import { env } from '$env/dynamic/private'
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
export async function POST({ request }) {
|
8 |
const headers = Object.fromEntries(request.headers.entries());
|
9 |
|
10 |
+
if (headers["x-hf-token"] !== env.SECRET_HF_TOKEN) {
|
11 |
return Response.json({
|
12 |
message: "Wrong castle fam :^)"
|
13 |
}, { status: 401 });
|
|
|
16 |
const { models } = await request.json();
|
17 |
|
18 |
const cards = await Promise.all(models.map(async (model: Record<string, string>) => {
|
19 |
+
const res = await fetch(`https://huggingface.co/api/models/${model.id}`)
|
20 |
const data = await res.json();
|
21 |
const mergedData = {
|
22 |
image: model.image,
|
23 |
+
id: model.repo,
|
24 |
title: model.title,
|
25 |
likes: data.likes,
|
26 |
downloads: data.downloads,
|
|
|
32 |
for (const model of cards) {
|
33 |
await prisma.model.create({
|
34 |
data: {
|
35 |
+
id: model.id,
|
36 |
image: model.image,
|
37 |
title: model.title,
|
38 |
likes: model.likes,
|
src/routes/api/community/+server.ts
CHANGED
@@ -1,22 +1,47 @@
|
|
1 |
-
import {
|
2 |
-
|
3 |
-
|
4 |
-
import jsonData from "$lib/cards.json";
|
5 |
|
6 |
/** @type {import('./$types').RequestHandler} */
|
7 |
|
8 |
export async function GET(request : RequestEvent) {
|
9 |
-
const hasError = false
|
10 |
-
|
11 |
const page = parseInt(request.url.searchParams.get('page') || '0')
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
return json({
|
19 |
cards,
|
20 |
-
total_items:
|
21 |
})
|
22 |
}
|
|
|
1 |
+
import { json, type RequestEvent } from '@sveltejs/kit';
|
2 |
+
import prisma from '$lib/prisma';
|
|
|
|
|
3 |
|
4 |
/** @type {import('./$types').RequestHandler} */
|
5 |
|
6 |
export async function GET(request : RequestEvent) {
|
|
|
|
|
7 |
const page = parseInt(request.url.searchParams.get('page') || '0')
|
8 |
+
const filter = request.url.searchParams.get('filter') || 'new'
|
9 |
+
const search = request.url.searchParams.get('search') || ''
|
10 |
+
const limit = parseInt(request.url.searchParams.get('limit') || '20')
|
11 |
+
|
12 |
+
const cards = await prisma.gallery.findMany({
|
13 |
+
where: {
|
14 |
+
OR: [
|
15 |
+
{ prompt: { contains: search } },
|
16 |
+
]
|
17 |
+
},
|
18 |
+
orderBy: {
|
19 |
+
...(filter === 'new' ? {
|
20 |
+
createdAt: 'desc'
|
21 |
+
} : {}
|
22 |
+
)
|
23 |
+
},
|
24 |
+
select: {
|
25 |
+
reactions: true,
|
26 |
+
id: true,
|
27 |
+
prompt: true,
|
28 |
+
image: true,
|
29 |
+
model: true,
|
30 |
+
},
|
31 |
+
skip: limit * page,
|
32 |
+
take: limit,
|
33 |
+
})
|
34 |
|
35 |
+
const total_reposId = await prisma.gallery.count({
|
36 |
+
where: {
|
37 |
+
OR: [
|
38 |
+
{ prompt: { contains: search } },
|
39 |
+
]
|
40 |
+
},
|
41 |
+
})
|
42 |
|
43 |
return json({
|
44 |
cards,
|
45 |
+
total_items: total_reposId
|
46 |
})
|
47 |
}
|
src/routes/api/generate/+server.ts
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('./$types').RequestHandler} */
|
2 |
+
|
3 |
+
import { json, type RequestEvent } from '@sveltejs/kit';
|
4 |
+
import { env } from '$env/dynamic/private'
|
5 |
+
|
6 |
+
export async function POST({ request } : RequestEvent) {
|
7 |
+
const generation = await request.json()
|
8 |
+
|
9 |
+
if (!generation?.model?.id) {
|
10 |
+
return json({
|
11 |
+
error: {
|
12 |
+
token: "A model id is required"
|
13 |
+
}
|
14 |
+
}, { status: 400 })
|
15 |
+
}
|
16 |
+
|
17 |
+
if (!generation?.inputs) {
|
18 |
+
return json({
|
19 |
+
error: {
|
20 |
+
token: "An inputs is required"
|
21 |
+
}
|
22 |
+
}, { status: 400 })
|
23 |
+
}
|
24 |
+
|
25 |
+
const response = await fetch(env.SECRET_INFERENCE_API_URL + "/models/" + generation?.model?.id, {
|
26 |
+
method: "POST",
|
27 |
+
headers: {
|
28 |
+
Authorization: `Bearer ${env.SECRET_HF_TOKEN}`,
|
29 |
+
'Content-Type': 'application/json',
|
30 |
+
['x-use-cache']: "0"
|
31 |
+
},
|
32 |
+
body: JSON.stringify(generation),
|
33 |
+
})
|
34 |
+
.then((res) => res.blob())
|
35 |
+
.then((blob) => blob)
|
36 |
+
.catch((error) => {
|
37 |
+
return {
|
38 |
+
error: error.message,
|
39 |
+
}
|
40 |
+
})
|
41 |
+
|
42 |
+
if ("error" in response) {
|
43 |
+
return json({
|
44 |
+
error: {
|
45 |
+
token: response.error
|
46 |
+
}
|
47 |
+
}, { status: 400 })
|
48 |
+
}
|
49 |
+
|
50 |
+
return new Response(response, {
|
51 |
+
headers: {
|
52 |
+
'Content-Type': 'image/png',
|
53 |
+
},
|
54 |
+
})
|
55 |
+
}
|
src/routes/api/generate/share/+server.ts
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('./$types').RequestHandler} */
|
2 |
+
|
3 |
+
import { UploaderDataset } from '$lib/utils/uploader';
|
4 |
+
import { json, type RequestEvent } from '@sveltejs/kit';
|
5 |
+
|
6 |
+
import prisma from '$lib/prisma';
|
7 |
+
|
8 |
+
export async function POST({ request } : RequestEvent) {
|
9 |
+
const { generation, image } = await request.json()
|
10 |
+
|
11 |
+
if (!generation?.model?.id) {
|
12 |
+
return json({
|
13 |
+
error: {
|
14 |
+
token: "A model id is required"
|
15 |
+
}
|
16 |
+
}, { status: 400 })
|
17 |
+
}
|
18 |
+
|
19 |
+
if (!generation?.inputs) {
|
20 |
+
return json({
|
21 |
+
error: {
|
22 |
+
token: "An inputs is required"
|
23 |
+
}
|
24 |
+
}, { status: 400 })
|
25 |
+
}
|
26 |
+
|
27 |
+
const blob = await fetch(image)
|
28 |
+
.then((res) => res.blob())
|
29 |
+
.then((blob) => blob)
|
30 |
+
.catch((error) => {
|
31 |
+
return json({
|
32 |
+
error: error.message,
|
33 |
+
}, { status: 400 })
|
34 |
+
})
|
35 |
+
|
36 |
+
const success: {
|
37 |
+
ok: boolean,
|
38 |
+
path?: string | undefined
|
39 |
+
} = await UploaderDataset(blob as Blob, generation.inputs)
|
40 |
+
|
41 |
+
if (!success.ok) {
|
42 |
+
return json({
|
43 |
+
error: {
|
44 |
+
token: "Error uploading image"
|
45 |
+
}
|
46 |
+
}, { status: 400 })
|
47 |
+
}
|
48 |
+
|
49 |
+
const gallery = prisma.gallery.create({
|
50 |
+
data: {
|
51 |
+
image: success.path as string,
|
52 |
+
prompt: generation.inputs,
|
53 |
+
model: {
|
54 |
+
connect: {
|
55 |
+
id: generation.model.id
|
56 |
+
}
|
57 |
+
},
|
58 |
+
}
|
59 |
+
})
|
60 |
+
.catch((error) => {
|
61 |
+
console.log(error)
|
62 |
+
})
|
63 |
+
|
64 |
+
return json({
|
65 |
+
message: "Successfully generated image",
|
66 |
+
gallery
|
67 |
+
})
|
68 |
+
}
|
src/routes/api/models/+server.ts
CHANGED
@@ -1,12 +1,8 @@
|
|
1 |
-
import {
|
2 |
import prisma from '$lib/prisma';
|
3 |
|
4 |
/** @type {import('./$types').RequestHandler} */
|
5 |
|
6 |
-
// TODO
|
7 |
-
// run hooks to update each model by repo using hugging face api.
|
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'
|
@@ -18,7 +14,7 @@ export async function GET(request : RequestEvent) {
|
|
18 |
isPublic: true,
|
19 |
OR: [
|
20 |
{ title: { contains: search } },
|
21 |
-
{
|
22 |
]
|
23 |
},
|
24 |
orderBy: {
|
@@ -33,16 +29,11 @@ export async function GET(request : RequestEvent) {
|
|
33 |
isPublic: true,
|
34 |
OR: [
|
35 |
{ title: { contains: search } },
|
36 |
-
{
|
37 |
]
|
38 |
},
|
39 |
})
|
40 |
|
41 |
-
const hasError = false
|
42 |
-
if (hasError) {
|
43 |
-
return error(500, 'Internal Server Error')
|
44 |
-
}
|
45 |
-
|
46 |
return json({
|
47 |
cards,
|
48 |
total_items: total_reposId
|
|
|
1 |
+
import { json, type RequestEvent } from '@sveltejs/kit';
|
2 |
import prisma from '$lib/prisma';
|
3 |
|
4 |
/** @type {import('./$types').RequestHandler} */
|
5 |
|
|
|
|
|
|
|
|
|
6 |
export async function GET(request : RequestEvent) {
|
7 |
const page = parseInt(request.url.searchParams.get('page') || '0')
|
8 |
const filter = request.url.searchParams.get('filter') || 'hotest'
|
|
|
14 |
isPublic: true,
|
15 |
OR: [
|
16 |
{ title: { contains: search } },
|
17 |
+
{ id: { contains: search } },
|
18 |
]
|
19 |
},
|
20 |
orderBy: {
|
|
|
29 |
isPublic: true,
|
30 |
OR: [
|
31 |
{ title: { contains: search } },
|
32 |
+
{ id: { contains: search } },
|
33 |
]
|
34 |
},
|
35 |
})
|
36 |
|
|
|
|
|
|
|
|
|
|
|
37 |
return json({
|
38 |
cards,
|
39 |
total_items: total_reposId
|
src/routes/api/models/{[repo] β [id]}/+server.ts
RENAMED
@@ -4,11 +4,11 @@ import prisma from '$lib/prisma';
|
|
4 |
/** @type {import('./$types').RequestHandler} */
|
5 |
|
6 |
export async function GET(request : RequestEvent) {
|
7 |
-
const
|
8 |
|
9 |
const model = await prisma.model.findFirst({
|
10 |
where: {
|
11 |
-
|
12 |
}
|
13 |
})
|
14 |
|
|
|
4 |
/** @type {import('./$types').RequestHandler} */
|
5 |
|
6 |
export async function GET(request : RequestEvent) {
|
7 |
+
const id = request.params.id?.replace("@", "/")
|
8 |
|
9 |
const model = await prisma.model.findFirst({
|
10 |
where: {
|
11 |
+
id
|
12 |
}
|
13 |
})
|
14 |
|
src/routes/api/models/submit/+server.ts
CHANGED
@@ -27,13 +27,13 @@ export async function POST({ request, fetch, cookies }) {
|
|
27 |
}
|
28 |
|
29 |
// get model on hugging face
|
30 |
-
const res = await fetch(`https://huggingface.co/api/models/${model.
|
31 |
const data = await res.json();
|
32 |
|
33 |
if (data?.error) {
|
34 |
return json({
|
35 |
error: {
|
36 |
-
|
37 |
}
|
38 |
}, { status: 404 })
|
39 |
}
|
@@ -54,7 +54,7 @@ export async function POST({ request, fetch, cookies }) {
|
|
54 |
|
55 |
await prisma.model.create({
|
56 |
data: {
|
57 |
-
|
58 |
image: model.image,
|
59 |
title: model.title,
|
60 |
likes: data.likes,
|
|
|
27 |
}
|
28 |
|
29 |
// get model on hugging face
|
30 |
+
const res = await fetch(`https://huggingface.co/api/models/${model.id}`)
|
31 |
const data = await res.json();
|
32 |
|
33 |
if (data?.error) {
|
34 |
return json({
|
35 |
error: {
|
36 |
+
id: "Model not found on Hugging Face"
|
37 |
}
|
38 |
}, { status: 404 })
|
39 |
}
|
|
|
54 |
|
55 |
await prisma.model.create({
|
56 |
data: {
|
57 |
+
id: model.id,
|
58 |
image: model.image,
|
59 |
title: model.title,
|
60 |
likes: data.likes,
|
src/routes/{models β gallery}/+page.svelte
RENAMED
@@ -1,23 +1,21 @@
|
|
1 |
<script lang="ts">
|
2 |
import { browser } from "$app/environment";
|
3 |
import InfiniteScroll from "svelte-infinite-scroll";
|
|
|
4 |
import Button from "$lib/components/Button.svelte";
|
5 |
-
import Card from "$lib/components/
|
6 |
import Input from "$lib/components/fields/Input.svelte";
|
7 |
import Radio from "$lib/components/fields/Radio.svelte";
|
8 |
-
import {
|
9 |
import GoTop from "$lib/components/GoTop.svelte";
|
10 |
-
import Dialog from "$lib/components/dialog/Dialog.svelte";
|
11 |
-
import SubmitModel from "$lib/components/models/Submit.svelte";
|
12 |
|
13 |
-
export let data
|
14 |
|
15 |
let form = {
|
16 |
-
filter: "
|
17 |
-
search: "",
|
18 |
page: "0",
|
|
|
19 |
}
|
20 |
-
let submitModelDialog = false;
|
21 |
|
22 |
$: elementScroll = browser ? document?.getElementById('app') : undefined;
|
23 |
|
@@ -37,7 +35,7 @@
|
|
37 |
}
|
38 |
|
39 |
const refetch = async (add: boolean) => {
|
40 |
-
const request = await fetch(`/api/
|
41 |
const response = await request.json();
|
42 |
if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
|
43 |
else data = response;
|
@@ -45,53 +43,38 @@
|
|
45 |
</script>
|
46 |
|
47 |
<svelte:head>
|
48 |
-
<title>
|
49 |
<meta name="description" content="Svelte demo app" />
|
50 |
</svelte:head>
|
51 |
|
52 |
-
<
|
53 |
-
<
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
<div class="
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
<
|
63 |
-
icon="
|
64 |
-
theme="
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
</Button>
|
70 |
</div>
|
71 |
-
<div class="
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
81 |
</div>
|
82 |
-
</
|
83 |
-
<div class="mt-5 max-w-sm">
|
84 |
-
<Input value={form.search} placeholder="Search a model" onChange={handleChangeSearch} />
|
85 |
-
</div>
|
86 |
-
<div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
|
87 |
-
{#each data.cards as card}
|
88 |
-
<Card card={card} />
|
89 |
-
{/each}
|
90 |
-
<InfiniteScroll
|
91 |
-
elementScroll="{elementScroll ?? undefined}"
|
92 |
-
threshold={100}
|
93 |
-
hasMore={data.total_items > data.cards.length}
|
94 |
-
on:loadMore={handleFetchMore}
|
95 |
-
/>
|
96 |
-
<GoTop />
|
97 |
-
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import { browser } from "$app/environment";
|
3 |
import InfiniteScroll from "svelte-infinite-scroll";
|
4 |
+
|
5 |
import Button from "$lib/components/Button.svelte";
|
6 |
+
import Card from "$lib/components/community/Card.svelte";
|
7 |
import Input from "$lib/components/fields/Input.svelte";
|
8 |
import Radio from "$lib/components/fields/Radio.svelte";
|
9 |
+
import { COMMUNITY_FILTER_OPTIONS } from "$lib/utils/index.js";
|
10 |
import GoTop from "$lib/components/GoTop.svelte";
|
|
|
|
|
11 |
|
12 |
+
export let data
|
13 |
|
14 |
let form = {
|
15 |
+
filter: "new",
|
|
|
16 |
page: "0",
|
17 |
+
search: ""
|
18 |
}
|
|
|
19 |
|
20 |
$: elementScroll = browser ? document?.getElementById('app') : undefined;
|
21 |
|
|
|
35 |
}
|
36 |
|
37 |
const refetch = async (add: boolean) => {
|
38 |
+
const request = await fetch(`/api/community?${new URLSearchParams(form)}`);
|
39 |
const response = await request.json();
|
40 |
if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
|
41 |
else data = response;
|
|
|
43 |
</script>
|
44 |
|
45 |
<svelte:head>
|
46 |
+
<title>Community Gallery</title>
|
47 |
<meta name="description" content="Svelte demo app" />
|
48 |
</svelte:head>
|
49 |
|
50 |
+
<main class="px-6 py-10 lg:px-10 lg:py-12">
|
51 |
+
<h1 class="text-white font-semibold text-2xl">
|
52 |
+
Community Gallery ({data.total_items})
|
53 |
+
</h1>
|
54 |
+
<div class="flex items-center justify-between mt-5">
|
55 |
+
<Radio options={COMMUNITY_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
|
56 |
+
<div class="items-center justify-end gap-5 hidden lg:flex">
|
57 |
+
<Button icon="ic:round-plus" theme="dark" size="lg">Upload own Image</Button>
|
58 |
+
<Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="lg">Generate</Button>
|
59 |
+
</div>
|
60 |
+
<div class="items-center justify-end gap-3 flex lg:hidden">
|
61 |
+
<Button icon="ic:round-plus" theme="dark" size="md">Upload own Image</Button>
|
62 |
+
<Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="md">Generate</Button>
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
<div class="mt-5 max-w-sm">
|
66 |
+
<Input value={form.search} placeholder="Search an image" onChange={handleChangeSearch} />
|
|
|
67 |
</div>
|
68 |
+
<div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
|
69 |
+
{#each data.cards as card}
|
70 |
+
<Card card={card} />
|
71 |
+
{/each}
|
72 |
+
<InfiniteScroll
|
73 |
+
elementScroll="{elementScroll ?? undefined}"
|
74 |
+
threshold={100}
|
75 |
+
hasMore={data.total_items > data.cards.length}
|
76 |
+
on:loadMore={handleFetchMore}
|
77 |
+
/>
|
78 |
+
<GoTop />
|
79 |
</div>
|
80 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/routes/{models β gallery}/+page.ts
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
export async function load({ fetch }) {
|
2 |
-
const response = await fetch("/api/
|
3 |
method: "GET",
|
4 |
headers: {
|
5 |
"Content-Type": "application/json"
|
|
|
1 |
export async function load({ fetch }) {
|
2 |
+
const response = await fetch("/api/community?page=0&filter=hotest", {
|
3 |
method: "GET",
|
4 |
headers: {
|
5 |
"Content-Type": "application/json"
|
src/routes/generate/+page.svelte
CHANGED
@@ -4,22 +4,63 @@
|
|
4 |
</svelte:head>
|
5 |
|
6 |
<script lang="ts">
|
|
|
|
|
|
|
|
|
7 |
import Autocomplete from "$lib/components/models/autocomplete/Autocomplete.svelte";
|
8 |
|
9 |
export let data
|
10 |
|
|
|
|
|
|
|
11 |
let form = {
|
12 |
model: data?.model ?? null,
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
}
|
15 |
</script>
|
16 |
|
17 |
-
<
|
18 |
-
<div class="w-full">
|
|
|
19 |
<h1 class="text-white font-semibold text-2xl">
|
20 |
Start generating
|
21 |
</h1>
|
22 |
-
<div class="mt-5 grid grid-cols-1 gap-
|
23 |
<div>
|
24 |
<p class="text-neutral-300 mb-2.5 text-base">Models</p>
|
25 |
<Autocomplete
|
@@ -28,11 +69,34 @@
|
|
28 |
onChange={(model) => form.model = model}
|
29 |
/>
|
30 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
</div>
|
32 |
</div>
|
33 |
-
<
|
34 |
-
|
35 |
-
Result of your generation
|
36 |
-
</p>
|
37 |
-
</div>
|
38 |
-
</div>
|
|
|
4 |
</svelte:head>
|
5 |
|
6 |
<script lang="ts">
|
7 |
+
import Button from "$lib/components/Button.svelte";
|
8 |
+
import Textarea from "$lib/components/fields/Textarea.svelte";
|
9 |
+
import Banner from "$lib/components/generate/Banner.svelte";
|
10 |
+
import Response from "$lib/components/generate/Response.svelte";
|
11 |
import Autocomplete from "$lib/components/models/autocomplete/Autocomplete.svelte";
|
12 |
|
13 |
export let data
|
14 |
|
15 |
+
let loading: boolean = false;
|
16 |
+
let response: string | ArrayBuffer | null = '';
|
17 |
+
|
18 |
let form = {
|
19 |
model: data?.model ?? null,
|
20 |
+
inputs: "",
|
21 |
+
parameters: {
|
22 |
+
negative_prompt: ""
|
23 |
+
}
|
24 |
+
}
|
25 |
+
|
26 |
+
const handleSubmit = async () => {
|
27 |
+
if (loading) return
|
28 |
+
loading = true
|
29 |
+
|
30 |
+
const request = await fetch(`/api/generate`, {
|
31 |
+
method: "POST",
|
32 |
+
headers: {
|
33 |
+
"Content-Type": "application/json"
|
34 |
+
},
|
35 |
+
body: JSON.stringify(form)
|
36 |
+
});
|
37 |
+
const blob = await request?.clone()?.blob()
|
38 |
+
|
39 |
+
if (blob) {
|
40 |
+
const reader = new FileReader()
|
41 |
+
reader.readAsDataURL(blob)
|
42 |
+
reader.onloadend = () => {
|
43 |
+
const base64data = reader.result
|
44 |
+
response = base64data
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
const res = await request.clone().json().catch(() => ({}))
|
49 |
+
if (res) {
|
50 |
+
response = res
|
51 |
+
}
|
52 |
+
|
53 |
+
loading = false
|
54 |
}
|
55 |
</script>
|
56 |
|
57 |
+
<main class="grid grid-cols-5 w-full h-full gap-10">
|
58 |
+
<div class="w-full px-6 py-10 lg:px-10 lg:py-12 col-span-3">
|
59 |
+
<Banner />
|
60 |
<h1 class="text-white font-semibold text-2xl">
|
61 |
Start generating
|
62 |
</h1>
|
63 |
+
<div class="mt-5 grid grid-cols-1 gap-6">
|
64 |
<div>
|
65 |
<p class="text-neutral-300 mb-2.5 text-base">Models</p>
|
66 |
<Autocomplete
|
|
|
69 |
onChange={(model) => form.model = model}
|
70 |
/>
|
71 |
</div>
|
72 |
+
<div>
|
73 |
+
<p class="text-neutral-300 mb-2.5 text-base">Prompt</p>
|
74 |
+
<Textarea
|
75 |
+
value={form?.inputs}
|
76 |
+
placeholder="Aerial photography of a desert through autumn forests, with vibrant red and orange foliage"
|
77 |
+
onChange={(inputs) => form.inputs = inputs}
|
78 |
+
/>
|
79 |
+
</div>
|
80 |
+
<div>
|
81 |
+
<p class="text-neutral-300 mb-2.5 text-base">Negative Prompt</p>
|
82 |
+
<Textarea
|
83 |
+
value={form?.parameters?.negative_prompt}
|
84 |
+
placeholder="Write your negative prompt here"
|
85 |
+
onChange={(negative_prompt) => form.parameters.negative_prompt = negative_prompt}
|
86 |
+
/>
|
87 |
+
</div>
|
88 |
+
<div class="flex justify-end">
|
89 |
+
<Button
|
90 |
+
icon="fluent:glance-horizontal-sparkles-16-filled"
|
91 |
+
theme="pink"
|
92 |
+
size="lg"
|
93 |
+
{loading}
|
94 |
+
onClick={handleSubmit}
|
95 |
+
>
|
96 |
+
Generate
|
97 |
+
</Button>
|
98 |
+
</div>
|
99 |
</div>
|
100 |
</div>
|
101 |
+
<Response response={response} form={form} />
|
102 |
+
</main>
|
|
|
|
|
|
|
|