victor HF Staff commited on
Commit
b9a2152
·
unverified ·
1 Parent(s): a2297a7

feat: add PRO badge and credits modal for HuggingChat (#2048)

Browse files

* feat: add PRO badge and credits modal for HuggingChat

- Add isPro field to User type and persist from OAuth login
- Fetch fresh isPro status from HuggingFace public API on page load
- Display "Get PRO" button for non-PRO users in sidebar
- Display "PRO" badge for PRO users in sidebar
- Update SubscribeModal to show "Purchase Credits" for PRO users
- Add IconPro component for gradient sparkle icon

* feat: improve SubscribeModal UI for PRO users

- Use IconDazzled with yellow-orange-red gradient for "Out of Credits"
- Use IconPro with pink-green-yellow gradient for "Upgrade Required"
- Update credits message to mention HF services and Inference Providers

src/lib/components/NavMenu.svelte CHANGED
@@ -29,6 +29,8 @@
29
  import { useAPIClient, handleResponse } from "$lib/APIClient";
30
  import { requireAuthUser } from "$lib/utils/auth";
31
  import { enabledServersCount } from "$lib/stores/mcpServers";
 
 
32
  import MCPServerManager from "./mcp/MCPServerManager.svelte";
33
 
34
  const publicConfig = usePublicConfig();
@@ -171,16 +173,37 @@
171
  >
172
  {#if user?.username || user?.email}
173
  <div
174
- class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
175
  >
176
  <span
177
  class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
178
  >{user?.username || user?.email}</span
179
  >
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  <img
182
  src="https://huggingface.co/api/users/{user.username}/avatar?redirect=true"
183
- class="ml-auto size-4 rounded-full border bg-gray-500 dark:border-white/40"
 
 
184
  alt=""
185
  />
186
  </div>
 
29
  import { useAPIClient, handleResponse } from "$lib/APIClient";
30
  import { requireAuthUser } from "$lib/utils/auth";
31
  import { enabledServersCount } from "$lib/stores/mcpServers";
32
+ import { isPro } from "$lib/stores/isPro";
33
+ import IconPro from "$lib/components/icons/IconPro.svelte";
34
  import MCPServerManager from "./mcp/MCPServerManager.svelte";
35
 
36
  const publicConfig = usePublicConfig();
 
173
  >
174
  {#if user?.username || user?.email}
175
  <div
176
+ class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 first:hover:bg-transparent dark:hover:bg-gray-700 first:dark:hover:bg-transparent"
177
  >
178
  <span
179
  class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
180
  >{user?.username || user?.email}</span
181
  >
182
 
183
+ {#if publicConfig.isHuggingChat && $isPro === false}
184
+ <a
185
+ href="https://huggingface.co/subscribe/pro?from=HuggingChat"
186
+ target="_blank"
187
+ rel="noopener noreferrer"
188
+ class="ml-auto flex h-[20px] items-center gap-1 rounded-md bg-gradient-to-r from-pink-500/10 via-green-500/10 to-green-500/5 px-1.5 py-0.5 text-xs text-gray-500 hover:from-pink-500/20 hover:via-green-500/20 dark:from-pink-500/20 dark:via-green-500/20 dark:to-green-500/10 dark:text-gray-400 dark:hover:from-pink-500/30 dark:hover:via-green-500/30"
189
+ >
190
+ <IconPro />
191
+ Get PRO
192
+ </a>
193
+ {:else if publicConfig.isHuggingChat && $isPro === true}
194
+ <span
195
+ class="ml-auto flex h-[20px] items-center gap-1 rounded-md bg-gradient-to-r from-pink-500/10 via-green-500/10 to-green-500/5 px-1.5 py-0.5 text-xs text-gray-500 hover:from-pink-500/20 hover:via-green-500/20 dark:from-pink-500/20 dark:via-green-500/20 dark:to-green-500/10 dark:text-gray-400 dark:hover:from-pink-500/30 dark:hover:via-green-500/30"
196
+ >
197
+ <IconPro />
198
+ PRO
199
+ </span>
200
+ {/if}
201
+
202
  <img
203
  src="https://huggingface.co/api/users/{user.username}/avatar?redirect=true"
204
+ class="{!(publicConfig.isHuggingChat && $isPro !== null)
205
+ ? 'ml-auto'
206
+ : ''} size-4 rounded-full border bg-gray-500 dark:border-white/40"
207
  alt=""
208
  />
209
  </div>
src/lib/components/SubscribeModal.svelte CHANGED
@@ -1,5 +1,8 @@
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
 
 
 
3
 
4
  interface Props {
5
  close: () => void;
@@ -17,58 +20,62 @@
17
  >
18
  <div class="flex flex-col items-center justify-center gap-2.5 px-8 text-center">
19
  <div
20
- class="flex size-14 items-center justify-center rounded-full bg-gradient-to-br from-pink-500/15 from-15% via-green-500/15 to-yellow-500/15 text-3xl"
 
 
21
  >
22
- <svg
23
- width="1em"
24
- height="1em"
25
- viewBox="0 0 12 12"
26
- fill="none"
27
- xmlns="http://www.w3.org/2000/svg"
28
- >
29
- <path
30
- d="M6.48 1.26001C6.48 2.81001 7.15 3.84001 7.98 4.50001C8.84 5.18001 9.88 5.50001 10.56 5.57001V6.43001C9.6233 6.5513 8.73602 6.92071 7.99 7.50001C7.50131 7.88332 7.10989 8.37647 6.84753 8.93943C6.58516 9.50238 6.45925 10.1193 6.48 10.74H5.52C5.52 9.19001 4.85 8.16001 4.02 7.50001C3.27114 6.91907 2.3802 6.54958 1.44 6.43001V5.57001C2.37671 5.44872 3.26398 5.07931 4.01 4.50001C4.4987 4.1167 4.89011 3.62355 5.15248 3.06059C5.41484 2.49764 5.54076 1.88075 5.52 1.26001H6.48Z"
31
- fill="url(#paint0_linear_141_2)"
32
- />
33
- <defs>
34
- <linearGradient
35
- id="paint0_linear_141_2"
36
- x1="3.37"
37
- y1="3.43001"
38
- x2="8.14"
39
- y2="8.90001"
40
- gradientUnits="userSpaceOnUse"
41
- >
42
- <stop stop-color="#FF0789" />
43
- <stop offset="0.63" stop-color="#21DE75" />
44
- <stop offset="1" stop-color="#FF8D00" />
45
- </linearGradient>
46
- </defs>
47
- </svg>
48
  </div>
49
- <h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Upgrade Required</h2>
 
 
50
  </div>
51
  </div>
52
 
53
  <div class="text-gray-700 dark:text-gray-200">
54
- <p class="text-[15px] leading-relaxed">
55
- You've reached your message limit. Upgrade to Hugging Face PRO to continue using
56
- HuggingChat.
57
- </p>
58
- <p class="mt-3 text-[15px] italic leading-relaxed opacity-75">
59
- It's also possible to use your PRO credits in your favorite AI tools.
60
- </p>
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
 
63
  <div class="flex flex-col gap-2.5">
64
- <a
65
- href="https://huggingface.co/subscribe/pro?from=HuggingChat"
66
- target="_blank"
67
- rel="noopener noreferrer"
68
- class="w-full rounded-xl bg-black px-5 py-2.5 text-center text-base font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
69
- >
70
- Upgrade to Pro
71
- </a>
 
 
 
 
 
 
 
 
 
 
 
72
  <button
73
  class="w-full rounded-xl bg-gray-200 px-5 py-2.5 text-base font-medium text-gray-700 hover:bg-gray-300/80 dark:bg-white/5 dark:text-gray-200 dark:hover:bg-white/10"
74
  onclick={close}
 
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
3
+ import { isPro } from "$lib/stores/isPro";
4
+ import IconPro from "$lib/components/icons/IconPro.svelte";
5
+ import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
6
 
7
  interface Props {
8
  close: () => void;
 
20
  >
21
  <div class="flex flex-col items-center justify-center gap-2.5 px-8 text-center">
22
  <div
23
+ class="flex size-14 items-center justify-center rounded-full text-3xl {$isPro
24
+ ? 'bg-gradient-to-br from-yellow-500/15 via-orange-500/15 to-red-500/15'
25
+ : 'bg-gradient-to-br from-pink-500/15 from-15% via-green-500/15 to-yellow-500/15'}"
26
  >
27
+ {#if $isPro}
28
+ <IconDazzled />
29
+ {:else}
30
+ <IconPro classNames="!mr-0" />
31
+ {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
33
+ <h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
34
+ {$isPro ? "Out of Credits" : "Upgrade Required"}
35
+ </h2>
36
  </div>
37
  </div>
38
 
39
  <div class="text-gray-700 dark:text-gray-200">
40
+ {#if $isPro}
41
+ <p class="text-[15px] leading-relaxed">
42
+ You've used all your available credits. Purchase additional credits to continue using
43
+ HuggingChat.
44
+ </p>
45
+ <p class="mt-3 text-[15px] italic leading-relaxed opacity-75">
46
+ Your credits can be used in other HF services and external apps via Inference Providers.
47
+ </p>
48
+ {:else}
49
+ <p class="text-[15px] leading-relaxed">
50
+ You've reached your message limit. Upgrade to Hugging Face PRO to continue using
51
+ HuggingChat.
52
+ </p>
53
+ <p class="mt-3 text-[15px] italic leading-relaxed opacity-75">
54
+ It's also possible to use your PRO credits in your favorite AI tools.
55
+ </p>
56
+ {/if}
57
  </div>
58
 
59
  <div class="flex flex-col gap-2.5">
60
+ {#if $isPro}
61
+ <a
62
+ href="https://huggingface.co/settings/billing?add-credits=true"
63
+ target="_blank"
64
+ rel="noopener noreferrer"
65
+ class="w-full rounded-xl bg-black px-5 py-2.5 text-center text-base font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
66
+ >
67
+ Purchase Credits
68
+ </a>
69
+ {:else}
70
+ <a
71
+ href="https://huggingface.co/subscribe/pro?from=HuggingChat"
72
+ target="_blank"
73
+ rel="noopener noreferrer"
74
+ class="w-full rounded-xl bg-black px-5 py-2.5 text-center text-base font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
75
+ >
76
+ Upgrade to Pro
77
+ </a>
78
+ {/if}
79
  <button
80
  class="w-full rounded-xl bg-gray-200 px-5 py-2.5 text-base font-medium text-gray-700 hover:bg-gray-300/80 dark:bg-white/5 dark:text-gray-200 dark:hover:bg-white/10"
81
  onclick={close}
src/lib/components/icons/IconPro.svelte ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ classNames?: string;
4
+ }
5
+
6
+ let { classNames = "" }: Props = $props();
7
+
8
+ // I've no idea wht a fixed id doesnt work...
9
+ const gradientId = `gradient-${Math.random().toString(36).slice(2, 9)}`;
10
+ </script>
11
+
12
+ <svg
13
+ class="text-gray-500 {classNames}"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ xmlns:xlink="http://www.w3.org/1999/xlink"
16
+ role="img"
17
+ width="1em"
18
+ height="1em"
19
+ viewBox="0 0 12 12"
20
+ ><defs
21
+ ><linearGradient
22
+ id={gradientId}
23
+ x1="3.371"
24
+ y1="3.43"
25
+ x2="8.141"
26
+ y2="8.9"
27
+ gradientUnits="userSpaceOnUse"
28
+ ><stop stop-color="#FF0789" /><stop offset=".63" stop-color="#21DE75" /><stop
29
+ offset="1"
30
+ stop-color="#FF8D00"
31
+ /></linearGradient
32
+ ></defs
33
+ ><path
34
+ d="M6.481 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86a5.3 5.3 0 0 0-2.57 1.07 3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86a5.3 5.3 0 0 0 2.57-1.07 3.95 3.95 0 0 0 1.51-3.24h.96Z"
35
+ fill="url(#{gradientId})"
36
+ /></svg
37
+ >
src/lib/server/api/routes/groups/user.ts CHANGED
@@ -34,6 +34,7 @@ export const userGroup = new Elysia()
34
  email: locals.user.email,
35
  isAdmin: locals.user.isAdmin ?? false,
36
  isEarlyAccess: locals.user.isEarlyAccess ?? false,
 
37
  }
38
  : null;
39
  })
 
34
  email: locals.user.email,
35
  isAdmin: locals.user.isAdmin ?? false,
36
  isEarlyAccess: locals.user.isEarlyAccess ?? false,
37
+ isPro: locals.user.isPro ?? false,
38
  }
39
  : null;
40
  })
src/lib/stores/isPro.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+
3
+ // null = unknown/loading, true = PRO, false = not PRO
4
+ export const isPro = writable<boolean | null>(null);
src/lib/types/User.ts CHANGED
@@ -11,4 +11,5 @@ export interface User extends Timestamps {
11
  hfUserId: string;
12
  isAdmin?: boolean;
13
  isEarlyAccess?: boolean;
 
14
  }
 
11
  hfUserId: string;
12
  isAdmin?: boolean;
13
  isEarlyAccess?: boolean;
14
+ isPro?: boolean;
15
  }
src/routes/+layout.svelte CHANGED
@@ -19,6 +19,7 @@
19
  import { setContext } from "svelte";
20
  import { handleResponse, useAPIClient } from "$lib/APIClient";
21
  import { isAborted } from "$lib/stores/isAborted";
 
22
  import IconShare from "$lib/components/icons/IconShare.svelte";
23
  import { shareModal } from "$lib/stores/shareModal";
24
  import BackgroundGenerationPoller from "$lib/components/BackgroundGenerationPoller.svelte";
@@ -123,6 +124,18 @@
123
  const settings = createSettingsStore(data.settings);
124
 
125
  onMount(async () => {
 
 
 
 
 
 
 
 
 
 
 
 
126
  if (page.url.searchParams.has("model")) {
127
  await settings
128
  .instantSet({
 
19
  import { setContext } from "svelte";
20
  import { handleResponse, useAPIClient } from "$lib/APIClient";
21
  import { isAborted } from "$lib/stores/isAborted";
22
+ import { isPro } from "$lib/stores/isPro";
23
  import IconShare from "$lib/components/icons/IconShare.svelte";
24
  import { shareModal } from "$lib/stores/shareModal";
25
  import BackgroundGenerationPoller from "$lib/components/BackgroundGenerationPoller.svelte";
 
124
  const settings = createSettingsStore(data.settings);
125
 
126
  onMount(async () => {
127
+ if (publicConfig.isHuggingChat && data.user?.username) {
128
+ fetch(`https://huggingface.co/api/users/${data.user.username}/overview`)
129
+ .then((res) => res.json())
130
+ .then((userData) => {
131
+ isPro.set(userData.isPro ?? false);
132
+ })
133
+ .catch(() => {
134
+ // Fallback to database value on error
135
+ isPro.set(data.user?.isPro ?? false);
136
+ });
137
+ }
138
+
139
  if (page.url.searchParams.has("model")) {
140
  await settings
141
  .instantSet({
src/routes/login/callback/updateUser.ts CHANGED
@@ -39,6 +39,7 @@ export async function updateUser(params: {
39
  picture: avatarUrl,
40
  sub: hfUserId,
41
  orgs,
 
42
  } = z
43
  .object({
44
  preferred_username: z.string().optional(),
@@ -46,6 +47,7 @@ export async function updateUser(params: {
46
  picture: z.string().optional(),
47
  sub: z.string(),
48
  email: z.string().email().optional(),
 
49
  orgs: z
50
  .array(
51
  z.object({
@@ -72,6 +74,7 @@ export async function updateUser(params: {
72
  picture?: string;
73
  sub: string;
74
  name: string;
 
75
  orgs?: Array<{
76
  sub: string;
77
  name: string;
@@ -134,7 +137,7 @@ export async function updateUser(params: {
134
  // update existing user if any
135
  await collections.users.updateOne(
136
  { _id: existingUser._id },
137
- { $set: { username, name, avatarUrl, isAdmin, isEarlyAccess } }
138
  );
139
 
140
  // remove previous session if it exists and add new one
@@ -164,6 +167,7 @@ export async function updateUser(params: {
164
  hfUserId,
165
  isAdmin,
166
  isEarlyAccess,
 
167
  });
168
 
169
  userId = insertedId;
 
39
  picture: avatarUrl,
40
  sub: hfUserId,
41
  orgs,
42
+ isPro,
43
  } = z
44
  .object({
45
  preferred_username: z.string().optional(),
 
47
  picture: z.string().optional(),
48
  sub: z.string(),
49
  email: z.string().email().optional(),
50
+ isPro: z.boolean().optional(),
51
  orgs: z
52
  .array(
53
  z.object({
 
74
  picture?: string;
75
  sub: string;
76
  name: string;
77
+ isPro?: boolean;
78
  orgs?: Array<{
79
  sub: string;
80
  name: string;
 
137
  // update existing user if any
138
  await collections.users.updateOne(
139
  { _id: existingUser._id },
140
+ { $set: { username, name, avatarUrl, isAdmin, isEarlyAccess, isPro } }
141
  );
142
 
143
  // remove previous session if it exists and add new one
 
167
  hfUserId,
168
  isAdmin,
169
  isEarlyAccess,
170
+ isPro,
171
  });
172
 
173
  userId = insertedId;