Performance enhancements galore! New themining system
This is once again a huge commit, but its mostly performance improvements along with some bug fixes and refactoring. It also includes changes to the theming systems. I'm still not 100% happy with the theming system, but its better than before. Model fetching has been dramatically improved! Nearly all the important computation and pre-processing has been moved to the server. This has also somehow fixed the way model details are loaded, which was causing many models to be missing their details despite models.dev having them. The markdown renderer has once again been changed, but I'm mostly certain that this is the last time major changes will be made to it. The renderer is not spamming components, bloating memory usage, and its not using a bug prone custom written chunking system. There's also a lot more that I haven't mentioned and honestly forgot. I need to get better commit hygiene tbh.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { sortByReleaseDate } from '~/utils/sort';
|
||||
import { encryptData, decrypt, uint8ArrayToBase64, base64ToUint8Array } from '~/utils/crypto';
|
||||
import { providerBaseUrls, SupportedModalities, type Model } from '~/types/model';
|
||||
import { providerBaseUrls, type Model } from '~/types/model';
|
||||
import { useSettings } from '~/composables/useSettings';
|
||||
import ModelItem from './ModelItem.vue';
|
||||
// @ts-ignore
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { pageParams } = useSettings();
|
||||
@@ -96,29 +96,17 @@ const updateProxyUrl = async (value: string) => {
|
||||
const fetchingModels = ref(false);
|
||||
|
||||
const fetchModels = async () => {
|
||||
const { user } = useAuth();
|
||||
const { user } = useAuth()
|
||||
|
||||
fetchingModels.value = true;
|
||||
|
||||
try {
|
||||
const [providerResponse, devDataResponse] = await Promise.all([
|
||||
$fetch(`/api/provider/${provider.value!.id}/models`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
providerApiKey: apiKey.value
|
||||
})
|
||||
}) as any,
|
||||
$fetch('https://models.dev/api.json') as any
|
||||
]);
|
||||
|
||||
// TODO: get model details correctly for ollama-cloud models
|
||||
let providerType = provider.value!.type as string;
|
||||
if (providerType === 'ollama') {
|
||||
providerType = 'ollama-cloud';
|
||||
}
|
||||
|
||||
const modelDetails = devDataResponse[providerType]?.models || {};
|
||||
console.log(modelDetails);
|
||||
const modelsData = await $fetch(`/api/provider/${provider.value!.id}/models`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
providerApiKey: apiKey.value
|
||||
})
|
||||
}) as any;
|
||||
|
||||
const existingModelsMap = new Map(
|
||||
(provider.value?.models || []).map((m: any) => [m.externalId, m])
|
||||
@@ -127,103 +115,34 @@ const fetchModels = async () => {
|
||||
const toInsert: any[] = [];
|
||||
const toUpdate: { id: string, data: any }[] = [];
|
||||
|
||||
providerResponse.models.forEach((pModel: any) => {
|
||||
let slug: string = pModel.id.toLowerCase();
|
||||
if (providerType === 'ollama-cloud') {
|
||||
slug = slug.replace(/:cloud$/, '');
|
||||
slug = slug.replace(/-cloud$/, '');
|
||||
slug = slug.replace(/:latest$/, '');
|
||||
}
|
||||
|
||||
let info = modelDetails[slug] || {};
|
||||
|
||||
const capabilities = [];
|
||||
|
||||
if (info.reasoning) {
|
||||
capabilities.push('reasoning');
|
||||
}
|
||||
|
||||
if (info.tool_call) {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
|
||||
let inputModalities = info.modalities?.input.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
if (inputModalities === undefined || inputModalities.length === 0) {
|
||||
inputModalities = ['text'];
|
||||
}
|
||||
|
||||
let outputModalities = info.modalities?.output.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
if (outputModalities === undefined || outputModalities.length === 0) {
|
||||
outputModalities = ['text'];
|
||||
}
|
||||
|
||||
// merge pModel.attributes and info.modalities, with a preference for pModel.attributes
|
||||
const attributes = {
|
||||
inputModalities: new Set(inputModalities),
|
||||
outputModalities: new Set(outputModalities),
|
||||
capabilities,
|
||||
contextWindow: pModel.context_length || info.limit?.context || null,
|
||||
supported_parameters: new Set(pModel.supported_parameters || ["temperature", "max_tokens"]),
|
||||
...(pModel.attributes || {}),
|
||||
};
|
||||
|
||||
let cost;
|
||||
if (pModel.pricing === undefined) {
|
||||
cost = {}
|
||||
} else {
|
||||
cost = {
|
||||
prompt: pModel.pricing.prompt || null,
|
||||
completion: pModel.pricing.completion || null,
|
||||
request: pModel.pricing.request || null,
|
||||
image: pModel.pricing.image || null,
|
||||
imageTokens: pModel.pricing.image_tokens || null,
|
||||
imageOutput: pModel.pricing.image_output || null,
|
||||
audio: pModel.pricing.audio || null,
|
||||
audioOutput: pModel.pricing.audio_output || null,
|
||||
inputAudioCache: pModel.pricing.input_audio_cache || null,
|
||||
webSearch: pModel.pricing.web_search || null,
|
||||
internalReasoning: pModel.pricing.internal_reasoning || null,
|
||||
inputCacheRead: pModel.pricing.input_cache_read || null,
|
||||
inputCacheWrite: pModel.pricing.input_cache_write || null,
|
||||
discount: pModel.pricing.discount || null,
|
||||
}
|
||||
}
|
||||
|
||||
const existing = existingModelsMap.get(pModel.id);
|
||||
for (const model of modelsData.models) {
|
||||
const existing = existingModelsMap.get(model.id);
|
||||
|
||||
if (existing) {
|
||||
// UPDATE logic: Remove 'id' from the payload as per Triplit requirements
|
||||
const { id, ...existingWithoutId } = existing;
|
||||
const { id, ...existingWithoutId } = model;
|
||||
console.log("existingWithoutId", existingWithoutId);
|
||||
|
||||
toUpdate.push({
|
||||
id: existing.id,
|
||||
data: {
|
||||
...existingWithoutId,
|
||||
name: existing.name || pModel.name || info.name || pModel.id,
|
||||
cost,
|
||||
attributes, // Update tech specs
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
}
|
||||
data: existingWithoutId,
|
||||
});
|
||||
} else {
|
||||
// INSERT logic: This is a brand new model
|
||||
toInsert.push({
|
||||
userId: user.value?.id,
|
||||
providerId: provider.value!.id,
|
||||
externalId: pModel.id,
|
||||
name: pModel.name || info.name || pModel.id,
|
||||
userId: user.value?.id!,
|
||||
externalId: model.id,
|
||||
providerId: provider.value!.id!,
|
||||
name: model.name || model.id,
|
||||
cost: model.cost || {},
|
||||
attributes: model.attributes,
|
||||
isCustom: false,
|
||||
enabled: false,
|
||||
cost,
|
||||
attributes,
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
createdAt: new Date(),
|
||||
releasedAt: model.releasedAt,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// delete models that are not in the API response and are not custom models
|
||||
const apiModelIds = new Set(providerResponse.models.map((p: any) => p.id));
|
||||
const apiModelIds = new Set(existingModelsMap.values().map((m: any) => m.externalId));
|
||||
const toDelete = (provider.value?.models || []).filter((m: any) =>
|
||||
!m.isCustom && !apiModelIds.has(m.externalId)
|
||||
);
|
||||
@@ -250,12 +169,12 @@ const deleteModels = async () => {
|
||||
|
||||
const enabledModels = computed(() =>
|
||||
filterModels(provider.value?.models.filter(m => m.enabled === true) as Model[] || [], modelSearch.value)
|
||||
.sort((a, b) => a.releasedAt && b.releasedAt ? b.releasedAt.getTime() - a.releasedAt.getTime() : 0)
|
||||
.sort(sortByReleaseDate)
|
||||
)
|
||||
|
||||
const disabledModels = computed(() =>
|
||||
filterModels(provider.value?.models.filter(m => m.enabled === false) as Model[] || [], modelSearch.value)
|
||||
.sort((a, b) => a.releasedAt && b.releasedAt ? b.releasedAt.getTime() - a.releasedAt.getTime() : 0)
|
||||
.sort(sortByReleaseDate)
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -274,13 +193,13 @@ defineEmits(['navigate']);
|
||||
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
<label class="whitespace-nowrap" for="provider-api-key">API Key</label>
|
||||
<div
|
||||
class="text-sm font-mono flex flex-row rounded-md bg-[var(--color-highlight)] items-center gap-1 w-7/10">
|
||||
<input class="w-full p-0 pl-2 py-1 bg-transparent" :type="apiKeyVisible ? 'text' : 'password'"
|
||||
id="provider-api-key" :value="apiKey" autocomplete="false" spellcheck="false"
|
||||
<div class="text-sm font-mono flex flex-row rounded-md bg-[var(--bg-container)] items-center gap-1 w-7/10">
|
||||
<input class="placeholder:text-[var(--text-tertiary)] w-full p-0 pl-2 py-1 bg-transparent"
|
||||
:type="apiKeyVisible ? 'text' : 'password'" id="provider-api-key" :value="apiKey"
|
||||
autocomplete="false" spellcheck="false"
|
||||
@input="updateApiKey(($event.target! as HTMLInputElement).value)" />
|
||||
<button @click="apiKeyVisible = !apiKeyVisible"
|
||||
class="text-sm p-2 text-[var(--color-muted)] hover:text-[var(--color-text)]">
|
||||
class="text-sm p-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
|
||||
<Icon :name="apiKeyVisible ? 'mynaui:eye' : 'mynaui:eye-slash'" class="text-4 min-h-4 min-w-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -288,16 +207,16 @@ defineEmits(['navigate']);
|
||||
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
<label class="whitespace-nowrap" for="provider-api-key">API Proxy URL</label>
|
||||
<div
|
||||
class="text-sm font-mono flex flex-row rounded-md bg-[var(--color-highlight)] items-center gap-1 w-7/10">
|
||||
<input :placeholder="providerBaseUrls[provider!.type] ?? ''" class="w-full px-2 py-1 bg-transparent"
|
||||
type="text" id="provider-proxy-url" :value="apiProxyUrl"
|
||||
<div class="text-sm font-mono flex flex-row rounded-md bg-[var(--bg-container)] items-center gap-1 w-7/10">
|
||||
<input :placeholder="providerBaseUrls[provider!.type] ?? ''"
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full px-2 py-1 bg-transparent" type="text"
|
||||
id="provider-proxy-url" :value="apiProxyUrl"
|
||||
@input="updateProxyUrl(($event.target! as HTMLInputElement).value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-center text-xs">
|
||||
<p class="text-[var(--color-muted)]">
|
||||
<p class="text-[var(--text-secondary)]">
|
||||
<Icon name="mynaui:lock" /> Your API key is encrypted using <a
|
||||
href="https://datatracker.ietf.org/doc/html/draft-ietf-avt-srtp-aes-gcm-01">AES-GCM</a> encryption.
|
||||
</p>
|
||||
@@ -307,26 +226,27 @@ defineEmits(['navigate']);
|
||||
<div class="pt-5 justify-between w-full flex flex-wrap gap-y-1 items-center">
|
||||
<h4 class="whitespace-nowrap m-0 flex gap-x-2 items-start">
|
||||
Model List
|
||||
<span class="text-sm text-[var(--color-muted)] font-normal text-xs flex items-center gap-1">
|
||||
{{ provider?.models.length }} models available <button @click="deleteModels">
|
||||
<span class="text-sm text-[var(--text-secondary)] font-normal text-xs flex items-center gap-1">
|
||||
{{ provider?.models.length }} models available <button
|
||||
class="p-0.5 hover:bg-[var(--color-hover)] rounded transition-colors duration-200"
|
||||
@click="deleteModels">
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex items-center justify-center px-2 py-1 bg-[var(--color-highlight)] text-xs rounded-md">
|
||||
<input class="p-0 bg-transparent" v-model="modelSearch" type="text"
|
||||
placeholder="Search models..." />
|
||||
<div class="flex items-center justify-center px-2 py-1 bg-[var(--bg-container)] text-xs rounded-md">
|
||||
<input class="placeholder:text-[var(--text-tertiary)] p-0 bg-transparent" v-model="modelSearch"
|
||||
type="text" placeholder="Search models..." />
|
||||
<button :class="modelSearch.length > 0 ? 'visible' : 'invisible'" @click="modelSearch = ''"
|
||||
class="right-1 hover:bg-[var(--color-highlight)] rounded p-0.5">
|
||||
<Icon name="mynaui:x" class="text-3.5 block text-[var(--color-subtle)]" />
|
||||
class="right-1 hover:bg-[var(--color-hover)] rounded transition-colors duration-200 p-0.5">
|
||||
<Icon name="mynaui:x" class="text-3.5 block text-[var(--text-secondary)]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="fetchModels"
|
||||
class="whitespace-nowrap flex bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)] text-sm rounded-md items-center px-2 py-0.5 gap-2 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="whitespace-nowrap flex bg-[var(--bg-container)] hover:bg-[var(--color-hover)] text-sm rounded-md items-center px-2 py-0.5 gap-2 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon :class="[fetchingModels ? 'animate-rotate' : '']" name="mynaui:refresh" />
|
||||
fetch models
|
||||
</button>
|
||||
@@ -335,14 +255,14 @@ defineEmits(['navigate']);
|
||||
|
||||
<div v-if="provider?.models?.length === 0" class="flex flex-row items-center justify-center gap-2 mt-2">
|
||||
<Icon name="mynaui:info-circle" class="text-4" />
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
<span class="text-sm text-[var(--text-secondary)]">
|
||||
No models found
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ClientOnly v-else>
|
||||
<div class="flex flex-col gap-1 mt-2">
|
||||
<span v-if="enabledModels.length > 0" class="text-sm text-[var(--color-muted)]">
|
||||
<span v-if="enabledModels.length > 0" class="text-sm text-[var(--text-secondary)]">
|
||||
Enabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -359,7 +279,7 @@ defineEmits(['navigate']);
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
|
||||
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--color-muted)]">
|
||||
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--text-secondary)]">
|
||||
Disabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -383,19 +303,3 @@ defineEmits(['navigate']);
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.animate-rotate {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,81 @@
|
||||
<template>
|
||||
<script setup lang="ts">
|
||||
const { settings, updateSettings } = await useUserSettings();
|
||||
const { colorScheme } = useTheme();
|
||||
|
||||
</template>
|
||||
const accents = ['violet', 'volcano', 'lime', 'sky', 'coral', 'emerald', 'amber', 'rose', 'cyan', 'indigo', 'magenta'];
|
||||
const neutrals = ['zinc', 'slate', 'obsidian'];
|
||||
|
||||
const updateAccent = (accent: string) => {
|
||||
updateSettings({ appearance: { accent } });
|
||||
};
|
||||
|
||||
const updateNeutral = (neutral: string) => {
|
||||
updateSettings({ appearance: { neutral } });
|
||||
};
|
||||
|
||||
const updateHinting = (e: Event) => {
|
||||
const hinting = parseInt((e.target as HTMLInputElement).value);
|
||||
updateSettings({ appearance: { hinting } });
|
||||
};
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 mt-4">
|
||||
<div class="flex flex-row items-center justify-between gap-2">
|
||||
<h4 class="font-medium">Theme</h4>
|
||||
<div class="flex gap-2">
|
||||
<button @click="updateSettings({ appearance: { colorScheme: 'system' } })"
|
||||
class="flex items-center gap-1 px-1 rounded-md hover:bg-[var(--color-hover)] transition-[background-color] duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
:class="colorScheme.preference.value === 'system' ? 'bg-[var(--color-hover)]' : ''">
|
||||
<Icon name="tabler:device-desktop" class="text-4" />
|
||||
<span>System</span>
|
||||
</button>
|
||||
<button @click="updateSettings({ appearance: { colorScheme: 'dark' } })"
|
||||
class="flex items-center gap-1 px-1 rounded-md hover:bg-[var(--color-hover)] transition-[background-color] duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
:class="colorScheme.preference.value === 'dark' ? 'bg-[var(--color-hover)]' : ''">
|
||||
<Icon name="mynaui:moon" class="text-4" />
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
<button @click="updateSettings({ appearance: { colorScheme: 'light' } })"
|
||||
class="flex items-center gap-1 px-1 rounded-md hover:bg-[var(--color-hover)] transition-[background-color] duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
:class="colorScheme.preference.value === 'light' ? 'bg-[var(--color-hover)]' : ''">
|
||||
<Icon name="mynaui:sun" class="text-4" />
|
||||
<span>Light</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<h4 class="text-sm font-medium">Accent Color</h4>
|
||||
<div class="grid grid-cols-5 gap-2">
|
||||
<button v-for="accent in accents" :key="accent" @click="updateAccent(accent)"
|
||||
class="h-8 rounded border-2 hover:scale-105 active:scale-95 transition-all duration-200 ease-[cubic-bezier(0.33,_1,_0.68,_1)]"
|
||||
:class="[
|
||||
settings.appearance.accent === accent ? 'dark:border-white/70 border-black/70' : 'border-transparent'
|
||||
]" :style="`background-color: var(--accent-${accent})`" :title="accent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<h4 class="text-sm font-medium">Neutral Color</h4>
|
||||
<div class="grid grid-cols-5 gap-2">
|
||||
<button v-for="neutral in neutrals" :key="neutral" @click="updateNeutral(neutral)"
|
||||
class="h-8 rounded border-2 hover:scale-105 active:scale-95 transition-all duration-200 ease-[cubic-bezier(0.33,_1,_0.68,_1)]"
|
||||
:class="[
|
||||
settings.appearance.neutral === neutral ? 'border-[var(--color-accent)]' : 'border-transparent hover:border-zinc-500'
|
||||
]" :style="`background-color: var(--palette-${neutral}-200)`" :title="neutral" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<h4 class="text-sm font-medium">Accent Hinting</h4>
|
||||
<span class="text-xs text-zinc-500">{{ settings.appearance.hinting }}%</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="100" step="1" :value="settings.appearance.hinting" @input="updateHinting"
|
||||
class="accent-[var(--color-accent)]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,14 +96,14 @@ onUnmounted(() => {
|
||||
enter-from-class="opacity-0 scale-95 translate-y-2" leave-from-class="opacity-100 scale-100 translate-y-0"
|
||||
enter-to-class="opacity-100 scale-100 translate-y-0" leave-to-class="opacity-0 scale-95 -translate-y-2">
|
||||
<div v-if="open" class="z-50 fixed top-1/2 left-1/2 -translate-x-1/2 flex items-center justify-center">
|
||||
<div class="absolute w-[85vw] max-w-6xl h-[70vh] bg-[var(--color-base)] rounded-xl shadow-2xl border border-[var(--color-highlight)]
|
||||
<div class="absolute w-[85vw] max-w-6xl h-[70vh] bg-[var(--bg-base)] rounded-2xl shadow-2xl border border-[var(--color-border)]
|
||||
overflow-hidden flex max-h-[90vh] p-2">
|
||||
<nav class="w-64 flex flex-col gap-1 mr-2 overflow-y-auto">
|
||||
<!-- If the page has a custom sidebar (for nested lists), show it; otherwise show default nav -->
|
||||
<component v-if="runtimePage?.sidebar" :is="runtimePage.sidebar" @navigate="setPage" />
|
||||
|
||||
<button v-else v-for="(config, id) in PAGES_CONFIG" :key="id" @click="setPage(id)"
|
||||
:class="[currentPage === id ? 'bg-[var(--color-highlight)]' : 'hover:bg-[var(--color-highlight)]', 'flex justify-between items-center shrink-0 px-1 rounded-lg transition-colors cursor-pointer h-9']">
|
||||
:class="[currentPage === id ? 'bg-[var(--color-hover)]' : 'hover:bg-[var(--color-hover)]', 'flex justify-between items-center shrink-0 px-1 rounded-lg transition-colors cursor-pointer h-9']">
|
||||
<div class="flex items-center gap-2 max-w-full flex-1">
|
||||
<Icon :name="config.icon" class="w-5 h-5" />
|
||||
{{ config.label }}
|
||||
@@ -114,11 +114,11 @@ onUnmounted(() => {
|
||||
<!-- DYNAMIC CONTENT -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex flex-col flex-1 p-3 bg-[var(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
class="flex flex-col flex-1 p-3 bg-[var(--bg-surface)] overflow-y-auto border rounded-lg border-[var(--color-border)]">
|
||||
<header class="flex items-center justify-between pl-2 pb-2 ">
|
||||
<h2 class="text-lg font-semibold m-0">{{ runtimePage.label }}</h2>
|
||||
<h2 class="text-lg font-semibold m-0 capitalize">{{ runtimePage.label }}</h2>
|
||||
<button
|
||||
class="hover:bg-[var(--color-highlight)] p-1.5 rounded-md transition-colors duration-200 ease-[cubic-bezier(0,0.55,0.45,1)]"
|
||||
class="hover:bg-[var(--color-hover)] p-1.5 rounded-md transition-colors duration-200 ease-[cubic-bezier(0,0.55,0.45,1)]"
|
||||
@click="close">
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
|
||||
@@ -3,4 +3,5 @@ defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
hi
|
||||
</template>
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const props = defineProps<{
|
||||
model: ModelWithProvider;
|
||||
}>();
|
||||
@@ -8,7 +6,7 @@ const props = defineProps<{
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="p-3 text-white flex max-w-full items-center justify-between gap-2 group hover:bg-[var(--color-highlight-low)] transition duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="p-3 text-white flex max-w-full items-center justify-between gap-2 group hover:bg-[var(--color-hover)] transition duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<ModelInfo :details="true" :model="model" :show-edit="true" :show-cost="true" :show-external-id="true"
|
||||
:show-release-date="true" />
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Providers } from '~/types/model';
|
||||
import { providerIcons } from '~/utils/model-mapping';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
if (providers.value === undefined) throw new Error('Providers not loaded');
|
||||
|
||||
// TODO: sometimes this code can create duplicate providers
|
||||
const { user } = useAuth();
|
||||
for (const provider of Providers) {
|
||||
if (!providers.value?.find(p => p.type === provider)) {
|
||||
@@ -20,6 +22,12 @@ for (const provider of Providers) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const provider of providers.value) {
|
||||
if (!Providers.includes(provider.type)) {
|
||||
await triplit.delete('providers', provider.id);
|
||||
}
|
||||
}
|
||||
|
||||
const toggleProvider = async (id: string) => {
|
||||
const provider = providers.value!.find(p => p.id === id);
|
||||
if (!provider) return;
|
||||
@@ -39,7 +47,7 @@ defineEmits(['navigate']);
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
Enabled <span class="text-sm bg-[var(--color-highlight)] px-2 rounded-md py-0.5 text-[var(--color-muted)]">
|
||||
Enabled <span class="text-sm bg-[var(--bg-container)] px-2 rounded-md py-0.5 text-[var(--text-secondary)]">
|
||||
{{providers?.filter(p => p.enabled).length}}
|
||||
</span>
|
||||
</h2>
|
||||
@@ -47,21 +55,25 @@ defineEmits(['navigate']);
|
||||
class="grid gap-4 grid-cols-[repeat(auto-fill,_minmax(max(240px,_calc((100%_-_16px_*_(3_-_1))_/_3)),_1fr))]">
|
||||
<button @click="$emit('navigate', 'providers', p.id)" v-for="p in providers?.filter(p => p.enabled)"
|
||||
:key="p.id"
|
||||
class="flex flex-col h-44 p-4 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg border border-[var(--color-highlight)] hover:border-[var(--color-highlight-high)]">
|
||||
class="flex flex-col h-44 p-4 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg border border-[var(--color-border)] hover:border-[var(--color-border-active)]">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<h3 class="text-md font-semibold text-start">{{ p.name }}</h3>
|
||||
<hr class="border-t border-[var(--color-highlight)]" />
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<component v-if="providerIcons[p.type]" :color="true" :is="providerIcons[p.type]"
|
||||
class="w-8 h-8 text-[var(--text-primary)]" />
|
||||
<h3 class="text-md font-semibold text-start capitalize">{{ p.name }}</h3>
|
||||
</div>
|
||||
<hr class="border-t border-[var(--color-border)]" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<!-- <input type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-transparent checked:bg-blue-600 checked:text-white checked:border-transparent focus:ring-0 border-2 border-[var(--color-highlight)]" /> -->
|
||||
class="w-4 h-4 text-blue-600 bg-transparent checked:bg-blue-600 checked:text-white checked:border-transparent focus:ring-0 border-2 border-[var(--color-border)]" /> -->
|
||||
<Slider :checked="p.enabled" @click.stop="toggleProvider(p.id)" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
Disabled <span class="text-sm bg-[var(--color-highlight)] px-2 rounded-md py-0.5 text-[var(--color-muted)]">
|
||||
Disabled <span class="text-sm bg-[var(--bg-container)] px-2 rounded-md py-0.5 text-[var(--text-secondary)]">
|
||||
{{providers?.filter(p => !p.enabled).length}}
|
||||
</span>
|
||||
</h2>
|
||||
@@ -69,14 +81,18 @@ defineEmits(['navigate']);
|
||||
class="grid gap-4 grid-cols-[repeat(auto-fill,_minmax(max(240px,_calc((100%_-_16px_*_(3_-_1))_/_3)),_1fr))]">
|
||||
<button @click="$emit('navigate', 'providers', p.id)" v-for="p in providers?.filter(p => !p.enabled)"
|
||||
:key="p.id"
|
||||
class="flex flex-col h-44 p-4 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg border border-[var(--color-highlight)] hover:border-[var(--color-highlight-high)]">
|
||||
class="flex flex-col h-44 p-4 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg border border-[var(--color-border)] hover:border-[var(--color-border-active)]">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<h3 class="text-md font-semibold text-start">{{ p.name }}</h3>
|
||||
<hr class="border-t border-[var(--color-highlight)]" />
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<component v-if="providerIcons[p.type]" :color="true" :is="providerIcons[p.type]"
|
||||
class="w-8 h-8 text-[var(--text-primary)]" />
|
||||
<h3 class="text-md font-semibold text-start capitalize">{{ p.name }}</h3>
|
||||
</div>
|
||||
<hr class="border-t border-[var(--color-border)]" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<!-- <input type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-transparent checked:bg-blue-600 checked:text-white checked:border-transparent focus:ring-0 border-2 border-[var(--color-highlight)]" /> -->
|
||||
class="w-4 h-4 text-blue-600 bg-transparent checked:bg-blue-600 checked:text-white checked:border-transparent focus:ring-0 border-2 border-[var(--color-border)]" /> -->
|
||||
<Slider :checked="p.enabled" @click.stop="toggleProvider(p.id)" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { providerIcons } from '~/utils/model-mapping';
|
||||
|
||||
const { pageParams } = useSettings();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
@@ -12,28 +14,36 @@ defineEmits(['navigate']);
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<button @click="$emit('navigate', 'general')"
|
||||
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-highlight)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:chevron-left" class="text-4" /> Back to General
|
||||
</button>
|
||||
|
||||
<button @click="$emit('navigate', 'providers')"
|
||||
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-highlight)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:envelope-open" class="text-4" /> All
|
||||
</button>
|
||||
|
||||
<div class="px-2 py-4 font-bold text-xs uppercase opacity-50">Enabled Providers</div>
|
||||
|
||||
<button v-for="p in providers?.filter(p => p.enabled)" :key="p.id" @click="$emit('navigate', 'providers', p.id)"
|
||||
:class="['flex items-center justify-between p-2 hover:bg-[var(--color-highlight)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg', p.id === pageParams[0] ? 'bg-[var(--color-highlight)]' : '']">
|
||||
<span>{{ p.name }}</span>
|
||||
:class="['capitalize flex items-center justify-between p-2 hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg', p.id === pageParams[0] ? 'bg-[var(--color-hover)]' : '']">
|
||||
<div class="flex items-center gap-2">
|
||||
<component v-if="providerIcons[p.type]" :color="true" :is="providerIcons[p.type]"
|
||||
class="w-4 h-4 text-[var(--text-primary)]" />
|
||||
<span>{{ p.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="px-2 py-4 font-bold text-xs uppercase opacity-50">Disabled Providers</div>
|
||||
|
||||
<button v-for="p in providers?.filter(p => !p.enabled)" :key="p.id"
|
||||
@click="$emit('navigate', 'providers', p.id)"
|
||||
:class="['flex items-center justify-between p-2 hover:bg-[var(--color-highlight)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg', p.id === pageParams[0] ? 'bg-[var(--color-highlight)]' : '']">
|
||||
<span>{{ p.name }}</span>
|
||||
:class="['capitalize flex items-center justify-between p-2 hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] rounded-lg', p.id === pageParams[0] ? 'bg-[var(--color-hover)]' : '']">
|
||||
<div class="flex items-center gap-2">
|
||||
<component v-if="providerIcons[p.type]" :color="true" :is="providerIcons[p.type]"
|
||||
class="w-4 h-4 text-[var(--text-primary)]" />
|
||||
<span>{{ p.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,31 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
const triplit = useTriplitClient();
|
||||
const { providers, unsubscribe: unsubscribeModels, allModels } = await useModels();
|
||||
const { settings, unsubscribe: unsubscribeSettings } = await useUserSettings();
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
|
||||
const { providers, allModels } = await useModels();
|
||||
const { settings, updateSettings } = await useUserSettings();
|
||||
|
||||
const toggle = async (key: string) => {
|
||||
console.log(key);
|
||||
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
const current = (settings.value.systemAssistants as any)[key];
|
||||
await updateSettings({
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
// @ts-ignore
|
||||
enabled: !settings.value.systemAssistants[key].enabled
|
||||
...current,
|
||||
enabled: !current.enabled
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateModel = async (key: string, model: ModelWithProvider | undefined | null) => {
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
const current = (settings.value.systemAssistants as any)[key];
|
||||
await updateSettings({
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
...current,
|
||||
modelId: model?.id ?? null
|
||||
}
|
||||
}
|
||||
@@ -40,23 +36,19 @@ const getModel = (id: string | null | undefined) => {
|
||||
|
||||
defineEmits(['navigate']);
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
unsubscribeSettings?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 flex-grow">
|
||||
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants">
|
||||
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants" :key="key">
|
||||
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center">
|
||||
<h4 class="capitalize">{{ key }}</h4>
|
||||
<Slider :id="`slider-${key}`" :checked="systemAssistant.enabled" @click="toggle(key)" />
|
||||
<Slider :id="`slider-${key}`" :checked="systemAssistant.enabled" @click="toggle(String(key))" />
|
||||
</label>
|
||||
<div class="flex-1 justify-between gap-4 items-center">
|
||||
<ModelSelector :providers="providers" :model-value="getModel(systemAssistant.modelId)"
|
||||
@update:model-value="(model) => updateModel(key, model)" />
|
||||
@update:model-value="(model) => updateModel(String(key), model)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user