Spaces:
Running
Running
add auth
Browse files- src/lib/components/auth/SigninModal.svelte +12 -0
- src/lib/components/chat/User.svelte +12 -3
- src/lib/components/flow/actions/PanelCanvasActions.svelte +27 -11
- src/lib/components/flow/actions/PanelRightActions.svelte +50 -9
- src/lib/components/ui/button/button.svelte +4 -1
- src/lib/helpers/types.ts +8 -0
- src/lib/state/auth.svelte.ts +65 -0
- src/lib/state/signin-modal.svelte.ts +3 -0
- src/lib/state/view.svelte.ts +2 -1
- src/routes/+layout.svelte +6 -1
- src/routes/+page.svelte +4 -0
- src/routes/api/+server.ts +1 -0
- src/routes/api/auth/callback/+server.ts +72 -0
- src/routes/api/auth/login/+server.ts +23 -0
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-
|
| 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' : '
|
| 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
|
| 13 |
</script>
|
| 14 |
|
| 15 |
-
<Panel position="bottom-left" class="
|
| 16 |
<div
|
| 17 |
-
class="inline-flex w-fit flex-
|
| 18 |
>
|
| 19 |
<Button
|
| 20 |
variant="ghost"
|
| 21 |
-
size="icon-
|
| 22 |
onclick={() => zoomIn({ duration: 200 })}
|
| 23 |
disabled={getZoom() >= 1}
|
| 24 |
>
|
|
@@ -26,7 +25,7 @@
|
|
| 26 |
</Button>
|
| 27 |
<Button
|
| 28 |
variant="ghost"
|
| 29 |
-
size="icon-
|
| 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-
|
| 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-
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
</Button>
|
| 34 |
{/snippet}
|
| 35 |
</DropdownMenu.Trigger>
|
| 36 |
-
<DropdownMenu.Content class="w-56" align="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 60 |
-
<
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|