From b32eb50c786f6394dcbd4579c22fe7c2396442df Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:29:44 +0100 Subject: [PATCH] model picker wip --- README.md | 2 + package.json | 1 + pnpm-lock.yaml | 54 ++++++++ src/routes/chat/model-picker.svelte | 202 +++++++++++++++++++++++++++- 4 files changed, 254 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 70bde96..6f1ce5b 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,14 @@ TODO: add instructions - [x] conversation title generation - [ ] kbd powered popover model picker - [x] autosize +- [x] AbortController for message generation ### Extra - [ ] Web Search - [ ] MCPs - [ ] Chat branching +- [ ] Regenerate message - [ ] Image generation - [ ] Chat sharing - [ ] 404 page/redirect diff --git a/package.json b/package.json index 76d712f..35a9c56 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@fontsource-variable/nunito-sans": "^5.2.6", "@fontsource-variable/open-sans": "^5.2.6", "better-auth": "^1.2.9", + "bits-ui": "^2.8.0", "convex-helpers": "^0.1.94", "markdown-it-async": "^2.2.0", "openai": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c75d23..29a7344 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: better-auth: specifier: ^1.2.9 version: 1.2.9 + bits-ui: + specifier: ^2.8.0 + version: 2.8.0(@internationalized/date@3.8.2)(svelte@5.34.1) convex-helpers: specifier: ^0.1.94 version: 0.1.94(convex@1.24.8)(typescript@5.8.3)(zod@3.25.64) @@ -662,6 +665,9 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + '@internationalized/date@3.8.2': + resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -906,6 +912,9 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tailwindcss/node@4.1.10': resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==} @@ -1223,6 +1232,13 @@ packages: better-call@1.0.9: resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} + bits-ui@2.8.0: + resolution: {integrity: sha512-WiTZcCbYLm4Cx6/67NqXVSD0BkfNmdX8Abs84HpIaplX/wRRbg8tkMtJYlLw7mepgGvwGR3enLi6tFkcHU3JXA==} + engines: {node: '>=20', pnpm: '>=8.7.0'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2381,6 +2397,12 @@ packages: peerDependencies: svelte: ^5.0.0 + svelte-toolbelt@0.9.2: + resolution: {integrity: sha512-REb1cENGnFbhNSmIdCb1SDIpjEa3n1kXhNVHqGNEesjmPX3bG87gUZiCG8cqOt9AAarqzTzOtI2jEEWr/ZbHwA==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + svelte@5.34.1: resolution: {integrity: sha512-jWNnN2hZFNtnzKPptCcJHBWrD9CtbHPDwIRIODufOYaWkR0kLmAIlM384lMt4ucwuIRX4hCJwD2D8ZtEcGJQ0Q==} engines: {node: '>=18'} @@ -2388,6 +2410,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwind-merge@3.0.2: resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} @@ -3061,6 +3086,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@internationalized/date@3.8.2': + dependencies: + '@swc/helpers': 0.5.17 + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -3306,6 +3335,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -3651,6 +3684,18 @@ snapshots: set-cookie-parser: 2.7.1 uncrypto: 0.1.3 + bits-ui@2.8.0(@internationalized/date@3.8.2)(svelte@5.34.1): + dependencies: + '@floating-ui/core': 1.7.1 + '@floating-ui/dom': 1.7.1 + '@internationalized/date': 3.8.2 + css.escape: 1.5.1 + esm-env: 1.2.2 + runed: 0.28.0(svelte@5.34.1) + svelte: 5.34.1 + svelte-toolbelt: 0.9.2(svelte@5.34.1) + tabbable: 6.2.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4744,6 +4789,13 @@ snapshots: style-to-object: 1.0.9 svelte: 5.34.1 + svelte-toolbelt@0.9.2(svelte@5.34.1): + dependencies: + clsx: 2.1.1 + runed: 0.28.0(svelte@5.34.1) + style-to-object: 1.0.9 + svelte: 5.34.1 + svelte@5.34.1: dependencies: '@ampproject/remapping': 2.3.0 @@ -4763,6 +4815,8 @@ snapshots: symbol-tree@3.2.4: {} + tabbable@6.2.0: {} + tailwind-merge@3.0.2: {} tailwind-merge@3.3.1: {} diff --git a/src/routes/chat/model-picker.svelte b/src/routes/chat/model-picker.svelte index bf5f97b..c18e340 100644 --- a/src/routes/chat/model-picker.svelte +++ b/src/routes/chat/model-picker.svelte @@ -4,6 +4,23 @@ import { session } from '$lib/state/session.svelte'; import { settings } from '$lib/state/settings.svelte'; import { cn } from '$lib/utils/utils'; + import { Command, Popover } from 'bits-ui'; + import { Button } from '$lib/components/ui/button'; + import ChevronDownIcon from '~icons/lucide/chevron-down'; + import CheckIcon from '~icons/lucide/check'; + + // Company icons from simple-icons + import OpenaiIcon from '~icons/simple-icons/openai'; + import GoogleIcon from '~icons/simple-icons/google'; + import MetaIcon from '~icons/simple-icons/meta'; + import MicrosoftIcon from '~icons/simple-icons/microsoft'; + import XIcon from '~icons/simple-icons/x'; + + // Fallback to lucide icons for companies without simple-icons + import BrainIcon from '~icons/lucide/brain'; + import ZapIcon from '~icons/lucide/zap'; + import CpuIcon from '~icons/lucide/cpu'; + import RobotIcon from '~icons/lucide/bot'; type Props = { class?: string; @@ -17,15 +34,190 @@ const enabledArr = $derived(Object.values(enabledModelsQuery.data ?? {})); + // Company icon mapping + const companyIcons: Record = { + openai: OpenaiIcon, + anthropic: BrainIcon, + google: GoogleIcon, + meta: MetaIcon, + mistral: ZapIcon, + 'x-ai': XIcon, + microsoft: MicrosoftIcon, + qwen: CpuIcon, + deepseek: RobotIcon, + cohere: CpuIcon, + }; + + // Function to extract company from model ID + function getCompanyFromModelId(modelId: string): string { + const id = modelId.toLowerCase(); + + // OpenAI models + if (id.includes('gpt') || id.includes('o1') || id.includes('openai')) return 'openai'; + + // Anthropic models + if (id.includes('claude') || id.includes('anthropic')) return 'anthropic'; + + // Google models + if ( + id.includes('gemini') || + id.includes('gemma') || + id.includes('google') || + id.includes('palm') + ) + return 'google'; + + // Meta models + if (id.includes('llama') || id.includes('meta')) return 'meta'; + + // Mistral models + if (id.includes('mistral') || id.includes('mixtral')) return 'mistral'; + + // xAI models + if (id.includes('grok') || id.includes('x-ai')) return 'x-ai'; + + // Microsoft models + if (id.includes('phi') || id.includes('microsoft')) return 'microsoft'; + + // Qwen models + if (id.includes('qwen') || id.includes('alibaba')) return 'qwen'; + + // DeepSeek models + if (id.includes('deepseek')) return 'deepseek'; + + // Cohere models + if (id.includes('command') || id.includes('cohere')) return 'cohere'; + + // Try to extract from model path (e.g., "anthropic/claude-3") + const pathParts = modelId.split('/'); + if (pathParts.length > 1) { + const provider = pathParts[0]?.toLowerCase(); + if (provider && companyIcons[provider]) return provider; + } + + return 'other'; + } + + // Group models by company + const groupedModels = $derived.by(() => { + console.log('📊 enabledArr:', enabledArr); + const groups: Record = {}; + + enabledArr.forEach((model) => { + const company = getCompanyFromModelId(model.model_id); + console.log(`🏢 Model ${model.model_id} -> Company: ${company}`); + if (!groups[company]) { + groups[company] = []; + } + groups[company].push(model); + }); + + console.log('📋 Groups:', groups); + + // Sort companies with known icons first + const result = Object.entries(groups).sort(([a], [b]) => { + const aHasIcon = companyIcons[a] ? 0 : 1; + const bHasIcon = companyIcons[b] ? 0 : 1; + return aHasIcon - bHasIcon || a.localeCompare(b); + }); + + console.log('🎯 Final grouped models:', result); + return result; + }); + + let open = $state(false); + + // Find current model details + const currentModel = $derived(enabledArr.find((m) => m.model_id === settings.modelId)); + const currentCompany = $derived( + currentModel ? getCompanyFromModelId(currentModel.model_id) : 'other' + ); + $effect(() => { if (!enabledArr.find((m) => m.model_id === settings.modelId) && enabledArr.length > 0) { settings.modelId = enabledArr[0]!.model_id; } }); + + function selectModel(modelId: string) { + settings.modelId = modelId; + open = false; + } - +{#if enabledArr.length === 0} + + +{:else} + + +
+ {#if companyIcons[currentCompany]} + {@const IconComponent = companyIcons[currentCompany]} + + {/if} + + {currentModel?.model_id.replace(/^[^/]+\//, '') || 'Select model'} + +
+ +
+ + + + + + + + No models available. Enable some models in the account settings. + + {#each groupedModels as [company, models]} + + + {#if companyIcons[company]} + {@const IconComponent = companyIcons[company]} + + {/if} + {company} ({models.length}) + + + {#each models as model (model._id)} + selectModel(model.model_id)} + class={cn( + 'data-selected:bg-accent data-selected:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none transition-colors', + settings.modelId === model.model_id && 'bg-primary text-primary-foreground' + )} + > +
+ + {model.model_id.replace(/^[^/]+\//, '')} + + {#if settings.modelId === model.model_id} + + {/if} +
+
+ {/each} +
+
+ {/each} +
+
+
+
+
+{/if}