├── .npmrc ├── static └── favicon.png ├── renovate.json ├── src ├── lib │ ├── components │ │ ├── ui │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── dialog │ │ │ │ ├── dialog-portal.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── index.ts │ │ │ │ └── dialog-content.svelte │ │ │ ├── alert-dialog │ │ │ │ ├── alert-dialog-portal.svelte │ │ │ │ ├── alert-dialog-header.svelte │ │ │ │ ├── alert-dialog-footer.svelte │ │ │ │ ├── alert-dialog-description.svelte │ │ │ │ ├── alert-dialog-title.svelte │ │ │ │ ├── alert-dialog-action.svelte │ │ │ │ ├── alert-dialog-cancel.svelte │ │ │ │ ├── alert-dialog-overlay.svelte │ │ │ │ ├── alert-dialog-content.svelte │ │ │ │ └── index.ts │ │ │ ├── tooltip │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ │ ├── sheet │ │ │ │ ├── sheet-portal.svelte │ │ │ │ ├── sheet-header.svelte │ │ │ │ ├── sheet-title.svelte │ │ │ │ ├── sheet-description.svelte │ │ │ │ ├── sheet-footer.svelte │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ ├── sheet-content.svelte │ │ │ │ └── index.ts │ │ │ ├── icons │ │ │ │ ├── IconVercel.svelte │ │ │ │ ├── IconPlus.svelte │ │ │ │ ├── IconCheck.svelte │ │ │ │ ├── IconSidebar.svelte │ │ │ │ ├── IconArrowDown.svelte │ │ │ │ ├── IconSeparator.svelte │ │ │ │ ├── IconArrowRight.svelte │ │ │ │ ├── IconArrowElbow.svelte │ │ │ │ ├── IconCopy.svelte │ │ │ │ ├── IconClose.svelte │ │ │ │ ├── IconStop.svelte │ │ │ │ ├── IconSpinner.svelte │ │ │ │ ├── IconUser.svelte │ │ │ │ ├── IconExternalLink.svelte │ │ │ │ ├── IconMessage.svelte │ │ │ │ ├── IconTrash.svelte │ │ │ │ ├── IconMoon.svelte │ │ │ │ ├── IconUsers.svelte │ │ │ │ ├── IconShare.svelte │ │ │ │ ├── IconRefresh.svelte │ │ │ │ ├── IconSun.svelte │ │ │ │ ├── IconSvelteChat.svelte │ │ │ │ ├── IconGitHub.svelte │ │ │ │ ├── index.ts │ │ │ │ └── IconOpenAI.svelte │ │ │ ├── badge │ │ │ │ ├── badge.svelte │ │ │ │ └── index.ts │ │ │ └── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ ├── SidebarFooter.svelte │ │ ├── ExternalLink.svelte │ │ ├── FooterText.svelte │ │ ├── ChatList.svelte │ │ ├── ThemeToggle.svelte │ │ ├── SidebarList.svelte │ │ ├── Sidebar.svelte │ │ ├── LoginButton.svelte │ │ ├── ChatMessage.svelte │ │ ├── ButtonScrollToBottom.svelte │ │ ├── Chat.svelte │ │ ├── ChatMessageActions.svelte │ │ ├── SidebarItem.svelte │ │ ├── ClearHistory.svelte │ │ ├── EmptyScreen.svelte │ │ ├── ChatPanel.svelte │ │ ├── PromptForm.svelte │ │ ├── Header.svelte │ │ ├── SidebarActions.svelte │ │ └── UserMenu.svelte │ ├── kv.ts │ ├── types.ts │ ├── theme.ts │ ├── chat.ts │ └── utils.ts ├── routes │ ├── +page.svelte │ ├── sign-in │ │ └── +page.svelte │ ├── chat │ │ └── [id] │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ ├── +layout.server.ts │ ├── +layout.svelte │ └── api │ │ └── chat │ │ └── +server.ts ├── app.d.ts ├── app.html ├── hooks.server.ts └── app.postcss ├── vite.config.ts ├── .gitignore ├── .eslintignore ├── .prettierignore ├── .prettierrc ├── components.json ├── postcss.config.cjs ├── .env.example ├── tsconfig.json ├── .eslintrc.cjs ├── svelte.config.js ├── LICENSE ├── tailwind.config.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianyuan/sveltekit-ai-chatbot/HEAD/static/favicon.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./separator.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator 7 | }; 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/routes/sign-in/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /src/lib/kv.ts: -------------------------------------------------------------------------------- 1 | import { KV_REST_API_TOKEN, KV_REST_API_URL } from '$env/static/private'; 2 | import { createClient } from '@vercel/kv'; 3 | 4 | export const kv = createClient({ 5 | url: KV_REST_API_URL, 6 | token: KV_REST_API_TOKEN, 7 | automaticDeserialization: false 8 | }); 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'ai'; 2 | 3 | export interface Chat extends Record { 4 | id: string; 5 | title: string; 6 | createdAt: Date; 7 | userId: string; 8 | path: string; 9 | messages: Message[]; 10 | sharePath?: string; 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/chat/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/SidebarFooter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
11 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app.postcss", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils" 12 | } 13 | } -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { getChats } from '$lib/chat'; 2 | import type { LayoutServerLoad } from './$types'; 3 | 4 | export const load: LayoutServerLoad = async (event) => { 5 | const session = await event.locals.getSession(); 6 | const chats = getChats(session?.user.id); 7 | 8 | return { 9 | session, 10 | chats 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer 10 | ] 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from "bits-ui"; 2 | import Content from "./tooltip-content.svelte"; 3 | 4 | const Root = TooltipPrimitive.Root; 5 | const Trigger = TooltipPrimitive.Trigger; 6 | 7 | export { 8 | Root, 9 | Trigger, 10 | Content, 11 | // 12 | Root as Tooltip, 13 | Content as TooltipContent, 14 | Trigger as TooltipTrigger 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-portal.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconVercel.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconPlus.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | 3 | import type { DefaultSession } from '@auth/core/types'; 4 | 5 | // for information about these interfaces 6 | declare global { 7 | namespace App { 8 | // interface Error {} 9 | // interface Locals {} 10 | // interface PageData {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | declare module '@auth/core/types' { 16 | interface Session extends DefaultSession { 17 | user: { 18 | id: string; 19 | } & DefaultSession['user']; 20 | } 21 | } 22 | 23 | export {}; 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconCheck.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ExternalLink.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconSidebar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconArrowDown.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconSeparator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## Get your OpenAI API Key 2 | OPENAI_API_KEY=XXXXXXXX 3 | 4 | ## Generate a random secret: https://generate-secret.vercel.app/32 5 | AUTH_SECRET=XXXXXXXX 6 | 7 | ## Create a GitHub OAuth app here: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app 8 | GITHUB_ID=XXXXXXXX 9 | GITHUB_SECRET=XXXXXXXX 10 | 11 | # instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and 12 | KV_URL=XXXXXXXX 13 | KV_REST_API_URL=XXXXXXXX 14 | KV_REST_API_TOKEN=XXXXXXXX 15 | KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconArrowRight.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/chat/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | import { getChat } from '$lib/chat'; 4 | 5 | export const load: PageServerLoad = async ({ params, locals }) => { 6 | const session = await locals.getSession(); 7 | if (!session?.user) { 8 | throw redirect(302, `/sign-in?redirect=/chat/${params.id}`); 9 | } 10 | 11 | const chat = await getChat(params.id, session.user.id); 12 | 13 | if (!chat) { 14 | throw error(404, 'Not found'); 15 | } 16 | 17 | return { chat }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconArrowElbow.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconCopy.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /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 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconClose.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/FooterText.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |

13 | Open source AI chatbot built with 14 | SvelteKit and 15 | Vercel KV 16 | . 17 |

18 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconStop.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ChatList.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if $messages?.length} 10 |
11 | {#each $messages as message, index} 12 |
13 | 14 | {#if index < $messages.length - 1} 15 | 16 | {/if} 17 |
18 | {/each} 19 |
20 | {/if} 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconSpinner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconUser.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconExternalLink.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconMessage.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import { derived, writable } from 'svelte/store'; 3 | 4 | export type Theme = 'system' | 'light' | 'dark'; 5 | 6 | // TODO: Add a way to persist the theme in local storage 7 | // TODO: Add a way to watch for system theme changes 8 | 9 | export const theme = writable('system'); 10 | 11 | export const resolvedTheme = derived(theme, ($theme) => { 12 | if ($theme === 'system') { 13 | if (!browser) return 'light'; 14 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches 15 | ? 'dark' 16 | : 'light'; 17 | return systemTheme === 'dark' ? 'dark' : 'light'; 18 | } 19 | return $theme; 20 | }); 21 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | 17 |
%sveltekit.body%
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconTrash.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ThemeToggle.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/lib/components/SidebarList.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if chats.length} 14 |
15 | {#each chats as chat (chat.id)} 16 | 17 | 18 | 19 | {/each} 20 |
21 | {:else} 22 |
23 |

No chat history

24 |
25 | {/if} 26 |
27 | -------------------------------------------------------------------------------- /src/lib/components/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | Chat History 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconMoon.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { SvelteKitAuth } from '@auth/sveltekit'; 2 | import GitHub from '@auth/core/providers/github'; 3 | import { GITHUB_ID, GITHUB_SECRET } from '$env/static/private'; 4 | 5 | export const handle = SvelteKitAuth({ 6 | providers: [ 7 | // @ts-ignore 8 | GitHub({ 9 | clientId: GITHUB_ID, 10 | clientSecret: GITHUB_SECRET 11 | }) 12 | ], 13 | callbacks: { 14 | async jwt({ token, profile }) { 15 | if (profile?.id) { 16 | token.id = `${profile.id}`; 17 | token.image = profile.picture; 18 | } 19 | return token; 20 | }, 21 | async session({ session, token }) { 22 | if (session.user && typeof token.id === 'string') { 23 | session.user.id = token.id; 24 | } 25 | return session; 26 | } 27 | }, 28 | pages: { 29 | signIn: '/sign-in' 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconUsers.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconShare.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/chat.ts: -------------------------------------------------------------------------------- 1 | import type { Chat } from '$lib/types'; 2 | import { kv } from '@vercel/kv'; 3 | 4 | export async function getChats(userId?: string | null) { 5 | if (!userId) { 6 | return []; 7 | } 8 | 9 | try { 10 | const pipeline = kv.pipeline(); 11 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { 12 | rev: true 13 | }); 14 | 15 | for (const chat of chats) { 16 | pipeline.hgetall(chat); 17 | } 18 | 19 | const results = await pipeline.exec(); 20 | 21 | return results as Chat[]; 22 | } catch (error) { 23 | return []; 24 | } 25 | } 26 | 27 | export async function getChat(id: string, userId: string) { 28 | const chat = await kv.hgetall(`chat:${id}`); 29 | 30 | if (!chat || (userId && `${chat.userId}` !== userId)) { 31 | return null; 32 | } 33 | 34 | return chat; 35 | } 36 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-auto'; 3 | import { vitePreprocess } from '@sveltejs/kit/vite'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 8 | // for more information about preprocessors 9 | preprocess: [ 10 | vitePreprocess(), 11 | preprocess({ 12 | postcss: true 13 | }) 14 | ], 15 | 16 | kit: { 17 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 18 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 19 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 20 | adapter: adapter() 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconRefresh.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/LoginButton.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | SvelteKit AI Chatbot 26 | 27 | 28 |
29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /src/lib/components/ChatMessage.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
17 | {#if message.role === 'user'} 18 | 19 | {:else} 20 | 21 | {/if} 22 |
23 |
24 | {message.content} 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { tv, type VariantProps } from "tailwind-variants"; 2 | export { default as Badge } from "./badge.svelte"; 3 | 4 | export const badgeVariants = tv({ 5 | base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 6 | variants: { 7 | variant: { 8 | default: 9 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground", 10 | secondary: 11 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", 12 | destructive: 13 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", 14 | outline: "text-foreground" 15 | } 16 | }, 17 | defaultVariants: { 18 | variant: "default" 19 | } 20 | }); 21 | 22 | export type Variant = VariantProps["variant"]; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | const Root = DialogPrimitive.Root; 4 | const Trigger = DialogPrimitive.Trigger; 5 | 6 | import Title from "./dialog-title.svelte"; 7 | import Portal from "./dialog-portal.svelte"; 8 | import Footer from "./dialog-footer.svelte"; 9 | import Header from "./dialog-header.svelte"; 10 | import Overlay from "./dialog-overlay.svelte"; 11 | import Content from "./dialog-content.svelte"; 12 | import Description from "./dialog-description.svelte"; 13 | 14 | export { 15 | Root, 16 | Title, 17 | Portal, 18 | Footer, 19 | Header, 20 | Trigger, 21 | Overlay, 22 | Content, 23 | Description, 24 | // 25 | Root as Dialog, 26 | Title as DialogTitle, 27 | Portal as DialogPortal, 28 | Footer as DialogFooter, 29 | Header as DialogHeader, 30 | Trigger as DialogTrigger, 31 | Overlay as DialogOverlay, 32 | Content as DialogContent, 33 | Description as DialogDescription 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconSun.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconSvelteChat.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ButtonScrollToBottom.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /src/lib/components/Chat.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | {#if $messages.length} 28 | 29 | {:else} 30 | 31 | {/if} 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jian Yuan Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconGitHub.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | GitHub 17 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ChatMessageActions.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
31 | 32 | 40 | 41 |
42 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; 2 | 3 | const Root = AlertDialogPrimitive.Root; 4 | const Trigger = AlertDialogPrimitive.Trigger; 5 | 6 | import Title from "./alert-dialog-title.svelte"; 7 | import Action from "./alert-dialog-action.svelte"; 8 | import Cancel from "./alert-dialog-cancel.svelte"; 9 | import Portal from "./alert-dialog-portal.svelte"; 10 | import Footer from "./alert-dialog-footer.svelte"; 11 | import Header from "./alert-dialog-header.svelte"; 12 | import Overlay from "./alert-dialog-overlay.svelte"; 13 | import Content from "./alert-dialog-content.svelte"; 14 | import Description from "./alert-dialog-description.svelte"; 15 | 16 | export { 17 | Root, 18 | Title, 19 | Action, 20 | Cancel, 21 | Portal, 22 | Footer, 23 | Header, 24 | Trigger, 25 | Overlay, 26 | Content, 27 | Description, 28 | // 29 | Root as AlertDialog, 30 | Title as AlertDialogTitle, 31 | Action as AlertDialogAction, 32 | Cancel as AlertDialogCancel, 33 | Portal as AlertDialogPortal, 34 | Footer as AlertDialogFooter, 35 | Header as AlertDialogHeader, 36 | Trigger as AlertDialogTrigger, 37 | Overlay as AlertDialogOverlay, 38 | Content as AlertDialogContent, 39 | Description as AlertDialogDescription 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 28 | 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/lib/components/SidebarItem.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | {#if chat.sharePath} 17 | 18 | 19 | 20 | 21 | This is a shared chat. 22 | 23 | {:else} 24 | 25 | {/if} 26 |
27 | 39 | {#if isActive} 40 |
41 | 42 |
43 | {/if} 44 |
45 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as IconArrowDown } from './IconArrowDown.svelte'; 2 | export { default as IconArrowElbow } from './IconArrowElbow.svelte'; 3 | export { default as IconArrowRight } from './IconArrowRight.svelte'; 4 | export { default as IconCheck } from './IconCheck.svelte'; 5 | export { default as IconClose } from './IconClose.svelte'; 6 | export { default as IconCopy } from './IconCopy.svelte'; 7 | export { default as IconExternalLink } from './IconExternalLink.svelte'; 8 | export { default as IconGitHub } from './IconGitHub.svelte'; 9 | export { default as IconMessage } from './IconMessage.svelte'; 10 | export { default as IconMoon } from './IconMoon.svelte'; 11 | export { default as IconOpenAI } from './IconOpenAI.svelte'; 12 | export { default as IconPlus } from './IconPlus.svelte'; 13 | export { default as IconRefresh } from './IconRefresh.svelte'; 14 | export { default as IconSeparator } from './IconSeparator.svelte'; 15 | export { default as IconShare } from './IconShare.svelte'; 16 | export { default as IconSidebar } from './IconSidebar.svelte'; 17 | export { default as IconSpinner } from './IconSpinner.svelte'; 18 | export { default as IconStop } from './IconStop.svelte'; 19 | export { default as IconSun } from './IconSun.svelte'; 20 | export { default as IconSvelteChat } from './IconSvelteChat.svelte'; 21 | export { default as IconTrash } from './IconTrash.svelte'; 22 | export { default as IconUser } from './IconUser.svelte'; 23 | export { default as IconUsers } from './IconUsers.svelte'; 24 | export { default as IconVercel } from './IconVercel.svelte'; 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-content.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | 39 | 40 | 43 | 44 | Close 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | const config = { 5 | darkMode: ["class"], 6 | content: ["./src/**/*.{html,js,svelte,ts}"], 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: "2rem", 11 | screens: { 12 | "2xl": "1400px" 13 | } 14 | }, 15 | extend: { 16 | colors: { 17 | border: "hsl(var(--border))", 18 | input: "hsl(var(--input))", 19 | ring: "hsl(var(--ring))", 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | primary: { 23 | DEFAULT: "hsl(var(--primary))", 24 | foreground: "hsl(var(--primary-foreground))" 25 | }, 26 | secondary: { 27 | DEFAULT: "hsl(var(--secondary))", 28 | foreground: "hsl(var(--secondary-foreground))" 29 | }, 30 | destructive: { 31 | DEFAULT: "hsl(var(--destructive))", 32 | foreground: "hsl(var(--destructive-foreground))" 33 | }, 34 | muted: { 35 | DEFAULT: "hsl(var(--muted))", 36 | foreground: "hsl(var(--muted-foreground))" 37 | }, 38 | accent: { 39 | DEFAULT: "hsl(var(--accent))", 40 | foreground: "hsl(var(--accent-foreground))" 41 | }, 42 | popover: { 43 | DEFAULT: "hsl(var(--popover))", 44 | foreground: "hsl(var(--popover-foreground))" 45 | }, 46 | card: { 47 | DEFAULT: "hsl(var(--card))", 48 | foreground: "hsl(var(--card-foreground))" 49 | } 50 | }, 51 | borderRadius: { 52 | lg: "var(--radius)", 53 | md: "calc(var(--radius) - 2px)", 54 | sm: "calc(var(--radius) - 4px)" 55 | }, 56 | fontFamily: { 57 | sans: [...fontFamily.sans] 58 | } 59 | } 60 | }, 61 | }; 62 | 63 | export default config; 64 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./button.svelte"; 2 | import { tv, type VariantProps } from "tailwind-variants"; 3 | import type { Button as ButtonPrimitive } from "bits-ui"; 4 | 5 | const buttonVariants = tv({ 6 | base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 7 | variants: { 8 | variant: { 9 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 10 | destructive: 11 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 12 | outline: 13 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 14 | secondary: 15 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline" 18 | }, 19 | size: { 20 | default: "h-10 px-4 py-2", 21 | sm: "h-9 rounded-md px-3", 22 | lg: "h-11 rounded-md px-8", 23 | icon: "h-10 w-10" 24 | } 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default" 29 | } 30 | }); 31 | 32 | type Variant = VariantProps["variant"]; 33 | type Size = VariantProps["size"]; 34 | 35 | type Props = ButtonPrimitive.Props & { 36 | variant?: Variant; 37 | size?: Size; 38 | }; 39 | 40 | type Events = ButtonPrimitive.Events; 41 | 42 | export { 43 | Root, 44 | type Props, 45 | type Events, 46 | // 47 | Root as Button, 48 | type Props as ButtonProps, 49 | type Events as ButtonEvents, 50 | buttonVariants 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/components/ClearHistory.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | Are you absolutely sure? 33 | 34 | This will permanently delete your chat history and remove your data from our servers. 35 | 36 | 37 | 38 | Cancel 39 | { 42 | event.preventDefault(); 43 | 44 | // TODO: Clear chat history 45 | isPending = true; 46 | setTimeout(() => { 47 | isPending = false; 48 | 49 | isOpen = false; 50 | goto('/'); 51 | }, 1000); 52 | }} 53 | > 54 | {#if isPending} 55 | 56 | {/if} 57 | Delete 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/lib/components/EmptyScreen.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |
27 |

Welcome to SvelteKit AI Chatbot!

28 |

29 | This is an open source AI chatbot app template built with 30 | SvelteKit and 31 | Vercel KV 32 | . 33 |

34 |

35 | You can start a conversation here or try the following examples: 36 |

37 |
38 | {#each exampleMessages as message} 39 | 47 | {/each} 48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --font-sans: 'Inter', sans-serif; 8 | --font-mono: 'JetBrains Mono', sans-serif; 9 | 10 | --background: 0 0% 100%; 11 | --foreground: 240 10% 3.9%; 12 | 13 | --muted: 240 4.8% 95.9%; 14 | --muted-foreground: 240 3.8% 46.1%; 15 | 16 | --popover: 0 0% 100%; 17 | --popover-foreground: 240 10% 3.9%; 18 | 19 | --card: 0 0% 100%; 20 | --card-foreground: 240 10% 3.9%; 21 | 22 | --border: 240 5.9% 90%; 23 | --input: 240 5.9% 90%; 24 | 25 | --primary: 240 5.9% 10%; 26 | --primary-foreground: 0 0% 98%; 27 | 28 | --secondary: 240 4.8% 95.9%; 29 | --secondary-foreground: 240 5.9% 10%; 30 | 31 | --accent: 240 4.8% 95.9%; 32 | --accent-foreground: ; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 0 0% 98%; 36 | 37 | --ring: 240 5% 64.9%; 38 | 39 | --radius: 0.5rem; 40 | } 41 | 42 | .dark { 43 | --background: 240 10% 3.9%; 44 | --foreground: 0 0% 98%; 45 | 46 | --muted: 240 3.7% 15.9%; 47 | --muted-foreground: 240 5% 64.9%; 48 | 49 | --popover: 240 10% 3.9%; 50 | --popover-foreground: 0 0% 98%; 51 | 52 | --card: 240 10% 3.9%; 53 | --card-foreground: 0 0% 98%; 54 | 55 | --border: 240 3.7% 15.9%; 56 | --input: 240 3.7% 15.9%; 57 | 58 | --primary: 0 0% 98%; 59 | --primary-foreground: 240 5.9% 10%; 60 | 61 | --secondary: 240 3.7% 15.9%; 62 | --secondary-foreground: 0 0% 98%; 63 | 64 | --accent: 240 3.7% 15.9%; 65 | --accent-foreground: ; 66 | 67 | --destructive: 0 62.8% 30.6%; 68 | --destructive-foreground: 0 85.7% 97.3%; 69 | 70 | --ring: 240 3.7% 15.9%; 71 | } 72 | } 73 | 74 | @layer base { 75 | * { 76 | @apply border-border; 77 | } 78 | body { 79 | @apply bg-background text-foreground antialiased; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/IconOpenAI.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | OpenAI icon 17 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ChatPanel.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 |
22 | {#if $isLoading} 23 | 27 | {:else if $messages?.length > 0} 28 | 32 | {/if} 33 |
34 |
37 | { 39 | await append({ 40 | id, 41 | content: event.detail, 42 | role: 'user' 43 | }); 44 | }} 45 | {input} 46 | {isLoading} 47 | /> 48 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-ai-chatbot", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write ." 13 | }, 14 | "dependencies": { 15 | "@auth/core": "^0.18.1", 16 | "@auth/sveltekit": "^0.3.12", 17 | "@fontsource/inter": "^5.0.15", 18 | "@fontsource/jetbrains-mono": "^5.0.17", 19 | "@vercel/kv": "^1.0.0", 20 | "ai": "^2.2.24", 21 | "bits-ui": "^0.9.8", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.0.0", 24 | "lucide-svelte": "^0.474.0", 25 | "nanoid": "^5.0.3", 26 | "openai-edge": "^1.2.2", 27 | "svelte-autosize": "^1.1.0", 28 | "svelte-copy-to-clipboard": "^0.2.5", 29 | "tailwind-merge": "^2.0.0", 30 | "tailwind-variants": "^0.3.0" 31 | }, 32 | "devDependencies": { 33 | "@rgossiaux/svelte-headlessui": "^2.0.0", 34 | "@sveltejs/adapter-auto": "^2.1.1", 35 | "@sveltejs/adapter-vercel": "^3.1.0", 36 | "@sveltejs/kit": "^1.30.4", 37 | "@tailwindcss/typography": "^0.5.10", 38 | "@typescript-eslint/eslint-plugin": "^6.11.0", 39 | "@typescript-eslint/parser": "^6.11.0", 40 | "autoprefixer": "^10.4.16", 41 | "eslint": "^8.54.0", 42 | "eslint-config-prettier": "^9.0.0", 43 | "eslint-plugin-svelte": "^2.35.0", 44 | "postcss": "^8.4.31", 45 | "postcss-load-config": "^4.0.1", 46 | "prettier": "^3.1.0", 47 | "prettier-plugin-svelte": "^3.1.0", 48 | "svelte": "^4.2.19", 49 | "svelte-check": "^3.6.0", 50 | "svelte-preprocess": "^5.1.0", 51 | "tailwindcss": "^3.3.5", 52 | "tailwindcss-animate": "^1.0.7", 53 | "tslib": "^2.6.2", 54 | "typescript": "^5.2.2", 55 | "vite": "^4.5.9" 56 | }, 57 | "overrides": { 58 | "@rgossiaux/svelte-headlessui": { 59 | "svelte": "$svelte" 60 | } 61 | }, 62 | "type": "module" 63 | } -------------------------------------------------------------------------------- /src/routes/api/chat/+server.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '$lib/kv'; 2 | import { nanoid } from '$lib/utils'; 3 | import type { Config } from '@sveltejs/adapter-vercel'; 4 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 5 | import { Configuration, OpenAIApi } from 'openai-edge'; 6 | 7 | import { env } from '$env/dynamic/private'; 8 | // You may want to replace the above with a static private env variable 9 | // for dead-code elimination and build-time type-checking: 10 | // import { OPENAI_API_KEY } from '$env/static/private' 11 | 12 | import type { RequestHandler } from './$types'; 13 | 14 | export const config: Config = { 15 | runtime: 'edge' 16 | }; 17 | 18 | export const POST = (async ({ request, locals: { getSession } }) => { 19 | const json = await request.json(); 20 | const { messages, previewToken } = json; 21 | const session = await getSession(); 22 | 23 | // Create an OpenAI API client 24 | const config = new Configuration({ 25 | apiKey: previewToken || env.OPENAI_API_KEY 26 | }); 27 | const openai = new OpenAIApi(config); 28 | 29 | // Ask OpenAI for a streaming chat completion given the prompt 30 | const response = await openai.createChatCompletion({ 31 | model: 'gpt-3.5-turbo', 32 | messages, 33 | temperature: 0.7, 34 | stream: true 35 | }); 36 | 37 | // Convert the response into a friendly text-stream 38 | const stream = OpenAIStream(response, { 39 | async onCompletion(completion) { 40 | const title = messages[0].content.substring(0, 100); 41 | const userId = session?.user?.id; 42 | if (userId) { 43 | const id = json.id ?? nanoid(); 44 | const createdAt = Date.now(); 45 | const path = `/chat/${id}`; 46 | const payload = { 47 | id, 48 | title, 49 | userId, 50 | createdAt, 51 | path, 52 | messages: [ 53 | ...messages, 54 | { 55 | content: completion, 56 | role: 'assistant' 57 | } 58 | ] 59 | }; 60 | await kv.hmset(`chat:${id}`, payload); 61 | await kv.zadd(`user:chat:${userId}`, { 62 | score: createdAt, 63 | member: `chat:${id}` 64 | }); 65 | } 66 | } 67 | }); 68 | 69 | // Respond with the stream 70 | return new StreamingTextResponse(stream); 71 | }) satisfies RequestHandler; 72 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { cubicOut } from 'svelte/easing'; 4 | import type { TransitionConfig } from 'svelte/transition'; 5 | import { customAlphabet } from 'nanoid'; 6 | 7 | export function cn(...inputs: ClassValue[]) { 8 | return twMerge(clsx(inputs)); 9 | } 10 | 11 | export const nanoid = customAlphabet( 12 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 13 | 7 14 | ); // 7-character random string 15 | 16 | export function formatDate(input: string | number | Date): string { 17 | const date = new Date(input); 18 | return date.toLocaleDateString('en-US', { 19 | month: 'long', 20 | day: 'numeric', 21 | year: 'numeric' 22 | }); 23 | } 24 | 25 | type FlyAndScaleParams = { 26 | y?: number; 27 | x?: number; 28 | start?: number; 29 | duration?: number; 30 | }; 31 | 32 | export const flyAndScale = ( 33 | node: Element, 34 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } 35 | ): TransitionConfig => { 36 | const style = getComputedStyle(node); 37 | const transform = style.transform === 'none' ? '' : style.transform; 38 | 39 | const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { 40 | const [minA, maxA] = scaleA; 41 | const [minB, maxB] = scaleB; 42 | 43 | const percentage = (valueA - minA) / (maxA - minA); 44 | const valueB = percentage * (maxB - minB) + minB; 45 | 46 | return valueB; 47 | }; 48 | 49 | const styleToString = (style: Record): string => { 50 | return Object.keys(style).reduce((str, key) => { 51 | if (style[key] === undefined) return str; 52 | return str + `${key}:${style[key]};`; 53 | }, ''); 54 | }; 55 | 56 | return { 57 | duration: params.duration ?? 200, 58 | delay: 0, 59 | css: (t) => { 60 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); 61 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); 62 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); 63 | 64 | return styleToString({ 65 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, 66 | opacity: t 67 | }); 68 | }, 69 | easing: cubicOut 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/lib/components/PromptForm.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
{ 26 | event.preventDefault(); 27 | if ($input === '') { 28 | return; 29 | } 30 | await dispatch('submit', $input); 31 | $input = ''; 32 | }} 33 | > 34 |
37 | 38 | 39 | 49 | 50 | New Chat 51 | 52 |