├── .env.example ├── .github └── workflows │ └── quality.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── components.json ├── drizzle.config.ts ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── app.css ├── app.d.ts ├── app.html ├── hooks.server.ts ├── hooks.ts ├── lib │ ├── ai │ │ └── models.ts │ ├── components │ │ ├── app-sidebar.svelte │ │ ├── artifact │ │ │ └── index.ts │ │ ├── auth-form.svelte │ │ ├── chat-header.svelte │ │ ├── chat.svelte │ │ ├── code-block.svelte │ │ ├── icons │ │ │ ├── arrow-up.svelte │ │ │ ├── check-circle-fill.svelte │ │ │ ├── chevron-down.svelte │ │ │ ├── chevron-up.svelte │ │ │ ├── globe.svelte │ │ │ ├── loader.svelte │ │ │ ├── lock.svelte │ │ │ ├── message.svelte │ │ │ ├── more-horizontal.svelte │ │ │ ├── panel-left.svelte │ │ │ ├── paperclip.svelte │ │ │ ├── pencil-edit.svelte │ │ │ ├── plus.svelte │ │ │ ├── share.svelte │ │ │ ├── sidebar-left.svelte │ │ │ ├── sparkles.svelte │ │ │ ├── stop.svelte │ │ │ ├── trash.svelte │ │ │ └── vercel.svelte │ │ ├── markdown │ │ │ ├── index.ts │ │ │ └── renderer.svelte │ │ ├── message-reasoning.svelte │ │ ├── messages.svelte │ │ ├── messages │ │ │ ├── overview.svelte │ │ │ ├── preview-message.svelte │ │ │ └── thinking-message.svelte │ │ ├── model-selector.svelte │ │ ├── multimodal-input.svelte │ │ ├── preview-attachment.svelte │ │ ├── sidebar-history │ │ │ ├── history.svelte │ │ │ ├── index.ts │ │ │ └── item.svelte │ │ ├── sidebar-toggle.svelte │ │ ├── sidebar-user-nav.svelte │ │ ├── submit-button.svelte │ │ ├── suggested-actions.svelte │ │ ├── ui │ │ │ ├── alert-dialog │ │ │ │ ├── alert-dialog-action.svelte │ │ │ │ ├── alert-dialog-cancel.svelte │ │ │ │ ├── alert-dialog-content.svelte │ │ │ │ ├── alert-dialog-description.svelte │ │ │ │ ├── alert-dialog-footer.svelte │ │ │ │ ├── alert-dialog-header.svelte │ │ │ │ ├── alert-dialog-overlay.svelte │ │ │ │ ├── alert-dialog-title.svelte │ │ │ │ ├── alert-dialog-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ ├── dropdown-menu-group.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ ├── dropdown-menu-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── sheet │ │ │ │ ├── index.ts │ │ │ │ ├── sheet-close.svelte │ │ │ │ ├── sheet-content.svelte │ │ │ │ ├── sheet-description.svelte │ │ │ │ ├── sheet-footer.svelte │ │ │ │ ├── sheet-header.svelte │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ ├── sheet-title.svelte │ │ │ │ └── sheet-trigger.svelte │ │ │ ├── sidebar │ │ │ │ ├── constants.ts │ │ │ │ ├── context.svelte.ts │ │ │ │ ├── index.ts │ │ │ │ ├── sidebar-content.svelte │ │ │ │ ├── sidebar-footer.svelte │ │ │ │ ├── sidebar-group-action.svelte │ │ │ │ ├── sidebar-group-content.svelte │ │ │ │ ├── sidebar-group-label.svelte │ │ │ │ ├── sidebar-group.svelte │ │ │ │ ├── sidebar-header.svelte │ │ │ │ ├── sidebar-input.svelte │ │ │ │ ├── sidebar-inset.svelte │ │ │ │ ├── sidebar-menu-action.svelte │ │ │ │ ├── sidebar-menu-badge.svelte │ │ │ │ ├── sidebar-menu-button.svelte │ │ │ │ ├── sidebar-menu-item.svelte │ │ │ │ ├── sidebar-menu-skeleton.svelte │ │ │ │ ├── sidebar-menu-sub-button.svelte │ │ │ │ ├── sidebar-menu-sub-item.svelte │ │ │ │ ├── sidebar-menu-sub.svelte │ │ │ │ ├── sidebar-menu.svelte │ │ │ │ ├── sidebar-provider.svelte │ │ │ │ ├── sidebar-rail.svelte │ │ │ │ ├── sidebar-separator.svelte │ │ │ │ ├── sidebar-trigger.svelte │ │ │ │ └── sidebar.svelte │ │ │ ├── skeleton │ │ │ │ ├── index.ts │ │ │ │ └── skeleton.svelte │ │ │ ├── sonner │ │ │ │ ├── index.ts │ │ │ │ └── sonner.svelte │ │ │ ├── textarea │ │ │ │ ├── index.ts │ │ │ │ └── textarea.svelte │ │ │ └── tooltip │ │ │ │ ├── index.ts │ │ │ │ ├── tooltip-content.svelte │ │ │ │ └── tooltip-trigger.svelte │ │ └── visibility-selector.svelte │ ├── errors │ │ ├── ai.ts │ │ ├── db.ts │ │ └── tagged-error.ts │ ├── hooks │ │ ├── chat-history.svelte.ts │ │ ├── is-mobile.svelte.ts │ │ ├── local-storage.svelte.ts │ │ ├── lock.ts │ │ └── selected-model.svelte.ts │ ├── server │ │ ├── ai │ │ │ ├── models.ts │ │ │ ├── prompts.ts │ │ │ └── utils.ts │ │ ├── auth │ │ │ ├── handle.ts │ │ │ └── index.ts │ │ └── db │ │ │ ├── migrate.ts │ │ │ ├── migrations │ │ │ ├── 0000_ambiguous_dragon_man.sql │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ └── _journal.json │ │ │ ├── queries.ts │ │ │ ├── schema.ts │ │ │ └── utils.ts │ └── utils │ │ ├── chat.ts │ │ ├── constants.ts │ │ ├── reactivity.svelte.ts │ │ ├── shadcn.ts │ │ └── types.ts ├── params │ └── authType.ts └── routes │ ├── (auth) │ ├── [authType=authType] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── signout │ │ └── +page.server.ts │ ├── (chat) │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── api │ │ ├── chat │ │ │ ├── +server.ts │ │ │ └── visibility │ │ │ │ └── +server.ts │ │ ├── files │ │ │ └── upload │ │ │ │ └── +server.ts │ │ ├── history │ │ │ └── +server.ts │ │ ├── suggestions │ │ │ └── [documentId] │ │ │ │ └── +server.ts │ │ ├── synchronized-cookie │ │ │ └── [cookieName] │ │ │ │ └── +server.ts │ │ └── vote │ │ │ └── [chatId] │ │ │ ├── +server.ts │ │ │ └── [messageId] │ │ │ └── +server.ts │ └── chat │ │ └── [chatId] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── +layout.svelte ├── static ├── favicon.png ├── fonts │ ├── geist-mono.woff2 │ └── geist.woff2 └── opengraph-image.png ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | # The following keys are automatically created when you deploy with Vercel 2 | # and use the suggested integrations. 3 | 4 | # Get your xAI API Key here for chat models: https://console.x.ai/ 5 | XAI_API_KEY=**** 6 | 7 | # Get your Groq API Key here for reasoning models: https://console.groq.com/keys 8 | GROQ_API_KEY=**** 9 | 10 | # Instructions to create a Vercel Blob Store here: https://vercel.com/docs/storage/vercel-blob 11 | BLOB_READ_WRITE_TOKEN=**** 12 | 13 | # Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart 14 | POSTGRES_URL=**** 15 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Enable corepack 16 | run: npm i -g --force corepack && corepack enable 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20.x 20 | cache: 'pnpm' 21 | 22 | - run: pnpm install 23 | env: 24 | CI: true 25 | 26 | - run: pnpm lint 27 | 28 | check: 29 | name: Type Check 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Enable corepack 34 | run: npm i -g --force corepack && corepack enable 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 20.x 38 | cache: 'pnpm' 39 | 40 | - run: pnpm install 41 | env: 42 | CI: true 43 | 44 | - run: pnpm check 45 | env: 46 | POSTGRES_URL: '' 47 | XAI_API_KEY: '' 48 | GROQ_API_KEY: '' 49 | BLOB_READ_WRITE_TOKEN: '' 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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-* 24 | .env*.local 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 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 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | SvelteKit AI chatbot. 3 |

SvelteKit AI Chatbot

4 |
5 | 6 |

7 | An Open-Source AI Chatbot Template Built With SvelteKit and the AI SDK by Vercel. 8 |

9 | 10 |

11 | Features · 12 | Model Providers · 13 | Deploy Your Own · 14 | Running locally 15 |

16 |
17 | 18 | ## Features 19 | 20 | - [SvelteKit + Svelte 5](https://svelte.dev) 21 | - [AI SDK](https://sdk.vercel.ai/docs) 22 | - Unified API for generating text, structured objects, and tool calls with LLMs 23 | - Hooks for building dynamic chat and generative user interfaces 24 | - Supports xAI (default), Groq, and other model providers 25 | - [shadcn-svelte](https://shadcn-svelte.com) 26 | - Styling with [Tailwind CSS](https://tailwindcss.com) 27 | - Component primitives from [Bits UI](https://www.bits-ui.com) for accessibility and flexibility 28 | - Data Persistence 29 | - [Vercel Postgres powered by Neon](https://vercel.com/storage/postgres) for saving chat history and user data 30 | - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage 31 | 32 | ## Model Providers 33 | 34 | This template ships with [xAI](https://x.ai) `grok-2-1212` as the default chat model. However, with the [AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://sdk.vercel.ai/providers/ai-sdk-providers) with just a few lines of code. 35 | 36 | ## Deploy Your Own 37 | 38 | You can deploy your own version of the SvelteKit AI Chatbot to Vercel with one click: 39 | 40 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot-svelte&project-name=my-awesome-chatbot&repository-name=my-awesome-chatbot&demo-title=AI%20Chatbot&demo-description=An%20Open-Source%20AI%20Chatbot%20Template%20Built%20With%20Next.js%20and%20the%20AI%20SDK%20by%20Vercel&demo-url=https%3A%2F%2Fsvelte-chat.vercel.ai&products=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22ai%22%2C%22productSlug%22%3A%22grok%22%2C%22integrationSlug%22%3A%22xai%22%7D%2C%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22ai%22%2C%22productSlug%22%3A%22api-key%22%2C%22integrationSlug%22%3A%22groq%22%7D%2C%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22productSlug%22%3A%22neon%22%2C%22integrationSlug%22%3A%22neon%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D) 41 | 42 | ## Running locally 43 | 44 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run SvelteKit AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. 45 | 46 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts. 47 | 48 | 1. Install Vercel CLI: `npm i -g vercel` 49 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 50 | 3. Download your environment variables: `vercel env pull` 51 | 52 | ```bash 53 | pnpm install 54 | pnpm db:generate 55 | pnpm dev 56 | ``` 57 | 58 | Your app template should now be running on [localhost:5173](http://localhost:5173/). 59 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "tailwind": { 4 | "css": "src/app.css", 5 | "baseColor": "zinc" 6 | }, 7 | "aliases": { 8 | "components": "$lib/components", 9 | "utils": "$lib/utils/shadcn", 10 | "ui": "$lib/components/ui", 11 | "hooks": "$lib/hooks", 12 | "lib": "$lib" 13 | }, 14 | "typescript": true, 15 | "registry": "https://next.shadcn-svelte.com/registry" 16 | } 17 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { defineConfig } from 'drizzle-kit'; 3 | 4 | config({ 5 | path: '.env.local' 6 | }); 7 | 8 | export default defineConfig({ 9 | schema: './src/lib/server/db/schema.ts', 10 | out: './src/lib/server/db/migrations', 11 | dialect: 'postgresql', 12 | dbCredentials: { 13 | url: process.env.POSTGRES_URL! 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | 10 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node } 22 | }, 23 | rules: { 'no-undef': 'off' } 24 | }, 25 | { 26 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 | languageOptions: { 28 | parserOptions: { 29 | projectService: true, 30 | extraFileExtensions: ['.svelte'], 31 | parser: ts.parser, 32 | svelteConfig 33 | } 34 | } 35 | }, 36 | { 37 | rules: { 38 | '@typescript-eslint/no-unused-vars': [ 39 | 'error', 40 | { 41 | args: 'all', 42 | argsIgnorePattern: '^_', 43 | caughtErrors: 'all', 44 | caughtErrorsIgnorePattern: '^_', 45 | destructuredArrayIgnorePattern: '^_', 46 | varsIgnorePattern: '^_', 47 | ignoreRestSiblings: true 48 | } 49 | ] 50 | } 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-chatbot-svelte", 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 | "format": "prettier --write .", 14 | "lint": "prettier --check . && eslint .", 15 | "db:generate": "drizzle-kit generate", 16 | "db:migrate": "npx --yes tsx src/lib/server/db/migrate.ts", 17 | "db:studio": "drizzle-kit studio", 18 | "db:push": "drizzle-kit push", 19 | "db:pull": "drizzle-kit pull", 20 | "db:check": "drizzle-kit check", 21 | "db:up": "drizzle-kit up" 22 | }, 23 | "devDependencies": { 24 | "@ai-sdk/groq": "^1.2.0", 25 | "@ai-sdk/svelte": "^2.1.0", 26 | "@ai-sdk/xai": "^1.2.1", 27 | "@eslint/compat": "^1.2.5", 28 | "@eslint/js": "^9.18.0", 29 | "@internationalized/date": "^3.5.6", 30 | "@lucide/svelte": "^0.511.0", 31 | "@oslojs/crypto": "^1.0.1", 32 | "@oslojs/encoding": "^1.1.0", 33 | "@sejohnson/svelte-themes": "^0.0.6", 34 | "@sveltejs/adapter-vercel": "^5.5.2", 35 | "@sveltejs/kit": "^2.16.0", 36 | "@sveltejs/vite-plugin-svelte": "^5.0.0", 37 | "@tailwindcss/vite": "^4.0.0", 38 | "@types/ms": "^2.1.0", 39 | "@vercel/blob": "^0.27.3", 40 | "ai": "^4.2.0", 41 | "autoprefixer": "^10.4.20", 42 | "bcrypt-ts": "^5.0.3", 43 | "bits-ui": "^2.4.1", 44 | "clsx": "^2.1.1", 45 | "date-fns": "^4.1.0", 46 | "dotenv": "^16.4.7", 47 | "drizzle-kit": "^0.30.4", 48 | "drizzle-orm": "^0.39.3", 49 | "eslint": "^9.18.0", 50 | "eslint-config-prettier": "^10.0.1", 51 | "eslint-plugin-svelte": "^3.0.0", 52 | "globals": "^16.0.0", 53 | "mode-watcher": "^1.0.7", 54 | "ms": "^2.1.3", 55 | "neverthrow": "^8.1.1", 56 | "postgres": "^3.4.5", 57 | "prettier": "^3.4.2", 58 | "prettier-plugin-svelte": "^3.3.3", 59 | "prettier-plugin-tailwindcss": "^0.6.11", 60 | "svelte": "^5.33.14", 61 | "svelte-check": "^4.0.0", 62 | "svelte-exmarkdown": "^5.0.0", 63 | "svelte-sonner": "^1.0.1", 64 | "tailwind-merge": "^3.0.2", 65 | "tailwind-variants": "^1.0.0", 66 | "tailwindcss": "^4.1.8", 67 | "tw-animate-css": "^1.3.4", 68 | "typescript": "^5.0.0", 69 | "typescript-eslint": "^8.20.0", 70 | "vite": "^6.0.0", 71 | "zod": "^3.24.2" 72 | }, 73 | "pnpm": { 74 | "onlyBuiltDependencies": [ 75 | "esbuild" 76 | ] 77 | }, 78 | "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912" 79 | } 80 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | 3 | import type { Session, User } from '$lib/server/db/schema'; 4 | 5 | // for information about these interfaces 6 | declare global { 7 | namespace App { 8 | // interface Error {} 9 | interface Locals { 10 | user?: User; 11 | session?: Session; 12 | } 13 | // interface PageData {} 14 | // interface PageState {} 15 | // interface Platform {} 16 | } 17 | } 18 | 19 | export {}; 20 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { sequence } from '@sveltejs/kit/hooks'; 2 | import { handle as authHandle } from '$lib/server/auth/handle'; 3 | 4 | export const handle = sequence(authHandle); 5 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ChatHistory } from '$lib/hooks/chat-history.svelte'; 2 | import { SelectedModel } from '$lib/hooks/selected-model.svelte'; 3 | import type { Transport } from '@sveltejs/kit'; 4 | 5 | export const transport: Transport = { 6 | SelectedModel: { 7 | encode: (value) => value instanceof SelectedModel && value.value, 8 | decode: (value) => new SelectedModel(value) 9 | }, 10 | ChatHistory: { 11 | encode: (value) => value instanceof ChatHistory && value.chats, 12 | decode: (value) => new ChatHistory(value) 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/ai/models.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_CHAT_MODEL: string = 'chat-model'; 2 | 3 | interface ChatModel { 4 | id: string; 5 | name: string; 6 | description: string; 7 | } 8 | 9 | export const chatModels: Array = [ 10 | { 11 | id: 'chat-model', 12 | name: 'Chat model', 13 | description: 'Primary model for all-purpose chat' 14 | }, 15 | { 16 | id: 'chat-model-reasoning', 17 | name: 'Reasoning model', 18 | description: 'Uses advanced reasoning' 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/lib/components/app-sidebar.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 25 | 26 |
27 | { 30 | context.setOpenMobile(false); 31 | }} 32 | class="flex flex-row items-center gap-3" 33 | > 34 | 35 | Chatbot 36 | 37 | 38 | 39 | 40 | {#snippet child({ props })} 41 | 53 | {/snippet} 54 | 55 | New Chat 56 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | {#if user} 65 | 66 | {/if} 67 | 68 |
69 | -------------------------------------------------------------------------------- /src/lib/components/artifact/index.ts: -------------------------------------------------------------------------------- 1 | export type ArtifactKind = 'text' | 'code' | 'image' | 'sheet'; 2 | -------------------------------------------------------------------------------- /src/lib/components/auth-form.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | 49 |
50 |
51 | 52 | 53 | 64 |
65 | 66 |
67 | 68 | 69 | 76 |
77 | 78 | {@render submitButton({ pending, success: !!form?.success })} 79 | {@render children()} 80 |
81 | -------------------------------------------------------------------------------- /src/lib/components/chat-header.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 29 | 30 | {#if !sidebar.open || (innerWidth.current ?? 768) < 768} 31 | 32 | 33 | {#snippet child({ props })} 34 | 47 | {/snippet} 48 | 49 | New Chat 50 | 51 | {/if} 52 | 53 | {#if !readonly} 54 | 55 | {/if} 56 | 57 | {#if !readonly && chat} 58 | 59 | {/if} 60 | 61 | {#if !user} 62 | 63 | {/if} 64 | 65 | 73 |
74 | -------------------------------------------------------------------------------- /src/lib/components/chat.svelte: -------------------------------------------------------------------------------- 1 | 62 | 63 |
64 | 65 | 70 | 71 |
72 | {#if !readonly} 73 | 74 | {/if} 75 | 76 |
77 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /src/lib/components/code-block.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if inline} 19 | 20 | {@render children?.()} 21 | 22 | {:else} 23 |
24 |
27 |           {@render children?.()}
28 |         
29 |
30 | {/if} 31 | -------------------------------------------------------------------------------- /src/lib/components/icons/arrow-up.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/check-circle-fill.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/chevron-down.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/chevron-up.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/lib/components/icons/globe.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/loader.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 23 | 24 | 30 | 36 | 42 | 48 | 54 | 60 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/lib/components/icons/lock.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/message.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/more-horizontal.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/panel-left.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /src/lib/components/icons/paperclip.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/icons/pencil-edit.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/plus.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/share.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/sidebar-left.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/sparkles.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 25 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /src/lib/components/icons/stop.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/icons/trash.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/vercel.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/markdown/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Markdown } from './renderer.svelte'; 2 | -------------------------------------------------------------------------------- /src/lib/components/markdown/renderer.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {#snippet ol(props)} 11 | {@const { children, ...rest } = props} 12 |
    13 | {@render children?.()} 14 |
15 | {/snippet} 16 | {#snippet ul(props)} 17 | {@const { children, ...rest } = props} 18 | 21 | {/snippet} 22 | {#snippet li(props)} 23 | {@const { children, ...rest } = props} 24 |
  • 25 | {@render children?.()} 26 |
  • 27 | {/snippet} 28 | 29 | {#snippet strong(props)} 30 | {@const { children, ...rest } = props} 31 | 32 | {@render children?.()} 33 | 34 | {/snippet} 35 | {#snippet a(props)} 36 | {@const { children, ...rest } = props} 37 | 43 | {@render children?.()} 44 | 45 | {/snippet} 46 | 47 | {#snippet h1(props)} 48 | {@const { children, ...rest } = props} 49 |

    50 | {@render children?.()} 51 |

    52 | {/snippet} 53 | {#snippet h2(props)} 54 | {@const { children, ...rest } = props} 55 |

    56 | {@render children?.()} 57 |

    58 | {/snippet} 59 | {#snippet h3(props)} 60 | {@const { children, ...rest } = props} 61 |

    62 | {@render children?.()} 63 |

    64 | {/snippet} 65 | {#snippet h4(props)} 66 | {@const { children, ...rest } = props} 67 |

    68 | {@render children?.()} 69 |

    70 | {/snippet} 71 | {#snippet h5(props)} 72 | {@const { children, ...rest } = props} 73 |
    74 | {@render children?.()} 75 |
    76 | {/snippet} 77 | {#snippet h6(props)} 78 | {@const { children, ...rest } = props} 79 |
    80 | {@render children?.()} 81 |
    82 | {/snippet} 83 | {#snippet code(props)} 84 | 85 | {/snippet} 86 |
    87 | -------------------------------------------------------------------------------- /src/lib/components/message-reasoning.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
    25 | {#if loading} 26 |
    27 |
    Reasoning
    28 |
    29 | 30 |
    31 |
    32 | {:else} 33 |
    34 |
    Reasoned for a few seconds
    35 | 36 | 37 |
    { 40 | expanded = !expanded; 41 | }} 42 | > 43 | 44 |
    45 |
    46 | {/if} 47 | 48 | {#if expanded} 49 |
    57 | 58 |
    59 | {/if} 60 |
    61 | -------------------------------------------------------------------------------- /src/lib/components/messages.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
    49 | {#if mounted && messages.length === 0} 50 | 51 | {/if} 52 | 53 | {#each messages as message (message.id)} 54 | 55 | {/each} 56 | 57 | {#if loading && messages.length > 0 && messages[messages.length - 1].role === 'user'} 58 | 59 | {/if} 60 | 61 |
    62 |
    63 | -------------------------------------------------------------------------------- /src/lib/components/messages/overview.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 |
    9 |

    10 | 11 | + 12 | 13 |

    14 |

    15 | This is an 16 | 21 | open source 22 | 23 | chatbot template built with SvelteKit and the AI SDK by Vercel. It uses the 24 | streamText 25 | function in the server and the 26 | useChat hook on the client to create a seamless 27 | chat experience. 28 |

    29 |

    30 | You can learn more about the AI SDK by visiting the 31 | 36 | docs 37 | 38 | . 39 |

    40 |
    41 |
    42 | -------------------------------------------------------------------------------- /src/lib/components/messages/preview-message.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
    24 |
    33 | {#if message.role === 'assistant'} 34 |
    37 |
    38 | 39 |
    40 |
    41 | {/if} 42 | 43 |
    44 | {#if message.experimental_attachments && message.experimental_attachments.length > 0} 45 |
    46 | {#each message.experimental_attachments as attachment (attachment.url)} 47 | 48 | {/each} 49 |
    50 | {/if} 51 | 52 | {#each message.parts as part, i (`${message.id}-${i}`)} 53 | {@const { type } = part} 54 | {#if type === 'reasoning'} 55 | 56 | {:else if type === 'text'} 57 | {#if mode === 'view'} 58 |
    59 | {#if message.role === 'user' && !readonly} 60 | 61 | 62 | {#snippet child({ props })} 63 | 73 | {/snippet} 74 | 75 | Edit message 76 | 77 | {/if} 78 |
    83 | 84 |
    85 |
    86 | {:else if mode === 'edit'} 87 |
    88 |
    89 | 90 | 91 | 92 |
    93 | {/if} 94 | 95 | 96 | 133 | {/if} 134 | {/each} 135 | 136 | 137 | 140 |
    141 |
    142 |
    143 | -------------------------------------------------------------------------------- /src/lib/components/messages/thinking-message.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    11 |
    18 |
    19 | 20 |
    21 | 22 |
    23 |
    Hmm...
    24 |
    25 |
    26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/model-selector.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | (open = val)}> 30 | 31 | {#snippet child({ props })} 32 | 43 | {/snippet} 44 | 45 | 46 | {#each chatModels as chatModel (chatModel.id)} 47 | { 49 | open = false; 50 | selectedChatModel.value = chatModel.id; 51 | }} 52 | class="group/item flex flex-row items-center justify-between gap-4" 53 | data-active={chatModel.id === selectedChatModel.value} 54 | > 55 |
    56 |
    {chatModel.name}
    57 |
    58 | {chatModel.description} 59 |
    60 |
    61 | 62 |
    65 | 66 |
    67 |
    68 | {/each} 69 |
    70 |
    71 | -------------------------------------------------------------------------------- /src/lib/components/preview-attachment.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 |
    20 | {#if contentType && contentType.startsWith('image')} 21 | {name 26 | {:else} 27 |
    28 | {/if} 29 | 30 | {#if uploading} 31 |
    32 | 33 |
    34 | {/if} 35 |
    36 |
    {name}
    37 |
    38 | -------------------------------------------------------------------------------- /src/lib/components/sidebar-history/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SidebarHistory } from './history.svelte'; 2 | -------------------------------------------------------------------------------- /src/lib/components/sidebar-history/item.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | {#snippet child({ props })} 41 | 50 | {/snippet} 51 | 52 | 53 | 54 | 55 | {#snippet child({ props })} 56 | 61 | 62 | More 63 | 64 | {/snippet} 65 | 66 | 67 | 68 | 69 | 70 | 71 | Share 72 | 73 | 74 | { 77 | chatHistory.updateVisibility(chat.id, 'private'); 78 | }} 79 | > 80 |
    81 | 82 | Private 83 |
    84 | {#if chatFromHistory?.visibility === 'private'} 85 | 86 | {/if} 87 |
    88 | { 91 | chatHistory.updateVisibility(chat.id, 'public'); 92 | }} 93 | > 94 |
    95 | 96 | Public 97 |
    98 | {#if chatFromHistory?.visibility === 'public'} 99 | 100 | {/if} 101 |
    102 |
    103 |
    104 | 105 | ondelete(chat.id)} 108 | > 109 | 110 | Delete 111 | 112 |
    113 |
    114 |
    115 | -------------------------------------------------------------------------------- /src/lib/components/sidebar-toggle.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | {#snippet child({ props })} 13 | 23 | {/snippet} 24 | 25 | Toggle Sidebar 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/sidebar-user-nav.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | {#snippet child({ props })} 24 | 28 | {user.email 35 | {user?.email} 36 | 37 | 38 | {/snippet} 39 | 40 | 41 | 44 | (theme.selectedTheme = theme.resolvedTheme === 'light' ? 'dark' : 'light')} 45 | > 46 | Toggle {theme.resolvedTheme === 'light' ? 'dark' : 'light'} mode 47 | 48 | 49 | 50 | {#snippet child({ props })} 51 | Sign out 58 | {/snippet} 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/lib/components/submit-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/lib/components/suggested-actions.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
    35 | {#each suggestedActions as suggestedAction, i (suggestedAction.title)} 36 |
    1 ? 'hidden sm:block' : 'block'} 39 | > 40 | 58 |
    59 | {/each} 60 |
    61 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; 2 | import Trigger from './alert-dialog-trigger.svelte'; 3 | import Title from './alert-dialog-title.svelte'; 4 | import Action from './alert-dialog-action.svelte'; 5 | import Cancel from './alert-dialog-cancel.svelte'; 6 | import Footer from './alert-dialog-footer.svelte'; 7 | import Header from './alert-dialog-header.svelte'; 8 | import Overlay from './alert-dialog-overlay.svelte'; 9 | import Content from './alert-dialog-content.svelte'; 10 | import Description from './alert-dialog-description.svelte'; 11 | import type { ComponentProps } from 'svelte'; 12 | 13 | const Root = AlertDialogPrimitive.Root; 14 | const Portal = AlertDialogPrimitive.Portal; 15 | 16 | type AlertDialogProps = ComponentProps; 17 | type AlertDialogTriggerProps = ComponentProps; 18 | type AlertDialogPortalProps = ComponentProps; 19 | type AlertDialogTitleProps = ComponentProps; 20 | type AlertDialogActionProps = ComponentProps; 21 | type AlertDialogCancelProps = ComponentProps; 22 | type AlertDialogFooterProps = ComponentProps; 23 | type AlertDialogHeaderProps = ComponentProps; 24 | type AlertDialogOverlayProps = ComponentProps; 25 | type AlertDialogContentProps = ComponentProps; 26 | type AlertDialogDescriptionProps = ComponentProps; 27 | 28 | export { 29 | Root, 30 | Title, 31 | Action, 32 | Cancel, 33 | Portal, 34 | Footer, 35 | Header, 36 | Trigger, 37 | Overlay, 38 | Content, 39 | Description, 40 | // 41 | Root as AlertDialog, 42 | type AlertDialogProps, 43 | Title as AlertDialogTitle, 44 | type AlertDialogTitleProps, 45 | Action as AlertDialogAction, 46 | type AlertDialogActionProps, 47 | Cancel as AlertDialogCancel, 48 | type AlertDialogCancelProps, 49 | Portal as AlertDialogPortal, 50 | type AlertDialogPortalProps, 51 | Footer as AlertDialogFooter, 52 | type AlertDialogFooterProps, 53 | Header as AlertDialogHeader, 54 | type AlertDialogHeaderProps, 55 | Trigger as AlertDialogTrigger, 56 | type AlertDialogTriggerProps, 57 | Overlay as AlertDialogOverlay, 58 | type AlertDialogOverlayProps, 59 | Content as AlertDialogContent, 60 | type AlertDialogContentProps, 61 | Description as AlertDialogDescription, 62 | type AlertDialogDescriptionProps 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 55 | 56 | {#if href} 57 | 67 | {@render children?.()} 68 | 69 | {:else} 70 | 80 | {/if} 81 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 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 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | {#snippet children({ checked, indeterminate })} 32 | 33 | {#if indeterminate} 34 | 35 | {:else} 36 | 37 | {/if} 38 | 39 | {@render childrenProp?.()} 40 | {/snippet} 41 | 42 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    23 | {@render children?.()} 24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | {#snippet children({ checked })} 24 | 25 | {#if checked} 26 | 27 | {/if} 28 | 29 | {@render childrenProp?.({ checked })} 30 | {/snippet} 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | {@render children?.()} 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; 2 | import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; 3 | import Content from './dropdown-menu-content.svelte'; 4 | import Group from './dropdown-menu-group.svelte'; 5 | import Item from './dropdown-menu-item.svelte'; 6 | import Label from './dropdown-menu-label.svelte'; 7 | import RadioGroup from './dropdown-menu-radio-group.svelte'; 8 | import RadioItem from './dropdown-menu-radio-item.svelte'; 9 | import Separator from './dropdown-menu-separator.svelte'; 10 | import Shortcut from './dropdown-menu-shortcut.svelte'; 11 | import Trigger from './dropdown-menu-trigger.svelte'; 12 | import SubContent from './dropdown-menu-sub-content.svelte'; 13 | import SubTrigger from './dropdown-menu-sub-trigger.svelte'; 14 | import GroupHeading from './dropdown-menu-group-heading.svelte'; 15 | import type { ComponentProps } from 'svelte'; 16 | 17 | const Sub = DropdownMenuPrimitive.Sub; 18 | const Root = DropdownMenuPrimitive.Root; 19 | 20 | type DropdownMenuSubProps = ComponentProps; 21 | type DropdownMenuProps = ComponentProps; 22 | type DropdownMenuTriggerProps = ComponentProps; 23 | type DropdownMenuGroupProps = ComponentProps; 24 | type DropdownMenuRadioGroupProps = ComponentProps; 25 | type DropdownMenuCheckboxItemProps = ComponentProps; 26 | type DropdownMenuContentProps = ComponentProps; 27 | type DropdownMenuGroupHeadingProps = ComponentProps; 28 | type DropdownMenuItemProps = ComponentProps; 29 | type DropdownMenuLabelProps = ComponentProps; 30 | type DropdownMenuRadioItemProps = ComponentProps; 31 | type DropdownMenuSeparatorProps = ComponentProps; 32 | type DropdownMenuShortcutProps = ComponentProps; 33 | type DropdownMenuSubContentProps = ComponentProps; 34 | type DropdownMenuSubTriggerProps = ComponentProps; 35 | 36 | export { 37 | CheckboxItem, 38 | Content, 39 | Root as DropdownMenu, 40 | type DropdownMenuProps, 41 | CheckboxItem as DropdownMenuCheckboxItem, 42 | type DropdownMenuCheckboxItemProps, 43 | Content as DropdownMenuContent, 44 | type DropdownMenuContentProps, 45 | Group as DropdownMenuGroup, 46 | type DropdownMenuGroupProps, 47 | GroupHeading as DropdownMenuGroupHeading, 48 | type DropdownMenuGroupHeadingProps, 49 | Item as DropdownMenuItem, 50 | type DropdownMenuItemProps, 51 | Label as DropdownMenuLabel, 52 | type DropdownMenuLabelProps, 53 | RadioGroup as DropdownMenuRadioGroup, 54 | type DropdownMenuRadioGroupProps, 55 | RadioItem as DropdownMenuRadioItem, 56 | type DropdownMenuRadioItemProps, 57 | Separator as DropdownMenuSeparator, 58 | type DropdownMenuSeparatorProps, 59 | Shortcut as DropdownMenuShortcut, 60 | type DropdownMenuShortcutProps, 61 | Sub as DropdownMenuSub, 62 | type DropdownMenuSubProps, 63 | SubContent as DropdownMenuSubContent, 64 | type DropdownMenuSubContentProps, 65 | SubTrigger as DropdownMenuSubTrigger, 66 | type DropdownMenuSubTriggerProps, 67 | Trigger as DropdownMenuTrigger, 68 | type DropdownMenuTriggerProps, 69 | Group, 70 | GroupHeading, 71 | Item, 72 | Label, 73 | RadioGroup, 74 | RadioItem, 75 | Root, 76 | Separator, 77 | Shortcut, 78 | Sub, 79 | SubContent, 80 | SubTrigger, 81 | Trigger 82 | }; 83 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import Root from './input.svelte'; 3 | 4 | type InputProps = ComponentProps; 5 | 6 | export { 7 | Root, 8 | // 9 | Root as Input, 10 | type InputProps 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#if type === 'file'} 23 | 37 | {:else} 38 | 51 | {/if} 52 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import Root from './label.svelte'; 3 | 4 | type LabelProps = ComponentProps; 5 | 6 | export { 7 | Root, 8 | // 9 | Root as Label, 10 | type LabelProps 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import Root from './separator.svelte'; 3 | 4 | type SeparatorProps = ComponentProps; 5 | 6 | export { 7 | Root, 8 | // 9 | Root as Separator, 10 | type SeparatorProps 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as SheetPrimitive } from 'bits-ui'; 2 | import Trigger from './sheet-trigger.svelte'; 3 | import Close from './sheet-close.svelte'; 4 | import Overlay from './sheet-overlay.svelte'; 5 | import Content from './sheet-content.svelte'; 6 | import Header from './sheet-header.svelte'; 7 | import Footer from './sheet-footer.svelte'; 8 | import Title from './sheet-title.svelte'; 9 | import Description from './sheet-description.svelte'; 10 | import type { ComponentProps } from 'svelte'; 11 | 12 | const Root = SheetPrimitive.Root; 13 | const Portal = SheetPrimitive.Portal; 14 | 15 | type SheetProps = ComponentProps; 16 | type CloseProps = ComponentProps; 17 | type TriggerProps = ComponentProps; 18 | type SheetPortalProps = ComponentProps; 19 | type SheetOverlayProps = ComponentProps; 20 | type SheetContentProps = ComponentProps; 21 | type SheetHeaderProps = ComponentProps; 22 | type SheetFooterProps = ComponentProps; 23 | type SheetTitleProps = ComponentProps; 24 | type SheetDescriptionProps = ComponentProps; 25 | 26 | export { 27 | Root, 28 | Close, 29 | Trigger, 30 | Portal, 31 | Overlay, 32 | Content, 33 | Header, 34 | Footer, 35 | Title, 36 | Description, 37 | // 38 | Root as Sheet, 39 | type SheetProps, 40 | Close as SheetClose, 41 | type CloseProps, 42 | Trigger as SheetTrigger, 43 | type TriggerProps, 44 | Portal as SheetPortal, 45 | type SheetPortalProps, 46 | Overlay as SheetOverlay, 47 | type SheetOverlayProps, 48 | Content as SheetContent, 49 | type SheetContentProps, 50 | Header as SheetHeader, 51 | type SheetHeaderProps, 52 | Footer as SheetFooter, 53 | type SheetFooterProps, 54 | Title as SheetTitle, 55 | type SheetTitleProps, 56 | Description as SheetDescription, 57 | type SheetDescriptionProps 58 | }; 59 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-content.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 43 | 44 | 45 | 46 | 52 | {@render children?.()} 53 | 56 | 57 | Close 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/constants.ts: -------------------------------------------------------------------------------- 1 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state'; 2 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; 3 | export const SIDEBAR_WIDTH = '16rem'; 4 | export const SIDEBAR_WIDTH_MOBILE = '18rem'; 5 | export const SIDEBAR_WIDTH_ICON = '3rem'; 6 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/context.svelte.ts: -------------------------------------------------------------------------------- 1 | import { IsMobile } from '$lib/hooks/is-mobile.svelte.js'; 2 | import { getContext, setContext } from 'svelte'; 3 | import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js'; 4 | 5 | type Getter = () => T; 6 | 7 | export type SidebarStateProps = { 8 | /** 9 | * A getter function that returns the current open state of the sidebar. 10 | * We use a getter function here to support `bind:open` on the `Sidebar.Provider` 11 | * component. 12 | */ 13 | open: Getter; 14 | 15 | /** 16 | * A function that sets the open state of the sidebar. To support `bind:open`, we need 17 | * a source of truth for changing the open state to ensure it will be synced throughout 18 | * the sub-components and any `bind:` references. 19 | */ 20 | setOpen: (open: boolean) => void; 21 | }; 22 | 23 | class SidebarState { 24 | readonly props: SidebarStateProps; 25 | open = $derived.by(() => this.props.open()); 26 | openMobile = $state(false); 27 | setOpen: SidebarStateProps['setOpen']; 28 | #isMobile: IsMobile; 29 | state = $derived.by(() => (this.open ? 'expanded' : 'collapsed')); 30 | 31 | constructor(props: SidebarStateProps) { 32 | this.setOpen = props.setOpen; 33 | this.#isMobile = new IsMobile(); 34 | this.props = props; 35 | } 36 | 37 | // Convenience getter for checking if the sidebar is mobile 38 | // without this, we would need to use `sidebar.isMobile.current` everywhere 39 | get isMobile() { 40 | return this.#isMobile.current; 41 | } 42 | 43 | // Event handler to apply to the `` 44 | handleShortcutKeydown = (e: KeyboardEvent) => { 45 | if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { 46 | e.preventDefault(); 47 | this.toggle(); 48 | } 49 | }; 50 | 51 | setOpenMobile = (value: boolean) => { 52 | this.openMobile = value; 53 | }; 54 | 55 | toggle = () => { 56 | return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open); 57 | }; 58 | } 59 | 60 | const SYMBOL_KEY = 'scn-sidebar'; 61 | 62 | /** 63 | * Instantiates a new `SidebarState` instance and sets it in the context. 64 | * 65 | * @param props The constructor props for the `SidebarState` class. 66 | * @returns The `SidebarState` instance. 67 | */ 68 | export function setSidebar(props: SidebarStateProps): SidebarState { 69 | return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); 70 | } 71 | 72 | /** 73 | * Retrieves the `SidebarState` instance from the context. This is a class instance, 74 | * so you cannot destructure it. 75 | * @returns The `SidebarState` instance. 76 | */ 77 | export function useSidebar(): SidebarState { 78 | return getContext(Symbol.for(SYMBOL_KEY)); 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import { useSidebar } from './context.svelte.js'; 3 | import Content from './sidebar-content.svelte'; 4 | import Footer from './sidebar-footer.svelte'; 5 | import GroupAction from './sidebar-group-action.svelte'; 6 | import GroupContent from './sidebar-group-content.svelte'; 7 | import GroupLabel from './sidebar-group-label.svelte'; 8 | import Group from './sidebar-group.svelte'; 9 | import Header from './sidebar-header.svelte'; 10 | import Input from './sidebar-input.svelte'; 11 | import Inset from './sidebar-inset.svelte'; 12 | import MenuAction from './sidebar-menu-action.svelte'; 13 | import MenuBadge from './sidebar-menu-badge.svelte'; 14 | import MenuButton from './sidebar-menu-button.svelte'; 15 | import MenuItem from './sidebar-menu-item.svelte'; 16 | import MenuSkeleton from './sidebar-menu-skeleton.svelte'; 17 | import MenuSubButton from './sidebar-menu-sub-button.svelte'; 18 | import MenuSubItem from './sidebar-menu-sub-item.svelte'; 19 | import MenuSub from './sidebar-menu-sub.svelte'; 20 | import Menu from './sidebar-menu.svelte'; 21 | import Provider from './sidebar-provider.svelte'; 22 | import Rail from './sidebar-rail.svelte'; 23 | import Separator from './sidebar-separator.svelte'; 24 | import Trigger from './sidebar-trigger.svelte'; 25 | import Root from './sidebar.svelte'; 26 | 27 | type SidebarProps = ComponentProps; 28 | type SidebarContentProps = ComponentProps; 29 | type SidebarFooterProps = ComponentProps; 30 | type SidebarGroupProps = ComponentProps; 31 | type SidebarGroupActionProps = ComponentProps; 32 | type SidebarGroupContentProps = ComponentProps; 33 | type SidebarGroupLabelProps = ComponentProps; 34 | type SidebarHeaderProps = ComponentProps; 35 | type SidebarInputProps = ComponentProps; 36 | type SidebarInsetProps = ComponentProps; 37 | type SidebarMenuProps = ComponentProps; 38 | type SidebarMenuActionProps = ComponentProps; 39 | type SidebarMenuBadgeProps = ComponentProps; 40 | type SidebarMenuButtonProps = ComponentProps; 41 | type SidebarMenuItemProps = ComponentProps; 42 | type SidebarMenuSkeletonProps = ComponentProps; 43 | type SidebarMenuSubProps = ComponentProps; 44 | type SidebarMenuSubButtonProps = ComponentProps; 45 | type SidebarMenuSubItemProps = ComponentProps; 46 | type SidebarProviderProps = ComponentProps; 47 | type SidebarRailProps = ComponentProps; 48 | type SidebarSeparatorProps = ComponentProps; 49 | type SidebarTriggerProps = ComponentProps; 50 | 51 | export { 52 | Content, 53 | Footer, 54 | Group, 55 | GroupAction, 56 | GroupContent, 57 | GroupLabel, 58 | Header, 59 | Input, 60 | Inset, 61 | Menu, 62 | MenuAction, 63 | MenuBadge, 64 | MenuButton, 65 | MenuItem, 66 | MenuSkeleton, 67 | MenuSub, 68 | MenuSubButton, 69 | MenuSubItem, 70 | Provider, 71 | Rail, 72 | Root, 73 | Separator, 74 | // 75 | Root as Sidebar, 76 | type SidebarProps, 77 | Content as SidebarContent, 78 | type SidebarContentProps, 79 | Footer as SidebarFooter, 80 | type SidebarFooterProps, 81 | Group as SidebarGroup, 82 | type SidebarGroupProps, 83 | GroupAction as SidebarGroupAction, 84 | type SidebarGroupActionProps, 85 | GroupContent as SidebarGroupContent, 86 | type SidebarGroupContentProps, 87 | GroupLabel as SidebarGroupLabel, 88 | type SidebarGroupLabelProps, 89 | Header as SidebarHeader, 90 | type SidebarHeaderProps, 91 | Input as SidebarInput, 92 | type SidebarInputProps, 93 | Inset as SidebarInset, 94 | type SidebarInsetProps, 95 | Menu as SidebarMenu, 96 | type SidebarMenuProps, 97 | MenuAction as SidebarMenuAction, 98 | type SidebarMenuActionProps, 99 | MenuBadge as SidebarMenuBadge, 100 | type SidebarMenuBadgeProps, 101 | MenuButton as SidebarMenuButton, 102 | type SidebarMenuButtonProps, 103 | MenuItem as SidebarMenuItem, 104 | type SidebarMenuItemProps, 105 | MenuSkeleton as SidebarMenuSkeleton, 106 | type SidebarMenuSkeletonProps, 107 | MenuSub as SidebarMenuSub, 108 | type SidebarMenuSubProps, 109 | MenuSubButton as SidebarMenuSubButton, 110 | type SidebarMenuSubButtonProps, 111 | MenuSubItem as SidebarMenuSubItem, 112 | type SidebarMenuSubItemProps, 113 | Provider as SidebarProvider, 114 | type SidebarProviderProps, 115 | Rail as SidebarRail, 116 | type SidebarRailProps, 117 | Separator as SidebarSeparator, 118 | type SidebarSeparatorProps, 119 | Trigger as SidebarTrigger, 120 | type SidebarTriggerProps, 121 | Trigger, 122 | useSidebar 123 | }; 124 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    23 | {@render children?.()} 24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-action.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#if child} 31 | {@render child({ props: mergedProps })} 32 | {:else} 33 | 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-label.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | {#if child} 29 | {@render child({ props: mergedProps })} 30 | {:else} 31 |
    32 | {@render children?.()} 33 |
    34 | {/if} 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-inset.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    23 | {@render children?.()} 24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-action.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | {#if child} 38 | {@render child({ props: mergedProps })} 39 | {:else} 40 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-badge.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    28 | {@render children?.()} 29 |
    30 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-button.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 67 | 68 | {#snippet Button({ props }: { props?: Record })} 69 | {@const mergedProps = mergeProps(buttonProps, props)} 70 | {#if child} 71 | {@render child({ props: mergedProps })} 72 | {:else} 73 | 76 | {/if} 77 | {/snippet} 78 | 79 | {#if !tooltipContent} 80 | {@render Button({})} 81 | {:else} 82 | 83 | 84 | {#snippet child({ props })} 85 | {@render Button({ props })} 86 | {/snippet} 87 | 88 | 100 | 101 | {/if} 102 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
  • 20 | {@render children?.()} 21 |
  • 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
    27 | {#if showIcon} 28 | 29 | {/if} 30 | 35 | {@render children?.()} 36 |
    37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | {#if child} 38 | {@render child({ props: mergedProps })} 39 | {:else} 40 | 41 | {@render children?.()} 42 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
  • 20 | {@render children?.()} 21 |
  • 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
      24 | {@render children?.()} 25 |
    26 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
      20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-provider.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | 41 |
    51 | {@render children?.()} 52 |
    53 |
    54 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-rail.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-trigger.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | {#if collapsible === 'none'} 26 |
    34 | {@render children?.()} 35 |
    36 | {:else if sidebar.isMobile} 37 | sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> 38 | 46 | 47 | Sidebar 48 | Displays the mobile sidebar. 49 | 50 |
    51 | {@render children?.()} 52 |
    53 |
    54 |
    55 | {:else} 56 | 101 | {/if} 102 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import Root from './skeleton.svelte'; 3 | 4 | type SkeletonProps = ComponentProps; 5 | 6 | export { 7 | Root, 8 | // 9 | Root as Skeleton, 10 | type SkeletonProps 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './sonner.svelte'; 2 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import Root from './textarea.svelte'; 3 | 4 | type TextareaProps = ComponentProps; 5 | 6 | export { 7 | Root, 8 | // 9 | Root as Textarea, 10 | type TextareaProps 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/textarea/textarea.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from 'bits-ui'; 2 | import Trigger from './tooltip-trigger.svelte'; 3 | import Content from './tooltip-content.svelte'; 4 | import type { ComponentProps } from 'svelte'; 5 | 6 | const Root = TooltipPrimitive.Root; 7 | const Provider = TooltipPrimitive.Provider; 8 | const Portal = TooltipPrimitive.Portal; 9 | 10 | type TooltipProps = ComponentProps; 11 | type TooltipTriggerProps = ComponentProps; 12 | type TooltipProviderProps = ComponentProps; 13 | type TooltipContentProps = ComponentProps; 14 | 15 | export { 16 | Root, 17 | Trigger, 18 | Content, 19 | Provider, 20 | Portal, 21 | // 22 | Root as Tooltip, 23 | type TooltipProps, 24 | Content as TooltipContent, 25 | type TooltipContentProps, 26 | Trigger as TooltipTrigger, 27 | type TooltipTriggerProps, 28 | Provider as TooltipProvider, 29 | type TooltipProviderProps 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 30 | {@render children?.()} 31 | 32 | {#snippet child({ props })} 33 |
    44 | {/snippet} 45 |
    46 |
    47 |
    48 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/visibility-selector.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 48 | 49 | (open = val)}> 50 | 51 | {#snippet child({ props })} 52 | 64 | {/snippet} 65 | 66 | 67 | 68 | {#each visibilities as visibility (visibility.id)} 69 | { 71 | chatHistory.updateVisibility(chat.id, visibility.id); 72 | open = false; 73 | }} 74 | class="group/item flex flex-row items-center justify-between gap-4" 75 | data-active={visibility.id === chatFromHistory?.visibility} 76 | > 77 |
    78 | {visibility.label} 79 |
    80 | {visibility.description} 81 |
    82 |
    83 |
    86 | 87 |
    88 |
    89 | {/each} 90 |
    91 |
    92 | -------------------------------------------------------------------------------- /src/lib/errors/ai.ts: -------------------------------------------------------------------------------- 1 | import { TaggedError } from './tagged-error'; 2 | 3 | export class AIInternalError extends TaggedError<'AIInternalError'> { 4 | constructor(options: ErrorOptions = {}) { 5 | super('Internal error', options); 6 | } 7 | } 8 | 9 | export type AIError = AIInternalError; 10 | -------------------------------------------------------------------------------- /src/lib/errors/db.ts: -------------------------------------------------------------------------------- 1 | import { TaggedError } from './tagged-error'; 2 | 3 | export class DbEntityNotFoundError extends TaggedError<'DbEntityNotFoundError'> { 4 | readonly id: string; 5 | readonly entityType: string; 6 | 7 | constructor(id: string, entityType: string, options: ErrorOptions = {}) { 8 | super(`${entityType} not found: ${id}`, options); 9 | this.id = id; 10 | this.entityType = entityType; 11 | } 12 | } 13 | 14 | export class DbInternalError extends TaggedError<'DbInternalError'> { 15 | constructor(options: ErrorOptions = {}) { 16 | super('Internal error', options); 17 | } 18 | } 19 | 20 | export type DbError = DbEntityNotFoundError | DbInternalError; 21 | -------------------------------------------------------------------------------- /src/lib/errors/tagged-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An abstract base class for creating tagged error types. 3 | * 4 | * This class extends the built-in Error class and adds a tagged union 5 | * pattern, allowing each error to have a unique tag type. The tag helps in 6 | * identifying and categorizing different types of errors 7 | * for structured error handling. 8 | * 9 | * @template Tag - The type of the tag used for identifying the error. 10 | * @abstract 11 | * @extends {Error} 12 | */ 13 | export abstract class TaggedError extends Error { 14 | readonly _tag: Tag; 15 | 16 | /** 17 | * Creates a new TaggedError instance. 18 | * 19 | * @param message - The error message. 20 | * @param options - Additional options for the error. 21 | */ 22 | constructor(message: string, options: ErrorOptions = {}) { 23 | super(message, options); 24 | this.name = this.constructor.name; 25 | this._tag = this.name as Tag; 26 | 27 | if (options.cause && options.cause instanceof Error) { 28 | this.stack = `${this.stack}\nCaused by: ${options.cause.stack}`; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/hooks/chat-history.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { VisibilityType } from '$lib/components/visibility-selector.svelte'; 2 | import type { Chat } from '$lib/server/db/schema'; 3 | import { getContext, setContext } from 'svelte'; 4 | import { toast } from 'svelte-sonner'; 5 | 6 | const contextKey = Symbol('ChatHistory'); 7 | 8 | export class ChatHistory { 9 | #loading = $state(false); 10 | #revalidating = $state(false); 11 | chats = $state([]); 12 | 13 | get loading() { 14 | return this.#loading; 15 | } 16 | 17 | get revalidating() { 18 | return this.#revalidating; 19 | } 20 | 21 | constructor(chatsPromise: Promise) { 22 | this.#loading = true; 23 | this.#revalidating = true; 24 | chatsPromise 25 | .then((chats) => (this.chats = chats)) 26 | .finally(() => { 27 | this.#loading = false; 28 | this.#revalidating = false; 29 | }); 30 | } 31 | 32 | getChatDetails = (chatId: string) => { 33 | return this.chats.find((c) => c.id === chatId); 34 | }; 35 | 36 | updateVisibility = async (chatId: string, visibility: VisibilityType) => { 37 | const chat = this.chats.find((c) => c.id === chatId); 38 | if (chat) { 39 | chat.visibility = visibility; 40 | } 41 | const res = await fetch('/api/chat/visibility', { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json' 45 | }, 46 | body: JSON.stringify({ chatId, visibility }) 47 | }); 48 | if (!res.ok) { 49 | toast.error('Failed to update chat visibility'); 50 | // try reloading data from source in case another competing mutation caused an issue 51 | await this.refetch(); 52 | } 53 | }; 54 | 55 | setContext() { 56 | setContext(contextKey, this); 57 | } 58 | 59 | async refetch() { 60 | this.#revalidating = true; 61 | try { 62 | const res = await fetch('/api/history'); 63 | if (res.ok) { 64 | this.chats = await res.json(); 65 | } 66 | } finally { 67 | this.#revalidating = false; 68 | } 69 | } 70 | 71 | static fromContext(): ChatHistory { 72 | return getContext(contextKey); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/hooks/is-mobile.svelte.ts: -------------------------------------------------------------------------------- 1 | import { BREAKPOINTS } from '$lib/utils/constants'; 2 | import { MediaQuery } from 'svelte/reactivity'; 3 | 4 | export class IsMobile extends MediaQuery { 5 | constructor() { 6 | super(`max-width: ${BREAKPOINTS.md.value - 1}${BREAKPOINTS.md.unit}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/hooks/local-storage.svelte.ts: -------------------------------------------------------------------------------- 1 | import { on } from 'svelte/events'; 2 | import { createSubscriber } from 'svelte/reactivity'; 3 | 4 | export class LocalStorage { 5 | #key: string; 6 | #defaultValue: T; 7 | #subscribe: () => void; 8 | 9 | constructor(key: string, defaultValue: T) { 10 | this.#key = key; 11 | this.#defaultValue = defaultValue; 12 | 13 | this.#subscribe = createSubscriber((update) => { 14 | const off = on(window, 'storage', (event) => { 15 | if (event.key === this.#key) { 16 | update(); 17 | } 18 | }); 19 | 20 | return off; 21 | }); 22 | } 23 | 24 | get value() { 25 | this.#subscribe(); 26 | const storedValue = localStorage.getItem(this.#key); 27 | return storedValue === null ? this.#defaultValue : JSON.parse(storedValue); 28 | } 29 | 30 | set value(v: T) { 31 | localStorage.setItem(this.#key, JSON.stringify(v)); 32 | } 33 | 34 | delete() { 35 | localStorage.removeItem(this.#key); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/hooks/lock.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | 3 | export class Lock { 4 | locked = false; 5 | } 6 | 7 | const lockKey = (key: string) => Symbol.for(`lock:${key}`); 8 | 9 | export function getLock(key: string): Lock { 10 | const k = lockKey(key); 11 | let lock = getContext(k); 12 | if (!lock) { 13 | lock = new Lock(); 14 | setContext(k, lock); 15 | } 16 | return lock; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/hooks/selected-model.svelte.ts: -------------------------------------------------------------------------------- 1 | import { SynchronizedCookie } from '$lib/utils/reactivity.svelte'; 2 | 3 | export class SelectedModel extends SynchronizedCookie { 4 | constructor(value: string) { 5 | super('selected-model', value); 6 | } 7 | 8 | static fromContext(): SelectedModel { 9 | return super.fromContext('selected-model'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/server/ai/models.ts: -------------------------------------------------------------------------------- 1 | import { createXai } from '@ai-sdk/xai'; 2 | import { createGroq } from '@ai-sdk/groq'; 3 | import { customProvider, extractReasoningMiddleware, wrapLanguageModel } from 'ai'; 4 | import { XAI_API_KEY, GROQ_API_KEY } from '$env/static/private'; 5 | 6 | const xai = createXai({ apiKey: XAI_API_KEY }); 7 | const groq = createGroq({ apiKey: GROQ_API_KEY }); 8 | 9 | export const myProvider = customProvider({ 10 | languageModels: { 11 | 'chat-model': xai('grok-2-1212'), 12 | 'chat-model-reasoning': wrapLanguageModel({ 13 | model: groq('deepseek-r1-distill-llama-70b'), 14 | middleware: extractReasoningMiddleware({ tagName: 'think' }) 15 | }), 16 | 'title-model': xai('grok-2-1212'), 17 | 'artifact-model': xai('grok-2-1212') 18 | }, 19 | imageModels: { 20 | 'small-model': xai.image('grok-2-image') 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/lib/server/ai/prompts.ts: -------------------------------------------------------------------------------- 1 | import type { ArtifactKind } from '$lib/components/artifact'; 2 | 3 | // TODO 4 | // export const artifactsPrompt = ` 5 | // Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user. 6 | 7 | // When asked to write code, always use artifacts. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language. 8 | 9 | // DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT. 10 | 11 | // This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation. 12 | 13 | // **When to use \`createDocument\`:** 14 | // - For substantial content (>10 lines) or code 15 | // - For content users will likely save/reuse (emails, code, essays, etc.) 16 | // - When explicitly requested to create a document 17 | // - For when content contains a single code snippet 18 | 19 | // **When NOT to use \`createDocument\`:** 20 | // - For informational/explanatory content 21 | // - For conversational responses 22 | // - When asked to keep it in chat 23 | 24 | // **Using \`updateDocument\`:** 25 | // - Default to full document rewrites for major changes 26 | // - Use targeted updates only for specific, isolated changes 27 | // - Follow user instructions for which parts to modify 28 | 29 | // **When NOT to use \`updateDocument\`:** 30 | // - Immediately after creating a document 31 | 32 | // Do not update document right after creating it. Wait for user feedback or request to update it. 33 | // `; 34 | 35 | export const regularPrompt = 36 | 'You are a friendly assistant! Keep your responses concise and helpful.'; 37 | 38 | export const systemPrompt = ({ selectedChatModel }: { selectedChatModel: string }) => { 39 | if (selectedChatModel === 'chat-model-reasoning') { 40 | return regularPrompt; 41 | } else { 42 | return regularPrompt; 43 | // return `${regularPrompt}\n\n${artifactsPrompt}`; 44 | } 45 | }; 46 | 47 | export const codePrompt = ` 48 | You are a Python code generator that creates self-contained, executable code snippets. When writing code: 49 | 50 | 1. Each snippet should be complete and runnable on its own 51 | 2. Prefer using print() statements to display outputs 52 | 3. Include helpful comments explaining the code 53 | 4. Keep snippets concise (generally under 15 lines) 54 | 5. Avoid external dependencies - use Python standard library 55 | 6. Handle potential errors gracefully 56 | 7. Return meaningful output that demonstrates the code's functionality 57 | 8. Don't use input() or other interactive functions 58 | 9. Don't access files or network resources 59 | 10. Don't use infinite loops 60 | 61 | Examples of good snippets: 62 | 63 | \`\`\`python 64 | # Calculate factorial iteratively 65 | def factorial(n): 66 | result = 1 67 | for i in range(1, n + 1): 68 | result *= i 69 | return result 70 | 71 | print(f"Factorial of 5 is: {factorial(5)}") 72 | \`\`\` 73 | `; 74 | 75 | export const sheetPrompt = ` 76 | You are a spreadsheet creation assistant. Create a spreadsheet in csv format based on the given prompt. The spreadsheet should contain meaningful column headers and data. 77 | `; 78 | 79 | export const updateDocumentPrompt = (currentContent: string | null, type: ArtifactKind) => 80 | type === 'text' 81 | ? `\ 82 | Improve the following contents of the document based on the given prompt. 83 | 84 | ${currentContent} 85 | ` 86 | : type === 'code' 87 | ? `\ 88 | Improve the following code snippet based on the given prompt. 89 | 90 | ${currentContent} 91 | ` 92 | : type === 'sheet' 93 | ? `\ 94 | Improve the following spreadsheet based on the given prompt. 95 | 96 | ${currentContent} 97 | ` 98 | : ''; 99 | -------------------------------------------------------------------------------- /src/lib/server/ai/utils.ts: -------------------------------------------------------------------------------- 1 | import { generateText, type Message } from 'ai'; 2 | import { myProvider } from './models'; 3 | import { AIInternalError, type AIError } from '$lib/errors/ai'; 4 | import { fromPromise, ok, safeTry, type ResultAsync } from 'neverthrow'; 5 | 6 | export function generateTitleFromUserMessage({ 7 | message 8 | }: { 9 | message: Message; 10 | }): ResultAsync { 11 | return safeTry(async function* () { 12 | const result = yield* fromPromise( 13 | generateText({ 14 | model: myProvider.languageModel('title-model'), 15 | system: `\n 16 | - you will generate a short title based on the first message a user begins a conversation with 17 | - ensure it is not more than 80 characters long 18 | - the title should be a summary of the user's message 19 | - do not use quotes or colons`, 20 | prompt: JSON.stringify(message) 21 | }), 22 | (e) => new AIInternalError({ cause: e }) 23 | ); 24 | 25 | return ok(result.text); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/server/auth/handle.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import { 3 | deleteSessionTokenCookie, 4 | getSessionCookie, 5 | setSessionTokenCookie, 6 | validateSessionToken 7 | } from '.'; 8 | 9 | export const handle: Handle = async ({ event, resolve }) => { 10 | const token = getSessionCookie(event); 11 | if (!token) { 12 | return resolve(event); 13 | } 14 | 15 | const validatedTokenResult = await validateSessionToken(token); 16 | if (validatedTokenResult.isErr()) { 17 | console.error(validatedTokenResult.error); 18 | } else { 19 | const { session, user } = validatedTokenResult.value; 20 | if (session) { 21 | setSessionTokenCookie(event.cookies, token, session.expiresAt); 22 | event.locals.session = session; 23 | event.locals.user = user; 24 | } else { 25 | deleteSessionTokenCookie(event.cookies); 26 | } 27 | } 28 | 29 | return resolve(event); 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/server/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; 2 | import { sha256 } from '@oslojs/crypto/sha2'; 3 | import type { Session, User } from '$lib/server/db/schema'; 4 | import { 5 | createSession as createSessionDb, 6 | deleteSession, 7 | deleteSessionsForUser, 8 | extendSession, 9 | getFullSession 10 | } from '$lib/server/db/queries'; 11 | import { ok, ResultAsync, safeTry } from 'neverthrow'; 12 | import type { DbError } from '$lib/errors/db'; 13 | import ms from 'ms'; 14 | import type { Cookies, RequestEvent } from '@sveltejs/kit'; 15 | 16 | export function generateSessionToken(): string { 17 | const bytes = new Uint8Array(32); 18 | crypto.getRandomValues(bytes); 19 | const token = encodeBase32LowerCaseNoPadding(bytes); 20 | return token; 21 | } 22 | 23 | export function createSession(token: string, userId: string): ResultAsync { 24 | return safeTry(async function* () { 25 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 26 | const session: Session = { 27 | id: sessionId, 28 | userId, 29 | expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) 30 | }; 31 | yield* createSessionDb(session); 32 | return ok(session); 33 | }); 34 | } 35 | 36 | export function validateSessionToken(token: string): ResultAsync { 37 | return safeTry(async function* () { 38 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 39 | const { user, session } = yield* getFullSession(sessionId); 40 | if (Date.now() >= session.expiresAt.getTime()) { 41 | yield* deleteSession(sessionId); 42 | return ok({ session: null, user: null }); 43 | } 44 | if (Date.now() >= session.expiresAt.getTime() - ms('15d')) { 45 | yield* extendSession(sessionId); 46 | } 47 | return ok({ session, user }); 48 | }); 49 | } 50 | 51 | export function invalidateSession(sessionId: string): ResultAsync { 52 | return deleteSession(sessionId); 53 | } 54 | 55 | export function invalidateAllSessions(userId: string): ResultAsync { 56 | return deleteSessionsForUser(userId); 57 | } 58 | 59 | export function getSessionCookie(event: RequestEvent): string | undefined { 60 | return event.cookies.get('session'); 61 | } 62 | 63 | export function setSessionTokenCookie(cookies: Cookies, token: string, expiresAt: Date): void { 64 | cookies.set('session', token, { 65 | httpOnly: true, 66 | sameSite: 'lax', 67 | expires: expiresAt, 68 | path: '/' 69 | }); 70 | } 71 | 72 | export function deleteSessionTokenCookie(cookies: Cookies): void { 73 | cookies.set('session', 'token', { 74 | httpOnly: true, 75 | sameSite: 'lax', 76 | maxAge: 0, 77 | path: '/' 78 | }); 79 | } 80 | 81 | export type SessionValidationResult = 82 | | { session: Session; user: User } 83 | | { session: null; user: null }; 84 | -------------------------------------------------------------------------------- /src/lib/server/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { drizzle } from 'drizzle-orm/postgres-js'; 3 | import { migrate } from 'drizzle-orm/postgres-js/migrator'; 4 | import postgres from 'postgres'; 5 | 6 | config({ 7 | path: '.env.local' 8 | }); 9 | 10 | const runMigrate = async () => { 11 | if (!process.env.POSTGRES_URL) { 12 | throw new Error('POSTGRES_URL is not defined'); 13 | } 14 | 15 | const connection = postgres(process.env.POSTGRES_URL, { max: 1 }); 16 | const db = drizzle(connection); 17 | 18 | console.log('⏳ Running migrations...'); 19 | 20 | const start = Date.now(); 21 | await migrate(db, { migrationsFolder: './src/lib/server/db/migrations' }); 22 | const end = Date.now(); 23 | 24 | console.log('✅ Migrations completed in', end - start, 'ms'); 25 | process.exit(0); 26 | }; 27 | 28 | runMigrate().catch((err) => { 29 | console.error('❌ Migration failed'); 30 | console.error(err); 31 | process.exit(1); 32 | }); 33 | -------------------------------------------------------------------------------- /src/lib/server/db/migrations/0000_ambiguous_dragon_man.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "Chat" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "createdAt" timestamp NOT NULL, 4 | "title" text NOT NULL, 5 | "userId" uuid NOT NULL, 6 | "visibility" varchar DEFAULT 'private' NOT NULL 7 | ); 8 | --> statement-breakpoint 9 | CREATE TABLE "Document" ( 10 | "id" uuid DEFAULT gen_random_uuid() NOT NULL, 11 | "createdAt" timestamp NOT NULL, 12 | "title" text NOT NULL, 13 | "content" text, 14 | "text" varchar DEFAULT 'text' NOT NULL, 15 | "userId" uuid NOT NULL 16 | ); 17 | --> statement-breakpoint 18 | CREATE TABLE "Message" ( 19 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 20 | "chatId" uuid NOT NULL, 21 | "role" varchar NOT NULL, 22 | "parts" json NOT NULL, 23 | "attachments" json NOT NULL, 24 | "createdAt" timestamp NOT NULL 25 | ); 26 | --> statement-breakpoint 27 | CREATE TABLE "Session" ( 28 | "id" text PRIMARY KEY NOT NULL, 29 | "userId" uuid NOT NULL, 30 | "expires_at" timestamp with time zone NOT NULL 31 | ); 32 | --> statement-breakpoint 33 | CREATE TABLE "Suggestion" ( 34 | "id" uuid DEFAULT gen_random_uuid() NOT NULL, 35 | "documentId" uuid NOT NULL, 36 | "documentCreatedAt" timestamp NOT NULL, 37 | "originalText" text NOT NULL, 38 | "suggestedText" text NOT NULL, 39 | "description" text, 40 | "isResolved" boolean DEFAULT false NOT NULL, 41 | "userId" uuid NOT NULL, 42 | "createdAt" timestamp NOT NULL 43 | ); 44 | --> statement-breakpoint 45 | CREATE TABLE "User" ( 46 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 47 | "email" varchar(64) NOT NULL, 48 | "password" varchar(64) NOT NULL, 49 | CONSTRAINT "User_email_unique" UNIQUE("email") 50 | ); 51 | --> statement-breakpoint 52 | CREATE TABLE "Vote" ( 53 | "chatId" uuid NOT NULL, 54 | "messageId" uuid NOT NULL, 55 | "isUpvoted" boolean NOT NULL 56 | ); 57 | --> statement-breakpoint 58 | ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 59 | ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 60 | ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 61 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 62 | ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 63 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 64 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_messageId_Message_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE no action ON UPDATE no action; -------------------------------------------------------------------------------- /src/lib/server/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1742595457496, 9 | "tag": "0000_ambiguous_dragon_man", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/server/db/schema.ts: -------------------------------------------------------------------------------- 1 | import type { InferSelectModel } from 'drizzle-orm'; 2 | import { 3 | pgTable, 4 | varchar, 5 | timestamp, 6 | json, 7 | uuid, 8 | text, 9 | primaryKey, 10 | foreignKey, 11 | boolean 12 | } from 'drizzle-orm/pg-core'; 13 | 14 | export const user = pgTable('User', { 15 | id: uuid('id').primaryKey().notNull().defaultRandom().primaryKey(), 16 | email: varchar('email', { length: 64 }).notNull().unique(), 17 | password: varchar('password', { length: 64 }).notNull() 18 | }); 19 | 20 | export type AuthUser = InferSelectModel; 21 | export type User = Omit; 22 | 23 | export const session = pgTable('Session', { 24 | id: text('id').primaryKey().notNull(), 25 | userId: uuid('userId') 26 | .notNull() 27 | .references(() => user.id), 28 | expiresAt: timestamp('expires_at', { 29 | withTimezone: true, 30 | mode: 'date' 31 | }).notNull() 32 | }); 33 | 34 | export type Session = InferSelectModel; 35 | 36 | export const chat = pgTable('Chat', { 37 | id: uuid('id').primaryKey().notNull().defaultRandom().primaryKey(), 38 | createdAt: timestamp('createdAt').notNull(), 39 | title: text('title').notNull(), 40 | userId: uuid('userId') 41 | .notNull() 42 | .references(() => user.id), 43 | visibility: varchar('visibility', { enum: ['public', 'private'] }) 44 | .notNull() 45 | .default('private') 46 | }); 47 | 48 | export type Chat = InferSelectModel; 49 | 50 | export const message = pgTable('Message', { 51 | id: uuid('id').primaryKey().notNull().defaultRandom(), 52 | chatId: uuid('chatId') 53 | .notNull() 54 | .references(() => chat.id), 55 | role: varchar('role').notNull(), 56 | parts: json('parts').notNull(), 57 | attachments: json('attachments').notNull(), 58 | createdAt: timestamp('createdAt').notNull() 59 | }); 60 | 61 | export type Message = InferSelectModel; 62 | 63 | export const vote = pgTable( 64 | 'Vote', 65 | { 66 | chatId: uuid('chatId') 67 | .notNull() 68 | .references(() => chat.id), 69 | messageId: uuid('messageId') 70 | .notNull() 71 | .references(() => message.id), 72 | isUpvoted: boolean('isUpvoted').notNull() 73 | }, 74 | (table) => [ 75 | { 76 | pk: primaryKey({ columns: [table.chatId, table.messageId] }) 77 | } 78 | ] 79 | ); 80 | 81 | export type Vote = InferSelectModel; 82 | 83 | export const document = pgTable( 84 | 'Document', 85 | { 86 | id: uuid('id').notNull().defaultRandom(), 87 | createdAt: timestamp('createdAt').notNull(), 88 | title: text('title').notNull(), 89 | content: text('content'), 90 | kind: varchar('text', { enum: ['text', 'code', 'image', 'sheet'] }) 91 | .notNull() 92 | .default('text'), 93 | userId: uuid('userId') 94 | .notNull() 95 | .references(() => user.id) 96 | }, 97 | (table) => [ 98 | { 99 | pk: primaryKey({ columns: [table.id, table.createdAt] }) 100 | } 101 | ] 102 | ); 103 | 104 | export type Document = InferSelectModel; 105 | 106 | export const suggestion = pgTable( 107 | 'Suggestion', 108 | { 109 | id: uuid('id').notNull().defaultRandom(), 110 | documentId: uuid('documentId').notNull(), 111 | documentCreatedAt: timestamp('documentCreatedAt').notNull(), 112 | originalText: text('originalText').notNull(), 113 | suggestedText: text('suggestedText').notNull(), 114 | description: text('description'), 115 | isResolved: boolean('isResolved').notNull().default(false), 116 | userId: uuid('userId') 117 | .notNull() 118 | .references(() => user.id), 119 | createdAt: timestamp('createdAt').notNull() 120 | }, 121 | (table) => [ 122 | { 123 | pk: primaryKey({ columns: [table.id] }), 124 | documentRef: foreignKey({ 125 | columns: [table.documentId, table.documentCreatedAt], 126 | foreignColumns: [document.id, document.createdAt] 127 | }) 128 | } 129 | ] 130 | ); 131 | 132 | export type Suggestion = InferSelectModel; 133 | -------------------------------------------------------------------------------- /src/lib/server/db/utils.ts: -------------------------------------------------------------------------------- 1 | import { DbEntityNotFoundError } from '$lib/errors/db'; 2 | import { err, ok, Result } from 'neverthrow'; 3 | 4 | export function unwrapSingleQueryResult( 5 | rows: T[], 6 | id: string, 7 | entityType: string 8 | ): Result { 9 | if (rows.length === 0) { 10 | return err(new DbEntityNotFoundError(id, entityType)); 11 | } 12 | return ok(rows[0]); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/utils/chat.ts: -------------------------------------------------------------------------------- 1 | import type { Attachment, CoreAssistantMessage, CoreToolMessage, Message } from 'ai'; 2 | import type { Message as DBMessage, Document } from '$lib/server/db/schema'; 3 | import type { UIMessage } from '@ai-sdk/svelte'; 4 | 5 | export function convertToUIMessages(messages: Array): Array { 6 | return messages.map((message) => ({ 7 | id: message.id, 8 | parts: message.parts as UIMessage['parts'], 9 | role: message.role as UIMessage['role'], 10 | // Note: content will soon be deprecated in @ai-sdk/react 11 | content: '', 12 | createdAt: message.createdAt, 13 | experimental_attachments: (message.attachments as Array) ?? [] 14 | })); 15 | } 16 | 17 | export function getMostRecentUserMessage(messages: Array) { 18 | const userMessages = messages.filter((message) => message.role === 'user'); 19 | return userMessages.at(-1); 20 | } 21 | 22 | export function getDocumentTimestampByIndex(documents: Array, index: number) { 23 | if (!documents) return new Date(); 24 | if (index > documents.length) return new Date(); 25 | 26 | return documents[index].createdAt; 27 | } 28 | 29 | type ResponseMessageWithoutId = CoreToolMessage | CoreAssistantMessage; 30 | type ResponseMessage = ResponseMessageWithoutId & { id: string }; 31 | 32 | export function getTrailingMessageId({ 33 | messages 34 | }: { 35 | messages: Array; 36 | }): string | null { 37 | const trailingMessage = messages.at(-1); 38 | 39 | if (!trailingMessage) return null; 40 | 41 | return trailingMessage.id; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/public'; 2 | 3 | export const BREAKPOINTS = { 4 | sm: { 5 | unit: 'px', 6 | value: 640 7 | }, 8 | md: { 9 | unit: 'px', 10 | value: 768 11 | }, 12 | lg: { 13 | unit: 'px', 14 | value: 1024 15 | }, 16 | xl: { 17 | unit: 'px', 18 | value: 1280 19 | }, 20 | '2xl': { 21 | unit: 'px', 22 | value: 1536 23 | } 24 | } as const; 25 | 26 | export const allowAnonymousChats = env.PUBLIC_ALLOW_ANONYMOUS_CHATS === 'true'; 27 | -------------------------------------------------------------------------------- /src/lib/utils/reactivity.svelte.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | 3 | export class Box { 4 | value = $state() as T; 5 | 6 | constructor(value: T) { 7 | this.value = value; 8 | } 9 | } 10 | 11 | /** 12 | * Expects there to be a route at `/api/synchronized-cookie/:key` that sets a cookie with the given key/value. 13 | * That handler is responsible for validating the cookie value and setting the cookie with the given key. 14 | * This uses fire-and-forget logic for setting the cookie, optimistically updating local state. 15 | */ 16 | export class SynchronizedCookie { 17 | #contextKey: symbol; 18 | #key: string; 19 | #value = $state()!; 20 | 21 | constructor(key: string, value: string) { 22 | this.#key = key; 23 | this.#value = value; 24 | this.#contextKey = Symbol.for(`SynchronizedCookie:${key}`); 25 | } 26 | 27 | get key() { 28 | return this.#key; 29 | } 30 | 31 | get value() { 32 | return this.#value; 33 | } 34 | 35 | set value(v: string) { 36 | fetch(`/api/synchronized-cookie/${this.#key}`, { 37 | method: 'POST', 38 | body: JSON.stringify({ value: v }), 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | } 42 | }).catch(console.error); 43 | this.#value = v; 44 | } 45 | 46 | setContext() { 47 | setContext(this.#contextKey, this); 48 | } 49 | 50 | static fromContext(key: string): SynchronizedCookie { 51 | return getContext(Symbol.for(`SynchronizedCookie:${key}`)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/utils/shadcn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export type WithoutChild = T extends { child?: any } ? Omit : T; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type WithoutChildren = T extends { children?: any } ? Omit : T; 12 | export type WithoutChildrenOrChild = WithoutChildren>; 13 | export type WithElementRef = T & { ref?: U | null }; 14 | -------------------------------------------------------------------------------- /src/lib/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { Snippet } from 'svelte'; 2 | 3 | export type WithElementRef = T & { 4 | ref?: U | null; 5 | }; 6 | 7 | export type WithElementRefAndChild< 8 | T, 9 | U extends HTMLElement | SVGElement = HTMLElement 10 | > = WithElementRef & { child?: Snippet<[{ props: T }]> }; 11 | -------------------------------------------------------------------------------- /src/params/authType.ts: -------------------------------------------------------------------------------- 1 | import type { ParamMatcher } from '@sveltejs/kit'; 2 | 3 | export const match = ((param: string): param is 'signup' | 'signin' => { 4 | return param === 'signup' || param === 'signin'; 5 | }) satisfies ParamMatcher; 6 | -------------------------------------------------------------------------------- /src/routes/(auth)/[authType=authType]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSession, 3 | generateSessionToken, 4 | setSessionTokenCookie 5 | } from '$lib/server/auth/index.js'; 6 | import { createAuthUser, getAuthUser } from '$lib/server/db/queries.js'; 7 | import type { AuthUser } from '$lib/server/db/schema.js'; 8 | import { fail, redirect } from '@sveltejs/kit'; 9 | import { compare } from 'bcrypt-ts'; 10 | import { err, ok, safeTry } from 'neverthrow'; 11 | import { z } from 'zod'; 12 | 13 | export function load({ locals }) { 14 | if (locals.session) { 15 | return redirect(307, '/'); 16 | } 17 | } 18 | 19 | const emailSchema = z.string().email(); 20 | const passwordSchema = z.string().min(8); 21 | 22 | export const actions = { 23 | default: async ({ request, params, cookies }) => { 24 | const formData = await request.formData(); 25 | const rawEmail = formData.get('email'); 26 | const email = emailSchema.safeParse(rawEmail); 27 | if (!email.success) { 28 | return fail(400, { 29 | success: false, 30 | message: 'Invalid email', 31 | email: (rawEmail ?? undefined) as string | undefined 32 | } as const); 33 | } 34 | const password = passwordSchema.safeParse(formData.get('password')); 35 | if (!password.success) { 36 | return fail(400, { success: false, message: 'Invalid password' } as const); 37 | } 38 | 39 | const actionResult = safeTry(async function* () { 40 | let user: AuthUser; 41 | if (params.authType === 'signup') { 42 | user = yield* createAuthUser(email.data, password.data); 43 | } else { 44 | user = yield* getAuthUser(email.data); 45 | const passwordIsCorrect = await compare(password.data, user.password); 46 | if (!passwordIsCorrect) { 47 | return err(undefined); 48 | } 49 | } 50 | 51 | const token = generateSessionToken(); 52 | const session = yield* createSession(token, user.id); 53 | setSessionTokenCookie(cookies, token, session.expiresAt); 54 | return ok(undefined); 55 | }); 56 | 57 | return actionResult.match( 58 | () => redirect(303, '/'), 59 | () => 60 | fail(400, { 61 | success: false, 62 | message: `Failed to ${params.authType === 'signup' ? 'sign up' : 'sign in'}. Please try again later.` 63 | }) 64 | ); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/routes/(auth)/[authType=authType]/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    14 |
    15 |
    16 |

    {signInSignUp}

    17 |

    18 | Use your email and password to {signInSignUp.toLowerCase()} 19 |

    20 |
    21 | 22 | {#snippet submitButton({ pending, success })} 23 | {signInSignUp} 24 | {/snippet} 25 | 26 | {#if page.params.authType === 'signup'} 27 | {@render switchAuthType({ 28 | question: 'Already have an account? ', 29 | href: '/signin', 30 | cta: 'Sign in', 31 | postscript: ' instead.' 32 | })} 33 | {:else} 34 | {@render switchAuthType({ 35 | question: "Don't have an account? ", 36 | href: '/signup', 37 | cta: 'Sign up', 38 | postscript: ' for free.' 39 | })} 40 | {/if} 41 | 42 |
    43 |
    44 | 45 | {#snippet switchAuthType({ 46 | question, 47 | href, 48 | cta, 49 | postscript 50 | }: { 51 | question: string; 52 | href: string; 53 | cta: string; 54 | postscript: string; 55 | })} 56 |

    57 | {question} 58 | 59 | {cta} 60 | 61 | {postscript} 62 |

    63 | {/snippet} 64 | -------------------------------------------------------------------------------- /src/routes/(auth)/signout/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/auth/index.js'; 2 | import { redirect } from '@sveltejs/kit'; 3 | 4 | export function load({ locals, cookies }) { 5 | if (locals.session) { 6 | invalidateSession(locals.session.id); 7 | deleteSessionTokenCookie(cookies); 8 | } 9 | 10 | redirect(307, '/signin'); 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/(chat)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { chatModels, DEFAULT_CHAT_MODEL } from '$lib/ai/models'; 2 | import { SelectedModel } from '$lib/hooks/selected-model.svelte.js'; 3 | 4 | export async function load({ cookies, locals }) { 5 | const { user } = locals; 6 | const sidebarCollapsed = cookies.get('sidebar:state') !== 'true'; 7 | 8 | let modelId = cookies.get('selected-model'); 9 | if (!modelId || !chatModels.find((model) => model.id === modelId)) { 10 | modelId = DEFAULT_CHAT_MODEL; 11 | cookies.set('selected-model', modelId, { 12 | path: '/', 13 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), 14 | httpOnly: true, 15 | sameSite: 'lax' 16 | }); 17 | } 18 | 19 | return { 20 | user, 21 | sidebarCollapsed, 22 | selectedChatModel: new SelectedModel(modelId) 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/routes/(chat)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | {@render children?.()} 16 | 17 | -------------------------------------------------------------------------------- /src/routes/(chat)/+layout.ts: -------------------------------------------------------------------------------- 1 | import type { Chat } from '$lib/server/db/schema.js'; 2 | 3 | export async function load({ data, fetch }) { 4 | const { user } = data; 5 | let chats = Promise.resolve([]); 6 | if (user) { 7 | chats = fetch('/api/history').then((res) => res.json()); 8 | } 9 | return { 10 | chats, 11 | ...data 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/(chat)/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/chat/+server.ts: -------------------------------------------------------------------------------- 1 | import { myProvider } from '$lib/server/ai/models'; 2 | import { systemPrompt } from '$lib/server/ai/prompts.js'; 3 | import { generateTitleFromUserMessage } from '$lib/server/ai/utils'; 4 | import { deleteChatById, getChatById, saveChat, saveMessages } from '$lib/server/db/queries.js'; 5 | import type { Chat } from '$lib/server/db/schema'; 6 | import { getMostRecentUserMessage, getTrailingMessageId } from '$lib/utils/chat.js'; 7 | import { allowAnonymousChats } from '$lib/utils/constants.js'; 8 | import { error } from '@sveltejs/kit'; 9 | import { 10 | appendResponseMessages, 11 | createDataStreamResponse, 12 | smoothStream, 13 | streamText, 14 | type UIMessage 15 | } from 'ai'; 16 | import { ok, safeTry } from 'neverthrow'; 17 | 18 | export async function POST({ request, locals: { user }, cookies }) { 19 | // TODO: zod? 20 | const { id, messages }: { id: string; messages: UIMessage[] } = await request.json(); 21 | const selectedChatModel = cookies.get('selected-model'); 22 | 23 | if (!user && !allowAnonymousChats) { 24 | error(401, 'Unauthorized'); 25 | } 26 | 27 | if (!selectedChatModel) { 28 | error(400, 'No chat model selected'); 29 | } 30 | 31 | const userMessage = getMostRecentUserMessage(messages); 32 | 33 | if (!userMessage) { 34 | error(400, 'No user message found'); 35 | } 36 | 37 | if (user) { 38 | await safeTry(async function* () { 39 | let chat: Chat; 40 | const chatResult = await getChatById({ id }); 41 | if (chatResult.isErr()) { 42 | if (chatResult.error._tag !== 'DbEntityNotFoundError') { 43 | return chatResult; 44 | } 45 | const title = yield* generateTitleFromUserMessage({ message: userMessage }); 46 | chat = yield* saveChat({ id, userId: user.id, title }); 47 | } else { 48 | chat = chatResult.value; 49 | } 50 | 51 | if (chat.userId !== user.id) { 52 | error(403, 'Forbidden'); 53 | } 54 | 55 | yield* saveMessages({ 56 | messages: [ 57 | { 58 | chatId: id, 59 | id: userMessage.id, 60 | role: 'user', 61 | parts: userMessage.parts, 62 | attachments: userMessage.experimental_attachments ?? [], 63 | createdAt: new Date() 64 | } 65 | ] 66 | }); 67 | 68 | return ok(undefined); 69 | }).orElse(() => error(500, 'An error occurred while processing your request')); 70 | } 71 | 72 | return createDataStreamResponse({ 73 | execute: (dataStream) => { 74 | const result = streamText({ 75 | model: myProvider.languageModel(selectedChatModel), 76 | system: systemPrompt({ selectedChatModel }), 77 | messages, 78 | maxSteps: 5, 79 | experimental_activeTools: [], 80 | // TODO 81 | // selectedChatModel === 'chat-model-reasoning' 82 | // ? [] 83 | // : ['getWeather', 'createDocument', 'updateDocument', 'requestSuggestions'], 84 | experimental_transform: smoothStream({ chunking: 'word' }), 85 | experimental_generateMessageId: crypto.randomUUID.bind(crypto), 86 | // TODO 87 | // tools: { 88 | // getWeather, 89 | // createDocument: createDocument({ session, dataStream }), 90 | // updateDocument: updateDocument({ session, dataStream }), 91 | // requestSuggestions: requestSuggestions({ 92 | // session, 93 | // dataStream 94 | // }) 95 | // }, 96 | onFinish: async ({ response }) => { 97 | if (!user) return; 98 | const assistantId = getTrailingMessageId({ 99 | messages: response.messages.filter((message) => message.role === 'assistant') 100 | }); 101 | 102 | if (!assistantId) { 103 | throw new Error('No assistant message found!'); 104 | } 105 | 106 | const [, assistantMessage] = appendResponseMessages({ 107 | messages: [userMessage], 108 | responseMessages: response.messages 109 | }); 110 | 111 | await saveMessages({ 112 | messages: [ 113 | { 114 | id: assistantId, 115 | chatId: id, 116 | role: assistantMessage.role, 117 | parts: assistantMessage.parts, 118 | attachments: assistantMessage.experimental_attachments ?? [], 119 | createdAt: new Date() 120 | } 121 | ] 122 | }); 123 | }, 124 | experimental_telemetry: { 125 | isEnabled: true, 126 | functionId: 'stream-text' 127 | } 128 | }); 129 | 130 | result.consumeStream(); 131 | 132 | result.mergeIntoDataStream(dataStream, { 133 | sendReasoning: true 134 | }); 135 | }, 136 | onError: (e) => { 137 | console.error(e); 138 | return 'Oops!'; 139 | } 140 | }); 141 | } 142 | 143 | export async function DELETE({ locals: { user }, request }) { 144 | // TODO: zod 145 | const { id }: { id: string } = await request.json(); 146 | if (!user) { 147 | error(401, 'Unauthorized'); 148 | } 149 | 150 | return await getChatById({ id }) 151 | .andTee((chat) => { 152 | if (chat.userId !== user.id) { 153 | error(403, 'Forbidden'); 154 | } 155 | }) 156 | .andThen(deleteChatById) 157 | .match( 158 | () => new Response('Chat deleted', { status: 200 }), 159 | () => error(500, 'An error occurred while processing your request') 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/chat/visibility/+server.ts: -------------------------------------------------------------------------------- 1 | import type { VisibilityType } from '$lib/components/visibility-selector.svelte'; 2 | import { updateChatVisiblityById } from '$lib/server/db/queries'; 3 | 4 | export async function POST({ request }) { 5 | const { chatId, visibility }: { chatId: string; visibility: VisibilityType } = 6 | await request.json(); 7 | // TODO: This definitely needs a user check too 8 | return updateChatVisiblityById({ chatId, visibility }).match( 9 | () => new Response('Chat visibility updated', { status: 200 }), 10 | () => new Response('An error occurred while processing your request', { status: 500 }) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/files/upload/+server.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { z } from 'zod'; 3 | import { put } from '@vercel/blob'; 4 | import { BLOB_READ_WRITE_TOKEN } from '$env/static/private'; 5 | 6 | const FileSchema = z.object({ 7 | file: z 8 | .instanceof(Blob) 9 | .refine((file) => file.size <= 5 * 1024 * 1024, { 10 | message: 'File size should be less than 5MB' 11 | }) 12 | // Update the file type based on the kind of files you want to accept 13 | .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { 14 | message: 'File type should be JPEG or PNG' 15 | }) 16 | }); 17 | 18 | export async function POST({ request, locals: { user } }) { 19 | if (!user) { 20 | error(401, 'Unauthorized'); 21 | } 22 | 23 | if (request.body === null) { 24 | error(400, 'Empty file received'); 25 | } 26 | 27 | try { 28 | const formData = await request.formData(); 29 | const file = formData.get('file') as File; 30 | 31 | if (!file) { 32 | return error(400, 'No file uploaded'); 33 | } 34 | 35 | const validatedFile = FileSchema.safeParse({ file }); 36 | 37 | if (!validatedFile.success) { 38 | const errorMessage = validatedFile.error.errors.map((error) => error.message).join(', '); 39 | 40 | return error(400, errorMessage); 41 | } 42 | 43 | // Get filename from formData since Blob doesn't have name property 44 | const filename = file.name; 45 | const fileBuffer = await file.arrayBuffer(); 46 | 47 | try { 48 | const data = await put(`${filename}`, fileBuffer, { 49 | access: 'public', 50 | token: BLOB_READ_WRITE_TOKEN 51 | }); 52 | 53 | return Response.json(data); 54 | } catch (e) { 55 | console.error(e); 56 | return error(500, 'Upload failed'); 57 | } 58 | } catch (e) { 59 | console.error(e); 60 | return error(500, 'Failed to process request'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/history/+server.ts: -------------------------------------------------------------------------------- 1 | import { getChatsByUserId } from '$lib/server/db/queries.js'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export async function GET({ locals: { user } }) { 5 | if (!user) { 6 | error(401, 'Unauthorized'); 7 | } 8 | 9 | return await getChatsByUserId({ id: user.id }).match( 10 | (chats) => Response.json(chats), 11 | () => error(500, 'An error occurred while processing your request') 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/suggestions/[documentId]/+server.ts: -------------------------------------------------------------------------------- 1 | import { getSuggestionsByDocumentId } from '$lib/server/db/queries'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export async function GET({ locals: { user }, params: { documentId } }) { 5 | if (!user) { 6 | error(401, 'Unauthorized'); 7 | } 8 | 9 | return await getSuggestionsByDocumentId({ documentId }) 10 | .andTee((suggestions) => { 11 | const suggestion = suggestions.at(0); 12 | if (suggestion?.userId !== user.id) { 13 | error(403, 'Forbidden'); 14 | } 15 | }) 16 | .match( 17 | (suggestions) => Response.json(suggestions), 18 | () => error(500, 'An error occurred while processing your request') 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/synchronized-cookie/[cookieName]/+server.ts: -------------------------------------------------------------------------------- 1 | import { chatModels } from '$lib/ai/models.js'; 2 | 3 | export async function POST({ params, cookies, request }) { 4 | const { value } = await request.json(); 5 | if (typeof value !== 'string') { 6 | return new Response(null, { 7 | status: 400 8 | }); 9 | } 10 | switch (params.cookieName) { 11 | case 'selected-model': 12 | if (!chatModels.find((model) => model.id === value)) { 13 | return new Response('Unknown model', { 14 | status: 400 15 | }); 16 | } 17 | break; 18 | default: { 19 | return new Response('Unknown cookie', { 20 | status: 404 21 | }); 22 | } 23 | } 24 | 25 | cookies.set(params.cookieName, value, { 26 | path: '/', 27 | sameSite: 'lax', 28 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), 29 | httpOnly: true 30 | }); 31 | return new Response(null, { 32 | status: 200 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/vote/[chatId]/+server.ts: -------------------------------------------------------------------------------- 1 | import { getChatById, getVotesByChatId } from '$lib/server/db/queries.js'; 2 | import { error } from '@sveltejs/kit'; 3 | import { ok, safeTry } from 'neverthrow'; 4 | 5 | export async function GET({ locals: { user }, params: { chatId } }) { 6 | if (!user) { 7 | error(401, 'Unauthorized'); 8 | } 9 | 10 | await safeTry(async function* () { 11 | const chat = yield* getChatById({ id: chatId }); 12 | if (chat.userId !== user.id) { 13 | error(403, 'Forbidden'); 14 | } 15 | return ok(undefined); 16 | }).orElse(() => error(404, 'Not found')); 17 | 18 | return getVotesByChatId({ id: chatId }).match( 19 | (votes) => Response.json(votes), 20 | () => error(500, 'An error occurred while processing your request') 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/(chat)/api/vote/[chatId]/[messageId]/+server.ts: -------------------------------------------------------------------------------- 1 | import { getChatById, voteMessage } from '$lib/server/db/queries'; 2 | import { error } from '@sveltejs/kit'; 3 | import { ok, safeTry } from 'neverthrow'; 4 | 5 | export async function PATCH({ locals: { user }, params: { chatId, messageId }, request }) { 6 | if (!user) { 7 | error(401, 'Unauthorized'); 8 | } 9 | 10 | await safeTry(async function* () { 11 | const chat = yield* getChatById({ id: chatId }); 12 | if (chat.userId !== user.id) { 13 | error(403, 'Forbidden'); 14 | } 15 | return ok(undefined); 16 | }).orElse(() => error(404, 'Not found')); 17 | 18 | const { type }: { type: 'up' | 'down' } = await request.json(); 19 | 20 | // TODO votes are flawed, anyone can change the vote on any message 21 | return voteMessage({ chatId, messageId, type }).match( 22 | () => new Response('Message voted', { status: 200 }), 23 | () => error(500, 'An error occurred while processing your request') 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/(chat)/chat/[chatId]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { getChatById, getMessagesByChatId } from '$lib/server/db/queries'; 2 | import { error } from '@sveltejs/kit'; 3 | import { ok, safeTry } from 'neverthrow'; 4 | 5 | export async function load({ params: { chatId }, locals: { user } }) { 6 | return safeTry(async function* () { 7 | const chat = yield* getChatById({ id: chatId }).mapErr(() => error(404, 'Not found')); 8 | if (chat.visibility === 'private') { 9 | if (!user || chat.userId !== user.id) { 10 | error(404, 'Not found'); 11 | } 12 | } 13 | const messages = yield* getMessagesByChatId({ id: chatId }); 14 | 15 | return ok({ chat, messages }); 16 | }).match( 17 | (result) => result, 18 | () => error(500, 'An error occurred while processing your request') 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/(chat)/chat/[chatId]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | {@render children()} 12 | 13 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/ai-chatbot-svelte/9247ebb9a4bba39284e144c98b22a496c158c271/static/favicon.png -------------------------------------------------------------------------------- /static/fonts/geist-mono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/ai-chatbot-svelte/9247ebb9a4bba39284e144c98b22a496c158c271/static/fonts/geist-mono.woff2 -------------------------------------------------------------------------------- /static/fonts/geist.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/ai-chatbot-svelte/9247ebb9a4bba39284e144c98b22a496c158c271/static/fonts/geist.woff2 -------------------------------------------------------------------------------- /static/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/ai-chatbot-svelte/9247ebb9a4bba39284e144c98b22a496c158c271/static/opengraph-image.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter() 12 | } 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()] 7 | }); 8 | --------------------------------------------------------------------------------