diff --git a/.prettierrc b/.prettierrc index 7ebb855..e378d4e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "useTabs": true, "singleQuote": true, - "trailingComma": "none", + "trailingComma": "es5", "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ diff --git a/README.md b/README.md index 5be1d6f..9fc2ddb 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ IDK, calm down ## TODO -- [-] Login & Auth +- [x] Login & Auth - [ ] Convex schemas for chats +- [ ] Actual fucking UI for chat - [ ] Providers (BYOK) - [ ] Openrouter - [ ] HuggingFace diff --git a/eslint.config.js b/eslint.config.js index ef07d32..3e0ca46 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,9 +18,9 @@ export default ts.config( ...svelte.configs.prettier, { languageOptions: { - globals: { ...globals.browser, ...globals.node } + globals: { ...globals.browser, ...globals.node }, }, - rules: { 'no-undef': 'off' } + rules: { 'no-undef': 'off' }, }, { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], @@ -29,8 +29,8 @@ export default ts.config( projectService: true, extraFileExtensions: ['.svelte'], parser: ts.parser, - svelteConfig - } - } + svelteConfig, + }, + }, } ); diff --git a/playwright.config.ts b/playwright.config.ts index f6c81af..1563896 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { command: 'npm run build && npm run preview', - port: 4173 + port: 4173, }, - testDir: 'e2e' + testDir: 'e2e', }); diff --git a/src/app.d.ts b/src/app.d.ts index 3546a27..c31e2c6 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,7 @@ // See https://svelte.dev/docs/kit/types#app.d.ts + +import type { Session, User } from 'better-auth'; + // for information about these interfaces declare global { namespace App { diff --git a/src/lib/actions/active.svelte.ts b/src/lib/actions/active.svelte.ts index 98aab91..cc74c6e 100644 --- a/src/lib/actions/active.svelte.ts +++ b/src/lib/actions/active.svelte.ts @@ -63,7 +63,7 @@ export function active(node: HTMLAnchorElement, opts: Omit = {}) */ export function attachActive(opts: Omit = {}) { return { - [createAttachmentKey()]: (node: HTMLAnchorElement) => active(node, opts) + [createAttachmentKey()]: (node: HTMLAnchorElement) => active(node, opts), }; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 69f1794..7a96061 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -11,8 +11,8 @@ export const auth = betterAuth({ socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET! - } + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + }, }, - plugins: [] + plugins: [], }); diff --git a/src/lib/backend/convex/README.md b/src/lib/backend/convex/README.md index 4d82e13..9dd98a6 100644 --- a/src/lib/backend/convex/README.md +++ b/src/lib/backend/convex/README.md @@ -7,29 +7,29 @@ A query function that takes two arguments looks like: ```ts // functions.js -import { query } from "./_generated/server"; -import { v } from "convex/values"; +import { query } from './_generated/server'; +import { v } from 'convex/values'; export const myQueryFunction = query({ - // Validators for arguments. - args: { - first: v.number(), - second: v.string(), - }, + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, - // Function implementation. - handler: async (ctx, args) => { - // Read the database as many times as you need here. - // See https://docs.convex.dev/database/reading-data. - const documents = await ctx.db.query("tablename").collect(); + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query('tablename').collect(); - // Arguments passed from the client are properties of the args object. - console.log(args.first, args.second); + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); - // Write arbitrary JavaScript here: filter, aggregate, build derived data, - // remove non-public properties, or create new objects. - return documents; - }, + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, }); ``` @@ -37,8 +37,8 @@ Using this query function in a React component looks like: ```ts const data = useQuery(api.functions.myQueryFunction, { - first: 10, - second: "hello", + first: 10, + second: 'hello', }); ``` @@ -46,27 +46,27 @@ A mutation function looks like: ```ts // functions.js -import { mutation } from "./_generated/server"; -import { v } from "convex/values"; +import { mutation } from './_generated/server'; +import { v } from 'convex/values'; export const myMutationFunction = mutation({ - // Validators for arguments. - args: { - first: v.string(), - second: v.string(), - }, + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, - // Function implementation. - handler: async (ctx, args) => { - // Insert or modify documents in the database here. - // Mutations can also read from the database like queries. - // See https://docs.convex.dev/database/writing-data. - const message = { body: args.first, author: args.second }; - const id = await ctx.db.insert("messages", message); + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert('messages', message); - // Optionally, return a value from your mutation. - return await ctx.db.get(id); - }, + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, }); ``` @@ -75,13 +75,11 @@ Using this mutation function in a React component looks like: ```ts const mutation = useMutation(api.functions.myMutationFunction); function handleButtonPress() { - // fire and forget, the most common way to use mutations - mutation({ first: "Hello!", second: "me" }); - // OR - // use the result once the mutation has completed - mutation({ first: "Hello!", second: "me" }).then((result) => - console.log(result), - ); + // fire and forget, the most common way to use mutations + mutation({ first: 'Hello!', second: 'me' }); + // OR + // use the result once the mutation has completed + mutation({ first: 'Hello!', second: 'me' }).then((result) => console.log(result)); } ``` diff --git a/src/lib/backend/convex/betterAuth.ts b/src/lib/backend/convex/betterAuth.ts index abd494b..b518756 100644 --- a/src/lib/backend/convex/betterAuth.ts +++ b/src/lib/backend/convex/betterAuth.ts @@ -6,7 +6,7 @@ const { betterAuth, query, insert, update, delete_, count, getSession } = Convex action, internalQuery, internalMutation, - internal + internal, }) as ConvexReturnType; export { betterAuth, query, insert, update, delete_, count, getSession }; diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 0d164b0..163cb71 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -1,8 +1,15 @@ import { defineSchema, defineTable } from 'convex/server'; import { v } from 'convex/values'; +import { Provider } from '../../../lib/types'; + +export const providerValidator = v.union(...Object.values(Provider).map((p) => v.literal(p))); export default defineSchema({ user_keys: defineTable({ - openRouter: v.string() + provider: providerValidator, + user_id: v.id('users'), + key: v.string(), }) + .index('by_user', ['user_id']) + .index('by_provider_user', ['provider', 'user_id']), }); diff --git a/src/lib/backend/convex/tasks.ts b/src/lib/backend/convex/tasks.ts deleted file mode 100644 index 5e9e986..0000000 --- a/src/lib/backend/convex/tasks.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { mutation } from './_generated/server'; -import { v } from 'convex/values'; diff --git a/src/lib/backend/convex/tsconfig.json b/src/lib/backend/convex/tsconfig.json index 7374127..a84f928 100644 --- a/src/lib/backend/convex/tsconfig.json +++ b/src/lib/backend/convex/tsconfig.json @@ -1,25 +1,25 @@ { - /* This TypeScript project config describes the environment that - * Convex functions run in and is used to typecheck them. - * You can modify it, but some settings are required to use Convex. - */ - "compilerOptions": { - /* These settings are not required by Convex and can be modified. */ - "allowJs": true, - "strict": true, - "moduleResolution": "Bundler", - "jsx": "react-jsx", - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings are required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, - /* These compiler options are required by Convex */ - "target": "ESNext", - "lib": ["ES2021", "dom"], - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "isolatedModules": true, - "noEmit": true - }, - "include": ["./**/*"], - "exclude": ["./_generated"] + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] } diff --git a/src/lib/backend/convex/user_keys.ts b/src/lib/backend/convex/user_keys.ts new file mode 100644 index 0000000..0839854 --- /dev/null +++ b/src/lib/backend/convex/user_keys.ts @@ -0,0 +1,46 @@ +import { v } from 'convex/values'; +import { Provider } from '../../types'; +import { mutation, query } from './_generated/server'; +import { providerValidator } from './schema'; + +export const get = query({ + args: { + user_id: v.id('users'), + }, + handler: async (ctx, args) => { + const allKeys = await ctx.db + .query('user_keys') + .withIndex('by_user', (q) => q.eq('user_id', args.user_id)) + .collect(); + + return Object.values(Provider).reduce( + (acc, key) => { + acc[key] = allKeys.find((item) => item.provider === key)?.key; + return acc; + }, + {} as Record + ); + }, +}); + +export const set = mutation({ + args: { + provider: providerValidator, + user_id: v.id('users'), + key: v.string(), + }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query('user_keys') + .withIndex('by_provider_user', (q) => + q.eq('provider', args.provider).eq('user_id', args.user_id) + ) + .first(); + + if (existing) { + await ctx.db.replace(existing._id, args); + } else { + await ctx.db.insert('user_keys', args); + } + }, +}); diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts index 34c3e59..4c99688 100644 --- a/src/lib/components/ui/button/index.ts +++ b/src/lib/components/ui/button/index.ts @@ -9,7 +9,7 @@ import Root, { type AnchorElementProps, type ButtonElementProps, type ButtonPropsWithoutHTML, - buttonVariants + buttonVariants, } from './button.svelte'; export { @@ -23,5 +23,5 @@ export { type ButtonVariant, type AnchorElementProps, type ButtonElementProps, - type ButtonPropsWithoutHTML + type ButtonPropsWithoutHTML, }; diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte index ce8f1e8..2e7c51e 100644 --- a/src/lib/components/ui/card/card-content.svelte +++ b/src/lib/components/ui/card/card-content.svelte @@ -1,10 +1,17 @@ - -
+ + {@render children?.()} -
+ diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts index ccf6d7e..4166cc4 100644 --- a/src/lib/components/ui/sidebar/index.ts +++ b/src/lib/components/ui/sidebar/index.ts @@ -3,4 +3,4 @@ import Sidebar from './sidebar-sidebar.svelte'; import Inset from './sidebar-inset.svelte'; import Trigger from './sidebar-trigger.svelte'; -export { Root, Sidebar, Inset, Trigger }; \ No newline at end of file +export { Root, Sidebar, Inset, Trigger }; diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte index 5876623..fd845a7 100644 --- a/src/lib/components/ui/sidebar/sidebar.svelte +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -11,7 +11,7 @@
{@render children?.()} diff --git a/src/lib/state/session.svelte.ts b/src/lib/state/session.svelte.ts new file mode 100644 index 0000000..28f5a65 --- /dev/null +++ b/src/lib/state/session.svelte.ts @@ -0,0 +1,8 @@ +import { page } from '$app/state'; +import type { Session, User } from 'better-auth'; + +export const session = { + get current() { + return page.data.session as { session: Session; user: User } | null; + }, +}; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..dbb34cc --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,8 @@ +export const Provider = { + OpenRouter: 'openrouter', + HuggingFace: 'huggingface', + OpenAI: 'openai', + Anthropic: 'anthropic', +} as const; + +export type Provider = (typeof Provider)[keyof typeof Provider]; diff --git a/src/lib/utils/object.ts b/src/lib/utils/object.ts new file mode 100644 index 0000000..03d7a4b --- /dev/null +++ b/src/lib/utils/object.ts @@ -0,0 +1,4 @@ +// typed object.keys +export function keys(obj: T): Array { + return Object.keys(obj) as Array; +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 3aee320..e81c390 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,7 +1,9 @@ -export async function load({locals}) { - const session = await locals.auth(); +import type { LayoutServerLoad } from './$types'; - return { - session - } -} \ No newline at end of file +export const load: LayoutServerLoad = async ({ locals }) => { + const session = await locals.auth(); + + return { + session, + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b208cf2..77bf533 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,10 +1,12 @@ diff --git a/src/routes/account/+layout.server.ts b/src/routes/account/+layout.server.ts index 987f3c7..c8ce521 100644 --- a/src/routes/account/+layout.server.ts +++ b/src/routes/account/+layout.server.ts @@ -6,6 +6,6 @@ export async function load({ locals, url }) { if (!session) redirectToLogin(url); return { - session + session, }; } diff --git a/src/routes/account/+layout.svelte b/src/routes/account/+layout.svelte index 1678b9f..3a5d1d5 100644 --- a/src/routes/account/+layout.svelte +++ b/src/routes/account/+layout.svelte @@ -12,20 +12,20 @@ const navigation: { title: string; href: string }[] = [ { title: 'Account', - href: '/account' + href: '/account', }, { title: 'Customization', - href: '/account/customization' + href: '/account/customization', }, { title: 'Models', - href: '/account/models' + href: '/account/models', }, { title: 'API Keys', - href: '/account/api-keys' - } + href: '/account/api-keys', + }, ]; async function signOut() { @@ -49,14 +49,19 @@