enzostvs HF Staff commited on
Commit
211077d
·
1 Parent(s): 39cd443
src/lib/components/auth/SigninModal.svelte ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as Dialog from '$lib/components/ui/dialog/index.js';
3
+ import { signinModalState } from '$lib/state/signin-modal.svelte';
4
+ </script>
5
+
6
+ <Dialog.Root bind:open={signinModalState.open}>
7
+ <Dialog.Content>
8
+ <Dialog.Header>
9
+ <Dialog.Title>Sign in</Dialog.Title>
10
+ </Dialog.Header>
11
+ </Dialog.Content>
12
+ </Dialog.Root>
src/lib/components/chat/User.svelte CHANGED
@@ -22,6 +22,8 @@
22
  import { MAX_MODELS_PER_NODE, MAX_SUGGESTIONS } from '$lib';
23
  import { SUGGESTIONS_PROMPT } from '$lib/consts';
24
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
 
 
25
 
26
  let { id, selected }: NodeProps = $props();
27
 
@@ -72,6 +74,10 @@
72
  }
73
 
74
  function handleTriggerAction(models: ChatModel[] = selectedModels) {
 
 
 
 
75
  const newNodes: Node[] = [];
76
  const newEdges: Edge[] = [];
77
  messages = [...messages, { role: 'user', content: prompt }];
@@ -123,7 +129,10 @@
123
  max_tokens: model.max_tokens,
124
  top_p: model.top_p
125
  }
126
- })
 
 
 
127
  });
128
  if (!response.ok) throw new Error(response.statusText);
129
  if (!response.body) throw new Error('No response body');
@@ -249,7 +258,7 @@
249
  }
250
  }}
251
  ></textarea>
252
- <div class="flex w-full items-center justify-between gap-1">
253
  {#if isFirstNode && !loading && !lastMessage}
254
  <div class="items flex w-full gap-1">
255
  {#each randomSuggestions as suggestion}
@@ -271,7 +280,7 @@
271
  <div></div>
272
  {/if}
273
  <Button
274
- variant={mode.current === 'dark' ? 'default' : 'outline'}
275
  size="icon-sm"
276
  class=""
277
  disabled={!selectedModels.length || !prompt || loading}
 
22
  import { MAX_MODELS_PER_NODE, MAX_SUGGESTIONS } from '$lib';
23
  import { SUGGESTIONS_PROMPT } from '$lib/consts';
24
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
25
+ import { authState } from '$lib/state/auth.svelte';
26
+ import { signinModalState } from '$lib/state/signin-modal.svelte';
27
 
28
  let { id, selected }: NodeProps = $props();
29
 
 
74
  }
75
 
76
  function handleTriggerAction(models: ChatModel[] = selectedModels) {
77
+ if (!authState.user) {
78
+ signinModalState.open = true;
79
+ return;
80
+ }
81
  const newNodes: Node[] = [];
82
  const newEdges: Edge[] = [];
83
  messages = [...messages, { role: 'user', content: prompt }];
 
129
  max_tokens: model.max_tokens,
130
  top_p: model.top_p
131
  }
132
+ }),
133
+ headers: {
134
+ Authorization: `Bearer ${authState.token ?? ''}`
135
+ }
136
  });
137
  if (!response.ok) throw new Error(response.statusText);
138
  if (!response.body) throw new Error('No response body');
 
258
  }
259
  }}
260
  ></textarea>
261
+ <div class="flex w-full items-end justify-between gap-1">
262
  {#if isFirstNode && !loading && !lastMessage}
263
  <div class="items flex w-full gap-1">
264
  {#each randomSuggestions as suggestion}
 
280
  <div></div>
281
  {/if}
282
  <Button
283
+ variant={mode.current === 'dark' ? 'default' : 'default'}
284
  size="icon-sm"
285
  class=""
286
  disabled={!selectedModels.length || !prompt || loading}
src/lib/components/flow/actions/PanelCanvasActions.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { Maximize, Lock, LockOpen, Minus, Plus } from '@lucide/svelte';
3
  import { Panel } from '@xyflow/svelte';
4
 
5
  import { Button } from '$lib/components/ui/button';
@@ -7,18 +7,17 @@
7
  import { viewState } from '$lib/state/view.svelte';
8
  import { useSvelteFlow } from '@xyflow/svelte';
9
  import HFLogo from '$lib/assets/hf-logo.svg';
10
- import { Separator } from '$lib/components/ui/separator';
11
 
12
- const { fitView, zoomIn, zoomOut, getZoom, getViewport } = useSvelteFlow();
13
  </script>
14
 
15
- <Panel position="bottom-left" class="space-y-2 p-1 lg:p-2">
16
  <div
17
- class="inline-flex w-fit flex-col gap-0.5 rounded-md border border-border bg-background p-0.5 dark:bg-gray-900"
18
  >
19
  <Button
20
  variant="ghost"
21
- size="icon-xs"
22
  onclick={() => zoomIn({ duration: 200 })}
23
  disabled={getZoom() >= 1}
24
  >
@@ -26,7 +25,7 @@
26
  </Button>
27
  <Button
28
  variant="ghost"
29
- size="icon-xs"
30
  onclick={() => zoomOut({ duration: 200 })}
31
  disabled={getZoom() <= 0.7}
32
  >
@@ -34,7 +33,7 @@
34
  </Button>
35
  <Button
36
  variant="ghost"
37
- size="icon-xs"
38
  onclick={() =>
39
  fitView({
40
  maxZoom: 1,
@@ -45,10 +44,9 @@
45
  >
46
  <Maximize />
47
  </Button>
48
- <Separator />
49
  <Button
50
  variant="ghost"
51
- size="icon-xs"
52
  onclick={() => (viewState.freeView = !viewState.freeView)}
53
  >
54
  {#if viewState.freeView}
@@ -59,9 +57,27 @@
59
  </Button>
60
  </div>
61
  <div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  class="flex items-center justify-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-3.5 pl-2.5 shadow-xs dark:bg-gray-900"
63
  >
64
  <img src={HFLogo} alt="HF Logo" class="size-5 lg:size-7" />
65
  <p class="text-xs text-accent-foreground">Hugging Face Playground</p>
66
- </div>
67
  </Panel>
 
1
  <script lang="ts">
2
+ import { Maximize, Lock, LockOpen, Minus, Plus, Move, MousePointer } from '@lucide/svelte';
3
  import { Panel } from '@xyflow/svelte';
4
 
5
  import { Button } from '$lib/components/ui/button';
 
7
  import { viewState } from '$lib/state/view.svelte';
8
  import { useSvelteFlow } from '@xyflow/svelte';
9
  import HFLogo from '$lib/assets/hf-logo.svg';
 
10
 
11
+ const { fitView, zoomIn, zoomOut, getZoom } = useSvelteFlow();
12
  </script>
13
 
14
+ <Panel position="bottom-left" class="flex items-center justify-start gap-2 p-1 lg:p-2">
15
  <div
16
+ class="inline-flex w-fit flex-row gap-0.5 rounded-lg border border-border bg-background p-1 dark:bg-gray-900"
17
  >
18
  <Button
19
  variant="ghost"
20
+ size="icon-sm"
21
  onclick={() => zoomIn({ duration: 200 })}
22
  disabled={getZoom() >= 1}
23
  >
 
25
  </Button>
26
  <Button
27
  variant="ghost"
28
+ size="icon-sm"
29
  onclick={() => zoomOut({ duration: 200 })}
30
  disabled={getZoom() <= 0.7}
31
  >
 
33
  </Button>
34
  <Button
35
  variant="ghost"
36
+ size="icon-sm"
37
  onclick={() =>
38
  fitView({
39
  maxZoom: 1,
 
44
  >
45
  <Maximize />
46
  </Button>
 
47
  <Button
48
  variant="ghost"
49
+ size="icon-sm"
50
  onclick={() => (viewState.freeView = !viewState.freeView)}
51
  >
52
  {#if viewState.freeView}
 
57
  </Button>
58
  </div>
59
  <div
60
+ class="inline-flex w-fit flex-row gap-0.5 rounded-lg border border-border bg-background p-1 dark:bg-gray-900"
61
+ >
62
+ <Button
63
+ variant={!viewState.draggable ? 'default' : 'ghost'}
64
+ size="icon-sm"
65
+ onclick={() => (viewState.draggable = false)}
66
+ >
67
+ <MousePointer />
68
+ </Button>
69
+ <Button
70
+ variant={viewState.draggable ? 'default' : 'ghost'}
71
+ size="icon-sm"
72
+ onclick={() => (viewState.draggable = true)}
73
+ >
74
+ <Move />
75
+ </Button>
76
+ </div>
77
+ <!-- <div
78
  class="flex items-center justify-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-3.5 pl-2.5 shadow-xs dark:bg-gray-900"
79
  >
80
  <img src={HFLogo} alt="HF Logo" class="size-5 lg:size-7" />
81
  <p class="text-xs text-accent-foreground">Hugging Face Playground</p>
82
+ </div> -->
83
  </Panel>
src/lib/components/flow/actions/PanelRightActions.svelte CHANGED
@@ -1,11 +1,21 @@
1
  <script lang="ts">
2
- import { Contrast, CreditCard, Monitor, Moon, RefreshCcw, Sun, User } from '@lucide/svelte';
 
 
 
 
 
 
 
 
 
3
  import { Panel } from '@xyflow/svelte';
4
  import { setMode } from 'mode-watcher';
5
 
6
  import { Button } from '$lib/components/ui/button';
7
  import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
8
  import { tokenModalState } from '$lib/state/token-modal.svelte';
 
9
  import defaultAvatar from '$lib/assets/default-avatar.svg';
10
 
11
  let { canReset }: { canReset: boolean } = $props();
@@ -28,17 +38,41 @@
28
  <DropdownMenu.Trigger>
29
  {#snippet child({ props })}
30
  <Button {...props} variant="outline">
31
- <img src={defaultAvatar} alt="User Avatar" class="size-4" />
32
- Account
 
 
 
 
 
 
 
 
 
33
  </Button>
34
  {/snippet}
35
  </DropdownMenu.Trigger>
36
- <DropdownMenu.Content class="w-56" align="start">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  <DropdownMenu.Group>
38
  <DropdownMenu.Item onclick={() => (tokenModalState.open = true)}>
39
  <CreditCard />
40
  Billings
41
- <!-- <DropdownMenu.Shortcut>⇧⌘T</DropdownMenu.Shortcut> -->
42
  </DropdownMenu.Item>
43
  <DropdownMenu.Sub>
44
  <DropdownMenu.SubTrigger>
@@ -56,10 +90,17 @@
56
  </DropdownMenu.Group>
57
  <DropdownMenu.Separator />
58
  <DropdownMenu.Group>
59
- <DropdownMenu.Item>
60
- <User />
61
- Sign In
62
- </DropdownMenu.Item>
 
 
 
 
 
 
 
63
  </DropdownMenu.Group>
64
  </DropdownMenu.Content>
65
  </DropdownMenu.Root>
 
1
  <script lang="ts">
2
+ import {
3
+ Contrast,
4
+ CreditCard,
5
+ LogOut,
6
+ Monitor,
7
+ Moon,
8
+ RefreshCcw,
9
+ Sun,
10
+ User
11
+ } from '@lucide/svelte';
12
  import { Panel } from '@xyflow/svelte';
13
  import { setMode } from 'mode-watcher';
14
 
15
  import { Button } from '$lib/components/ui/button';
16
  import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
17
  import { tokenModalState } from '$lib/state/token-modal.svelte';
18
+ import { authState, login, logout } from '$lib/state/auth.svelte';
19
  import defaultAvatar from '$lib/assets/default-avatar.svg';
20
 
21
  let { canReset }: { canReset: boolean } = $props();
 
38
  <DropdownMenu.Trigger>
39
  {#snippet child({ props })}
40
  <Button {...props} variant="outline">
41
+ {#if authState.user}
42
+ <img
43
+ src={authState.user.avatarUrl}
44
+ alt={authState.user.name}
45
+ class="size-4 rounded-full"
46
+ />
47
+ {authState.user.name}
48
+ {:else}
49
+ <img src={defaultAvatar} alt="User Avatar" class="size-4" />
50
+ Account
51
+ {/if}
52
  </Button>
53
  {/snippet}
54
  </DropdownMenu.Trigger>
55
+ <DropdownMenu.Content class="w-56" align="end">
56
+ {#if authState.user}
57
+ <DropdownMenu.Group>
58
+ <div class="flex items-center gap-2 px-2 py-1.5">
59
+ <img
60
+ src={authState.user.avatarUrl}
61
+ alt={authState.user.name}
62
+ class="size-8 rounded-full"
63
+ />
64
+ <div class="flex flex-col">
65
+ <span class="text-sm font-medium">{authState.user.name}</span>
66
+ <span class="text-xs text-muted-foreground">@{authState.user.username}</span>
67
+ </div>
68
+ </div>
69
+ </DropdownMenu.Group>
70
+ <DropdownMenu.Separator />
71
+ {/if}
72
  <DropdownMenu.Group>
73
  <DropdownMenu.Item onclick={() => (tokenModalState.open = true)}>
74
  <CreditCard />
75
  Billings
 
76
  </DropdownMenu.Item>
77
  <DropdownMenu.Sub>
78
  <DropdownMenu.SubTrigger>
 
90
  </DropdownMenu.Group>
91
  <DropdownMenu.Separator />
92
  <DropdownMenu.Group>
93
+ {#if authState.user}
94
+ <DropdownMenu.Item onclick={logout}>
95
+ <LogOut />
96
+ Sign Out
97
+ </DropdownMenu.Item>
98
+ {:else}
99
+ <DropdownMenu.Item onclick={login}>
100
+ <User />
101
+ Sign In with Hugging Face
102
+ </DropdownMenu.Item>
103
+ {/if}
104
  </DropdownMenu.Group>
105
  </DropdownMenu.Content>
106
  </DropdownMenu.Root>
src/lib/components/ui/button/button.svelte CHANGED
@@ -7,7 +7,10 @@
7
  base: "focus-visible:border-ring cursor-pointer focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
  variants: {
9
  variant: {
10
- default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
 
 
 
11
  destructive:
12
  'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs',
13
  outline:
 
7
  base: "focus-visible:border-ring cursor-pointer focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
  variants: {
9
  variant: {
10
+ default:
11
+ 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs disabled:bg-primary/50!',
12
+ highlight:
13
+ 'bg-linear-to-br from-blue-500 to-sky-400 text-white hover:brightness-110 shadow-xs',
14
  destructive:
15
  'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs',
16
  outline:
src/lib/helpers/types.ts CHANGED
@@ -18,3 +18,11 @@ export interface ChatMessage {
18
  timestamp?: number;
19
  isHidden?: boolean;
20
  }
 
 
 
 
 
 
 
 
 
18
  timestamp?: number;
19
  isHidden?: boolean;
20
  }
21
+
22
+ export interface HFUser {
23
+ id: string;
24
+ name: string;
25
+ username: string;
26
+ avatarUrl: string;
27
+ email?: string;
28
+ }
src/lib/state/auth.svelte.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SvelteURL, SvelteURLSearchParams } from 'svelte/reactivity';
2
+ import type { HFUser } from '$lib/helpers/types';
3
+
4
+ const AUTH_STORAGE_KEY = 'hf_auth';
5
+
6
+ interface AuthData {
7
+ token: string;
8
+ user: HFUser;
9
+ }
10
+
11
+ export const authState = $state<{ user: HFUser | null; token: string | null; loading: boolean }>({
12
+ user: null,
13
+ token: null,
14
+ loading: true
15
+ });
16
+
17
+ export async function initAuth() {
18
+ try {
19
+ authState.loading = true;
20
+ const stored = localStorage.getItem(AUTH_STORAGE_KEY);
21
+ if (stored) {
22
+ const data: AuthData = JSON.parse(stored);
23
+ authState.user = data.user;
24
+ authState.token = data.token;
25
+ }
26
+ } catch {
27
+ localStorage.removeItem(AUTH_STORAGE_KEY);
28
+ } finally {
29
+ authState.loading = false;
30
+ }
31
+ }
32
+
33
+ export function handleAuthCallback(): boolean {
34
+ const params = new SvelteURLSearchParams(window.location.search);
35
+ const authCallback = params.get('auth_callback');
36
+
37
+ if (!authCallback) return false;
38
+
39
+ try {
40
+ const data: AuthData = JSON.parse(decodeURIComponent(authCallback));
41
+ authState.user = data.user;
42
+ authState.token = data.token;
43
+ localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(data));
44
+
45
+ // Clean up the URL
46
+ const url = new SvelteURL(window.location.href);
47
+ url.searchParams.delete('auth_callback');
48
+ window.history.replaceState({}, '', url.pathname);
49
+
50
+ return true;
51
+ } catch (e) {
52
+ console.error('Failed to process auth callback:', e);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ export function logout() {
58
+ authState.user = null;
59
+ authState.token = null;
60
+ localStorage.removeItem(AUTH_STORAGE_KEY);
61
+ }
62
+
63
+ export function login() {
64
+ window.location.href = '/api/auth/login';
65
+ }
src/lib/state/signin-modal.svelte.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const signinModalState = $state({
2
+ open: false
3
+ });
src/lib/state/view.svelte.ts CHANGED
@@ -1,3 +1,4 @@
1
  export const viewState = $state({
2
- freeView: false
 
3
  });
 
1
  export const viewState = $state({
2
+ freeView: false,
3
+ draggable: true
4
  });
src/routes/+layout.svelte CHANGED
@@ -8,12 +8,16 @@
8
  import MainLoading from '$lib/components/loading/MainLoading.svelte';
9
  import TokenManagementModal from '$lib/components/token/TokenManagementModal.svelte';
10
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
 
 
11
 
12
  interface Props {
13
  children?: import('svelte').Snippet;
14
  }
15
 
16
  onMount(() => {
 
 
17
  fetchModels();
18
  handleBreakoints();
19
  window.addEventListener('resize', handleBreakoints);
@@ -41,10 +45,11 @@
41
  <TokenManagementModal />
42
  <svelte:boundary>
43
  <div class="min-h-screen overflow-hidden bg-white">
44
- {#if modelsState.loading}
45
  <MainLoading />
46
  {:else}
47
  {@render children?.()}
48
  {/if}
 
49
  </div>
50
  </svelte:boundary>
 
8
  import MainLoading from '$lib/components/loading/MainLoading.svelte';
9
  import TokenManagementModal from '$lib/components/token/TokenManagementModal.svelte';
10
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
11
+ import { initAuth, handleAuthCallback, authState } from '$lib/state/auth.svelte';
12
+ import SigninModal from '$lib/components/auth/SigninModal.svelte';
13
 
14
  interface Props {
15
  children?: import('svelte').Snippet;
16
  }
17
 
18
  onMount(() => {
19
+ handleAuthCallback();
20
+ initAuth();
21
  fetchModels();
22
  handleBreakoints();
23
  window.addEventListener('resize', handleBreakoints);
 
45
  <TokenManagementModal />
46
  <svelte:boundary>
47
  <div class="min-h-screen overflow-hidden bg-white">
48
+ {#if modelsState.loading || authState.loading}
49
  <MainLoading />
50
  {:else}
51
  {@render children?.()}
52
  {/if}
53
+ <SigninModal />
54
  </div>
55
  </svelte:boundary>
src/routes/+page.svelte CHANGED
@@ -22,6 +22,7 @@
22
  import { MAX_DEFAULT_MODELS } from '$lib';
23
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
24
  import PanelCanvasActions from '$lib/components/flow/actions/PanelCanvasActions.svelte';
 
25
 
26
  const nodeTypes = {
27
  user: User,
@@ -66,6 +67,7 @@
66
  fitView
67
  zoomOnScroll={false}
68
  panOnScroll={true}
 
69
  panOnScrollMode={PanOnScrollMode.Free}
70
  colorMode={mode.current}
71
  proOptions={{ hideAttribution: true }}
@@ -75,6 +77,8 @@
75
  interpolate: 'smooth',
76
  duration: 500
77
  }}
 
 
78
  onbeforedelete={() => Promise.resolve(false)}
79
  defaultEdgeOptions={{ type: 'smoothstep' }}
80
  class="bg-background!"
 
22
  import { MAX_DEFAULT_MODELS } from '$lib';
23
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
24
  import PanelCanvasActions from '$lib/components/flow/actions/PanelCanvasActions.svelte';
25
+ import { viewState } from '$lib/state/view.svelte';
26
 
27
  const nodeTypes = {
28
  user: User,
 
67
  fitView
68
  zoomOnScroll={false}
69
  panOnScroll={true}
70
+ panOnScrollSpeed={0.8}
71
  panOnScrollMode={PanOnScrollMode.Free}
72
  colorMode={mode.current}
73
  proOptions={{ hideAttribution: true }}
 
77
  interpolate: 'smooth',
78
  duration: 500
79
  }}
80
+ nodesDraggable={viewState.draggable}
81
+ panOnDrag={viewState.draggable}
82
  onbeforedelete={() => Promise.resolve(false)}
83
  defaultEdgeOptions={{ type: 'smoothstep' }}
84
  class="bg-background!"
src/routes/api/+server.ts CHANGED
@@ -37,6 +37,7 @@ export async function POST({ request }: RequestEvent) {
37
  for await (const chunk of stream) {
38
  const content = chunk.choices?.[0]?.delta?.content ?? '';
39
  console.log(chunk);
 
40
  if (content) {
41
  controller.enqueue(encoder.encode(content));
42
  }
 
37
  for await (const chunk of stream) {
38
  const content = chunk.choices?.[0]?.delta?.content ?? '';
39
  console.log(chunk);
40
+ // const usage = {};
41
  if (content) {
42
  controller.enqueue(encoder.encode(content));
43
  }
src/routes/api/auth/callback/+server.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { redirect, type RequestEvent } from '@sveltejs/kit';
2
+ import { env } from '$env/dynamic/private';
3
+
4
+ export async function GET({ url }: RequestEvent) {
5
+ const code = url.searchParams.get('code');
6
+
7
+ if (!code) {
8
+ return new Response('Missing authorization code', { status: 400 });
9
+ }
10
+
11
+ const clientId = env.HF_CLIENT_ID;
12
+ const clientSecret = env.HF_CLIENT_SECRET;
13
+ const redirectUri = env.HF_REDIRECT_URI;
14
+
15
+ if (!clientId || !clientSecret || !redirectUri) {
16
+ return new Response('Missing OAuth configuration', { status: 500 });
17
+ }
18
+
19
+ // Exchange code for access token
20
+ const tokenResponse = await fetch('https://huggingface.co/oauth/token', {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/x-www-form-urlencoded'
24
+ },
25
+ body: new URLSearchParams({
26
+ client_id: clientId,
27
+ client_secret: clientSecret,
28
+ code,
29
+ grant_type: 'authorization_code',
30
+ redirect_uri: redirectUri
31
+ })
32
+ });
33
+
34
+ if (!tokenResponse.ok) {
35
+ const error = await tokenResponse.text();
36
+ console.error('Token exchange failed:', error);
37
+ return new Response('Authentication failed', { status: 401 });
38
+ }
39
+
40
+ const tokenData = await tokenResponse.json();
41
+ const accessToken = tokenData.access_token;
42
+
43
+ // Fetch user info from HF
44
+ const userResponse = await fetch('https://huggingface.co/oauth/userinfo', {
45
+ headers: {
46
+ Authorization: `Bearer ${accessToken}`
47
+ }
48
+ });
49
+
50
+ if (!userResponse.ok) {
51
+ console.error('Failed to fetch user info');
52
+ return new Response('Failed to fetch user info', { status: 500 });
53
+ }
54
+
55
+ const userInfo = await userResponse.json();
56
+
57
+ // Encode auth data to pass to the client via query params
58
+ const authData = encodeURIComponent(
59
+ JSON.stringify({
60
+ token: accessToken,
61
+ user: {
62
+ id: userInfo.sub,
63
+ name: userInfo.name || userInfo.preferred_username,
64
+ username: userInfo.preferred_username,
65
+ avatarUrl: userInfo.picture,
66
+ email: userInfo.email
67
+ }
68
+ })
69
+ );
70
+
71
+ throw redirect(302, `/?auth_callback=${authData}`);
72
+ }
src/routes/api/auth/login/+server.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { redirect } from '@sveltejs/kit';
2
+ import { env } from '$env/dynamic/private';
3
+
4
+ export async function GET() {
5
+ const clientId = env.HF_CLIENT_ID;
6
+ const redirectUri = env.HF_REDIRECT_URI;
7
+
8
+ if (!clientId || !redirectUri) {
9
+ return new Response('Missing OAuth configuration', { status: 500 });
10
+ }
11
+
12
+ const state = crypto.randomUUID();
13
+
14
+ const params = new URLSearchParams({
15
+ client_id: clientId,
16
+ redirect_uri: redirectUri,
17
+ response_type: 'code',
18
+ scope: 'openid profile read-billing inference-api',
19
+ state
20
+ });
21
+
22
+ throw redirect(302, `https://huggingface.co/oauth/authorize?${params.toString()}`);
23
+ }