search functionality
This commit is contained in:
parent
d5dbab44fa
commit
1aaae9b8f8
11 changed files with 326 additions and 275 deletions
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"functions": "src/lib/backend/convex"
|
||||
"functions": "src/lib/backend/convex"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9754 0.0084 325.6414);
|
||||
|
|
|
|||
1
src/lib/cache/cached-query.svelte.ts
vendored
1
src/lib/cache/cached-query.svelte.ts
vendored
|
|
@ -93,4 +93,3 @@ export function clearQueryCache(): void {
|
|||
}
|
||||
|
||||
export { globalCache as queryCache };
|
||||
|
||||
|
|
|
|||
214
src/lib/cache/lru-cache.ts
vendored
214
src/lib/cache/lru-cache.ts
vendored
|
|
@ -1,136 +1,136 @@
|
|||
interface CacheNode<K, V> {
|
||||
key: K;
|
||||
value: V;
|
||||
size: number;
|
||||
prev: CacheNode<K, V> | null;
|
||||
next: CacheNode<K, V> | null;
|
||||
key: K;
|
||||
value: V;
|
||||
size: number;
|
||||
prev: CacheNode<K, V> | null;
|
||||
next: CacheNode<K, V> | null;
|
||||
}
|
||||
|
||||
export class LRUCache<K = string, V = unknown> {
|
||||
private capacity: number;
|
||||
private currentSize = 0;
|
||||
private cache = new Map<K, CacheNode<K, V>>();
|
||||
private head: CacheNode<K, V> | null = null;
|
||||
private tail: CacheNode<K, V> | null = null;
|
||||
private capacity: number;
|
||||
private currentSize = 0;
|
||||
private cache = new Map<K, CacheNode<K, V>>();
|
||||
private head: CacheNode<K, V> | null = null;
|
||||
private tail: CacheNode<K, V> | null = null;
|
||||
|
||||
constructor(maxSizeBytes = 1024 * 1024) {
|
||||
this.capacity = maxSizeBytes;
|
||||
}
|
||||
constructor(maxSizeBytes = 1024 * 1024) {
|
||||
this.capacity = maxSizeBytes;
|
||||
}
|
||||
|
||||
private calculateSize(value: V): number {
|
||||
try {
|
||||
return new Blob([JSON.stringify(value)]).size;
|
||||
} catch {
|
||||
return JSON.stringify(value).length * 2;
|
||||
}
|
||||
}
|
||||
private calculateSize(value: V): number {
|
||||
try {
|
||||
return new Blob([JSON.stringify(value)]).size;
|
||||
} catch {
|
||||
return JSON.stringify(value).length * 2;
|
||||
}
|
||||
}
|
||||
|
||||
private removeNode(node: CacheNode<K, V>): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
} else {
|
||||
this.head = node.next;
|
||||
}
|
||||
private removeNode(node: CacheNode<K, V>): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
} else {
|
||||
this.head = node.next;
|
||||
}
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = node.prev;
|
||||
} else {
|
||||
this.tail = node.prev;
|
||||
}
|
||||
}
|
||||
if (node.next) {
|
||||
node.next.prev = node.prev;
|
||||
} else {
|
||||
this.tail = node.prev;
|
||||
}
|
||||
}
|
||||
|
||||
private addToHead(node: CacheNode<K, V>): void {
|
||||
node.prev = null;
|
||||
node.next = this.head;
|
||||
private addToHead(node: CacheNode<K, V>): void {
|
||||
node.prev = null;
|
||||
node.next = this.head;
|
||||
|
||||
if (this.head) {
|
||||
this.head.prev = node;
|
||||
}
|
||||
if (this.head) {
|
||||
this.head.prev = node;
|
||||
}
|
||||
|
||||
this.head = node;
|
||||
this.head = node;
|
||||
|
||||
if (!this.tail) {
|
||||
this.tail = node;
|
||||
}
|
||||
}
|
||||
if (!this.tail) {
|
||||
this.tail = node;
|
||||
}
|
||||
}
|
||||
|
||||
private evictLRU(): void {
|
||||
while (this.tail && this.currentSize > this.capacity) {
|
||||
const lastNode = this.tail;
|
||||
this.removeNode(lastNode);
|
||||
this.cache.delete(lastNode.key);
|
||||
this.currentSize -= lastNode.size;
|
||||
}
|
||||
}
|
||||
private evictLRU(): void {
|
||||
while (this.tail && this.currentSize > this.capacity) {
|
||||
const lastNode = this.tail;
|
||||
this.removeNode(lastNode);
|
||||
this.cache.delete(lastNode.key);
|
||||
this.currentSize -= lastNode.size;
|
||||
}
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
const node = this.cache.get(key);
|
||||
if (!node) return undefined;
|
||||
get(key: K): V | undefined {
|
||||
const node = this.cache.get(key);
|
||||
if (!node) return undefined;
|
||||
|
||||
this.removeNode(node);
|
||||
this.addToHead(node);
|
||||
this.removeNode(node);
|
||||
this.addToHead(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
const size = this.calculateSize(value);
|
||||
set(key: K, value: V): void {
|
||||
const size = this.calculateSize(value);
|
||||
|
||||
if (size > this.capacity) {
|
||||
return;
|
||||
}
|
||||
if (size > this.capacity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingNode = this.cache.get(key);
|
||||
const existingNode = this.cache.get(key);
|
||||
|
||||
if (existingNode) {
|
||||
existingNode.value = value;
|
||||
this.currentSize = this.currentSize - existingNode.size + size;
|
||||
existingNode.size = size;
|
||||
this.removeNode(existingNode);
|
||||
this.addToHead(existingNode);
|
||||
} else {
|
||||
const newNode: CacheNode<K, V> = {
|
||||
key,
|
||||
value,
|
||||
size,
|
||||
prev: null,
|
||||
next: null,
|
||||
};
|
||||
if (existingNode) {
|
||||
existingNode.value = value;
|
||||
this.currentSize = this.currentSize - existingNode.size + size;
|
||||
existingNode.size = size;
|
||||
this.removeNode(existingNode);
|
||||
this.addToHead(existingNode);
|
||||
} else {
|
||||
const newNode: CacheNode<K, V> = {
|
||||
key,
|
||||
value,
|
||||
size,
|
||||
prev: null,
|
||||
next: null,
|
||||
};
|
||||
|
||||
this.currentSize += size;
|
||||
this.cache.set(key, newNode);
|
||||
this.addToHead(newNode);
|
||||
}
|
||||
this.currentSize += size;
|
||||
this.cache.set(key, newNode);
|
||||
this.addToHead(newNode);
|
||||
}
|
||||
|
||||
this.evictLRU();
|
||||
}
|
||||
this.evictLRU();
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const node = this.cache.get(key);
|
||||
if (!node) return false;
|
||||
delete(key: K): boolean {
|
||||
const node = this.cache.get(key);
|
||||
if (!node) return false;
|
||||
|
||||
this.removeNode(node);
|
||||
this.cache.delete(key);
|
||||
this.currentSize -= node.size;
|
||||
return true;
|
||||
}
|
||||
this.removeNode(node);
|
||||
this.cache.delete(key);
|
||||
this.currentSize -= node.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this.currentSize = 0;
|
||||
}
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this.currentSize = 0;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
get bytes(): number {
|
||||
return this.currentSize;
|
||||
}
|
||||
get bytes(): number {
|
||||
return this.currentSize;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
has(key: K): boolean {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
}
|
||||
256
src/lib/cache/session-cache.ts
vendored
256
src/lib/cache/session-cache.ts
vendored
|
|
@ -1,160 +1,156 @@
|
|||
import { LRUCache } from './lru-cache.js';
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl?: number;
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
export class SessionStorageCache<T = unknown> {
|
||||
private memoryCache: LRUCache<string, CacheEntry<T>>;
|
||||
private storageKey: string;
|
||||
private writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private debounceMs: number;
|
||||
private pendingWrites = new Set<string>();
|
||||
private memoryCache: LRUCache<string, CacheEntry<T>>;
|
||||
private storageKey: string;
|
||||
private writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private debounceMs: number;
|
||||
private pendingWrites = new Set<string>();
|
||||
|
||||
constructor(
|
||||
storageKey = 'query-cache',
|
||||
maxSizeBytes = 1024 * 1024,
|
||||
debounceMs = 300
|
||||
) {
|
||||
this.storageKey = storageKey;
|
||||
this.debounceMs = debounceMs;
|
||||
this.memoryCache = new LRUCache<string, CacheEntry<T>>(maxSizeBytes);
|
||||
this.loadFromSessionStorage();
|
||||
}
|
||||
constructor(storageKey = 'query-cache', maxSizeBytes = 1024 * 1024, debounceMs = 300) {
|
||||
this.storageKey = storageKey;
|
||||
this.debounceMs = debounceMs;
|
||||
this.memoryCache = new LRUCache<string, CacheEntry<T>>(maxSizeBytes);
|
||||
this.loadFromSessionStorage();
|
||||
}
|
||||
|
||||
private loadFromSessionStorage(): void {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(this.storageKey);
|
||||
if (!stored) return;
|
||||
private loadFromSessionStorage(): void {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(this.storageKey);
|
||||
if (!stored) return;
|
||||
|
||||
const data = JSON.parse(stored) as Record<string, CacheEntry<T>>;
|
||||
const now = Date.now();
|
||||
const data = JSON.parse(stored) as Record<string, CacheEntry<T>>;
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, entry] of Object.entries(data)) {
|
||||
if (entry.ttl && now - entry.timestamp > entry.ttl) {
|
||||
continue;
|
||||
}
|
||||
this.memoryCache.set(key, entry);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load cache from sessionStorage:', error);
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of Object.entries(data)) {
|
||||
if (entry.ttl && now - entry.timestamp > entry.ttl) {
|
||||
continue;
|
||||
}
|
||||
this.memoryCache.set(key, entry);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load cache from sessionStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private debouncedWrite(): void {
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
}
|
||||
private debouncedWrite(): void {
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
}
|
||||
|
||||
this.writeTimeout = setTimeout(() => {
|
||||
this.writeToSessionStorage();
|
||||
this.writeTimeout = null;
|
||||
}, this.debounceMs);
|
||||
}
|
||||
this.writeTimeout = setTimeout(() => {
|
||||
this.writeToSessionStorage();
|
||||
this.writeTimeout = null;
|
||||
}, this.debounceMs);
|
||||
}
|
||||
|
||||
private writeToSessionStorage(): void {
|
||||
try {
|
||||
const cacheData: Record<string, CacheEntry<T>> = {};
|
||||
const now = Date.now();
|
||||
private writeToSessionStorage(): void {
|
||||
try {
|
||||
const cacheData: Record<string, CacheEntry<T>> = {};
|
||||
const now = Date.now();
|
||||
|
||||
for (const key of this.pendingWrites) {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||
cacheData[key] = entry;
|
||||
}
|
||||
}
|
||||
for (const key of this.pendingWrites) {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||
cacheData[key] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
const existingData = sessionStorage.getItem(this.storageKey);
|
||||
if (existingData) {
|
||||
const existing = JSON.parse(existingData) as Record<string, CacheEntry<T>>;
|
||||
for (const [key, entry] of Object.entries(existing)) {
|
||||
if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||
cacheData[key] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
const existingData = sessionStorage.getItem(this.storageKey);
|
||||
if (existingData) {
|
||||
const existing = JSON.parse(existingData) as Record<string, CacheEntry<T>>;
|
||||
for (const [key, entry] of Object.entries(existing)) {
|
||||
if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||
cacheData[key] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData));
|
||||
this.pendingWrites.clear();
|
||||
} catch (error) {
|
||||
console.warn('Failed to write cache to sessionStorage:', error);
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData));
|
||||
this.pendingWrites.clear();
|
||||
} catch (error) {
|
||||
console.warn('Failed to write cache to sessionStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
get(key: string): T | undefined {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return undefined;
|
||||
get(key: string): T | undefined {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
set(key: string, data: T, ttlMs?: number): void {
|
||||
const entry: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttlMs,
|
||||
};
|
||||
set(key: string, data: T, ttlMs?: number): void {
|
||||
const entry: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttlMs,
|
||||
};
|
||||
|
||||
this.memoryCache.set(key, entry);
|
||||
this.pendingWrites.add(key);
|
||||
this.debouncedWrite();
|
||||
}
|
||||
this.memoryCache.set(key, entry);
|
||||
this.pendingWrites.add(key);
|
||||
this.debouncedWrite();
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
const deleted = this.memoryCache.delete(key);
|
||||
if (deleted) {
|
||||
this.pendingWrites.add(key);
|
||||
this.debouncedWrite();
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
delete(key: string): boolean {
|
||||
const deleted = this.memoryCache.delete(key);
|
||||
if (deleted) {
|
||||
this.pendingWrites.add(key);
|
||||
this.debouncedWrite();
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
sessionStorage.removeItem(this.storageKey);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear sessionStorage:', error);
|
||||
}
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
this.writeTimeout = null;
|
||||
}
|
||||
this.pendingWrites.clear();
|
||||
}
|
||||
clear(): void {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
sessionStorage.removeItem(this.storageKey);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear sessionStorage:', error);
|
||||
}
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
this.writeTimeout = null;
|
||||
}
|
||||
this.pendingWrites.clear();
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return false;
|
||||
has(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||
this.delete(key);
|
||||
return false;
|
||||
}
|
||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||
this.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.memoryCache.size;
|
||||
}
|
||||
get size(): number {
|
||||
return this.memoryCache.size;
|
||||
}
|
||||
|
||||
get bytes(): number {
|
||||
return this.memoryCache.bytes;
|
||||
}
|
||||
get bytes(): number {
|
||||
return this.memoryCache.bytes;
|
||||
}
|
||||
|
||||
forceWrite(): void {
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
this.writeTimeout = null;
|
||||
}
|
||||
this.writeToSessionStorage();
|
||||
}
|
||||
forceWrite(): void {
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
this.writeTimeout = null;
|
||||
}
|
||||
this.writeToSessionStorage();
|
||||
}
|
||||
}
|
||||
1
src/lib/components/ui/search/index.ts
Normal file
1
src/lib/components/ui/search/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Search } from './search.svelte';
|
||||
13
src/lib/components/ui/search/search.svelte
Normal file
13
src/lib/components/ui/search/search.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import Search from '~icons/lucide/search';
|
||||
|
||||
let { value = $bindable(''), ...rest }: HTMLInputAttributes = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="border-input focus-within:ring-ring ring-offset-background relative flex h-9 items-center rounded-md border p-2 text-base ring-offset-2 focus-within:ring-2 md:text-sm"
|
||||
>
|
||||
<Search class="text-muted-foreground size-4" />
|
||||
<input {...rest} bind:value type="text" class="flex-1 bg-transparent px-2 outline-none" />
|
||||
</div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { redirect } from "@sveltejs/kit";
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load() {
|
||||
// temporary redirect to /chat
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
import { api } from '$lib/backend/convex/_generated/api';
|
||||
import { useCachedQuery } from '$lib/cache/cached-query.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Search } from '$lib/components/ui/search';
|
||||
import { session } from '$lib/state/session.svelte';
|
||||
import { Provider } from '$lib/types.js';
|
||||
import { cn } from '$lib/utils/utils';
|
||||
import ModelCard from './model-card.svelte';
|
||||
import { Toggle } from 'melt/builders';
|
||||
import XIcon from '~icons/lucide/x';
|
||||
import PlusIcon from '~icons/lucide/plus';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
|
|
@ -21,6 +25,23 @@
|
|||
const hasOpenRouterKey = $derived(
|
||||
openRouterKeyQuery.data !== undefined && openRouterKeyQuery.data !== ''
|
||||
);
|
||||
|
||||
let search = $state('');
|
||||
|
||||
const openRouterModels = $derived(
|
||||
data.openRouterModels.filter((model) => {
|
||||
if (search !== '' && !hasOpenRouterKey) return false;
|
||||
if (!openRouterToggle.value) return false;
|
||||
|
||||
return model.name.toLowerCase().includes(search.toLowerCase());
|
||||
})
|
||||
);
|
||||
|
||||
const openRouterToggle = new Toggle({
|
||||
value: true,
|
||||
// TODO: enable this if and when when we use multiple providers
|
||||
disabled: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -32,28 +53,51 @@
|
|||
Choose which models appear in your model selector. This won't affect existing conversations.
|
||||
</h2>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">OpenRouter</h3>
|
||||
<p class="text-muted-foreground text-sm">Easy access to over 400 models.</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
class={cn('flex flex-col gap-4 overflow-hidden', {
|
||||
'pointer-events-none max-h-96 mask-b-from-0% mask-b-to-80%': !hasOpenRouterKey,
|
||||
})}
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<Search bind:value={search} placeholder="Search models" />
|
||||
<div class="flex place-items-center gap-2">
|
||||
<button
|
||||
{...openRouterToggle.trigger}
|
||||
aria-label="OpenRouter"
|
||||
class="group text-primary-foreground bg-primary aria-[pressed=false]:border-border border-primary aria-[pressed=false]:bg-background flex place-items-center gap-1 rounded-full border px-2 py-1 text-xs transition-all disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each data.openRouterModels as model (model.id)}
|
||||
{@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined}
|
||||
<ModelCard provider={Provider.OpenRouter} {model} {enabled} disabled={!hasOpenRouterKey} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if !hasOpenRouterKey}
|
||||
<div
|
||||
class="absolute bottom-10 left-0 z-10 flex w-full place-items-center justify-center gap-2"
|
||||
>
|
||||
<Button href="/account/api-keys#openrouter" class="w-fit">Add API Key</Button>
|
||||
</div>
|
||||
{/if}
|
||||
OpenRouter
|
||||
<XIcon class="inline size-3 group-aria-[pressed=false]:hidden" />
|
||||
<PlusIcon class="inline size-3 group-aria-[pressed=true]:hidden" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if openRouterModels.length > 0}
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">OpenRouter</h3>
|
||||
<p class="text-muted-foreground text-sm">Easy access to over 400 models.</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
class={cn('flex flex-col gap-4 overflow-hidden', {
|
||||
'pointer-events-none max-h-96 mask-b-from-0% mask-b-to-80%': !hasOpenRouterKey,
|
||||
})}
|
||||
>
|
||||
{#each openRouterModels as model (model.id)}
|
||||
{@const enabled =
|
||||
enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined}
|
||||
<ModelCard
|
||||
provider={Provider.OpenRouter}
|
||||
{model}
|
||||
{enabled}
|
||||
disabled={!hasOpenRouterKey}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !hasOpenRouterKey}
|
||||
<div
|
||||
class="absolute bottom-10 left-0 z-10 flex w-full place-items-center justify-center gap-2"
|
||||
>
|
||||
<Button href="/account/api-keys#openrouter" class="w-fit">Add API Key</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@
|
|||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"unplugin-icons/types/svelte"
|
||||
]
|
||||
"types": ["unplugin-icons/types/svelte"]
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue