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';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.9754 0.0084 325.6414);
|
--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 };
|
export { globalCache as queryCache };
|
||||||
|
|
||||||
|
|
|
||||||
220
src/lib/cache/lru-cache.ts
vendored
220
src/lib/cache/lru-cache.ts
vendored
|
|
@ -1,136 +1,136 @@
|
||||||
interface CacheNode<K, V> {
|
interface CacheNode<K, V> {
|
||||||
key: K;
|
key: K;
|
||||||
value: V;
|
value: V;
|
||||||
size: number;
|
size: number;
|
||||||
prev: CacheNode<K, V> | null;
|
prev: CacheNode<K, V> | null;
|
||||||
next: CacheNode<K, V> | null;
|
next: CacheNode<K, V> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LRUCache<K = string, V = unknown> {
|
export class LRUCache<K = string, V = unknown> {
|
||||||
private capacity: number;
|
private capacity: number;
|
||||||
private currentSize = 0;
|
private currentSize = 0;
|
||||||
private cache = new Map<K, CacheNode<K, V>>();
|
private cache = new Map<K, CacheNode<K, V>>();
|
||||||
private head: CacheNode<K, V> | null = null;
|
private head: CacheNode<K, V> | null = null;
|
||||||
private tail: CacheNode<K, V> | null = null;
|
private tail: CacheNode<K, V> | null = null;
|
||||||
|
|
||||||
constructor(maxSizeBytes = 1024 * 1024) {
|
constructor(maxSizeBytes = 1024 * 1024) {
|
||||||
this.capacity = maxSizeBytes;
|
this.capacity = maxSizeBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateSize(value: V): number {
|
private calculateSize(value: V): number {
|
||||||
try {
|
try {
|
||||||
return new Blob([JSON.stringify(value)]).size;
|
return new Blob([JSON.stringify(value)]).size;
|
||||||
} catch {
|
} catch {
|
||||||
return JSON.stringify(value).length * 2;
|
return JSON.stringify(value).length * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeNode(node: CacheNode<K, V>): void {
|
private removeNode(node: CacheNode<K, V>): void {
|
||||||
if (node.prev) {
|
if (node.prev) {
|
||||||
node.prev.next = node.next;
|
node.prev.next = node.next;
|
||||||
} else {
|
} else {
|
||||||
this.head = node.next;
|
this.head = node.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.next) {
|
if (node.next) {
|
||||||
node.next.prev = node.prev;
|
node.next.prev = node.prev;
|
||||||
} else {
|
} else {
|
||||||
this.tail = node.prev;
|
this.tail = node.prev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToHead(node: CacheNode<K, V>): void {
|
private addToHead(node: CacheNode<K, V>): void {
|
||||||
node.prev = null;
|
node.prev = null;
|
||||||
node.next = this.head;
|
node.next = this.head;
|
||||||
|
|
||||||
if (this.head) {
|
if (this.head) {
|
||||||
this.head.prev = node;
|
this.head.prev = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.head = node;
|
this.head = node;
|
||||||
|
|
||||||
if (!this.tail) {
|
if (!this.tail) {
|
||||||
this.tail = node;
|
this.tail = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private evictLRU(): void {
|
private evictLRU(): void {
|
||||||
while (this.tail && this.currentSize > this.capacity) {
|
while (this.tail && this.currentSize > this.capacity) {
|
||||||
const lastNode = this.tail;
|
const lastNode = this.tail;
|
||||||
this.removeNode(lastNode);
|
this.removeNode(lastNode);
|
||||||
this.cache.delete(lastNode.key);
|
this.cache.delete(lastNode.key);
|
||||||
this.currentSize -= lastNode.size;
|
this.currentSize -= lastNode.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: K): V | undefined {
|
get(key: K): V | undefined {
|
||||||
const node = this.cache.get(key);
|
const node = this.cache.get(key);
|
||||||
if (!node) return undefined;
|
if (!node) return undefined;
|
||||||
|
|
||||||
this.removeNode(node);
|
this.removeNode(node);
|
||||||
this.addToHead(node);
|
this.addToHead(node);
|
||||||
|
|
||||||
return node.value;
|
return node.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: K, value: V): void {
|
set(key: K, value: V): void {
|
||||||
const size = this.calculateSize(value);
|
const size = this.calculateSize(value);
|
||||||
|
|
||||||
if (size > this.capacity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingNode = this.cache.get(key);
|
if (size > this.capacity) {
|
||||||
|
return;
|
||||||
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;
|
const existingNode = this.cache.get(key);
|
||||||
this.cache.set(key, newNode);
|
|
||||||
this.addToHead(newNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.evictLRU();
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
delete(key: K): boolean {
|
this.currentSize += size;
|
||||||
const node = this.cache.get(key);
|
this.cache.set(key, newNode);
|
||||||
if (!node) return false;
|
this.addToHead(newNode);
|
||||||
|
}
|
||||||
|
|
||||||
this.removeNode(node);
|
this.evictLRU();
|
||||||
this.cache.delete(key);
|
}
|
||||||
this.currentSize -= node.size;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
delete(key: K): boolean {
|
||||||
this.cache.clear();
|
const node = this.cache.get(key);
|
||||||
this.head = null;
|
if (!node) return false;
|
||||||
this.tail = null;
|
|
||||||
this.currentSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get size(): number {
|
this.removeNode(node);
|
||||||
return this.cache.size;
|
this.cache.delete(key);
|
||||||
}
|
this.currentSize -= node.size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
get bytes(): number {
|
clear(): void {
|
||||||
return this.currentSize;
|
this.cache.clear();
|
||||||
}
|
this.head = null;
|
||||||
|
this.tail = null;
|
||||||
|
this.currentSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
has(key: K): boolean {
|
get size(): number {
|
||||||
return this.cache.has(key);
|
return this.cache.size;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
get bytes(): number {
|
||||||
|
return this.currentSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.cache.has(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
262
src/lib/cache/session-cache.ts
vendored
262
src/lib/cache/session-cache.ts
vendored
|
|
@ -1,160 +1,156 @@
|
||||||
import { LRUCache } from './lru-cache.js';
|
import { LRUCache } from './lru-cache.js';
|
||||||
|
|
||||||
interface CacheEntry<T> {
|
interface CacheEntry<T> {
|
||||||
data: T;
|
data: T;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
ttl?: number;
|
ttl?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SessionStorageCache<T = unknown> {
|
export class SessionStorageCache<T = unknown> {
|
||||||
private memoryCache: LRUCache<string, CacheEntry<T>>;
|
private memoryCache: LRUCache<string, CacheEntry<T>>;
|
||||||
private storageKey: string;
|
private storageKey: string;
|
||||||
private writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
private writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
private debounceMs: number;
|
private debounceMs: number;
|
||||||
private pendingWrites = new Set<string>();
|
private pendingWrites = new Set<string>();
|
||||||
|
|
||||||
constructor(
|
constructor(storageKey = 'query-cache', maxSizeBytes = 1024 * 1024, debounceMs = 300) {
|
||||||
storageKey = 'query-cache',
|
this.storageKey = storageKey;
|
||||||
maxSizeBytes = 1024 * 1024,
|
this.debounceMs = debounceMs;
|
||||||
debounceMs = 300
|
this.memoryCache = new LRUCache<string, CacheEntry<T>>(maxSizeBytes);
|
||||||
) {
|
this.loadFromSessionStorage();
|
||||||
this.storageKey = storageKey;
|
}
|
||||||
this.debounceMs = debounceMs;
|
|
||||||
this.memoryCache = new LRUCache<string, CacheEntry<T>>(maxSizeBytes);
|
|
||||||
this.loadFromSessionStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadFromSessionStorage(): void {
|
private loadFromSessionStorage(): void {
|
||||||
try {
|
try {
|
||||||
const stored = sessionStorage.getItem(this.storageKey);
|
const stored = sessionStorage.getItem(this.storageKey);
|
||||||
if (!stored) return;
|
if (!stored) return;
|
||||||
|
|
||||||
const data = JSON.parse(stored) as Record<string, CacheEntry<T>>;
|
const data = JSON.parse(stored) as Record<string, CacheEntry<T>>;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const [key, entry] of Object.entries(data)) {
|
for (const [key, entry] of Object.entries(data)) {
|
||||||
if (entry.ttl && now - entry.timestamp > entry.ttl) {
|
if (entry.ttl && now - entry.timestamp > entry.ttl) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.memoryCache.set(key, entry);
|
this.memoryCache.set(key, entry);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load cache from sessionStorage:', error);
|
console.warn('Failed to load cache from sessionStorage:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private debouncedWrite(): void {
|
private debouncedWrite(): void {
|
||||||
if (this.writeTimeout) {
|
if (this.writeTimeout) {
|
||||||
clearTimeout(this.writeTimeout);
|
clearTimeout(this.writeTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.writeTimeout = setTimeout(() => {
|
this.writeTimeout = setTimeout(() => {
|
||||||
this.writeToSessionStorage();
|
this.writeToSessionStorage();
|
||||||
this.writeTimeout = null;
|
this.writeTimeout = null;
|
||||||
}, this.debounceMs);
|
}, this.debounceMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeToSessionStorage(): void {
|
private writeToSessionStorage(): void {
|
||||||
try {
|
try {
|
||||||
const cacheData: Record<string, CacheEntry<T>> = {};
|
const cacheData: Record<string, CacheEntry<T>> = {};
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const key of this.pendingWrites) {
|
for (const key of this.pendingWrites) {
|
||||||
const entry = this.memoryCache.get(key);
|
const entry = this.memoryCache.get(key);
|
||||||
if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||||
cacheData[key] = entry;
|
cacheData[key] = entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingData = sessionStorage.getItem(this.storageKey);
|
const existingData = sessionStorage.getItem(this.storageKey);
|
||||||
if (existingData) {
|
if (existingData) {
|
||||||
const existing = JSON.parse(existingData) as Record<string, CacheEntry<T>>;
|
const existing = JSON.parse(existingData) as Record<string, CacheEntry<T>>;
|
||||||
for (const [key, entry] of Object.entries(existing)) {
|
for (const [key, entry] of Object.entries(existing)) {
|
||||||
if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) {
|
||||||
cacheData[key] = entry;
|
cacheData[key] = entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData));
|
sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData));
|
||||||
this.pendingWrites.clear();
|
this.pendingWrites.clear();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to write cache to sessionStorage:', error);
|
console.warn('Failed to write cache to sessionStorage:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: string): T | undefined {
|
get(key: string): T | undefined {
|
||||||
const entry = this.memoryCache.get(key);
|
const entry = this.memoryCache.get(key);
|
||||||
if (!entry) return undefined;
|
if (!entry) return undefined;
|
||||||
|
|
||||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry.data;
|
return entry.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: string, data: T, ttlMs?: number): void {
|
set(key: string, data: T, ttlMs?: number): void {
|
||||||
const entry: CacheEntry<T> = {
|
const entry: CacheEntry<T> = {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
ttl: ttlMs,
|
ttl: ttlMs,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.memoryCache.set(key, entry);
|
this.memoryCache.set(key, entry);
|
||||||
this.pendingWrites.add(key);
|
this.pendingWrites.add(key);
|
||||||
this.debouncedWrite();
|
this.debouncedWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(key: string): boolean {
|
delete(key: string): boolean {
|
||||||
const deleted = this.memoryCache.delete(key);
|
const deleted = this.memoryCache.delete(key);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
this.pendingWrites.add(key);
|
this.pendingWrites.add(key);
|
||||||
this.debouncedWrite();
|
this.debouncedWrite();
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.memoryCache.clear();
|
this.memoryCache.clear();
|
||||||
try {
|
try {
|
||||||
sessionStorage.removeItem(this.storageKey);
|
sessionStorage.removeItem(this.storageKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to clear sessionStorage:', error);
|
console.warn('Failed to clear sessionStorage:', error);
|
||||||
}
|
}
|
||||||
if (this.writeTimeout) {
|
if (this.writeTimeout) {
|
||||||
clearTimeout(this.writeTimeout);
|
clearTimeout(this.writeTimeout);
|
||||||
this.writeTimeout = null;
|
this.writeTimeout = null;
|
||||||
}
|
}
|
||||||
this.pendingWrites.clear();
|
this.pendingWrites.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: string): boolean {
|
has(key: string): boolean {
|
||||||
const entry = this.memoryCache.get(key);
|
const entry = this.memoryCache.get(key);
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
|
|
||||||
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
|
||||||
this.delete(key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get size(): number {
|
if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) {
|
||||||
return this.memoryCache.size;
|
this.delete(key);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
get bytes(): number {
|
return true;
|
||||||
return this.memoryCache.bytes;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
forceWrite(): void {
|
get size(): number {
|
||||||
if (this.writeTimeout) {
|
return this.memoryCache.size;
|
||||||
clearTimeout(this.writeTimeout);
|
}
|
||||||
this.writeTimeout = null;
|
|
||||||
}
|
get bytes(): number {
|
||||||
this.writeToSessionStorage();
|
return this.memoryCache.bytes;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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,3 +1,3 @@
|
||||||
import Switch from './switch.svelte';
|
import Switch from './switch.svelte';
|
||||||
|
|
||||||
export { Switch };
|
export { Switch };
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
// temporary redirect to /chat
|
// temporary redirect to /chat
|
||||||
redirect(303, '/chat');
|
redirect(303, '/chat');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@
|
||||||
import { api } from '$lib/backend/convex/_generated/api';
|
import { api } from '$lib/backend/convex/_generated/api';
|
||||||
import { useCachedQuery } from '$lib/cache/cached-query.svelte';
|
import { useCachedQuery } from '$lib/cache/cached-query.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Search } from '$lib/components/ui/search';
|
||||||
import { session } from '$lib/state/session.svelte';
|
import { session } from '$lib/state/session.svelte';
|
||||||
import { Provider } from '$lib/types.js';
|
import { Provider } from '$lib/types.js';
|
||||||
import { cn } from '$lib/utils/utils';
|
import { cn } from '$lib/utils/utils';
|
||||||
import ModelCard from './model-card.svelte';
|
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();
|
let { data } = $props();
|
||||||
|
|
||||||
|
|
@ -21,6 +25,23 @@
|
||||||
const hasOpenRouterKey = $derived(
|
const hasOpenRouterKey = $derived(
|
||||||
openRouterKeyQuery.data !== undefined && openRouterKeyQuery.data !== ''
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -32,28 +53,51 @@
|
||||||
Choose which models appear in your model selector. This won't affect existing conversations.
|
Choose which models appear in your model selector. This won't affect existing conversations.
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="mt-8 flex flex-col gap-4">
|
<div class="mt-4 flex flex-col gap-2">
|
||||||
<div>
|
<Search bind:value={search} placeholder="Search models" />
|
||||||
<h3 class="text-lg font-bold">OpenRouter</h3>
|
<div class="flex place-items-center gap-2">
|
||||||
<p class="text-muted-foreground text-sm">Easy access to over 400 models.</p>
|
<button
|
||||||
</div>
|
{...openRouterToggle.trigger}
|
||||||
<div class="relative">
|
aria-label="OpenRouter"
|
||||||
<div
|
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"
|
||||||
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 data.openRouterModels as model (model.id)}
|
OpenRouter
|
||||||
{@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined}
|
<XIcon class="inline size-3 group-aria-[pressed=false]:hidden" />
|
||||||
<ModelCard provider={Provider.OpenRouter} {model} {enabled} disabled={!hasOpenRouterKey} />
|
<PlusIcon class="inline size-3 group-aria-[pressed=true]:hidden" />
|
||||||
{/each}
|
</button>
|
||||||
</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>
|
||||||
</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,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"types": [
|
"types": ["unplugin-icons/types/svelte"]
|
||||||
"unplugin-icons/types/svelte"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
// 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
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue