feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { encryptData, decrypt, uint8ArrayToBase64, base64ToUint8Array } from '~/utils/crypto';
|
||||
import { providerBaseUrls } from '~/types/model';
|
||||
import { providerBaseUrls, SupportedModalities, 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();
|
||||
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const provider = computed(() => {
|
||||
if (pageParams.value.length === 0) return null;
|
||||
@@ -21,12 +24,22 @@ watch(provider, async () => {
|
||||
const apiKeyVisible = ref(false);
|
||||
|
||||
const apiKey = ref('');
|
||||
const apiProxyUrl = ref(provider.value!.config.apiProxyUrl ?? '');
|
||||
const apiProxyUrl = ref(provider.value?.config.apiProxyUrl ?? '');
|
||||
const modelSearch = ref('');
|
||||
|
||||
const providerApiUrl = computed(() => apiProxyUrl.value === '' ? providerBaseUrls[provider.value!.type] : apiProxyUrl.value);
|
||||
watch(pageParams, () => {
|
||||
if (pageParams.value.length === 0) return;
|
||||
apiKey.value = provider.value?.config.apiKey ?? '';
|
||||
apiProxyUrl.value = provider.value?.config.apiProxyUrl ?? '';
|
||||
modelSearch.value = '';
|
||||
})
|
||||
|
||||
const decryptApiKey = async () => {
|
||||
if (!provider.value?.config.apiKey) {
|
||||
apiKey.value = '';
|
||||
return
|
||||
};
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
JSON.parse(window.localStorage.getItem("encryptionKey")!),
|
||||
@@ -70,6 +83,8 @@ const updateApiKey = async (value: string) => {
|
||||
const updateProxyUrl = async (value: string) => {
|
||||
if (!provider.value) return;
|
||||
|
||||
apiProxyUrl.value = value;
|
||||
|
||||
await triplit.update('providers', provider.value.id, {
|
||||
config: {
|
||||
...provider.value.config,
|
||||
@@ -78,14 +93,6 @@ const updateProxyUrl = async (value: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleModel = async (id: string) => {
|
||||
if (!provider.value) return;
|
||||
|
||||
await triplit.update('models', id, {
|
||||
enabled: !provider.value!.models.find(m => m.id === id)!.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchingModels = ref(false);
|
||||
|
||||
const fetchModels = async () => {
|
||||
@@ -95,12 +102,23 @@ const fetchModels = async () => {
|
||||
|
||||
try {
|
||||
const [providerResponse, devDataResponse] = await Promise.all([
|
||||
$fetch(`${providerApiUrl.value}/models`),
|
||||
$fetch('https://models.dev/api.json')
|
||||
$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
|
||||
]);
|
||||
|
||||
const providerType = provider.value!.type;
|
||||
// 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 existingModelsMap = new Map(
|
||||
(provider.value?.models || []).map((m: any) => [m.externalId, m])
|
||||
@@ -109,11 +127,15 @@ const fetchModels = async () => {
|
||||
const toInsert: any[] = [];
|
||||
const toUpdate: { id: string, data: any }[] = [];
|
||||
|
||||
providerResponse.data.forEach((pModel: any) => {
|
||||
const slug = pModel.id.toLowerCase();
|
||||
const info = modelDetails[slug] || {};
|
||||
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$/, '');
|
||||
}
|
||||
|
||||
console.log("INFO", info);
|
||||
let info = modelDetails[slug] || {};
|
||||
|
||||
const capabilities = [];
|
||||
|
||||
@@ -125,14 +147,48 @@ const fetchModels = async () => {
|
||||
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(info.modalities?.input.filter(m => ['text', 'image'].includes(m)) || ['text']),
|
||||
outputModalities: new Set(info.modalities?.output.filter(m => ['text', 'image'].includes(m)) || ['text']),
|
||||
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);
|
||||
|
||||
if (existing) {
|
||||
@@ -143,10 +199,10 @@ const fetchModels = async () => {
|
||||
id: existing.id,
|
||||
data: {
|
||||
...existingWithoutId,
|
||||
name: existing.name || info.name || pModel.name || pModel.id,
|
||||
attributes: attributes, // Update tech specs
|
||||
releasedAt: new Date(pModel.created * 1000),
|
||||
updatedAt: new Date()
|
||||
name: existing.name || pModel.name || info.name || pModel.id,
|
||||
cost,
|
||||
attributes, // Update tech specs
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -155,22 +211,29 @@ const fetchModels = async () => {
|
||||
userId: user.value?.id,
|
||||
providerId: provider.value!.id,
|
||||
externalId: pModel.id,
|
||||
name: info.name || pModel.name || pModel.id,
|
||||
name: pModel.name || info.name || pModel.id,
|
||||
isCustom: false,
|
||||
enabled: false,
|
||||
attributes: attributes,
|
||||
releasedAt: new Date(pModel.created * 1000),
|
||||
cost,
|
||||
attributes,
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 toDelete = (provider.value?.models || []).filter((m: any) =>
|
||||
!m.isCustom && !apiModelIds.has(m.externalId)
|
||||
);
|
||||
|
||||
console.log({ toUpdate, toInsert, toDelete });
|
||||
|
||||
await Promise.all([
|
||||
...toInsert.map(item => triplit.insert('models', item)),
|
||||
...toUpdate.map(item => triplit.update('models', item.id, (m) => {
|
||||
Object.assign(m, item.data);
|
||||
}))
|
||||
...toUpdate.map(item => triplit.update('models', item.id, item.data)),
|
||||
...toDelete.map(item => triplit.delete('models', item.id))
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch models:', error);
|
||||
@@ -179,14 +242,34 @@ const fetchModels = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteModels = async () => {
|
||||
if (!provider.value) return;
|
||||
|
||||
await Promise.all(provider.value.models.map(m => triplit.delete('models', m.id)));
|
||||
};
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
<div class="flex flex-col gap-4 mt-4" v-if="provider">
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
<label class="whitespace-nowrap" for="provider-api-key">Enabled</label>
|
||||
<Slider :checked="provider!.enabled" @click.stop="toggleProvider()" />
|
||||
<Slider :checked="provider.enabled" @click.stop="toggleProvider()" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
@@ -194,11 +277,11 @@ defineEmits(['navigate']);
|
||||
<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"
|
||||
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)]">
|
||||
<Icon :name="apiKeyVisible ? 'mynaui:eye' : 'mynaui:eye-slash'" class="text-4" />
|
||||
<Icon :name="apiKeyVisible ? 'mynaui:eye' : 'mynaui:eye-slash'" class="text-4 min-h-4 min-w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,8 +290,8 @@ defineEmits(['navigate']);
|
||||
<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="apiKeyVisible ? 'text' : 'password'" id="provider-api-key" :value="apiProxyUrl"
|
||||
<input :placeholder="providerBaseUrls[provider!.type] ?? ''" class="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>
|
||||
@@ -221,17 +304,26 @@ defineEmits(['navigate']);
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="pt-5 justify-between w-full flex">
|
||||
<h4 class="whitespace-nowrap m-0">
|
||||
<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">
|
||||
{{ provider?.models.length }} models available
|
||||
<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">
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="modelSearch" type="text" class="px-2 py-1 bg-[var(--color-highlight)] text-xs"
|
||||
placeholder="Search models..." />
|
||||
<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..." />
|
||||
<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)]" />
|
||||
</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)]">
|
||||
@@ -248,70 +340,46 @@ defineEmits(['navigate']);
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-1 mt-2">
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Enabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="p-3 flex items-center justify-between"
|
||||
v-for="model in provider?.models.filter(m => m.enabled === true).filter(m => !modelSearch || m.name.toLowerCase().includes(modelSearch.toLowerCase()))"
|
||||
:key="model.id">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex items-center">
|
||||
<ModelIcon :avatar="true" variant="color" :model-id="model.externalId" size="32" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 ml-2">
|
||||
<div
|
||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] flex items-center gap-1">
|
||||
{{ model.name }}
|
||||
<span
|
||||
class="text-xs text-[var(--color-muted)] px-1 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ model.externalId }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--color-muted)]">
|
||||
Released on {{
|
||||
model.releasedAt?.toISOString().split('T')[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slider :checked="model.enabled" @click="toggleModel(model.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Disabled
|
||||
<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)]">
|
||||
Enabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="p-3 flex items-center justify-between"
|
||||
v-for="model in provider?.models.filter(m => m.enabled === false).filter(m => !modelSearch || m.name.toLowerCase().includes(modelSearch.toLowerCase()))"
|
||||
:key="model.id">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex items-center">
|
||||
<ModelIcon :avatar="true" variant="color" :model-id="model.externalId" size="32" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 ml-2">
|
||||
<div
|
||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] flex items-center gap-1">
|
||||
{{ model.name }}
|
||||
<span
|
||||
class="text-xs text-[var(--color-muted)] px-1 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ model.externalId }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--color-muted)]">
|
||||
Released on {{ model.releasedAt?.toISOString().split('T')[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slider :checked="model.enabled" @click="toggleModel(model.id)" />
|
||||
</div>
|
||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
||||
:items="enabledModels" key-field="id">
|
||||
<template v-slot="{ item: model, index, active }">
|
||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
||||
model.name,
|
||||
model.externalId,
|
||||
modelSearch
|
||||
]" :data-index="index">
|
||||
<ModelItem :model="model" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
|
||||
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--color-muted)]">
|
||||
Disabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
||||
:items="disabledModels" key-field="id">
|
||||
<template v-slot="{ item: model, index, active }">
|
||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
||||
model.name,
|
||||
model.externalId,
|
||||
model.cost,
|
||||
modelSearch
|
||||
]" :data-index="index">
|
||||
<ModelItem :model="model" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
@@ -2,13 +2,11 @@
|
||||
import GeneralSettings from './GeneralSettings.vue';
|
||||
import ProviderSettings from './ProviderSettings.vue';
|
||||
import ProviderSidebar from './ProviderSidebar.vue';
|
||||
import SystemAssistants from './SystemAssistants.vue';
|
||||
import AppearanceSettings from './AppearanceSettings.vue';
|
||||
import AIServiceProvider from './AIServiceProvider.vue';
|
||||
|
||||
const { providers } = await useModels();
|
||||
|
||||
const { currentPage, pageParams, open, setPage, close } = useSettings();
|
||||
|
||||
console.log(providers.value);
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const PAGES_CONFIG = {
|
||||
general: {
|
||||
@@ -16,14 +14,26 @@ const PAGES_CONFIG = {
|
||||
icon: 'mynaui:cog-four',
|
||||
component: GeneralSettings
|
||||
},
|
||||
appearance: {
|
||||
label: 'Appearance',
|
||||
icon: 'tabler:palette',
|
||||
component: AppearanceSettings
|
||||
},
|
||||
providers: {
|
||||
label: 'AI Providers',
|
||||
icon: 'mynaui:api',
|
||||
component: ProviderSettings,
|
||||
sidebar: ProviderSidebar
|
||||
},
|
||||
systemAssistants: {
|
||||
label: 'System Assistants',
|
||||
icon: 'mynaui:sparkles',
|
||||
component: SystemAssistants
|
||||
},
|
||||
} as const;
|
||||
|
||||
const { currentPage, pageParams, open, setPage, close } = useSettings();
|
||||
|
||||
const runtimePage = computed(() => {
|
||||
// 1. Get the base config (e.g., 'providers' or 'general')
|
||||
const config = PAGES_CONFIG[currentPage.value as keyof typeof PAGES_CONFIG] || PAGES_CONFIG.general;
|
||||
@@ -35,7 +45,6 @@ const runtimePage = computed(() => {
|
||||
if (currentPage.value === 'providers' && pageParams.value.length > 0) {
|
||||
component = AIServiceProvider;
|
||||
const providerId = pageParams.value[0];
|
||||
console.log("PROVIDERS", providers.value);
|
||||
const provider = providers.value!.find(p => p.id === providerId);
|
||||
label = provider ? provider.name : 'Unknown Provider';
|
||||
}
|
||||
@@ -72,6 +81,7 @@ onUnmounted(() => {
|
||||
if (open.value) {
|
||||
document.body.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -88,7 +98,7 @@ onUnmounted(() => {
|
||||
<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)]
|
||||
overflow-hidden flex max-h-[90vh] p-2">
|
||||
<nav class="w-64 flex flex-col gap-1 mr-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" />
|
||||
|
||||
@@ -104,7 +114,7 @@ onUnmounted(() => {
|
||||
<!-- DYNAMIC CONTENT -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="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(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
<header class="flex items-center justify-between pl-2 pb-2 ">
|
||||
<h2 class="text-lg font-semibold m-0">{{ runtimePage.label }}</h2>
|
||||
<button
|
||||
@@ -113,10 +123,7 @@ onUnmounted(() => {
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
</header>
|
||||
<!-- KeepAlive preserves state if the user clicks back/forth between tabs -->
|
||||
<KeepAlive>
|
||||
<component @navigate="setPage" :is="runtimePage.component" />
|
||||
</KeepAlive>
|
||||
<component @navigate="setPage" :is="runtimePage.component" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const props = defineProps<{
|
||||
model: ModelWithProvider;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<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)]">
|
||||
<ModelInfo :model="model" :show-edit="true" :show-cost="true" :show-external-id="true"
|
||||
:show-release-date="true" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { Providers } from '~/types/model';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
if (providers.value === undefined) throw new Error('Providers not loaded');
|
||||
|
||||
const { user } = useAuth();
|
||||
for (const provider of Providers) {
|
||||
if (!providers.value?.find(p => p.type === provider)) {
|
||||
// create a new provider
|
||||
await triplit.insert('providers', {
|
||||
name: provider,
|
||||
userId: user.value!.id,
|
||||
type: provider,
|
||||
enabled: false,
|
||||
config: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleProvider = async (id: string) => {
|
||||
const provider = providers.value!.find(p => p.id === id);
|
||||
@@ -11,6 +29,10 @@ const toggleProvider = async (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { pageParams } = useSettings();
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
console.log("PROVIDERS", providers.value);
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const triplit = useTriplitClient();
|
||||
const { providers, unsubscribe: unsubscribeModels, allModels } = await useModels();
|
||||
const { settings, unsubscribe: unsubscribeSettings } = await useUserSettings();
|
||||
|
||||
const toggle = async (key: string) => {
|
||||
console.log(key);
|
||||
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
// @ts-ignore
|
||||
enabled: !settings.value.systemAssistants[key].enabled
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateModel = async (key: string, model: ModelWithProvider | undefined | null) => {
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
modelId: model?.id ?? null
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getModel = (id: string | null | undefined) => {
|
||||
if (!id) return null;
|
||||
|
||||
return allModels.value.find(m => m.id === id);
|
||||
}
|
||||
|
||||
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">
|
||||
<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)" />
|
||||
</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)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user