enzostvs HF Staff commited on
Commit
db66673
·
0 Parent(s):

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +23 -0
  2. .npmrc +1 -0
  3. .prettierignore +9 -0
  4. .prettierrc +16 -0
  5. .vscode/settings.json +5 -0
  6. README.md +42 -0
  7. components.json +16 -0
  8. eslint.config.js +39 -0
  9. package.json +53 -0
  10. pnpm-lock.yaml +0 -0
  11. pnpm-workspace.yaml +2 -0
  12. src/app.d.ts +13 -0
  13. src/app.html +11 -0
  14. src/lib/actions/autofocus.ts +7 -0
  15. src/lib/assets/hf-logo.svg +8 -0
  16. src/lib/components/channel/Channel.svelte +29 -0
  17. src/lib/components/chat/Chat.svelte +220 -0
  18. src/lib/components/chat/FollowUp.svelte +220 -0
  19. src/lib/components/chat/Messages.svelte +26 -0
  20. src/lib/components/flow/FitViewOnResize.svelte +215 -0
  21. src/lib/components/loading/MainLoading.svelte +12 -0
  22. src/lib/components/loading/Spinner.svelte +24 -0
  23. src/lib/components/model/ComboBoxModels.svelte +82 -0
  24. src/lib/components/ui/button/button.svelte +85 -0
  25. src/lib/components/ui/button/index.ts +17 -0
  26. src/lib/components/ui/command/command-dialog.svelte +40 -0
  27. src/lib/components/ui/command/command-empty.svelte +17 -0
  28. src/lib/components/ui/command/command-group.svelte +32 -0
  29. src/lib/components/ui/command/command-input.svelte +26 -0
  30. src/lib/components/ui/command/command-item.svelte +20 -0
  31. src/lib/components/ui/command/command-link-item.svelte +20 -0
  32. src/lib/components/ui/command/command-list.svelte +17 -0
  33. src/lib/components/ui/command/command-loading.svelte +7 -0
  34. src/lib/components/ui/command/command-separator.svelte +17 -0
  35. src/lib/components/ui/command/command-shortcut.svelte +20 -0
  36. src/lib/components/ui/command/command.svelte +28 -0
  37. src/lib/components/ui/command/index.ts +37 -0
  38. src/lib/components/ui/dialog/dialog-close.svelte +7 -0
  39. src/lib/components/ui/dialog/dialog-content.svelte +45 -0
  40. src/lib/components/ui/dialog/dialog-description.svelte +17 -0
  41. src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
  42. src/lib/components/ui/dialog/dialog-header.svelte +20 -0
  43. src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
  44. src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  45. src/lib/components/ui/dialog/dialog-title.svelte +17 -0
  46. src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
  47. src/lib/components/ui/dialog/dialog.svelte +7 -0
  48. src/lib/components/ui/dialog/index.ts +34 -0
  49. src/lib/components/ui/sheet/index.ts +34 -0
  50. src/lib/components/ui/sheet/sheet-close.svelte +7 -0
.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+
3
+ # Output
4
+ .output
5
+ .vercel
6
+ .netlify
7
+ .wrangler
8
+ /.svelte-kit
9
+ /build
10
+
11
+ # OS
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Env
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+ !.env.test
20
+
21
+ # Vite
22
+ vite.config.js.timestamp-*
23
+ vite.config.ts.timestamp-*
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Package Managers
2
+ package-lock.json
3
+ pnpm-lock.yaml
4
+ yarn.lock
5
+ bun.lock
6
+ bun.lockb
7
+
8
+ # Miscellaneous
9
+ /static/
.prettierrc ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7
+ "overrides": [
8
+ {
9
+ "files": "*.svelte",
10
+ "options": {
11
+ "parser": "svelte"
12
+ }
13
+ }
14
+ ],
15
+ "tailwindStylesheet": "./src/routes/layout.css"
16
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "files.associations": {
3
+ "*.css": "tailwindcss"
4
+ }
5
+ }
README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sv
2
+
3
+ Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
4
+
5
+ ## Creating a project
6
+
7
+ If you're seeing this, you've probably already done this step. Congrats!
8
+
9
+ ```sh
10
+ # create a new project
11
+ npx sv create my-app
12
+ ```
13
+
14
+ To recreate this project with the same configuration:
15
+
16
+ ```sh
17
+ # recreate this project
18
+ pnpm dlx sv create --template minimal --types ts --add prettier eslint tailwindcss="plugins:typography" sveltekit-adapter="adapter:auto" --install pnpm hf-playground
19
+ ```
20
+
21
+ ## Developing
22
+
23
+ Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
24
+
25
+ ```sh
26
+ npm run dev
27
+
28
+ # or start the server and open the app in a new browser tab
29
+ npm run dev -- --open
30
+ ```
31
+
32
+ ## Building
33
+
34
+ To create a production version of your app:
35
+
36
+ ```sh
37
+ npm run build
38
+ ```
39
+
40
+ You can preview the production build with `npm run preview`.
41
+
42
+ > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
components.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://shadcn-svelte.com/schema.json",
3
+ "tailwind": {
4
+ "css": "src/routes/layout.css",
5
+ "baseColor": "gray"
6
+ },
7
+ "aliases": {
8
+ "components": "$lib/components",
9
+ "utils": "$lib/utils",
10
+ "ui": "$lib/components/ui",
11
+ "hooks": "$lib/hooks",
12
+ "lib": "$lib"
13
+ },
14
+ "typescript": true,
15
+ "registry": "https://shadcn-svelte.com/registry"
16
+ }
eslint.config.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prettier from 'eslint-config-prettier';
2
+ import path from 'node:path';
3
+ import { includeIgnoreFile } from '@eslint/compat';
4
+ import js from '@eslint/js';
5
+ import svelte from 'eslint-plugin-svelte';
6
+ import { defineConfig } from 'eslint/config';
7
+ import globals from 'globals';
8
+ import ts from 'typescript-eslint';
9
+ import svelteConfig from './svelte.config.js';
10
+
11
+ const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
12
+
13
+ export default defineConfig(
14
+ includeIgnoreFile(gitignorePath),
15
+ js.configs.recommended,
16
+ ...ts.configs.recommended,
17
+ ...svelte.configs.recommended,
18
+ prettier,
19
+ ...svelte.configs.prettier,
20
+ {
21
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
22
+ rules: {
23
+ // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
24
+ // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
25
+ 'no-undef': 'off'
26
+ }
27
+ },
28
+ {
29
+ files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
30
+ languageOptions: {
31
+ parserOptions: {
32
+ projectService: true,
33
+ extraFileExtensions: ['.svelte'],
34
+ parser: ts.parser,
35
+ svelteConfig
36
+ }
37
+ }
38
+ }
39
+ );
package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "hf-playground",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "prepare": "svelte-kit sync || echo ''",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "lint": "prettier --check . && eslint .",
14
+ "format": "prettier --write ."
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/compat": "^2.0.2",
18
+ "@eslint/js": "^9.39.2",
19
+ "@internationalized/date": "^3.11.0",
20
+ "@lucide/svelte": "^0.561.0",
21
+ "@sveltejs/adapter-auto": "^7.0.0",
22
+ "@sveltejs/kit": "^2.50.2",
23
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
24
+ "@tailwindcss/typography": "^0.5.19",
25
+ "@tailwindcss/vite": "^4.1.18",
26
+ "@types/node": "^24",
27
+ "bits-ui": "^2.15.5",
28
+ "eslint": "^9.39.2",
29
+ "eslint-config-prettier": "^10.1.8",
30
+ "eslint-plugin-svelte": "^3.14.0",
31
+ "globals": "^17.3.0",
32
+ "prettier": "^3.8.1",
33
+ "prettier-plugin-svelte": "^3.4.1",
34
+ "prettier-plugin-tailwindcss": "^0.7.2",
35
+ "svelte": "^5.49.2",
36
+ "svelte-check": "^4.3.6",
37
+ "tailwind-variants": "^3.2.2",
38
+ "tailwindcss": "^4.1.18",
39
+ "tw-animate-css": "^1.4.0",
40
+ "typescript": "^5.9.3",
41
+ "typescript-eslint": "^8.54.0",
42
+ "vite": "^7.3.1"
43
+ },
44
+ "dependencies": {
45
+ "@dagrejs/dagre": "^2.0.4",
46
+ "@huggingface/inference": "^4.13.12",
47
+ "@xyflow/svelte": "^1.5.0",
48
+ "clsx": "^2.1.1",
49
+ "elkjs": "^0.11.0",
50
+ "svelte-markdown": "^0.4.1",
51
+ "tailwind-merge": "^3.4.0"
52
+ }
53
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
pnpm-workspace.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ onlyBuiltDependencies:
2
+ - esbuild
src/app.d.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
+ }
12
+
13
+ export {};
src/app.html ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ %sveltekit.head%
7
+ </head>
8
+ <body data-sveltekit-preload-data="hover">
9
+ <div style="display: contents">%sveltekit.body%</div>
10
+ </body>
11
+ </html>
src/lib/actions/autofocus.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export function autofocus(node: HTMLElement) {
2
+ node.focus();
3
+ return {
4
+ destroy() {
5
+ }
6
+ };
7
+ }
src/lib/assets/hf-logo.svg ADDED
src/lib/components/channel/Channel.svelte ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Menu } from "@lucide/svelte";
3
+ import { X } from "@lucide/svelte";
4
+
5
+ import { Button } from "$lib/components/ui/button";
6
+ import * as Sheet from "$lib/components/ui/sheet";
7
+ </script>
8
+
9
+ <div class="">
10
+ <Sheet.Root>
11
+ <Sheet.Trigger asChild>
12
+ <Button variant="outline" size="icon">
13
+ <Menu />
14
+ </Button>
15
+ </Sheet.Trigger>
16
+ <Sheet.Content side="left">
17
+ <Sheet.Header>
18
+ <Sheet.Title>Channel</Sheet.Title>
19
+ </Sheet.Header>
20
+ <Sheet.Footer>
21
+ <Sheet.Close asChild>
22
+ <Button variant="outline" size="icon">
23
+ <X />
24
+ </Button>
25
+ </Sheet.Close>
26
+ </Sheet.Footer>
27
+ </Sheet.Content>
28
+ </Sheet.Root>
29
+ </div>
src/lib/components/chat/Chat.svelte ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Send, X } from '@lucide/svelte';
3
+ import { Handle, useEdges, useNodes, useNodesData, Position, type NodeProps, type Edge , type Node, useSvelteFlow} from '@xyflow/svelte';
4
+
5
+ import type { ChatModel, ChatMessage } from '$lib/helpers/types';
6
+ import { Button } from '$lib/components/ui/button';
7
+ import ComboBoxModels from '$lib/components/model/ComboBoxModels.svelte';
8
+ import Spinner from '$lib/components/loading/Spinner.svelte';
9
+ import { onMount } from 'svelte';
10
+ import Messages from './Messages.svelte';
11
+
12
+ let { id }: NodeProps = $props();
13
+
14
+ // svelte-ignore state_referenced_locally
15
+ const nodeData = useNodesData(id)
16
+ const { current: nodes, set: setNodes, update: updateNodes } = useNodes();
17
+ const { current: edges, set: setEdges, update: updateEdges } = useEdges();
18
+ const { fitView, updateNodeData } = useSvelteFlow();
19
+
20
+ let selectedModels = $state.raw<ChatModel[]>(nodeData.current?.data.selectedModels as ChatModel[] ?? []);
21
+ let prompt = $state.raw<string>(nodeData.current?.data.prompt as string ?? '');
22
+ let provider = $state.raw<string>('auto');
23
+ let loading = $state.raw<boolean>(false);
24
+ let aiCallDone = $state.raw<boolean>(false);
25
+
26
+ let messages = $state.raw<ChatMessage[]>([]);
27
+
28
+ function addModel(model: ChatModel) {
29
+ if (!selectedModels.some((m) => m.id === model.id)) {
30
+ selectedModels = [...selectedModels, model];
31
+ }
32
+ }
33
+ function removeModel(model: ChatModel) {
34
+ selectedModels = selectedModels.filter((m) => m.id !== model.id);
35
+ }
36
+
37
+ function handleTriggerAction() {
38
+ const newNodes: Node[] = [];
39
+ if (selectedModels.length > 1) {
40
+ updateNodeData(id, {
41
+ selectedModels: selectedModels.slice(1),
42
+ }, { replace: true });
43
+ selectedModels.slice(1).forEach((m) => {
44
+ const newNodeId = `chat-${crypto.randomUUID()}`;
45
+ const position = {
46
+ y: 0,
47
+ x: 630,
48
+ }
49
+ newNodes.push({
50
+ id: newNodeId,
51
+ type: 'chat',
52
+ position,
53
+ data: {
54
+ prompt,
55
+ selectedModels: [m],
56
+ },
57
+ });
58
+ });
59
+ selectedModels = selectedModels.slice(0, 1);
60
+ };
61
+ updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
62
+ fitView({
63
+ maxZoom: 1,
64
+ minZoom: 1,
65
+ interpolate: 'smooth',
66
+ duration: 500,
67
+ })
68
+ handleTriggerAiCall();
69
+ }
70
+
71
+ async function handleTriggerAiCall() {
72
+ const start = Date.now();
73
+ try {
74
+ messages = [...messages, { role: 'user', content: prompt }];
75
+ loading = true;
76
+ const response = await fetch('/api', {
77
+ method: 'POST',
78
+ body: JSON.stringify({
79
+ model: selectedModels[0].id,
80
+ prompt,
81
+ }),
82
+ });
83
+ if (!response.ok) throw new Error(response.statusText);
84
+ if (!response.body) throw new Error('No response body');
85
+
86
+ messages = [...messages, { role: 'assistant', content: '' }];
87
+ const assistantIndex = messages.length - 1;
88
+ let content = '';
89
+
90
+ const reader = response.body.getReader();
91
+ const decoder = new TextDecoder();
92
+
93
+ while (true) {
94
+ const { done, value } = await reader.read();
95
+ if (done) {
96
+ aiCallDone = true;
97
+ const newNodeId = `message-${crypto.randomUUID()}`;
98
+ const newNode: Node = {
99
+ id: newNodeId,
100
+ type: 'followUp',
101
+ position: {
102
+ x: 0,
103
+ y: 0,
104
+ },
105
+ data: {
106
+ // map messages to add isHidden: true
107
+ messages,
108
+ selectedModels,
109
+ },
110
+ }
111
+ const newEdge: Edge = {
112
+ id: `edge-${crypto.randomUUID()}`,
113
+ source: id,
114
+ target: newNodeId,
115
+ }
116
+ updateNodes((currentNodes) => [...currentNodes, newNode]);
117
+ updateEdges((currentEdges) => [...currentEdges, newEdge]);
118
+ break;
119
+ }
120
+
121
+ content += decoder.decode(value, { stream: true });
122
+ messages = messages.map((m, i) =>
123
+ i === assistantIndex ? { ...m, content } : m
124
+ );
125
+ }
126
+
127
+ messages = messages.map((m, i) =>
128
+ i === assistantIndex ? { ...m, content, timestamp: Date.now() - start } : m
129
+ );
130
+ } catch (error) {
131
+ console.error(error);
132
+ } finally {
133
+ loading = false;
134
+ }
135
+ }
136
+
137
+ onMount(() => {
138
+ if (nodeData.current?.data.prompt) {
139
+ handleTriggerAiCall();
140
+ }
141
+ });
142
+ </script>
143
+
144
+ <article class="bg-white border border-gray-200 shadow-lg/5 p-5 w-[600px] rounded-3xl">
145
+ <div class="nodrag">
146
+ <header class="flex items-center justify-between mb-3">
147
+ <div class="flex items-center gap-1 flex-wrap">
148
+ {#each selectedModels as model}
149
+ <Button variant="outline" size="sm" class="font-normal! shadow-none! relative group">
150
+ <img src={model.avatarUrl} alt={model.modelName} class="size-3.5 rounded-full" />
151
+ {model.modelName}
152
+ <Button variant="default" size="icon-3xs" class="!shadow-none! absolute -top-1 -right-1 rounded-full! opacity-0 group-hover:opacity-100 transition-opacity duration-300" onclick={() => removeModel(model)}>
153
+ <X class="size-3" />
154
+ </Button>
155
+ </Button>
156
+ {/each}
157
+ {#if selectedModels.length < 3 && !loading && !aiCallDone}
158
+ <ComboBoxModels
159
+ onSelect={addModel}
160
+ excludeIds={selectedModels.map((m) => m.id)}
161
+ />
162
+ {/if}
163
+ </div>
164
+ </header>
165
+ <Messages {messages} />
166
+ {#if !aiCallDone}
167
+ <footer class="flex transition-all duration-300 {!loading ? 'flex-col items-end' : 'items-start mt-4 gap-2'}">
168
+ {#if loading}
169
+ <input
170
+ name="message"
171
+ id="message"
172
+ placeholder="Ask me anything..."
173
+ disabled={loading}
174
+ class="w-full resize-none bg-transparent border rounded-lg py-1.5 px-3 outline-none text-sm text-muted-foreground"
175
+ bind:value={prompt}
176
+ onkeydown={(e: KeyboardEvent) => {
177
+ if (e.key === 'Enter' && !e.shiftKey) {
178
+ e.preventDefault();
179
+ prompt = prompt.trim();
180
+ if (prompt) {
181
+ handleTriggerAction();
182
+ }
183
+ }
184
+ }} />
185
+ {:else}
186
+ <textarea
187
+ name="message"
188
+ id="message"
189
+ placeholder="Ask me anything..."
190
+ disabled={loading}
191
+ class="w-full resize-none bg-transparent border-none outline-none text-base text-accent-foreground"
192
+ bind:value={prompt}
193
+ onkeydown={(e: KeyboardEvent) => {
194
+ if (e.key === 'Enter' && !e.shiftKey) {
195
+ e.preventDefault();
196
+ prompt = prompt.trim();
197
+ if (prompt) {
198
+ handleTriggerAction();
199
+ }
200
+ }
201
+ }}
202
+ ></textarea>
203
+ {/if}
204
+ <Button variant="outline" size="icon-sm" class="" disabled={!selectedModels.length || !prompt || loading} onclick={handleTriggerAction}>
205
+ {#if loading}
206
+ <Spinner className="size-5"/>
207
+ {:else}
208
+ <Send />
209
+ {/if}
210
+ </Button>
211
+ </footer>
212
+ {/if}
213
+ </div>
214
+ </article>
215
+ <Handle type="target" position={Position.Top} class="opacity-0"/>
216
+ <Handle type="target" position={Position.Left} class="opacity-0"/>
217
+ <Handle type="target" position={Position.Right} class="opacity-0"/>
218
+ <Handle type="source" position={Position.Bottom} class="opacity-0" />
219
+ <Handle type="source" position={Position.Left} class="opacity-0" />
220
+ <Handle type="source" position={Position.Right} class="opacity-0" />
src/lib/components/chat/FollowUp.svelte ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Send, X } from '@lucide/svelte';
3
+ import { Handle, useEdges, useNodes, useNodesData, Position, type NodeProps, type Edge , type Node, useSvelteFlow} from '@xyflow/svelte';
4
+
5
+ import type { ChatModel, ChatMessage } from '$lib/helpers/types';
6
+ import { Button } from '$lib/components/ui/button';
7
+ import ComboBoxModels from '$lib/components/model/ComboBoxModels.svelte';
8
+ import Spinner from '$lib/components/loading/Spinner.svelte';
9
+ import { onMount } from 'svelte';
10
+ import Messages from './Messages.svelte';
11
+
12
+ let { id }: NodeProps = $props();
13
+
14
+ // svelte-ignore state_referenced_locally
15
+ const nodeData = useNodesData(id)
16
+ const { current: nodes, set: setNodes, update: updateNodes } = useNodes();
17
+ const { current: edges, set: setEdges, update: updateEdges } = useEdges();
18
+ const { fitView, updateNodeData } = useSvelteFlow();
19
+
20
+ let selectedModels = $state.raw<ChatModel[]>(nodeData.current?.data.selectedModels as ChatModel[] ?? []);
21
+ let prompt = $state.raw<string>(nodeData.current?.data.prompt as string ?? '');
22
+ let provider = $state.raw<string>('auto');
23
+ let loading = $state.raw<boolean>(false);
24
+ let aiCallDone = $state.raw<boolean>(false);
25
+
26
+ let messages = $state.raw<ChatMessage[]>([]);
27
+
28
+ function addModel(model: ChatModel) {
29
+ if (!selectedModels.some((m) => m.id === model.id)) {
30
+ selectedModels = [...selectedModels, model];
31
+ }
32
+ }
33
+ function removeModel(model: ChatModel) {
34
+ selectedModels = selectedModels.filter((m) => m.id !== model.id);
35
+ }
36
+
37
+ function handleTriggerAction() {
38
+ const newNodes: Node[] = [];
39
+ if (selectedModels.length > 1) {
40
+ updateNodeData(id, {
41
+ selectedModels: selectedModels.slice(1),
42
+ }, { replace: true });
43
+ selectedModels.slice(1).forEach((m) => {
44
+ const newNodeId = `chat-${crypto.randomUUID()}`;
45
+ const position = {
46
+ y: 0,
47
+ x: 630,
48
+ }
49
+ newNodes.push({
50
+ id: newNodeId,
51
+ type: 'chat',
52
+ position,
53
+ data: {
54
+ prompt,
55
+ selectedModels: [m],
56
+ },
57
+ });
58
+ });
59
+ selectedModels = selectedModels.slice(0, 1);
60
+ };
61
+ updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
62
+ fitView({
63
+ maxZoom: 1,
64
+ minZoom: 1,
65
+ interpolate: 'smooth',
66
+ duration: 500,
67
+ })
68
+ handleTriggerAiCall();
69
+ }
70
+
71
+ async function handleTriggerAiCall() {
72
+ const start = Date.now();
73
+ try {
74
+ messages = [...messages, { role: 'user', content: prompt }];
75
+ loading = true;
76
+ const response = await fetch('/api', {
77
+ method: 'POST',
78
+ body: JSON.stringify({
79
+ model: selectedModels[0].id,
80
+ prompt,
81
+ }),
82
+ });
83
+ if (!response.ok) throw new Error(response.statusText);
84
+ if (!response.body) throw new Error('No response body');
85
+
86
+ messages = [...messages, { role: 'assistant', content: '' }];
87
+ const assistantIndex = messages.length - 1;
88
+ let content = '';
89
+
90
+ const reader = response.body.getReader();
91
+ const decoder = new TextDecoder();
92
+
93
+ while (true) {
94
+ const { done, value } = await reader.read();
95
+ if (done) {
96
+ aiCallDone = true;
97
+ console.log("AI CALL DONE")
98
+ const newNodeId = `message-${crypto.randomUUID()}`;
99
+ const newNode: Node = {
100
+ id: newNodeId,
101
+ type: 'follow-up',
102
+ position: {
103
+ x: 0,
104
+ y: 0,
105
+ },
106
+ data: {
107
+ messages,
108
+ selectedModels,
109
+ },
110
+ }
111
+ const newEdge: Edge = {
112
+ id: `edge-${crypto.randomUUID()}`,
113
+ source: id,
114
+ target: newNodeId,
115
+ }
116
+ updateNodes((currentNodes) => [...currentNodes, newNode]);
117
+ updateEdges((currentEdges) => [...currentEdges, newEdge]);
118
+ break;
119
+ }
120
+
121
+ content += decoder.decode(value, { stream: true });
122
+ messages = messages.map((m, i) =>
123
+ i === assistantIndex ? { ...m, content } : m
124
+ );
125
+ }
126
+
127
+ messages = messages.map((m, i) =>
128
+ i === assistantIndex ? { ...m, content, timestamp: Date.now() - start } : m
129
+ );
130
+ } catch (error) {
131
+ console.error(error);
132
+ } finally {
133
+ loading = false;
134
+ }
135
+ }
136
+
137
+ onMount(() => {
138
+ if (nodeData.current?.data.prompt) {
139
+ handleTriggerAiCall();
140
+ }
141
+ });
142
+ </script>
143
+
144
+ <article class="bg-white border border-gray-200 shadow-lg/5 p-5 w-[600px] rounded-3xl">
145
+ <div class="nodrag">
146
+ <header class="flex items-center justify-between mb-3">
147
+ <div class="flex items-center gap-1 flex-wrap">
148
+ {#each selectedModels as model}
149
+ <Button variant="outline" size="sm" class="font-normal! shadow-none! relative group">
150
+ <img src={model.avatarUrl} alt={model.modelName} class="size-3.5 rounded-full" />
151
+ {model.modelName}
152
+ <Button variant="default" size="icon-3xs" class="!shadow-none! absolute -top-1 -right-1 rounded-full! opacity-0 group-hover:opacity-100 transition-opacity duration-300" onclick={() => removeModel(model)}>
153
+ <X class="size-3" />
154
+ </Button>
155
+ </Button>
156
+ {/each}
157
+ {#if selectedModels.length < 3 && !loading && !aiCallDone}
158
+ <ComboBoxModels
159
+ onSelect={addModel}
160
+ excludeIds={selectedModels.map((m) => m.id)}
161
+ />
162
+ {/if}
163
+ </div>
164
+ </header>
165
+ <Messages {messages} />
166
+ {#if !aiCallDone}
167
+ <footer class="flex transition-all duration-300 {!loading ? 'flex-col items-end' : 'items-start mt-4 gap-2'}">
168
+ {#if loading}
169
+ <input
170
+ name="message"
171
+ id="message"
172
+ placeholder="Ask me anything..."
173
+ disabled={loading}
174
+ class="w-full resize-none bg-transparent border rounded-lg py-1.5 px-3 outline-none text-sm text-muted-foreground"
175
+ bind:value={prompt}
176
+ onkeydown={(e: KeyboardEvent) => {
177
+ if (e.key === 'Enter' && !e.shiftKey) {
178
+ e.preventDefault();
179
+ prompt = prompt.trim();
180
+ if (prompt) {
181
+ handleTriggerAction();
182
+ }
183
+ }
184
+ }} />
185
+ {:else}
186
+ <textarea
187
+ name="message"
188
+ id="message"
189
+ placeholder="Ask me anything..."
190
+ disabled={loading}
191
+ class="w-full resize-none bg-transparent border-none outline-none text-base text-accent-foreground"
192
+ bind:value={prompt}
193
+ onkeydown={(e: KeyboardEvent) => {
194
+ if (e.key === 'Enter' && !e.shiftKey) {
195
+ e.preventDefault();
196
+ prompt = prompt.trim();
197
+ if (prompt) {
198
+ handleTriggerAction();
199
+ }
200
+ }
201
+ }}
202
+ ></textarea>
203
+ {/if}
204
+ <Button variant="outline" size="icon-sm" class="" disabled={!selectedModels.length || !prompt || loading} onclick={handleTriggerAction}>
205
+ {#if loading}
206
+ <Spinner className="size-5"/>
207
+ {:else}
208
+ <Send />
209
+ {/if}
210
+ </Button>
211
+ </footer>
212
+ {/if}
213
+ </div>
214
+ </article>
215
+ <Handle type="target" position={Position.Top} class="opacity-0"/>
216
+ <Handle type="target" position={Position.Left} class="opacity-0"/>
217
+ <Handle type="target" position={Position.Right} class="opacity-0"/>
218
+ <Handle type="source" position={Position.Bottom} class="opacity-0" />
219
+ <Handle type="source" position={Position.Left} class="opacity-0" />
220
+ <Handle type="source" position={Position.Right} class="opacity-0" />
src/lib/components/chat/Messages.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import SvelteMarkdown from 'svelte-markdown';
3
+
4
+ import type { ChatMessage } from '$lib/helpers/types';
5
+
6
+ let { messages }: { messages: ChatMessage[] } = $props();
7
+ </script>
8
+
9
+ <main class="p-1 space-y-2 cursor-auto select-auto">
10
+ {#each messages as message}
11
+ <div class="flex items-center justify-end {message.role === 'user' ? 'justify-end' : 'justify-start'}">
12
+ <div class="flex flex-col justify-center items-start gap-1.5">
13
+ {#if message.role === 'user'}
14
+ <p class="text-sm text-muted-foreground bg-accent-foreground/5 px-2 py-1 rounded-md">{message.content}</p>
15
+ {:else}
16
+ <SvelteMarkdown source={message.content} />
17
+ {/if}
18
+ {#if message.timestamp}
19
+ <p class="text-[10px] text-muted-foreground bg-muted px-2 py-1 rounded-md font-mono">
20
+ {message.timestamp / 1000}s
21
+ </p>
22
+ {/if}
23
+ </div>
24
+ </div>
25
+ {/each}
26
+ </main>
src/lib/components/flow/FitViewOnResize.svelte ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import type { Node, Edge } from '@xyflow/svelte';
4
+ import { useNodes, useEdges } from '@xyflow/svelte';
5
+ import { useSvelteFlow } from '@xyflow/svelte';
6
+
7
+ let { initialNodes }: { initialNodes: Node[] } = $props();
8
+
9
+ // Fallback dimensions (used before nodes are measured by xyflow)
10
+ const DEFAULT_WIDTH = 600;
11
+ const DEFAULT_HEIGHT = 200;
12
+ const H_SPACING = 100;
13
+ const V_SPACING = 80;
14
+
15
+ const { fitView } = useSvelteFlow();
16
+ const nodesStore = useNodes();
17
+ const edgesStore = useEdges();
18
+
19
+ let lastLayoutKey = $state<string | null>(null);
20
+
21
+ const isChat = (n: Node) => n.type === 'chat';
22
+ const isFollowUp = (n: Node) => n.type === 'followUp' || n.type === 'follow-up';
23
+
24
+ /** Get the actual measured height of a node, or fallback */
25
+ function getMeasuredHeight(node: Node): number {
26
+ return node.measured?.height ?? DEFAULT_HEIGHT;
27
+ }
28
+
29
+ /** Get the actual measured width of a node, or fallback */
30
+ function getMeasuredWidth(node: Node): number {
31
+ return node.measured?.width ?? DEFAULT_WIDTH;
32
+ }
33
+
34
+ // Layout key: changes when nodes/edges are added/removed, OR when measured dimensions change
35
+ const layoutKey = $derived(
36
+ (() => {
37
+ const nodes = nodesStore.current;
38
+ const edges = edgesStore.current;
39
+ const ns = nodes.length === 0 ? initialNodes : nodes;
40
+ const es = nodes.length === 0 ? [] : edges;
41
+ const nodeIds = ns.map((n) => n.id).sort().join(',');
42
+ const edgeKeys = es.map((e) => `${e.source}-${e.target}`).sort().join(',');
43
+ // Include measured dimensions so layout re-runs when nodes get measured
44
+ const dims = ns.map((n) => `${n.id}:${n.measured?.width ?? 0}x${n.measured?.height ?? 0}`).sort().join(',');
45
+ return `${ns.length}-${es.length}-${nodeIds}-${edgeKeys}-${dims}`;
46
+ })()
47
+ );
48
+
49
+ $effect(() => {
50
+ const key = layoutKey;
51
+ if (key === lastLayoutKey) return;
52
+ lastLayoutKey = key;
53
+
54
+ const nodes = nodesStore.current;
55
+ const edges = edgesStore.current;
56
+ const ns = nodes.length === 0 ? initialNodes : nodes;
57
+ const es = nodes.length === 0 ? [] : edges;
58
+
59
+ runLayout(ns, es);
60
+ });
61
+
62
+ function handleWindowResize() {
63
+ fitView({
64
+ maxZoom: 1,
65
+ minZoom: 0.5,
66
+ interpolate: 'smooth',
67
+ duration: 500,
68
+ });
69
+ }
70
+
71
+ onMount(() => {
72
+ window.addEventListener('resize', handleWindowResize);
73
+ });
74
+
75
+ onDestroy(() => {
76
+ window.removeEventListener('resize', handleWindowResize);
77
+ });
78
+
79
+ /**
80
+ * Custom layout:
81
+ * - All chat nodes on the same horizontal row
82
+ * - Follow-up nodes below their source, using real measured heights
83
+ * - Multiple follow-ups for one source: stacked vertically
84
+ */
85
+ function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
86
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
87
+ const chatNodes = nodes.filter(isChat).sort((a, b) => a.id.localeCompare(b.id));
88
+ const followUpNodes = nodes.filter(isFollowUp);
89
+
90
+ // Build source->targets mapping from edges
91
+ const sourceByTarget = new Map<string, string>();
92
+ for (const e of edges) {
93
+ sourceByTarget.set(e.target, e.source);
94
+ }
95
+
96
+ const followUpsBySource = new Map<string, string[]>();
97
+ for (const node of followUpNodes) {
98
+ const sourceId = sourceByTarget.get(node.id);
99
+ if (sourceId) {
100
+ const list = followUpsBySource.get(sourceId) ?? [];
101
+ list.push(node.id);
102
+ followUpsBySource.set(sourceId, list);
103
+ }
104
+ }
105
+
106
+ const positions = new Map<string, { x: number; y: number }>();
107
+
108
+ // Chat nodes: same horizontal row, spaced by max width + H_SPACING
109
+ chatNodes.forEach((node, i) => {
110
+ positions.set(node.id, {
111
+ x: i * (getMeasuredWidth(node) + H_SPACING),
112
+ y: 0,
113
+ });
114
+ });
115
+
116
+ // Place follow-ups recursively, depth-first
117
+ const processed = new Set<string>();
118
+
119
+ const placeFollowUps = (sourceId: string) => {
120
+ const targets = followUpsBySource.get(sourceId);
121
+ if (!targets?.length) return;
122
+
123
+ const sourcePos = positions.get(sourceId);
124
+ if (!sourcePos) return;
125
+
126
+ const sourceNode = nodeMap.get(sourceId);
127
+ const sourceHeight = sourceNode ? getMeasuredHeight(sourceNode) : DEFAULT_HEIGHT;
128
+
129
+ let nextY = sourcePos.y + sourceHeight + V_SPACING;
130
+
131
+ for (const targetId of targets) {
132
+ if (processed.has(targetId)) continue;
133
+
134
+ positions.set(targetId, { x: sourcePos.x, y: nextY });
135
+ processed.add(targetId);
136
+
137
+ const targetNode = nodeMap.get(targetId);
138
+ const targetHeight = targetNode ? getMeasuredHeight(targetNode) : DEFAULT_HEIGHT;
139
+
140
+ // Recurse: place any follow-ups of this follow-up
141
+ placeFollowUps(targetId);
142
+
143
+ // Advance Y past this target and all its descendants
144
+ nextY = getSubtreeBottom(targetId, nodeMap, positions, followUpsBySource) + V_SPACING;
145
+ }
146
+ };
147
+
148
+ // Start from chat nodes
149
+ for (const chat of chatNodes) {
150
+ placeFollowUps(chat.id);
151
+ }
152
+
153
+ // Orphan follow-ups (no source found)
154
+ for (const node of followUpNodes) {
155
+ if (positions.has(node.id)) continue;
156
+ const allY = Array.from(positions.values()).map((p) => p.y);
157
+ const maxY = allY.length > 0 ? Math.max(...allY) : 0;
158
+ positions.set(node.id, {
159
+ x: 0,
160
+ y: maxY + DEFAULT_HEIGHT + V_SPACING,
161
+ });
162
+ }
163
+
164
+ return nodes.map((node) => ({
165
+ ...node,
166
+ position: positions.get(node.id) ?? { x: 0, y: 0 },
167
+ }));
168
+ }
169
+
170
+ /** Get the bottom Y edge of a node and all its follow-up descendants */
171
+ function getSubtreeBottom(
172
+ nodeId: string,
173
+ nodeMap: Map<string, Node>,
174
+ positions: Map<string, { x: number; y: number }>,
175
+ followUpsBySource: Map<string, string[]>,
176
+ ): number {
177
+ const pos = positions.get(nodeId);
178
+ const node = nodeMap.get(nodeId);
179
+ if (!pos) return 0;
180
+ const height = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
181
+ let bottom = pos.y + height;
182
+
183
+ const children = followUpsBySource.get(nodeId);
184
+ if (children) {
185
+ for (const childId of children) {
186
+ bottom = Math.max(bottom, getSubtreeBottom(childId, nodeMap, positions, followUpsBySource));
187
+ }
188
+ }
189
+ return bottom;
190
+ }
191
+
192
+ function runLayout(nodes: Node[], edges: Edge[]) {
193
+ const result = computeLayout(nodes, edges);
194
+
195
+ const current = nodesStore.current;
196
+ if (current.length === 0) {
197
+ nodesStore.set(result);
198
+ } else {
199
+ const positionMap = new Map(result.map((n) => [n.id, n.position]));
200
+ nodesStore.update((prev) =>
201
+ prev.map((n) => {
202
+ const pos = positionMap.get(n.id);
203
+ return pos ? { ...n, position: pos } : n;
204
+ })
205
+ );
206
+ }
207
+
208
+ fitView({
209
+ maxZoom: 1,
210
+ minZoom: 0.5,
211
+ interpolate: 'smooth',
212
+ duration: 250,
213
+ });
214
+ }
215
+ </script>
src/lib/components/loading/MainLoading.svelte ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import HuggingFaceLogo from '$lib/assets/hf-logo.svg';
3
+ import Spinner from './Spinner.svelte';
4
+ </script>
5
+
6
+ <div class="abs-center absolute flex flex-col items-center h-screen w-screen justify-center">
7
+ <img src={HuggingFaceLogo} alt="HF Logo" class="size-16" />
8
+ <p class="text-base text-muted-foreground mt-1 flex items-center justify-center gap-2">
9
+ Loading playground...
10
+ <Spinner className="size-5"/>
11
+ </p>
12
+ </div>
src/lib/components/loading/Spinner.svelte ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ let { className }: { className?: string } = $props();
3
+ </script>
4
+
5
+ <span class="loader {className}"></span>
6
+
7
+ <style>
8
+ .loader {
9
+ width: 20px;
10
+ padding: 4px;
11
+ aspect-ratio: 1;
12
+ border-radius: 50%;
13
+ background: #94a3b8;
14
+ --_m:
15
+ conic-gradient(#0000 10%,#000),
16
+ linear-gradient(#000 0 0) content-box;
17
+ -webkit-mask: var(--_m);
18
+ mask: var(--_m);
19
+ -webkit-mask-composite: source-out;
20
+ mask-composite: subtract;
21
+ animation: l3 1s infinite linear;
22
+ }
23
+ @keyframes l3 {to{transform: rotate(1turn)}}
24
+ </style>
src/lib/components/model/ComboBoxModels.svelte ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Plus, Info, ChevronLeft } from '@lucide/svelte';
3
+
4
+ import * as Command from '$lib/components/ui/command/';
5
+ import { Button } from '$lib/components/ui/button/';
6
+ import { modelsState } from '$lib/state/models.svelte';
7
+ import type { ChatModel } from '$lib/helpers/types';
8
+
9
+ interface Props {
10
+ onSelect?: (model: ChatModel) => void;
11
+ excludeIds?: string[];
12
+ }
13
+
14
+ let { onSelect, excludeIds = [] }: Props = $props();
15
+ let open = $state(false);
16
+ let search = $state('');
17
+
18
+ const filteredModels = $derived(
19
+ modelsState.models.filter((m) => {
20
+ const matchesSearch = !search || m.modelId.toLowerCase().includes(search.toLowerCase());
21
+ const notExcluded = !excludeIds.includes(m.id);
22
+ return matchesSearch && notExcluded;
23
+ }),
24
+ );
25
+
26
+ const MAX_TRENDING_MODELS = 6;
27
+
28
+ let trendingModels = $derived(filteredModels.slice(0, MAX_TRENDING_MODELS));
29
+ let otherModels = $derived(filteredModels.slice(MAX_TRENDING_MODELS));
30
+ </script>
31
+
32
+ <Button variant="outline" size="icon-sm" class="!shadow-none!" onclick={() => { open = true; search = ''; }}>
33
+ <Plus />
34
+ </Button>
35
+ {#if excludeIds.length === 0}
36
+ <p class="text-xs text-gray-600 ml-1 bg-gray-500/10 py-2 px-2.5 rounded-md relative flex items-center gap-1">
37
+ <ChevronLeft class="size-3 absolute -left-2 top-1/2 -translate-y-1/2 fill-gray-500/10 text-gray-500/10" />
38
+ <Info class="size-3" />
39
+ Please select at least one model.
40
+ </p>
41
+ {/if}
42
+ <Command.Dialog bind:open shouldFilter={false}>
43
+ <Command.Input bind:value={search} placeholder="Search models..." />
44
+ <Command.List>
45
+ {#if modelsState.loading}
46
+ <Command.Loading>Loading models...</Command.Loading>
47
+ {:else if modelsState.error}
48
+ <Command.Empty>{modelsState.error}</Command.Empty>
49
+ {:else if filteredModels.length === 0}
50
+ <Command.Empty>No results found.</Command.Empty>
51
+ {:else}
52
+ <Command.Group heading="🔥 Trending">
53
+ {#each trendingModels as model (model.id)}
54
+ <Command.Item
55
+ onSelect={() => {
56
+ onSelect?.(model);
57
+ open = false;
58
+ }}
59
+ >
60
+ <img src={model.avatarUrl} alt="" class="size-4 rounded-full" />
61
+ <span>{model.modelName}</span>
62
+ </Command.Item>
63
+ {/each}
64
+ <Command.Separator />
65
+ </Command.Group>
66
+ <Command.Group heading="Other models">
67
+ {#each otherModels as model (model.id)}
68
+ <Command.Item
69
+ onSelect={() => {
70
+ onSelect?.(model);
71
+ open = false;
72
+ }}
73
+ >
74
+ <span>
75
+ <span class="font-medium">{model.author}</span>/{model.modelName}
76
+ </span>
77
+ </Command.Item>
78
+ {/each}
79
+ </Command.Group>
80
+ {/if}
81
+ </Command.List>
82
+ </Command.Dialog>
src/lib/components/ui/button/button.svelte ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" module>
2
+ import { cn, type WithElementRef } from "$lib/utils.js";
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4
+ import { type VariantProps, tv } from "tailwind-variants";
5
+
6
+ export const buttonVariants = tv({
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:
14
+ "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs text-gray-600 dark:text-gray-400",
15
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
16
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
17
+ link: "text-primary underline-offset-4 hover:underline",
18
+ },
19
+ size: {
20
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
21
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
22
+ xs: "h-7 gap-1 rounded-md px-2.5 has-[>svg]:px-2",
23
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
24
+ icon: "size-9",
25
+ "icon-sm": "size-8",
26
+ "icon-lg": "size-10",
27
+ "icon-xs": "size-7",
28
+ "icon-3xs": "size-4",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ });
36
+
37
+ export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
38
+ export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
39
+
40
+ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
41
+ WithElementRef<HTMLAnchorAttributes> & {
42
+ variant?: ButtonVariant;
43
+ size?: ButtonSize;
44
+ };
45
+ </script>
46
+
47
+ <script lang="ts">
48
+ let {
49
+ class: className,
50
+ variant = "default",
51
+ size = "default",
52
+ ref = $bindable(null),
53
+ href = undefined,
54
+ type = "button",
55
+ disabled,
56
+ children,
57
+ ...restProps
58
+ }: ButtonProps = $props();
59
+ </script>
60
+
61
+ {#if href}
62
+ <a
63
+ bind:this={ref}
64
+ data-slot="button"
65
+ class={cn(buttonVariants({ variant, size }), className)}
66
+ href={disabled ? undefined : href}
67
+ aria-disabled={disabled}
68
+ role={disabled ? "link" : undefined}
69
+ tabindex={disabled ? -1 : undefined}
70
+ {...restProps}
71
+ >
72
+ {@render children?.()}
73
+ </a>
74
+ {:else}
75
+ <button
76
+ bind:this={ref}
77
+ data-slot="button"
78
+ class={cn(buttonVariants({ variant, size }), className)}
79
+ {type}
80
+ {disabled}
81
+ {...restProps}
82
+ >
83
+ {@render children?.()}
84
+ </button>
85
+ {/if}
src/lib/components/ui/button/index.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Root, {
2
+ type ButtonProps,
3
+ type ButtonSize,
4
+ type ButtonVariant,
5
+ buttonVariants,
6
+ } from "./button.svelte";
7
+
8
+ export {
9
+ Root,
10
+ type ButtonProps as Props,
11
+ //
12
+ Root as Button,
13
+ buttonVariants,
14
+ type ButtonProps,
15
+ type ButtonSize,
16
+ type ButtonVariant,
17
+ };
src/lib/components/ui/command/command-dialog.svelte ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
3
+ import type { Snippet } from "svelte";
4
+ import Command from "./command.svelte";
5
+ import * as Dialog from "$lib/components/ui/dialog/index.js";
6
+ import type { WithoutChildrenOrChild } from "$lib/utils.js";
7
+
8
+ let {
9
+ open = $bindable(false),
10
+ ref = $bindable(null),
11
+ value = $bindable(""),
12
+ title = "Command Palette",
13
+ description = "Search for a command to run",
14
+ portalProps,
15
+ children,
16
+ ...restProps
17
+ }: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
18
+ WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
19
+ portalProps?: DialogPrimitive.PortalProps;
20
+ children: Snippet;
21
+ title?: string;
22
+ description?: string;
23
+ } = $props();
24
+ </script>
25
+
26
+ <Dialog.Root bind:open {...restProps}>
27
+ <Dialog.Header class="sr-only">
28
+ <Dialog.Title>{title}</Dialog.Title>
29
+ <Dialog.Description>{description}</Dialog.Description>
30
+ </Dialog.Header>
31
+ <Dialog.Content class="overflow-hidden p-0" {portalProps}>
32
+ <Command
33
+ class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]]:px-2 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
34
+ {...restProps}
35
+ bind:value
36
+ bind:ref
37
+ {children}
38
+ />
39
+ </Dialog.Content>
40
+ </Dialog.Root>
src/lib/components/ui/command/command-empty.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: CommandPrimitive.EmptyProps = $props();
10
+ </script>
11
+
12
+ <CommandPrimitive.Empty
13
+ bind:ref
14
+ data-slot="command-empty"
15
+ class={cn("py-6 text-center text-sm", className)}
16
+ {...restProps}
17
+ />
src/lib/components/ui/command/command-group.svelte ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive, useId } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ heading,
10
+ value,
11
+ ...restProps
12
+ }: CommandPrimitive.GroupProps & {
13
+ heading?: string;
14
+ } = $props();
15
+ </script>
16
+
17
+ <CommandPrimitive.Group
18
+ bind:ref
19
+ data-slot="command-group"
20
+ class={cn("text-foreground overflow-hidden p-1", className)}
21
+ value={value ?? heading ?? `----${useId()}`}
22
+ {...restProps}
23
+ >
24
+ {#if heading}
25
+ <CommandPrimitive.GroupHeading
26
+ class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
27
+ >
28
+ {heading}
29
+ </CommandPrimitive.GroupHeading>
30
+ {/if}
31
+ <CommandPrimitive.GroupItems {children} />
32
+ </CommandPrimitive.Group>
src/lib/components/ui/command/command-input.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import SearchIcon from "@lucide/svelte/icons/search";
4
+ import { cn } from "$lib/utils.js";
5
+
6
+ let {
7
+ ref = $bindable(null),
8
+ class: className,
9
+ value = $bindable(""),
10
+ ...restProps
11
+ }: CommandPrimitive.InputProps = $props();
12
+ </script>
13
+
14
+ <div class="flex h-9 items-center gap-2 border-b ps-3 pe-8" data-slot="command-input-wrapper">
15
+ <SearchIcon class="size-4 shrink-0 opacity-50" />
16
+ <CommandPrimitive.Input
17
+ data-slot="command-input"
18
+ class={cn(
19
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
20
+ className
21
+ )}
22
+ bind:ref
23
+ {...restProps}
24
+ bind:value
25
+ />
26
+ </div>
src/lib/components/ui/command/command-item.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: CommandPrimitive.ItemProps = $props();
10
+ </script>
11
+
12
+ <CommandPrimitive.Item
13
+ bind:ref
14
+ data-slot="command-item"
15
+ class={cn(
16
+ "aria-selected:bg-accent cursor-pointer aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-0.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17
+ className
18
+ )}
19
+ {...restProps}
20
+ />
src/lib/components/ui/command/command-link-item.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: CommandPrimitive.LinkItemProps = $props();
10
+ </script>
11
+
12
+ <CommandPrimitive.LinkItem
13
+ bind:ref
14
+ data-slot="command-item"
15
+ class={cn(
16
+ "aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17
+ className
18
+ )}
19
+ {...restProps}
20
+ />
src/lib/components/ui/command/command-list.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: CommandPrimitive.ListProps = $props();
10
+ </script>
11
+
12
+ <CommandPrimitive.List
13
+ bind:ref
14
+ data-slot="command-list"
15
+ class={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
16
+ {...restProps}
17
+ />
src/lib/components/ui/command/command-loading.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+
4
+ let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
5
+ </script>
6
+
7
+ <CommandPrimitive.Loading bind:ref {...restProps} />
src/lib/components/ui/command/command-separator.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Command as CommandPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: CommandPrimitive.SeparatorProps = $props();
10
+ </script>
11
+
12
+ <CommandPrimitive.Separator
13
+ bind:ref
14
+ data-slot="command-separator"
15
+ class={cn("bg-border -mx-1 h-px", className)}
16
+ {...restProps}
17
+ />
src/lib/components/ui/command/command-shortcut.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "$lib/utils.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
11
+ </script>
12
+
13
+ <span
14
+ bind:this={ref}
15
+ data-slot="command-shortcut"
16
+ class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
17
+ {...restProps}
18
+ >
19
+ {@render children?.()}
20
+ </span>
src/lib/components/ui/command/command.svelte ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cn } from "$lib/utils.js";
3
+ import { Command as CommandPrimitive } from "bits-ui";
4
+
5
+ export type CommandRootApi = CommandPrimitive.Root;
6
+
7
+ let {
8
+ api = $bindable(null),
9
+ ref = $bindable(null),
10
+ value = $bindable(""),
11
+ class: className,
12
+ ...restProps
13
+ }: CommandPrimitive.RootProps & {
14
+ api?: CommandRootApi | null;
15
+ } = $props();
16
+ </script>
17
+
18
+ <CommandPrimitive.Root
19
+ bind:this={api}
20
+ bind:value
21
+ bind:ref
22
+ data-slot="command"
23
+ class={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className
26
+ )}
27
+ {...restProps}
28
+ />
src/lib/components/ui/command/index.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Root from "./command.svelte";
2
+ import Loading from "./command-loading.svelte";
3
+ import Dialog from "./command-dialog.svelte";
4
+ import Empty from "./command-empty.svelte";
5
+ import Group from "./command-group.svelte";
6
+ import Item from "./command-item.svelte";
7
+ import Input from "./command-input.svelte";
8
+ import List from "./command-list.svelte";
9
+ import Separator from "./command-separator.svelte";
10
+ import Shortcut from "./command-shortcut.svelte";
11
+ import LinkItem from "./command-link-item.svelte";
12
+
13
+ export {
14
+ Root,
15
+ Dialog,
16
+ Empty,
17
+ Group,
18
+ Item,
19
+ LinkItem,
20
+ Input,
21
+ List,
22
+ Separator,
23
+ Shortcut,
24
+ Loading,
25
+ //
26
+ Root as Command,
27
+ Dialog as CommandDialog,
28
+ Empty as CommandEmpty,
29
+ Group as CommandGroup,
30
+ Item as CommandItem,
31
+ LinkItem as CommandLinkItem,
32
+ Input as CommandInput,
33
+ List as CommandList,
34
+ Separator as CommandSeparator,
35
+ Shortcut as CommandShortcut,
36
+ Loading as CommandLoading,
37
+ };
src/lib/components/ui/dialog/dialog-close.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+
4
+ let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
5
+ </script>
6
+
7
+ <DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
src/lib/components/ui/dialog/dialog-content.svelte ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+ import DialogPortal from "./dialog-portal.svelte";
4
+ import XIcon from "@lucide/svelte/icons/x";
5
+ import type { Snippet } from "svelte";
6
+ import * as Dialog from "./index.js";
7
+ import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
8
+ import type { ComponentProps } from "svelte";
9
+
10
+ let {
11
+ ref = $bindable(null),
12
+ class: className,
13
+ portalProps,
14
+ children,
15
+ showCloseButton = true,
16
+ ...restProps
17
+ }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
18
+ portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
19
+ children: Snippet;
20
+ showCloseButton?: boolean;
21
+ } = $props();
22
+ </script>
23
+
24
+ <DialogPortal {...portalProps}>
25
+ <Dialog.Overlay />
26
+ <DialogPrimitive.Content
27
+ bind:ref
28
+ data-slot="dialog-content"
29
+ class={cn(
30
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
31
+ className
32
+ )}
33
+ {...restProps}
34
+ >
35
+ {@render children?.()}
36
+ {#if showCloseButton}
37
+ <DialogPrimitive.Close
38
+ class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
39
+ >
40
+ <XIcon />
41
+ <span class="sr-only">Close</span>
42
+ </DialogPrimitive.Close>
43
+ {/if}
44
+ </DialogPrimitive.Content>
45
+ </DialogPortal>
src/lib/components/ui/dialog/dialog-description.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: DialogPrimitive.DescriptionProps = $props();
10
+ </script>
11
+
12
+ <DialogPrimitive.Description
13
+ bind:ref
14
+ data-slot="dialog-description"
15
+ class={cn("text-muted-foreground text-sm", className)}
16
+ {...restProps}
17
+ />
src/lib/components/ui/dialog/dialog-footer.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "$lib/utils.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="dialog-footer"
16
+ class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
17
+ {...restProps}
18
+ >
19
+ {@render children?.()}
20
+ </div>
src/lib/components/ui/dialog/dialog-header.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import { cn, type WithElementRef } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="dialog-header"
16
+ class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
17
+ {...restProps}
18
+ >
19
+ {@render children?.()}
20
+ </div>
src/lib/components/ui/dialog/dialog-overlay.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: DialogPrimitive.OverlayProps = $props();
10
+ </script>
11
+
12
+ <DialogPrimitive.Overlay
13
+ bind:ref
14
+ data-slot="dialog-overlay"
15
+ class={cn(
16
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
17
+ className
18
+ )}
19
+ {...restProps}
20
+ />
src/lib/components/ui/dialog/dialog-portal.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+
4
+ let { ...restProps }: DialogPrimitive.PortalProps = $props();
5
+ </script>
6
+
7
+ <DialogPrimitive.Portal {...restProps} />
src/lib/components/ui/dialog/dialog-title.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: DialogPrimitive.TitleProps = $props();
10
+ </script>
11
+
12
+ <DialogPrimitive.Title
13
+ bind:ref
14
+ data-slot="dialog-title"
15
+ class={cn("text-lg leading-none font-semibold", className)}
16
+ {...restProps}
17
+ />
src/lib/components/ui/dialog/dialog-trigger.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+
4
+ let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
5
+ </script>
6
+
7
+ <DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
src/lib/components/ui/dialog/dialog.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+
4
+ let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
5
+ </script>
6
+
7
+ <DialogPrimitive.Root bind:open {...restProps} />
src/lib/components/ui/dialog/index.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Root from "./dialog.svelte";
2
+ import Portal from "./dialog-portal.svelte";
3
+ import Title from "./dialog-title.svelte";
4
+ import Footer from "./dialog-footer.svelte";
5
+ import Header from "./dialog-header.svelte";
6
+ import Overlay from "./dialog-overlay.svelte";
7
+ import Content from "./dialog-content.svelte";
8
+ import Description from "./dialog-description.svelte";
9
+ import Trigger from "./dialog-trigger.svelte";
10
+ import Close from "./dialog-close.svelte";
11
+
12
+ export {
13
+ Root,
14
+ Title,
15
+ Portal,
16
+ Footer,
17
+ Header,
18
+ Trigger,
19
+ Overlay,
20
+ Content,
21
+ Description,
22
+ Close,
23
+ //
24
+ Root as Dialog,
25
+ Title as DialogTitle,
26
+ Portal as DialogPortal,
27
+ Footer as DialogFooter,
28
+ Header as DialogHeader,
29
+ Trigger as DialogTrigger,
30
+ Overlay as DialogOverlay,
31
+ Content as DialogContent,
32
+ Description as DialogDescription,
33
+ Close as DialogClose,
34
+ };
src/lib/components/ui/sheet/index.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Root from "./sheet.svelte";
2
+ import Portal from "./sheet-portal.svelte";
3
+ import Trigger from "./sheet-trigger.svelte";
4
+ import Close from "./sheet-close.svelte";
5
+ import Overlay from "./sheet-overlay.svelte";
6
+ import Content from "./sheet-content.svelte";
7
+ import Header from "./sheet-header.svelte";
8
+ import Footer from "./sheet-footer.svelte";
9
+ import Title from "./sheet-title.svelte";
10
+ import Description from "./sheet-description.svelte";
11
+
12
+ export {
13
+ Root,
14
+ Close,
15
+ Trigger,
16
+ Portal,
17
+ Overlay,
18
+ Content,
19
+ Header,
20
+ Footer,
21
+ Title,
22
+ Description,
23
+ //
24
+ Root as Sheet,
25
+ Close as SheetClose,
26
+ Trigger as SheetTrigger,
27
+ Portal as SheetPortal,
28
+ Overlay as SheetOverlay,
29
+ Content as SheetContent,
30
+ Header as SheetHeader,
31
+ Footer as SheetFooter,
32
+ Title as SheetTitle,
33
+ Description as SheetDescription,
34
+ };
src/lib/components/ui/sheet/sheet-close.svelte ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from "bits-ui";
3
+
4
+ let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
5
+ </script>
6
+
7
+ <SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />