Spaces:
Running
Running
Commit ·
db66673
0
Parent(s):
initial commit
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +23 -0
- .npmrc +1 -0
- .prettierignore +9 -0
- .prettierrc +16 -0
- .vscode/settings.json +5 -0
- README.md +42 -0
- components.json +16 -0
- eslint.config.js +39 -0
- package.json +53 -0
- pnpm-lock.yaml +0 -0
- pnpm-workspace.yaml +2 -0
- src/app.d.ts +13 -0
- src/app.html +11 -0
- src/lib/actions/autofocus.ts +7 -0
- src/lib/assets/hf-logo.svg +8 -0
- src/lib/components/channel/Channel.svelte +29 -0
- src/lib/components/chat/Chat.svelte +220 -0
- src/lib/components/chat/FollowUp.svelte +220 -0
- src/lib/components/chat/Messages.svelte +26 -0
- src/lib/components/flow/FitViewOnResize.svelte +215 -0
- src/lib/components/loading/MainLoading.svelte +12 -0
- src/lib/components/loading/Spinner.svelte +24 -0
- src/lib/components/model/ComboBoxModels.svelte +82 -0
- src/lib/components/ui/button/button.svelte +85 -0
- src/lib/components/ui/button/index.ts +17 -0
- src/lib/components/ui/command/command-dialog.svelte +40 -0
- src/lib/components/ui/command/command-empty.svelte +17 -0
- src/lib/components/ui/command/command-group.svelte +32 -0
- src/lib/components/ui/command/command-input.svelte +26 -0
- src/lib/components/ui/command/command-item.svelte +20 -0
- src/lib/components/ui/command/command-link-item.svelte +20 -0
- src/lib/components/ui/command/command-list.svelte +17 -0
- src/lib/components/ui/command/command-loading.svelte +7 -0
- src/lib/components/ui/command/command-separator.svelte +17 -0
- src/lib/components/ui/command/command-shortcut.svelte +20 -0
- src/lib/components/ui/command/command.svelte +28 -0
- src/lib/components/ui/command/index.ts +37 -0
- src/lib/components/ui/dialog/dialog-close.svelte +7 -0
- src/lib/components/ui/dialog/dialog-content.svelte +45 -0
- src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
- src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
- src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
- src/lib/components/ui/dialog/dialog.svelte +7 -0
- src/lib/components/ui/dialog/index.ts +34 -0
- src/lib/components/ui/sheet/index.ts +34 -0
- 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} />
|